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.androidx.fragment)
implementation(libs.hiddenApiBypass) implementation(libs.hiddenApiBypass)
implementation(libs.libsu) implementation(libs.libsu)
implementation(libs.reoderable)
implementation(libs.serialization) implementation(libs.serialization)
implementation(kotlin("reflect")) implementation(kotlin("reflect"))
} }

View File

@@ -3,6 +3,7 @@ package com.bintianqi.owndroid
import android.content.Context import android.content.Context
import android.hardware.biometrics.BiometricPrompt import android.hardware.biometrics.BiometricPrompt
import android.hardware.biometrics.BiometricPrompt.AuthenticationCallback import android.hardware.biometrics.BiometricPrompt.AuthenticationCallback
import android.hardware.fingerprint.FingerprintManager
import android.os.Build import android.os.Build
import android.os.CancellationSignal import android.os.CancellationSignal
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
@@ -92,6 +93,7 @@ fun AppLockDialog(onSucceed: () -> Unit, onDismiss: () -> Unit) = Dialog(onDismi
@RequiresApi(28) @RequiresApi(28)
fun startBiometricsUnlock(context: Context, onSucceed: () -> Unit) { fun startBiometricsUnlock(context: Context, onSucceed: () -> Unit) {
context.getSystemService(FingerprintManager::class.java) ?: return
val callback = object : AuthenticationCallback() { val callback = object : AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult?) { override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult?) {
super.onAuthenticationSucceeded(result) 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.LazyItemScope
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
@@ -118,6 +119,8 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState
val String.isValidPackageName val String.isValidPackageName
get() = Regex("""^(?:[a-zA-Z]\w*\.)+[a-zA-Z]\w*$""").matches(this) get() = Regex("""^(?:[a-zA-Z]\w*\.)+[a-zA-Z]\w*$""").matches(this)
@@ -265,7 +268,7 @@ fun ApplicationDetailsScreen(
val appRestrictions by vm.appRestrictions.collectAsStateWithLifecycle() val appRestrictions by vm.appRestrictions.collectAsStateWithLifecycle()
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
vm.getAppStatus(packageName) vm.getAppStatus(packageName)
vm.getAppRestrictions(packageName) if (VERSION.SDK_INT >= 23) vm.getAppRestrictions(packageName)
} }
MySmallTitleScaffold(R.string.place_holder, onNavigateUp, 0.dp) { MySmallTitleScaffold(R.string.place_holder, onNavigateUp, 0.dp) {
Column(Modifier.align(Alignment.CenterHorizontally).padding(top = 16.dp), horizontalAlignment = Alignment.CenterHorizontally) { 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.StringItem -> entry.value?.take(30)
is AppRestriction.BooleanItem -> entry.value?.toString() is AppRestriction.BooleanItem -> entry.value?.toString()
is AppRestriction.ChoiceItem -> entry.value is AppRestriction.ChoiceItem -> entry.value
is AppRestriction.MultiSelectItem -> { is AppRestriction.MultiSelectItem -> entry.value?.joinToString(limit = 30)
if (entry.value != null) {
entry.entryValues
.filter { entry.value?.contains(it) ?: false }
.joinToString(limit = 30)
} else null
}
} }
Text( Text(
text ?: "null", Modifier.alpha(0.7F), text ?: "null", Modifier.alpha(0.7F),
@@ -1131,7 +1128,6 @@ fun ManagedConfigurationScreen(
shape = AlertDialogDefaults.shape, shape = AlertDialogDefaults.shape,
tonalElevation = AlertDialogDefaults.TonalElevation, tonalElevation = AlertDialogDefaults.TonalElevation,
) { ) {
Column(Modifier.verticalScroll(rememberScrollState()).padding(12.dp)) {
ManagedConfigurationDialog(dialog!!) { ManagedConfigurationDialog(dialog!!) {
if (it != null) { if (it != null) {
setRestriction(params.packageName, it) setRestriction(params.packageName, it)
@@ -1140,7 +1136,6 @@ fun ManagedConfigurationScreen(
} }
} }
} }
}
if (clearRestrictionDialog) AlertDialog( if (clearRestrictionDialog) AlertDialog(
text = { text = {
Text(stringResource(R.string.clear_configurations)) Text(stringResource(R.string.clear_configurations))
@@ -1167,13 +1162,24 @@ fun ManagedConfigurationScreen(
} }
@Composable @Composable
fun ColumnScope.ManagedConfigurationDialog( fun ManagedConfigurationDialog(
restriction: AppRestriction, setRestriction: (AppRestriction?) -> Unit restriction: AppRestriction, setRestriction: (AppRestriction?) -> Unit
) { ) {
var specifyValue by remember { mutableStateOf(false) } var specifyValue by remember { mutableStateOf(false) }
var input by remember { mutableStateOf("") } var input by remember { mutableStateOf("") }
var inputState by remember { mutableStateOf(false) } 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) { LaunchedEffect(Unit) {
when (restriction) { when (restriction) {
is AppRestriction.IntItem -> restriction.value?.let { is AppRestriction.IntItem -> restriction.value?.let {
@@ -1193,11 +1199,17 @@ fun ColumnScope.ManagedConfigurationDialog(
specifyValue = true specifyValue = true
} }
is AppRestriction.MultiSelectItem -> restriction.value?.let { is AppRestriction.MultiSelectItem -> restriction.value?.let {
inputSelections.addAll(it)
specifyValue = true 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 { SelectionContainer {
Column { Column {
restriction.title?.let { restriction.title?.let {
@@ -1218,23 +1230,23 @@ fun ColumnScope.ManagedConfigurationDialog(
Text(stringResource(R.string.specify_value)) Text(stringResource(R.string.specify_value))
Switch(specifyValue, { specifyValue = it }) Switch(specifyValue, { specifyValue = it })
} }
}
if (specifyValue) when (restriction) { if (specifyValue) when (restriction) {
is AppRestriction.IntItem -> { is AppRestriction.IntItem -> item {
OutlinedTextField( OutlinedTextField(
input, { input = it }, Modifier.fillMaxWidth(), input, { input = it }, Modifier.fillMaxWidth(),
isError = input.toIntOrNull() == null isError = input.toIntOrNull() == null
) )
} }
is AppRestriction.StringItem -> { is AppRestriction.StringItem -> item {
OutlinedTextField( OutlinedTextField(
input, { input = it }, Modifier.fillMaxWidth() input, { input = it }, Modifier.fillMaxWidth()
) )
} }
is AppRestriction.BooleanItem -> { is AppRestriction.BooleanItem -> item {
Switch(inputState, { inputState = it }) Switch(inputState, { inputState = it })
} }
is AppRestriction.ChoiceItem -> { is AppRestriction.ChoiceItem -> itemsIndexed(restriction.entryValues) { index, value ->
restriction.entryValues.forEachIndexed { index, value ->
val label = restriction.entries.getOrNull(index) val label = restriction.entries.getOrNull(index)
Row( Row(
Modifier.fillMaxWidth().clickable { Modifier.fillMaxWidth().clickable {
@@ -1253,32 +1265,39 @@ fun ColumnScope.ManagedConfigurationDialog(
} }
} }
} }
} is AppRestriction.MultiSelectItem -> itemsIndexed(
is AppRestriction.MultiSelectItem -> { multiSelectList, { _, v -> v.value }
restriction.entryValues.forEachIndexed { index, value -> ) { index, entry ->
val label = restriction.entries.getOrNull(index) ReorderableItem(reorderableListState, entry.value) {
Row( Row(
Modifier.fillMaxWidth().clickable { Modifier.fillMaxWidth().clickable {
if (value in inputSelections) val old = multiSelectList[index]
inputSelections -= value else inputSelections += value multiSelectList[index] = old.copy(selected = !old.selected)
}.padding(8.dp, 4.dp), }.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)) Spacer(Modifier.width(8.dp))
if (label == null) { if (entry.title == null) {
Text(value) Text(entry.value)
} else { } else {
Column { Column {
Text(label) Text(entry.title)
Text(value, Modifier.alpha(0.7F), style = typography.bodyMedium) Text(entry.value, Modifier.alpha(0.7F), style = typography.bodyMedium)
}
}
}
Icon(
painterResource(R.drawable.drag_indicator_fill0), null,
Modifier.draggableHandle()
)
} }
} }
} }
} }
} item {
} Row(Modifier.fillMaxWidth().padding(top = 4.dp), Arrangement.End) {
Row(Modifier.align(Alignment.End).padding(top = 4.dp)) {
TextButton({ TextButton({
setRestriction(null) setRestriction(null)
}, Modifier.padding(end = 4.dp)) { }, Modifier.padding(end = 4.dp)) {
@@ -1299,7 +1318,10 @@ fun ColumnScope.ManagedConfigurationDialog(
value = if (specifyValue) input else null value = if (specifyValue) input else null
) )
is AppRestriction.MultiSelectItem -> restriction.copy( 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) setRestriction(newRestriction)
@@ -1308,6 +1330,8 @@ fun ColumnScope.ManagedConfigurationDialog(
} }
} }
} }
}
}
sealed class AppRestriction( sealed class AppRestriction(
open val key: String, open val title: String?, open val description: String? open val key: String, open val title: String?, open val description: String?
@@ -1347,3 +1371,5 @@ sealed class AppRestriction(
var value: Array<String>? var value: Array<String>?
) : AppRestriction(key, title, description) ) : 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" dhizuku-server = "0.0.10"
hiddenApiBypass = "6.1" hiddenApiBypass = "6.1"
libsu = "6.0.0" libsu = "6.0.0"
reoderable = "3.0.0"
serialization = "1.9.0" serialization = "1.9.0"
[libraries] [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" } dhizuku-server-api = { group = "io.github.iamr0s", name = "Dhizuku-SERVER_API", version.ref = "dhizuku-server" }
hiddenApiBypass = { module = "org.lsposed.hiddenapibypass:hiddenapibypass", version.ref = "hiddenApiBypass" } hiddenApiBypass = { module = "org.lsposed.hiddenapibypass:hiddenapibypass", version.ref = "hiddenApiBypass" }
libsu = { module = "com.github.topjohnwu.libsu:core", version.ref = "libsu" } 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" } serialization = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" }