Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ class MediaSessionManager(
FeatureFlag.isEnabled(Feature.MEDIA3_SESSION)
}
private val isAutomotive = Util.isAutomotive(context)
private val isTv = Util.isTv(context)

// Automotive always needs a MediaLibrarySession for the service contract (it's the
// app entry point on AAOS), even when the Media3 flag is OFF. The internal behavior
Expand All @@ -134,7 +135,9 @@ class MediaSessionManager(
val mediaSession: MediaSessionCompat? by lazy {
if (!useMedia3Session && !isAutomotive) {
MediaSessionCompat(context, "PocketCastsMediaSession").also { session ->
session.setSessionActivity(context.getLaunchActivityPendingIntent())
if (!isTv) {
session.setSessionActivity(context.getLaunchActivityPendingIntent())
}
session.setRatingType(RatingCompat.RATING_HEART)
session.setExtras(
Bundle().apply {
Expand Down Expand Up @@ -382,7 +385,7 @@ class MediaSessionManager(
},
)
.apply {
if (!Util.isAutomotive(context)) {
if (!isAutomotive && !isTv) {
setSessionActivity(context.getLaunchActivityPendingIntent())
}
Comment thread
sztomek marked this conversation as resolved.
}
Expand Down
32 changes: 31 additions & 1 deletion 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 thread
sztomek marked this conversation as resolved.

<uses-feature
android:name="android.software.leanback"
android:required="true" />
Expand Down Expand Up @@ -38,7 +43,8 @@

<activity
android:name=".TvActivity"
android:exported="true">
android:exported="true"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
Expand All @@ -57,6 +63,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
25 changes: 25 additions & 0 deletions tv/src/main/java/au/com/shiftyjelly/pocketcasts/TvActivity.kt
Original file line number Diff line number Diff line change
@@ -1,20 +1,45 @@
package au.com.shiftyjelly.pocketcasts

import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import au.com.shiftyjelly.pocketcasts.onboarding.TvOnboardingNavHost
import au.com.shiftyjelly.pocketcasts.theme.TvTheme
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

@AndroidEntryPoint
class TvActivity : ComponentActivity() {

@Inject
lateinit var launchRequests: TvLaunchRequests

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
handleLaunchIntent(intent)
}
setContent {
TvTheme {
TvOnboardingNavHost()
}
}
}

override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
handleLaunchIntent(intent)
}

private fun handleLaunchIntent(intent: Intent?) {
val isMediaCardLaunch = intent != null &&
intent.action == Intent.ACTION_MAIN &&
intent.hasCategory(Intent.CATEGORY_LAUNCHER) &&
!intent.hasCategory(Intent.CATEGORY_LEANBACK_LAUNCHER)
if (isMediaCardLaunch) {
launchRequests.requestOpenNowPlaying()
}
}
Comment thread
sztomek marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@ 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 au.com.shiftyjelly.pocketcasts.utils.featureflag.FeatureFlag
import au.com.shiftyjelly.pocketcasts.utils.featureflag.providers.DefaultReleaseFeatureProvider
import au.com.shiftyjelly.pocketcasts.utils.featureflag.providers.FirebaseRemoteFeatureProvider
import au.com.shiftyjelly.pocketcasts.utils.featureflag.providers.PreferencesFeatureProvider
import au.com.shiftyjelly.pocketcasts.utils.log.RxJavaUncaughtExceptionHandling
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject
Expand All @@ -23,6 +29,14 @@ class TvApplication :

@Inject lateinit var playbackManager: PlaybackManager

@Inject lateinit var notificationHelper: NotificationHelper

@Inject lateinit var defaultReleaseFeatureProvider: DefaultReleaseFeatureProvider

@Inject lateinit var firebaseRemoteFeatureProvider: FirebaseRemoteFeatureProvider

@Inject lateinit var preferencesFeatureProvider: PreferencesFeatureProvider

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

override fun onCreate() {
Expand All @@ -31,13 +45,28 @@ class TvApplication :
Timber.plant(TimberDebugTree())
}
RxJavaUncaughtExceptionHandling.setUp()
setupFeatureFlags()
notificationHelper.setupNotificationChannels()
PlaybackServiceToggle.ensureCorrectServiceEnabled(this)
Comment thread
sztomek marked this conversation as resolved.
// setup() subscribes the Up Next queue's sync pipeline itself, so there must be no
// separate UpNextQueue.setupBlocking() call on TV.
applicationScope.launch {
playbackManager.setup()
}
}

private fun setupFeatureFlags() {
val providers = if (BuildConfig.DEBUG || BuildConfig.IS_PROTOTYPE) {
Comment thread
sztomek marked this conversation as resolved.
listOf(preferencesFeatureProvider)
} else {
listOf(
firebaseRemoteFeatureProvider,
defaultReleaseFeatureProvider,
)
}
FeatureFlag.initialize(providers)
}

override val workManagerConfiguration: Configuration
get() = Configuration.Builder()
.setWorkerFactory(workerFactory)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package au.com.shiftyjelly.pocketcasts

import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.receiveAsFlow

@Singleton
class TvLaunchRequests @Inject constructor() {
private val openNowPlayingChannel = Channel<Unit>(Channel.CONFLATED)

val openNowPlaying: Flow<Unit> = openNowPlayingChannel.receiveAsFlow()

fun requestOpenNowPlaying() {
openNowPlayingChannel.trySend(Unit)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
Expand Down Expand Up @@ -61,6 +62,11 @@ fun TvScaffold(
isNowPlayingOpenRequested = true
}
}
LaunchedEffect(viewModel) {
viewModel.openNowPlayingRequests.collect {
openNowPlaying()
}
}

CompositionLocalProvider(
LocalTvTopBarVisibility provides topBarVisibility,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package au.com.shiftyjelly.pocketcasts.home

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import au.com.shiftyjelly.pocketcasts.TvLaunchRequests
import au.com.shiftyjelly.pocketcasts.auth.TvSignOutManager
import au.com.shiftyjelly.pocketcasts.repositories.playback.UpNextQueue
import au.com.shiftyjelly.pocketcasts.repositories.sync.SyncManager
Expand All @@ -20,8 +21,11 @@ import kotlinx.coroutines.rx2.asFlow
class TvScaffoldViewModel @Inject constructor(
private val syncManager: SyncManager,
private val signOutManager: TvSignOutManager,
launchRequests: TvLaunchRequests,
Comment thread
sztomek marked this conversation as resolved.
upNextQueue: UpNextQueue,
) : ViewModel() {
val openNowPlayingRequests = launchRequests.openNowPlaying

private val selectedTab = MutableStateFlow<TvTab>(TvTab.Home)

private val hasCurrentEpisode = upNextQueue.changesObservable.asFlow()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,10 @@ class TvScaffoldViewModelTest {
queue = emptyList(),
)

private val launchRequests = TvLaunchRequests()

private val viewModel by lazy {
TvScaffoldViewModel(syncManager, signOutManager, upNextQueue)
TvScaffoldViewModel(syncManager, signOutManager, launchRequests, upNextQueue)
}

@Test
Expand Down Expand Up @@ -216,6 +218,14 @@ class TvScaffoldViewModelTest {
}
}

@Test
fun `openNowPlayingRequests emits when a launch open request is made`() = runTest {
viewModel.openNowPlayingRequests.test {
launchRequests.requestOpenNowPlaying()
awaitItem()
}
}

@Test
fun `signOut delegates to the sign out manager`() = runTest {
viewModel.signOut()
Expand Down