diff --git a/app/src/main/java/org/jellyfin/androidtv/integration/LeanbackChannelWorker.kt b/app/src/main/java/org/jellyfin/androidtv/integration/LeanbackChannelWorker.kt index a9993e55d..aaa0c448a 100644 --- a/app/src/main/java/org/jellyfin/androidtv/integration/LeanbackChannelWorker.kt +++ b/app/src/main/java/org/jellyfin/androidtv/integration/LeanbackChannelWorker.kt @@ -5,6 +5,7 @@ import android.content.ContentUris import android.content.ContentValues import android.content.Context import android.content.Intent +import android.content.pm.PackageManager import android.net.Uri import androidx.core.content.edit import androidx.core.content.res.ResourcesCompat @@ -72,6 +73,14 @@ class LeanbackChannelWorker( ) : CoroutineWorker(context, workerParams), KoinComponent { companion object { private const val PERIODIC_UPDATE_REQUEST_NAME = "LeanbackChannelPeriodicUpdateRequest" + private const val CHANNEL_STORE_NAME = "leanback_channels" + + /** + * Check if the app can use Android TV home screen channels on this device. + */ + fun isAvailable(context: Context) = AndroidVersion.isAtLeastO && + context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK) && + context.packageManager.resolveContentProvider(TvContractCompat.AUTHORITY, 0) != null suspend fun enqueue(workManager: WorkManager) { workManager.enqueueUniquePeriodicWork( @@ -89,21 +98,17 @@ class LeanbackChannelWorker( private val userViewsRepository by inject() private val imageHelper by inject() - /** - * Check if the app can use Leanback features and is API level 26 or higher. - */ - private val isSupported = AndroidVersion.isAtLeastO && - // Check for leanback support - context.packageManager.hasSystemFeature("android.software.leanback") - // Check for "android.media.tv" provider to workaround a false-positive in the previous check - && context.packageManager.resolveContentProvider(TvContractCompat.AUTHORITY, 0) != null - /** * Update all channels for the currently authenticated user. */ override suspend fun doWork(): Result = when { - // Fail when not supported - !isSupported -> Result.failure() + // Succeed immediatly when not available + !isAvailable(context) -> Result.success() + // Clear all integration data when disabled + !userPreferences[UserPreferences.tvProviderEnabled] -> { + clearChannels() + Result.success() + } // Retry later if no authenticated user is found !api.isUsable -> Result.retry() else -> try { @@ -194,13 +199,28 @@ class LeanbackChannelWorker( } } + /** + * Remove all channels and programs managed by the Android TV home screen integration. + */ + private fun clearChannels() { + context.contentResolver.delete(TvContractCompat.PreviewPrograms.CONTENT_URI, null, null) + context.contentResolver.delete(WatchNextPrograms.CONTENT_URI, null, null) + + val store = context.getSharedPreferences(CHANNEL_STORE_NAME, Context.MODE_PRIVATE) + store.all.values + .filterIsInstance() + .map(Uri::parse) + .forEach { uri -> context.contentResolver.delete(uri, null, null) } + store.edit { clear() } + } + /** * Get the uri for a channel or create it if it doesn't exist. Uses the [settings] parameter to * update or create the channel. The [name] parameter is used to store the id and should be * unique. */ private fun getChannelUri(name: String, settings: Channel, default: Boolean = false): Uri? { - val store = context.getSharedPreferences("leanback_channels", Context.MODE_PRIVATE) + val store = context.getSharedPreferences(CHANNEL_STORE_NAME, Context.MODE_PRIVATE) var uri: Uri? = null // Try and re-use our existing channel definition diff --git a/app/src/main/java/org/jellyfin/androidtv/preference/UserPreferences.kt b/app/src/main/java/org/jellyfin/androidtv/preference/UserPreferences.kt index cf99809c8..4ef6bd124 100644 --- a/app/src/main/java/org/jellyfin/androidtv/preference/UserPreferences.kt +++ b/app/src/main/java/org/jellyfin/androidtv/preference/UserPreferences.kt @@ -312,6 +312,11 @@ class UserPreferences(context: Context) : SharedPreferenceStore( * Stores the interval for the photo player. */ var photoPlayerPresentationDelay = longPreference("photo_player_presentation_delay", 8000) + + /** + * Integrate with androidx.tvprovider for the Android TV home screen. + */ + var tvProviderEnabled = booleanPreference("tv_provider_enabled", true) } init { diff --git a/app/src/main/java/org/jellyfin/androidtv/ui/settings/screen/customization/SettingsCustomizationScreen.kt b/app/src/main/java/org/jellyfin/androidtv/ui/settings/screen/customization/SettingsCustomizationScreen.kt index 2f463a64a..792705f91 100644 --- a/app/src/main/java/org/jellyfin/androidtv/ui/settings/screen/customization/SettingsCustomizationScreen.kt +++ b/app/src/main/java/org/jellyfin/androidtv/ui/settings/screen/customization/SettingsCustomizationScreen.kt @@ -2,11 +2,16 @@ package org.jellyfin.androidtv.ui.settings.screen.customization import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager import org.jellyfin.androidtv.R +import org.jellyfin.androidtv.integration.LeanbackChannelWorker import org.jellyfin.androidtv.preference.UserPreferences import org.jellyfin.androidtv.ui.base.Icon import org.jellyfin.androidtv.ui.base.Text @@ -22,8 +27,11 @@ import org.koin.compose.koinInject @Composable fun SettingsCustomizationScreen() { + val context = LocalContext.current val router = LocalRouter.current val userPreferences = koinInject() + val workManager = koinInject() + val leanbackChannelWorkerAvailable = remember(context) { LeanbackChannelWorker.isAvailable(context) } SettingsColumn { item { @@ -89,6 +97,21 @@ fun SettingsCustomizationScreen() { ) } + if (leanbackChannelWorkerAvailable) item { + var tvProviderEnabled by rememberPreference(userPreferences, UserPreferences.tvProviderEnabled) + + ListButton( + headingContent = { Text(stringResource(R.string.tv_provider_enabled)) }, + captionContent = { Text(stringResource(R.string.tv_provider_enabled_description)) }, + trailingContent = { Checkbox(checked = tvProviderEnabled) }, + onClick = { + tvProviderEnabled = !tvProviderEnabled + workManager.enqueue(OneTimeWorkRequestBuilder().build()) + }, + modifier = Modifier.focusKey("leanback_channels_enabled") + ) + } + item { ListSection(headingContent = { Text(stringResource(R.string.pref_browsing)) }) } item { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0ae69c0c3..8f2fa96d7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -433,6 +433,8 @@ Home section %1$d Next up date cutoff Disabled + Integrate with Android TV home + Show Jellyfin channels and recommendations on the Android TV home screen Search Jellyfin Media Enable background