Items ordering in managed configurations

Check FingerprintManager exist before startBiometricsUnlock(),
fix #200, #201, #202
This commit is contained in:
BinTianqi
2025-11-25 23:22:57 +08:00
parent b242488a2a
commit c8c428e929
5 changed files with 138 additions and 98 deletions

View File

@@ -103,6 +103,7 @@ dependencies {
implementation(libs.androidx.fragment)
implementation(libs.hiddenApiBypass)
implementation(libs.libsu)
implementation(libs.reoderable)
implementation(libs.serialization)
implementation(kotlin("reflect"))
}

View File

@@ -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)

View File

@@ -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,7 +1128,6 @@ fun ManagedConfigurationScreen(
shape = AlertDialogDefaults.shape,
tonalElevation = AlertDialogDefaults.TonalElevation,
) {
Column(Modifier.verticalScroll(rememberScrollState()).padding(12.dp)) {
ManagedConfigurationDialog(dialog!!) {
if (it != null) {
setRestriction(params.packageName, it)
@@ -1140,7 +1136,6 @@ fun ManagedConfigurationScreen(
}
}
}
}
if (clearRestrictionDialog) AlertDialog(
text = {
Text(stringResource(R.string.clear_configurations))
@@ -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<String>() }
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,11 +1199,17 @@ fun ColumnScope.ManagedConfigurationDialog(
specifyValue = true
}
is AppRestriction.MultiSelectItem -> restriction.value?.let {
inputSelections.addAll(it)
specifyValue = true
}
}
}
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))
}
LazyColumn(Modifier.padding(12.dp), listState) {
item {
SelectionContainer {
Column {
restriction.title?.let {
@@ -1218,23 +1230,23 @@ fun ColumnScope.ManagedConfigurationDialog(
Text(stringResource(R.string.specify_value))
Switch(specifyValue, { specifyValue = it })
}
}
if (specifyValue) when (restriction) {
is AppRestriction.IntItem -> {
is AppRestriction.IntItem -> item {
OutlinedTextField(
input, { input = it }, Modifier.fillMaxWidth(),
isError = input.toIntOrNull() == null
)
}
is AppRestriction.StringItem -> {
is AppRestriction.StringItem -> item {
OutlinedTextField(
input, { input = it }, Modifier.fillMaxWidth()
)
}
is AppRestriction.BooleanItem -> {
is AppRestriction.BooleanItem -> item {
Switch(inputState, { inputState = it })
}
is AppRestriction.ChoiceItem -> {
restriction.entryValues.forEachIndexed { index, value ->
is AppRestriction.ChoiceItem -> itemsIndexed(restriction.entryValues) { index, value ->
val label = restriction.entries.getOrNull(index)
Row(
Modifier.fillMaxWidth().clickable {
@@ -1253,32 +1265,39 @@ fun ColumnScope.ManagedConfigurationDialog(
}
}
}
}
is AppRestriction.MultiSelectItem -> {
restriction.entryValues.forEachIndexed { index, value ->
val label = restriction.entries.getOrNull(index)
is AppRestriction.MultiSelectItem -> itemsIndexed(
multiSelectList, { _, v -> v.value }
) { index, entry ->
ReorderableItem(reorderableListState, entry.value) {
Row(
Modifier.fillMaxWidth().clickable {
if (value in inputSelections)
inputSelections -= value else inputSelections += value
val old = multiSelectList[index]
multiSelectList[index] = old.copy(selected = !old.selected)
}.padding(8.dp, 4.dp),
verticalAlignment = Alignment.CenterVertically
Arrangement.SpaceBetween, Alignment.CenterVertically
) {
Checkbox(value in inputSelections, null)
Row(Modifier.weight(1F), verticalAlignment = Alignment.CenterVertically) {
Checkbox(entry.selected, null)
Spacer(Modifier.width(8.dp))
if (label == null) {
Text(value)
if (entry.title == null) {
Text(entry.value)
} else {
Column {
Text(label)
Text(value, Modifier.alpha(0.7F), style = typography.bodyMedium)
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)) {
item {
Row(Modifier.fillMaxWidth().padding(top = 4.dp), Arrangement.End) {
TextButton({
setRestriction(null)
}, Modifier.padding(end = 4.dp)) {
@@ -1299,7 +1318,10 @@ fun ColumnScope.ManagedConfigurationDialog(
value = if (specifyValue) input else null
)
is AppRestriction.MultiSelectItem -> restriction.copy(
value = if (specifyValue) inputSelections.toTypedArray() else null
value = if (specifyValue)
multiSelectList.filter { it.selected }
.map { it.value }.toTypedArray()
else null
)
}
setRestriction(newRestriction)
@@ -1308,6 +1330,8 @@ fun ColumnScope.ManagedConfigurationDialog(
}
}
}
}
}
sealed class AppRestriction(
open val key: String, open val title: String?, open val description: String?
@@ -1347,3 +1371,5 @@ sealed class AppRestriction(
var value: Array<String>?
) : AppRestriction(key, title, description)
}
data class MultiSelectEntry(val value: String, val title: String?, val selected: Boolean)

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M360,800q-33,0 -56.5,-23.5T280,720q0,-33 23.5,-56.5T360,640q33,0 56.5,23.5T440,720q0,33 -23.5,56.5T360,800ZM600,800q-33,0 -56.5,-23.5T520,720q0,-33 23.5,-56.5T600,640q33,0 56.5,23.5T680,720q0,33 -23.5,56.5T600,800ZM360,560q-33,0 -56.5,-23.5T280,480q0,-33 23.5,-56.5T360,400q33,0 56.5,23.5T440,480q0,33 -23.5,56.5T360,560ZM600,560q-33,0 -56.5,-23.5T520,480q0,-33 23.5,-56.5T600,400q33,0 56.5,23.5T680,480q0,33 -23.5,56.5T600,560ZM360,320q-33,0 -56.5,-23.5T280,240q0,-33 23.5,-56.5T360,160q33,0 56.5,23.5T440,240q0,33 -23.5,56.5T360,320ZM600,320q-33,0 -56.5,-23.5T520,240q0,-33 23.5,-56.5T600,160q33,0 56.5,23.5T680,240q0,33 -23.5,56.5T600,320Z"
android:fillColor="#000000"/>
</vector>

View File

@@ -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" }