From 6c9c6239b9eca586a5090bdffe61e493f3fa00df Mon Sep 17 00:00:00 2001 From: rumboalla Date: Sun, 30 Jul 2023 09:32:39 +0200 Subject: [PATCH] Added additional Android TV UI similar to 2.x UI. Apks are streamed directly to SessionInstaller instead of being saved to disk first. --- app/build.gradle | 3 +- .../main/java/com/apkupdater/di/MainModule.kt | 3 +- .../main/java/com/apkupdater/prefs/Prefs.kt | 6 +- .../repository/ApkMirrorRepository.kt | 5 +- .../java/com/apkupdater/ui/component/Grid.kt | 51 +++++++ .../java/com/apkupdater/ui/component/Icons.kt | 2 +- .../java/com/apkupdater/ui/component/Image.kt | 14 +- .../java/com/apkupdater/ui/component/Text.kt | 21 ++- .../apkupdater/ui/component/TvComponents.kt | 129 ++++++++++++++++++ .../apkupdater/ui/component/UiComponents.kt | 54 +++----- .../com/apkupdater/ui/screen/AppsScreen.kt | 20 ++- .../com/apkupdater/ui/screen/SearchScreen.kt | 28 +++- .../apkupdater/ui/screen/SettingsScreen.kt | 15 +- .../com/apkupdater/ui/screen/UpdatesScreen.kt | 25 +++- .../java/com/apkupdater/util/Downloader.kt | 21 ++- .../java/com/apkupdater/util/Extensions.kt | 5 + .../com/apkupdater/util/SessionInstaller.kt | 11 +- .../apkupdater/viewmodel/SearchViewModel.kt | 7 +- .../apkupdater/viewmodel/SettingsViewModel.kt | 2 + .../apkupdater/viewmodel/UpdatesViewModel.kt | 7 +- app/src/main/res/values/strings.xml | 1 + 21 files changed, 355 insertions(+), 75 deletions(-) create mode 100644 app/src/main/java/com/apkupdater/ui/component/Grid.kt create mode 100644 app/src/main/java/com/apkupdater/ui/component/TvComponents.kt diff --git a/app/build.gradle b/app/build.gradle index bb64b226..a15d0267 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -47,7 +47,8 @@ dependencies { implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.6.1' implementation 'androidx.compose.ui:ui:1.4.3' - implementation 'androidx.compose.ui:ui-tooling-preview:1.4.3' + implementation "androidx.tv:tv-foundation:1.0.0-alpha08" +// implementation "androidx.tv:tv-material:1.0.0-alpha08" implementation 'androidx.compose.material3:material3:1.1.1' implementation 'androidx.work:work-runtime-ktx:2.8.1' implementation 'io.insert-koin:koin-android:3.4.2' diff --git a/app/src/main/java/com/apkupdater/di/MainModule.kt b/app/src/main/java/com/apkupdater/di/MainModule.kt index fb814241..5248ee34 100644 --- a/app/src/main/java/com/apkupdater/di/MainModule.kt +++ b/app/src/main/java/com/apkupdater/di/MainModule.kt @@ -17,6 +17,7 @@ import com.apkupdater.service.GitHubService import com.apkupdater.util.Downloader import com.apkupdater.util.SessionInstaller import com.apkupdater.util.UpdatesNotification +import com.apkupdater.util.isAndroidTv import com.apkupdater.viewmodel.AppsViewModel import com.apkupdater.viewmodel.MainViewModel import com.apkupdater.viewmodel.SearchViewModel @@ -106,7 +107,7 @@ val mainModule = module { single { KryptoBuilder.nocrypt(get(), androidContext().getString(R.string.app_name)) } - single { Prefs(get()) } + single { Prefs(get(), androidContext().isAndroidTv()) } single { UpdatesNotification(get()) } diff --git a/app/src/main/java/com/apkupdater/prefs/Prefs.kt b/app/src/main/java/com/apkupdater/prefs/Prefs.kt index aae283d1..b11a71f0 100644 --- a/app/src/main/java/com/apkupdater/prefs/Prefs.kt +++ b/app/src/main/java/com/apkupdater/prefs/Prefs.kt @@ -5,7 +5,10 @@ import com.kryptoprefs.gson.json import com.kryptoprefs.preferences.KryptoPrefs -class Prefs(prefs: KryptoPrefs): KryptoContext(prefs) { +class Prefs( + prefs: KryptoPrefs, + isAndroidTv: Boolean +): KryptoContext(prefs) { val ignoredApps = json("ignoredApps", emptyList(), true) val excludeSystem = boolean("excludeSystem", defValue = true, backed = true) val excludeDisabled = boolean("excludeDisabled", defValue = true, backed = true) @@ -20,4 +23,5 @@ class Prefs(prefs: KryptoPrefs): KryptoContext(prefs) { val enableAlarm = boolean("enableAlarm", defValue = false, backed = true) val alarmHour = int("alarmHour", defValue = 12, backed = true) val alarmFrequency = int("alarmFrequency", 0, backed = true) + val androidTvUi = boolean("androidTvUi", defValue = isAndroidTv, backed = true) } diff --git a/app/src/main/java/com/apkupdater/repository/ApkMirrorRepository.kt b/app/src/main/java/com/apkupdater/repository/ApkMirrorRepository.kt index 20eebe8b..01689a38 100644 --- a/app/src/main/java/com/apkupdater/repository/ApkMirrorRepository.kt +++ b/app/src/main/java/com/apkupdater/repository/ApkMirrorRepository.kt @@ -18,6 +18,7 @@ import com.apkupdater.data.ui.getVersionCode import com.apkupdater.prefs.Prefs import com.apkupdater.service.ApkMirrorService import com.apkupdater.util.combine +import com.apkupdater.util.isAndroidTv import com.apkupdater.util.orFalse import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collect @@ -39,7 +40,7 @@ class ApkMirrorRepository( else -> "arm" } - private val isAndroidTV = packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK) + private val isAndroidTV = packageManager.isAndroidTv() private val api = Build.VERSION.SDK_INT suspend fun updates(apps: List) = flow { @@ -64,7 +65,7 @@ class ApkMirrorRepository( name = h5[it].attr("title"), link = "$baseUrl${h5[it].selectFirst("a")?.attr("href")}", iconUri = Uri.parse("$baseUrl${img[it].attr("src")}".replace("=32", "=128")), - version = "", + version = "?", versionCode = 0L, source = ApkMirrorSource, packageName = a[it].text() // Developer name in this case diff --git a/app/src/main/java/com/apkupdater/ui/component/Grid.kt b/app/src/main/java/com/apkupdater/ui/component/Grid.kt new file mode 100644 index 00000000..0457deb6 --- /dev/null +++ b/app/src/main/java/com/apkupdater/ui/component/Grid.kt @@ -0,0 +1,51 @@ +package com.apkupdater.ui.component + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyGridScope +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.unit.dp +import androidx.tv.foundation.lazy.grid.TvGridCells +import androidx.tv.foundation.lazy.grid.TvLazyGridScope +import androidx.tv.foundation.lazy.grid.TvLazyVerticalGrid +import com.apkupdater.prefs.Prefs +import org.koin.androidx.compose.get + +@Composable +fun InstalledGrid(content: LazyGridScope.() -> Unit) = LazyVerticalGrid( + columns = GridCells.Fixed(getNumColumns(LocalConfiguration.current.orientation)), + contentPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + content = content +) + +@Composable +fun TvInstalledGrid(content: TvLazyGridScope.() -> Unit) = TvLazyVerticalGrid( + columns = TvGridCells.Fixed(getTvNumColumns()), + contentPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + content = content +) + +@Composable +fun getNumColumns(orientation: Int): Int { + val prefs = get() + return if(orientation == Configuration.ORIENTATION_PORTRAIT) + prefs.portraitColumns.get() + else + prefs.landscapeColumns.get() +} + +@Composable +fun getTvNumColumns(): Int { + return if(LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT) + 1 + else + 2 +} diff --git a/app/src/main/java/com/apkupdater/ui/component/Icons.kt b/app/src/main/java/com/apkupdater/ui/component/Icons.kt index da417221..a28b3c82 100644 --- a/app/src/main/java/com/apkupdater/ui/component/Icons.kt +++ b/app/src/main/java/com/apkupdater/ui/component/Icons.kt @@ -68,7 +68,7 @@ fun ExcludeDisabledIcon(exclude: Boolean) = ExcludeIcon( fun SourceIcon(source: Source, modifier: Modifier) = Icon( painterResource(id = source.resourceId), source.name, - Modifier.size(32.dp).then(modifier) + modifier ) @Composable diff --git a/app/src/main/java/com/apkupdater/ui/component/Image.kt b/app/src/main/java/com/apkupdater/ui/component/Image.kt index f423bcb4..26a226bf 100644 --- a/app/src/main/java/com/apkupdater/ui/component/Image.kt +++ b/app/src/main/java/com/apkupdater/ui/component/Image.kt @@ -23,14 +23,12 @@ import com.apkupdater.util.getAppIcon @Composable private fun BaseLoadingImage( request: ImageRequest, - height: Dp = 120.dp, + modifier: Modifier, color: Color = Color.Transparent ) = AsyncImage( model = request, contentDescription = stringResource(R.string.app_cd), - modifier = Modifier - .fillMaxSize() - .height(height) + modifier = modifier .padding(10.dp) .clip(RoundedCornerShape(8.dp)) .background(color), @@ -40,23 +38,23 @@ private fun BaseLoadingImage( @Composable fun LoadingImage( uri: Uri, - height: Dp = 120.dp, + modifier: Modifier = Modifier.height(120.dp).fillMaxSize(), crossfade: Boolean = true, color: Color = Color.Transparent ) = BaseLoadingImage( ImageRequest.Builder(LocalContext.current).data(uri).crossfade(crossfade).build(), - height, + modifier, color ) @Composable fun LoadingImageApp( packageName: String, - height: Dp = 120.dp, + modifier: Modifier = Modifier.height(120.dp).fillMaxSize(), crossfade: Boolean = true, color: Color = Color.Transparent ) = BaseLoadingImage( ImageRequest.Builder(LocalContext.current).data(LocalContext.current.getAppIcon(packageName)).crossfade(crossfade).build(), - height, + modifier, color ) diff --git a/app/src/main/java/com/apkupdater/ui/component/Text.kt b/app/src/main/java/com/apkupdater/ui/component/Text.kt index 46ebd82f..b1b8e15f 100644 --- a/app/src/main/java/com/apkupdater/ui/component/Text.kt +++ b/app/src/main/java/com/apkupdater/ui/component/Text.kt @@ -51,7 +51,16 @@ fun SmallText(text: String, modifier: Modifier = Modifier) = Text( ) @Composable -fun TitleText(text: String, modifier: Modifier = Modifier) = Text( +fun MediumText(text: String, modifier: Modifier = Modifier) = Text( + text = text, + style = MaterialTheme.typography.bodyMedium, + maxLines = 1, + modifier = modifier, + overflow = TextOverflow.Ellipsis +) + +@Composable +fun MediumTitle(text: String, modifier: Modifier = Modifier) = Text( text = text, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, @@ -60,6 +69,16 @@ fun TitleText(text: String, modifier: Modifier = Modifier) = Text( modifier = modifier ) +@Composable +fun LargeTitle(text: String, modifier: Modifier = Modifier) = Text( + text = text, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = modifier +) + @Composable fun HugeText(text: String, modifier: Modifier = Modifier, maxLines: Int = 1) = Text( text = text, diff --git a/app/src/main/java/com/apkupdater/ui/component/TvComponents.kt b/app/src/main/java/com/apkupdater/ui/component/TvComponents.kt new file mode 100644 index 00000000..5c7734df --- /dev/null +++ b/app/src/main/java/com/apkupdater/ui/component/TvComponents.kt @@ -0,0 +1,129 @@ +package com.apkupdater.ui.component + +import android.net.Uri +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ElevatedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.apkupdater.R +import com.apkupdater.data.ui.AppInstalled +import com.apkupdater.data.ui.AppUpdate + +@Composable +fun TvCommonItem( + packageName: String, + name: String, + version: String, + versionCode: Long, + uri: Uri? = null +) = Row { + if (uri == null) { + LoadingImageApp( + packageName, + Modifier + .height(90.dp) + .align(Alignment.CenterVertically) + ) + } else { + LoadingImage( + uri, + Modifier + .height(90.dp) + .align(Alignment.CenterVertically) + ) + } + Column( + Modifier + .align(Alignment.CenterVertically) + .padding(horizontal = 8.dp) + ) { + LargeTitle(name) + MediumText(version) + MediumText(versionCode.toString()) + } +} + +@Composable +fun TvInstallButton( + app: AppUpdate, + onInstall: (String) -> Unit +) = ElevatedButton( + modifier = Modifier + .padding(top = 0.dp, bottom = 8.dp, start = 8.dp, end = 8.dp) + .width(120.dp), + onClick = { onInstall(app.packageName) } +) { + if (app.isInstalling) { + CircularProgressIndicator(Modifier.size(24.dp)) + } else { + Text(stringResource(R.string.install_cd)) + } +} + +@Composable +fun BoxScope.TvSourceIcon(app: AppUpdate) = SourceIcon( + app.source, + Modifier + .align(Alignment.CenterStart) + .padding(top = 0.dp, bottom = 8.dp, start = 8.dp, end = 8.dp) + .size(32.dp) +) + +@Composable +fun TvInstalledItem(app: AppInstalled, onIgnore: (String) -> Unit = {}) = Card( + modifier = Modifier.alpha(if (app.ignored) 0.5f else 1f) +) { + Column { + TvCommonItem(app.packageName, app.name, app.version, app.versionCode) + Row(modifier = Modifier.fillMaxSize(), horizontalArrangement = Arrangement.End) { + ElevatedButton( + modifier = Modifier.padding(top = 0.dp, bottom = 8.dp, start = 8.dp, end = 8.dp), + onClick = { onIgnore(app.packageName) } + ) { + Text(stringResource(R.string.ignore_cd)) + } + } + } +} + +@Composable +fun TvUpdateItem(app: AppUpdate, onInstall: (String) -> Unit = {}) = Card { + Column { + TvCommonItem(app.packageName, app.name, app.version, app.versionCode) + Box { + TvSourceIcon(app) + Row(modifier = Modifier.fillMaxSize(), horizontalArrangement = Arrangement.End) { + TvInstallButton(app, onInstall) + } + } + } +} + +@Composable +fun TvSearchItem(app: AppUpdate, onInstall: (String) -> Unit = {}) = Card { + Column { + TvCommonItem(app.packageName, app.name, app.version, app.versionCode, app.iconUri) + Box { + TvSourceIcon(app) + Row(modifier = Modifier.fillMaxSize(), horizontalArrangement = Arrangement.End) { + TvInstallButton(app, onInstall) + } + } + } +} diff --git a/app/src/main/java/com/apkupdater/ui/component/UiComponents.kt b/app/src/main/java/com/apkupdater/ui/component/UiComponents.kt index 118ee86b..981ebc46 100644 --- a/app/src/main/java/com/apkupdater/ui/component/UiComponents.kt +++ b/app/src/main/java/com/apkupdater/ui/component/UiComponents.kt @@ -1,50 +1,24 @@ package com.apkupdater.ui.component - import android.content.res.Configuration - import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column - import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding - import androidx.compose.foundation.lazy.grid.GridCells - import androidx.compose.foundation.lazy.grid.LazyGridScope - import androidx.compose.foundation.lazy.grid.LazyVerticalGrid + import androidx.compose.foundation.layout.size import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha - import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.apkupdater.R import com.apkupdater.data.ui.AppInstalled import com.apkupdater.data.ui.AppUpdate - import com.apkupdater.prefs.Prefs import com.apkupdater.util.getAppName - import org.koin.androidx.compose.get -@Composable -fun getNumColumns(orientation: Int): Int { - val prefs = get() - return if(orientation == Configuration.ORIENTATION_PORTRAIT) - prefs.portraitColumns.get() - else - prefs.landscapeColumns.get() -} - -@Composable -fun InstalledGrid(content: LazyGridScope.() -> Unit) = LazyVerticalGrid( - columns = GridCells.Fixed(getNumColumns(LocalConfiguration.current.orientation)), - contentPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - content = content -) - @Composable fun AppImage(app: AppInstalled, onIgnore: (String) -> Unit = {}) = Box { LoadingImageApp(app.packageName) @@ -52,7 +26,9 @@ fun AppImage(app: AppInstalled, onIgnore: (String) -> Unit = {}) = Box { IgnoreIcon( app.ignored, { onIgnore(app.packageName) }, - Modifier.align(Alignment.TopEnd).padding(4.dp) + Modifier + .align(Alignment.TopEnd) + .padding(4.dp) ) } @@ -61,7 +37,13 @@ fun UpdateImage(app: AppUpdate, onInstall: (String) -> Unit = {}) = Box { LoadingImageApp(app.packageName) TextBubble(app.versionCode.toString(), Modifier.align(Alignment.BottomStart)) InstallProgressIcon(app.isInstalling) { onInstall(app.link) } - SourceIcon(app.source, Modifier.align(Alignment.TopStart).padding(4.dp)) + SourceIcon( + app.source, + Modifier + .align(Alignment.TopStart) + .padding(4.dp) + .size(32.dp) + ) } @@ -71,7 +53,13 @@ fun SearchImage(app: AppUpdate, onInstall: (String) -> Unit = {}) = Box { if (app.versionCode != 0L) TextBubble(app.versionCode.toString(), Modifier.align(Alignment.BottomStart)) InstallProgressIcon(app.isInstalling) { onInstall(app.link) } - SourceIcon(app.source, Modifier.align(Alignment.TopStart).padding(4.dp)) + SourceIcon( + app.source, + Modifier + .align(Alignment.TopStart) + .padding(4.dp) + .size(32.dp) + ) } @Composable @@ -81,7 +69,7 @@ fun InstalledItem(app: AppInstalled, onIgnore: (String) -> Unit = {}) = Column( AppImage(app, onIgnore) Column(Modifier.padding(top = 4.dp)) { ScrollableText { SmallText(app.packageName) } - TitleText(app.name) + MediumTitle(app.name) } } @@ -90,7 +78,7 @@ fun UpdateItem(app: AppUpdate, onInstall: (String) -> Unit = {}) = Column { UpdateImage(app, onInstall) Column(Modifier.padding(top = 4.dp)) { ScrollableText { SmallText(app.packageName) } - TitleText(app.name.ifEmpty { LocalContext.current.getAppName(app.packageName) }) + MediumTitle(app.name.ifEmpty { LocalContext.current.getAppName(app.packageName) }) } } @@ -99,7 +87,7 @@ fun SearchItem(app: AppUpdate, onInstall: (String) -> Unit = {}) = Column { SearchImage(app, onInstall) Column(Modifier.padding(top = 4.dp)) { ScrollableText { SmallText(app.packageName) } - TitleText(app.name) + MediumTitle(app.name) } } diff --git a/app/src/main/java/com/apkupdater/ui/screen/AppsScreen.kt b/app/src/main/java/com/apkupdater/ui/screen/AppsScreen.kt index 820bfceb..7d79249a 100644 --- a/app/src/main/java/com/apkupdater/ui/screen/AppsScreen.kt +++ b/app/src/main/java/com/apkupdater/ui/screen/AppsScreen.kt @@ -2,6 +2,7 @@ package com.apkupdater.ui.screen import androidx.compose.foundation.layout.Column import androidx.compose.foundation.lazy.grid.items +import androidx.tv.foundation.lazy.grid.items import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -13,6 +14,7 @@ import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.apkupdater.R import com.apkupdater.data.ui.AppsUiState +import com.apkupdater.prefs.Prefs import com.apkupdater.ui.component.DefaultErrorScreen import com.apkupdater.ui.component.DefaultLoadingScreen import com.apkupdater.ui.component.ExcludeAppStoreIcon @@ -20,8 +22,11 @@ import com.apkupdater.ui.component.ExcludeDisabledIcon import com.apkupdater.ui.component.ExcludeSystemIcon import com.apkupdater.ui.component.InstalledGrid import com.apkupdater.ui.component.InstalledItem +import com.apkupdater.ui.component.TvInstalledItem +import com.apkupdater.ui.component.TvInstalledGrid import com.apkupdater.ui.theme.statusBarColor import com.apkupdater.viewmodel.AppsViewModel +import org.koin.androidx.compose.get import org.koin.androidx.compose.koinViewModel @@ -41,9 +46,18 @@ fun AppsScreen( @Composable fun AppsScreenSuccess(viewModel: AppsViewModel, state: AppsUiState.Success) = Column { AppsTopBar(viewModel, state) - InstalledGrid { - items(state.apps) { - InstalledItem(it) { app -> viewModel.ignore(app) } + val prefs: Prefs = get() + if (prefs.androidTvUi.get()) { + TvInstalledGrid { + items(state.apps) { + TvInstalledItem(it) { app -> viewModel.ignore(app) } + } + } + } else { + InstalledGrid { + items(state.apps) { + InstalledItem(it) { app -> viewModel.ignore(app) } + } } } } diff --git a/app/src/main/java/com/apkupdater/ui/screen/SearchScreen.kt b/app/src/main/java/com/apkupdater/ui/screen/SearchScreen.kt index f3c28c52..7b64c734 100644 --- a/app/src/main/java/com/apkupdater/ui/screen/SearchScreen.kt +++ b/app/src/main/java/com/apkupdater/ui/screen/SearchScreen.kt @@ -15,22 +15,27 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.tv.foundation.lazy.grid.items import com.apkupdater.data.ui.SearchUiState +import com.apkupdater.prefs.Prefs import com.apkupdater.ui.component.DefaultErrorScreen import com.apkupdater.ui.component.DefaultLoadingScreen import com.apkupdater.ui.component.InstalledGrid import com.apkupdater.ui.component.SearchItem +import com.apkupdater.ui.component.TvInstalledGrid +import com.apkupdater.ui.component.TvSearchItem import com.apkupdater.viewmodel.SearchViewModel import kotlinx.coroutines.delay +import org.koin.androidx.compose.get import org.koin.androidx.compose.koinViewModel + @Composable fun SearchScreen( viewModel: SearchViewModel = koinViewModel() @@ -51,16 +56,27 @@ fun SearchScreenSuccess( viewModel: SearchViewModel ) = Column { val uriHandler = LocalUriHandler.current - InstalledGrid { - items(state.updates) { update -> - SearchItem(update) { - viewModel.install(update, uriHandler) + val prefs: Prefs = get() + + if (prefs.androidTvUi.get()) { + TvInstalledGrid { + items(state.updates) { update -> + TvSearchItem(update) { + viewModel.install(update, uriHandler) + } + } + } + } else { + InstalledGrid { + items(state.updates) { update -> + SearchItem(update) { + viewModel.install(update, uriHandler) + } } } } } -@OptIn(ExperimentalComposeUiApi::class) @Composable fun SearchTopBar(viewModel: SearchViewModel) = Box { val keyboardController = LocalSoftwareKeyboardController.current diff --git a/app/src/main/java/com/apkupdater/ui/screen/SettingsScreen.kt b/app/src/main/java/com/apkupdater/ui/screen/SettingsScreen.kt index bddaabe0..26bc74c2 100644 --- a/app/src/main/java/com/apkupdater/ui/screen/SettingsScreen.kt +++ b/app/src/main/java/com/apkupdater/ui/screen/SettingsScreen.kt @@ -18,7 +18,7 @@ import com.apkupdater.R import com.apkupdater.ui.component.DropDownSetting import com.apkupdater.ui.component.SliderSetting import com.apkupdater.ui.component.SwitchSetting -import com.apkupdater.ui.component.TitleText +import com.apkupdater.ui.component.MediumTitle import com.apkupdater.ui.theme.statusBarColor import com.apkupdater.viewmodel.SettingsViewModel import org.koin.androidx.compose.koinViewModel @@ -32,7 +32,12 @@ fun SettingsScreen(viewModel: SettingsViewModel = koinViewModel()) = Column { LazyColumn { item { - TitleText(stringResource(R.string.settings_ui), Modifier.padding(horizontal = 8.dp)) + MediumTitle(stringResource(R.string.settings_ui), Modifier.padding(horizontal = 8.dp)) + SwitchSetting( + getValue = { viewModel.getAndroidTvUi() }, + setValue = { viewModel.setAndroidTvUi(it) }, + text = stringResource(R.string.settings_android_tv_ui) + ) SliderSetting( { viewModel.getPortraitColumns().toFloat() }, { viewModel.setPortraitColumns(it.toInt()) }, @@ -50,7 +55,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = koinViewModel()) = Column { } item { - TitleText(stringResource(R.string.settings_sources), Modifier.padding(horizontal = 8.dp)) + MediumTitle(stringResource(R.string.settings_sources), Modifier.padding(horizontal = 8.dp)) SwitchSetting( { viewModel.getUseGitHub() }, { viewModel.setUseGitHub(it) }, @@ -69,7 +74,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = koinViewModel()) = Column { } item { - TitleText(stringResource(R.string.settings_options), Modifier.padding(horizontal = 8.dp, vertical = 8.dp)) + MediumTitle(stringResource(R.string.settings_options), Modifier.padding(horizontal = 8.dp, vertical = 8.dp)) SwitchSetting( { viewModel.getIgnoreAlpha() }, { viewModel.setIgnoreAlpha(it) }, @@ -83,7 +88,7 @@ fun SettingsScreen(viewModel: SettingsViewModel = koinViewModel()) = Column { } item { - TitleText(stringResource(R.string.settings_alarm), Modifier.padding(horizontal = 8.dp, vertical = 8.dp)) + MediumTitle(stringResource(R.string.settings_alarm), Modifier.padding(horizontal = 8.dp, vertical = 8.dp)) SwitchSetting( getValue = { viewModel.getEnableAlarm() }, setValue = { viewModel.setEnableAlarm(it, launcher) }, diff --git a/app/src/main/java/com/apkupdater/ui/screen/UpdatesScreen.kt b/app/src/main/java/com/apkupdater/ui/screen/UpdatesScreen.kt index 6db5bb48..60608efc 100644 --- a/app/src/main/java/com/apkupdater/ui/screen/UpdatesScreen.kt +++ b/app/src/main/java/com/apkupdater/ui/screen/UpdatesScreen.kt @@ -12,15 +12,20 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.tv.foundation.lazy.grid.items import com.apkupdater.R import com.apkupdater.data.ui.AppUpdate +import com.apkupdater.prefs.Prefs import com.apkupdater.ui.component.DefaultErrorScreen import com.apkupdater.ui.component.DefaultLoadingScreen import com.apkupdater.ui.component.InstalledGrid import com.apkupdater.ui.component.RefreshIcon +import com.apkupdater.ui.component.TvInstalledGrid +import com.apkupdater.ui.component.TvUpdateItem import com.apkupdater.ui.component.UpdateItem import com.apkupdater.ui.theme.statusBarColor import com.apkupdater.viewmodel.UpdatesViewModel +import org.koin.androidx.compose.get @Composable @@ -60,12 +65,24 @@ fun UpdatesScreenSuccess( updates: List ) = Column { val uriHandler = LocalUriHandler.current + val prefs: Prefs = get() UpdatesTopBar(viewModel) - InstalledGrid { - items(updates) { update -> - UpdateItem(update) { - viewModel.install(update, uriHandler) + + if (prefs.androidTvUi.get()) { + TvInstalledGrid { + items(updates) { update -> + TvUpdateItem(update) { + viewModel.install(update, uriHandler) + } + } + } + } else { + InstalledGrid { + items(updates) { update -> + UpdateItem(update) { + viewModel.install(update, uriHandler) + } } } } diff --git a/app/src/main/java/com/apkupdater/util/Downloader.kt b/app/src/main/java/com/apkupdater/util/Downloader.kt index 962006b0..6ecf7ba8 100644 --- a/app/src/main/java/com/apkupdater/util/Downloader.kt +++ b/app/src/main/java/com/apkupdater/util/Downloader.kt @@ -1,9 +1,11 @@ package com.apkupdater.util import android.content.Context +import android.util.Log import okhttp3.OkHttpClient import okhttp3.Request import java.io.File +import java.io.InputStream import java.util.UUID @@ -12,10 +14,10 @@ class Downloader(context: Context) { private val client = OkHttpClient.Builder().followRedirects(true).build() private val dir = File(context.cacheDir, "downloads").apply { mkdirs() } + @Suppress("unused") fun download(url: String): File { - val request = Request.Builder().url(url).build() val file = File(dir, UUID.randomUUID().toString()) - client.newCall(request).execute().use { + client.newCall(downloadRequest(url)).execute().use { if (it.isSuccessful) { it.body?.byteStream()?.copyTo(file.outputStream()) } @@ -23,4 +25,19 @@ class Downloader(context: Context) { return file } + fun downloadStream(url: String): InputStream? = runCatching { + val response = client.newCall(downloadRequest(url)).execute() + if (response.isSuccessful) { + response.body?.let { + return it.byteStream() + } + } + return null + }.getOrElse { + Log.e("Downloader", "Error downloading", it) + null + } + + private fun downloadRequest(url: String) = Request.Builder().url(url).build() + } diff --git a/app/src/main/java/com/apkupdater/util/Extensions.kt b/app/src/main/java/com/apkupdater/util/Extensions.kt index da25be19..3de3054e 100644 --- a/app/src/main/java/com/apkupdater/util/Extensions.kt +++ b/app/src/main/java/com/apkupdater/util/Extensions.kt @@ -3,6 +3,7 @@ package com.apkupdater.util import android.content.Context import android.content.Intent import android.content.pm.PackageInfo +import android.content.pm.PackageManager import android.os.Build import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -83,3 +84,7 @@ fun Intent.getIntentExtra(): Intent? = when { fun Intent.getAppId() = runCatching { action?.split(".")?.get(1)?.toInt() }.getOrNull() + +fun PackageManager.isAndroidTv() = hasSystemFeature(PackageManager.FEATURE_LEANBACK) + +fun Context.isAndroidTv() = packageManager.isAndroidTv() \ No newline at end of file diff --git a/app/src/main/java/com/apkupdater/util/SessionInstaller.kt b/app/src/main/java/com/apkupdater/util/SessionInstaller.kt index e9a34ee9..6f4a4b74 100644 --- a/app/src/main/java/com/apkupdater/util/SessionInstaller.kt +++ b/app/src/main/java/com/apkupdater/util/SessionInstaller.kt @@ -12,7 +12,8 @@ import androidx.core.content.ContextCompat.startActivity import com.apkupdater.BuildConfig import com.apkupdater.data.ui.AppUpdate import com.apkupdater.ui.activity.MainActivity -import java.io.File +import java.io.InputStream +import java.util.UUID import java.util.concurrent.atomic.AtomicBoolean @@ -24,7 +25,7 @@ class SessionInstaller(private val context: Context) { private val installMutex = AtomicBoolean(false) - suspend fun install(app: AppUpdate, file: File) { + suspend fun install(app: AppUpdate, stream: InputStream) { val packageInstaller: PackageInstaller = context.packageManager.packageInstaller val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL) params.setAppPackageName(app.packageName) @@ -41,10 +42,10 @@ class SessionInstaller(private val context: Context) { val sessionId = packageInstaller.createSession(params) val session = packageInstaller.openSession(sessionId) - session.openWrite(file.name, 0, file.length()).use { output -> - file.inputStream().copyTo(output) + session.openWrite(UUID.randomUUID().toString(), 0, -1).use { output -> + stream.copyTo(output) + stream.close() session.fsync(output) - file.delete() } val intent = Intent(context, MainActivity::class.java).apply { diff --git a/app/src/main/java/com/apkupdater/viewmodel/SearchViewModel.kt b/app/src/main/java/com/apkupdater/viewmodel/SearchViewModel.kt index 2308f0d4..87c1d9ce 100644 --- a/app/src/main/java/com/apkupdater/viewmodel/SearchViewModel.kt +++ b/app/src/main/java/com/apkupdater/viewmodel/SearchViewModel.kt @@ -85,7 +85,12 @@ class SearchViewModel( private fun downloadAndInstall(update: AppUpdate) = viewModelScope.launch(Dispatchers.IO) { if(installer.checkPermission()) { state.value = SearchUiState.Success(setIsInstalling(update.id, true)) - installer.install(update, downloader.download(update.link)) + val stream = downloader.downloadStream(update.link) + if (stream != null) { + installer.install(update, stream) + } else { + cancelInstall(update.id) + } } } diff --git a/app/src/main/java/com/apkupdater/viewmodel/SettingsViewModel.kt b/app/src/main/java/com/apkupdater/viewmodel/SettingsViewModel.kt index 9bb82027..86e7b9e2 100644 --- a/app/src/main/java/com/apkupdater/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/com/apkupdater/viewmodel/SettingsViewModel.kt @@ -27,6 +27,8 @@ class SettingsViewModel( fun setUseFdroid(b: Boolean) = prefs.useFdroid.put(b) fun getUseGitHub() = prefs.useGitHub.get() fun setUseGitHub(b: Boolean) = prefs.useGitHub.put(b) + fun getAndroidTvUi() = prefs.androidTvUi.get() + fun setAndroidTvUi(b: Boolean) = prefs.androidTvUi.put(b) fun getEnableAlarm() = prefs.enableAlarm.get() fun getAlarmHour() = prefs.alarmHour.get() fun getAlarmFrequency() = prefs.alarmFrequency.get() diff --git a/app/src/main/java/com/apkupdater/viewmodel/UpdatesViewModel.kt b/app/src/main/java/com/apkupdater/viewmodel/UpdatesViewModel.kt index 78a7942a..f27bd208 100644 --- a/app/src/main/java/com/apkupdater/viewmodel/UpdatesViewModel.kt +++ b/app/src/main/java/com/apkupdater/viewmodel/UpdatesViewModel.kt @@ -74,7 +74,12 @@ class UpdatesViewModel( private fun downloadAndInstall(update: AppUpdate) = viewModelScope.launch(Dispatchers.IO) { if(installer.checkPermission()) { state.value = UpdatesUiState.Success(setIsInstalling(update.id, true)) - installer.install(update, downloader.download(update.link)) + val stream = downloader.downloadStream(update.link) + if (stream != null) { + installer.install(update, stream) + } else { + cancelInstall(update.id) + } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1d661dae..f97c8eb1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -30,6 +30,7 @@ Daily 3-Day Weekly + Android TV UI Ignore Alpha Ignore Beta ApkMirror