Skip to content

Commit f90d5ba

Browse files
committed
fix: Show SDK-incompatible versions as disabled, block patching when no versions are compatible with device SDK
Closes #481
1 parent 13767de commit f90d5ba

5 files changed

Lines changed: 205 additions & 21 deletions

File tree

app/src/main/java/app/morphe/manager/patcher/patch/PatchInfo.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,16 @@ data class PatchInfo(
4949
}
5050
.toMap()
5151
.toImmutableMap()
52-
.takeIf { it.isNotEmpty() }
52+
.takeIf { it.isNotEmpty() },
53+
versionMinSdks = compatibility.targets
54+
.mapNotNull { target ->
55+
val v = target.version ?: return@mapNotNull null
56+
val sdk = target.minSdk ?: return@mapNotNull null
57+
v to sdk
58+
}
59+
.toMap()
60+
.toImmutableMap()
61+
.takeIf { it.isNotEmpty() },
5362
)
5463
}
5564
?.toImmutableList()
@@ -131,6 +140,8 @@ data class CompatiblePackage(
131140
val signatures: ImmutableSet<String>? = null,
132141
/** Per-version user-facing descriptions. */
133142
val versionDescriptions: ImmutableMap<String, String>? = null,
143+
/** Minimum Android SDK version required per app version. */
144+
val versionMinSdks: ImmutableMap<String, Int>? = null,
134145
)
135146

136147
@Immutable

app/src/main/java/app/morphe/manager/ui/screen/home/HomeDialogs.kt

Lines changed: 129 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package app.morphe.manager.ui.screen.home
77

88
import android.annotation.SuppressLint
9+
import android.os.Build
910
import android.widget.Toast
1011
import androidx.compose.animation.AnimatedVisibility
1112
import androidx.compose.animation.core.tween
@@ -25,6 +26,7 @@ import androidx.compose.material3.*
2526
import androidx.compose.runtime.*
2627
import androidx.compose.ui.Alignment
2728
import androidx.compose.ui.Modifier
29+
import androidx.compose.ui.draw.alpha
2830
import androidx.compose.ui.graphics.Color
2931
import androidx.compose.ui.platform.LocalContext
3032
import androidx.compose.ui.platform.LocalUriHandler
@@ -50,6 +52,7 @@ import app.morphe.manager.ui.viewmodel.SavedApkInfo
5052
import app.morphe.manager.util.KnownApps
5153
import app.morphe.manager.util.MppManifest
5254
import app.morphe.manager.util.RemoteAvatar
55+
import app.morphe.manager.util.androidVersionName
5356
import app.morphe.manager.util.htmlAnnotatedString
5457
import app.morphe.manager.util.toast
5558
import app.morphe.patcher.patch.AppTarget
@@ -242,6 +245,21 @@ fun HomeDialogs(
242245
)
243246
}
244247

248+
// No compatible versions dialog - shown when every declared version requires a higher SDK
249+
AnimatedVisibility(
250+
visible = homeViewModel.showNoCompatibleVersionsDialog != null,
251+
enter = fadeIn(tween(MorpheDefaults.ANIMATION_DURATION)),
252+
exit = fadeOut(tween(MorpheDefaults.ANIMATION_DURATION))
253+
) {
254+
val packageName = homeViewModel.showNoCompatibleVersionsDialog ?: return@AnimatedVisibility
255+
val appName = homeViewModel.bundleAppMetadataFlow.value[packageName]?.displayName
256+
?: KnownApps.getAppName(packageName)
257+
NoCompatibleVersionsDialog(
258+
appName = appName,
259+
onDismiss = { homeViewModel.showNoCompatibleVersionsDialog = null }
260+
)
261+
}
262+
245263
// Split APK Warning Dialog - shown when user picks a split APK for an app that prefers full APK
246264
if (homeViewModel.showSplitApkWarningDialog) {
247265
val appName = homeViewModel.pendingAppName ?: ""
@@ -504,6 +522,18 @@ private fun ApkAvailabilityDialog(
504522
onNeedApk: () -> Unit,
505523
onUseSaved: () -> Unit
506524
) {
525+
val deviceSdk = Build.VERSION.SDK_INT
526+
527+
// Versions whose minSdk exceeds the current device - shown greyed-out and non-selectable
528+
val incompatibleSdkVersions: Set<String> = remember(compatibleVersions, deviceSdk) {
529+
compatibleVersions
530+
.mapNotNull { b ->
531+
val v = b.target.version ?: return@mapNotNull null
532+
val minSdk = b.target.minSdk ?: return@mapNotNull null
533+
if (deviceSdk < minSdk) v else null
534+
}
535+
.toSet()
536+
}
507537
MorpheDialog(
508538
onDismissRequest = onDismiss,
509539
title = stringResource(R.string.home_apk_availability_dialog_title),
@@ -566,7 +596,8 @@ private fun ApkAvailabilityDialog(
566596
recommendedBundleVersions = recommendedBundleVersions,
567597
onVersionSelect = onVersionSelect,
568598
anyString = anyString,
569-
hasMultipleBundles = compatibleVersions.map { it.bundleUid }.distinct().size > 1
599+
hasMultipleBundles = compatibleVersions.map { it.bundleUid }.distinct().size > 1,
600+
incompatibleSdkVersions = incompatibleSdkVersions,
570601
)
571602
} else {
572603
VersionListCard(
@@ -577,7 +608,8 @@ private fun ApkAvailabilityDialog(
577608
.toSet(),
578609
descriptions = compatibleVersions
579610
.mapNotNull { b -> b.target.version?.let { v -> b.target.description?.let { d -> v to d } } }
580-
.toMap()
611+
.toMap(),
612+
incompatibleSdkVersions = incompatibleSdkVersions,
581613
)
582614
}
583615
} else {
@@ -1210,19 +1242,72 @@ fun WrongPackageDialog(
12101242
}
12111243

12121244
/**
1213-
* Version list card where each row is tappable.
1214-
* The selected version gets a checkmark; the recommended version is labeled when not selected.
1215-
* Experimental versions are always labeled regardless of selection state.
1245+
* Shown when the device SDK is lower than the minSdk of every declared AppTarget for this app.
1246+
* Informs the user that their device does not meet the requirements for any supported version.
12161247
*/
12171248
@Composable
1249+
private fun NoCompatibleVersionsDialog(
1250+
appName: String,
1251+
onDismiss: () -> Unit
1252+
) {
1253+
val deviceSdk = Build.VERSION.SDK_INT
1254+
1255+
MorpheDialog(
1256+
onDismissRequest = onDismiss,
1257+
title = stringResource(R.string.home_apk_no_compatible_versions_title),
1258+
footer = {
1259+
MorpheDialogButton(
1260+
text = stringResource(android.R.string.ok),
1261+
onClick = onDismiss,
1262+
modifier = Modifier.fillMaxWidth()
1263+
)
1264+
}
1265+
) {
1266+
Column(
1267+
modifier = Modifier.fillMaxWidth(),
1268+
verticalArrangement = Arrangement.spacedBy(16.dp),
1269+
horizontalAlignment = Alignment.CenterHorizontally
1270+
) {
1271+
Icon(
1272+
imageVector = Icons.Outlined.PhoneAndroid,
1273+
contentDescription = null,
1274+
tint = MaterialTheme.colorScheme.error,
1275+
modifier = Modifier.size(48.dp)
1276+
)
1277+
Text(
1278+
text = htmlAnnotatedString(
1279+
stringResource(
1280+
R.string.home_apk_no_compatible_versions_message,
1281+
appName,
1282+
deviceSdk.androidVersionName(),
1283+
deviceSdk
1284+
)
1285+
),
1286+
style = MaterialTheme.typography.bodyLarge,
1287+
color = LocalDialogSecondaryTextColor.current,
1288+
textAlign = TextAlign.Center,
1289+
modifier = Modifier.fillMaxWidth()
1290+
)
1291+
}
1292+
}
1293+
}
1294+
1295+
/**
1296+
* The selected version gets a checkmark; the recommended version is labeled when not selected.
1297+
* Experimental versions are always labeled regardless of selection state.
1298+
* Versions whose [AppTarget.minSdk] exceeds the current device SDK are shown greyed-out
1299+
* and cannot be selected.
1300+
*/
1301+
@Composable
12181302
private fun SelectableVersionListCard(
1303+
modifier: Modifier = Modifier,
12191304
versions: List<BundledAppTarget>,
12201305
selectedVersion: AppTarget?,
12211306
recommendedBundleVersions: Map<Int, AppTarget>,
12221307
onVersionSelect: (AppTarget) -> Unit,
12231308
anyString: String,
12241309
hasMultipleBundles: Boolean,
1225-
modifier: Modifier = Modifier
1310+
incompatibleSdkVersions: Set<String> = emptySet()
12261311
) {
12271312
if (versions.isEmpty()) return
12281313

@@ -1238,12 +1323,16 @@ private fun SelectableVersionListCard(
12381323
versions.forEachIndexed { index, bundled ->
12391324
val target = bundled.target
12401325
val versionString = target.version ?: anyString
1241-
val isSelected = target.version != null && target.version == selectedVersion?.version
1242-
val isRecommended = target.version != null &&
1326+
val isIncompatibleSdk = target.version != null && target.version in incompatibleSdkVersions
1327+
val isSelected = !isIncompatibleSdk && target.version != null && target.version == selectedVersion?.version
1328+
val isRecommended = !isIncompatibleSdk && target.version != null &&
12431329
target.version == recommendedBundleVersions[bundled.bundleUid]?.version
12441330
val recommendedLabel = stringResource(R.string.home_apk_availability_recommended_label)
12451331
val experimentalLabel = stringResource(R.string.home_dialog_unsupported_version_experimental_label)
12461332
val selectedLabel = stringResource(R.string.home_selected_version)
1333+
val requiresAndroidLabel = target.minSdk?.let { sdk ->
1334+
stringResource(R.string.home_version_requires_android, sdk.androidVersionName())
1335+
}
12471336

12481337
// Bundle section header - only when multiple bundles are present and uid changes
12491338
if (hasMultipleBundles && bundled.bundleUid != lastBundleUid) {
@@ -1278,6 +1367,13 @@ private fun SelectableVersionListCard(
12781367
}
12791368

12801369
val badge: @Composable (() -> Unit)? = when {
1370+
isIncompatibleSdk -> ({
1371+
InfoBadge(
1372+
text = requiresAndroidLabel ?: "API ${target.minSdk ?: "?"}+",
1373+
style = InfoBadgeStyle.Error,
1374+
isCompact = true
1375+
)
1376+
})
12811377
target.isExperimental -> ({
12821378
InfoBadge(
12831379
text = experimentalLabel,
@@ -1298,6 +1394,7 @@ private fun SelectableVersionListCard(
12981394
val rowContentDesc = buildString {
12991395
append(versionString)
13001396
when {
1397+
isIncompatibleSdk -> requiresAndroidLabel?.let { append(", $it") }
13011398
target.isExperimental -> append(", $experimentalLabel")
13021399
isRecommended -> append(", $recommendedLabel")
13031400
}
@@ -1309,10 +1406,13 @@ private fun SelectableVersionListCard(
13091406
Row(
13101407
modifier = Modifier
13111408
.fillMaxWidth()
1312-
.selectable(
1313-
selected = isSelected,
1314-
onClick = { onVersionSelect(target) },
1315-
role = Role.RadioButton
1409+
.then(
1410+
if (isIncompatibleSdk) Modifier
1411+
else Modifier.selectable(
1412+
selected = isSelected,
1413+
onClick = { onVersionSelect(target) },
1414+
role = Role.RadioButton
1415+
)
13161416
)
13171417
.semantics { contentDescription = rowContentDesc }
13181418
.padding(horizontal = 16.dp, vertical = 12.dp),
@@ -1335,7 +1435,9 @@ private fun SelectableVersionListCard(
13351435
}
13361436

13371437
Column(
1338-
modifier = Modifier.weight(1f),
1438+
modifier = Modifier
1439+
.weight(1f)
1440+
.then(if (isIncompatibleSdk) Modifier.alpha(0.4f) else Modifier),
13391441
verticalArrangement = Arrangement.spacedBy(2.dp)
13401442
) {
13411443
Row(
@@ -1348,6 +1450,7 @@ private fun SelectableVersionListCard(
13481450
fontFamily = FontFamily.Monospace,
13491451
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
13501452
color = when {
1453+
isIncompatibleSdk -> LocalDialogTextColor.current
13511454
isSelected -> MaterialTheme.colorScheme.primary
13521455
target.isExperimental -> MaterialTheme.colorScheme.tertiary
13531456
else -> LocalDialogTextColor.current
@@ -1393,7 +1496,8 @@ private fun VersionListCard(
13931496
isCompatible: Boolean = false,
13941497
showUnpatchedBadge: Boolean = false,
13951498
experimentalVersions: Set<String> = emptySet(),
1396-
descriptions: Map<String, String> = emptyMap()
1499+
descriptions: Map<String, String> = emptyMap(),
1500+
incompatibleSdkVersions: Set<String> = emptySet(),
13971501
) {
13981502
if (versions.isEmpty()) return
13991503

@@ -1423,10 +1527,18 @@ private fun VersionListCard(
14231527
) {
14241528
versions.forEachIndexed { index, version ->
14251529
val isExperimentalVersion = version in experimentalVersions
1530+
val isIncompatibleSdk = version in incompatibleSdkVersions
14261531
val versionDescription = descriptions[version]
14271532

14281533
// Resolve badge once - drives both the badge composable and version text color
14291534
val badge: @Composable (() -> Unit)? = when {
1535+
isIncompatibleSdk -> ({
1536+
InfoBadge(
1537+
text = stringResource(R.string.home_apk_availability_incompatible_label),
1538+
style = InfoBadgeStyle.Error,
1539+
isCompact = true
1540+
)
1541+
})
14301542
isExperimentalVersion -> ({
14311543
InfoBadge(
14321544
text = stringResource(R.string.home_dialog_unsupported_version_experimental_label),
@@ -1452,7 +1564,9 @@ private fun VersionListCard(
14521564
}
14531565

14541566
Column(
1455-
modifier = Modifier.fillMaxWidth(),
1567+
modifier = Modifier
1568+
.fillMaxWidth()
1569+
.then(if (isIncompatibleSdk) Modifier.alpha(0.4f) else Modifier),
14561570
verticalArrangement = Arrangement.spacedBy(3.dp)
14571571
) {
14581572
// Version + optional badge inline

0 commit comments

Comments
 (0)