diff --git a/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt b/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt index 62bb659..5e66a62 100644 --- a/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt +++ b/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt @@ -1,6 +1,5 @@ package com.bintianqi.owndroid -import android.app.Application import android.content.Intent import android.content.pm.ApplicationInfo import android.content.pm.PackageManager @@ -32,7 +31,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold @@ -56,9 +55,8 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp -import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewModelScope +import androidx.lifecycle.lifecycleScope import com.bintianqi.owndroid.ui.theme.OwnDroidTheme import com.google.accompanist.drawablepainter.rememberDrawablePainter import kotlinx.coroutines.Dispatchers @@ -70,46 +68,41 @@ import kotlinx.coroutines.withContext class PackageChooserActivity: ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val myVm by viewModels() - val vm by viewModels() - vm.initialize() + val vm by viewModels() + if(getPackagesProgress.value < 1F) getPackages() setContent { - val theme by myVm.theme.collectAsStateWithLifecycle() + val theme by vm.theme.collectAsStateWithLifecycle() OwnDroidTheme(theme) { - val packages by vm.packages.collectAsStateWithLifecycle() - val progress by vm.progress.collectAsStateWithLifecycle() - PackageChooserScreen(packages, progress, vm::getPackages) { + val packages by installedPackages.collectAsStateWithLifecycle() + val progress by getPackagesProgress.collectAsStateWithLifecycle() + PackageChooserScreen(packages, progress, ::getPackages) { setResult(0, Intent().putExtra("package", it)) finish() } } } } -} - -class PackageChooserViewModel(application: Application): AndroidViewModel(application) { - val packages = MutableStateFlow(emptyList()) - val progress = MutableStateFlow(0F) val flags = if(Build.VERSION.SDK_INT >= 24) PackageManager.MATCH_DISABLED_COMPONENTS or PackageManager.MATCH_UNINSTALLED_PACKAGES else 0 - fun initialize() { - if(progress.value < 1F) getPackages() - } fun getPackages() { - packages.value = emptyList() - viewModelScope.launch(Dispatchers.IO) { - val pm = getApplication().packageManager + installedPackages.value = emptyList() + lifecycleScope.launch(Dispatchers.IO) { + val pm = packageManager val apps = pm.getInstalledApplications(flags) for(pkg in apps) { - packages.update { + installedPackages.update { it + PackageInfo( pkg.packageName, pkg.loadLabel(pm).toString(), pkg.loadIcon(pm), (pkg.flags and ApplicationInfo.FLAG_SYSTEM) != 0 ) } - withContext(Dispatchers.Main) { progress.value = packages.value.size.toFloat() / apps.size } + withContext(Dispatchers.Main) { getPackagesProgress.value = installedPackages.value.size.toFloat() / apps.size } } } } + companion object { + val installedPackages = MutableStateFlow(emptyList()) + val getPackagesProgress = MutableStateFlow(0F) + } } data class PackageInfo( @@ -186,7 +179,7 @@ private fun PackageChooserScreen( Icon(Icons.AutoMirrored.Default.ArrowBack, null) } }, - colors = TopAppBarDefaults.topAppBarColors(containerColor = colorScheme.background) + colors = TopAppBarDefaults.topAppBarColors(MaterialTheme.colorScheme.surfaceContainer) ) } ) { paddingValues-> diff --git a/app/src/main/java/com/bintianqi/owndroid/Utils.kt b/app/src/main/java/com/bintianqi/owndroid/Utils.kt index 720a7f5..6553439 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Utils.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Utils.kt @@ -10,7 +10,6 @@ import android.content.Intent import android.net.Uri import android.os.Build import android.os.Bundle -import android.os.Process import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.result.contract.ActivityResultContract @@ -97,6 +96,9 @@ fun parseDate(date: Date) val Long.humanReadableDate: String get() = SimpleDateFormat("yyyy/MM/dd", Locale.getDefault()).format(Date(this)) +val Long.humanReadableDateTime: String + get() = SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.getDefault()).format(Date(this)) + fun Context.showOperationResultToast(success: Boolean) { Toast.makeText(this, if(success) R.string.success else R.string.failed, Toast.LENGTH_SHORT).show() } 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 6e75add..45d49b3 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt @@ -2,7 +2,6 @@ package com.bintianqi.owndroid.dpm import android.app.AlertDialog import android.app.PendingIntent -import android.app.admin.DevicePolicyManager 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 @@ -15,7 +14,6 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.pm.PackageInstaller -import android.content.pm.PackageManager.NameNotFoundException import android.net.Uri import android.os.Build.VERSION import android.os.Looper @@ -50,11 +48,11 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton 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.TextField import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable @@ -88,8 +86,8 @@ import androidx.navigation.compose.rememberNavController import com.bintianqi.owndroid.APK_MIME import com.bintianqi.owndroid.AppInstallerActivity import com.bintianqi.owndroid.AppInstallerViewModel -import com.bintianqi.owndroid.R import com.bintianqi.owndroid.ChoosePackageContract +import com.bintianqi.owndroid.R import com.bintianqi.owndroid.showOperationResultToast import com.bintianqi.owndroid.ui.Animations import com.bintianqi.owndroid.ui.FunctionItem @@ -117,13 +115,13 @@ fun ApplicationsScreen(onNavigateUp: () -> Unit) { topBar = { TopAppBar( title = { - TextField( + 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(onDone = { focusMgr.clearFocus() }), + keyboardActions = KeyboardActions { focusMgr.clearFocus() }, trailingIcon = { IconButton({ focusMgr.clearFocus() @@ -137,7 +135,7 @@ fun ApplicationsScreen(onNavigateUp: () -> Unit) { ) }, navigationIcon = { NavIcon(onNavigateUp) }, - colors = TopAppBarDefaults.topAppBarColors(containerColor = colorScheme.background) + colors = TopAppBarDefaults.topAppBarColors(colorScheme.surfaceContainer) ) } ) { paddingValues-> @@ -167,6 +165,7 @@ fun ApplicationsScreen(onNavigateUp: () -> Unit) { @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 */ var dialogStatus by remember { mutableIntStateOf(0) } val context = LocalContext.current val dpm = context.getDPM() @@ -176,31 +175,26 @@ private fun HomeScreen(pkgName: String, onNavigate: (Any) -> Unit) { var suspend by remember { mutableStateOf(false) } var hide by remember { mutableStateOf(false) } var blockUninstall by remember { mutableStateOf(false) } - var appControlAction by remember { mutableIntStateOf(0) } + var appControlAction by remember { mutableIntStateOf(0) } // 1:Suspend, 2:Hide, 3:Block uninstall val focusMgr = LocalFocusManager.current - val appControl: (Boolean) -> Unit = { - when(appControlAction) { - 1 -> if(VERSION.SDK_INT >= 24) dpm.setPackagesSuspended(receiver, arrayOf(pkgName), it) - 2 -> dpm.setApplicationHidden(receiver, pkgName, it) - 3 -> dpm.setUninstallBlocked(receiver, pkgName, it) + fun refresh() { + if(VERSION.SDK_INT >= 24) { + try { + suspend = dpm.isPackageSuspended(receiver, pkgName) + } catch(_: Exception) {} } - when(appControlAction) { - 1 -> { - suspend = try{ if(VERSION.SDK_INT >= 24) dpm.isPackageSuspended(receiver, pkgName) else false } - catch(_: NameNotFoundException) { false } - catch(_: IllegalArgumentException) { false } - } - 2 -> hide = dpm.isApplicationHidden(receiver,pkgName) - 3 -> blockUninstall = dpm.isUninstallBlocked(receiver,pkgName) - } - } - LaunchedEffect(pkgName) { - suspend = try{ if(VERSION.SDK_INT >= 24) dpm.isPackageSuspended(receiver, pkgName) else false } - catch(_: NameNotFoundException) { false } - catch(_: IllegalArgumentException) { false } hide = dpm.isApplicationHidden(receiver, pkgName) - blockUninstall = dpm.isUninstallBlocked(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) + } + refresh() + } + LaunchedEffect(pkgName) { refresh() } Column( modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()) ) { @@ -222,7 +216,7 @@ private fun HomeScreen(pkgName: String, onNavigate: (Any) -> Unit) { ) } SwitchItem( - title = R.string.hide, desc = stringResource(R.string.isapphidden_desc), icon = R.drawable.visibility_off_fill0, + title = R.string.hide, icon = R.drawable.visibility_off_fill0, state = hide, onCheckedChange = { appControlAction = 2; appControl(it) }, onClickBlank = { appControlAction = 2; dialogStatus = 4 } @@ -270,8 +264,10 @@ private fun HomeScreen(pkgName: String, onNavigate: (Any) -> Unit) { 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) { dialogStatus = 5 } FunctionItem(title = R.string.uninstall_app, icon = R.drawable.delete_fill0) { onNavigate(UninstallPackage) } if(VERSION.SDK_INT >= 34 && (deviceOwner || dpm.isOrgProfile(receiver))) { FunctionItem(title = R.string.set_default_dialer, icon = R.drawable.call_fill0) { @@ -287,22 +283,20 @@ private fun HomeScreen(pkgName: String, onNavigate: (Any) -> Unit) { }, onDismissRequest = { dialogStatus = 0 }, dismissButton = { - TextButton(onClick = { dialogStatus = 0 }) { + TextButton({ dialogStatus = 0 }) { Text(stringResource(R.string.cancel)) } }, confirmButton = { - TextButton( - onClick = { - try { - dpm.enableSystemApp(receiver, pkgName) - context.showOperationResultToast(true) - } catch(_: IllegalArgumentException) { - Toast.makeText(context, R.string.failed, Toast.LENGTH_SHORT).show() - } - dialogStatus = 0 + 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)) } }, @@ -317,16 +311,15 @@ private fun HomeScreen(pkgName: String, onNavigate: (Any) -> Unit) { TextButton( onClick = { val executor = Executors.newCachedThreadPool() - val onClear = DevicePolicyManager.OnClearApplicationUserDataListener { pkg: String, succeed: Boolean -> + dpm.clearApplicationUserData(receiver, pkgName, executor) { pkg: String, succeed: Boolean -> Looper.prepare() val toastText = - if(pkg!="") { "$pkg\n" }else{ "" } + + 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() } - dpm.clearApplicationUserData(receiver, pkgName, executor, onClear) dialogStatus = 0 }, colors = ButtonDefaults.textButtonColors(contentColor = colorScheme.error) @@ -335,9 +328,7 @@ private fun HomeScreen(pkgName: String, onNavigate: (Any) -> Unit) { } }, dismissButton = { - TextButton( - onClick = { dialogStatus = 0 } - ) { + TextButton({ dialogStatus = 0 }) { Text(text = stringResource(R.string.cancel)) } }, @@ -351,22 +342,20 @@ private fun HomeScreen(pkgName: String, onNavigate: (Any) -> Unit) { }, onDismissRequest = { dialogStatus = 0 }, dismissButton = { - TextButton(onClick = { dialogStatus = 0 }) { + TextButton({ dialogStatus = 0 }) { Text(stringResource(R.string.cancel)) } }, confirmButton = { - TextButton( - onClick = { - try{ - dpm.setDefaultDialerApplication(pkgName) - context.showOperationResultToast(true) - } catch(_: IllegalArgumentException) { - Toast.makeText(context, R.string.failed, Toast.LENGTH_SHORT).show() - } - dialogStatus = 0 + TextButton({ + try { + dpm.setDefaultDialerApplication(pkgName) + context.showOperationResultToast(true) + } catch(_: IllegalArgumentException) { + context.showOperationResultToast(false) } - ) { + dialogStatus = 0 + }) { Text(stringResource(R.string.confirm)) } }, @@ -379,18 +368,15 @@ private fun HomeScreen(pkgName: String, onNavigate: (Any) -> Unit) { AlertDialog( onDismissRequest = { dialogStatus = 0 }, title = { - Text( - 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 - } - ), - style = typography.headlineMedium - ) + 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){ @@ -406,27 +392,38 @@ private fun HomeScreen(pkgName: String, onNavigate: (Any) -> Unit) { } }, confirmButton = { - TextButton( - onClick = { - appControl(true) - dialogStatus = 0 - } - ) { + TextButton({ + appControl(true) + dialogStatus = 0 + }) { Text(text = stringResource(R.string.enable)) } }, dismissButton = { - TextButton( - onClick = { - appControl(false) - dialogStatus = 0 - } - ) { + 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 } + ) LaunchedEffect(dialogStatus) { focusMgr.clearFocus() } } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt index afb64d6..0942cb0 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Network.kt @@ -98,6 +98,7 @@ import androidx.compose.material3.TabRow import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberDatePickerState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -129,6 +130,7 @@ import com.bintianqi.owndroid.R import com.bintianqi.owndroid.SharedPrefs import com.bintianqi.owndroid.formatFileSize import com.bintianqi.owndroid.humanReadableDate +import com.bintianqi.owndroid.humanReadableDateTime import com.bintianqi.owndroid.showOperationResultToast import com.bintianqi.owndroid.ui.CheckBoxItem import com.bintianqi.owndroid.ui.ExpandExposedTextFieldIcon @@ -136,6 +138,7 @@ import com.bintianqi.owndroid.ui.FunctionItem import com.bintianqi.owndroid.ui.InfoCard import com.bintianqi.owndroid.ui.ListItem import com.bintianqi.owndroid.ui.MyScaffold +import com.bintianqi.owndroid.ui.MySmallTitleScaffold import com.bintianqi.owndroid.ui.NavIcon import com.bintianqi.owndroid.ui.RadioButtonItem import com.bintianqi.owndroid.ui.SwitchItem @@ -231,7 +234,8 @@ fun WifiScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit, onNavigateTo topBar = { TopAppBar( title = { Text(stringResource(R.string.wifi)) }, - navigationIcon = { NavIcon(onNavigateUp) } + navigationIcon = { NavIcon(onNavigateUp) }, + colors = TopAppBarDefaults.topAppBarColors(MaterialTheme.colorScheme.surfaceContainer) ) } ) { paddingValues -> @@ -466,7 +470,7 @@ object AddNetwork @Composable fun AddNetworkScreen(data: Bundle, onNavigateUp: () -> Unit) { - MyScaffold(R.string.update_network, 0.dp, onNavigateUp, false) { + MySmallTitleScaffold(R.string.update_network, 0.dp, onNavigateUp) { AddNetworkScreen(data.getParcelable("wifi_configuration"), onNavigateUp) } } @@ -1213,8 +1217,6 @@ fun NetworkStatsScreen(onNavigateUp: () -> Unit, onNavigateToViewer: (NetworkSta context.showOperationResultToast(false) } } else { - val bundle = Bundle() - bundle.putInt("size", buckets.size) val stats = buckets.map { NetworkStatsViewer.Data( it.rxBytes, it.rxPackets, it.txBytes, it.txPackets, @@ -1289,7 +1291,7 @@ data class NetworkStatsViewer( fun NetworkStatsViewerScreen(nsv: NetworkStatsViewer, onNavigateUp: () -> Unit) { var index by remember { mutableIntStateOf(0) } val size = nsv.stats.size - MyScaffold(R.string.place_holder, 8.dp, onNavigateUp, false) { + MySmallTitleScaffold(R.string.place_holder, 8.dp, onNavigateUp) { if(size > 1) Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.align(Alignment.CenterHorizontally).padding(bottom = 8.dp) @@ -1310,7 +1312,7 @@ fun NetworkStatsViewerScreen(nsv: NetworkStatsViewer, onNavigateUp: () -> Unit) } val data = nsv.stats[index] Text( - data.startTime.humanReadableDate + " ~ " + data.endTime.humanReadableDate, + data.startTime.humanReadableDateTime + " ~ " + data.endTime.humanReadableDateTime, modifier = Modifier.align(Alignment.CenterHorizontally).padding(bottom = 8.dp) ) val txBytes = data.txBytes @@ -1556,10 +1558,11 @@ fun RecommendedGlobalProxyScreen(onNavigateUp: () -> Unit) { label = { Text(stringResource(R.string.excluded_hosts)) }, maxLines = 5, minLines = 2, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions { focusMgr.clearFocus() }, modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp) ) } - Spacer(Modifier.padding(vertical = 4.dp)) Button( onClick = { if(proxyType == 0) { @@ -1597,7 +1600,7 @@ fun RecommendedGlobalProxyScreen(onNavigateUp: () -> Unit) { dpm.setRecommendedGlobalProxy(receiver, proxyInfo) context.showOperationResultToast(true) }, - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp) ) { Text(stringResource(R.string.apply)) } @@ -1716,7 +1719,7 @@ fun PreferentialNetworkServiceScreen(onNavigateUp: () -> Unit, onNavigate: (AddP configs.addAll(dpm.preferentialNetworkServiceConfigs) } LaunchedEffect(Unit) { refresh() } - MyScaffold(R.string.preferential_network_service, 0.dp, onNavigateUp, false) { + MySmallTitleScaffold(R.string.preferential_network_service, 0.dp, onNavigateUp) { SwitchItem(R.string.enabled, state = masterEnabled, onCheckedChange = { dpm.isPreferentialNetworkServiceEnabled = it refresh() @@ -1780,7 +1783,7 @@ fun AddPreferentialNetworkServiceConfigScreen(route: AddPreferentialNetworkServi var blockNonMatching by remember { mutableStateOf(route.blockNonMatching) } var excludedUids by remember { mutableStateOf(route.excludedUids.joinToString("\n")) } var includedUids by remember { mutableStateOf(route.includedUids.joinToString("\n")) } - MyScaffold(R.string.preferential_network_service, 8.dp, onNavigateUp, false) { + MySmallTitleScaffold(R.string.preferential_network_service, 8.dp, onNavigateUp) { SwitchItem(title = R.string.enabled, state = enabled, onCheckedChange = { enabled = it }, padding = false) AnimatedVisibility(enabled) { Row(verticalAlignment = Alignment.CenterVertically) { @@ -1882,7 +1885,7 @@ fun OverrideApnScreen(onNavigateUp: () -> Unit, onNavigateToAddSetting: (Bundle) settings.addAll(dpm.getOverrideApns(receiver)) } LaunchedEffect(Unit) { refresh() } - MyScaffold(R.string.override_apn, 0.dp, onNavigateUp, false) { + MyScaffold(R.string.override_apn, 0.dp, onNavigateUp) { SwitchItem( R.string.enable, state = enabled, onCheckedChange = { @@ -1966,7 +1969,7 @@ fun AddApnSettingScreen(origin: ApnSetting?, onNavigateUp: () -> Unit) { var persistent by remember { mutableStateOf(if(VERSION.SDK_INT >= 33) origin?.isPersistent == true else false) } var alwaysOn by remember { mutableStateOf(VERSION.SDK_INT >= 35 && origin?.isAlwaysOn == true) } var errorMessage: String? by remember { mutableStateOf(null) } - MyScaffold(R.string.apn_setting, 8.dp, onNavigateUp, false) { + MySmallTitleScaffold(R.string.apn_setting, 8.dp, onNavigateUp) { val protocolMap = mapOf( ApnSetting.PROTOCOL_IP to "IPv4", ApnSetting.PROTOCOL_IPV6 to "IPv6", ApnSetting.PROTOCOL_IPV4V6 to "IPv4/v6", ApnSetting.PROTOCOL_PPP to "PPP" 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 6e46d27..5f02ab7 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt @@ -15,6 +15,7 @@ import android.os.RemoteException import android.os.UserManager import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.annotation.Keep import androidx.annotation.RequiresApi import androidx.annotation.StringRes import androidx.compose.animation.AnimatedVisibility @@ -495,6 +496,7 @@ fun DeviceOwnerScreen(onNavigateUp: () -> Unit) { } } +@Keep @Suppress("InlinedApi") enum class DelegatedScope(val id: String, @StringRes val string: Int, val requiresApi: Int = 0) { AppRestrictions(DevicePolicyManager.DELEGATION_APP_RESTRICTIONS, R.string.manage_application_restrictions), @@ -585,7 +587,7 @@ fun AddDelegatedAdminScreen(data: AddDelegatedAdmin, onNavigateUp: () -> Unit) { val choosePackage = rememberLauncherForActivityResult(ChoosePackageContract()) { result -> result?.let { input = it } } - MyScaffold(if(updateMode) R.string.place_holder else R.string.add_delegated_admin, 0.dp, onNavigateUp, !updateMode) { + MySmallTitleScaffold(if(updateMode) R.string.place_holder else R.string.add_delegated_admin, 0.dp, onNavigateUp,) { OutlinedTextField( value = input, onValueChange = { input = it }, label = { Text(stringResource(R.string.package_name)) }, diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Shizuku.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Shizuku.kt index aa46716..65d3818 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Shizuku.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Shizuku.kt @@ -38,7 +38,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.bintianqi.owndroid.IUserService import com.bintianqi.owndroid.R -import com.bintianqi.owndroid.ui.MyScaffold +import com.bintianqi.owndroid.ui.MySmallTitleScaffold import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.serialization.Serializable @@ -66,7 +66,7 @@ fun ShizukuScreen(navArgs: Bundle, onNavigateUp: () -> Unit, onNavigateToAccount null } } - MyScaffold(R.string.shizuku, 0.dp, onNavigateUp, false) { + MySmallTitleScaffold(R.string.shizuku, 0.dp, onNavigateUp) { Button( onClick = { @@ -185,7 +185,7 @@ data class Accounts( @Composable fun AccountsScreen(accounts: Accounts, onNavigateUp: () -> Unit) { - MyScaffold(R.string.accounts, 8.dp, onNavigateUp, false) { + MySmallTitleScaffold(R.string.accounts, 8.dp, onNavigateUp) { accounts.list.forEach { Column( modifier = Modifier diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt index 225d031..73e5064 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt @@ -99,6 +99,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TimePicker import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberDatePickerState import androidx.compose.material3.rememberTimePickerState import androidx.compose.runtime.Composable @@ -138,6 +139,7 @@ import com.bintianqi.owndroid.ui.FunctionItem import com.bintianqi.owndroid.ui.InfoCard import com.bintianqi.owndroid.ui.ListItem import com.bintianqi.owndroid.ui.MyScaffold +import com.bintianqi.owndroid.ui.MySmallTitleScaffold import com.bintianqi.owndroid.ui.NavIcon import com.bintianqi.owndroid.ui.RadioButtonItem import com.bintianqi.owndroid.ui.SwitchItem @@ -431,7 +433,7 @@ fun HardwareMonitorScreen(onNavigateUp: () -> Unit) { delay(refreshIntervalMs) } } - MyScaffold(R.string.hardware_monitor, 8.dp, onNavigateUp, false) { + MySmallTitleScaffold(R.string.hardware_monitor, 8.dp, onNavigateUp) { Text(stringResource(R.string.refresh_interval), style = typography.titleLarge, modifier = Modifier.padding(vertical = 4.dp)) Slider(refreshInterval, { refreshInterval = it }, valueRange = 0.5F..2F, steps = 14) Text("${refreshIntervalMs}ms") @@ -943,7 +945,7 @@ fun NearbyStreamingPolicyScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() var appPolicy by remember { mutableIntStateOf(dpm.nearbyAppStreamingPolicy) } - MyScaffold(R.string.nearby_streaming_policy, 0.dp, onNavigateUp, false) { + MyScaffold(R.string.nearby_streaming_policy, 0.dp, onNavigateUp) { Text( stringResource(R.string.nearby_app_streaming), Modifier.padding(start = 8.dp, top = 10.dp, bottom = 4.dp), style = typography.titleLarge @@ -1018,7 +1020,8 @@ fun LockTaskModeScreen(onNavigateUp: () -> Unit) { topBar = { TopAppBar( title = { Text(stringResource(R.string.lock_task_mode)) }, - navigationIcon = { NavIcon(onNavigateUp) } + navigationIcon = { NavIcon(onNavigateUp) }, + colors = TopAppBarDefaults.topAppBarColors(colorScheme.surfaceContainer) ) } ) { paddingValues -> @@ -1108,7 +1111,8 @@ private fun ColumnScope.StartLockTaskMode() { } else { Toast.makeText(context, R.string.failed, Toast.LENGTH_SHORT).show() } - } + }, + enabled = startLockTaskApp.isNotBlank() && (!specifyActivity || startLockTaskActivity.isNotBlank()) ) { Text(stringResource(R.string.start)) } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/UserRestriction.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/UserRestriction.kt index 273d986..b78d390 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/UserRestriction.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/UserRestriction.kt @@ -75,7 +75,7 @@ fun UserRestrictionOptionsScreen( data: UserRestrictionOptions, restrictions: Bundle, onNavigateUp: () -> Unit, onRestrictionChange: (String, Boolean) -> Unit ) { - MyScaffold(data.title, 0.dp, onNavigateUp, false) { + MyScaffold(data.title, 0.dp, onNavigateUp) { data.items.filter { Build.VERSION.SDK_INT >= it.requiresApi }.forEach { restriction -> SwitchItem( restriction.name, restriction.id, restriction.icon, diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/WorkProfile.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/WorkProfile.kt index f24a933..f066020 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/WorkProfile.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/WorkProfile.kt @@ -171,7 +171,7 @@ fun CreateWorkProfileScreen(onNavigateUp: () -> Unit) { fun OrganizationOwnedProfileScreen(onNavigateUp: () -> Unit) { val context = LocalContext.current val dpm = context.getDPM() - MyScaffold(R.string.org_owned_work_profile, 8.dp, onNavigateUp, false) { + MyScaffold(R.string.org_owned_work_profile, 8.dp, onNavigateUp) { CardItem(R.string.org_owned_work_profile, dpm.isOrganizationOwnedDeviceWithManagedProfile.yesOrNo) Spacer(Modifier.padding(vertical = 5.dp)) if(!dpm.isOrganizationOwnedDeviceWithManagedProfile) { 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 1f7fed9..044ebd8 100644 --- a/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt +++ b/app/src/main/java/com/bintianqi/owndroid/ui/Components.kt @@ -23,9 +23,9 @@ import androidx.compose.material3.MaterialTheme.typography import androidx.compose.runtime.* 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.draw.rotate +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -292,20 +292,16 @@ fun MyScaffold( @StringRes title: Int, horizonPadding: Dp, onNavIconClicked: () -> Unit, - displayTitle: Boolean = true, content: @Composable ColumnScope.() -> Unit ) { - val scrollState = rememberScrollState() + val sb = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() Scaffold( + Modifier.nestedScroll(sb.nestedScrollConnection), topBar = { - TopAppBar( - title = { - Text( - text = stringResource(title), - modifier = if(displayTitle) Modifier.alpha((maxOf(scrollState.value-90,0)).toFloat()/50) else Modifier - ) - }, - navigationIcon = { NavIcon (onNavIconClicked) } + LargeTopAppBar( + { Text(stringResource(title)) }, + navigationIcon = { NavIcon(onNavIconClicked) }, + scrollBehavior = sb ) } ) { paddingValues -> @@ -314,14 +310,39 @@ fun MyScaffold( .fillMaxSize() .padding(paddingValues) .padding(horizontal = horizonPadding) - .verticalScroll(scrollState) + .verticalScroll(rememberScrollState()) .padding(bottom = 80.dp) ) { - if(displayTitle) Text( - text = stringResource(title), - style = typography.headlineLarge, - modifier = Modifier.padding(start = if(horizonPadding == 0.dp) 16.dp else 0.dp,top = 10.dp, bottom = 5.dp) + content() + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MySmallTitleScaffold( + @StringRes title: Int, + horizonPadding: Dp, + onNavIconClicked: () -> Unit, + content: @Composable ColumnScope.() -> Unit +) { + Scaffold( + topBar = { + TopAppBar( + { Text(stringResource(title)) }, + navigationIcon = { NavIcon(onNavIconClicked) }, + colors = TopAppBarDefaults.topAppBarColors(colorScheme.surfaceContainer) ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(horizontal = horizonPadding) + .verticalScroll(rememberScrollState()) + .padding(bottom = 80.dp) + ) { content() } } diff --git a/app/src/main/java/com/bintianqi/owndroid/ui/theme/Theme.kt b/app/src/main/java/com/bintianqi/owndroid/ui/theme/Theme.kt index 4fcc2e0..7e13e2c 100644 --- a/app/src/main/java/com/bintianqi/owndroid/ui/theme/Theme.kt +++ b/app/src/main/java/com/bintianqi/owndroid/ui/theme/Theme.kt @@ -6,19 +6,11 @@ import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.Stable -import androidx.compose.runtime.State import androidx.compose.ui.graphics.Color -import androidx.compose.runtime.getValue -import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.core.view.WindowCompat -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.bintianqi.owndroid.MyViewModel import com.bintianqi.owndroid.ThemeSettings -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow private val lightScheme = lightColorScheme( primary = primaryLight, @@ -111,8 +103,6 @@ fun OwnDroidTheme( else -> lightScheme }.let { if(darkTheme && theme.blackTheme) it.copy(background = Color.Black) else it - }.let { - if(!darkTheme) it.copy(background = it.primary.copy(alpha = 0.05f)) else it } val view = LocalView.current SideEffect { diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 537e7ba..98e0599 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -410,6 +410,9 @@ Тихое удаление Запросить удаление Установить приложение + + Choose an APK file + Install existing app Поиск @@ -701,6 +704,7 @@ Приостановленный пакет не сможет запускать активности. Его уведомления будут скрыты, он не будет отображаться в списке последних запущенных приложений, не сможет показывать всплывающие уведомления или диалоговые окна и звонить на устройство.\nНекоторые приложения не могут быть приостановлены, такие как администраторы устройства, активный лаунчер и приложение для набора номера по умолчанию. Пользователь не сможет очищать данные приложений или принудительно останавливать пакеты. Установить список приложений, которые нужно сохранить в виде APK-файлов, даже если ни у одного пользователя в данный момент они не установлены. + Install an existing package that has been installed in another user, or has been kept after uninstall. Режим "безголового" системного пользователя означает, что системный пользователь запускает системные службы и некоторый системный интерфейс, но он не связан с каким-либо реальным человеком, и для связи с реальными людьми должны быть созданы дополнительные пользователи. 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 6b58d34..e106884 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -416,7 +416,10 @@ Sessiz kaldırma Kaldırma isteği Uygulamayı yükle - Search + + Choose an APK file + Install existing app + Search Kullanıcı kısıtlaması diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 385f038..59f3afd 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -372,7 +372,7 @@ 作用域: 工作资料 应用详情 未安装 - 阻止 + 阻止卸载 禁止用户控制 用户将无法清除这些应用的存储空间或强制停止这些应用 应用列表: @@ -398,6 +398,8 @@ 静默卸载 请求卸载 安装应用 + 选择一个APK文件 + 安装已存在的应用 启用系统应用 重新启用一个默认被禁用的系统应用 搜索 @@ -684,6 +686,7 @@ 挂起的应用无法被打开,通知会被隐藏,不会在最近任务中显示,不能弹窗,不能发送Toast。\n有些应用无法被挂起,比如Device admin、启动器和默认拨号应用。 用户无法清除这些应用的存储空间,也无法强制停止应用 这个列表中的应用的APK将会一直保留,即使没有任何用户安装这个应用 + 安装一个已经在其他用户中安装或在卸载后保留的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 022e7a5..5130aac 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -437,6 +437,8 @@ Silent uninstall Request uninstall Install app + Choose an APK file + Install existing app Search @@ -724,6 +726,7 @@ A suspended package will not be able to start activities. Its notifications will be hidden, it will not show up in recent activities, will not be able to show toasts or dialogs or ring the device.\nSome apps cannot be suspended, such as device admins, the active launcher and the default dialer. 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. 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