Share what's playing in any Android music app

A local Expo module listens to system media sessions (the only way
Android exposes other apps' playback, gated by notification access; we
never read notifications). It filters to known music apps, debounces and
deduplicates changes, sends a heartbeat while playing, and delivers
reports through WorkManager so they survive the app being closed.

- One-screen onboarding that doubles as Google Play's prominent
  disclosure, opening straight to the app's toggle in Android settings
- Only media notifications are pre-ticked on that settings screen
- Device registration is single-flight (several screens react to the
  same permission change) and self-heals if the server revokes a token
- Sign-out revokes the phone's token; resuming sharing re-sends the
  current track immediately
- Pause controls in Settings and on the feed
- Fix invisible tab icons and labels on Android
- Keep Metro out of build output so it doesn't exhaust file watchers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
This commit is contained in:
2026-10-03 17:37:30 -04:00
co-authored by Claude Opus 5.5
parent 23faffc251
commit 15ab815df6
26 changed files with 865 additions and 8 deletions
@@ -0,0 +1,14 @@
export type NativePlayback = {
app: string;
appName: string | null;
state: 'playing' | 'paused' | 'stopped';
title: string | null;
artist: string | null;
album: string | null;
durationMs: number | null;
};
export type NowPlayingReaderModuleEvents = {
onPlaybackChange: (event: { playback: NativePlayback | null }) => void;
onStatusChange: (event: { status: 'unauthorized' }) => void;
};
@@ -0,0 +1,19 @@
import { NativeModule, requireOptionalNativeModule } from 'expo';
import type { NativePlayback, NowPlayingReaderModuleEvents } from './NowPlayingReader.types';
declare class NowPlayingReaderModule extends NativeModule<NowPlayingReaderModuleEvents> {
isPermissionGranted(): boolean;
openPermissionSettings(): void;
configure(apiUrl: string, deviceToken: string, allowedPackages: string[]): void;
clear(): void;
resend(): void;
needsReregistration(): boolean;
getCurrent(): NativePlayback | null;
}
/**
* Android only. null on iOS, web, and in Expo Go (no custom native code there),
* so callers must feature-check.
*/
export default requireOptionalNativeModule<NowPlayingReaderModule>('NowPlayingReader');