Fix crash caused by DelegatedScope

Use LargeTopAppBar in most screens
Optimize Package Chooser
Install existing app
This commit is contained in:
BinTianqi
2025-02-23 11:50:24 +08:00
parent fa81a2f30e
commit b734522171
15 changed files with 182 additions and 157 deletions

View File

@@ -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<MyViewModel>()
val vm by viewModels<PackageChooserViewModel>()
vm.initialize()
val vm by viewModels<MyViewModel>()
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<PackageInfo>())
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<Application>().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<PackageInfo>())
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->

View File

@@ -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()
}

View File

@@ -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)
}
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,13 +283,12 @@ 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 = {
TextButton({
try {
dpm.enableSystemApp(receiver, pkgName)
context.showOperationResultToast(true)
@@ -301,8 +296,7 @@ private fun HomeScreen(pkgName: String, onNavigate: (Any) -> Unit) {
Toast.makeText(context, R.string.failed, Toast.LENGTH_SHORT).show()
}
dialogStatus = 0
}
) {
}) {
Text(stringResource(R.string.confirm))
}
},
@@ -317,7 +311,7 @@ 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 { "" } +
@@ -326,7 +320,6 @@ private fun HomeScreen(pkgName: String, onNavigate: (Any) -> Unit) {
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 = {
TextButton({
try {
dpm.setDefaultDialerApplication(pkgName)
context.showOperationResultToast(true)
} catch(_: IllegalArgumentException) {
Toast.makeText(context, R.string.failed, Toast.LENGTH_SHORT).show()
context.showOperationResultToast(false)
}
dialogStatus = 0
}
) {
}) {
Text(stringResource(R.string.confirm))
}
},
@@ -379,8 +368,7 @@ private fun HomeScreen(pkgName: String, onNavigate: (Any) -> Unit) {
AlertDialog(
onDismissRequest = { dialogStatus = 0 },
title = {
Text(
text = stringResource(
Text(stringResource(
when(appControlAction) {
1 -> R.string.suspend
2 -> R.string.hide
@@ -388,9 +376,7 @@ private fun HomeScreen(pkgName: String, onNavigate: (Any) -> Unit) {
4 -> R.string.always_on_vpn
else -> R.string.unknown
}
),
style = typography.headlineMedium
)
))
},
text = {
val enabled = when(appControlAction){
@@ -406,27 +392,38 @@ private fun HomeScreen(pkgName: String, onNavigate: (Any) -> Unit) {
}
},
confirmButton = {
TextButton(
onClick = {
TextButton({
appControl(true)
dialogStatus = 0
}
) {
}) {
Text(text = stringResource(R.string.enable))
}
},
dismissButton = {
TextButton(
onClick = {
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() }
}

View File

@@ -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"

View File

@@ -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)) },

View File

@@ -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

View File

@@ -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))
}

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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()
}
}

View File

@@ -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 {

View File

@@ -410,6 +410,9 @@
<string name="silent_uninstall">Тихое удаление</string>
<string name="request_uninstall">Запросить удаление</string>
<string name="install_app">Установить приложение</string>
<!--TODO: 2 strings-->
<string name="choose_apk_file">Choose an APK file</string>
<string name="install_existing_app">Install existing app</string>
<string name="search">Поиск</string>
<!--Ограничения пользователя-->
@@ -701,6 +704,7 @@
<string name="info_suspend_app">Приостановленный пакет не сможет запускать активности. Его уведомления будут скрыты, он не будет отображаться в списке последних запущенных приложений, не сможет показывать всплывающие уведомления или диалоговые окна и звонить на устройство.\nНекоторые приложения не могут быть приостановлены, такие как администраторы устройства, активный лаунчер и приложение для набора номера по умолчанию.</string>
<string name="info_disable_user_control">Пользователь не сможет очищать данные приложений или принудительно останавливать пакеты.</string>
<string name="info_keep_uninstalled_apps">Установить список приложений, которые нужно сохранить в виде APK-файлов, даже если ни у одного пользователя в данный момент они не установлены.</string>
<!--TODO--><string name="info_install_existing_app">Install an existing package that has been installed in another user, or has been kept after uninstall.</string>
<string name="info_headless_system_user_mode">Режим "безголового" системного пользователя означает, что системный пользователь запускает системные службы и некоторый системный интерфейс, но он не связан с каким-либо реальным человеком, и для связи с реальными людьми должны быть созданы дополнительные пользователи.</string>
<string name="info_logout">If the current user is not switched by OwnDroid, this function cannot be used.</string> <!--TODO-->
<string name="info_affiliation_id">Когда владелец устройства создает управляемого пользователя, управляемый пользователь не является аффилированным. Чтобы сделать управляемого пользователя аффилированным с владельцем устройства, вам следует установить одинаковые аффилированные идентификаторы в основном и управляемом пользователях.</string>

View File

@@ -416,7 +416,10 @@
<string name="silent_uninstall">Sessiz kaldırma</string>
<string name="request_uninstall">Kaldırma isteği</string>
<string name="install_app">Uygulamayı yükle</string>
<string name="search">Search</string> <!--TODO-->
<!--TODO: 3 strings-->
<string name="choose_apk_file">Choose an APK file</string>
<string name="install_existing_app">Install existing app</string>
<string name="search">Search</string>
<!--UserRestriction-->
<string name="user_restriction">Kullanıcı kısıtlaması</string>

View File

@@ -372,7 +372,7 @@
<string name="scope_is_work_profile">作用域: 工作资料</string>
<string name="app_info">应用详情</string>
<string name="not_installed">未安装</string>
<string name="block_uninstall">阻止</string>
<string name="block_uninstall">阻止卸载</string>
<string name="ucd">禁止用户控制</string>
<string name="ucd_desc">用户将无法清除这些应用的存储空间或强制停止这些应用</string>
<string name="app_list_is">应用列表:</string>
@@ -398,6 +398,8 @@
<string name="silent_uninstall">静默卸载</string>
<string name="request_uninstall">请求卸载</string>
<string name="install_app">安装应用</string>
<string name="choose_apk_file">选择一个APK文件</string>
<string name="install_existing_app">安装已存在的应用</string>
<string name="enable_system_app">启用系统应用</string>
<string name="enable_system_app_desc">重新启用一个默认被禁用的系统应用</string>
<string name="search">搜索</string>
@@ -684,6 +686,7 @@
<string name="info_suspend_app">挂起的应用无法被打开通知会被隐藏不会在最近任务中显示不能弹窗不能发送Toast。\n有些应用无法被挂起比如Device admin、启动器和默认拨号应用。</string>
<string name="info_disable_user_control">用户无法清除这些应用的存储空间,也无法强制停止应用</string>
<string name="info_keep_uninstalled_apps">这个列表中的应用的APK将会一直保留即使没有任何用户安装这个应用</string>
<string name="info_install_existing_app">安装一个已经在其他用户中安装或在卸载后保留的app。</string>
<string name="info_headless_system_user_mode">无头系统用户模式意味着系统用户运行系统服务和一些系统UI但它不与任何真实的人相关联必须创建额外的用户才能与真实的人相关联。</string>
<string name="info_logout">如果当前用户不是由OwnDroid切换的无法使用此功能。</string>
<string name="info_affiliation_id">当Device owner创建并管理用户时新的用户不是附属用户。Device owner设置和受管理用户完全相同的附属用户ID后受管理用户成为附属于Device owner的用户</string>

View File

@@ -437,6 +437,8 @@
<string name="silent_uninstall">Silent uninstall</string>
<string name="request_uninstall">Request uninstall</string>
<string name="install_app">Install app</string>
<string name="choose_apk_file">Choose an APK file</string>
<string name="install_existing_app">Install existing app</string>
<string name="search">Search</string>
<!--UserRestriction-->
@@ -724,6 +726,7 @@
<string name="info_suspend_app">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.</string>
<string name="info_disable_user_control">User will not be able to clear app data or force-stop packages.</string>
<string name="info_keep_uninstalled_apps">Set a list of apps to keep around as APKs even if no user has currently installed it. </string>
<string name="info_install_existing_app">Install an existing package that has been installed in another user, or has been kept after uninstall.</string>
<string name="info_headless_system_user_mode">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.</string>
<string name="info_logout">If the current user is not switched by OwnDroid, this function cannot be used.</string>
<string name="info_affiliation_id">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</string>