From b547c8add8db59482b4496f3615dce0eccab8d44 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Sat, 17 May 2025 14:03:17 +0800 Subject: [PATCH] Improve UI --- app/build.gradle.kts | 8 +- .../com/bintianqi/owndroid/MainActivity.kt | 35 ++----- .../com/bintianqi/owndroid/MyViewModel.kt | 2 - .../java/com/bintianqi/owndroid/Settings.kt | 93 ++++++++++++++----- .../java/com/bintianqi/owndroid/dpm/DPM.kt | 7 +- .../com/bintianqi/owndroid/dpm/Permissions.kt | 3 +- .../bintianqi/owndroid/dpm/UserRestriction.kt | 28 +++++- .../com/bintianqi/owndroid/ui/theme/Theme.kt | 2 +- gradle/libs.versions.toml | 2 +- 9 files changed, 112 insertions(+), 68 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3c63d89..1b571d6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -38,10 +38,6 @@ android { "proguard-rules.pro" ) signingConfig = signingConfigs.getByName("defaultSignature") - composeCompiler { - includeSourceInformation = false - includeTraceMarkers = false - } } debug { signingConfig = signingConfigs.getByName("defaultSignature") @@ -64,6 +60,10 @@ android { dependenciesInfo { includeInApk = false } + composeCompiler { + includeSourceInformation = false + includeTraceMarkers = false + } } kotlin { diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt index b9305d6..96173e6 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -1,6 +1,5 @@ package com.bintianqi.owndroid -import android.annotation.SuppressLint import android.os.Build.VERSION import android.os.Bundle import android.widget.Toast @@ -237,13 +236,11 @@ import com.bintianqi.owndroid.ui.Animations import com.bintianqi.owndroid.ui.theme.OwnDroidTheme import com.rosan.dhizuku.api.Dhizuku import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import kotlinx.serialization.Serializable import org.lsposed.hiddenapibypass.HiddenApiBypass import java.util.Locale -val backToHomeStateFlow = MutableStateFlow(false) @ExperimentalMaterial3Api class MainActivity : FragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -291,12 +288,7 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) { val context = LocalContext.current val receiver = context.getReceiver() val focusMgr = LocalFocusManager.current - val backToHome by backToHomeStateFlow.collectAsState() val lifecycleOwner = LocalLifecycleOwner.current - LaunchedEffect(backToHome) { - if(backToHome) { navController.navigateUp(); backToHomeStateFlow.value = false } - } - val userRestrictions by vm.userRestrictions.collectAsStateWithLifecycle() fun navigateUp() { navController.navigateUp() } fun navigate(destination: Any) { navController.navigate(destination) } LaunchedEffect(Unit) { @@ -320,7 +312,7 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) { popEnterTransition = Animations.navHostPopEnterTransition, popExitTransition = Animations.navHostPopExitTransition ) { - composable { HomeScreen { navController.navigate(it) } } + composable { HomeScreen(::navigate) } composable { WorkModesScreen(it.toRoute(), ::navigateUp, { navController.navigate(Home) { @@ -330,9 +322,7 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) { navController.navigate(WorkModes(false)) { popUpTo { inclusive = true } } - }, { - navController.navigate(it) - }) + }, ::navigate) } composable { DelegatedAdminsScreen(::navigateUp, ::navigate) } @@ -369,14 +359,14 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) { composable { WipeDataScreen(::navigateUp) } composable { NetworkScreen(::navigateUp, ::navigate) } - composable { WifiScreen(::navigateUp, { navController.navigate(it) }) { navController.navigate(AddNetwork, it)} } + composable { WifiScreen(::navigateUp, ::navigate) { navController.navigate(AddNetwork, it)} } composable { NetworkOptionsScreen(::navigateUp) } composable { AddNetworkScreen(it.arguments!!, ::navigateUp) } composable { WifiSecurityLevelScreen(::navigateUp) } composable { WifiSsidPolicyScreen(::navigateUp) } composable { NetworkStatsScreen(::navigateUp, ::navigate) } composable(mapOf(serializableNavTypePair>())) { - NetworkStatsViewerScreen(it.toRoute()) { navController.navigateUp() } + NetworkStatsViewerScreen(it.toRoute(), ::navigateUp) } composable { PrivateDnsScreen(::navigateUp) } composable { AlwaysOnVpnPackageScreen(::navigateUp) } @@ -433,25 +423,12 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) { composable { SetDefaultDialerScreen(::navigateUp) } composable { - LaunchedEffect(Unit) { - vm.userRestrictions.value = context.getDPM().getUserRestrictions(receiver) - } UserRestrictionScreen(::navigateUp) { title, items -> - navController.navigate(UserRestrictionOptions(title, items)) + navigate(UserRestrictionOptions(title, items)) } } composable(mapOf(serializableNavTypePair>())) { - UserRestrictionOptionsScreen(it.toRoute(), userRestrictions, ::navigateUp) { id, status -> - try { - val dpm = context.getDPM() - if(status) dpm.addUserRestriction(receiver, id) - else dpm.clearUserRestriction(receiver, id) - @SuppressLint("NewApi") - vm.userRestrictions.value = dpm.getUserRestrictions(receiver) - } catch(_: Exception) { - context.showOperationResultToast(false) - } - } + UserRestrictionOptionsScreen(it.toRoute(), ::navigateUp) } composable { UsersScreen(::navigateUp, ::navigate) } diff --git a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt index f4f0eea..6b639a6 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt @@ -1,7 +1,6 @@ package com.bintianqi.owndroid import android.app.Application -import android.os.Bundle import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableStateFlow @@ -9,7 +8,6 @@ import kotlinx.coroutines.launch class MyViewModel(application: Application): AndroidViewModel(application) { val theme = MutableStateFlow(ThemeSettings()) - val userRestrictions = MutableStateFlow(Bundle()) init { val sp = SharedPrefs(application) diff --git a/app/src/main/java/com/bintianqi/owndroid/Settings.kt b/app/src/main/java/com/bintianqi/owndroid/Settings.kt index 5693d55..12c51f6 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Settings.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Settings.kt @@ -12,20 +12,29 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.Button import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold import androidx.compose.material3.Switch import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -35,6 +44,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.painterResource @@ -49,6 +59,7 @@ import androidx.core.net.toUri import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.bintianqi.owndroid.ui.FunctionItem import com.bintianqi.owndroid.ui.MyScaffold +import com.bintianqi.owndroid.ui.NavIcon import com.bintianqi.owndroid.ui.Notes import com.bintianqi.owndroid.ui.SwitchItem import kotlinx.serialization.Serializable @@ -59,6 +70,7 @@ import java.util.Locale @Serializable object Settings +@OptIn(ExperimentalMaterial3Api::class) @Composable fun SettingsScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) { val context = LocalContext.current @@ -66,19 +78,55 @@ fun SettingsScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) { val exportLogsLauncher = rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("text/plain")) { if(it != null) exportLogs(context, it) } - MyScaffold(R.string.settings, onNavigateUp, 0.dp) { - FunctionItem(title = R.string.options, icon = R.drawable.tune_fill0) { onNavigate(SettingsOptions) } - FunctionItem(title = R.string.appearance, icon = R.drawable.format_paint_fill0) { onNavigate(Appearance) } - FunctionItem(R.string.app_lock, icon = R.drawable.lock_fill0) { onNavigate(AppLockSettings) } - if (privilege.device || privilege.profile) - FunctionItem(title = R.string.api, icon = R.drawable.code_fill0) { onNavigate(ApiSettings) } - if (privilege.device && !privilege.dhizuku) - FunctionItem(R.string.notifications, icon = R.drawable.notifications_fill0) { onNavigate(Notifications) } - FunctionItem(title = R.string.export_logs, icon = R.drawable.description_fill0) { - val time = SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault()).format(Date(System.currentTimeMillis())) - exportLogsLauncher.launch("owndroid_log_$time") + val sb = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + var dropdown by remember { mutableStateOf(false) } + Scaffold( + Modifier.nestedScroll(sb.nestedScrollConnection), + topBar = { + LargeTopAppBar( + { Text(stringResource(R.string.settings)) }, + navigationIcon = { NavIcon(onNavigateUp) }, + scrollBehavior = sb, + actions = { + Box { + IconButton({ dropdown = true }) { + Icon(Icons.Default.MoreVert, null) + } + DropdownMenu(dropdown, { dropdown = false }) { + DropdownMenuItem( + { Text(stringResource(R.string.export_logs)) }, + { + dropdown = false + val time = SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault()) + .format(Date(System.currentTimeMillis())) + exportLogsLauncher.launch("owndroid_log_$time") + }, + leadingIcon = { + Icon(painterResource(R.drawable.description_fill0), null) + } + ) + } + } + } + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + .padding(bottom = 80.dp) + ) { + FunctionItem(title = R.string.options, icon = R.drawable.tune_fill0) { onNavigate(SettingsOptions) } + FunctionItem(title = R.string.appearance, icon = R.drawable.format_paint_fill0) { onNavigate(Appearance) } + FunctionItem(R.string.app_lock, icon = R.drawable.lock_fill0) { onNavigate(AppLockSettings) } + if (privilege.device || privilege.profile) + FunctionItem(title = R.string.api, icon = R.drawable.code_fill0) { onNavigate(ApiSettings) } + if (privilege.device && !privilege.dhizuku) + FunctionItem(R.string.notifications, icon = R.drawable.notifications_fill0) { onNavigate(Notifications) } + FunctionItem(title = R.string.about, icon = R.drawable.info_fill0) { onNavigate(About) } } - FunctionItem(title = R.string.about, icon = R.drawable.info_fill0) { onNavigate(About) } } } @@ -111,6 +159,10 @@ fun SettingsOptionsScreen(onNavigateUp: () -> Unit) { fun AppearanceScreen(onNavigateUp: () -> Unit, currentTheme: ThemeSettings, onThemeChange: (ThemeSettings) -> Unit) { var darkThemeMenu by remember { mutableStateOf(false) } var theme by remember { mutableStateOf(currentTheme) } + fun update(it: ThemeSettings) { + theme = it + onThemeChange(it) + } val darkThemeTextID = when(theme.darkTheme) { 1 -> R.string.on 0 -> R.string.off @@ -121,7 +173,7 @@ fun AppearanceScreen(onNavigateUp: () -> Unit, currentTheme: ThemeSettings, onTh SwitchItem( R.string.material_you_color, state = theme.materialYou, - onCheckedChange = { theme = theme.copy(materialYou = it) } + onCheckedChange = { update(theme.copy(materialYou = it)) } ) } Box { @@ -133,13 +185,14 @@ fun AppearanceScreen(onNavigateUp: () -> Unit, currentTheme: ThemeSettings, onTh DropdownMenuItem( text = { Text(stringResource(R.string.follow_system)) }, onClick = { - theme = theme.copy(darkTheme = -1) + update(theme.copy(darkTheme = -1)) darkThemeMenu = false } ) DropdownMenuItem( text = { Text(stringResource(R.string.on)) }, onClick = { + update(theme.copy(darkTheme = 1)) theme = theme.copy(darkTheme = 1) darkThemeMenu = false } @@ -147,19 +200,17 @@ fun AppearanceScreen(onNavigateUp: () -> Unit, currentTheme: ThemeSettings, onTh DropdownMenuItem( text = { Text(stringResource(R.string.off)) }, onClick = { - theme = theme.copy(darkTheme = 0) + update(theme.copy(darkTheme = 0)) darkThemeMenu = false } ) } } AnimatedVisibility(theme.darkTheme == 1 || (theme.darkTheme == -1 && isSystemInDarkTheme())) { - SwitchItem(R.string.black_theme, state = theme.blackTheme, onCheckedChange = { theme = theme.copy(blackTheme = it) }) - } - AnimatedVisibility(theme != currentTheme, Modifier.fillMaxWidth().padding(8.dp)) { - Button({onThemeChange(theme)}) { - Text(stringResource(R.string.apply)) - } + SwitchItem( + R.string.black_theme, state = theme.blackTheme, + onCheckedChange = { update(theme.copy(blackTheme = it)) } + ) } } } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt index a0dd6c2..da61b35 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/DPM.kt @@ -32,7 +32,6 @@ import com.bintianqi.owndroid.backToHomeStateFlow import com.bintianqi.owndroid.createShortcuts import com.bintianqi.owndroid.myPrivilege import com.rosan.dhizuku.api.Dhizuku -import com.rosan.dhizuku.api.Dhizuku.binderWrapper import com.rosan.dhizuku.api.DhizukuBinderWrapper import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.serialization.encodeToString @@ -73,7 +72,7 @@ fun binderWrapperDevicePolicyManager(appContext: Context): DevicePolicyManager? val oldInterface = field[manager] as IDevicePolicyManager if (oldInterface is DhizukuBinderWrapper) return manager val oldBinder = oldInterface.asBinder() - val newBinder = binderWrapper(oldBinder) + val newBinder = Dhizuku.binderWrapper(oldBinder) val newInterface = IDevicePolicyManager.Stub.asInterface(newBinder) field[manager] = newInterface return manager @@ -93,7 +92,7 @@ private fun binderWrapperPackageInstaller(appContext: Context): PackageInstaller val oldInterface = field[installer] as IPackageInstaller if (oldInterface is DhizukuBinderWrapper) return installer val oldBinder = oldInterface.asBinder() - val newBinder = binderWrapper(oldBinder) + val newBinder = Dhizuku.binderWrapper(oldBinder) val newInterface = IPackageInstaller.Stub.asInterface(newBinder) field[installer] = newInterface return installer @@ -107,7 +106,6 @@ fun Context.getPackageInstaller(): PackageInstaller { if(SharedPrefs(this).dhizuku) { if (!dhizukuPermissionGranted()) { dhizukuErrorStatus.value = 2 - backToHomeStateFlow.value = true return this.packageManager.packageInstaller } return binderWrapperPackageInstaller(this) ?: this.packageManager.packageInstaller @@ -120,7 +118,6 @@ fun Context.getDPM(): DevicePolicyManager { if(SharedPrefs(this).dhizuku) { if (!dhizukuPermissionGranted()) { dhizukuErrorStatus.value = 2 - backToHomeStateFlow.value = true return this.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager } return binderWrapperDevicePolicyManager(this) ?: this.getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt index 3928913..0f885e9 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt @@ -295,7 +295,7 @@ fun WorkModesScreen( Spacer(Modifier.padding(horizontal = 2.dp)) Button({ dialog = 5 }) { Text(stringResource(R.string.adb_command)) } Spacer(Modifier.padding(horizontal = 2.dp)) - if (VERSION.SDK_INT == 35) Button({ + if (VERSION.SDK_INT >= 33) Button({ dialog = 6 }) { Text(stringResource(R.string.root_force_activate)) @@ -575,6 +575,7 @@ fun LockScreenInfoScreen(onNavigateUp: () -> Unit) { onClick = { focusMgr.clearFocus() dpm.setDeviceOwnerLockScreenInfo(receiver, null) + infoText = "" context.showOperationResultToast(true) }, modifier = Modifier.fillMaxWidth() diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/UserRestriction.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/UserRestriction.kt index e603326..6564565 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/UserRestriction.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/UserRestriction.kt @@ -10,8 +10,12 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -73,14 +77,30 @@ data class UserRestrictionOptions( @RequiresApi(24) @Composable fun UserRestrictionOptionsScreen( - data: UserRestrictionOptions, restrictions: Bundle, - onNavigateUp: () -> Unit, onRestrictionChange: (String, Boolean) -> Unit + data: UserRestrictionOptions, onNavigateUp: () -> Unit ) { + val context = LocalContext.current + val dpm = context.getDPM() + val receiver = context.getReceiver() + val status = remember { mutableStateMapOf() } + LaunchedEffect(Unit) { + val restrictions = dpm.getUserRestrictions(receiver) + data.items.forEach { + status.put(it.id, restrictions.getBoolean(it.id)) + } + } MyScaffold(data.title, onNavigateUp, 0.dp) { data.items.filter { Build.VERSION.SDK_INT >= it.requiresApi }.forEach { restriction -> SwitchItem( - restriction.name, restriction.id, restriction.icon, - restrictions.getBoolean(restriction.id), { onRestrictionChange(restriction.id, it) }, padding = true + restriction.name, restriction.id, restriction.icon, status[restriction.id] == true, + { + if (it) { + dpm.addUserRestriction(receiver, restriction.id) + } else { + dpm.clearUserRestriction(receiver, restriction.id) + } + status[restriction.id] = dpm.getUserRestrictions(receiver).getBoolean(restriction.id) + }, padding = true ) } } diff --git a/app/src/main/java/com/bintianqi/owndroid/ui/theme/Theme.kt b/app/src/main/java/com/bintianqi/owndroid/ui/theme/Theme.kt index 7e13e2c..0a546a0 100644 --- a/app/src/main/java/com/bintianqi/owndroid/ui/theme/Theme.kt +++ b/app/src/main/java/com/bintianqi/owndroid/ui/theme/Theme.kt @@ -102,7 +102,7 @@ fun OwnDroidTheme( darkTheme -> darkScheme else -> lightScheme }.let { - if(darkTheme && theme.blackTheme) it.copy(background = Color.Black) else it + if(darkTheme && theme.blackTheme) it.copy(background = Color.Black, surface = Color.Black) else it } val view = LocalView.current SideEffect { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4a49976..7efdefa 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.9.2" +agp = "8.10.0" kotlin = "2.1.20" navigation-compose = "2.9.0"