From 364b382088a3ec88a327eb54a4e979c44b449817 Mon Sep 17 00:00:00 2001 From: Afzal Najam Date: Sat, 10 Sep 2022 17:59:17 -0400 Subject: [PATCH] Fix AppScreen stutter Load app banner using a custom Coil fetcher --- .../besttvlauncher/BestTvApplication.kt | 4 +- .../besttvlauncher/data/AppInfoRepository.kt | 2 +- .../besttvlauncher/data/models/AppInfo.kt | 3 +- .../besttvlauncher/data/models/Program.kt | 2 +- .../besttvlauncher/image/AppBannerFetcher.kt | 36 +++++++++++ .../image/AppInfoImageLoaderFactory.kt | 18 ++++++ .../besttvlauncher/ui/apps/AppsScreen.kt | 63 +++++++++---------- .../ui/channels/ChannelsScreen.kt | 2 +- .../besttvlauncher/ui/components/TitleBar.kt | 11 ++-- .../besttvlauncher/ui/home/HomeScreen.kt | 4 +- .../ui/itemdetails/ItemDetailsScreen.kt | 2 +- .../afzaln/besttvlauncher/ui/theme/Theme.kt | 15 ++--- 12 files changed, 107 insertions(+), 55 deletions(-) create mode 100644 tv/src/main/java/com/afzaln/besttvlauncher/image/AppBannerFetcher.kt create mode 100644 tv/src/main/java/com/afzaln/besttvlauncher/image/AppInfoImageLoaderFactory.kt diff --git a/tv/src/main/java/com/afzaln/besttvlauncher/BestTvApplication.kt b/tv/src/main/java/com/afzaln/besttvlauncher/BestTvApplication.kt index 3ccf408..2a79623 100644 --- a/tv/src/main/java/com/afzaln/besttvlauncher/BestTvApplication.kt +++ b/tv/src/main/java/com/afzaln/besttvlauncher/BestTvApplication.kt @@ -2,6 +2,7 @@ package com.afzaln.besttvlauncher import android.app.Application import com.afzaln.besttvlauncher.core.Locator +import com.afzaln.besttvlauncher.image.AppInfoImageLoaderFactory import logcat.AndroidLogcatLogger import logcat.LogPriority import logcat.logcat @@ -12,7 +13,8 @@ class BestTvApplication: Application() { super.onCreate() Locator.init(this) + AppInfoImageLoaderFactory(this).init() AndroidLogcatLogger.installOnDebuggableApp(this, minPriority = LogPriority.DEBUG) logcat { "Initialized locator" } } -} \ No newline at end of file +} diff --git a/tv/src/main/java/com/afzaln/besttvlauncher/data/AppInfoRepository.kt b/tv/src/main/java/com/afzaln/besttvlauncher/data/AppInfoRepository.kt index ab11391..ce00a21 100644 --- a/tv/src/main/java/com/afzaln/besttvlauncher/data/AppInfoRepository.kt +++ b/tv/src/main/java/com/afzaln/besttvlauncher/data/AppInfoRepository.kt @@ -40,7 +40,7 @@ class AppInfoRepository(private val context: Context) { AppInfo( app.loadLabel(packageManager).toString(), app.activityInfo.packageName, - app.activityInfo.loadBanner(packageManager) + app.activityInfo.name, ) }.toImmutableList() } diff --git a/tv/src/main/java/com/afzaln/besttvlauncher/data/models/AppInfo.kt b/tv/src/main/java/com/afzaln/besttvlauncher/data/models/AppInfo.kt index 8c44d6d..1528950 100644 --- a/tv/src/main/java/com/afzaln/besttvlauncher/data/models/AppInfo.kt +++ b/tv/src/main/java/com/afzaln/besttvlauncher/data/models/AppInfo.kt @@ -2,12 +2,11 @@ package com.afzaln.besttvlauncher.data.models import android.content.Context import android.content.Intent -import android.graphics.drawable.Drawable data class AppInfo( val label: String, val packageName: String, - val banner: Drawable + val activityName: String, ) fun AppInfo.getLaunchIntent(context: Context): Intent? { diff --git a/tv/src/main/java/com/afzaln/besttvlauncher/data/models/Program.kt b/tv/src/main/java/com/afzaln/besttvlauncher/data/models/Program.kt index 6df5c38..0f88abb 100644 --- a/tv/src/main/java/com/afzaln/besttvlauncher/data/models/Program.kt +++ b/tv/src/main/java/com/afzaln/besttvlauncher/data/models/Program.kt @@ -6,7 +6,7 @@ import androidx.tvprovider.media.tv.TvContractCompat data class Program( val id: Long, - val title: String, + val title: String?, val description: String, val genre: String?, val releaseDate: String?, diff --git a/tv/src/main/java/com/afzaln/besttvlauncher/image/AppBannerFetcher.kt b/tv/src/main/java/com/afzaln/besttvlauncher/image/AppBannerFetcher.kt new file mode 100644 index 0000000..3dd7c99 --- /dev/null +++ b/tv/src/main/java/com/afzaln/besttvlauncher/image/AppBannerFetcher.kt @@ -0,0 +1,36 @@ +package com.afzaln.besttvlauncher.image + +import android.content.ComponentName +import android.content.Context +import android.net.Uri +import coil.ImageLoader +import coil.decode.DataSource +import coil.fetch.DrawableResult +import coil.fetch.FetchResult +import coil.fetch.Fetcher +import coil.request.Options +import com.afzaln.besttvlauncher.data.models.AppInfo + +private const val SCHEME_APPINFO = "appinfo" + +class AppBannerFetcher(val context: Context, val data: Uri) : Fetcher { + override suspend fun fetch(): FetchResult? { + if (data.scheme != SCHEME_APPINFO) return null + + val packageName = data.host ?: return null + val activityName = data.lastPathSegment ?: return null + val packageManager = context.packageManager + + val drawable = packageManager.getActivityBanner(ComponentName.createRelative(packageName, activityName)) + drawable ?: return null + + return DrawableResult(drawable, false, DataSource.DISK) + } + + class Factory(private val context: Context) : Fetcher.Factory { + override fun create(data: Uri, options: Options, imageLoader: ImageLoader): Fetcher = + AppBannerFetcher(context, data) + } +} + +fun AppInfo.toPackageUri() = "$SCHEME_APPINFO://$packageName/$activityName" diff --git a/tv/src/main/java/com/afzaln/besttvlauncher/image/AppInfoImageLoaderFactory.kt b/tv/src/main/java/com/afzaln/besttvlauncher/image/AppInfoImageLoaderFactory.kt new file mode 100644 index 0000000..cf8791a --- /dev/null +++ b/tv/src/main/java/com/afzaln/besttvlauncher/image/AppInfoImageLoaderFactory.kt @@ -0,0 +1,18 @@ +package com.afzaln.besttvlauncher.image + +import android.content.Context +import coil.Coil +import coil.ImageLoader +import coil.ImageLoaderFactory + +class AppInfoImageLoaderFactory(val context: Context) : ImageLoaderFactory { + fun init() { + Coil.setImageLoader(this) + } + + override fun newImageLoader(): ImageLoader = ImageLoader.Builder(context) + .components { + add(AppBannerFetcher.Factory(context)) + } + .build() +} diff --git a/tv/src/main/java/com/afzaln/besttvlauncher/ui/apps/AppsScreen.kt b/tv/src/main/java/com/afzaln/besttvlauncher/ui/apps/AppsScreen.kt index 02beb82..1e3aa3e 100644 --- a/tv/src/main/java/com/afzaln/besttvlauncher/ui/apps/AppsScreen.kt +++ b/tv/src/main/java/com/afzaln/besttvlauncher/ui/apps/AppsScreen.kt @@ -1,31 +1,26 @@ package com.afzaln.besttvlauncher.ui.apps -import android.graphics.BitmapFactory -import android.graphics.drawable.BitmapDrawable -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.relocation.BringIntoViewRequester -import androidx.compose.foundation.relocation.bringIntoViewRequester +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.foundation.layout.requiredWidth import androidx.compose.material.Card import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.core.graphics.drawable.toBitmap import androidx.tv.foundation.lazy.grid.* -import com.afzaln.besttvlauncher.R +import coil.compose.AsyncImage +import coil.request.ImageRequest import com.afzaln.besttvlauncher.data.models.AppInfo import com.afzaln.besttvlauncher.data.models.getLaunchIntent +import com.afzaln.besttvlauncher.image.toPackageUri import com.afzaln.besttvlauncher.ui.theme.AppTheme import com.afzaln.besttvlauncher.utils.dpadFocusable import kotlinx.collections.immutable.ImmutableList @@ -40,11 +35,9 @@ fun AppsScreen(state: HomeViewModel.State) { } } -@OptIn(ExperimentalFoundationApi::class) @Composable fun AppList(appList: ImmutableList) { val gridState = rememberTvLazyGridState() - val relocationRequester = remember { BringIntoViewRequester() } TvLazyVerticalGrid( modifier = Modifier.background(MaterialTheme.colorScheme.background), @@ -60,7 +53,6 @@ fun AppList(appList: ImmutableList) { span = { TvGridItemSpan(1) }) { appInfo -> AppCard( appInfo, - modifier = Modifier.bringIntoViewRequester(relocationRequester), onFocus = {} ) } @@ -98,10 +90,10 @@ fun AppCard( horizontalAlignment = Alignment.CenterHorizontally ) { Card(shape = AppTheme.cardShape) { - Image( - bitmap = appInfo.banner.toBitmap().asImageBitmap(), - contentScale = ContentScale.FillWidth, - modifier = Modifier.fillMaxSize(), + AsyncImage( + model = ImageRequest.Builder(context) + .data(appInfo.toPackageUri()) + .build(), contentDescription = "Icon for ${appInfo.label}" ) } @@ -113,21 +105,22 @@ fun AppCard( fun DefaultAppCard() { AppTheme { val resources = LocalContext.current.resources - AppCard( - AppInfo( - label = "App name", - packageName = "com.afzaln.example", - banner = BitmapDrawable( - resources, - BitmapFactory.decodeResource( - resources, - R.drawable.app_icon_your_company - ) - ) - ), - onFocus = { - - } - ) + // TODO provide a slot API for the image +// AppCard( +// AppInfo( +// label = "App name", +// packageName = "com.afzaln.example", +// activityName = BitmapDrawable( +// resources, +// BitmapFactory.decodeResource( +// resources, +// R.drawable.app_icon_your_company +// ) +// ) +// ), +// onFocus = { +// +// } +// ) } } diff --git a/tv/src/main/java/com/afzaln/besttvlauncher/ui/channels/ChannelsScreen.kt b/tv/src/main/java/com/afzaln/besttvlauncher/ui/channels/ChannelsScreen.kt index c7dbe9a..98515fc 100644 --- a/tv/src/main/java/com/afzaln/besttvlauncher/ui/channels/ChannelsScreen.kt +++ b/tv/src/main/java/com/afzaln/besttvlauncher/ui/channels/ChannelsScreen.kt @@ -137,7 +137,7 @@ fun ProgramInChannelRow( val context = LocalContext.current CardRow( - title = channel.displayName.toString(), + title = channel.displayName, programs = programs, onClick = { programId -> onProgramClicked(channel.id, programId) diff --git a/tv/src/main/java/com/afzaln/besttvlauncher/ui/components/TitleBar.kt b/tv/src/main/java/com/afzaln/besttvlauncher/ui/components/TitleBar.kt index 0aed95a..9abeb40 100644 --- a/tv/src/main/java/com/afzaln/besttvlauncher/ui/components/TitleBar.kt +++ b/tv/src/main/java/com/afzaln/besttvlauncher/ui/components/TitleBar.kt @@ -14,6 +14,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.TabRow import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier @@ -64,6 +65,12 @@ fun TabItem( val isItemFocused by interactionSource.collectIsFocusedAsState() val isFocusedOrSelected = isItemFocused || selected + LaunchedEffect(isItemFocused) { + if (isItemFocused) { + onTabSelected() + } + } + val animatedBackground by animateColorAsState( animationSpec = tween(500), targetValue = if (isFocusedOrSelected) { @@ -98,10 +105,6 @@ fun TabItem( ) } ) - - if (isItemFocused) { - onTabSelected() - } } @Preview(showBackground = true) diff --git a/tv/src/main/java/com/afzaln/besttvlauncher/ui/home/HomeScreen.kt b/tv/src/main/java/com/afzaln/besttvlauncher/ui/home/HomeScreen.kt index 49fbde7..cba81f0 100644 --- a/tv/src/main/java/com/afzaln/besttvlauncher/ui/home/HomeScreen.kt +++ b/tv/src/main/java/com/afzaln/besttvlauncher/ui/home/HomeScreen.kt @@ -46,9 +46,9 @@ fun HomeScreen() { val currentDestination = currentBackStack?.destination val currentTab = tabs.find { it.route == currentDestination?.route } ?: Channels - LaunchedEffect(key1 = Unit, block = { + LaunchedEffect(key1 = Unit) { viewModel.loadData() - }) + } Column(Modifier.fillMaxSize()) { TitleBar( diff --git a/tv/src/main/java/com/afzaln/besttvlauncher/ui/itemdetails/ItemDetailsScreen.kt b/tv/src/main/java/com/afzaln/besttvlauncher/ui/itemdetails/ItemDetailsScreen.kt index 1a8ea82..87299a9 100644 --- a/tv/src/main/java/com/afzaln/besttvlauncher/ui/itemdetails/ItemDetailsScreen.kt +++ b/tv/src/main/java/com/afzaln/besttvlauncher/ui/itemdetails/ItemDetailsScreen.kt @@ -118,7 +118,7 @@ private fun ConstraintLayoutScope.ItemInfo( .alpha(animationState.opacity) ) { Text( - text = program.title, + text = program.title ?: "Empty title", style = MaterialTheme.typography.headlineLarge, color = MaterialTheme.colorScheme.onSurface ) diff --git a/tv/src/main/java/com/afzaln/besttvlauncher/ui/theme/Theme.kt b/tv/src/main/java/com/afzaln/besttvlauncher/ui/theme/Theme.kt index e402432..37317c2 100644 --- a/tv/src/main/java/com/afzaln/besttvlauncher/ui/theme/Theme.kt +++ b/tv/src/main/java/com/afzaln/besttvlauncher/ui/theme/Theme.kt @@ -46,13 +46,14 @@ fun AppTheme( else -> DarkColorScheme } - val view = LocalView.current - if (!view.isInEditMode) { - SideEffect { - (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb() - ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme - } - } + // FIXME What is this for? +// val view = LocalView.current +// if (!view.isInEditMode) { +// SideEffect { +// (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb() +// ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme +// } +// } CompositionLocalProvider( LocalCardShape provides AppTheme.cardShape