Add resume/continue watching items to Android TV watch next row (#372)

## Details

Adds resume and continue watching items for the logged in user to the
Android TV OS level watch next "channel"
(https://developer.android.com/training/tv/discovery/watch-next-add-programs).

## Issues
Closes #206
Related to #581
This commit is contained in:
Ray 2025-12-27 15:40:45 -05:00 committed by GitHub
parent 639ce0de71
commit 9ae4965c2a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 645 additions and 189 deletions

View file

@ -1,5 +1,6 @@
package com.github.damontecres.wholphin
import android.content.Intent
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.viewModels
@ -47,10 +48,13 @@ import com.github.damontecres.wholphin.services.SetupDestination
import com.github.damontecres.wholphin.services.SetupNavigationManager
import com.github.damontecres.wholphin.services.UpdateChecker
import com.github.damontecres.wholphin.services.hilt.AuthOkHttpClient
import com.github.damontecres.wholphin.services.tvprovider.TvProviderSchedulerService
import com.github.damontecres.wholphin.ui.CoilConfig
import com.github.damontecres.wholphin.ui.LocalImageUrlService
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.nav.ApplicationContent
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.setup.SwitchServerContent
import com.github.damontecres.wholphin.ui.setup.SwitchUserContent
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
@ -60,6 +64,7 @@ import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.firstOrNull
import okhttp3.OkHttpClient
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import timber.log.Timber
import javax.inject.Inject
@ -96,6 +101,9 @@ class MainActivity : AppCompatActivity() {
@Inject
lateinit var refreshRateService: RefreshRateService
@Inject
lateinit var tvProviderSchedulerService: TvProviderSchedulerService
private var signInAuto = true
@OptIn(ExperimentalTvMaterial3Api::class)
@ -115,6 +123,7 @@ class MainActivity : AppCompatActivity() {
}
}
viewModel.appStart()
val requestedDestination = this.intent?.let(::extractDestination)
setContent {
val appPreferences by userPreferencesDataStore.data.collectAsState(null)
appPreferences?.let { appPreferences ->
@ -225,6 +234,9 @@ class MainActivity : AppCompatActivity() {
ApplicationContent(
user = current.user,
server = current.server,
startDestination =
requestedDestination
?: Destination.Home(),
navigationManager = navigationManager,
preferences = preferences,
modifier = Modifier.fillMaxSize(),
@ -278,6 +290,7 @@ class MainActivity : AppCompatActivity() {
override fun onStop() {
super.onStop()
Timber.d("onStop")
tvProviderSchedulerService.launchOneTimeRefresh()
}
override fun onPause() {
@ -304,6 +317,54 @@ class MainActivity : AppCompatActivity() {
super.onDestroy()
Timber.d("onDestroy")
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
Timber.v("onNewIntent")
extractDestination(intent)?.let {
navigationManager.replace(it)
}
}
private fun extractDestination(intent: Intent): Destination? =
intent.let {
val itemId =
it.getStringExtra(INTENT_ITEM_ID)?.toUUIDOrNull()
val type =
it.getStringExtra(INTENT_ITEM_TYPE)?.let(BaseItemKind::fromNameOrNull)
if (itemId != null && type != null) {
val seriesId = it.getStringExtra(INTENT_SERIES_ID)?.toUUIDOrNull()
val seasonId = it.getStringExtra(INTENT_SEASON_ID)?.toUUIDOrNull()
val episodeNumber = it.getIntExtra(INTENT_EPISODE_NUMBER, -1)
val seasonNumber = it.getIntExtra(INTENT_SEASON_NUMBER, -1)
if (seriesId != null && seasonId != null && episodeNumber >= 0 && seasonNumber >= 0) {
Destination.SeriesOverview(
itemId = seriesId,
type = BaseItemKind.SERIES,
seasonEpisode =
SeasonEpisodeIds(
seasonId = seasonId,
seasonNumber = seasonNumber,
episodeId = itemId,
episodeNumber = episodeNumber,
),
)
} else {
Destination.MediaItem(itemId, type)
}
} else {
null
}
}
companion object {
const val INTENT_ITEM_ID = "itemId"
const val INTENT_ITEM_TYPE = "itemType"
const val INTENT_SERIES_ID = "seriesId"
const val INTENT_EPISODE_NUMBER = "epNum"
const val INTENT_SEASON_NUMBER = "seaNum"
const val INTENT_SEASON_ID = "seaId"
}
}
@HiltViewModel