diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt index 7ff3010..e230016 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -74,8 +74,12 @@ import com.bintianqi.owndroid.dpm.AffiliationId import com.bintianqi.owndroid.dpm.AffiliationIdScreen import com.bintianqi.owndroid.dpm.AlwaysOnVpnPackage import com.bintianqi.owndroid.dpm.AlwaysOnVpnPackageScreen -import com.bintianqi.owndroid.dpm.Applications -import com.bintianqi.owndroid.dpm.ApplicationsScreen +import com.bintianqi.owndroid.dpm.ApplicationDetails +import com.bintianqi.owndroid.dpm.ApplicationDetailsScreen +import com.bintianqi.owndroid.dpm.ApplicationsFeatures +import com.bintianqi.owndroid.dpm.ApplicationsFeaturesScreen +import com.bintianqi.owndroid.dpm.BlockUninstall +import com.bintianqi.owndroid.dpm.BlockUninstallScreen import com.bintianqi.owndroid.dpm.CaCert import com.bintianqi.owndroid.dpm.CaCertScreen import com.bintianqi.owndroid.dpm.ChangeTime @@ -84,14 +88,22 @@ import com.bintianqi.owndroid.dpm.ChangeTimeZone import com.bintianqi.owndroid.dpm.ChangeTimeZoneScreen import com.bintianqi.owndroid.dpm.ChangeUsername import com.bintianqi.owndroid.dpm.ChangeUsernameScreen +import com.bintianqi.owndroid.dpm.ClearAppStorage +import com.bintianqi.owndroid.dpm.ClearAppStorageScreen import com.bintianqi.owndroid.dpm.ContentProtectionPolicy import com.bintianqi.owndroid.dpm.ContentProtectionPolicyScreen import com.bintianqi.owndroid.dpm.CreateUser import com.bintianqi.owndroid.dpm.CreateUserScreen import com.bintianqi.owndroid.dpm.CreateWorkProfile import com.bintianqi.owndroid.dpm.CreateWorkProfileScreen +import com.bintianqi.owndroid.dpm.CredentialManagerPolicy +import com.bintianqi.owndroid.dpm.CredentialManagerPolicyScreen import com.bintianqi.owndroid.dpm.CrossProfileIntentFilter import com.bintianqi.owndroid.dpm.CrossProfileIntentFilterScreen +import com.bintianqi.owndroid.dpm.CrossProfilePackages +import com.bintianqi.owndroid.dpm.CrossProfilePackagesScreen +import com.bintianqi.owndroid.dpm.CrossProfileWidgetProviders +import com.bintianqi.owndroid.dpm.CrossProfileWidgetProvidersScreen import com.bintianqi.owndroid.dpm.DelegatedAdmins import com.bintianqi.owndroid.dpm.DelegatedAdminsScreen import com.bintianqi.owndroid.dpm.DeleteWorkProfile @@ -104,12 +116,24 @@ import com.bintianqi.owndroid.dpm.DeviceOwner import com.bintianqi.owndroid.dpm.DeviceOwnerScreen import com.bintianqi.owndroid.dpm.DisableAccountManagement import com.bintianqi.owndroid.dpm.DisableAccountManagementScreen +import com.bintianqi.owndroid.dpm.DisableMeteredData +import com.bintianqi.owndroid.dpm.DisableMeteredDataScreen +import com.bintianqi.owndroid.dpm.DisableUserControl +import com.bintianqi.owndroid.dpm.DisableUserControlScreen +import com.bintianqi.owndroid.dpm.EnableSystemApp +import com.bintianqi.owndroid.dpm.EnableSystemAppScreen import com.bintianqi.owndroid.dpm.FrpPolicy import com.bintianqi.owndroid.dpm.FrpPolicyScreen import com.bintianqi.owndroid.dpm.HardwareMonitor import com.bintianqi.owndroid.dpm.HardwareMonitorScreen +import com.bintianqi.owndroid.dpm.Hide +import com.bintianqi.owndroid.dpm.HideScreen +import com.bintianqi.owndroid.dpm.InstallExistingApp +import com.bintianqi.owndroid.dpm.InstallExistingAppScreen import com.bintianqi.owndroid.dpm.InstallSystemUpdate import com.bintianqi.owndroid.dpm.InstallSystemUpdateScreen +import com.bintianqi.owndroid.dpm.KeepUninstalledPackages +import com.bintianqi.owndroid.dpm.KeepUninstalledPackagesScreen import com.bintianqi.owndroid.dpm.Keyguard import com.bintianqi.owndroid.dpm.KeyguardDisabledFeatures import com.bintianqi.owndroid.dpm.KeyguardDisabledFeaturesScreen @@ -142,7 +166,13 @@ import com.bintianqi.owndroid.dpm.PasswordScreen import com.bintianqi.owndroid.dpm.PermissionPolicy import com.bintianqi.owndroid.dpm.PermissionPolicyScreen import com.bintianqi.owndroid.dpm.Permissions +import com.bintianqi.owndroid.dpm.PermissionsManager +import com.bintianqi.owndroid.dpm.PermissionsManagerScreen import com.bintianqi.owndroid.dpm.PermissionsScreen +import com.bintianqi.owndroid.dpm.PermittedAccessibilityServices +import com.bintianqi.owndroid.dpm.PermittedAccessibilityServicesScreen +import com.bintianqi.owndroid.dpm.PermittedInputMethods +import com.bintianqi.owndroid.dpm.PermittedInputMethodsScreen import com.bintianqi.owndroid.dpm.PreferentialNetworkService import com.bintianqi.owndroid.dpm.PreferentialNetworkServiceScreen import com.bintianqi.owndroid.dpm.PrivateDns @@ -163,12 +193,16 @@ import com.bintianqi.owndroid.dpm.ResetPasswordTokenScreen import com.bintianqi.owndroid.dpm.Restriction import com.bintianqi.owndroid.dpm.SecurityLogging import com.bintianqi.owndroid.dpm.SecurityLoggingScreen +import com.bintianqi.owndroid.dpm.SetDefaultDialer +import com.bintianqi.owndroid.dpm.SetDefaultDialerScreen import com.bintianqi.owndroid.dpm.SetSystemUpdatePolicy import com.bintianqi.owndroid.dpm.ShizukuScreen import com.bintianqi.owndroid.dpm.SupportMessage import com.bintianqi.owndroid.dpm.SupportMessageScreen +import com.bintianqi.owndroid.dpm.Suspend import com.bintianqi.owndroid.dpm.SuspendPersonalApp import com.bintianqi.owndroid.dpm.SuspendPersonalAppScreen +import com.bintianqi.owndroid.dpm.SuspendScreen import com.bintianqi.owndroid.dpm.SystemManager import com.bintianqi.owndroid.dpm.SystemManagerScreen import com.bintianqi.owndroid.dpm.SystemOptions @@ -176,6 +210,8 @@ import com.bintianqi.owndroid.dpm.SystemOptionsScreen import com.bintianqi.owndroid.dpm.SystemUpdatePolicyScreen import com.bintianqi.owndroid.dpm.TransferOwnership import com.bintianqi.owndroid.dpm.TransferOwnershipScreen +import com.bintianqi.owndroid.dpm.UninstallApp +import com.bintianqi.owndroid.dpm.UninstallAppScreen import com.bintianqi.owndroid.dpm.UserInfo import com.bintianqi.owndroid.dpm.UserInfoScreen import com.bintianqi.owndroid.dpm.UserOperation @@ -348,7 +384,42 @@ fun Home(vm: MyViewModel) { composable { CrossProfileIntentFilterScreen(::navigateUp) } composable { DeleteWorkProfileScreen(::navigateUp) } - composable { ApplicationsScreen(::navigateUp) } + composable { + AppChooserScreen(it.toRoute(), { + if(it == null) navigateUp() else navigate(ApplicationDetails(it)) + }, { + SharedPrefs(context).applicationsListView = false + navController.navigate(ApplicationsFeatures) { + popUpTo(Home) + } + }) + } + composable { + ApplicationsFeaturesScreen(::navigateUp, ::navigate) { + SharedPrefs(context).applicationsListView = true + navController.navigate(ApplicationsList(true)) { + popUpTo(Home) + } + } + } + composable { ApplicationDetailsScreen(it.toRoute(), ::navigateUp, ::navigate) } + composable { SuspendScreen(::navigateUp) } + composable { HideScreen(::navigateUp) } + composable { BlockUninstallScreen(::navigateUp) } + composable { DisableUserControlScreen(::navigateUp) } + composable { PermissionsManagerScreen(::navigateUp, it.toRoute()) } + composable { DisableMeteredDataScreen(::navigateUp) } + composable { ClearAppStorageScreen(::navigateUp) } + composable { UninstallAppScreen(::navigateUp) } + composable { KeepUninstalledPackagesScreen(::navigateUp) } + composable { InstallExistingAppScreen(::navigateUp) } + composable { CrossProfilePackagesScreen(::navigateUp) } + composable { CrossProfileWidgetProvidersScreen(::navigateUp) } + composable { CredentialManagerPolicyScreen(::navigateUp) } + composable { PermittedAccessibilityServicesScreen(::navigateUp) } + composable { PermittedInputMethodsScreen(::navigateUp) } + composable { EnableSystemAppScreen(::navigateUp) } + composable { SetDefaultDialerScreen(::navigateUp) } composable { LaunchedEffect(Unit) { @@ -493,7 +564,9 @@ private fun HomeScreen(onNavigate: (Any) -> Unit) { ) { HomePageItem(R.string.work_profile, R.drawable.work_fill0) { onNavigate(WorkProfile) } } - if(deviceOwner || profileOwner) HomePageItem(R.string.applications, R.drawable.apps_fill0) { onNavigate(Applications) } + if(deviceOwner || profileOwner) HomePageItem(R.string.applications, R.drawable.apps_fill0) { + onNavigate(if(SharedPrefs(context).applicationsListView) ApplicationsList(true) else ApplicationsFeatures) + } if(VERSION.SDK_INT >= 24 && (profileOwner || deviceOwner)) { HomePageItem(R.string.user_restriction, R.drawable.person_off) { onNavigate(UserRestriction) } } diff --git a/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt b/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt index 5e66a62..805d9bc 100644 --- a/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt +++ b/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt @@ -1,5 +1,6 @@ package com.bintianqi.owndroid +import android.content.Context import android.content.Intent import android.content.pm.ApplicationInfo import android.content.pm.PackageManager @@ -27,6 +28,7 @@ import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.List import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -41,8 +43,10 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -56,77 +60,64 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.lifecycleScope import com.bintianqi.owndroid.ui.theme.OwnDroidTheme import com.google.accompanist.drawablepainter.rememberDrawablePainter +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.serialization.Serializable class PackageChooserActivity: ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val vm by viewModels() - if(getPackagesProgress.value < 1F) getPackages() setContent { val theme by vm.theme.collectAsStateWithLifecycle() OwnDroidTheme(theme) { - val packages by installedPackages.collectAsStateWithLifecycle() - val progress by getPackagesProgress.collectAsStateWithLifecycle() - PackageChooserScreen(packages, progress, ::getPackages) { + AppChooserScreen(ApplicationsList(false), { setResult(0, Intent().putExtra("package", it)) finish() - } + }, {}) } } } - val flags = if(Build.VERSION.SDK_INT >= 24) PackageManager.MATCH_DISABLED_COMPONENTS or PackageManager.MATCH_UNINSTALLED_PACKAGES else 0 - fun getPackages() { - installedPackages.value = emptyList() - lifecycleScope.launch(Dispatchers.IO) { - val pm = packageManager - val apps = pm.getInstalledApplications(flags) - for(pkg in apps) { - installedPackages.update { - it + PackageInfo( - pkg.packageName, pkg.loadLabel(pm).toString(), pkg.loadIcon(pm), - (pkg.flags and ApplicationInfo.FLAG_SYSTEM) != 0 - ) - } - withContext(Dispatchers.Main) { getPackagesProgress.value = installedPackages.value.size.toFloat() / apps.size } - } - } - } - companion object { - val installedPackages = MutableStateFlow(emptyList()) - val getPackagesProgress = MutableStateFlow(0F) - } } -data class PackageInfo( +val installedApps = MutableStateFlow(emptyList()) + +data class AppInfo( val name: String, val label: String, val icon: Drawable, - val system: Boolean + val flags: Int ) +private fun searchInString(query: String, content: String) + = query.split(' ').all { content.contains(it, true) } + +@Serializable data class ApplicationsList(val canSwitchView: Boolean) + @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable -private fun PackageChooserScreen( - packages: List, progress: Float, onRefresh: () -> Unit, onChoosePackage: (String?) -> Unit -) { +fun AppChooserScreen(params: ApplicationsList, onChoosePackage: (String?) -> Unit, onSwitchView: () -> Unit) { + val packages by installedApps.collectAsStateWithLifecycle() + val coroutine = rememberCoroutineScope() val context = LocalContext.current + var progress by remember { mutableFloatStateOf(1F) } var system by remember { mutableStateOf(false) } - var search by remember { mutableStateOf("") } + var query by remember { mutableStateOf("") } var searchMode by remember { mutableStateOf(false) } val filteredPackages = packages.filter { - system == it.system && - (if(search.isEmpty()) true - else it.name.contains(search, ignoreCase = true) || it.label.contains(search, ignoreCase = true)) + system == (it.flags and ApplicationInfo.FLAG_SYSTEM != 0) && + (query.isEmpty() || (searchInString(query, it.label) || searchInString(query, it.name))) } val focusMgr = LocalFocusManager.current + LaunchedEffect(Unit) { + if(packages.size <= 1) getInstalledApps(coroutine, context) { progress = it } + } Scaffold( topBar = { TopAppBar( @@ -141,9 +132,15 @@ private fun PackageChooserScreen( }) { Icon(painter = painterResource(R.drawable.filter_alt_fill0), contentDescription = null) } - IconButton(onRefresh) { + IconButton( + { getInstalledApps(coroutine, context) { progress = it } }, + enabled = progress == 1F + ) { Icon(painter = painterResource(R.drawable.refresh_fill0), contentDescription = null) } + if(params.canSwitchView) IconButton(onSwitchView) { + Icon(Icons.AutoMirrored.Default.List, null) + } } }, title = { @@ -151,8 +148,8 @@ private fun PackageChooserScreen( val fr = FocusRequester() LaunchedEffect(Unit) { fr.requestFocus() } OutlinedTextField( - value = search, - onValueChange = { search = it }, + value = query, + onValueChange = { query = it }, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), keyboardActions = KeyboardActions { focusMgr.clearFocus() }, placeholder = { Text(stringResource(R.string.search)) }, @@ -162,7 +159,7 @@ private fun PackageChooserScreen( contentDescription = null, modifier = Modifier.clickable { focusMgr.clearFocus() - search = "" + query = "" searchMode = false } ) @@ -170,8 +167,6 @@ private fun PackageChooserScreen( textStyle = typography.bodyLarge, modifier = Modifier.fillMaxWidth().focusRequester(fr) ) - } else { - Text(stringResource(R.string.package_chooser)) } }, navigationIcon = { @@ -182,11 +177,8 @@ private fun PackageChooserScreen( colors = TopAppBarDefaults.topAppBarColors(MaterialTheme.colorScheme.surfaceContainer) ) } - ) { paddingValues-> - LazyColumn( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxSize().padding(top = paddingValues.calculateTopPadding()) - ) { + ) { paddingValues -> + LazyColumn(Modifier.fillMaxSize().padding(paddingValues)) { stickyHeader { AnimatedVisibility(progress < 1F) { LinearProgressIndicator(progress = { progress }, modifier = Modifier.fillMaxWidth()) @@ -215,3 +207,24 @@ private fun PackageChooserScreen( } } } + +fun getInstalledApps(scope: CoroutineScope, context: Context, onProgressUpdated: (Float) -> Unit) { + installedApps.value = emptyList() + scope.launch(Dispatchers.IO) { + val pm = context.packageManager + val apps = pm.getInstalledApplications(getInstalledAppsFlags) + for(pkg in apps) { + val label = pkg.loadLabel(pm).toString() + val icon = pkg.loadIcon(pm) + withContext(Dispatchers.Main) { + installedApps.update { + it + AppInfo(pkg.packageName, label, icon, pkg.flags) + } + onProgressUpdated(installedApps.value.size.toFloat() / apps.size) + } + } + } +} + +val getInstalledAppsFlags = + if(Build.VERSION.SDK_INT >= 24) PackageManager.MATCH_DISABLED_COMPONENTS or PackageManager.MATCH_UNINSTALLED_PACKAGES else 0 diff --git a/app/src/main/java/com/bintianqi/owndroid/SharedPrefs.kt b/app/src/main/java/com/bintianqi/owndroid/SharedPrefs.kt index 8b84a0c..b16d4b8 100644 --- a/app/src/main/java/com/bintianqi/owndroid/SharedPrefs.kt +++ b/app/src/main/java/com/bintianqi/owndroid/SharedPrefs.kt @@ -20,6 +20,7 @@ class SharedPrefs(context: Context) { var blackTheme by BooleanSharedPref("theme.black") var lockPassword by StringSharedPref("lock.password") var biometricsUnlock by BooleanSharedPref("lock.biometrics") + var applicationsListView by BooleanSharedPref("applications.list_view") } private class BooleanSharedPref(val key: String, val defValue: Boolean = false): ReadWriteProperty { 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 7fa9a45..81ee54c 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt @@ -5,24 +5,18 @@ import android.app.admin.DevicePolicyManager.PERMISSION_GRANT_STATE_DEFAULT import android.app.admin.DevicePolicyManager.PERMISSION_GRANT_STATE_DENIED import android.app.admin.DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED import android.app.admin.PackagePolicy -import android.app.admin.PackagePolicy.PACKAGE_POLICY_ALLOWLIST -import android.app.admin.PackagePolicy.PACKAGE_POLICY_ALLOWLIST_AND_SYSTEM -import android.app.admin.PackagePolicy.PACKAGE_POLICY_BLOCKLIST import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.content.pm.ApplicationInfo import android.content.pm.PackageInstaller -import android.net.Uri +import android.content.pm.PackageManager import android.os.Build.VERSION import android.os.Looper -import android.provider.Settings -import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.RequiresApi -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.animateContentSize +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -32,6 +26,9 @@ 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.size +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions @@ -39,20 +36,22 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.List +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.outlined.CheckCircle import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.LinearProgressIndicator 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.TextButton -import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -62,538 +61,497 @@ import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +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 import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat -import androidx.core.content.ContextCompat.startActivity -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import com.bintianqi.owndroid.APK_MIME +import com.bintianqi.owndroid.AppInfo import com.bintianqi.owndroid.AppInstallerActivity import com.bintianqi.owndroid.AppInstallerViewModel import com.bintianqi.owndroid.ChoosePackageContract import com.bintianqi.owndroid.HorizontalPadding import com.bintianqi.owndroid.R +import com.bintianqi.owndroid.getInstalledAppsFlags +import com.bintianqi.owndroid.installedApps import com.bintianqi.owndroid.showOperationResultToast -import com.bintianqi.owndroid.ui.Animations import com.bintianqi.owndroid.ui.ErrorDialog +import com.bintianqi.owndroid.ui.FullWidthRadioButtonItem import com.bintianqi.owndroid.ui.FunctionItem -import com.bintianqi.owndroid.ui.ListItem +import com.bintianqi.owndroid.ui.MyLazyScaffold +import com.bintianqi.owndroid.ui.MyScaffold +import com.bintianqi.owndroid.ui.MySmallTitleScaffold import com.bintianqi.owndroid.ui.NavIcon import com.bintianqi.owndroid.ui.Notes -import com.bintianqi.owndroid.ui.RadioButtonItem import com.bintianqi.owndroid.ui.SwitchItem +import com.google.accompanist.drawablepainter.rememberDrawablePainter import kotlinx.serialization.Serializable import java.util.concurrent.Executors -@Serializable -object Applications +fun PackageManager.retrieveAppInfo(packageName: String) = + getApplicationInfo(packageName, getInstalledAppsFlags).retrieveAppInfo(this) + +fun ApplicationInfo.retrieveAppInfo(pm: PackageManager) = + installedApps.value.find { it.name == packageName } ?: AppInfo(packageName, loadLabel(pm).toString(), loadIcon(pm), flags) + +val String.isValidPackageName + get() = Regex("""^(?:[a-zA-Z]\w*\.)+[a-zA-Z]\w*$""").matches(this) + +@Composable +fun LazyItemScope.ApplicationItem(info: AppInfo, onClear: () -> Unit) { + Row( + Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 6.dp).animateItem(), + Arrangement.SpaceBetween, Alignment.CenterVertically + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Image( + painter = rememberDrawablePainter(info.icon), contentDescription = null, + modifier = Modifier.padding(start = 12.dp, end = 18.dp).size(30.dp) + ) + Column { + Text(info.label) + Text(info.name, Modifier.alpha(0.8F), style = typography.bodyMedium) + } + } + IconButton(onClear) { + Icon(Icons.Default.Clear, null) + } + } +} + +@Composable +fun PackageNameTextField(value: String, modifier: Modifier = Modifier, onValueChange: (String) -> Unit) { + val launcher = rememberLauncherForActivityResult(ChoosePackageContract()) { + if(it != null) onValueChange(it) + } + val fm = LocalFocusManager.current + OutlinedTextField( + value, onValueChange, Modifier.fillMaxWidth().then(modifier), + label = { Text(stringResource(R.string.package_name)) }, + trailingIcon = { + IconButton({ + launcher.launch(null) + }) { + Icon(Icons.AutoMirrored.Default.List, null) + } + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Done), + keyboardActions = KeyboardActions { fm.clearFocus() } + ) +} + +@Serializable object ApplicationsFeatures @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ApplicationsScreen(onNavigateUp: () -> Unit) { - val focusMgr = LocalFocusManager.current - val navController = rememberNavController() - var pkgName by rememberSaveable { mutableStateOf("") } - val choosePackage = rememberLauncherForActivityResult(ChoosePackageContract()) {result -> - result?.let { pkgName = it } - } +fun ApplicationsFeaturesScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit, onSwitchView: () -> Unit) { + val context = LocalContext.current + val sb = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() Scaffold( + Modifier.nestedScroll(sb.nestedScrollConnection), topBar = { - TopAppBar( - title = { - OutlinedTextField( - value = pkgName, - onValueChange = { pkgName = it }, - label = { Text(stringResource(R.string.package_name)) }, - modifier = Modifier.fillMaxWidth(), - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Done), - keyboardActions = KeyboardActions { focusMgr.clearFocus() }, - trailingIcon = { - IconButton({ - focusMgr.clearFocus() - choosePackage.launch(null) - }) { - Icon(Icons.AutoMirrored.Default.List, stringResource(R.string.package_chooser)) - } - }, - textStyle = typography.bodyLarge, - singleLine = true - ) - }, + LargeTopAppBar( + { Text(stringResource(R.string.applications)) }, navigationIcon = { NavIcon(onNavigateUp) }, - colors = TopAppBarDefaults.topAppBarColors(colorScheme.surfaceContainer) + actions = { + IconButton(onSwitchView) { + Icon(painterResource(R.drawable.android_fill0), null) + } + }, + scrollBehavior = sb ) } - ) { paddingValues-> - @Suppress("NewApi") NavHost( - modifier = Modifier.padding(top = paddingValues.calculateTopPadding()), - navController = navController, startDestination = Home, - enterTransition = Animations.navHostEnterTransition, - exitTransition = Animations.navHostExitTransition, - popEnterTransition = Animations.navHostPopEnterTransition, - popExitTransition = Animations.navHostPopExitTransition + ) { paddingValues -> + Column( + Modifier + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + .padding(bottom = 80.dp) ) { - composable { HomeScreen(pkgName) { navController.navigate(it) } } - composable { UserControlDisabledPackagesScreen(pkgName) } - composable { PermissionsScreen(pkgName) } - composable { DisableMeteredDataScreen(pkgName) } - composable { CrossProfilePackagesScreen(pkgName) } - composable { CrossProfileWidgetProvidersScreen(pkgName) } - composable { CredentialManagerPolicyScreen(pkgName) } - composable { PermittedAccessibilityServicesScreen(pkgName) } - composable { PermittedInputMethodsScreen(pkgName) } - composable { KeepUninstalledPackagesScreen(pkgName) } + val dpm = context.getDPM() + val receiver = context.getReceiver() + val deviceOwner = context.isDeviceOwner + val profileOwner = context.isProfileOwner + if(VERSION.SDK_INT >= 24) FunctionItem(R.string.suspend, icon = R.drawable.block_fill0) { onNavigate(Suspend) } + FunctionItem(R.string.hide, icon = R.drawable.visibility_off_fill0) { onNavigate(Hide) } + FunctionItem(R.string.block_uninstall, icon = R.drawable.delete_forever_fill0) { onNavigate(BlockUninstall) } + if(VERSION.SDK_INT >= 30 && (deviceOwner || (VERSION.SDK_INT >= 33 && profileOwner))) { + FunctionItem(R.string.disable_user_control, icon = R.drawable.do_not_touch_fill0) { onNavigate(DisableUserControl) } + } + if(VERSION.SDK_INT >= 23) { + FunctionItem(R.string.permissions, icon = R.drawable.shield_fill0) { onNavigate(PermissionsManager()) } + } + if(VERSION.SDK_INT >= 28) { + FunctionItem(R.string.disable_metered_data, icon = R.drawable.money_off_fill0) { onNavigate(DisableMeteredData) } + } + if(VERSION.SDK_INT >= 28) { + FunctionItem(R.string.clear_app_storage, icon = R.drawable.mop_fill0) { onNavigate(ClearAppStorage) } + } + FunctionItem(R.string.install_app, icon = R.drawable.install_mobile_fill0) { + context.startActivity(Intent(context, AppInstallerActivity::class.java)) + } + FunctionItem(R.string.uninstall_app, icon = R.drawable.delete_fill0) { onNavigate(UninstallApp) } + if(VERSION.SDK_INT >= 28 && deviceOwner) { + FunctionItem(R.string.keep_uninstalled_packages, icon = R.drawable.delete_fill0) { onNavigate(KeepUninstalledPackages) } + } + if(VERSION.SDK_INT >= 28) FunctionItem(R.string.install_existing_app, icon = R.drawable.install_mobile_fill0) { + onNavigate(InstallExistingApp) + } + if(VERSION.SDK_INT >= 30 && profileOwner && dpm.isManagedProfile(receiver)) { + FunctionItem(R.string.cross_profile_apps, icon = R.drawable.work_fill0) { onNavigate(CrossProfilePackages) } + } + if(profileOwner) { + FunctionItem(R.string.cross_profile_widget, icon = R.drawable.widgets_fill0) { onNavigate(CrossProfileWidgetProviders) } + } + if(VERSION.SDK_INT >= 34 && deviceOwner) { + FunctionItem(R.string.credential_manager_policy, icon = R.drawable.license_fill0) { onNavigate(CredentialManagerPolicy) } + } + FunctionItem(R.string.permitted_accessibility_services, icon = R.drawable.settings_accessibility_fill0) { + onNavigate(PermittedAccessibilityServices) + } + FunctionItem(R.string.permitted_ime, icon = R.drawable.keyboard_fill0) { onNavigate(PermittedInputMethods) } + FunctionItem(R.string.enable_system_app, icon = R.drawable.enable_fill0) { onNavigate(EnableSystemApp) } + if(VERSION.SDK_INT >= 34 && (deviceOwner || dpm.isOrgProfile(receiver))) { + FunctionItem(R.string.set_default_dialer, icon = R.drawable.call_fill0) { onNavigate(SetDefaultDialer) } + } } } } -@Serializable private object Home +@Serializable data class ApplicationDetails(val packageName: String) @Composable -private fun HomeScreen(pkgName: String, onNavigate: (Any) -> Unit) { - /** 1:Enable system app, 2:Clear app storage, 3:Set default dialer, 4:App control, 5:Install existing app, 6:Uninstall confirmation */ - var dialogStatus by remember { mutableIntStateOf(0) } - var errorMessage by remember { mutableStateOf("") } +fun ApplicationDetailsScreen(param: ApplicationDetails, onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) { + val packageName = param.packageName + val context = LocalContext.current + val pm = context.packageManager + val dpm = context.getDPM() + val receiver = context.getReceiver() + var dialog by remember { mutableIntStateOf(0) } // 1: clear storage, 2: uninstall + val info = pm.getApplicationInfo(packageName, getInstalledAppsFlags) + MySmallTitleScaffold(R.string.place_holder, onNavigateUp, 0.dp) { + Column(Modifier.align(Alignment.CenterHorizontally).padding(top = 16.dp), horizontalAlignment = Alignment.CenterHorizontally) { + Image(rememberDrawablePainter(info.loadIcon(pm)), null, Modifier.size(50.dp)) + Text(info.loadLabel(pm).toString(), Modifier.padding(top = 4.dp)) + Text(info.packageName, Modifier.alpha(0.7F).padding(bottom = 8.dp), style = typography.bodyMedium) + } + FunctionItem(R.string.permissions, icon = R.drawable.shield_fill0) { onNavigate(PermissionsManager(packageName)) } + if(VERSION.SDK_INT >= 24) SwitchItem( + R.string.suspend, icon = R.drawable.block_fill0, + getState = { dpm.isPackageSuspended(receiver, packageName) }, + onCheckedChange = { dpm.setPackagesSuspended(receiver, arrayOf(packageName), it) } + ) + SwitchItem( + R.string.hide, icon = R.drawable.visibility_off_fill0, + getState = { dpm.isApplicationHidden(receiver, packageName) }, + onCheckedChange = { dpm.setApplicationHidden(receiver, packageName, it) } + ) + SwitchItem( + R.string.block_uninstall, icon = R.drawable.delete_forever_fill0, + getState = { dpm.isUninstallBlocked(receiver, packageName) }, + onCheckedChange = { dpm.setUninstallBlocked(receiver, packageName, it) } + ) + if(VERSION.SDK_INT >= 30) SwitchItem( + R.string.disable_user_control, icon = R.drawable.do_not_touch_fill0, + getState = { packageName in dpm.getUserControlDisabledPackages(receiver) }, + onCheckedChange = { state -> + dpm.setUserControlDisabledPackages(receiver, + dpm.getUserControlDisabledPackages(receiver).let { if(state) it.plus(packageName) else it.minus(packageName) } + ) + } + ) + if(VERSION.SDK_INT >= 28) SwitchItem( + R.string.disable_metered_data, icon = R.drawable.money_off_fill0, + getState = { packageName in dpm.getMeteredDataDisabledPackages(receiver) }, + onCheckedChange = { state -> + dpm.setMeteredDataDisabledPackages(receiver, + dpm.getMeteredDataDisabledPackages(receiver).let { if(state) it.plus(packageName) else it.minus(packageName) } + ) + } + ) + if(VERSION.SDK_INT >= 28) SwitchItem( + R.string.keep_after_uninstall, icon = R.drawable.delete_fill0, + getState = { dpm.getKeepUninstalledPackages(receiver)?.contains(packageName) == true }, + onCheckedChange = { state -> + dpm.setKeepUninstalledPackages(receiver, + dpm.getKeepUninstalledPackages(receiver)?.let { if(state) it.plus(packageName) else it.minus(packageName) } ?: listOf(packageName) + ) + } + ) + if(VERSION.SDK_INT >= 28) FunctionItem(R.string.clear_app_storage, icon = R.drawable.mop_fill0) { dialog = 1 } + FunctionItem(R.string.uninstall, icon = R.drawable.delete_fill0) { dialog = 2 } + } + if(dialog == 1 && VERSION.SDK_INT >= 28) ClearAppStorageDialog(packageName) { dialog = 0 } + if(dialog == 2) UninstallAppDialog(packageName) { dialog = 0 } +} + +@Serializable object Suspend + +@RequiresApi(24) +@Composable +fun SuspendScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() - val deviceOwner = context.isDeviceOwner - val profileOwner = context.isProfileOwner - var suspend by remember { mutableStateOf(false) } - var hide by remember { mutableStateOf(false) } - var blockUninstall by remember { mutableStateOf(false) } - var appControlAction by remember { mutableIntStateOf(0) } // 1:Suspend, 2:Hide, 3:Block uninstall - val focusMgr = LocalFocusManager.current + var packageName by remember { mutableStateOf("") } + val packages = remember { mutableStateListOf() } fun refresh() { - if(VERSION.SDK_INT >= 24) { - try { - suspend = dpm.isPackageSuspended(receiver, pkgName) - } catch(_: Exception) {} + val pm = context.packageManager + packages.clear() + pm.getInstalledApplications(getInstalledAppsFlags).filter { dpm.isPackageSuspended(receiver, it.packageName) }.forEach { + packages += it.retrieveAppInfo(pm) } - hide = dpm.isApplicationHidden(receiver, pkgName) - blockUninstall = dpm.isUninstallBlocked(receiver, pkgName) } - fun appControl(status: Boolean) { - when(appControlAction) { - 1 -> if(VERSION.SDK_INT >= 24) dpm.setPackagesSuspended(receiver, arrayOf(pkgName), status) - 2 -> dpm.setApplicationHidden(receiver, pkgName, status) - 3 -> dpm.setUninstallBlocked(receiver, pkgName, status) + LaunchedEffect(Unit) { refresh() } + MyLazyScaffold(R.string.suspend, onNavigateUp) { + items(packages, { it.name }) { + ApplicationItem(it) { + dpm.setPackagesSuspended(receiver, arrayOf(it.name), false) + refresh() + } + } + item { + Column(Modifier.padding(horizontal = HorizontalPadding)) { + PackageNameTextField(packageName, Modifier.padding(vertical = 8.dp)) { packageName = it } + Button( + { + if(dpm.setPackagesSuspended(receiver, arrayOf(packageName), true).isEmpty()) packageName = "" + else context.showOperationResultToast(false) + refresh() + }, + Modifier.fillMaxWidth(), + packageName.isValidPackageName + ) { + Text(stringResource(R.string.suspend)) + } + Notes(R.string.info_suspend_app) + } } - refresh() } - LaunchedEffect(pkgName) { refresh() } - Column( - modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()) - ) { - Spacer(Modifier.padding(vertical = 5.dp)) - if(VERSION.SDK_INT >= 24 && profileOwner && dpm.isManagedProfile(receiver)) { - Text(text = stringResource(R.string.scope_is_work_profile), textAlign = TextAlign.Center,modifier = Modifier.fillMaxWidth()) - } - FunctionItem(title = R.string.app_info, icon = R.drawable.open_in_new) { - val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) - intent.setData(Uri.parse("package:$pkgName")) - startActivity(context, intent, null) - } - if(VERSION.SDK_INT >= 24) { - SwitchItem( - title = R.string.suspend, icon = R.drawable.block_fill0, - state = suspend, - onCheckedChange = { appControlAction = 1; appControl(it) }, - onClickBlank = { appControlAction = 1; dialogStatus = 4 } - ) - } - SwitchItem( - title = R.string.hide, icon = R.drawable.visibility_off_fill0, - state = hide, - onCheckedChange = { appControlAction = 2; appControl(it) }, - onClickBlank = { appControlAction = 2; dialogStatus = 4 } - ) - SwitchItem( - title = R.string.block_uninstall, icon = R.drawable.delete_forever_fill0, - state = blockUninstall, - onCheckedChange = { appControlAction = 3; appControl(it) }, - onClickBlank = { appControlAction = 3; dialogStatus = 4 } - ) - if(VERSION.SDK_INT >= 30 && (deviceOwner || (VERSION.SDK_INT >= 33 && profileOwner))) { - FunctionItem(title = R.string.ucd, icon = R.drawable.do_not_touch_fill0) { onNavigate(UserControlDisabledPackages) } - } - if(VERSION.SDK_INT >= 23) { - FunctionItem(title = R.string.permissions, icon = R.drawable.key_fill0) { onNavigate(PermissionsManager) } - } - if(VERSION.SDK_INT >= 28) { - FunctionItem(R.string.disable_metered_data, icon = R.drawable.money_off_fill0) { onNavigate(DisableMeteredData) } - } - if(VERSION.SDK_INT >= 30 && profileOwner && dpm.isManagedProfile(receiver)) { - FunctionItem(title = R.string.cross_profile_package, icon = R.drawable.work_fill0) { onNavigate(CrossProfilePackages) } - } - if(profileOwner) { - FunctionItem(title = R.string.cross_profile_widget, icon = R.drawable.widgets_fill0) { onNavigate(CrossProfileWidgetProviders) } - } - if(VERSION.SDK_INT >= 34 && deviceOwner) { - FunctionItem(title = R.string.credential_manager_policy, icon = R.drawable.license_fill0) { onNavigate(CredentialManagerPolicy) } - } - FunctionItem(title = R.string.permitted_accessibility_services, icon = R.drawable.settings_accessibility_fill0) { - onNavigate(PermittedAccessibilityServices) - } - FunctionItem(title = R.string.permitted_ime, icon = R.drawable.keyboard_fill0) { onNavigate(PermittedInputMethods) } - FunctionItem(title = R.string.enable_system_app, icon = R.drawable.enable_fill0) { - if(pkgName != "") dialogStatus = 1 - } - if(VERSION.SDK_INT >= 28 && deviceOwner) { - FunctionItem(title = R.string.keep_uninstalled_packages, icon = R.drawable.delete_fill0) { onNavigate(KeepUninstalledPackages) } - } - if(VERSION.SDK_INT >= 28) { - FunctionItem(title = R.string.clear_app_storage, icon = R.drawable.mop_fill0) { - if(pkgName != "") dialogStatus = 2 - } - } - val chooseApks = rememberLauncherForActivityResult(ActivityResultContracts.GetMultipleContents()) { - if(it.isEmpty()) return@rememberLauncherForActivityResult - val intent = Intent(context, AppInstallerActivity::class.java) - intent.putExtra(Intent.EXTRA_STREAM, it.toTypedArray()) - startActivity(context, intent, null) - } - FunctionItem(title = R.string.install_app, icon = R.drawable.install_mobile_fill0) { - Toast.makeText(context, R.string.choose_apk_file, Toast.LENGTH_SHORT).show() - chooseApks.launch(APK_MIME) - } - if(VERSION.SDK_INT >= 28) FunctionItem(R.string.install_existing_app, icon = R.drawable.install_mobile_fill0) { - if(pkgName.isNotBlank()) dialogStatus = 5 - } - FunctionItem(title = R.string.uninstall_app, icon = R.drawable.delete_fill0) { - if(pkgName.isNotBlank()) dialogStatus = 6 - } - if(VERSION.SDK_INT >= 34 && (deviceOwner || dpm.isOrgProfile(receiver))) { - FunctionItem(title = R.string.set_default_dialer, icon = R.drawable.call_fill0) { - if(pkgName != "") dialogStatus = 3 - } - } - Spacer(Modifier.padding(vertical = 30.dp)) - } - if(dialogStatus == 1) AlertDialog( - title = { Text(stringResource(R.string.enable_system_app)) }, - text = { - Text(stringResource(R.string.enable_system_app_desc) + "\n" + pkgName) - }, - onDismissRequest = { dialogStatus = 0 }, - dismissButton = { - TextButton({ dialogStatus = 0 }) { - Text(stringResource(R.string.cancel)) - } - }, - confirmButton = { - TextButton({ - try { - dpm.enableSystemApp(receiver, pkgName) - context.showOperationResultToast(true) - } catch(_: IllegalArgumentException) { - Toast.makeText(context, R.string.failed, Toast.LENGTH_SHORT).show() - } - dialogStatus = 0 - }) { - Text(stringResource(R.string.confirm)) - } - }, - modifier = Modifier.fillMaxWidth() - ) - if(dialogStatus == 2 && VERSION.SDK_INT >= 28) AlertDialog( - title = { Text(text = stringResource(R.string.clear_app_storage)) }, - text = { - Text(stringResource(R.string.app_storage_will_be_cleared) + "\n" + pkgName) - }, - confirmButton = { - TextButton( - onClick = { - val executor = Executors.newCachedThreadPool() - dpm.clearApplicationUserData(receiver, pkgName, executor) { pkg: String, succeed: Boolean -> - Looper.prepare() - val toastText = - if(pkg != "") { "$pkg\n" } else { "" } + - context.getString(R.string.clear_data) + - context.getString(if(succeed) R.string.success else R.string.failed ) - Toast.makeText(context, toastText, Toast.LENGTH_SHORT).show() - Looper.loop() - } - dialogStatus = 0 - }, - colors = ButtonDefaults.textButtonColors(contentColor = colorScheme.error) - ) { - Text(text = stringResource(R.string.clear)) - } - }, - dismissButton = { - TextButton({ dialogStatus = 0 }) { - Text(text = stringResource(R.string.cancel)) - } - }, - onDismissRequest = { dialogStatus = 0 }, - modifier = Modifier.fillMaxWidth() - ) - if(dialogStatus == 3 && VERSION.SDK_INT >= 34) AlertDialog( - title = { Text(stringResource(R.string.set_default_dialer)) }, - text = { - Text(stringResource(R.string.app_will_be_default_dialer) + "\n" + pkgName) - }, - onDismissRequest = { dialogStatus = 0 }, - dismissButton = { - TextButton({ dialogStatus = 0 }) { - Text(stringResource(R.string.cancel)) - } - }, - confirmButton = { - TextButton({ - try { - dpm.setDefaultDialerApplication(pkgName) - context.showOperationResultToast(true) - } catch(_: IllegalArgumentException) { - context.showOperationResultToast(false) - } - dialogStatus = 0 - }) { - Text(stringResource(R.string.confirm)) - } - }, - modifier = Modifier.fillMaxWidth() - ) - if(dialogStatus == 4) { - LaunchedEffect(Unit) { - focusMgr.clearFocus() - } - AlertDialog( - onDismissRequest = { dialogStatus = 0 }, - title = { - Text(stringResource( - when(appControlAction) { - 1 -> R.string.suspend - 2 -> R.string.hide - 3 -> R.string.block_uninstall - 4 -> R.string.always_on_vpn - else -> R.string.unknown - } - )) - }, - text = { - val enabled = when(appControlAction){ - 1 -> suspend - 2 -> hide - 3 -> blockUninstall - else -> false - } - Column { - Text(stringResource(R.string.current_state, stringResource(if(enabled) R.string.enabled else R.string.disabled))) - Spacer(Modifier.padding(vertical = 4.dp)) - if(appControlAction == 1) Text(stringResource(R.string.info_suspend_app)) - } - }, - confirmButton = { - TextButton({ - appControl(true) - dialogStatus = 0 - }) { - Text(text = stringResource(R.string.enable)) - } - }, - dismissButton = { - TextButton({ - appControl(false) - dialogStatus = 0 - }) { - Text(text = stringResource(R.string.disable)) - } - } - ) - } - if(dialogStatus == 5 && VERSION.SDK_INT >= 28) AlertDialog( - text = { Text(stringResource(R.string.info_install_existing_app)) }, - confirmButton = { - TextButton({ - context.showOperationResultToast(dpm.installExistingPackage(receiver, pkgName)) - dialogStatus = 0 - }) { - Text(stringResource(R.string.confirm)) - } - }, - dismissButton = { - TextButton({ dialogStatus = 0 }) { Text(stringResource(R.string.cancel)) } - }, - onDismissRequest = { dialogStatus = 0 } - ) - if(dialogStatus == 6) AlertDialog( - title = { Text(stringResource(R.string.uninstall)) }, - text = { Text(pkgName) }, - onDismissRequest = { dialogStatus = 0 }, - confirmButton = { - TextButton( - onClick = { uninstallPackage(context, pkgName) { errorMessage = it } }, - colors = ButtonDefaults.textButtonColors(contentColor = colorScheme.error) - ) { - Text(stringResource(R.string.confirm)) - } - }, - dismissButton = { - TextButton({ - dialogStatus = 0 - }) { - Text(stringResource(R.string.cancel)) - } - } - ) - ErrorDialog(errorMessage) { errorMessage = "" } - LaunchedEffect(dialogStatus) { focusMgr.clearFocus() } } -@Serializable private object UserControlDisabledPackages +@Serializable object Hide + +@Composable +fun HideScreen(onNavigateUp: () -> Unit) { + val context = LocalContext.current + val dpm = context.getDPM() + val receiver = context.getReceiver() + var packageName by remember { mutableStateOf("") } + val packages = remember { mutableStateListOf() } + fun refresh() { + val pm = context.packageManager + packages.clear() + pm.getInstalledApplications(getInstalledAppsFlags).filter { dpm.isApplicationHidden(receiver, it.packageName) }.forEach { + packages += it.retrieveAppInfo(pm) + } + } + LaunchedEffect(Unit) { refresh() } + MyLazyScaffold(R.string.hide, onNavigateUp) { + items(packages, { it.name }) { + ApplicationItem(it) { + dpm.setApplicationHidden(receiver, it.name, false) + refresh() + } + } + item { + Column(Modifier.padding(horizontal = HorizontalPadding)) { + PackageNameTextField(packageName, Modifier.padding(vertical = 8.dp)) { packageName = it } + Button( + { + if(dpm.setApplicationHidden(receiver, packageName, true)) packageName = "" + else context.showOperationResultToast(false) + refresh() + }, + Modifier.fillMaxWidth(), + packageName.isValidPackageName + ) { + Text(stringResource(R.string.hide)) + } + } + } + } +} + +@Serializable object BlockUninstall + +@Composable +fun BlockUninstallScreen(onNavigateUp: () -> Unit) { + val context = LocalContext.current + val dpm = context.getDPM() + val receiver = context.getReceiver() + var packageName by remember { mutableStateOf("") } + val packages = remember { mutableStateListOf() } + fun refresh() { + val pm = context.packageManager + packages.clear() + pm.getInstalledApplications(getInstalledAppsFlags).filter { dpm.isUninstallBlocked(receiver, it.packageName) }.forEach { + packages += it.retrieveAppInfo(pm) + } + } + LaunchedEffect(Unit) { refresh() } + MyLazyScaffold(R.string.block_uninstall, onNavigateUp) { + items(packages, { it.name }) { + ApplicationItem(it) { + dpm.setUninstallBlocked(receiver, it.name, false) + refresh() + } + } + item { + Column(Modifier.padding(horizontal = HorizontalPadding)) { + PackageNameTextField(packageName, Modifier.padding(vertical = 8.dp)) { packageName = it } + Button( + { + dpm.setUninstallBlocked(receiver, packageName, true) + packageName = "" + refresh() + }, + Modifier.fillMaxWidth(), + packageName.isValidPackageName + ) { + Text(stringResource(R.string.block_uninstall)) + } + } + } + } +} + +@Serializable object DisableUserControl @RequiresApi(30) @Composable -private fun UserControlDisabledPackagesScreen(pkgName:String) { +fun DisableUserControlScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() - val pkgList = remember { mutableStateListOf() } - Column(modifier = Modifier.fillMaxSize().padding(horizontal = HorizontalPadding).verticalScroll(rememberScrollState())) { - val refresh = { - pkgList.clear() - pkgList.addAll(dpm.getUserControlDisabledPackages(receiver)) + val packages = remember { mutableStateListOf() } + fun refresh() { + val pm = context.packageManager + packages.clear() + dpm.getUserControlDisabledPackages(receiver).forEach { + packages += pm.retrieveAppInfo(it) } - LaunchedEffect(Unit) { refresh() } - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.ucd), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) - Text(text = stringResource(R.string.ucd_desc)) - Spacer(Modifier.padding(vertical = 5.dp)) - Text(text = stringResource(R.string.app_list_is)) - Column(modifier = Modifier.animateContentSize()) { - if(pkgList.isEmpty()) Text(stringResource(R.string.none)) - for(i in pkgList) { - ListItem(i) { pkgList -= i } + } + LaunchedEffect(Unit) { refresh() } + MyLazyScaffold(R.string.disable_user_control, onNavigateUp) { + items(packages, { it.name }) { + ApplicationItem(it) { + dpm.setUserControlDisabledPackages(receiver, packages.minus(it).map { it.name }) + refresh() } } - Spacer(Modifier.padding(vertical = 5.dp)) - Button( - onClick = { pkgList += pkgName }, - modifier = Modifier.fillMaxWidth() - ) { - Text(stringResource(R.string.add)) + item { + var packageName by remember { mutableStateOf("") } + PackageNameTextField(packageName, Modifier.padding(vertical = 8.dp, horizontal = HorizontalPadding)) { packageName = it } + Button( + { + dpm.setUserControlDisabledPackages(receiver, packages.map { it.name } + packageName) + refresh() + }, + Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding).padding(bottom = 8.dp), + ) { + Text(stringResource(R.string.add)) + } + Notes(R.string.info_disable_user_control, HorizontalPadding) } - Button( - onClick = { dpm.setUserControlDisabledPackages(receiver, pkgList); refresh() }, - modifier = Modifier.fillMaxWidth().padding(top = 8.dp) - ) { - Text(stringResource(R.string.apply)) - } - Notes(R.string.info_disable_user_control) - Spacer(Modifier.padding(vertical = 30.dp)) } } -@Serializable private object PermissionsManager +@Serializable data class PermissionsManager(val packageName: String? = null) @RequiresApi(23) @Composable -private fun PermissionsScreen(pkgName: String) { +fun PermissionsManagerScreen(onNavigateUp: () -> Unit, param: PermissionsManager) { + val packageNameParam = param.packageName val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() - var showDialog by remember { mutableStateOf(false) } - var selectedPermission by remember { mutableStateOf(PermissionItem("", R.string.unknown, R.drawable.block_fill0)) } + var packageName by remember { mutableStateOf(packageNameParam ?: "") } + var selectedPermission by remember { mutableStateOf(null) } val statusMap = remember { mutableStateMapOf() } - val grantState = mapOf( - PERMISSION_GRANT_STATE_DEFAULT to stringResource(R.string.default_stringres), - PERMISSION_GRANT_STATE_GRANTED to stringResource(R.string.granted), - PERMISSION_GRANT_STATE_DENIED to stringResource(R.string.denied) - ) - LaunchedEffect(pkgName) { - if(pkgName != "") { - permissionList().forEach { statusMap[it.permission] = dpm.getPermissionGrantState(receiver, pkgName, it.permission) } + LaunchedEffect(packageName) { + if(packageName.isValidPackageName) { + permissionList().forEach { statusMap[it.permission] = dpm.getPermissionGrantState(receiver, packageName, it.permission) } } else { statusMap.clear() } } - Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 4.dp)) - for(permission in permissionList()) { + MyLazyScaffold(R.string.permissions, onNavigateUp) { + item { + if(packageNameParam == null) { + PackageNameTextField(packageName, Modifier.padding(HorizontalPadding, 8.dp)) { packageName = it } + Spacer(Modifier.padding(vertical = 4.dp)) + } + } + items(permissionList(), { it.permission }) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() - .clickable { - if(pkgName != "") { - selectedPermission = permission - showDialog = true - } + .clickable(packageName.isValidPackageName) { + selectedPermission = it } .padding(8.dp) ) { - Icon( - painter = painterResource(permission.icon), - contentDescription = stringResource(permission.label), - modifier = Modifier.padding(horizontal = 12.dp) - ) + Icon(painterResource(it.icon), null, Modifier.padding(horizontal = 12.dp)) Column { - Text(text = stringResource(permission.label)) - Text( - text = grantState[statusMap[permission.permission]]?: stringResource(R.string.unknown), - modifier = Modifier.alpha(0.7F), style = typography.bodyMedium - ) + val state = when(statusMap[it.permission]) { + PERMISSION_GRANT_STATE_DEFAULT -> R.string.default_stringres + PERMISSION_GRANT_STATE_GRANTED -> R.string.granted + PERMISSION_GRANT_STATE_DENIED -> R.string.denied + else -> R.string.unknown + } + Text(stringResource(it.label)) + Text(stringResource(state), Modifier.alpha(0.7F), style = typography.bodyMedium) } } } - Spacer(Modifier.padding(vertical = 30.dp)) + item { + Spacer(Modifier.padding(vertical = 30.dp)) + } } - if(showDialog) { - val grantPermission: (Int)->Unit = { - dpm.setPermissionGrantState(receiver, pkgName, selectedPermission.permission, it) - statusMap[selectedPermission.permission] = dpm.getPermissionGrantState(receiver, pkgName, selectedPermission.permission) - showDialog = false + if(selectedPermission != null) { + fun changeState(state: Int) { + dpm.setPermissionGrantState(receiver, packageName, selectedPermission!!.permission, state) + statusMap[selectedPermission!!.permission] = dpm.getPermissionGrantState(receiver, packageName, selectedPermission!!.permission) + selectedPermission = null } @Composable fun GrantPermissionItem(label: Int, status: Int) { - val selected = statusMap[selectedPermission.permission] == status + val selected = statusMap[selectedPermission!!.permission] == status Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier + Modifier .fillMaxWidth() .clip(RoundedCornerShape(8.dp)) .background(if(selected) colorScheme.primaryContainer else Color.Transparent) - .clickable { grantPermission(status) } - .padding(vertical = 16.dp, horizontal = 12.dp) + .clickable { changeState(status) } + .padding(vertical = 16.dp, horizontal = 12.dp), + Arrangement.SpaceBetween, Alignment.CenterVertically, ) { - Text(text = stringResource(label), color = if(selected) colorScheme.primary else Color.Unspecified) - if(selected) { - Icon( - painter = painterResource(R.drawable.check_circle_fill0), - contentDescription = stringResource(label), - tint = colorScheme.primary - ) - } + Text(stringResource(label), color = if(selected) colorScheme.primary else Color.Unspecified) + if(selected) Icon(Icons.Outlined.CheckCircle, null, tint = colorScheme.primary) } } AlertDialog( - onDismissRequest = { showDialog = false }, - confirmButton = { TextButton(onClick = { showDialog = false }) { Text(stringResource(R.string.cancel)) } }, - title = { Text(stringResource(selectedPermission.label)) }, + onDismissRequest = { selectedPermission = null }, + confirmButton = { TextButton({ selectedPermission = null }) { Text(stringResource(R.string.cancel)) } }, + title = { Text(stringResource(selectedPermission!!.label)) }, text = { Column { - Text(selectedPermission.permission) + Text(selectedPermission!!.permission) Spacer(Modifier.padding(vertical = 4.dp)) - if(!(VERSION.SDK_INT >=31 && context.isProfileOwner && selectedPermission.profileOwnerRestricted)) { + if(!(VERSION.SDK_INT >= 31 && selectedPermission!!.profileOwnerRestricted && context.isProfileOwner)) { GrantPermissionItem(R.string.granted, PERMISSION_GRANT_STATE_GRANTED) } GrantPermissionItem(R.string.denied, PERMISSION_GRANT_STATE_DENIED) @@ -604,369 +562,539 @@ private fun PermissionsScreen(pkgName: String) { } } -@Serializable private object DisableMeteredData +@Serializable object DisableMeteredData @RequiresApi(28) @Composable -private fun DisableMeteredDataScreen(pkgName: String) { +fun DisableMeteredDataScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() - val packages = remember { mutableStateListOf() } + var packageName by remember { mutableStateOf("") } + val packages = remember { mutableStateListOf() } fun refresh() { + val pm = context.packageManager packages.clear() - packages.addAll(dpm.getMeteredDataDisabledPackages(receiver)) + dpm.getMeteredDataDisabledPackages(receiver).forEach { + packages += pm.retrieveAppInfo(it) + } } LaunchedEffect(Unit) { refresh() } - Column(Modifier.fillMaxSize().padding(horizontal = HorizontalPadding).verticalScroll(rememberScrollState())) { - Text(stringResource(R.string.disable_metered_data), Modifier.padding(vertical = 8.dp), style = typography.headlineLarge) - Column(Modifier.animateContentSize()) { - packages.forEach { pkg -> - ListItem(pkg) { packages -= pkg } + MyLazyScaffold(R.string.disable_metered_data, onNavigateUp) { + items(packages, { it.name }) { + ApplicationItem(it) { + dpm.setMeteredDataDisabledPackages(receiver, packages.minus(it).map { it.name }) + refresh() } } - Button( - onClick = { packages += pkgName }, - modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp), - enabled = pkgName.isNotBlank() - ) { - Text(stringResource(R.string.add)) - } - Button( - onClick = { - context.showOperationResultToast(dpm.setMeteredDataDisabledPackages(receiver, packages).isEmpty()) - refresh() - }, - modifier = Modifier.fillMaxWidth() - ) { - Text(stringResource(R.string.apply)) + item { + PackageNameTextField(packageName, Modifier.padding(HorizontalPadding, 8.dp)) { packageName = it } + Button( + { + if(dpm.setMeteredDataDisabledPackages(receiver, packages.map { it.name } + packageName).isEmpty()) { + packageName = "" + } else { + context.showOperationResultToast(false) + } + refresh() + }, + Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding), + packageName.isValidPackageName + ) { + Text(stringResource(R.string.add)) + } } } } -@Serializable private object CrossProfilePackages +@Serializable object ClearAppStorage + +@RequiresApi(28) +@Composable +fun ClearAppStorageScreen(onNavigateUp: () -> Unit) { + var dialog by remember { mutableStateOf(false) } + var packageName by remember { mutableStateOf("") } + MyScaffold(R.string.clear_app_storage, onNavigateUp) { + PackageNameTextField(packageName, Modifier.padding(vertical = 8.dp)) { packageName = it } + Button( + { dialog = true }, + Modifier.fillMaxWidth(), + packageName.isValidPackageName + ) { + Text(stringResource(R.string.clear)) + } + } + if(dialog) ClearAppStorageDialog(packageName) { dialog = false } +} + +@RequiresApi(28) +@Composable +private fun ClearAppStorageDialog(packageName: String, onClose: () -> Unit) { + val context = LocalContext.current + var clearing by remember { mutableStateOf(false) } + AlertDialog( + title = { Text(stringResource(R.string.clear_app_storage)) }, + text = { + if(clearing) LinearProgressIndicator(Modifier.fillMaxWidth()) + }, + confirmButton = { + TextButton( + { + clearing = true + context.getDPM().clearApplicationUserData( + context.getReceiver(), packageName, Executors.newSingleThreadExecutor() + ) { _, it -> + Looper.prepare() + context.showOperationResultToast(it) + onClose() + } + }, + enabled = !clearing, + colors = ButtonDefaults.textButtonColors(contentColor = colorScheme.error) + ) { + Text(stringResource(R.string.confirm)) + } + }, + dismissButton = { + TextButton(onClose, enabled = !clearing) { Text(stringResource(R.string.cancel)) } + }, + onDismissRequest = onClose + ) +} + +@Serializable object UninstallApp + +@Composable +fun UninstallAppScreen(onNavigateUp: () -> Unit) { + var dialog by remember { mutableStateOf(false) } + var packageName by remember { mutableStateOf("") } + MyScaffold(R.string.uninstall_app, onNavigateUp) { + PackageNameTextField(packageName, Modifier.padding(vertical = 8.dp)) { packageName = it } + Button( + { dialog = true }, + Modifier.fillMaxWidth(), + packageName.isValidPackageName + ) { + Text(stringResource(R.string.uninstall)) + } + } + if(dialog) UninstallAppDialog(packageName) { dialog = false } +} + +@Composable +private fun UninstallAppDialog(packageName: String, onClose: () -> Unit) { + val context = LocalContext.current + var uninstalling by remember { mutableStateOf(false) } + var errorMessage by remember { mutableStateOf(null) } + AlertDialog( + title = { Text(stringResource(R.string.uninstall)) }, + text = { + if(errorMessage != null) Text(errorMessage!!) + if(uninstalling) LinearProgressIndicator(Modifier.fillMaxWidth()) + }, + confirmButton = { + TextButton( + { + uninstalling = true + uninstallPackage(context, packageName) { + uninstalling = false + if(it == null) onClose() else errorMessage = it + } + }, + enabled = !uninstalling, + colors = ButtonDefaults.textButtonColors(contentColor = colorScheme.error) + ) { + Text(stringResource(R.string.confirm)) + } + }, + dismissButton = { + TextButton(onClose, enabled = !uninstalling) { Text(stringResource(R.string.cancel)) } + }, + onDismissRequest = onClose + ) +} + +@Serializable object KeepUninstalledPackages + +@RequiresApi(28) +@Composable +fun KeepUninstalledPackagesScreen(onNavigateUp: () -> Unit) { + val context = LocalContext.current + val dpm = context.getDPM() + val receiver = context.getReceiver() + val packages = remember { mutableStateListOf() } + fun refresh() { + val pm = context.packageManager + packages.clear() + dpm.getKeepUninstalledPackages(receiver)?.forEach { + packages += pm.retrieveAppInfo(it) + } + } + LaunchedEffect(Unit) { refresh() } + MyLazyScaffold(R.string.keep_uninstalled_packages, onNavigateUp) { + items(packages, { it.name }) { + ApplicationItem(it) { + dpm.setKeepUninstalledPackages(receiver, packages.minus(it).map { it.name }) + refresh() + } + } + item { + var packageName by remember { mutableStateOf("") } + PackageNameTextField(packageName, Modifier.padding(HorizontalPadding, 8.dp)) { packageName = it } + Button( + { + dpm.setKeepUninstalledPackages(receiver, packages.map { it.name } + packageName) + packageName = "" + }, + Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding).padding(bottom = 8.dp), + packageName.isValidPackageName + ) { + Text(stringResource(R.string.add)) + } + Notes(R.string.info_keep_uninstalled_apps, HorizontalPadding) + } + } +} + +@Serializable object InstallExistingApp + +@RequiresApi(28) +@Composable +fun InstallExistingAppScreen(onNavigateUp: () -> Unit) { + val context = LocalContext.current + MyScaffold(R.string.install_existing_app, onNavigateUp) { + var packageName by remember { mutableStateOf("") } + PackageNameTextField(packageName, Modifier.padding(vertical = 8.dp)) { packageName = it } + Button( + { + context.showOperationResultToast( + context.getDPM().installExistingPackage(context.getReceiver(), packageName) + ) + }, + Modifier.fillMaxWidth(), + packageName.isValidPackageName + ) { + Text(stringResource(R.string.install)) + } + Notes(R.string.info_install_existing_app) + } +} + +@Serializable object CrossProfilePackages @RequiresApi(30) @Composable -private fun CrossProfilePackagesScreen(pkgName: String) { +fun CrossProfilePackagesScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() - val crossProfilePkg = remember { mutableStateListOf() } - val refresh = { - crossProfilePkg.clear() - crossProfilePkg.addAll(dpm.getCrossProfilePackages(receiver)) + val packages = remember { mutableStateListOf() } + fun refresh() { + val pm = context.packageManager + packages.clear() + dpm.getCrossProfilePackages(receiver).forEach { + packages += pm.retrieveAppInfo(it) + } } LaunchedEffect(Unit) { refresh() } - Column(modifier = Modifier.fillMaxSize().padding(horizontal = HorizontalPadding).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.cross_profile_package), style = typography.headlineLarge) - Text(text = stringResource(R.string.app_list_is)) - Column(modifier = Modifier.animateContentSize()) { - if(crossProfilePkg.isEmpty()) Text(stringResource(R.string.none)) - for(i in crossProfilePkg) { - ListItem(i) { crossProfilePkg -= i } + MyLazyScaffold(R.string.cross_profile_apps, onNavigateUp) { + items(packages, { it.name }) { + ApplicationItem(it) { + dpm.setCrossProfilePackages(receiver, packages.minus(it).map { it.name }.toSet()) + refresh() } } - Button( - onClick = { crossProfilePkg += pkgName }, - modifier = Modifier.fillMaxWidth() - ) { - Text(stringResource(R.string.add)) + item { + var packageName by remember { mutableStateOf("") } + PackageNameTextField(packageName, Modifier.padding(vertical = 8.dp)) { packageName = it } + Button( + { + dpm.setCrossProfilePackages(receiver, packages.map { it.name }.toSet() + packageName) + packageName = "" + refresh() + }, + Modifier.fillMaxWidth(), + packageName.isValidPackageName + ) { + Text(stringResource(R.string.add)) + } } - Button( - onClick = { - dpm.setCrossProfilePackages(receiver, crossProfilePkg.toSet()) - refresh() - }, - modifier = Modifier.fillMaxWidth().padding(top = 8.dp) - ) { - Text(stringResource(R.string.apply)) - } - Spacer(Modifier.padding(vertical = 30.dp)) } } -@Serializable private object CrossProfileWidgetProviders +@Serializable object CrossProfileWidgetProviders @Composable -private fun CrossProfileWidgetProvidersScreen(pkgName: String) { +fun CrossProfileWidgetProvidersScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() val receiver = context.getReceiver() - val pkgList = remember { mutableStateListOf() } - val refresh = { - pkgList.clear() - pkgList.addAll(dpm.getCrossProfileWidgetProviders(receiver)) + val packages = remember { mutableStateListOf() } + fun refresh() { + val pm = context.packageManager + packages.clear() + dpm.getCrossProfileWidgetProviders(receiver).forEach { + packages += pm.retrieveAppInfo(it) + } } LaunchedEffect(Unit) { refresh() } - Column(modifier = Modifier.fillMaxSize().padding(horizontal = HorizontalPadding).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.cross_profile_widget), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) - Text(text = stringResource(R.string.app_list_is)) - Column(modifier = Modifier.animateContentSize()) { - if(pkgList.isEmpty()) Text(stringResource(R.string.none)) - for(i in pkgList) { - ListItem(i) { - dpm.removeCrossProfileWidgetProvider(receiver, i) - refresh() - } + MyLazyScaffold(R.string.cross_profile_widget, onNavigateUp) { + items(packages, { it.name }) { + ApplicationItem(it) { + dpm.removeCrossProfileWidgetProvider(receiver, it.name) + refresh() } } - Spacer(Modifier.padding(vertical = 5.dp)) - Button( - onClick = { - if(pkgName != "") { dpm.addCrossProfileWidgetProvider(receiver, pkgName) } - refresh() - }, - modifier = Modifier.fillMaxWidth() - ) { - Text(stringResource(R.string.add)) + item { + var packageName by remember { mutableStateOf("") } + PackageNameTextField(packageName, Modifier.padding(vertical = 8.dp, horizontal = HorizontalPadding)) { packageName = it } + Button( + { + dpm.addCrossProfileWidgetProvider(receiver, packageName) + packageName = "" + refresh() + }, + Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding), + packageName.isValidPackageName + ) { + Text(stringResource(R.string.add)) + } } - Spacer(Modifier.padding(vertical = 10.dp)) } } -@Serializable private object CredentialManagerPolicy +@Serializable object CredentialManagerPolicy @RequiresApi(34) @Composable -private fun CredentialManagerPolicyScreen(pkgName: String) { +fun CredentialManagerPolicyScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current + val pm = context.packageManager val dpm = context.getDPM() - var policy: PackagePolicy? var policyType by remember{ mutableIntStateOf(-1) } - val pkgList = remember { mutableStateListOf() } - val refreshPolicy = { - policy = dpm.credentialManagerPolicy + val packages = remember { mutableStateListOf() } + fun refresh() { + val policy = dpm.credentialManagerPolicy policyType = policy?.policyType ?: -1 - pkgList.clear() - pkgList.addAll(policy?.packageNames ?: setOf()) + packages.clear() + policy?.packageNames?.forEach { + packages += pm.retrieveAppInfo(it) + } } - LaunchedEffect(Unit) { refreshPolicy() } - Column(modifier = Modifier.fillMaxSize().padding(horizontal = HorizontalPadding).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.credential_manager_policy), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) - RadioButtonItem(R.string.none, policyType == -1) { policyType = -1 } - RadioButtonItem(R.string.blacklist, policyType == PACKAGE_POLICY_BLOCKLIST) { policyType = PACKAGE_POLICY_BLOCKLIST } - RadioButtonItem(R.string.whitelist, policyType == PACKAGE_POLICY_ALLOWLIST){ policyType = PACKAGE_POLICY_ALLOWLIST } - RadioButtonItem( - R.string.whitelist_and_system_app, - policyType == PACKAGE_POLICY_ALLOWLIST_AND_SYSTEM - ) { policyType = PACKAGE_POLICY_ALLOWLIST_AND_SYSTEM } - Spacer(Modifier.padding(vertical = 5.dp)) - AnimatedVisibility(policyType != -1) { - Column { - Text(stringResource(R.string.app_list_is)) - Column(modifier = Modifier.animateContentSize()) { - if(pkgList.isEmpty()) Text(stringResource(R.string.none)) - for(i in pkgList) { - ListItem(i) { pkgList -= i } - } - } + LaunchedEffect(Unit) { refresh() } + MyLazyScaffold(R.string.credential_manager_policy, onNavigateUp) { + item { + mapOf( + -1 to R.string.none, + PackagePolicy.PACKAGE_POLICY_BLOCKLIST to R.string.blacklist, + PackagePolicy.PACKAGE_POLICY_ALLOWLIST to R.string.whitelist, + PackagePolicy.PACKAGE_POLICY_ALLOWLIST_AND_SYSTEM to R.string.whitelist_and_system_app + ).forEach { (key, value) -> + FullWidthRadioButtonItem(value, policyType == key) { policyType = key } + } + Spacer(Modifier.padding(vertical = 4.dp)) + } + items(packages, { it.name }) { + ApplicationItem(it) { packages -= it } + } + item { + Column(Modifier.padding(horizontal = HorizontalPadding)) { + var packageName by remember { mutableStateOf("") } + PackageNameTextField(packageName, Modifier.padding(vertical = 8.dp)) { packageName = it } Button( - onClick = { pkgList += pkgName }, - modifier = Modifier.fillMaxWidth() + { + packages += pm.retrieveAppInfo(packageName) + }, + Modifier.fillMaxWidth(), + enabled = packageName.isValidPackageName ) { Text(stringResource(R.string.add)) } Button( - onClick = { + { try { - if(policyType != -1 && pkgList.isNotEmpty()) { - dpm.credentialManagerPolicy = PackagePolicy(policyType, pkgList.toSet()) + if(policyType != -1 && packages.isNotEmpty()) { + dpm.credentialManagerPolicy = PackagePolicy(policyType, packages.map { it.name }.toSet()) } else { - dpm.credentialManagerPolicy = null + dpm.credentialManagerPolicy = null } context.showOperationResultToast(true) } catch(_: IllegalArgumentException) { - Toast.makeText(context, R.string.failed, Toast.LENGTH_SHORT).show() + context.showOperationResultToast(false) } finally { - refreshPolicy() + refresh() } }, - modifier = Modifier.fillMaxWidth().padding(top = 8.dp) + Modifier.fillMaxWidth() ) { Text(stringResource(R.string.apply)) } } } - Spacer(Modifier.padding(vertical = 30.dp)) } } -@Serializable private object PermittedAccessibilityServices +@Serializable object PermittedAccessibilityServices @Composable -private fun PermittedAccessibilityServicesScreen(pkgName: String) { +fun PermittedAccessibilityServicesScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current + val pm = context.packageManager val dpm = context.getDPM() val receiver = context.getReceiver() - val pkgList = remember { mutableStateListOf() } + val packages = remember { mutableStateListOf() } var allowAll by remember { mutableStateOf(true) } - val refresh = { - pkgList.clear() - val getList = dpm.getPermittedAccessibilityServices(receiver) - allowAll = getList == null - pkgList.addAll(getList ?: listOf()) + fun refresh() { + packages.clear() + val list = dpm.getPermittedAccessibilityServices(receiver) + allowAll = list == null + list?.forEach { + packages += pm.retrieveAppInfo(it) + } } LaunchedEffect(Unit) { refresh() } - Column(modifier = Modifier.fillMaxSize().padding(horizontal = HorizontalPadding).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.permitted_accessibility_services), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) - Row( - horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth().padding(horizontal = 6.dp, vertical = 8.dp) - ) { - Text(stringResource(R.string.allow_all), style = typography.titleLarge) - Switch( - checked = allowAll, - onCheckedChange = { - dpm.setPermittedAccessibilityServices(receiver, if(it) null else listOf()) + MyLazyScaffold(R.string.permitted_accessibility_services, onNavigateUp) { + item { + SwitchItem(R.string.allow_all, state = allowAll, onCheckedChange = { allowAll = it }) + } + items(packages, { it.name }) { + ApplicationItem(it) { packages -= it } + } + item { + var packageName by remember { mutableStateOf("") } + PackageNameTextField(packageName, Modifier.padding(vertical = 8.dp, horizontal = HorizontalPadding)) { packageName = it } + Button( + { + packages += pm.retrieveAppInfo(packageName) + packageName = "" + }, + Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding), + packageName.isValidPackageName + ) { + Text(stringResource(R.string.add)) + } + Button( + { + val result = dpm.setPermittedAccessibilityServices(receiver, if(allowAll) null else packages.map { it.name }) + context.showOperationResultToast(result) refresh() - } - ) - } - AnimatedVisibility(!allowAll) { - Column { - Column(modifier = Modifier.animateContentSize()) { - Text(stringResource(if(pkgList.isEmpty()) R.string.only_system_accessibility_allowed else R.string.permitted_packages_is)) - if(pkgList.isEmpty()) Text(stringResource(R.string.none)) - for(i in pkgList) { - ListItem(i) { pkgList -= i } - } - } - Spacer(Modifier.padding(vertical = 5.dp)) - Button( - onClick = { pkgList += pkgName }, - modifier = Modifier.fillMaxWidth() - ) { - Text(stringResource(R.string.add)) - } - Button( - onClick = { - dpm.setPermittedAccessibilityServices(receiver, pkgList) - refresh() - }, - modifier = Modifier.fillMaxWidth().padding(top = 8.dp) - ) { - Text(stringResource(R.string.apply)) - } + }, + Modifier.fillMaxWidth().padding(top = 8.dp).padding(horizontal = HorizontalPadding) + ) { + Text(stringResource(R.string.apply)) } + Notes(R.string.system_accessibility_always_allowed, HorizontalPadding) } - Notes(R.string.system_accessibility_always_allowed) - Spacer(Modifier.padding(vertical = 30.dp)) } } -@Serializable private object PermittedInputMethods +@Serializable object PermittedInputMethods @Composable -private fun PermittedInputMethodsScreen(pkgName: String) { +fun PermittedInputMethodsScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current + val pm = context.packageManager val dpm = context.getDPM() val receiver = context.getReceiver() - val pkgList = remember { mutableStateListOf() } + val packages = remember { mutableStateListOf() } var allowAll by remember { mutableStateOf(true) } - val refresh = { - pkgList.clear() - val getList = dpm.getPermittedInputMethods(receiver) - allowAll = getList == null - pkgList.addAll(getList ?: listOf()) + fun refresh() { + packages.clear() + val list = dpm.getPermittedInputMethods(receiver) + allowAll = list == null + list?.forEach { + packages += pm.retrieveAppInfo(it) + } } LaunchedEffect(Unit) { refresh() } - Column(modifier = Modifier.fillMaxSize().padding(horizontal = HorizontalPadding).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.permitted_ime), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) - SwitchItem( - R.string.allow_all, state = allowAll, - onCheckedChange = { - dpm.setPermittedInputMethods(receiver, if(it) null else listOf()) - refresh() - }, padding = false - ) - AnimatedVisibility(!allowAll) { - Column { - Column(modifier = Modifier.animateContentSize()) { - Text(stringResource(if(pkgList.isEmpty()) R.string.only_system_ime_allowed else R.string.permitted_packages_is)) - for(i in pkgList) { - ListItem(i) { pkgList -= i } - } - } - Spacer(Modifier.padding(vertical = 5.dp)) - Button( - onClick = { pkgList += pkgName }, - modifier = Modifier.fillMaxWidth() - ) { - Text(stringResource(R.string.add)) - } - Button( - onClick = { - dpm.setPermittedInputMethods(receiver, pkgList) - refresh() - }, - modifier = Modifier.fillMaxWidth().padding(top = 8.dp) - ) { - Text(stringResource(R.string.apply)) - } - } + MyLazyScaffold(R.string.permitted_ime, onNavigateUp) { + item { + SwitchItem(R.string.allow_all, state = allowAll, onCheckedChange = { allowAll = it }) + } + items(packages, { it.name }) { + ApplicationItem(it) { packages -= it } + } + item { + var packageName by remember { mutableStateOf("") } + PackageNameTextField(packageName, Modifier.padding(vertical = 8.dp, horizontal = HorizontalPadding)) { packageName = it } + Button( + { + packages += pm.retrieveAppInfo(packageName) + packageName = "" + }, + Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding), + packageName.isValidPackageName + ) { + Text(stringResource(R.string.add)) + } + Button( + { + val result = dpm.setPermittedInputMethods(receiver, if(allowAll) null else packages.map { it.name }) + context.showOperationResultToast(result) + refresh() + }, + Modifier.fillMaxWidth().padding(top = 8.dp).padding(horizontal = HorizontalPadding) + ) { + Text(stringResource(R.string.apply)) + } + Notes(R.string.system_ime_always_allowed, HorizontalPadding) } - Notes(R.string.system_ime_always_allowed) - Spacer(Modifier.padding(vertical = 30.dp)) } } -@Serializable private object KeepUninstalledPackages +@Serializable object EnableSystemApp -@RequiresApi(28) @Composable -private fun KeepUninstalledPackagesScreen(pkgName: String) { +fun EnableSystemAppScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current - val dpm = context.getDPM() - val receiver = context.getReceiver() - val pkgList = remember { mutableStateListOf() } - val refresh = { - pkgList.clear() - dpm.getKeepUninstalledPackages(receiver)?.forEach { pkgList += it } - } - LaunchedEffect(Unit) { refresh() } - Column(modifier = Modifier.fillMaxSize().padding(horizontal = HorizontalPadding).verticalScroll(rememberScrollState())) { - Spacer(Modifier.padding(vertical = 10.dp)) - Text(text = stringResource(R.string.keep_uninstalled_packages), style = typography.headlineLarge) - Spacer(Modifier.padding(vertical = 5.dp)) - Text(text = stringResource(R.string.app_list_is)) - Column(modifier = Modifier.animateContentSize()) { - if(pkgList.isEmpty()) Text(stringResource(R.string.none)) - for(i in pkgList) { - ListItem(i) { pkgList -= i } - } - } - Spacer(Modifier.padding(vertical = 5.dp)) + MyScaffold(R.string.enable_system_app, onNavigateUp) { + var packageName by remember { mutableStateOf("") } + Spacer(Modifier.padding(vertical = 4.dp)) + PackageNameTextField(packageName, Modifier.padding(bottom = 8.dp)) { packageName = it } Button( - onClick = { pkgList += pkgName }, - modifier = Modifier.fillMaxWidth() - ) { - Text(stringResource(R.string.add)) - } - Button( - onClick = { - dpm.setKeepUninstalledPackages(receiver, pkgList) - refresh() + { + context.getDPM().enableSystemApp(context.getReceiver(), packageName) + packageName = "" + context.showOperationResultToast(true) }, - modifier = Modifier.fillMaxWidth().padding(top = 8.dp) + Modifier.fillMaxWidth(), + packageName.isValidPackageName ) { - Text(stringResource(R.string.apply)) + Text(stringResource(R.string.enable)) } - Notes(R.string.info_keep_uninstalled_apps) - Spacer(Modifier.padding(vertical = 30.dp)) } } -private fun uninstallPackage(context: Context, packageName: String, onError: (String) -> Unit) { +@Serializable object SetDefaultDialer + +@RequiresApi(34) +@Composable +fun SetDefaultDialerScreen(onNavigateUp: () -> Unit) { + val context = LocalContext.current + var errorMessage by remember { mutableStateOf(null) } + MyScaffold(R.string.set_default_dialer, onNavigateUp) { + var packageName by remember { mutableStateOf("") } + Spacer(Modifier.padding(vertical = 4.dp)) + PackageNameTextField(packageName, Modifier.padding(bottom = 8.dp)) { packageName = it } + Button( + { + try { + context.getDPM().setDefaultDialerApplication(packageName) + context.showOperationResultToast(true) + } catch(e: Exception) { + errorMessage = e.message + } + }, + Modifier.fillMaxWidth(), + packageName.isValidPackageName + ) { + Text(stringResource(R.string.set)) + } + } + ErrorDialog(errorMessage) { errorMessage = null } +} + +private fun uninstallPackage(context: Context, packageName: String, onComplete: (String?) -> Unit) { val receiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val statusExtra = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 999) @@ -976,9 +1104,9 @@ private fun uninstallPackage(context: Context, packageName: String, onError: (St } else { context.unregisterReceiver(this) if(statusExtra == PackageInstaller.STATUS_SUCCESS) { - context.showOperationResultToast(true) + onComplete(null) } else { - onError(parsePackageInstallerMessage(context, intent)) + onComplete(parsePackageInstallerMessage(context, intent)) } } } 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 baf8b2c..0e88921 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt @@ -602,9 +602,17 @@ fun AddDelegatedAdminScreen(data: AddDelegatedAdmin, onNavigateUp: () -> Unit) { readOnly = updateMode, modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp, horizontal = HorizontalPadding) ) - DelegatedScope.entries.filter { VERSION.SDK_INT >= it.requiresApi }.forEach {scope -> - FullWidthCheckBoxItem(scope.string, scope in scopes) { - if(it) scopes += scope else scopes -= scope + DelegatedScope.entries.filter { VERSION.SDK_INT >= it.requiresApi }.forEach { scope -> + val checked = scope in scopes + Row( + Modifier.fillMaxWidth().clickable { if(!checked) scopes += scope else scopes -= scope }.padding(vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox(checked, { if(it) scopes += scope else scopes -= scope }, modifier = Modifier.padding(horizontal = 4.dp)) + Column { + Text(stringResource(scope.string)) + Text(scope.id, style = typography.bodyMedium, color = colorScheme.onSurfaceVariant) + } } } Button( diff --git a/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt b/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt index 45cb619..016efdc 100644 --- a/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt +++ b/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt @@ -9,6 +9,8 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.selection.SelectionContainer @@ -319,6 +321,28 @@ fun MyScaffold( } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MyLazyScaffold( + @StringRes title: Int, + onNavIconClicked: () -> Unit, + content: LazyListScope.() -> Unit +) { + val sb = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + Scaffold( + Modifier.nestedScroll(sb.nestedScrollConnection), + topBar = { + LargeTopAppBar( + { Text(stringResource(title)) }, + navigationIcon = { NavIcon(onNavIconClicked) }, + scrollBehavior = sb + ) + } + ) { paddingValues -> + LazyColumn(Modifier.fillMaxSize().padding(paddingValues), content = content) + } +} + @OptIn(ExperimentalMaterial3Api::class) @Composable fun MySmallTitleScaffold( diff --git a/app/src/main/res/drawable/shield_fill0.xml b/app/src/main/res/drawable/shield_fill0.xml new file mode 100644 index 0000000..8e3b2ca --- /dev/null +++ b/app/src/main/res/drawable/shield_fill0.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 12c39f9..a8eed7d 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -377,39 +377,27 @@ Включить блокировку Очистить текущую конфигурацию Разрешение - Область действия: рабочий профиль - Информация о приложении Не установлено Блокировать удаление Включить системное приложение - Повторно включить системное приложение, отключенное по умолчанию - Отключить управление пользователем - Если вы установите этот флажок, вы не сможете очистить хранилище этих приложений или принудительно остановить их. - Список приложений: + Отключить управление пользователем Disable metered data - Кросс-профильный пакет + Cross profile apps Кросс-профильный виджет Credential manager policy Белый список и системные приложения - Разрешенные пакеты: \n Разрешенные службы доступности - Разрешены только системные службы доступности Системные службы доступности всегда разрешены. - Разрешены только системные методы ввода Системные методы ввода всегда разрешены. Разрешенный метод ввода Сохранять удаленные пакеты - Очистить данные Очистить хранилище приложения - Хранилище этого приложения будет очищено Установить приложение для звонков по умолчанию - Это приложение будет установлено в качестве приложения для звонков по умолчанию. Удалить приложение Установить приложение - - Choose an APK file - Install existing app + Install existing app + Keep after uninstall Поиск @@ -699,6 +687,7 @@ Пользователь не сможет очищать данные приложений или принудительно останавливать пакеты. Установить список приложений, которые нужно сохранить в виде APK-файлов, даже если ни у одного пользователя в данный момент они не установлены. Install an existing package that has been installed in another user, or has been kept after uninstall. + Re-enable a system app that was disabled by default when the user was initialized. Режим "безголового" системного пользователя означает, что системный пользователь запускает системные службы и некоторый системный интерфейс, но он не связан с каким-либо реальным человеком, и для связи с реальными людьми должны быть созданы дополнительные пользователи. If the current user is not switched by OwnDroid, this function cannot be used. Когда владелец устройства создает управляемого пользователя, управляемый пользователь не является аффилированным. Чтобы сделать управляемого пользователя аффилированным с владельцем устройства, вам следует установить одинаковые аффилированные идентификаторы в основном и управляемом пользователях. diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 93fff2e..12a75a2 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -385,38 +385,27 @@ Kilitlemeyi Etkinleştir Mevcut Yapılandırmayı Temizle İzinler - Kapsam: İş Profili - Uygulama Bilgisi Yüklenmedi Kaldırmayı Engelle Sistem Uygulamasını Etkinleştir - Varsayılan olarak devre dışı bırakılmış bir sistem uygulamasını yeniden etkinleştir - Kullanıcı Kontrolünü Devre Dışı Bırak - Bunu ayarlarsanız, bu uygulamaların depolamasını temizleyemez veya zorla durduramazsınız. - Uygulama listesi: + Kullanıcı Kontrolünü Devre Dışı Bırak Disable metered data - Çapraz Profil Paketi + Cross profile apps Çapraz Profil Widget\'ı Credential manager policy Beyaz Liste ve Sistem Uygulaması - İzin Verilen Paketler: \n İzin Verilen Erişilebilirlik Servisleri - Yalnızca sistem erişilebilirlik servislerine izin verilir Sistem erişilebilirlik servisleri her zaman izinlidir. - Yalnızca sistem giriş yöntemlerine izin verilir Sistem giriş yöntemleri her zaman izinlidir. İzin Verilen IME Kaldırılmış Paketleri Koru - Verileri Temizle Uygulama Depolamasını Temizle - Bu uygulamanın depolaması temizlenecek Varsayılan Arama Uygulamasını Ayarla - Bu uygulama varsayılan arama uygulaması olarak ayarlanacak. Uygulamayı Kaldır Uygulama Yükle - Bir APK Dosyası Seç Mevcut Uygulamayı Yükle + Keep after uninstall Ara @@ -702,6 +691,7 @@ Kullanıcı uygulama verilerini temizleyemez veya paketleri zorla durduramaz. Hiçbir kullanıcının şu anda yüklemediği uygulamaları APK olarak saklanacak bir liste ayarlar. Başka bir kullanıcıda yüklenmiş veya kaldırıldıktan sonra saklanmış mevcut bir paketi yükler. + Re-enable a system app that was disabled by default when the user was initialized. Başıboş sistem kullanıcı modu, sistem kullanıcısının sistem servislerini ve bazı sistem kullanıcı arayüzlerini çalıştırdığı, ancak herhangi bir gerçek kişiyle ilişkilendirilmediği ve gerçek kişilerle ilişkilendirilecek ek kullanıcıların oluşturulması gerektiği anlamına gelir. Mevcut kullanıcı OwnDroid tarafından değiştirilmediyse bu işlev kullanılamaz. Cihaz Sahibi bir yönetilen kullanıcı oluşturduğunda, yönetilen kullanıcı bağlı değildir. Yönetilen kullanıcının Cihaz Sahibi ile bağlı hale getirilmesi için ana kullanıcı ve yönetilen kullanıcıda aynı bağlılık kimlikleri ayarlanmalıdır. diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 643423f..c3fee51 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -368,37 +368,26 @@ 启用锁定 清除当前配置 权限 - 作用域: 工作资料 - 应用详情 未安装 阻止卸载 - 禁止用户控制 - 用户将无法清除这些应用的存储空间或强制停止这些应用 - 应用列表: + 禁止用户控制 禁用计量数据 - 跨资料应用 + 跨资料应用 跨资料微件 凭据管理器策略 白名单和系统应用 - 许可的应用:\n 许可的无障碍服务 - 只允许系统无障碍服务 系统的无障碍服务不受影响 - 只允许系统输入法 系统输入法不受影响 许可的输入法 卸载后保留的应用 - 数据清除 清除应用存储 - 这个应用的存储空间将被清空 - 设为默认拨号应用 - 这个应用将被设为默认拨号应用 + 设置默认拨号器 卸载应用 安装应用 - 选择一个APK文件 安装已存在的应用 启用系统应用 - 重新启用一个默认被禁用的系统应用 + 卸载后保留 搜索 @@ -698,6 +687,7 @@ 用户无法清除这些应用的存储空间,也无法强制停止应用 这个列表中的应用的APK将会一直保留,即使没有任何用户安装这个应用 安装一个已经在其他用户中安装或在卸载后保留的app。 + 重新启用一个在用户初始化时默认被禁用的系统app 无头系统用户模式意味着系统用户运行系统服务和一些系统UI,但它不与任何真实的人相关联,必须创建额外的用户才能与真实的人相关联。 如果当前用户不是由OwnDroid切换的,无法使用此功能。 当Device owner创建并管理用户时,新的用户不是附属用户。Device owner设置和受管理用户完全相同的附属用户ID后,受管理用户成为附属于Device owner的用户 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a3dc99d..62827ca 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -404,38 +404,26 @@ Enable lockdown Clear current config Permissions - Scope: work profile - App info Not installed Block uninstall Enable system app - Re-enable a system app that was disabled by default - - Disable user control - If you set this, you cannot clear these apps\' storage or force stop them. - App list: + Disable user control Disable metered data - Cross profile package + Cross profile apps Cross profile widget Credential manager policy Whitelist and system app - Permitted packages: \n Permitted accessibility services - Only system accessibility services are allowed System accessibility services are always allowed. - Only system input methods are allowed System input methods are always allowed. Permitted IME Keep uninstalled packages - Clear data Clear app storage - This app\'s storage will be cleared Set default dialer - This app will be set as the default dialer application. Uninstall app Install app - Choose an APK file Install existing app + Keep after uninstall Search @@ -738,6 +726,7 @@ User will not be able to clear app data or force-stop packages. Set a list of apps to keep around as APKs even if no user has currently installed it. Install an existing package that has been installed in another user, or has been kept after uninstall. + Re-enable a system app that was disabled by default when the user was initialized. Headless system user mode means the system user runs system services and some system UI, but it is not associated with any real person and additional users must be created to be associated with real persons. If the current user is not switched by OwnDroid, this function cannot be used. When Device owner create a managed user, the managed user isn\'t affiliated. In order to make the managed user affiliated with the Device owner, you should set same affiliated IDs in main user and managed user