diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7f3188f..23877a5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,6 +13,7 @@ + Unit) { composable { SetDefaultDialerScreen(::navigateUp) } composable { - UserRestrictionScreen(::navigateUp) { title, items -> - navigate(UserRestrictionOptions(title, items)) + UserRestrictionScreen(::navigateUp) { + navigate(it) } } + composable { + UserRestrictionEditorScreen(::navigateUp) + } composable(mapOf(serializableNavTypePair>())) { UserRestrictionOptionsScreen(it.toRoute(), ::navigateUp) } diff --git a/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt b/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt index 2f9f5cd..39a771e 100644 --- a/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt +++ b/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt @@ -180,10 +180,8 @@ fun AppChooserScreen(params: ApplicationsList, onChoosePackage: (String?) -> Uni } ) { paddingValues -> LazyColumn(Modifier.fillMaxSize().padding(paddingValues)) { - stickyHeader { - AnimatedVisibility(progress < 1F) { - LinearProgressIndicator(progress = { progress }, modifier = Modifier.fillMaxWidth()) - } + if (progress < 1F) stickyHeader { + LinearProgressIndicator({ progress }, Modifier.fillMaxWidth()) } items(filteredPackages, { it.name }) { Row( diff --git a/app/src/main/java/com/bintianqi/owndroid/Settings.kt b/app/src/main/java/com/bintianqi/owndroid/Settings.kt index 12c51f6..1b1eaa1 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Settings.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Settings.kt @@ -21,6 +21,7 @@ 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.Close import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.Button import androidx.compose.material3.DropdownMenu @@ -67,6 +68,7 @@ import java.security.SecureRandom import java.text.SimpleDateFormat import java.util.Date import java.util.Locale +import kotlin.system.exitProcess @Serializable object Settings @@ -105,6 +107,11 @@ fun SettingsScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) { Icon(painterResource(R.drawable.description_fill0), null) } ) + DropdownMenuItem( + { Text(stringResource(R.string.exit)) }, + { exitProcess(0) }, + leadingIcon = { Icon(Icons.Default.Close, null) } + ) } } } 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 bb00140..62ed867 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt @@ -271,7 +271,8 @@ fun WorkModesScreen( ) } if ((privilege.device || privilege.profile) && !privilege.dhizuku) Row( - Modifier.padding(top = 20.dp).fillMaxWidth().clickable { onNavigate(DhizukuServerSettings) }, + Modifier.padding(top = 20.dp).fillMaxWidth() + .clickable { onNavigate(DhizukuServerSettings) }.padding(vertical = 4.dp), verticalAlignment = Alignment.CenterVertically ) { Icon(painterResource(R.drawable.dhizuku_icon), null, Modifier.padding(8.dp).size(28.dp)) 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 6564565..b92edc2 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/UserRestriction.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/UserRestriction.kt @@ -6,24 +6,67 @@ import android.os.UserManager import androidx.annotation.DrawableRes import androidx.annotation.RequiresApi import androidx.annotation.StringRes +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +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.offset import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +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.Add +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.material3.MaterialTheme.typography +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.bintianqi.owndroid.HorizontalPadding import com.bintianqi.owndroid.R import com.bintianqi.owndroid.myPrivilege import com.bintianqi.owndroid.ui.FunctionItem +import com.bintianqi.owndroid.ui.MyLazyScaffold import com.bintianqi.owndroid.ui.MyScaffold +import com.bintianqi.owndroid.ui.NavIcon import com.bintianqi.owndroid.ui.SwitchItem +import com.bintianqi.owndroid.zhCN import kotlinx.serialization.Serializable @Serializable @@ -36,35 +79,62 @@ data class Restriction( @Serializable object UserRestriction +@OptIn(ExperimentalMaterial3Api::class) @RequiresApi(24) @Composable -fun UserRestrictionScreen(onNavigateUp: () -> Unit, onNavigate: (Int, List) -> Unit) { +fun UserRestrictionScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) { val privilege by myPrivilege.collectAsStateWithLifecycle() - MyScaffold(R.string.user_restriction, onNavigateUp, 0.dp) { - Spacer(Modifier.padding(vertical = 2.dp)) - Text(text = stringResource(R.string.switch_to_disable_feature), modifier = Modifier.padding(start = 16.dp)) - if(privilege.profile) { Text(text = stringResource(R.string.profile_owner_is_restricted), modifier = Modifier.padding(start = 16.dp)) } - if(privilege.work) { - Text(text = stringResource(R.string.some_features_invalid_in_work_profile), modifier = Modifier.padding(start = 16.dp)) + val sb = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + fun navigateToOptions(title: Int, items: List) { + onNavigate(UserRestrictionOptions(title, items)) + } + Scaffold( + Modifier.nestedScroll(sb.nestedScrollConnection), + topBar = { + LargeTopAppBar( + { Text(stringResource(R.string.user_restriction)) }, + navigationIcon = { NavIcon(onNavigateUp) }, + actions = { + IconButton({ onNavigate(UserRestrictionEditor) }) { + Icon(Icons.Default.Edit, null) + } + }, + scrollBehavior = sb + ) } - Spacer(Modifier.padding(vertical = 2.dp)) - FunctionItem(R.string.network, icon = R.drawable.language_fill0) { - onNavigate(R.string.network, RestrictionData.internet) - } - FunctionItem(R.string.connectivity, icon = R.drawable.devices_other_fill0) { - onNavigate(R.string.connectivity, RestrictionData.connectivity) - } - FunctionItem(R.string.applications, icon = R.drawable.apps_fill0) { - onNavigate(R.string.applications, RestrictionData.applications) - } - FunctionItem(R.string.users, icon = R.drawable.account_circle_fill0) { - onNavigate(R.string.users, RestrictionData.users) - } - FunctionItem(R.string.media, icon = R.drawable.volume_up_fill0) { - onNavigate(R.string.media, RestrictionData.media) - } - FunctionItem(R.string.other, icon = R.drawable.more_horiz_fill0) { - onNavigate(R.string.other, RestrictionData.other) + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + .padding(bottom = 80.dp) + ) { + Spacer(Modifier.padding(vertical = 2.dp)) + Text(text = stringResource(R.string.switch_to_disable_feature), modifier = Modifier.padding(start = 16.dp)) + if(privilege.profile) { Text(text = stringResource(R.string.profile_owner_is_restricted), modifier = Modifier.padding(start = 16.dp)) } + if(privilege.work) { + Text(text = stringResource(R.string.some_features_invalid_in_work_profile), modifier = Modifier.padding(start = 16.dp)) + } + Spacer(Modifier.padding(vertical = 2.dp)) + FunctionItem(R.string.network, icon = R.drawable.language_fill0) { + navigateToOptions(R.string.network, RestrictionData.internet) + } + FunctionItem(R.string.connectivity, icon = R.drawable.devices_other_fill0) { + navigateToOptions(R.string.connectivity, RestrictionData.connectivity) + } + FunctionItem(R.string.applications, icon = R.drawable.apps_fill0) { + navigateToOptions(R.string.applications, RestrictionData.applications) + } + FunctionItem(R.string.users, icon = R.drawable.account_circle_fill0) { + navigateToOptions(R.string.users, RestrictionData.users) + } + FunctionItem(R.string.media, icon = R.drawable.volume_up_fill0) { + navigateToOptions(R.string.media, RestrictionData.media) + } + FunctionItem(R.string.other, icon = R.drawable.more_horiz_fill0) { + navigateToOptions(R.string.other, RestrictionData.other) + } } } } @@ -89,19 +159,37 @@ fun UserRestrictionOptionsScreen( 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, status[restriction.id] == true, - { - if (it) { - dpm.addUserRestriction(receiver, restriction.id) - } else { - dpm.clearUserRestriction(receiver, restriction.id) + MyLazyScaffold(data.title, onNavigateUp) { + items(data.items.filter { Build.VERSION.SDK_INT >= it.requiresApi }) { restriction -> + Row( + Modifier.fillMaxWidth().padding(15.dp, 6.dp), + Arrangement.SpaceBetween, Alignment.CenterVertically + ) { + Row(Modifier.fillMaxWidth(0.8F), verticalAlignment = Alignment.CenterVertically) { + Icon(painterResource(restriction.icon), null, Modifier.padding(start = 6.dp, end = 16.dp)) + Column { + Text(stringResource(restriction.name), style = typography.titleMedium) + Text( + restriction.id, style = typography.bodyMedium, + color = colorScheme.onBackground.copy(alpha = 0.8F) + ) } - status[restriction.id] = dpm.getUserRestrictions(receiver).getBoolean(restriction.id) - }, padding = true - ) + } + Switch( + 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) + } + ) + } + } + item { + Spacer(Modifier.padding(vertical = 30.dp)) } } } @@ -161,6 +249,8 @@ object RestrictionData { Restriction(UserManager.DISALLOW_ADD_USER, R.string.add_user, R.drawable.account_circle_fill0), Restriction(UserManager.DISALLOW_REMOVE_USER, R.string.remove_user, R.drawable.account_circle_fill0), Restriction(UserManager.DISALLOW_USER_SWITCH, R.string.switch_user, R.drawable.account_circle_fill0, 28), + Restriction(UserManager.DISALLOW_ADD_MANAGED_PROFILE, R.string.create_work_profile, R.drawable.work_fill0, 26), + Restriction(UserManager.DISALLOW_REMOVE_MANAGED_PROFILE, R.string.delete_work_profile, R.drawable.delete_forever_fill0, 26), Restriction(UserManager.DISALLOW_ADD_PRIVATE_PROFILE, R.string.create_private_space, R.drawable.lock_fill0, 35), Restriction(UserManager.DISALLOW_SET_USER_ICON, R.string.set_user_icon, R.drawable.account_circle_fill0, 24), Restriction(UserManager.DISALLOW_CROSS_PROFILE_COPY_PASTE, R.string.cross_profile_copy, R.drawable.content_paste_fill0), @@ -187,3 +277,64 @@ object RestrictionData { ) fun getAllRestrictions() = internet + connectivity + media + applications + users + other } + +@Serializable object UserRestrictionEditor + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun UserRestrictionEditorScreen(onNavigateUp: () -> Unit) { + val context = LocalContext.current + val dpm = context.getDPM() + val receiver = context.getReceiver() + val list = remember { mutableStateListOf() } + fun refresh() { + val restrictions = dpm.getUserRestrictions(receiver) + list.clear() + list.addAll(restrictions.keySet().filter { restrictions.getBoolean(it) }) + } + LaunchedEffect(Unit) { refresh() } + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.edit)) }, + navigationIcon = { NavIcon(onNavigateUp) } + ) + } + ) { paddingValues -> + LazyColumn(Modifier.fillMaxSize().padding(paddingValues)) { + items(list, { it }) { + Row( + Modifier.fillMaxWidth().padding(HorizontalPadding, 2.dp).animateItem(), + Arrangement.SpaceBetween, Alignment.CenterVertically + ) { + Text(it) + IconButton({ + dpm.clearUserRestriction(receiver, it) + refresh() + }) { + Icon(Icons.Outlined.Delete, null) + } + } + } + item { + var input by remember { mutableStateOf("") } + fun add() { + dpm.addUserRestriction(receiver, input) + refresh() + input = "" + } + OutlinedTextField( + input, { input = it }, Modifier.fillMaxWidth().padding(HorizontalPadding, 20.dp), + label = { Text("id") }, + trailingIcon = { + IconButton(::add, enabled = input.isNotBlank()) { + Icon(Icons.Default.Add, null) + } + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Done), + keyboardActions = KeyboardActions { add() } + ) + } + } + } +} diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 4096b7a..7d3469b 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -68,6 +68,7 @@ 默认 超时 继续 + 退出 Profile owner diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 38b0a81..04ba9d9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -73,6 +73,7 @@ Default Timeout Continue + Exit Profile owner