From c8c428e92915b0b3a6b78fbcdf793a29b5368814 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Tue, 25 Nov 2025 23:22:57 +0800 Subject: [PATCH] Items ordering in managed configurations Check FingerprintManager exist before startBiometricsUnlock(), fix #200, #201, #202 --- app/build.gradle.kts | 1 + .../java/com/bintianqi/owndroid/AppLock.kt | 2 + .../bintianqi/owndroid/dpm/Applications.kt | 222 ++++++++++-------- .../res/drawable/drag_indicator_fill0.xml | 9 + gradle/libs.versions.toml | 2 + 5 files changed, 138 insertions(+), 98 deletions(-) create mode 100644 app/src/main/res/drawable/drag_indicator_fill0.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 84c2009..f6a280d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -103,6 +103,7 @@ dependencies { implementation(libs.androidx.fragment) implementation(libs.hiddenApiBypass) implementation(libs.libsu) + implementation(libs.reoderable) implementation(libs.serialization) implementation(kotlin("reflect")) } \ No newline at end of file diff --git a/app/src/main/java/com/bintianqi/owndroid/AppLock.kt b/app/src/main/java/com/bintianqi/owndroid/AppLock.kt index 820c554..9c64c20 100644 --- a/app/src/main/java/com/bintianqi/owndroid/AppLock.kt +++ b/app/src/main/java/com/bintianqi/owndroid/AppLock.kt @@ -3,6 +3,7 @@ package com.bintianqi.owndroid import android.content.Context import android.hardware.biometrics.BiometricPrompt import android.hardware.biometrics.BiometricPrompt.AuthenticationCallback +import android.hardware.fingerprint.FingerprintManager import android.os.Build import android.os.CancellationSignal import androidx.activity.compose.BackHandler @@ -92,6 +93,7 @@ fun AppLockDialog(onSucceed: () -> Unit, onDismiss: () -> Unit) = Dialog(onDismi @RequiresApi(28) fun startBiometricsUnlock(context: Context, onSucceed: () -> Unit) { + context.getSystemService(FingerprintManager::class.java) ?: return val callback = object : AuthenticationCallback() { override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult?) { super.onAuthenticationSucceeded(result) diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt index 201b66b..e5807e5 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt @@ -27,6 +27,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions @@ -118,6 +119,8 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.serialization.Serializable +import sh.calvin.reorderable.ReorderableItem +import sh.calvin.reorderable.rememberReorderableLazyListState val String.isValidPackageName get() = Regex("""^(?:[a-zA-Z]\w*\.)+[a-zA-Z]\w*$""").matches(this) @@ -265,7 +268,7 @@ fun ApplicationDetailsScreen( val appRestrictions by vm.appRestrictions.collectAsStateWithLifecycle() LaunchedEffect(Unit) { vm.getAppStatus(packageName) - vm.getAppRestrictions(packageName) + if (VERSION.SDK_INT >= 23) vm.getAppRestrictions(packageName) } MySmallTitleScaffold(R.string.place_holder, onNavigateUp, 0.dp) { Column(Modifier.align(Alignment.CenterHorizontally).padding(top = 16.dp), horizontalAlignment = Alignment.CenterHorizontally) { @@ -1102,13 +1105,7 @@ fun ManagedConfigurationScreen( is AppRestriction.StringItem -> entry.value?.take(30) is AppRestriction.BooleanItem -> entry.value?.toString() is AppRestriction.ChoiceItem -> entry.value - is AppRestriction.MultiSelectItem -> { - if (entry.value != null) { - entry.entryValues - .filter { entry.value?.contains(it) ?: false } - .joinToString(limit = 30) - } else null - } + is AppRestriction.MultiSelectItem -> entry.value?.joinToString(limit = 30) } Text( text ?: "null", Modifier.alpha(0.7F), @@ -1131,13 +1128,11 @@ fun ManagedConfigurationScreen( shape = AlertDialogDefaults.shape, tonalElevation = AlertDialogDefaults.TonalElevation, ) { - Column(Modifier.verticalScroll(rememberScrollState()).padding(12.dp)) { - ManagedConfigurationDialog(dialog!!) { - if (it != null) { - setRestriction(params.packageName, it) - } - dialog = null + ManagedConfigurationDialog(dialog!!) { + if (it != null) { + setRestriction(params.packageName, it) } + dialog = null } } } @@ -1167,13 +1162,24 @@ fun ManagedConfigurationScreen( } @Composable -fun ColumnScope.ManagedConfigurationDialog( +fun ManagedConfigurationDialog( restriction: AppRestriction, setRestriction: (AppRestriction?) -> Unit ) { var specifyValue by remember { mutableStateOf(false) } var input by remember { mutableStateOf("") } var inputState by remember { mutableStateOf(false) } - val inputSelections = remember { mutableStateListOf() } + val multiSelectList = remember { + mutableStateListOf( + *(if (restriction is AppRestriction.MultiSelectItem) { + restriction.entryValues.mapIndexed { index, value -> + MultiSelectEntry( + value, restriction.entries.getOrNull(index), + restriction.value?.contains(value) ?: false + ) + } + } else emptyList()).toTypedArray() + ) + } LaunchedEffect(Unit) { when (restriction) { is AppRestriction.IntItem -> restriction.value?.let { @@ -1193,48 +1199,54 @@ fun ColumnScope.ManagedConfigurationDialog( specifyValue = true } is AppRestriction.MultiSelectItem -> restriction.value?.let { - inputSelections.addAll(it) specifyValue = true } } } - SelectionContainer { - Column { - restriction.title?.let { - Text(it, style = typography.titleLarge) - } - Text(restriction.key, Modifier.padding(vertical = 4.dp), style = typography.labelLarge) - Spacer(Modifier.height(4.dp)) - restriction.description?.let { - Text(it, Modifier.alpha(0.8F), style = typography.bodyMedium) - } - Spacer(Modifier.height(8.dp)) - } + val listState = rememberLazyListState() + val reorderableListState = rememberReorderableLazyListState(listState) { from, to -> + // `-1` because there's an `item` before items + multiSelectList.add(from.index - 1, multiSelectList.removeAt(to.index - 1)) } - Row( - Modifier.fillMaxWidth().padding(bottom = 4.dp), - Arrangement.SpaceBetween, Alignment.CenterVertically - ) { - Text(stringResource(R.string.specify_value)) - Switch(specifyValue, { specifyValue = it }) - } - if (specifyValue) when (restriction) { - is AppRestriction.IntItem -> { - OutlinedTextField( - input, { input = it }, Modifier.fillMaxWidth(), - isError = input.toIntOrNull() == null - ) + LazyColumn(Modifier.padding(12.dp), listState) { + item { + SelectionContainer { + Column { + restriction.title?.let { + Text(it, style = typography.titleLarge) + } + Text(restriction.key, Modifier.padding(vertical = 4.dp), style = typography.labelLarge) + Spacer(Modifier.height(4.dp)) + restriction.description?.let { + Text(it, Modifier.alpha(0.8F), style = typography.bodyMedium) + } + Spacer(Modifier.height(8.dp)) + } + } + Row( + Modifier.fillMaxWidth().padding(bottom = 4.dp), + Arrangement.SpaceBetween, Alignment.CenterVertically + ) { + Text(stringResource(R.string.specify_value)) + Switch(specifyValue, { specifyValue = it }) + } } - is AppRestriction.StringItem -> { - OutlinedTextField( - input, { input = it }, Modifier.fillMaxWidth() - ) - } - is AppRestriction.BooleanItem -> { - Switch(inputState, { inputState = it }) - } - is AppRestriction.ChoiceItem -> { - restriction.entryValues.forEachIndexed { index, value -> + if (specifyValue) when (restriction) { + is AppRestriction.IntItem -> item { + OutlinedTextField( + input, { input = it }, Modifier.fillMaxWidth(), + isError = input.toIntOrNull() == null + ) + } + is AppRestriction.StringItem -> item { + OutlinedTextField( + input, { input = it }, Modifier.fillMaxWidth() + ) + } + is AppRestriction.BooleanItem -> item { + Switch(inputState, { inputState = it }) + } + is AppRestriction.ChoiceItem -> itemsIndexed(restriction.entryValues) { index, value -> val label = restriction.entries.getOrNull(index) Row( Modifier.fillMaxWidth().clickable { @@ -1253,58 +1265,70 @@ fun ColumnScope.ManagedConfigurationDialog( } } } - } - is AppRestriction.MultiSelectItem -> { - restriction.entryValues.forEachIndexed { index, value -> - val label = restriction.entries.getOrNull(index) - Row( - Modifier.fillMaxWidth().clickable { - if (value in inputSelections) - inputSelections -= value else inputSelections += value - }.padding(8.dp, 4.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Checkbox(value in inputSelections, null) - Spacer(Modifier.width(8.dp)) - if (label == null) { - Text(value) - } else { - Column { - Text(label) - Text(value, Modifier.alpha(0.7F), style = typography.bodyMedium) + is AppRestriction.MultiSelectItem -> itemsIndexed( + multiSelectList, { _, v -> v.value } + ) { index, entry -> + ReorderableItem(reorderableListState, entry.value) { + Row( + Modifier.fillMaxWidth().clickable { + val old = multiSelectList[index] + multiSelectList[index] = old.copy(selected = !old.selected) + }.padding(8.dp, 4.dp), + Arrangement.SpaceBetween, Alignment.CenterVertically + ) { + Row(Modifier.weight(1F), verticalAlignment = Alignment.CenterVertically) { + Checkbox(entry.selected, null) + Spacer(Modifier.width(8.dp)) + if (entry.title == null) { + Text(entry.value) + } else { + Column { + Text(entry.title) + Text(entry.value, Modifier.alpha(0.7F), style = typography.bodyMedium) + } + } } + Icon( + painterResource(R.drawable.drag_indicator_fill0), null, + Modifier.draggableHandle() + ) } } } } - } - Row(Modifier.align(Alignment.End).padding(top = 4.dp)) { - TextButton({ - setRestriction(null) - }, Modifier.padding(end = 4.dp)) { - Text(stringResource(R.string.cancel)) - } - TextButton({ - val newRestriction = when (restriction) { - is AppRestriction.IntItem -> restriction.copy( - value = if (specifyValue) input.toIntOrNull() else null - ) - is AppRestriction.StringItem -> restriction.copy( - value = if (specifyValue) input else null - ) - is AppRestriction.BooleanItem -> restriction.copy( - value = if (specifyValue) inputState else null - ) - is AppRestriction.ChoiceItem -> restriction.copy( - value = if (specifyValue) input else null - ) - is AppRestriction.MultiSelectItem -> restriction.copy( - value = if (specifyValue) inputSelections.toTypedArray() else null - ) + item { + Row(Modifier.fillMaxWidth().padding(top = 4.dp), Arrangement.End) { + TextButton({ + setRestriction(null) + }, Modifier.padding(end = 4.dp)) { + Text(stringResource(R.string.cancel)) + } + TextButton({ + val newRestriction = when (restriction) { + is AppRestriction.IntItem -> restriction.copy( + value = if (specifyValue) input.toIntOrNull() else null + ) + is AppRestriction.StringItem -> restriction.copy( + value = if (specifyValue) input else null + ) + is AppRestriction.BooleanItem -> restriction.copy( + value = if (specifyValue) inputState else null + ) + is AppRestriction.ChoiceItem -> restriction.copy( + value = if (specifyValue) input else null + ) + is AppRestriction.MultiSelectItem -> restriction.copy( + value = if (specifyValue) + multiSelectList.filter { it.selected } + .map { it.value }.toTypedArray() + else null + ) + } + setRestriction(newRestriction) + }) { + Text(stringResource(R.string.confirm)) + } } - setRestriction(newRestriction) - }) { - Text(stringResource(R.string.confirm)) } } } @@ -1347,3 +1371,5 @@ sealed class AppRestriction( var value: Array? ) : AppRestriction(key, title, description) } + +data class MultiSelectEntry(val value: String, val title: String?, val selected: Boolean) diff --git a/app/src/main/res/drawable/drag_indicator_fill0.xml b/app/src/main/res/drawable/drag_indicator_fill0.xml new file mode 100644 index 0000000..3719a6b --- /dev/null +++ b/app/src/main/res/drawable/drag_indicator_fill0.xml @@ -0,0 +1,9 @@ + + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1a68b36..1090da1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,6 +12,7 @@ dhizuku = "2.5.4" dhizuku-server = "0.0.10" hiddenApiBypass = "6.1" libsu = "6.0.0" +reoderable = "3.0.0" serialization = "1.9.0" [libraries] @@ -33,6 +34,7 @@ dhizuku-api = { module = "io.github.iamr0s:Dhizuku-API", version.ref = "dhizuku" dhizuku-server-api = { group = "io.github.iamr0s", name = "Dhizuku-SERVER_API", version.ref = "dhizuku-server" } hiddenApiBypass = { module = "org.lsposed.hiddenapibypass:hiddenapibypass", version.ref = "hiddenApiBypass" } libsu = { module = "com.github.topjohnwu.libsu:core", version.ref = "libsu" } +reoderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reoderable" } serialization = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" }