Handle apps not found in app details

This commit is contained in:
Torsten Grote
2026-05-05 09:52:29 -03:00
parent 05d5e73959
commit eebb8a58b0
14 changed files with 106 additions and 68 deletions

View File

@@ -4,7 +4,7 @@ import org.fdroid.LocaleChooser.getBestLocale
import org.fdroid.download.NetworkState
import org.fdroid.install.InstallState
import org.fdroid.ui.details.AppDetails
import org.fdroid.ui.details.AppDetailsItem
import org.fdroid.ui.details.LoadedAppDetailsItem
import org.fdroid.ui.details.VersionItem
import org.fdroid.ui.utils.getAppDetailsActions
import org.fdroid.ui.utils.testVersion1
@@ -23,7 +23,7 @@ class DetailsScreenshotTest(localeName: String) : LocalizedScreenshotTest(locale
fun appDetails() =
screenshotTest("4_Details", showBottomBar = false, dark = true) { localeList ->
val item =
AppDetailsItem(
LoadedAppDetailsItem(
app = appMetadata,
actions = getAppDetailsActions(),
installState = InstallState.Unknown,

View File

@@ -2,9 +2,11 @@ package org.fdroid.ui.details
import android.util.Log
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
@@ -42,6 +44,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.rememberVectorPainter
@@ -99,11 +102,21 @@ fun AppDetails(
var showInstallError by remember { mutableStateOf(false) }
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(topAppBarState)
if (item == null) BigLoadingIndicator()
else
else {
Scaffold(
topBar = { AppDetailsTopAppBar(item, topAppBarState, scrollBehavior, onBackNav) },
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
) { innerPadding ->
if (item is NotFoundAppDetailsItem) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize().padding(innerPadding),
) {
Text(stringResource(R.string.no_such_app))
}
return@Scaffold
}
item as LoadedAppDetailsItem
// react to install state changes
LaunchedEffect(item.installState) {
val state = item.installState
@@ -413,33 +426,34 @@ fun AppDetails(
}
}
}
if (showInstallError && item != null && item.installState is InstallState.Error)
AlertDialog(
onDismissRequest = { showInstallError = false },
containerColor = MaterialTheme.colorScheme.errorContainer,
title = { Text(stringResource(R.string.install_error_notify_title, item.name)) },
text = {
if (item.installState.msg == null) {
Text(stringResource(R.string.app_details_install_error_text))
} else {
ExpandableSection(
icon = null,
title = stringResource(R.string.app_details_install_error_text),
) {
SelectionContainer {
Text(
text = item.installState.msg,
fontFamily = FontFamily.Monospace,
modifier = Modifier.padding(top = 8.dp),
)
if (item is LoadedAppDetailsItem && showInstallError && item.installState is InstallState.Error)
AlertDialog(
onDismissRequest = { showInstallError = false },
containerColor = MaterialTheme.colorScheme.errorContainer,
title = { Text(stringResource(R.string.install_error_notify_title, item.name)) },
text = {
if (item.installState.msg == null) {
Text(stringResource(R.string.app_details_install_error_text))
} else {
ExpandableSection(
icon = null,
title = stringResource(R.string.app_details_install_error_text),
) {
SelectionContainer {
Text(
text = item.installState.msg,
fontFamily = FontFamily.Monospace,
modifier = Modifier.padding(top = 8.dp),
)
}
}
}
}
},
confirmButton = {
TextButton(onClick = { showInstallError = false }) { Text(stringResource(R.string.ok)) }
},
)
},
confirmButton = {
TextButton(onClick = { showInstallError = false }) { Text(stringResource(R.string.ok)) }
},
)
}
}
@Preview
@@ -448,6 +462,12 @@ fun AppDetailsLoadingPreview() {
FDroidContent { AppDetails(null, {}, {}) }
}
@Preview
@Composable
fun AppDetailsNotFoundPreview() {
FDroidContent { AppDetails(NotFoundAppDetailsItem, {}, {}) }
}
@Preview
@Composable
fun AppDetailsPreview() {

View File

@@ -93,7 +93,7 @@ import org.fdroid.ui.utils.testApp
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
/** Timestamp [now] gets passed in for screenshot tests to have a stable download speed. */
fun AppDetailsHeader(
item: AppDetailsItem,
item: LoadedAppDetailsItem,
onNav: (NavigationKey) -> Unit,
innerPadding: PaddingValues,
now: Long = System.currentTimeMillis(),

View File

@@ -24,7 +24,11 @@ import org.fdroid.install.SessionInstallManager
import org.fdroid.search.SearchHelper.removeZeroWhiteSpace
import org.fdroid.ui.categories.CategoryItem
data class AppDetailsItem(
sealed class AppDetailsItem
data object NotFoundAppDetailsItem : AppDetailsItem()
data class LoadedAppDetailsItem(
val app: AppMetadata,
val actions: AppDetailsActions,
val installState: InstallState,
@@ -66,7 +70,7 @@ data class AppDetailsItem(
val issue: AppIssue? = null,
val authorHasMoreThanOneApp: Boolean = false,
val proxy: ProxyConfig? = null,
) {
) : AppDetailsItem() {
constructor(
repository: Repository,
preferredRepoId: Long,

View File

@@ -28,7 +28,7 @@ import org.fdroid.ui.utils.testApp
@Composable
fun AppDetailsMenu(
item: AppDetailsItem,
item: LoadedAppDetailsItem,
uninstallLauncher: ActivityResultLauncher<Intent>,
onDismiss: () -> Unit,
) {

View File

@@ -32,28 +32,30 @@ fun AppDetailsTopAppBar(
TopAppBar(
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent),
title = {
if (topAppBarState.overlappedFraction == 1f) {
if (item is LoadedAppDetailsItem && topAppBarState.overlappedFraction == 1f) {
Text(item.name, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
},
navigationIcon = { if (onBackNav != null) BackButton(onClick = onBackNav) },
actions = {
val context = LocalContext.current
item.actions.shareIntent?.let { shareIntent ->
TopAppBarButton(
imageVector = Icons.Filled.Share,
contentDescription = stringResource(R.string.menu_share),
onClick = { context.startActivitySafe(shareIntent) },
)
}
// the launcher needs to be at least here,
// because if the menu is dismissed we don't get the result
val uninstallLauncher =
rememberLauncherForActivityResult(StartActivityForResult()) {
item.actions.onUninstallResult(it)
if (item is LoadedAppDetailsItem) {
val context = LocalContext.current
item.actions.shareIntent?.let { shareIntent ->
TopAppBarButton(
imageVector = Icons.Filled.Share,
contentDescription = stringResource(R.string.menu_share),
onClick = { context.startActivitySafe(shareIntent) },
)
}
// the launcher needs to be at least here,
// because if the menu is dismissed we don't get the result
val uninstallLauncher =
rememberLauncherForActivityResult(StartActivityForResult()) {
item.actions.onUninstallResult(it)
}
TopAppBarOverflowButton { onDismissRequest ->
AppDetailsMenu(item, uninstallLauncher, onDismissRequest)
}
TopAppBarOverflowButton { onDismissRequest ->
AppDetailsMenu(item, uninstallLauncher, onDismissRequest)
}
},
scrollBehavior = scrollBehavior,

View File

@@ -165,7 +165,7 @@ constructor(
@UiThread
fun onUninstallResult(activityResult: ActivityResult) {
val name = appDetails.value?.name
val name = (appDetails.value as? LoadedAppDetailsItem)?.name
val result = appInstallManager.onUninstallResult(packageName, name, activityResult)
log.info { "Uninstall result was: $result" }
}
@@ -195,7 +195,7 @@ constructor(
val diskCache = SingletonImageLoader.get(application).diskCache
if (diskCache != null)
scope.launch {
appDetails.value?.phoneScreenshots?.forEach { screenshot ->
(appDetails.value as? LoadedAppDetailsItem)?.phoneScreenshots?.forEach { screenshot ->
if (screenshot is DownloadRequest) {
try {
diskCache.remove(screenshot.getCacheKey())
@@ -209,7 +209,7 @@ constructor(
@UiThread
fun allowBetaUpdates() {
val appPrefs = appDetails.value?.appPrefs ?: return
val appPrefs = (appDetails.value as? LoadedAppDetailsItem)?.appPrefs ?: return
scope.launch {
db.getAppPrefsDao().update(appPrefs.toggleReleaseChannel(RELEASE_CHANNEL_BETA))
updatesManager.loadUpdates()
@@ -218,7 +218,7 @@ constructor(
@UiThread
fun ignoreAllUpdates() {
val appPrefs = appDetails.value?.appPrefs ?: return
val appPrefs = (appDetails.value as? LoadedAppDetailsItem)?.appPrefs ?: return
scope.launch {
db.getAppPrefsDao().update(appPrefs.toggleIgnoreAllUpdates())
updatesManager.loadUpdates()
@@ -227,8 +227,9 @@ constructor(
@UiThread
fun ignoreThisUpdate() {
val appPrefs = appDetails.value?.appPrefs ?: return
val versionCode = appDetails.value?.possibleUpdate?.versionCode ?: return
val appPrefs = (appDetails.value as? LoadedAppDetailsItem)?.appPrefs ?: return
val versionCode =
(appDetails.value as? LoadedAppDetailsItem)?.possibleUpdate?.versionCode ?: return
scope.launch {
db.getAppPrefsDao().update(appPrefs.toggleIgnoreVersionCodeUpdate(versionCode))
updatesManager.loadUpdates()

View File

@@ -30,7 +30,7 @@ import org.fdroid.ui.FDroidContent
import org.fdroid.ui.utils.testApp
@Composable
fun AppDetailsWarnings(item: AppDetailsItem, modifier: Modifier = Modifier) {
fun AppDetailsWarnings(item: LoadedAppDetailsItem, modifier: Modifier = Modifier) {
val (color, string) =
when {
// app issues take priority

View File

@@ -68,7 +68,7 @@ fun DetailsPresenter(
}
}
}
.value ?: return null
.value ?: return NotFoundAppDetailsItem
val versions =
produceState<List<AppVersion>?>(null, currentRepoId) {
withContext(dispatcher) {
@@ -123,7 +123,7 @@ fun DetailsPresenter(
produceState<Repository?>(null, app) {
withContext(dispatcher) { value = repoManager.getRepository(app.repoId) }
}
.value ?: return null
.value ?: return NotFoundAppDetailsItem
val repositories =
produceState(emptyList(), packageName) {
withContext(dispatcher) {
@@ -196,7 +196,7 @@ fun DetailsPresenter(
Log.d(TAG, " versions: ${versions?.size}")
Log.d(TAG, " appPrefs: $appPrefs")
Log.d(TAG, " installState: $installState")
return AppDetailsItem(
return LoadedAppDetailsItem(
repository = repo,
preferredRepoId = preferredRepoId,
repositories = repositories,

View File

@@ -17,7 +17,7 @@ import org.fdroid.ui.FDroidContent
import org.fdroid.ui.utils.testApp
@Composable
fun TechnicalInfo(item: AppDetailsItem) {
fun TechnicalInfo(item: LoadedAppDetailsItem) {
val items = mutableMapOf(stringResource(R.string.package_name) to item.app.packageName)
if (item.installedVersionCode != null) {
items[stringResource(R.string.installed_version)] =

View File

@@ -47,7 +47,7 @@ import org.fdroid.ui.utils.testApp
@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
fun Versions(item: AppDetailsItem, scrollUp: suspend () -> Unit) {
fun Versions(item: LoadedAppDetailsItem, scrollUp: suspend () -> Unit) {
ExpandableSection(
icon = rememberVectorPainter(Icons.Default.AccessTime),
title = stringResource(R.string.versions),

View File

@@ -34,6 +34,7 @@ import org.fdroid.ui.categories.CategoryItem
import org.fdroid.ui.details.AntiFeature
import org.fdroid.ui.details.AppDetailsActions
import org.fdroid.ui.details.AppDetailsItem
import org.fdroid.ui.details.LoadedAppDetailsItem
import org.fdroid.ui.details.VersionItem
import org.fdroid.ui.lists.AntiFeatureItem
import org.fdroid.ui.lists.AppListActions
@@ -144,7 +145,7 @@ val categoryItems =
)
val testApp =
AppDetailsItem(
LoadedAppDetailsItem(
app =
AppMetadata(
repoId = 1,

View File

@@ -18,13 +18,14 @@ fun DetailsTest() =
testApp.copy(
versions = emptyList(),
categories = testApp.categories?.subList(0, 2),
phoneScreenshots = listOf(
"https://f-droid.org/repo/org.fdroid.fdroid.2024-05-31-17-00-00.png",
"https://f-droid.org/repo/org.fdroid.fdroid.2024-05-31-17-00-00.png",
"https://f-droid.org/repo/org.fdroid.fdroid.2024-05-31-17-00-00.png",
"https://f-droid.org/repo/org.fdroid.fdroid.2024-05-31-17-00-00.png",
"https://f-droid.org/repo/org.fdroid.fdroid.2024-05-31-17-00-00.png",
),
phoneScreenshots =
listOf(
"https://f-droid.org/repo/org.fdroid.fdroid.2024-05-31-17-00-00.png",
"https://f-droid.org/repo/org.fdroid.fdroid.2024-05-31-17-00-00.png",
"https://f-droid.org/repo/org.fdroid.fdroid.2024-05-31-17-00-00.png",
"https://f-droid.org/repo/org.fdroid.fdroid.2024-05-31-17-00-00.png",
"https://f-droid.org/repo/org.fdroid.fdroid.2024-05-31-17-00-00.png",
),
networkState = NetworkState(isOnline = true, isMetered = false),
),
onNav = {},
@@ -32,6 +33,12 @@ fun DetailsTest() =
)
}
@Preview
@Composable
@PreviewTest
fun AppDetailsNotFoundTest() =
ScreenshotTest(showBottomBar = false) { AppDetails(NotFoundAppDetailsItem, {}, {}) }
@Composable
@PreviewTest
@Preview(showBackground = true, showSystemUi = true, heightDp = 3000)

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9bc256fb0c2f4e7736bda965bff6fcfd2004cca3ac6bd307235ad068aef821af
size 21381