Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions tv/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
Comment on lines +6 to +9

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two notes on this permission block (neither blocking):

  1. POST_NOTIFICATIONS is already contributed by a library manifestmodules/services/repositories/src/main/AndroidManifest.xml line 5 declares it, so it merges into the TV APK regardless. Harmless to repeat it explicitly, just don't count on this line as the thing that fixed anything. Separately: nothing in tv/src requests the runtime permission (no hasNotificationsPermission() / request flow), so on API 33+ the playback foreground-service notification is posted but not displayed. That doesn't stop the FGS or the session — worth knowing before claiming the notification surface works.

  2. FOREGROUND_SERVICE_DATA_SYNC — the PR calls this out as out of scope, and I agree it's pre-existing, but note the gap is slightly sharper than "TV has no foreground-service permissions": the same repositories library manifest merges in androidx.work.impl.foreground.SystemForegroundService with android:foregroundServiceType="dataSync", so the TV APK declares a dataSync FGS with no matching permission. Reachable callers are DownloadEpisodeWorker (wraps setForegroundAsync(...).get() in a try/catch → degrades safely), FixDownloadsWorker and OpmlImportTask (unguarded setForeground(...)). TV doesn't trigger any of them today, so deferring is defensible — it's just a one-line addition if you'd rather close it here.


<uses-feature
android:name="android.software.leanback"
android:required="true" />
Expand Down Expand Up @@ -57,6 +62,30 @@
android:resource="@xml/authenticator" />
</service>

<!-- Both services start disabled; PlaybackServiceToggle enables the correct one. -->
<service
android:name="au.com.shiftyjelly.pocketcasts.repositories.playback.PlaybackService"
android:exported="true"
android:enabled="false"
android:foregroundServiceType="mediaPlayback"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
<action android:name="androidx.media3.session.MediaLibraryService" />
</intent-filter>
</service>

<service
android:name="au.com.shiftyjelly.pocketcasts.repositories.playback.LegacyPlaybackService"
android:exported="true"
android:enabled="false"
android:foregroundServiceType="mediaPlayback"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>

<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package au.com.shiftyjelly.pocketcasts
import android.app.Application
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import au.com.shiftyjelly.pocketcasts.repositories.notification.NotificationHelper
import au.com.shiftyjelly.pocketcasts.repositories.playback.PlaybackManager
import au.com.shiftyjelly.pocketcasts.repositories.playback.PlaybackServiceToggle
import au.com.shiftyjelly.pocketcasts.utils.TimberDebugTree
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject
Expand All @@ -22,13 +24,17 @@ class TvApplication :

@Inject lateinit var playbackManager: PlaybackManager

@Inject lateinit var notificationHelper: NotificationHelper

private val applicationScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
Timber.plant(TimberDebugTree())
}
notificationHelper.setupNotificationChannels()
PlaybackServiceToggle.ensureCorrectServiceEnabled(this)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FeatureFlag is never initialised on TV, so this always reads the compile-time default.

PlaybackServiceToggle.ensureCorrectServiceEnabled()FeatureFlag.isEnabled(Feature.MEDIA3_SESSION), and FeatureFlag.isEnabled falls back to feature.defaultValue when no providers are registered:

findProviderForFeature<FeatureProvider>(feature)?.isEnabled(feature) ?: feature.defaultValue

FeatureFlag.initialize(...) is only called from AppLifecycleObserver.setup() (line 115) and AutomotiveApplication. TvApplication calls neither — there is no FeatureFlag/FeatureProvider reference anywhere under tv/src. Feature.MEDIA3_SESSION.defaultValue is isDebugOrPrototypeBuild, so:

  • debug / prototype TV builds → media3 PlaybackService enabled (this is what the emulator run in the description verified),
  • release TV buildsLegacyPlaybackService enabled, and the Firebase remote flag and the dev toggle have no effect on TV at all.

The good news is it's self-consistent: MediaSessionManager.useMedia3Session reads the same uninitialised flag, so the enabled component and the session type can't disagree. But the release path (legacy MediaSessionCompat on TV) is the one that ships and is the one that hasn't been exercised.

Two suggestions:

  1. Either initialise the flags on TV the way wear does — appLifecycleObserver.setup() immediately before this call (wear: PocketCastsWearApplication.setupApp() lines 109–111) — or state in the PR that TV deliberately ships the compile-time default, and verify a prototype build too, since the legacy service is what release users get.
  2. Whichever you pick, keep/insert FeatureFlag.initialize before ensureCorrectServiceEnabled. If it is ever added after this line, the toggle would enable a component chosen from the default while MediaSessionManager.useMedia3Session (lazy, first read in startObserving()) resolves from the initialised providers — a mismatch for that launch.

Fix this →

// setup() subscribes the Up Next queue's sync pipeline itself, so there must be no
// separate UpNextQueue.setupBlocking() call on TV.
applicationScope.launch {
Expand Down