From 0b39bdc7883e6504c789bebfca1b3e6425593c8d Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Thu, 20 Jun 2024 23:42:02 +0800 Subject: [PATCH 01/17] add TaskReceiver --- app/src/main/AndroidManifest.xml | 7 ++++ .../java/com/bintianqi/owndroid/Setting.kt | 19 ++++++++++ .../com/bintianqi/owndroid/TaskReceiver.kt | 38 +++++++++++++++++++ app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 5 files changed, 66 insertions(+) create mode 100644 app/src/main/java/com/bintianqi/owndroid/TaskReceiver.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 93b4c89..9c895ed 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -90,6 +90,13 @@ android:description="@string/app_name" android:permission="android.permission.BIND_DEVICE_ADMIN"> + + + + + , bl composable(route = "Home") { Home(localNavCtrl) } composable(route = "Theme") { ThemeSettings(materialYou, blackTheme) } composable(route = "Auth") { AuthSettings() } + composable(route = "Automation") { Automation() } composable(route = "About") { About() } } } @@ -53,6 +56,7 @@ private fun Home(navCtrl: NavHostController) { Column(modifier = Modifier.fillMaxSize()) { SubPageItem(R.string.theme, "", R.drawable.format_paint_fill0) { navCtrl.navigate("Theme") } SubPageItem(R.string.security, "", R.drawable.lock_fill0) { navCtrl.navigate("Auth") } + SubPageItem(R.string.automation, "", R.drawable.apps_fill0) { navCtrl.navigate("Automation") } SubPageItem(R.string.about, "", R.drawable.info_fill0) { navCtrl.navigate("About") } } } @@ -122,6 +126,21 @@ private fun AuthSettings() { } } +@Composable +private fun Automation() { + val sharedPref = LocalContext.current.getSharedPreferences("data", Context.MODE_PRIVATE) + Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())) { + var pkgName by remember { mutableStateOf("") } + LaunchedEffect(Unit) { + pkgName = sharedPref.getString("AutomationApp", "")?: "" + } + TextField(value = pkgName, onValueChange = { pkgName = it }, label = { Text("Package name")}) + Button(onClick = {sharedPref.edit().putString("AutomationApp", pkgName).apply()}) { + Text("apply") + } + } +} + @Composable private fun About() { val context = LocalContext.current diff --git a/app/src/main/java/com/bintianqi/owndroid/TaskReceiver.kt b/app/src/main/java/com/bintianqi/owndroid/TaskReceiver.kt new file mode 100644 index 0000000..ddf601b --- /dev/null +++ b/app/src/main/java/com/bintianqi/owndroid/TaskReceiver.kt @@ -0,0 +1,38 @@ +package com.bintianqi.owndroid + +import android.app.admin.DevicePolicyManager +import android.content.BroadcastReceiver +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.os.Build.VERSION +import android.util.Log +import androidx.activity.ComponentActivity + +class TaskReceiver: BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + Log.d("OwnDroid", ("TaskReceiver: pkgName: " + intent.component?.packageName)) + Log.d("OwnDroid", ("TaskReceiver: pkg: " + intent.`package`)) + val sharedPref = context.getSharedPreferences("data", Context.MODE_PRIVATE) + if(sharedPref.getString("AutomationApp", "") != intent.component?.packageName) return + val category = intent.getStringExtra("category") + if(category == "app") { + val action = intent.getStringExtra("action") + if(action == "suspend") { + val dpm = context.getSystemService(ComponentActivity.DEVICE_POLICY_SERVICE) as DevicePolicyManager + val receiver = ComponentName(context,Receiver::class.java) + val app = intent.getStringExtra("app") + val mode = intent.getBooleanExtra("mode", false) + if(VERSION.SDK_INT >= 24) { + dpm.setPackagesSuspended(receiver, arrayOf(app), mode) + } else { + Log.d("OwnDroid", "unsupported") + } + } else { + Log.d("OwnDroid", "unknown action") + } + } else { + Log.d("OwnDroid", "unknown category") + } + } +} diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 05a71bb..585b315 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -535,6 +535,7 @@ 你不能清除OwnDroid的存储空间 清除存储空间 清除存储空间成功\n应用即将退出 + 自动化 读取外部存储 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index da159d2..21abef9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -551,6 +551,7 @@ You can\'t clear storage of OwnDroid Clear storage Clear storage success\nApplication will exit + Automation Read external storage From 015c5715469d0c964bbbc527c1a80b726ffd1b5a Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Sat, 29 Jun 2024 10:59:36 +0800 Subject: [PATCH 02/17] automation: unsuspend app --- .github/workflows/build.yml | 1 + .../com/bintianqi/owndroid/TaskReceiver.kt | 30 ++++++------------- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 01da48b..e50da26 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,6 +4,7 @@ on: push: paths-ignore: - '**.md' + branches: [ "master" ] jobs: build: diff --git a/app/src/main/java/com/bintianqi/owndroid/TaskReceiver.kt b/app/src/main/java/com/bintianqi/owndroid/TaskReceiver.kt index ddf601b..5e92e60 100644 --- a/app/src/main/java/com/bintianqi/owndroid/TaskReceiver.kt +++ b/app/src/main/java/com/bintianqi/owndroid/TaskReceiver.kt @@ -11,28 +11,16 @@ import androidx.activity.ComponentActivity class TaskReceiver: BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { - Log.d("OwnDroid", ("TaskReceiver: pkgName: " + intent.component?.packageName)) - Log.d("OwnDroid", ("TaskReceiver: pkg: " + intent.`package`)) - val sharedPref = context.getSharedPreferences("data", Context.MODE_PRIVATE) - if(sharedPref.getString("AutomationApp", "") != intent.component?.packageName) return - val category = intent.getStringExtra("category") - if(category == "app") { - val action = intent.getStringExtra("action") - if(action == "suspend") { - val dpm = context.getSystemService(ComponentActivity.DEVICE_POLICY_SERVICE) as DevicePolicyManager - val receiver = ComponentName(context,Receiver::class.java) - val app = intent.getStringExtra("app") - val mode = intent.getBooleanExtra("mode", false) - if(VERSION.SDK_INT >= 24) { - dpm.setPackagesSuspended(receiver, arrayOf(app), mode) - } else { - Log.d("OwnDroid", "unsupported") - } - } else { - Log.d("OwnDroid", "unknown action") - } + val action = intent.getStringExtra("action") + val dpm = context.getSystemService(ComponentActivity.DEVICE_POLICY_SERVICE) as DevicePolicyManager + val receiver = ComponentName(context,Receiver::class.java) + val app = intent.getStringExtra("app") + if(action == "suspend") { + dpm.setPackagesSuspended(receiver, arrayOf(app), true) + } else if(action == "unsuspend") { + dpm.setPackagesSuspended(receiver, arrayOf(app), false) } else { - Log.d("OwnDroid", "unknown category") + Log.d("OwnDroid", "unknown action") } } } From 172f7d081ea0e586dd3eac3f383f772af67be4ea Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Tue, 2 Jul 2024 14:31:01 +0800 Subject: [PATCH 03/17] add AutomationActivity --- app/src/main/AndroidManifest.xml | 12 ++++- .../bintianqi/owndroid/AutomationActivity.kt | 25 +++++++++++ .../bintianqi/owndroid/AutomationReceiver.kt | 44 +++++++++++++++++++ .../java/com/bintianqi/owndroid/Setting.kt | 30 ++++++++++--- .../com/bintianqi/owndroid/TaskReceiver.kt | 26 ----------- app/src/main/res/values-zh-rCN/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 7 files changed, 107 insertions(+), 34 deletions(-) create mode 100644 app/src/main/java/com/bintianqi/owndroid/AutomationActivity.kt create mode 100644 app/src/main/java/com/bintianqi/owndroid/AutomationReceiver.kt delete mode 100644 app/src/main/java/com/bintianqi/owndroid/TaskReceiver.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9c895ed..ee5b5f3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -51,6 +51,16 @@ android:windowSoftInputMode="adjustResize|stateHidden" android:theme="@style/Theme.OwnDroid"> + + + + + diff --git a/app/src/main/java/com/bintianqi/owndroid/AutomationActivity.kt b/app/src/main/java/com/bintianqi/owndroid/AutomationActivity.kt new file mode 100644 index 0000000..464f93c --- /dev/null +++ b/app/src/main/java/com/bintianqi/owndroid/AutomationActivity.kt @@ -0,0 +1,25 @@ +package com.bintianqi.owndroid + +import android.content.Context +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material3.Text + +class AutomationActivity: ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val result = handleTask(applicationContext, this.intent) + val sharedPrefs = applicationContext.getSharedPreferences("data", Context.MODE_PRIVATE) + if(sharedPrefs.getBoolean("automation_debug", false)) { + setContent { + SelectionContainer { + Text(result) + } + } + } else { + finish() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/bintianqi/owndroid/AutomationReceiver.kt b/app/src/main/java/com/bintianqi/owndroid/AutomationReceiver.kt new file mode 100644 index 0000000..a360199 --- /dev/null +++ b/app/src/main/java/com/bintianqi/owndroid/AutomationReceiver.kt @@ -0,0 +1,44 @@ +package com.bintianqi.owndroid + +import android.app.admin.DevicePolicyManager +import android.content.BroadcastReceiver +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.util.Log +import androidx.activity.ComponentActivity + +class AutomationReceiver: BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + handleTask(context, intent) + } +} + +fun handleTask(context: Context, intent: Intent): String { + val sharedPrefs = context.getSharedPreferences("data", Context.MODE_PRIVATE) + val key = sharedPrefs.getString("automation_key", "") ?: "" + if(key.length < 6) { + return "Key length must longer than 6" + } + if(key != intent.getStringExtra("key")) { + return "Wrong key" + } + val operation = intent.getStringExtra("operation") + val dpm = context.getSystemService(ComponentActivity.DEVICE_POLICY_SERVICE) as DevicePolicyManager + val receiver = ComponentName(context,Receiver::class.java) + val app = intent.getStringExtra("app") + try { + when(operation) { + "suspend" -> dpm.setPackagesSuspended(receiver, arrayOf(app), true) + "unsuspend" -> dpm.setPackagesSuspended(receiver, arrayOf(app), false) + "hide" -> dpm.setApplicationHidden(receiver, app, true) + "unhide" -> dpm.setApplicationHidden(receiver, app, false) + "lock" -> dpm.lockNow() + "reboot" -> dpm.reboot(receiver) + else -> return "Operation not defined" + } + } catch(e: Exception) { + return e.message ?: "Failed to get error message" + } + return "No error, or error is unhandled" +} diff --git a/app/src/main/java/com/bintianqi/owndroid/Setting.kt b/app/src/main/java/com/bintianqi/owndroid/Setting.kt index 43a0852..92f026e 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Setting.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Setting.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.os.Build.VERSION +import android.widget.Toast import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState @@ -128,16 +129,31 @@ private fun AuthSettings() { @Composable private fun Automation() { - val sharedPref = LocalContext.current.getSharedPreferences("data", Context.MODE_PRIVATE) - Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())) { - var pkgName by remember { mutableStateOf("") } + val context = LocalContext.current + val sharedPref = context.getSharedPreferences("data", Context.MODE_PRIVATE) + Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { + var key by remember { mutableStateOf("") } LaunchedEffect(Unit) { - pkgName = sharedPref.getString("AutomationApp", "")?: "" + key = sharedPref.getString("automation_key", "")?: "" } - TextField(value = pkgName, onValueChange = { pkgName = it }, label = { Text("Package name")}) - Button(onClick = {sharedPref.edit().putString("AutomationApp", pkgName).apply()}) { - Text("apply") + TextField( + value = key, onValueChange = { key = it }, label = { Text("Key")}, + modifier = Modifier.fillMaxWidth() + ) + Button( + modifier = Modifier.fillMaxWidth(), + onClick = { + sharedPref.edit().putString("automation_key", key).apply() + Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + } + ) { + Text(stringResource(R.string.apply)) } + SwitchItem( + R.string.automation_debug, "", null, + { sharedPref.getBoolean("automation_debug", false) }, + { sharedPref.edit().putBoolean("automation_debug", it).apply() } + ) } } diff --git a/app/src/main/java/com/bintianqi/owndroid/TaskReceiver.kt b/app/src/main/java/com/bintianqi/owndroid/TaskReceiver.kt deleted file mode 100644 index 5e92e60..0000000 --- a/app/src/main/java/com/bintianqi/owndroid/TaskReceiver.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.bintianqi.owndroid - -import android.app.admin.DevicePolicyManager -import android.content.BroadcastReceiver -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.os.Build.VERSION -import android.util.Log -import androidx.activity.ComponentActivity - -class TaskReceiver: BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - val action = intent.getStringExtra("action") - val dpm = context.getSystemService(ComponentActivity.DEVICE_POLICY_SERVICE) as DevicePolicyManager - val receiver = ComponentName(context,Receiver::class.java) - val app = intent.getStringExtra("app") - if(action == "suspend") { - dpm.setPackagesSuspended(receiver, arrayOf(app), true) - } else if(action == "unsuspend") { - dpm.setPackagesSuspended(receiver, arrayOf(app), false) - } else { - Log.d("OwnDroid", "unknown action") - } - } -} diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 585b315..d9a0639 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -535,7 +535,9 @@ 你不能清除OwnDroid的存储空间 清除存储空间 清除存储空间成功\n应用即将退出 + 自动化 + 调试模式 读取外部存储 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 21abef9..9ff1e1a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -551,7 +551,9 @@ You can\'t clear storage of OwnDroid Clear storage Clear storage success\nApplication will exit + Automation + Debug mode Read external storage From 4d8c3a7a604f05d156638796cf26fd0a7bba4ebc Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Fri, 5 Jul 2024 12:02:36 +0800 Subject: [PATCH 04/17] add deactivate dialog --- .../com/bintianqi/owndroid/MainActivity.kt | 2 +- .../com/bintianqi/owndroid/dpm/Permissions.kt | 111 +++++++++++++++--- 2 files changed, 96 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt index d4e613d..e568df1 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -100,7 +100,7 @@ fun Home(materialYou:MutableState, blackTheme:MutableState) { val pkgName = mutableStateOf("") val dialogStatus = mutableIntStateOf(0) val backToHome by backToHomeStateFlow.collectAsState() - LaunchedEffect(Unit) { + LaunchedEffect(backToHome) { if(backToHome) { navCtrl.navigateUp(); backToHomeStateFlow.value = false } } NavHost( 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 222773e..c6840b9 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt @@ -36,6 +36,7 @@ import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import com.bintianqi.owndroid.R import com.bintianqi.owndroid.Receiver +import com.bintianqi.owndroid.backToHomeStateFlow import com.bintianqi.owndroid.ui.* import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -178,8 +179,8 @@ private fun DeviceAdmin() { val context = LocalContext.current val dpm = context.getSystemService(ComponentActivity.DEVICE_POLICY_SERVICE) as DevicePolicyManager val receiver = ComponentName(context,Receiver::class.java) - val co = rememberCoroutineScope() var showDeactivateButton by remember { mutableStateOf(dpm.isAdminActive(receiver)) } + var deactivateDialog by remember { mutableStateOf(false) } Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 8.dp)) { Spacer(Modifier.padding(vertical = 10.dp)) Text(text = stringResource(R.string.device_admin), style = typography.headlineLarge) @@ -187,10 +188,7 @@ private fun DeviceAdmin() { Spacer(Modifier.padding(vertical = 5.dp)) AnimatedVisibility(showDeactivateButton) { Button( - onClick = { - dpm.removeActiveAdmin(receiver) - co.launch{ delay(400); showDeactivateButton = dpm.isAdminActive(receiver) } - }, + onClick = { deactivateDialog = true }, enabled = !isProfileOwner(dpm) && !isDeviceOwner(dpm), colors = ButtonDefaults.buttonColors(containerColor = colorScheme.error, contentColor = colorScheme.onError) ) { @@ -210,6 +208,35 @@ private fun DeviceAdmin() { } } } + if(deactivateDialog) { + val co = rememberCoroutineScope() + AlertDialog( + title = { Text(stringResource(R.string.deactivate)) }, + onDismissRequest = { deactivateDialog = false }, + dismissButton = { + TextButton( + onClick = { deactivateDialog = false } + ) { + Text(stringResource(R.string.cancel)) + } + }, + confirmButton = { + TextButton( + onClick = { + dpm.removeActiveAdmin(receiver) + co.launch{ + delay(300) + deactivateDialog = false + showDeactivateButton = dpm.isAdminActive(receiver) + backToHomeStateFlow.value = !dpm.isAdminActive(receiver) + } + } + ) { + Text(stringResource(R.string.confirm)) + } + } + ) + } } @Composable @@ -218,19 +245,16 @@ private fun ProfileOwner() { val dpm = context.getSystemService(ComponentActivity.DEVICE_POLICY_SERVICE) as DevicePolicyManager val receiver = ComponentName(context,Receiver::class.java) var showDeactivateButton by remember { mutableStateOf(isProfileOwner(dpm)) } + var deactivateDialog by remember { mutableStateOf(false) } Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 8.dp)) { Spacer(Modifier.padding(vertical = 10.dp)) Text(text = stringResource(R.string.profile_owner), style = typography.headlineLarge) Text(stringResource(if(isProfileOwner(dpm)) R.string.activated else R.string.deactivated), style = typography.titleLarge) Spacer(Modifier.padding(vertical = 5.dp)) - if(VERSION.SDK_INT>=24) { + if(VERSION.SDK_INT >= 24) { AnimatedVisibility(showDeactivateButton) { - val co = rememberCoroutineScope() Button( - onClick = { - dpm.clearProfileOwner(receiver) - co.launch { delay(400); showDeactivateButton=isProfileOwner(dpm) } - }, + onClick = { deactivateDialog = true }, enabled = !dpm.isManagedProfile(receiver), colors = ButtonDefaults.buttonColors(containerColor = colorScheme.error, contentColor = colorScheme.onError) ) { @@ -247,14 +271,43 @@ private fun ProfileOwner() { } } } + if(deactivateDialog && VERSION.SDK_INT >= 24) { + val co = rememberCoroutineScope() + AlertDialog( + title = { Text(stringResource(R.string.deactivate)) }, + onDismissRequest = { deactivateDialog = false }, + dismissButton = { + TextButton( + onClick = { deactivateDialog = false } + ) { + Text(stringResource(R.string.cancel)) + } + }, + confirmButton = { + TextButton( + onClick = { + dpm.clearProfileOwner(receiver) + co.launch{ + delay(300) + deactivateDialog = false + showDeactivateButton = isProfileOwner(dpm) + backToHomeStateFlow.value = !isProfileOwner(dpm) + } + } + ) { + Text(stringResource(R.string.confirm)) + } + } + ) + } } @Composable private fun DeviceOwner() { val context = LocalContext.current val dpm = context.getSystemService(ComponentActivity.DEVICE_POLICY_SERVICE) as DevicePolicyManager - val co = rememberCoroutineScope() var showDeactivateButton by remember { mutableStateOf(isDeviceOwner(dpm)) } + var deactivateDialog by remember { mutableStateOf(false) } Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 8.dp)) { Spacer(Modifier.padding(vertical = 10.dp)) Text(text = stringResource(R.string.device_owner), style = typography.headlineLarge) @@ -262,10 +315,7 @@ private fun DeviceOwner() { Spacer(Modifier.padding(vertical = 5.dp)) AnimatedVisibility(showDeactivateButton) { Button( - onClick = { - dpm.clearDeviceOwnerApp(context.packageName) - co.launch{ delay(400); showDeactivateButton=isDeviceOwner(dpm) } - }, + onClick = { deactivateDialog = true }, colors = ButtonDefaults.buttonColors(containerColor = colorScheme.error, contentColor = colorScheme.onError) ) { Text(text = stringResource(R.string.deactivate)) @@ -280,6 +330,35 @@ private fun DeviceOwner() { } } } + if(deactivateDialog) { + val co = rememberCoroutineScope() + AlertDialog( + title = { Text(stringResource(R.string.deactivate)) }, + onDismissRequest = { deactivateDialog = false }, + dismissButton = { + TextButton( + onClick = { deactivateDialog = false } + ) { + Text(stringResource(R.string.cancel)) + } + }, + confirmButton = { + TextButton( + onClick = { + dpm.clearDeviceOwnerApp(context.packageName) + co.launch{ + delay(300) + deactivateDialog = false + showDeactivateButton = isDeviceOwner(dpm) + backToHomeStateFlow.value = !isDeviceOwner(dpm) + } + } + ) { + Text(stringResource(R.string.confirm)) + } + } + ) + } } @Composable From 89cfafb03a2cd8cebd365bdd47757ad162e03638 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Sat, 6 Jul 2024 11:03:54 +0800 Subject: [PATCH 05/17] app installer: get basic apk info --- .../internal/apk/AndroidBinXmlParser.java | 635 ++++++++++++++++++ .../bintianqi/owndroid/InstallAppActivity.kt | 55 +- .../com/bintianqi/owndroid/MainActivity.kt | 12 +- .../java/com/github/fishb1/apkinfo/ApkInfo.kt | 52 ++ .../github/fishb1/apkinfo/ApkInfoBuilder.kt | 64 ++ .../github/fishb1/apkinfo/ManifestUtils.kt | 55 ++ app/src/main/res/values-tr/strings.xml | 5 +- app/src/main/res/values-zh-rCN/strings.xml | 3 + app/src/main/res/values/strings.xml | 3 + 9 files changed, 866 insertions(+), 18 deletions(-) create mode 100644 app/src/main/java/com/android/apksig/internal/apk/AndroidBinXmlParser.java create mode 100644 app/src/main/java/com/github/fishb1/apkinfo/ApkInfo.kt create mode 100644 app/src/main/java/com/github/fishb1/apkinfo/ApkInfoBuilder.kt create mode 100644 app/src/main/java/com/github/fishb1/apkinfo/ManifestUtils.kt diff --git a/app/src/main/java/com/android/apksig/internal/apk/AndroidBinXmlParser.java b/app/src/main/java/com/android/apksig/internal/apk/AndroidBinXmlParser.java new file mode 100644 index 0000000..573fd93 --- /dev/null +++ b/app/src/main/java/com/android/apksig/internal/apk/AndroidBinXmlParser.java @@ -0,0 +1,635 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.apksig.internal.apk; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@SuppressWarnings("unused") +public class AndroidBinXmlParser { + public static final int EVENT_START_DOCUMENT = 1; + public static final int EVENT_END_DOCUMENT = 2; + public static final int EVENT_START_ELEMENT = 3; + public static final int EVENT_END_ELEMENT = 4; + public static final int VALUE_TYPE_UNSUPPORTED = 0; + public static final int VALUE_TYPE_STRING = 1; + public static final int VALUE_TYPE_INT = 2; + public static final int VALUE_TYPE_REFERENCE = 3; + public static final int VALUE_TYPE_BOOLEAN = 4; + private static final long NO_NAMESPACE = 0xffffffffL; + private final ByteBuffer mXml; + private StringPool mStringPool; + private ResourceMap mResourceMap; + private int mDepth; + private int mCurrentEvent = EVENT_START_DOCUMENT; + private String mCurrentElementName; + private String mCurrentElementNamespace; + private int mCurrentElementAttributeCount; + private List mCurrentElementAttributes; + private ByteBuffer mCurrentElementAttributesContents; + private int mCurrentElementAttrSizeBytes; + + public AndroidBinXmlParser(ByteBuffer xml) throws XmlParserException { + xml.order(ByteOrder.LITTLE_ENDIAN); + Chunk resXmlChunk = null; + while (xml.hasRemaining()) { + Chunk chunk = Chunk.get(xml); + if (chunk == null) { + break; + } + if (chunk.getType() == Chunk.TYPE_RES_XML) { + resXmlChunk = chunk; + break; + } + } + if (resXmlChunk == null) { + throw new XmlParserException("No XML chunk in file"); + } + mXml = resXmlChunk.getContents(); + } + + public int getDepth() { + return mDepth; + } + + public int getEventType() { + return mCurrentEvent; + } + + public String getName() { + if ((mCurrentEvent != EVENT_START_ELEMENT) && (mCurrentEvent != EVENT_END_ELEMENT)) { + return null; + } + return mCurrentElementName; + } + + public String getNamespace() { + if ((mCurrentEvent != EVENT_START_ELEMENT) && (mCurrentEvent != EVENT_END_ELEMENT)) { + return null; + } + return mCurrentElementNamespace; + } + + public int getAttributeCount() { + if (mCurrentEvent != EVENT_START_ELEMENT) { + return -1; + } + return mCurrentElementAttributeCount; + } + + public int getAttributeNameResourceId(int index) throws XmlParserException { + return getAttribute(index).getNameResourceId(); + } + + public String getAttributeName(int index) throws XmlParserException { + return getAttribute(index).getName(); + } + + public String getAttributeNamespace(int index) throws XmlParserException { + return getAttribute(index).getNamespace(); + } + + public int getAttributeValueType(int index) throws XmlParserException { + int type = getAttribute(index).getValueType(); + switch (type) { + case Attribute.TYPE_STRING: + return VALUE_TYPE_STRING; + case Attribute.TYPE_INT_DEC: + case Attribute.TYPE_INT_HEX: + return VALUE_TYPE_INT; + case Attribute.TYPE_REFERENCE: + return VALUE_TYPE_REFERENCE; + case Attribute.TYPE_INT_BOOLEAN: + return VALUE_TYPE_BOOLEAN; + default: + return VALUE_TYPE_UNSUPPORTED; + } + } + + public int getAttributeIntValue(int index) throws XmlParserException { + return getAttribute(index).getIntValue(); + } + + public boolean getAttributeBooleanValue(int index) throws XmlParserException { + return getAttribute(index).getBooleanValue(); + } + + public String getAttributeStringValue(int index) throws XmlParserException { + return getAttribute(index).getStringValue(); + } + private Attribute getAttribute(int index) { + if (mCurrentEvent != EVENT_START_ELEMENT) { + throw new IndexOutOfBoundsException("Current event not a START_ELEMENT"); + } + if (index < 0) { + throw new IndexOutOfBoundsException("index must be >= 0"); + } + if (index >= mCurrentElementAttributeCount) { + throw new IndexOutOfBoundsException( + "index must be <= attr count (" + mCurrentElementAttributeCount + ")"); + } + parseCurrentElementAttributesIfNotParsed(); + return mCurrentElementAttributes.get(index); + } + + public int next() throws XmlParserException { + if (mCurrentEvent == EVENT_END_ELEMENT) { + mDepth--; + } + while (mXml.hasRemaining()) { + Chunk chunk = Chunk.get(mXml); + if (chunk == null) { + break; + } + switch (chunk.getType()) { + case Chunk.TYPE_STRING_POOL: + if (mStringPool != null) { + throw new XmlParserException("Multiple string pools not supported"); + } + mStringPool = new StringPool(chunk); + break; + case Chunk.RES_XML_TYPE_START_ELEMENT: + { + if (mStringPool == null) { + throw new XmlParserException( + "Named element encountered before string pool"); + } + ByteBuffer contents = chunk.getContents(); + if (contents.remaining() < 20) { + throw new XmlParserException( + "Start element chunk too short. Need at least 20 bytes. Available: " + + contents.remaining() + " bytes"); + } + long nsId = getUnsignedInt32(contents); + long nameId = getUnsignedInt32(contents); + int attrStartOffset = getUnsignedInt16(contents); + int attrSizeBytes = getUnsignedInt16(contents); + int attrCount = getUnsignedInt16(contents); + long attrEndOffset = attrStartOffset + ((long) attrCount) * attrSizeBytes; + contents.position(0); + if (attrStartOffset > contents.remaining()) { + throw new XmlParserException( + "Attributes start offset out of bounds: " + attrStartOffset + + ", max: " + contents.remaining()); + } + if (attrEndOffset > contents.remaining()) { + throw new XmlParserException( + "Attributes end offset out of bounds: " + attrEndOffset + + ", max: " + contents.remaining()); + } + mCurrentElementName = mStringPool.getString(nameId); + mCurrentElementNamespace = + (nsId == NO_NAMESPACE) ? "" : mStringPool.getString(nsId); + mCurrentElementAttributeCount = attrCount; + mCurrentElementAttributes = null; + mCurrentElementAttrSizeBytes = attrSizeBytes; + mCurrentElementAttributesContents = + sliceFromTo(contents, attrStartOffset, attrEndOffset); + mDepth++; + mCurrentEvent = EVENT_START_ELEMENT; + return mCurrentEvent; + } + case Chunk.RES_XML_TYPE_END_ELEMENT: + { + if (mStringPool == null) { + throw new XmlParserException( + "Named element encountered before string pool"); + } + ByteBuffer contents = chunk.getContents(); + if (contents.remaining() < 8) { + throw new XmlParserException( + "End element chunk too short. Need at least 8 bytes. Available: " + + contents.remaining() + " bytes"); + } + long nsId = getUnsignedInt32(contents); + long nameId = getUnsignedInt32(contents); + mCurrentElementName = mStringPool.getString(nameId); + mCurrentElementNamespace = + (nsId == NO_NAMESPACE) ? "" : mStringPool.getString(nsId); + mCurrentEvent = EVENT_END_ELEMENT; + mCurrentElementAttributes = null; + mCurrentElementAttributesContents = null; + return mCurrentEvent; + } + case Chunk.RES_XML_TYPE_RESOURCE_MAP: + if (mResourceMap != null) { + throw new XmlParserException("Multiple resource maps not supported"); + } + mResourceMap = new ResourceMap(chunk); + break; + default: + break; + } + } + mCurrentEvent = EVENT_END_DOCUMENT; + return mCurrentEvent; + } + private void parseCurrentElementAttributesIfNotParsed() { + if (mCurrentElementAttributes != null) { + return; + } + mCurrentElementAttributes = new ArrayList<>(mCurrentElementAttributeCount); + for (int i = 0; i < mCurrentElementAttributeCount; i++) { + int startPosition = i * mCurrentElementAttrSizeBytes; + ByteBuffer attr = + sliceFromTo( + mCurrentElementAttributesContents, + startPosition, + startPosition + mCurrentElementAttrSizeBytes); + long nsId = getUnsignedInt32(attr); + long nameId = getUnsignedInt32(attr); + attr.position(attr.position() + 7); // skip ignored fields + int valueType = getUnsignedInt8(attr); + long valueData = getUnsignedInt32(attr); + mCurrentElementAttributes.add( + new Attribute( + nsId, + nameId, + valueType, + (int) valueData, + mStringPool, + mResourceMap)); + } + } + private static class Attribute { + private static final int TYPE_REFERENCE = 1; + private static final int TYPE_STRING = 3; + private static final int TYPE_INT_DEC = 0x10; + private static final int TYPE_INT_HEX = 0x11; + private static final int TYPE_INT_BOOLEAN = 0x12; + private final long mNsId; + private final long mNameId; + private final int mValueType; + private final int mValueData; + private final StringPool mStringPool; + private final ResourceMap mResourceMap; + private Attribute( + long nsId, + long nameId, + int valueType, + int valueData, + StringPool stringPool, + ResourceMap resourceMap) { + mNsId = nsId; + mNameId = nameId; + mValueType = valueType; + mValueData = valueData; + mStringPool = stringPool; + mResourceMap = resourceMap; + } + public int getNameResourceId() { + return (mResourceMap != null) ? mResourceMap.getResourceId(mNameId) : 0; + } + public String getName() throws XmlParserException { + return mStringPool.getString(mNameId); + } + public String getNamespace() throws XmlParserException { + return (mNsId != NO_NAMESPACE) ? mStringPool.getString(mNsId) : ""; + } + public int getValueType() { + return mValueType; + } + public int getIntValue() throws XmlParserException { + switch (mValueType) { + case TYPE_REFERENCE: + case TYPE_INT_DEC: + case TYPE_INT_HEX: + case TYPE_INT_BOOLEAN: + return mValueData; + default: + throw new XmlParserException("Cannot coerce to int: value type " + mValueType); + } + } + public boolean getBooleanValue() throws XmlParserException { + //noinspection SwitchStatementWithTooFewBranches + switch (mValueType) { + case TYPE_INT_BOOLEAN: + return mValueData != 0; + default: + throw new XmlParserException( + "Cannot coerce to boolean: value type " + mValueType); + } + } + public String getStringValue() throws XmlParserException { + switch (mValueType) { + case TYPE_STRING: + return mStringPool.getString(mValueData & 0xffffffffL); + case TYPE_INT_DEC: + return Integer.toString(mValueData); + case TYPE_INT_HEX: + return "0x" + Integer.toHexString(mValueData); + case TYPE_INT_BOOLEAN: + return Boolean.toString(mValueData != 0); + case TYPE_REFERENCE: + return "@" + Integer.toHexString(mValueData); + default: + throw new XmlParserException( + "Cannot coerce to string: value type " + mValueType); + } + } + } + + private static class Chunk { + public static final int TYPE_STRING_POOL = 1; + public static final int TYPE_RES_XML = 3; + public static final int RES_XML_TYPE_START_ELEMENT = 0x0102; + public static final int RES_XML_TYPE_END_ELEMENT = 0x0103; + public static final int RES_XML_TYPE_RESOURCE_MAP = 0x0180; + static final int HEADER_MIN_SIZE_BYTES = 8; + private final int mType; + private final ByteBuffer mHeader; + private final ByteBuffer mContents; + public Chunk(int type, ByteBuffer header, ByteBuffer contents) { + mType = type; + mHeader = header; + mContents = contents; + } + public ByteBuffer getContents() { + ByteBuffer result = mContents.slice(); + result.order(mContents.order()); + return result; + } + public ByteBuffer getHeader() { + ByteBuffer result = mHeader.slice(); + result.order(mHeader.order()); + return result; + } + public int getType() { + return mType; + } + + public static Chunk get(ByteBuffer input) throws XmlParserException { + if (input.remaining() < HEADER_MIN_SIZE_BYTES) { + // Android ignores the last chunk if its header is too big to fit into the file + input.position(input.limit()); + return null; + } + int originalPosition = input.position(); + int type = getUnsignedInt16(input); + int headerSize = getUnsignedInt16(input); + long chunkSize = getUnsignedInt32(input); + long chunkRemaining = chunkSize - 8; + if (chunkRemaining > input.remaining()) { + input.position(input.limit()); + return null; + } + if (headerSize < HEADER_MIN_SIZE_BYTES) { + throw new XmlParserException( + "Malformed chunk: header too short: " + headerSize + " bytes"); + } else if (headerSize > chunkSize) { + throw new XmlParserException( + "Malformed chunk: header too long: " + headerSize + " bytes. Chunk size: " + + chunkSize + " bytes"); + } + int contentStartPosition = originalPosition + headerSize; + long chunkEndPosition = originalPosition + chunkSize; + Chunk chunk = + new Chunk( + type, + sliceFromTo(input, originalPosition, contentStartPosition), + sliceFromTo(input, contentStartPosition, chunkEndPosition)); + input.position((int) chunkEndPosition); + return chunk; + } + } + + private static class StringPool { + private static final int FLAG_UTF8 = 1 << 8; + private final ByteBuffer mChunkContents; + private final ByteBuffer mStringsSection; + private final int mStringCount; + private final boolean mUtf8Encoded; + private final Map mCachedStrings = new HashMap<>(); + + public StringPool(Chunk chunk) throws XmlParserException { + ByteBuffer header = chunk.getHeader(); + int headerSizeBytes = header.remaining(); + header.position(Chunk.HEADER_MIN_SIZE_BYTES); + if (header.remaining() < 20) { + throw new XmlParserException( + "XML chunk's header too short. Required at least 20 bytes. Available: " + + header.remaining() + " bytes"); + } + long stringCount = getUnsignedInt32(header); + if (stringCount > Integer.MAX_VALUE) { + throw new XmlParserException("Too many strings: " + stringCount); + } + mStringCount = (int) stringCount; + long styleCount = getUnsignedInt32(header); + if (styleCount > Integer.MAX_VALUE) { + throw new XmlParserException("Too many styles: " + styleCount); + } + long flags = getUnsignedInt32(header); + long stringsStartOffset = getUnsignedInt32(header); + long stylesStartOffset = getUnsignedInt32(header); + ByteBuffer contents = chunk.getContents(); + if (mStringCount > 0) { + int stringsSectionStartOffsetInContents = + (int) (stringsStartOffset - headerSizeBytes); + int stringsSectionEndOffsetInContents; + if (styleCount > 0) { + if (stylesStartOffset < stringsStartOffset) { + throw new XmlParserException( + "Styles offset (" + stylesStartOffset + ") < strings offset (" + + stringsStartOffset + ")"); + } + stringsSectionEndOffsetInContents = (int) (stylesStartOffset - headerSizeBytes); + } else { + stringsSectionEndOffsetInContents = contents.remaining(); + } + mStringsSection = + sliceFromTo( + contents, + stringsSectionStartOffsetInContents, + stringsSectionEndOffsetInContents); + } else { + mStringsSection = ByteBuffer.allocate(0); + } + mUtf8Encoded = (flags & FLAG_UTF8) != 0; + mChunkContents = contents; + } + + public String getString(long index) throws XmlParserException { + if (index < 0) { + throw new XmlParserException("Unsuported string index: " + index); + } else if (index >= mStringCount) { + throw new XmlParserException( + "Unsuported string index: " + index + ", max: " + (mStringCount - 1)); + } + int idx = (int) index; + String result = mCachedStrings.get(idx); + if (result != null) { + return result; + } + long offsetInStringsSection = getUnsignedInt32(mChunkContents, idx * 4); + if (offsetInStringsSection >= mStringsSection.capacity()) { + throw new XmlParserException( + "Offset of string idx " + idx + " out of bounds: " + offsetInStringsSection + + ", max: " + (mStringsSection.capacity() - 1)); + } + mStringsSection.position((int) offsetInStringsSection); + result = + (mUtf8Encoded) + ? getLengthPrefixedUtf8EncodedString(mStringsSection) + : getLengthPrefixedUtf16EncodedString(mStringsSection); + mCachedStrings.put(idx, result); + return result; + } + private static String getLengthPrefixedUtf16EncodedString(ByteBuffer encoded) + throws XmlParserException { + int lengthChars = getUnsignedInt16(encoded); + if ((lengthChars & 0x8000) != 0) { + lengthChars = ((lengthChars & 0x7fff) << 16) | getUnsignedInt16(encoded); + } + if (lengthChars > Integer.MAX_VALUE / 2) { + throw new XmlParserException("String too long: " + lengthChars + " uint16s"); + } + int lengthBytes = lengthChars * 2; + byte[] arr; + int arrOffset; + if (encoded.hasArray()) { + arr = encoded.array(); + arrOffset = encoded.arrayOffset() + encoded.position(); + encoded.position(encoded.position() + lengthBytes); + } else { + arr = new byte[lengthBytes]; + arrOffset = 0; + encoded.get(arr); + } + if ((arr[arrOffset + lengthBytes] != 0) + || (arr[arrOffset + lengthBytes + 1] != 0)) { + throw new XmlParserException("UTF-16 encoded form of string not NULL terminated"); + } + return new String(arr, arrOffset, lengthBytes, StandardCharsets.UTF_16LE); + } + private static String getLengthPrefixedUtf8EncodedString(ByteBuffer encoded) + throws XmlParserException { + int lengthBytes = getUnsignedInt8(encoded); + if ((lengthBytes & 0x80) != 0) { + lengthBytes = ((lengthBytes & 0x7f) << 8) | getUnsignedInt8(encoded); + } + lengthBytes = getUnsignedInt8(encoded); + if ((lengthBytes & 0x80) != 0) { + lengthBytes = ((lengthBytes & 0x7f) << 8) | getUnsignedInt8(encoded); + } + byte[] arr; + int arrOffset; + if (encoded.hasArray()) { + arr = encoded.array(); + arrOffset = encoded.arrayOffset() + encoded.position(); + encoded.position(encoded.position() + lengthBytes); + } else { + arr = new byte[lengthBytes]; + arrOffset = 0; + encoded.get(arr); + } + if (arr[arrOffset + lengthBytes] != 0) { + throw new XmlParserException("UTF-8 encoded form of string not NULL terminated"); + } + return new String(arr, arrOffset, lengthBytes, StandardCharsets.UTF_8); + } + } + + private static class ResourceMap { + private final ByteBuffer mChunkContents; + private final int mEntryCount; + + public ResourceMap(Chunk chunk) throws XmlParserException { + mChunkContents = chunk.getContents().slice(); + mChunkContents.order(chunk.getContents().order()); + // Each entry of the map is four bytes long, containing the int32 resource ID. + mEntryCount = mChunkContents.remaining() / 4; + } + + public int getResourceId(long index) { + if ((index < 0) || (index >= mEntryCount)) { + return 0; + } + int idx = (int) index; + // Each entry of the map is four bytes long, containing the int32 resource ID. + return mChunkContents.getInt(idx * 4); + } + } + + private static ByteBuffer sliceFromTo(ByteBuffer source, long start, long end) { + if (start < 0) { + throw new IllegalArgumentException("start: " + start); + } + if (end < start) { + throw new IllegalArgumentException("end < start: " + end + " < " + start); + } + int capacity = source.capacity(); + if (end > source.capacity()) { + throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity); + } + return sliceFromTo(source, (int) start, (int) end); + } + + private static ByteBuffer sliceFromTo(ByteBuffer source, int start, int end) { + if (start < 0) { + throw new IllegalArgumentException("start: " + start); + } + if (end < start) { + throw new IllegalArgumentException("end < start: " + end + " < " + start); + } + int capacity = source.capacity(); + if (end > source.capacity()) { + throw new IllegalArgumentException("end > capacity: " + end + " > " + capacity); + } + int originalLimit = source.limit(); + int originalPosition = source.position(); + try { + source.position(0); + source.limit(end); + source.position(start); + ByteBuffer result = source.slice(); + result.order(source.order()); + return result; + } finally { + source.position(0); + source.limit(originalLimit); + source.position(originalPosition); + } + } + private static int getUnsignedInt8(ByteBuffer buffer) { + return buffer.get() & 0xff; + } + private static int getUnsignedInt16(ByteBuffer buffer) { + return buffer.getShort() & 0xffff; + } + private static long getUnsignedInt32(ByteBuffer buffer) { + return buffer.getInt() & 0xffffffffL; + } + private static long getUnsignedInt32(ByteBuffer buffer, int position) { + return buffer.getInt(position) & 0xffffffffL; + } + + public static class XmlParserException extends Exception { + private static final long serialVersionUID = 1L; + public XmlParserException(String message) { + super(message); + } + public XmlParserException(String message, Throwable cause) { + super(message, cause); + } + } +} diff --git a/app/src/main/java/com/bintianqi/owndroid/InstallAppActivity.kt b/app/src/main/java/com/bintianqi/owndroid/InstallAppActivity.kt index 84f8802..178b19e 100644 --- a/app/src/main/java/com/bintianqi/owndroid/InstallAppActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/InstallAppActivity.kt @@ -8,7 +8,9 @@ import android.os.Bundle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.material3.AlertDialog import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.Text @@ -17,13 +19,21 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties +import androidx.core.view.WindowCompat import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.lifecycleScope import com.bintianqi.owndroid.dpm.installPackage import com.bintianqi.owndroid.ui.theme.OwnDroidTheme +import com.github.fishb1.apkinfo.ApkInfo +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.FileInputStream class InstallAppActivity: FragmentActivity() { override fun onNewIntent(intent: Intent?) { @@ -34,37 +44,64 @@ class InstallAppActivity: FragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() + WindowCompat.setDecorFitsSystemWindows(window, false) + val context = applicationContext val sharedPref = applicationContext.getSharedPreferences("data", Context.MODE_PRIVATE) window.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + val uri = this.intent.data!! + var apkInfoText by mutableStateOf(context.getString(R.string.parsing_apk_info)) + var status by mutableStateOf("parsing") + this.lifecycleScope.launch(Dispatchers.IO) { + val fd = applicationContext.contentResolver.openFileDescriptor(uri, "r") + val apkInfo = ApkInfo.fromInputStream( + FileInputStream(fd?.fileDescriptor) + ) + fd?.close() + withContext(Dispatchers.Main) { + status = "waiting" + apkInfoText = "${context.getString(R.string.package_name)}: ${apkInfo.packageName}\n" + apkInfoText += "${context.getString(R.string.version_name)}: ${apkInfo.versionName}\n" + apkInfoText += "${context.getString(R.string.version_code)}: ${apkInfo.versionCode}" + } + } setContent { - var installing by remember { mutableStateOf(false) } OwnDroidTheme( sharedPref.getBoolean("material_you", true), sharedPref.getBoolean("black_theme", false) ) { AlertDialog( + properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false), title = { Text(stringResource(R.string.install_app)) }, onDismissRequest = { - finish() + if(status != "installing") finish() }, text = { - AnimatedVisibility(installing) { - LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + Column { + AnimatedVisibility(status != "waiting") { + LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + } + Text(text = apkInfoText, modifier = Modifier.padding(top = 4.dp)) } }, dismissButton = { - TextButton(onClick = { finish() }) { Text(stringResource(R.string.cancel)) } + TextButton( + onClick = { finish() }, + enabled = status != "installing" + ) { + Text(stringResource(R.string.cancel)) + } }, confirmButton = { TextButton( onClick = { - installing = true + status = "installing" uriToStream(applicationContext, this.intent.data) { stream -> installPackage(applicationContext, stream) } - } + }, + enabled = status != "installing" ) { - Text(stringResource(R.string.confirm)) + Text(stringResource(R.string.install)) } }, modifier = Modifier.fillMaxWidth() diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt index e568df1..a180ab2 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -1,6 +1,5 @@ package com.bintianqi.owndroid -import android.annotation.SuppressLint import android.app.admin.DevicePolicyManager import android.content.ComponentName import android.content.Context @@ -41,7 +40,6 @@ import androidx.navigation.compose.rememberNavController import com.bintianqi.owndroid.dpm.* import com.bintianqi.owndroid.ui.Animations import com.bintianqi.owndroid.ui.theme.OwnDroidTheme -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import java.util.Locale @@ -50,7 +48,6 @@ var backToHomeStateFlow = MutableStateFlow(false) class MainActivity : FragmentActivity() { private val showAuth = mutableStateOf(false) - @SuppressLint("UnrememberedMutableState") override fun onCreate(savedInstanceState: Bundle?) { registerActivityResult(this) enableEdgeToEdge() @@ -63,8 +60,8 @@ class MainActivity : FragmentActivity() { val locale = applicationContext.resources?.configuration?.locale zhCN = locale == Locale.SIMPLIFIED_CHINESE || locale == Locale.CHINESE || locale == Locale.CHINA setContent { - val materialYou = mutableStateOf(sharedPref.getBoolean("material_you", true)) - val blackTheme = mutableStateOf(sharedPref.getBoolean("black_theme", false)) + val materialYou = remember { mutableStateOf(sharedPref.getBoolean("material_you", true)) } + val blackTheme = remember { mutableStateOf(sharedPref.getBoolean("black_theme", false)) } OwnDroidTheme(materialYou.value, blackTheme.value) { Home(materialYou, blackTheme) if(showAuth.value) { @@ -87,7 +84,6 @@ class MainActivity : FragmentActivity() { } -@SuppressLint("UnrememberedMutableState") @ExperimentalMaterial3Api @Composable fun Home(materialYou:MutableState, blackTheme:MutableState) { @@ -97,8 +93,8 @@ fun Home(materialYou:MutableState, blackTheme:MutableState) { val receiver = ComponentName(context,Receiver::class.java) val sharedPref = LocalContext.current.getSharedPreferences("data", Context.MODE_PRIVATE) val focusMgr = LocalFocusManager.current - val pkgName = mutableStateOf("") - val dialogStatus = mutableIntStateOf(0) + val pkgName = remember { mutableStateOf("") } + val dialogStatus = remember { mutableIntStateOf(0) } val backToHome by backToHomeStateFlow.collectAsState() LaunchedEffect(backToHome) { if(backToHome) { navCtrl.navigateUp(); backToHomeStateFlow.value = false } diff --git a/app/src/main/java/com/github/fishb1/apkinfo/ApkInfo.kt b/app/src/main/java/com/github/fishb1/apkinfo/ApkInfo.kt new file mode 100644 index 0000000..716e55f --- /dev/null +++ b/app/src/main/java/com/github/fishb1/apkinfo/ApkInfo.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2022 fishbone + * + * This code is licensed under MIT license (see LICENSE file for details) + */ + +package com.github.fishb1.apkinfo + +import com.android.apksig.internal.apk.AndroidBinXmlParser +import com.android.apksig.internal.apk.AndroidBinXmlParser.XmlParserException +import java.io.InputStream +import java.nio.ByteBuffer +import java.util.zip.ZipEntry +import java.util.zip.ZipInputStream + +data class ApkInfo( + val compileSdkVersion: Int = 0, + val compileSdkVersionCodename: String = "", + val installLocation: String = "", + val packageName: String = "", + val platformBuildVersionCode: Int = 0, + val platformBuildVersionName: String = "", + val versionCode: Int = 0, + val versionName: String = "", +) { + + companion object { + + private val EMPTY = ApkInfo() + + private const val MANIFEST_FILE_NAME = "AndroidManifest.xml" + + fun fromInputStream(stream: InputStream): ApkInfo { + ZipInputStream(stream).use { zip -> + var entry: ZipEntry? + do { + entry = zip.nextEntry + if (entry?.name == MANIFEST_FILE_NAME) { + val data = ByteBuffer.wrap(zip.readBytes()) + return try { + val parser = AndroidBinXmlParser(data) + ManifestUtils.readApkInfo(parser) + } catch (e: XmlParserException) { + EMPTY + } + } + } while (entry != null) + } + return EMPTY + } + } +} diff --git a/app/src/main/java/com/github/fishb1/apkinfo/ApkInfoBuilder.kt b/app/src/main/java/com/github/fishb1/apkinfo/ApkInfoBuilder.kt new file mode 100644 index 0000000..b932e11 --- /dev/null +++ b/app/src/main/java/com/github/fishb1/apkinfo/ApkInfoBuilder.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2022 fishbone + * + * This code is licensed under MIT license (see LICENSE file for details) + */ + +package com.github.fishb1.apkinfo + +internal class ApkInfoBuilder { + + private var compileSdkVersion: Int = 0 + private var compileSdkVersionCodename: String = "" + private var installLocation: String = "" + private var packageName: String = "" + private var platformBuildVersionCode: Int = 0 + private var platformBuildVersionName: String = "" + private var versionCode: Int = 0 + private var versionName: String = "" + + fun compileSdkVersion(value: Int) = apply { + compileSdkVersion = value + } + + fun compileSdkVersionCodename(value: String) = apply { + compileSdkVersionCodename = value + } + + fun installLocation(value: String) = apply { + installLocation = value + } + + fun packageName(value: String) = apply { + packageName = value + } + + fun platformBuildVersionCode(value: Int) = apply { + platformBuildVersionCode = value + } + + fun platformBuildVersionName(value: String) = apply { + platformBuildVersionName = value + } + + fun versionCode(value: Int) = apply { + versionCode = value + } + + fun versionName(value: String) = apply { + versionName = value + } + + fun build(): ApkInfo { + return ApkInfo( + compileSdkVersion = compileSdkVersion, + compileSdkVersionCodename =compileSdkVersionCodename, + installLocation = installLocation, + packageName = packageName, + platformBuildVersionCode = platformBuildVersionCode, + platformBuildVersionName = platformBuildVersionName, + versionCode = versionCode, + versionName = versionName, + ) + } +} diff --git a/app/src/main/java/com/github/fishb1/apkinfo/ManifestUtils.kt b/app/src/main/java/com/github/fishb1/apkinfo/ManifestUtils.kt new file mode 100644 index 0000000..f178e45 --- /dev/null +++ b/app/src/main/java/com/github/fishb1/apkinfo/ManifestUtils.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2022 fishbone + * + * This code is licensed under MIT license (see LICENSE file for details) + */ + +package com.github.fishb1.apkinfo + +import com.android.apksig.internal.apk.AndroidBinXmlParser + +internal object ManifestUtils { + + private const val TAG_MANIFEST = "manifest" + + private const val ATTR_COMPILE_SDK_VERSION = "compileSdkVersion" + private const val ATTR_COMPILE_SDK_VERSION_CODENAME = "compileSdkVersionCodename" + private const val ATTR_INSTALL_LOCATION = "installLocation" + private const val ATTR_PACKAGE = "package" + private const val ATTR_PLATFORM_BUILD_VERSION_CODE = "platformBuildVersionCode" + private const val ATTR_PLATFORM_BUILD_VERSION_NAME = "platformBuildVersionName" + private const val ATTR_VERSION_CODE = "versionCode" + private const val ATTR_VERSION_NAME = "versionName" + + fun readApkInfo(parser: AndroidBinXmlParser): ApkInfo { + val builder = ApkInfoBuilder() + var eventType = parser.eventType + while (eventType != AndroidBinXmlParser.EVENT_END_DOCUMENT) { + if (eventType == AndroidBinXmlParser.EVENT_START_ELEMENT && parser.name == TAG_MANIFEST) { + + for (i in 0 until parser.attributeCount) { + when (parser.getAttributeName(i)) { + ATTR_COMPILE_SDK_VERSION -> + builder.compileSdkVersion(parser.getAttributeIntValue(i)) + ATTR_COMPILE_SDK_VERSION_CODENAME -> + builder.compileSdkVersionCodename(parser.getAttributeStringValue(i)) + ATTR_INSTALL_LOCATION -> + builder.installLocation(parser.getAttributeStringValue(i)) + ATTR_PACKAGE -> + builder.packageName(parser.getAttributeStringValue(i)) + ATTR_PLATFORM_BUILD_VERSION_CODE -> + builder.platformBuildVersionCode(parser.getAttributeIntValue(i)) + ATTR_PLATFORM_BUILD_VERSION_NAME -> + builder.platformBuildVersionName(parser.getAttributeStringValue(i)) + ATTR_VERSION_CODE -> + builder.versionCode(parser.getAttributeIntValue(i)) + ATTR_VERSION_NAME -> + builder.versionName(parser.getAttributeStringValue(i)) + } + } + } + eventType = parser.next() + } + return builder.build() + } +} diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 42d07e5..d6a939e 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -571,5 +571,8 @@ Arka planda vücut sensörlerine eriş Aktivite tanıma Bildirim gönder - + + Version name + Version code + Parsing APK info... diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 05a71bb..a762204 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -563,4 +563,7 @@ 查看使用情况 发送通知 + 版本名 + 版本号 + 解析APK信息中... diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index da159d2..842c01f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -579,4 +579,7 @@ Activity recognition Post notifications + Version name + Version code + Parsing APK info... From 4ebb9b48f095f9dd94ef8093d5c5d46bf5dda801 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Sat, 6 Jul 2024 12:36:02 +0800 Subject: [PATCH 06/17] app installer: reject self-update close #47 --- .../com/bintianqi/owndroid/InstallAppActivity.kt | 16 +++++++++++----- app/src/main/res/values-tr/strings.xml | 1 + app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/bintianqi/owndroid/InstallAppActivity.kt b/app/src/main/java/com/bintianqi/owndroid/InstallAppActivity.kt index 178b19e..a8af9e2 100644 --- a/app/src/main/java/com/bintianqi/owndroid/InstallAppActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/InstallAppActivity.kt @@ -58,28 +58,34 @@ class InstallAppActivity: FragmentActivity() { ) fd?.close() withContext(Dispatchers.Main) { - status = "waiting" apkInfoText = "${context.getString(R.string.package_name)}: ${apkInfo.packageName}\n" apkInfoText += "${context.getString(R.string.version_name)}: ${apkInfo.versionName}\n" apkInfoText += "${context.getString(R.string.version_code)}: ${apkInfo.versionCode}" + if(apkInfo.packageName == packageName) { + status = "self_update" + apkInfoText += "\n" + context.getString(R.string.update_using_system_installer) + } else { + status = "waiting" + } } } setContent { + val canExit = status == "waiting" || status == "self_update" OwnDroidTheme( sharedPref.getBoolean("material_you", true), sharedPref.getBoolean("black_theme", false) ) { AlertDialog( - properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false), + properties = DialogProperties(dismissOnBackPress = canExit, dismissOnClickOutside = canExit), title = { Text(stringResource(R.string.install_app)) }, onDismissRequest = { - if(status != "installing") finish() + if(canExit) finish() }, text = { Column { - AnimatedVisibility(status != "waiting") { + AnimatedVisibility(status != "waiting" && status != "self_update") { LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) } Text(text = apkInfoText, modifier = Modifier.padding(top = 4.dp)) @@ -99,7 +105,7 @@ class InstallAppActivity: FragmentActivity() { status = "installing" uriToStream(applicationContext, this.intent.data) { stream -> installPackage(applicationContext, stream) } }, - enabled = status != "installing" + enabled = status == "waiting" ) { Text(stringResource(R.string.install)) } diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index d6a939e..a93314c 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -574,5 +574,6 @@ Version name Version code + Please update OwnDroid with system installer. Parsing APK info... diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index a762204..14ecaeb 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -565,5 +565,6 @@ 版本名 版本号 + 请使用系统的应用安装器更新OwnDroid。 解析APK信息中... diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 842c01f..8412fef 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -581,5 +581,6 @@ Version name Version code + Please update OwnDroid with system installer. Parsing APK info... From e3f06911c54cb6bb41d50e7793cb035b28c0a08e Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Sat, 6 Jul 2024 16:25:50 +0800 Subject: [PATCH 07/17] fix setAlwaysOnVpnPackage --- .../owndroid/dpm/ApplicationManage.kt | 70 +++++++++++++------ app/src/main/res/values-tr/strings.xml | 3 + app/src/main/res/values-zh-rCN/strings.xml | 3 + app/src/main/res/values/strings.xml | 3 + 4 files changed, 57 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/ApplicationManage.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/ApplicationManage.kt index 8922642..93ee438 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/ApplicationManage.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/ApplicationManage.kt @@ -158,6 +158,7 @@ fun ApplicationManage(navCtrl:NavHostController, pkgName: MutableState, composable(route = "Home") { Home(localNavCtrl, pkgName.value, dialogStatus, clearAppDataDialog, defaultDialerAppDialog, enableSystemAppDialog) } + composable(route = "AlwaysOnVpn") { AlwaysOnVPNPackage(pkgName.value) } composable(route = "UserControlDisabled") { UserCtrlDisabledPkg(pkgName.value) } composable(route = "PermissionManage") { PermissionManage(pkgName.value, navCtrl) } composable(route = "CrossProfilePackage") { CrossProfilePkg(pkgName.value) } @@ -213,8 +214,8 @@ private fun Home( if(VERSION.SDK_INT>=24 && (isDeviceOwner(dpm) || isProfileOwner(dpm))) { val getSuspendStatus = { try{ dpm.isPackageSuspended(receiver, pkgName) } - catch(e:NameNotFoundException) { false } - catch(e:IllegalArgumentException) { false } + catch(e:NameNotFoundException) { false } + catch(e:IllegalArgumentException) { false } } SwitchItem( title = R.string.suspend, desc = "", icon = R.drawable.block_fill0, @@ -255,26 +256,7 @@ private fun Home( ) } if(VERSION.SDK_INT>=24 && (isDeviceOwner(dpm) || isProfileOwner(dpm))) { - val setAlwaysOnVpn: (Boolean)->Unit = { - try { - dpm.setAlwaysOnVpnPackage(receiver, pkgName, it) - } catch(e: UnsupportedOperationException) { - Toast.makeText(context, R.string.unsupported, Toast.LENGTH_SHORT).show() - } catch(e: NameNotFoundException) { - Toast.makeText(context, R.string.not_installed, Toast.LENGTH_SHORT).show() - } - } - SwitchItem( - title = R.string.always_on_vpn, desc = "", icon = R.drawable.vpn_key_fill0, - getState = { pkgName == dpm.getAlwaysOnVpnPackage(receiver) }, - onCheckedChange = setAlwaysOnVpn, - onClickBlank = { - dialogGetStatus = { pkgName == dpm.getAlwaysOnVpnPackage(receiver) } - dialogConfirmButtonAction = { setAlwaysOnVpn(true) } - dialogDismissButtonAction = { setAlwaysOnVpn(false) } - dialogStatus.intValue = 4 - } - ) + SubPageItem(R.string.always_on_vpn, "", R.drawable.vpn_key_fill0) { navCtrl.navigate("AlwaysOnVpn") } } if((VERSION.SDK_INT>=33&&isProfileOwner(dpm))||(VERSION.SDK_INT>=30&&isDeviceOwner(dpm))) { SubPageItem(R.string.ucd, "", R.drawable.do_not_touch_fill0) { navCtrl.navigate("UserControlDisabled") } @@ -318,6 +300,50 @@ private fun Home( } } +@SuppressLint("NewApi") +@Composable +fun AlwaysOnVPNPackage(pkgName: String) { + val context = LocalContext.current + val dpm = context.getSystemService(ComponentActivity.DEVICE_POLICY_SERVICE) as DevicePolicyManager + val receiver = ComponentName(context,Receiver::class.java) + var lockdown by remember { mutableStateOf(false) } + var pkg by remember { mutableStateOf("") } + val refresh = { pkg = dpm.getAlwaysOnVpnPackage(receiver) } + LaunchedEffect(Unit) { refresh() } + val setAlwaysOnVpn: (String?, Boolean)->Unit = { vpnPkg: String?, lockdownEnabled: Boolean -> + try { + dpm.setAlwaysOnVpnPackage(receiver, vpnPkg, lockdownEnabled) + Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show() + } catch(e: UnsupportedOperationException) { + Toast.makeText(context, R.string.unsupported, Toast.LENGTH_SHORT).show() + } catch(e: NameNotFoundException) { + Toast.makeText(context, R.string.not_installed, Toast.LENGTH_SHORT).show() + } + } + Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())) { + Spacer(Modifier.padding(vertical = 10.dp)) + Text(text = stringResource(R.string.always_on_vpn), style = typography.headlineLarge, modifier = Modifier.padding(8.dp)) + Spacer(Modifier.padding(vertical = 5.dp)) + Text(text = stringResource(R.string.current_app_is) + pkg, modifier = Modifier.padding(8.dp)) + SwitchItem(R.string.enable_lockdown, "", null, { lockdown }, { lockdown = it }) + Spacer(Modifier.padding(vertical = 5.dp)) + Button( + onClick = { setAlwaysOnVpn(pkgName, lockdown); refresh() }, + modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp) + ) { + Text(stringResource(R.string.apply)) + } + Spacer(Modifier.padding(vertical = 5.dp)) + Button( + onClick = { setAlwaysOnVpn(null, false); refresh() }, + modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp) + ) { + Text(stringResource(R.string.clear_current_config)) + } + Spacer(Modifier.padding(vertical = 30.dp)) + } +} + @SuppressLint("NewApi") @Composable private fun UserCtrlDisabledPkg(pkgName:String) { diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index a93314c..0c93aef 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -286,6 +286,9 @@ Gizle Mevcut olmayan uygulamalar gizlidir Her zaman açık VPN + Enable lockdown + Current app: + Clear current config İzin Kapsam: iş profili Uygulama bilgisi diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 14ecaeb..bd14fee 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -281,6 +281,9 @@ 隐藏 如果隐藏,有可能是没安装 VPN保持打开 + 启用锁定 + 当前应用: + 清除当前配置 权限 作用域: 工作资料 应用详情 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8412fef..ab75ac8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -295,6 +295,9 @@ Hide Non-existent apps is hidden Always-on VPN + Enable lockdown + Current app: + Clear current config Permission Scope: work profile App info From 168ed085c403026a636aab27833e2bfe26f590dd Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Sun, 7 Jul 2024 17:30:17 +0800 Subject: [PATCH 08/17] update dependencies, fix disable account management --- app/build.gradle.kts | 4 +--- app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt | 2 +- gradle/libs.versions.toml | 6 ++++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f53e97e..0ffd3f3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) + alias(libs.plugins.cc) } var keyPassword: String? = null @@ -56,9 +57,6 @@ android { compose = true aidl = true } - composeOptions { - kotlinCompilerExtensionVersion = "1.5.13" - } packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" 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 c6840b9..7b4c38f 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Permissions.kt @@ -120,7 +120,7 @@ private fun Home(localNavCtrl:NavHostController,listScrollState:ScrollState) { SubPageItem(R.string.enrollment_specific_id, "", R.drawable.id_card_fill0) { localNavCtrl.navigate("SpecificID") } } if(isDeviceOwner(dpm) || isProfileOwner(dpm)) { - SubPageItem(R.string.disable_account_management, "", R.drawable.account_circle_fill0) { localNavCtrl.navigate("NoManagementAccount") } + SubPageItem(R.string.disable_account_management, "", R.drawable.account_circle_fill0) { localNavCtrl.navigate("DisableAccountManagement") } } if(VERSION.SDK_INT >= 24 && (isDeviceOwner(dpm) || dpm.isOrgProfile(receiver))) { SubPageItem(R.string.device_owner_lock_screen_info, "", R.drawable.screen_lock_portrait_fill0) { localNavCtrl.navigate("LockScreenInfo") } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bc284af..01038b9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,7 @@ [versions] -agp = "8.4.0" -kt-android = "1.9.23" +agp = "8.5.0" +kt-android = "2.0.0" +cc = "2.0.0" androidx-activity-compose = "1.8.2" navigation-compose = "2.7.7" @@ -27,3 +28,4 @@ androidx-fragment = { group = "androidx.fragment", name = "fragment", version.re [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kt-android" } +cc = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "cc" } From 7156e1ebeb7e1a7c99ea6ca8656ae3c6b13fe1cb Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Tue, 16 Jul 2024 16:56:17 +0800 Subject: [PATCH 09/17] fix unable to use App installer close #47 --- .../main/java/com/bintianqi/owndroid/InstallAppActivity.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/bintianqi/owndroid/InstallAppActivity.kt b/app/src/main/java/com/bintianqi/owndroid/InstallAppActivity.kt index a8af9e2..9e9a8d8 100644 --- a/app/src/main/java/com/bintianqi/owndroid/InstallAppActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/InstallAppActivity.kt @@ -115,7 +115,10 @@ class InstallAppActivity: FragmentActivity() { } val installDone by installAppDone.collectAsState() LaunchedEffect(installDone) { - if(installDone) finish() + if(installDone) { + installAppDone.value = false + finish() + } } } } From 97cd7447e4c61f08cd7b19fd25bc5978cea1bb42 Mon Sep 17 00:00:00 2001 From: BinTianqi Date: Tue, 16 Jul 2024 21:15:12 +0800 Subject: [PATCH 10/17] update Automation API --- app/src/main/AndroidManifest.xml | 5 +++-- .../com/bintianqi/owndroid/AutomationActivity.kt | 12 +++++++----- .../com/bintianqi/owndroid/AutomationReceiver.kt | 6 +++++- app/src/main/java/com/bintianqi/owndroid/Setting.kt | 8 ++++---- app/src/main/res/values-zh-rCN/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- app/src/main/res/values/themes.xml | 2 +- 7 files changed, 22 insertions(+), 15 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ee5b5f3..3da0824 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -55,8 +55,9 @@ android:name=".AutomationActivity" android:exported="true" android:launchMode="singleInstance" + android:excludeFromRecents="true" android:windowSoftInputMode="adjustResize|stateHidden" - android:theme="@style/Theme.OwnDroid"> + android:theme="@style/Theme.Transparent"> @@ -67,7 +68,7 @@ android:windowSoftInputMode="adjustResize|stateHidden" android:excludeFromRecents="true" android:launchMode="singleInstance" - android:theme="@style/Theme.OwnDroidAppInstaller"> + android:theme="@style/Theme.Transparent"> diff --git a/app/src/main/java/com/bintianqi/owndroid/AutomationActivity.kt b/app/src/main/java/com/bintianqi/owndroid/AutomationActivity.kt index 464f93c..2245e64 100644 --- a/app/src/main/java/com/bintianqi/owndroid/AutomationActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/AutomationActivity.kt @@ -1,11 +1,11 @@ package com.bintianqi.owndroid +import android.app.AlertDialog import android.content.Context import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.material3.Text +import androidx.compose.ui.platform.LocalContext class AutomationActivity: ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { @@ -14,9 +14,11 @@ class AutomationActivity: ComponentActivity() { val sharedPrefs = applicationContext.getSharedPreferences("data", Context.MODE_PRIVATE) if(sharedPrefs.getBoolean("automation_debug", false)) { setContent { - SelectionContainer { - Text(result) - } + AlertDialog.Builder(LocalContext.current) + .setMessage(result) + .setOnDismissListener { finish() } + .setPositiveButton(R.string.confirm) { _, _ -> finish() } + .show() } } else { finish() diff --git a/app/src/main/java/com/bintianqi/owndroid/AutomationReceiver.kt b/app/src/main/java/com/bintianqi/owndroid/AutomationReceiver.kt index a360199..ee32652 100644 --- a/app/src/main/java/com/bintianqi/owndroid/AutomationReceiver.kt +++ b/app/src/main/java/com/bintianqi/owndroid/AutomationReceiver.kt @@ -1,11 +1,11 @@ package com.bintianqi.owndroid +import android.annotation.SuppressLint import android.app.admin.DevicePolicyManager import android.content.BroadcastReceiver import android.content.ComponentName import android.content.Context import android.content.Intent -import android.util.Log import androidx.activity.ComponentActivity class AutomationReceiver: BroadcastReceiver() { @@ -14,6 +14,7 @@ class AutomationReceiver: BroadcastReceiver() { } } +@SuppressLint("NewApi") fun handleTask(context: Context, intent: Intent): String { val sharedPrefs = context.getSharedPreferences("data", Context.MODE_PRIVATE) val key = sharedPrefs.getString("automation_key", "") ?: "" @@ -27,6 +28,7 @@ fun handleTask(context: Context, intent: Intent): String { val dpm = context.getSystemService(ComponentActivity.DEVICE_POLICY_SERVICE) as DevicePolicyManager val receiver = ComponentName(context,Receiver::class.java) val app = intent.getStringExtra("app") + val restriction = intent.getStringExtra("restriction") try { when(operation) { "suspend" -> dpm.setPackagesSuspended(receiver, arrayOf(app), true) @@ -35,6 +37,8 @@ fun handleTask(context: Context, intent: Intent): String { "unhide" -> dpm.setApplicationHidden(receiver, app, false) "lock" -> dpm.lockNow() "reboot" -> dpm.reboot(receiver) + "addUserRestriction" -> dpm.addUserRestriction(receiver, restriction) + "clearUserRestriction" -> dpm.clearUserRestriction(receiver, restriction) else -> return "Operation not defined" } } catch(e: Exception) { diff --git a/app/src/main/java/com/bintianqi/owndroid/Setting.kt b/app/src/main/java/com/bintianqi/owndroid/Setting.kt index 92f026e..e0ed1bc 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Setting.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Setting.kt @@ -57,7 +57,7 @@ private fun Home(navCtrl: NavHostController) { Column(modifier = Modifier.fillMaxSize()) { SubPageItem(R.string.theme, "", R.drawable.format_paint_fill0) { navCtrl.navigate("Theme") } SubPageItem(R.string.security, "", R.drawable.lock_fill0) { navCtrl.navigate("Auth") } - SubPageItem(R.string.automation, "", R.drawable.apps_fill0) { navCtrl.navigate("Automation") } + SubPageItem(R.string.automation_api, "", R.drawable.apps_fill0) { navCtrl.navigate("Automation") } SubPageItem(R.string.about, "", R.drawable.info_fill0) { navCtrl.navigate("About") } } } @@ -132,10 +132,10 @@ private fun Automation() { val context = LocalContext.current val sharedPref = context.getSharedPreferences("data", Context.MODE_PRIVATE) Column(modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp).verticalScroll(rememberScrollState())) { + Spacer(Modifier.padding(vertical = 10.dp)) + Text(text = stringResource(R.string.automation_api), style = typography.headlineLarge) + Spacer(Modifier.padding(vertical = 5.dp)) var key by remember { mutableStateOf("") } - LaunchedEffect(Unit) { - key = sharedPref.getString("automation_key", "")?: "" - } TextField( value = key, onValueChange = { key = it }, label = { Text("Key")}, modifier = Modifier.fillMaxWidth() diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index d9a0639..19c025d 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -536,7 +536,7 @@ 清除存储空间 清除存储空间成功\n应用即将退出 - 自动化 + 自动化API 调试模式 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9ff1e1a..b6573d3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -552,7 +552,7 @@ Clear storage Clear storage success\nApplication will exit - Automation + Automation API Debug mode diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 83f2a43..7ed11ac 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -4,7 +4,7 @@ #FFFFFF #FFFFFF -