App manager: show a dialog when click suspend, hide, block uninstall or always-on vpn

raise pkgName state to save it
This commit is contained in:
BinTianqi
2024-05-12 19:17:13 +08:00
parent f8f66220f3
commit 2009baac40
8 changed files with 171 additions and 115 deletions

View File

@@ -1,5 +1,6 @@
package com.bintianqi.owndroid
import android.annotation.SuppressLint
import android.app.admin.DevicePolicyManager
import android.content.ComponentName
import android.content.Context
@@ -21,8 +22,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -60,6 +60,7 @@ class MainActivity : ComponentActivity() {
}
}
@SuppressLint("UnrememberedMutableState")
@ExperimentalMaterial3Api
@Composable
fun MyScaffold(){
@@ -69,6 +70,8 @@ fun MyScaffold(){
val myComponent = ComponentName(myContext,Receiver::class.java)
val sharedPref = LocalContext.current.getSharedPreferences("data", Context.MODE_PRIVATE)
val focusMgr = LocalFocusManager.current
val pkgName = mutableStateOf("")
val dialogStatus = mutableIntStateOf(0)
SetDarkTheme()
LaunchedEffect(Unit){
while(true){
@@ -89,17 +92,17 @@ fun MyScaffold(){
popEnterTransition = Animations.navHostPopEnterTransition,
popExitTransition = Animations.navHostPopExitTransition
){
composable(route = "HomePage", content = { HomePage(navCtrl)})
composable(route = "HomePage", content = { HomePage(navCtrl, pkgName)})
composable(route = "SystemManage", content = { SystemManage(navCtrl) })
composable(route = "ManagedProfile", content = {ManagedProfile(navCtrl)})
composable(route = "Permissions", content = { DpmPermissions(navCtrl)})
composable(route = "ApplicationManage", content = { ApplicationManage(navCtrl)})
composable(route = "ApplicationManage", content = { ApplicationManage(navCtrl, pkgName, dialogStatus)})
composable(route = "UserRestriction", content = { UserRestriction(navCtrl)})
composable(route = "UserManage", content = { UserManage(navCtrl)})
composable(route = "Password", content = { Password(navCtrl)})
composable(route = "AppSetting", content = { AppSetting(navCtrl)})
composable(route = "Network", content = {Network(navCtrl)})
composable(route = "PackageSelector"){PackageSelector(navCtrl)}
composable(route = "PackageSelector"){PackageSelector(navCtrl, pkgName)}
composable(route = "PermissionPicker"){PermissionPicker(navCtrl)}
}
LaunchedEffect(Unit){
@@ -114,7 +117,7 @@ fun MyScaffold(){
}
@Composable
private fun HomePage(navCtrl:NavHostController){
private fun HomePage(navCtrl:NavHostController, pkgName: MutableState<String>){
val myContext = LocalContext.current
val myDpm = myContext.getSystemService(ComponentActivity.DEVICE_POLICY_SERVICE) as DevicePolicyManager
val myComponent = ComponentName(myContext,Receiver::class.java)
@@ -125,6 +128,7 @@ private fun HomePage(navCtrl:NavHostController){
}
else if(myDpm.isAdminActive(myComponent)){R.string.device_admin}else{R.string.click_to_activate}
)
LaunchedEffect(Unit){ pkgName.value = "" }
Column(modifier = Modifier.statusBarsPadding().verticalScroll(rememberScrollState())) {
Spacer(Modifier.padding(vertical = 25.dp))
Text(text = stringResource(R.string.app_name), style = typography.headlineLarge, modifier = Modifier.padding(start = 10.dp), color = colorScheme.onBackground)

View File

@@ -21,8 +21,6 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import com.bintianqi.owndroid.dpm.applySelectedPackage
import com.bintianqi.owndroid.dpm.selectedPackage
import com.bintianqi.owndroid.ui.NavIcon
import com.bintianqi.owndroid.ui.theme.bgColor
import com.google.accompanist.drawablepainter.rememberDrawablePainter
@@ -40,7 +38,7 @@ private val pkgs = mutableListOf<PkgInfo>()
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
fun PackageSelector(navCtrl:NavHostController){
fun PackageSelector(navCtrl:NavHostController, pkgName: MutableState<String>){
val context = LocalContext.current
val pm = context.packageManager
val apps = pm.getInstalledApplications(0)
@@ -144,7 +142,7 @@ fun PackageSelector(navCtrl:NavHostController){
if(show) {
items(pkgs) {
if(filter==it.type){
PackageItem(it, navCtrl)
PackageItem(it, navCtrl, pkgName)
}
}
items(1){Spacer(Modifier.padding(vertical = 30.dp))}
@@ -163,12 +161,12 @@ fun PackageSelector(navCtrl:NavHostController){
}
@Composable
private fun PackageItem(pkg: PkgInfo, navCtrl: NavHostController){
private fun PackageItem(pkg: PkgInfo, navCtrl: NavHostController, pkgName: MutableState<String>){
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable{selectedPackage =pkg.pkgName;applySelectedPackage =true;navCtrl.navigateUp()}
.clickable{ pkgName.value = pkg.pkgName; navCtrl.navigateUp()}
.padding(vertical = 3.dp)
){
Spacer(Modifier.padding(start = 15.dp))

View File

@@ -32,7 +32,6 @@ import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.*
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
@@ -43,6 +42,7 @@ 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.compose.ui.window.Dialog
import androidx.core.content.ContextCompat.startActivity
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
@@ -64,10 +64,13 @@ private var keepUninstallPkg = mutableListOf<String>()
private var permittedIme = mutableListOf<String>()
private var permittedAccessibility = mutableListOf<String>()
private var dialogConfirmButtonAction = {}
private var dialogDismissButtonAction = {}
private var dialogGetStatus = { false }
@Composable
fun ApplicationManage(navCtrl:NavHostController){
fun ApplicationManage(navCtrl:NavHostController, pkgName: MutableState<String>, dialogStatus: MutableIntState){
val focusMgr = LocalFocusManager.current
var pkgName by rememberSaveable{ mutableStateOf("") }
val localNavCtrl = rememberNavController()
val backStackEntry by localNavCtrl.currentBackStackEntryAsState()
val titleMap = mapOf(
@@ -96,16 +99,10 @@ fun ApplicationManage(navCtrl:NavHostController){
}
){ paddingValues->
Column(modifier = Modifier.fillMaxSize().padding(top = paddingValues.calculateTopPadding())){
LaunchedEffect(Unit) {
while(true){
if(applySelectedPackage){ pkgName = selectedPackage; applySelectedPackage = false; applySelectedPermission = true}
delay(100)
}
}
if(backStackEntry?.destination?.route!="InstallApp"){
TextField(
value = pkgName,
onValueChange = { pkgName = it },
value = pkgName.value,
onValueChange = { pkgName.value = it },
label = { Text(stringResource(R.string.package_name)) },
modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Done),
@@ -128,27 +125,30 @@ fun ApplicationManage(navCtrl:NavHostController){
popExitTransition = Animations.navHostPopExitTransition,
modifier = Modifier.background(bgColor)
){
composable(route = "Home"){Home(localNavCtrl,pkgName)}
composable(route = "BlockUninstall"){BlockUninstall(pkgName)}
composable(route = "UserControlDisabled"){UserCtrlDisabledPkg(pkgName)}
composable(route = "PermissionManage"){PermissionManage(pkgName,navCtrl)}
composable(route = "CrossProfilePackage"){CrossProfilePkg(pkgName)}
composable(route = "CrossProfileWidget"){CrossProfileWidget(pkgName)}
composable(route = "CredentialManagePolicy"){CredentialManagePolicy(pkgName)}
composable(route = "Accessibility"){PermittedAccessibility(pkgName)}
composable(route = "IME"){PermittedIME(pkgName)}
composable(route = "KeepUninstalled"){KeepUninstalledApp(pkgName)}
composable(route = "Home"){Home(localNavCtrl, pkgName.value, dialogStatus)}
composable(route = "UserControlDisabled"){UserCtrlDisabledPkg(pkgName.value)}
composable(route = "PermissionManage"){PermissionManage(pkgName.value, navCtrl)}
composable(route = "CrossProfilePackage"){CrossProfilePkg(pkgName.value)}
composable(route = "CrossProfileWidget"){CrossProfileWidget(pkgName.value)}
composable(route = "CredentialManagePolicy"){CredentialManagePolicy(pkgName.value)}
composable(route = "Accessibility"){PermittedAccessibility(pkgName.value)}
composable(route = "IME"){PermittedIME(pkgName.value)}
composable(route = "KeepUninstalled"){KeepUninstalledApp(pkgName.value)}
composable(route = "InstallApp"){InstallApp()}
composable(route = "UninstallApp"){UninstallApp(pkgName)}
composable(route = "ClearAppData"){ClearAppData(pkgName)}
composable(route = "DefaultDialer"){DefaultDialerApp(pkgName)}
composable(route = "UninstallApp"){UninstallApp(pkgName.value)}
composable(route = "ClearAppData"){ClearAppData(pkgName.value)}
composable(route = "DefaultDialer"){DefaultDialerApp(pkgName.value)}
}
}
}
if(dialogStatus.intValue!=0){
LocalFocusManager.current.clearFocus()
AppControlDialog(dialogStatus)
}
}
@Composable
private fun Home(navCtrl:NavHostController, pkgName: String){
private fun Home(navCtrl:NavHostController, pkgName: String, dialogStatus: MutableIntState){
Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())){
val myContext = LocalContext.current
val myDpm = myContext.getSystemService(ComponentActivity.DEVICE_POLICY_SERVICE) as DevicePolicyManager
@@ -163,38 +163,70 @@ private fun Home(navCtrl:NavHostController, pkgName: String){
startActivity(myContext,intent,null)
}
if(VERSION.SDK_INT>=24&&(isDeviceOwner(myDpm)||isProfileOwner(myDpm))){
val getSuspendStatus = {
try{ myDpm.isPackageSuspended(myComponent, pkgName) }
catch(e:NameNotFoundException){ false }
catch(e:IllegalArgumentException){ false }
}
SwitchItem(
R.string.suspend,"",R.drawable.block_fill0,
{
try{ myDpm.isPackageSuspended(myComponent,pkgName) }
catch(e:NameNotFoundException){ false }
catch(e:IllegalArgumentException){ false }
},
{myDpm.setPackagesSuspended(myComponent, arrayOf(pkgName), it)}
)
}
if(isDeviceOwner(myDpm)||isProfileOwner(myDpm)){
SwitchItem(
R.string.hide, stringResource(R.string.isapphidden_desc),R.drawable.visibility_off_fill0,
{myDpm.isApplicationHidden(myComponent,pkgName)},{myDpm.setApplicationHidden(myComponent, pkgName, it)}
)
}
if(VERSION.SDK_INT>=24&&(isDeviceOwner(myDpm)||isProfileOwner(myDpm))){
SwitchItem(
R.string.always_on_vpn,"",R.drawable.vpn_key_fill0,{pkgName == myDpm.getAlwaysOnVpnPackage(myComponent)},
{
try {
myDpm.setAlwaysOnVpnPackage(myComponent, pkgName, it)
} catch(e: UnsupportedOperationException) {
Toast.makeText(myContext, R.string.unsupported, Toast.LENGTH_SHORT).show()
} catch(e: NameNotFoundException) {
Toast.makeText(myContext, R.string.not_installed, Toast.LENGTH_SHORT).show()
}
title = R.string.suspend, desc = "", icon = R.drawable.block_fill0,
getState = getSuspendStatus,
onCheckedChange = { myDpm.setPackagesSuspended(myComponent, arrayOf(pkgName), it) },
onClickBlank = {
dialogGetStatus = getSuspendStatus
dialogConfirmButtonAction = { myDpm.setPackagesSuspended(myComponent, arrayOf(pkgName), true) }
dialogDismissButtonAction = { myDpm.setPackagesSuspended(myComponent, arrayOf(pkgName), false) }
dialogStatus.intValue = 1
}
)
}
if(isDeviceOwner(myDpm)||isProfileOwner(myDpm)){
SubPageItem(R.string.block_uninstall,"",R.drawable.delete_forever_fill0){navCtrl.navigate("BlockUninstall")}
SwitchItem(
title = R.string.hide, desc = stringResource(R.string.isapphidden_desc), icon = R.drawable.visibility_off_fill0,
getState = { myDpm.isApplicationHidden(myComponent,pkgName) },
onCheckedChange = { myDpm.setApplicationHidden(myComponent, pkgName, it) },
onClickBlank = {
dialogGetStatus = { myDpm.isApplicationHidden(myComponent,pkgName) }
dialogConfirmButtonAction = { myDpm.setApplicationHidden(myComponent, pkgName, true) }
dialogDismissButtonAction = { myDpm.setApplicationHidden(myComponent, pkgName, false) }
dialogStatus.intValue = 2
}
)
}
if(isDeviceOwner(myDpm)||isProfileOwner(myDpm)){
SwitchItem(
title = R.string.block_uninstall, desc = "", icon = R.drawable.delete_forever_fill0,
getState = { myDpm.isUninstallBlocked(myComponent,pkgName) },
onCheckedChange = { myDpm.setUninstallBlocked(myComponent,pkgName,it) },
onClickBlank = {
dialogGetStatus = { myDpm.isUninstallBlocked(myComponent,pkgName) }
dialogConfirmButtonAction = { myDpm.setUninstallBlocked(myComponent,pkgName,true) }
dialogDismissButtonAction = { myDpm.setUninstallBlocked(myComponent,pkgName,false) }
dialogStatus.intValue = 3
}
)
}
if(VERSION.SDK_INT>=24&&(isDeviceOwner(myDpm)||isProfileOwner(myDpm))){
val setAlwaysOnVpn: (Boolean)->Unit = {
try {
myDpm.setAlwaysOnVpnPackage(myComponent, pkgName, it)
} catch(e: UnsupportedOperationException) {
Toast.makeText(myContext, R.string.unsupported, Toast.LENGTH_SHORT).show()
} catch(e: NameNotFoundException) {
Toast.makeText(myContext, R.string.not_installed, Toast.LENGTH_SHORT).show()
}
}
SwitchItem(
title = R.string.always_on_vpn, desc = "", icon = R.drawable.vpn_key_fill0,
getState = {pkgName == myDpm.getAlwaysOnVpnPackage(myComponent)},
onCheckedChange = setAlwaysOnVpn,
onClickBlank = {
dialogGetStatus = { pkgName == myDpm.getAlwaysOnVpnPackage(myComponent) }
dialogConfirmButtonAction = { setAlwaysOnVpn(true) }
dialogDismissButtonAction = { setAlwaysOnVpn(false) }
dialogStatus.intValue = 4
}
)
}
if((VERSION.SDK_INT>=33&&isProfileOwner(myDpm))||(VERSION.SDK_INT>=30&&isDeviceOwner(myDpm))){
SubPageItem(R.string.ucd,"",R.drawable.do_not_touch_fill0){navCtrl.navigate("UserControlDisabled")}
@@ -300,49 +332,6 @@ private fun UserCtrlDisabledPkg(pkgName:String){
}
}
@Composable
private fun BlockUninstall(pkgName: String){
val myContext = LocalContext.current
val myDpm = myContext.getSystemService(ComponentActivity.DEVICE_POLICY_SERVICE) as DevicePolicyManager
val myComponent = ComponentName(myContext,Receiver::class.java)
val focusMgr = LocalFocusManager.current
Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())){
var state by remember{mutableStateOf(myDpm.isUninstallBlocked(myComponent,pkgName))}
Spacer(Modifier.padding(vertical = 10.dp))
Text(text = stringResource(R.string.block_uninstall), style = typography.headlineLarge)
Spacer(Modifier.padding(vertical = 5.dp))
Text(stringResource(R.string.current_state, stringResource(if(state){R.string.enabled}else{R.string.disabled})))
Spacer(Modifier.padding(vertical = 3.dp))
Text(text = stringResource(R.string.sometimes_get_wrong_block_uninstall_state))
Spacer(Modifier.padding(vertical = 5.dp))
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
Button(
onClick = {
focusMgr.clearFocus()
myDpm.setUninstallBlocked(myComponent,pkgName,true)
Toast.makeText(myContext, R.string.success, Toast.LENGTH_SHORT).show()
state = myDpm.isUninstallBlocked(myComponent,pkgName)
},
modifier = Modifier.fillMaxWidth(0.49F)
) {
Text(stringResource(R.string.enable))
}
Button(
onClick = {
focusMgr.clearFocus()
myDpm.setUninstallBlocked(myComponent,pkgName,false)
Toast.makeText(myContext, R.string.success, Toast.LENGTH_SHORT).show()
state = myDpm.isUninstallBlocked(myComponent,pkgName)
},
modifier = Modifier.fillMaxWidth(0.96F)
){
Text(stringResource(R.string.disable))
}
}
Spacer(Modifier.padding(vertical = 30.dp))
}
}
@SuppressLint("NewApi")
@Composable
private fun PermissionManage(pkgName: String, navCtrl: NavHostController){
@@ -899,6 +888,62 @@ private fun DefaultDialerApp(pkgName: String){
}
}
@Composable
fun AppControlDialog(status: MutableIntState){
val enabled = dialogGetStatus()
Dialog(
onDismissRequest = { status.intValue = 0 }
) {
Card(
modifier = Modifier.fillMaxWidth()
){
Column(
modifier = Modifier.fillMaxWidth().padding(15.dp)
){
Text(
text = stringResource(
when(status.intValue){
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,
modifier = Modifier.padding(start = 5.dp)
)
Text(
text = stringResource(R.string.current_status_is) + stringResource(if(enabled){R.string.enabled}else{R.string.disabled}),
modifier = Modifier.padding(start = 5.dp, top = 5.dp, bottom = 5.dp)
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
){
TextButton(
onClick = { status.intValue = 0 }
){
Text(text = stringResource(R.string.cancel))
}
Row{
TextButton(
onClick = { dialogDismissButtonAction(); status.intValue = 0 }
){
Text(text = stringResource(R.string.disable))
}
TextButton(
onClick = { dialogConfirmButtonAction(); status.intValue = 0 }
){
Text(text = stringResource(R.string.enable))
}
}
}
}
}
}
}
@Throws(IOException::class)
private fun installPackage(context: Context, inputStream: InputStream){
val packageInstaller = context.packageManager.packageInstaller

View File

@@ -5,8 +5,6 @@ import android.content.Intent
import android.net.Uri
import androidx.activity.result.ActivityResultLauncher
var selectedPackage = ""
var applySelectedPackage = false
var selectedPermission = ""
var applySelectedPermission = false
lateinit var createManagedProfile: ActivityResultLauncher<Intent>

View File

@@ -5,7 +5,9 @@ import android.widget.Toast
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
@@ -128,12 +130,21 @@ fun SwitchItem(
@DrawableRes icon: Int?,
getState: ()->Boolean,
onCheckedChange: (Boolean)->Unit,
enable:Boolean=true
enable:Boolean = true,
onClickBlank: (() -> Unit)? = null
){
var checked by remember{mutableStateOf(false)}
checked = getState()
Box(modifier = Modifier.fillMaxWidth().padding(vertical = 5.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.align(Alignment.CenterStart)){
Box(
modifier = Modifier
.fillMaxWidth()
.clickable(enabled = onClickBlank!=null, onClick = onClickBlank?:{})
.padding(vertical = 5.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.align(Alignment.CenterStart)
){
Spacer(Modifier.padding(start = 30.dp))
if(icon!=null){
Icon(painter = painterResource(icon),contentDescription = null)

View File

@@ -45,6 +45,7 @@
<string name="copy">复制</string>
<string name="file_not_exist">文件不存在</string>
<string name="io_exception">IO异常</string>
<string name="current_status_is">当前状态:</string>
<!--Permissions-->
<string name="click_to_activate">点击以激活</string>
@@ -256,7 +257,6 @@
<string name="app_info">应用详情</string>
<string name="not_installed">未安装</string>
<string name="block_uninstall">防卸载</string>
<string name="sometimes_get_wrong_block_uninstall_state">有时候无法正确获取防卸载状态</string>
<string name="ucd">禁止用户控制</string>
<string name="ucd_desc">用户将无法清除应用的存储空间和缓存</string>
<string name="app_list_is">应用列表:</string>

View File

@@ -48,6 +48,7 @@
<string name="copy">Copy</string>
<string name="file_not_exist">File not exist</string>
<string name="io_exception">IO Exception</string>
<string name="current_status_is">Current status:&#160;</string>
<!--Permissions-->
<string name="click_to_activate">Click to activate</string>
@@ -269,7 +270,6 @@
<string name="app_info">App info</string>
<string name="not_installed">Not installed</string>
<string name="block_uninstall">Block uninstall</string>
<string name="sometimes_get_wrong_block_uninstall_state">Sometimes it shows a wrong status</string>
<!--ucd: user control disabled-->
<string name="ucd">Disable user control</string>
<string name="ucd_desc">If you set this, you cannot clear storage or cache of the app. </string>