diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 06b7c4e..5eb926a 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -72,12 +72,6 @@ jobs:
with:
path: artifacts
- - name: Download telegram-bot-api
- run: |
- mkdir ./binaries
- wget "https://github.com/jakbin/telegram-bot-api-binary/releases/download/latest/telegram-bot-api" -O ./binaries/telegram-bot-api
- chmod +x ./binaries/telegram-bot-api
-
- name: Start API Server & Upload
env:
COMMIT_MESSAGE: |+
@@ -91,8 +85,7 @@ jobs:
mv ./$RELEASE_TEST_PWD/app-release.apk ./$RELEASE_TEST_PWD.apk && rm -rf ./$RELEASE_TEST_PWD
export RELEASE_SIGNED_PWD=$(find . -name "*release-signed*")
mv ./$RELEASE_SIGNED_PWD/app-release.apk ./$RELEASE_SIGNED_PWD.apk && rm -rf ./$RELEASE_SIGNED_PWD
- ../binaries/telegram-bot-api --api-id=${{ secrets.TELEGRAM_API_APP_ID }} --api-hash=${{ secrets.TELEGRAM_API_HASH }} --local 2>&1 > /dev/null &
export token=${{ secrets.TELEGRAM_BOT_KEY }}
- curl -v "http://127.0.0.1:8081/bot$token/sendMediaGroup?chat_id=-1002203528169&media=%5B%7B%22type%22%3A%22document%22%2C%22media%22%3A%22attach%3A%2F%2FreleaseTest%22%7D%2C%7B%22type%22%3A%22document%22%2C%22media%22%3A%22attach%3A%2F%2FreleaseSigned%22%2C%22parse_mode%22%3A%22HTML%22%2C%22caption%22%3A${ESCAPED}%7D%5D" \
+ curl -v "http://api.telegram.org/bot$token/sendMediaGroup?chat_id=-1002203528169&media=%5B%7B%22type%22%3A%22document%22%2C%22media%22%3A%22attach%3A%2F%2FreleaseTest%22%7D%2C%7B%22type%22%3A%22document%22%2C%22media%22%3A%22attach%3A%2F%2FreleaseSigned%22%2C%22parse_mode%22%3A%22HTML%22%2C%22caption%22%3A${ESCAPED}%7D%5D" \
-F releaseTest="@$RELEASE_TEST_PWD.apk" \
-F releaseSigned="@$RELEASE_SIGNED_PWD.apk"
diff --git a/Readme-zh_CN.md b/Readme-zh_CN.md
index 01e7af0..349d306 100644
--- a/Readme-zh_CN.md
+++ b/Readme-zh_CN.md
@@ -57,7 +57,7 @@ java.lang.IllegalStateException: Not allowed to set the device owner because the
> [!NOTE]
> 一些系统有应用克隆、儿童空间等功能,它们通常是用户。
-#### Device owner 已存在
+### Device owner 已存在
```text
java.lang.IllegalStateException: Trying to set the device owner (com.bintianqi.owndroid/.Receiver), but device owner (xxx) is already set.
@@ -142,7 +142,7 @@ context.sendBroadcast(intent)
[License.md](LICENSE.md)
-> Copyright (C) 2024 BinTianqi
+> Copyright (C) 2026 BinTianqi
>
> This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
>
diff --git a/Readme.md b/Readme.md
index 2a2b9a0..a462f4d 100644
--- a/Readme.md
+++ b/Readme.md
@@ -57,7 +57,7 @@ Solutions:
> [!NOTE]
> Some systems have features such as app cloning and children space, which are usually users.
-#### Device owner is already set
+### Device owner is already set
```text
java.lang.IllegalStateException: Trying to set the device owner (com.bintianqi.owndroid/.Receiver), but device owner (xxx) is already set.
@@ -144,7 +144,7 @@ You can use Gradle in command line to build OwnDroid.
[License.md](LICENSE.md)
-> Copyright (C) 2024 BinTianqi
+> Copyright (C) 2026 BinTianqi
>
> This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
>
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 84c2009..fa9225d 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -24,10 +24,10 @@ android {
defaultConfig {
applicationId = "com.bintianqi.owndroid"
- minSdk = 21
+ minSdk = 23
targetSdk = 36
- versionCode = 41
- versionName = "7.2"
+ versionCode = 42
+ versionName = "7.3"
multiDexEnabled = false
}
@@ -103,6 +103,7 @@ dependencies {
implementation(libs.androidx.fragment)
implementation(libs.hiddenApiBypass)
implementation(libs.libsu)
+ implementation(libs.reoderable)
implementation(libs.serialization)
implementation(kotlin("reflect"))
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 506c110..e328d50 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -12,6 +12,8 @@
+
+
+ android:exported="false">
@@ -99,6 +101,12 @@
android:name=".ApiReceiver"
android:exported="true">
+
+
+
()
vm.initialize(intent)
+ vm.registerInstallerReceiver(this)
val theme = ThemeSettings(SP.materialYou, SP.darkTheme, SP.blackTheme)
setContent {
OwnDroidTheme(theme) {
diff --git a/app/src/main/java/com/bintianqi/owndroid/AppInstallerViewModel.kt b/app/src/main/java/com/bintianqi/owndroid/AppInstallerViewModel.kt
index db89514..75cc03c 100644
--- a/app/src/main/java/com/bintianqi/owndroid/AppInstallerViewModel.kt
+++ b/app/src/main/java/com/bintianqi/owndroid/AppInstallerViewModel.kt
@@ -36,13 +36,19 @@ class AppInstallerViewModel(application: Application): AndroidViewModel(applicat
intent.getParcelableExtra(Intent.EXTRA_STREAM)?.let { list += it }
intent.getParcelableArrayExtra(Intent.EXTRA_STREAM)?.forEach { list += it as Uri }
intent.clipData?.let { clipData ->
- for(i in 0..clipData.itemCount - 1) {
+ for(i in 0..) {
uiState.update {
it.copy(packages = it.packages.plus(packages).distinct())
@@ -93,17 +99,14 @@ class AppInstallerViewModel(application: Application): AndroidViewModel(applicat
uiState.update { it.copy(installing = false, packageWriting = -1) }
return
}
- ContextCompat.registerReceiver(
- application, Receiver(), IntentFilter(ACTION), null,
- null, ContextCompat.RECEIVER_EXPORTED
- )
+ val intent = Intent(ACTION).setPackage(application.packageName)
val pi = if(Build.VERSION.SDK_INT >= 34) {
PendingIntent.getBroadcast(
- application, sessionId, Intent(ACTION),
+ application, sessionId, intent,
PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT or PendingIntent.FLAG_MUTABLE
).intentSender
} else {
- PendingIntent.getBroadcast(application, sessionId, Intent(ACTION), PendingIntent.FLAG_MUTABLE).intentSender
+ PendingIntent.getBroadcast(application, sessionId, intent, PendingIntent.FLAG_MUTABLE).intentSender
}
session.commit(pi)
}
@@ -119,7 +122,6 @@ class AppInstallerViewModel(application: Application): AndroidViewModel(applicat
)
} else {
uiState.update { it.copy(result = intent) }
- context.unregisterReceiver(this)
}
}
}
diff --git a/app/src/main/java/com/bintianqi/owndroid/AppLock.kt b/app/src/main/java/com/bintianqi/owndroid/AppLock.kt
index 820c554..98535c9 100644
--- a/app/src/main/java/com/bintianqi/owndroid/AppLock.kt
+++ b/app/src/main/java/com/bintianqi/owndroid/AppLock.kt
@@ -3,6 +3,7 @@ package com.bintianqi.owndroid
import android.content.Context
import android.hardware.biometrics.BiometricPrompt
import android.hardware.biometrics.BiometricPrompt.AuthenticationCallback
+import android.hardware.fingerprint.FingerprintManager
import android.os.Build
import android.os.CancellationSignal
import androidx.activity.compose.BackHandler
@@ -19,6 +20,7 @@ import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -39,6 +41,8 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
@@ -50,6 +54,7 @@ fun AppLockDialog(onSucceed: () -> Unit, onDismiss: () -> Unit) = Dialog(onDismi
val fr = remember { FocusRequester() }
var input by rememberSaveable { mutableStateOf("") }
var isError by rememberSaveable { mutableStateOf(false) }
+ var showPassword by remember { mutableStateOf(false) }
fun unlock() {
if(input.hash() == SP.lockPasswordHash) {
fm.clearFocus()
@@ -75,7 +80,18 @@ fun AppLockDialog(onSucceed: () -> Unit, onDismiss: () -> Unit) = Dialog(onDismi
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password, imeAction = if(input.length >= 4) ImeAction.Go else ImeAction.Done
),
- keyboardActions = KeyboardActions({ fm.clearFocus() }, { unlock() })
+ keyboardActions = KeyboardActions({ fm.clearFocus() }, { unlock() }),
+ visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(),
+ trailingIcon = {
+ IconButton(onClick = { showPassword = !showPassword }) {
+ Icon(
+ painter = painterResource(
+ id = if (showPassword) R.drawable.visibility_fill0 else R.drawable.visibility_off_fill0
+ ),
+ contentDescription = if (showPassword) "Hide password" else "Show password"
+ )
+ }
+ }
)
if(Build.VERSION.SDK_INT >= 28 && SP.biometricsUnlock) {
FilledTonalIconButton({ startBiometricsUnlock(context, onSucceed) }, Modifier.padding(start = 4.dp)) {
@@ -83,7 +99,7 @@ fun AppLockDialog(onSucceed: () -> Unit, onDismiss: () -> Unit) = Dialog(onDismi
}
}
}
- Button(::unlock, Modifier.align(Alignment.End).padding(top = 8.dp), input.length >= 4) {
+ Button(::unlock, Modifier.align(Alignment.End).padding(top = 8.dp)) {
Text(stringResource(R.string.unlock))
}
}
@@ -92,6 +108,7 @@ fun AppLockDialog(onSucceed: () -> Unit, onDismiss: () -> Unit) = Dialog(onDismi
@RequiresApi(28)
fun startBiometricsUnlock(context: Context, onSucceed: () -> Unit) {
+ context.getSystemService(FingerprintManager::class.java) ?: return
val callback = object : AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult?) {
super.onAuthenticationSucceeded(result)
diff --git a/app/src/main/java/com/bintianqi/owndroid/LockTaskService.kt b/app/src/main/java/com/bintianqi/owndroid/LockTaskService.kt
new file mode 100644
index 0000000..301117d
--- /dev/null
+++ b/app/src/main/java/com/bintianqi/owndroid/LockTaskService.kt
@@ -0,0 +1,84 @@
+package com.bintianqi.owndroid
+
+import android.app.ActivityManager
+import android.app.PendingIntent
+import android.app.Service
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.ServiceInfo
+import android.os.Build
+import android.os.IBinder
+import androidx.annotation.RequiresApi
+import androidx.core.app.NotificationCompat
+import androidx.core.app.ServiceCompat
+import androidx.core.content.ContextCompat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+
+@RequiresApi(28)
+class LockTaskService: Service() {
+ val coroutineScope = CoroutineScope(Dispatchers.IO)
+
+ override fun onBind(intent: Intent?): IBinder? = null
+
+ val stopReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ coroutineScope.cancel()
+ stopLockTask()
+ stop()
+ }
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ super.onStartCommand(intent, flags, startId)
+ val filter = IntentFilter(STOP_ACTION)
+ ContextCompat.registerReceiver(
+ this, stopReceiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED
+ )
+ val pendingIntent = PendingIntent.getBroadcast(
+ this, 0, Intent(STOP_ACTION).setPackage(this.packageName), PendingIntent.FLAG_IMMUTABLE
+ )
+ val notification = NotificationCompat.Builder(this, MyNotificationChannel.LockTaskMode.id)
+ .setContentTitle(getText(R.string.lock_task_mode))
+ .setSmallIcon(R.drawable.lock_fill0)
+ .addAction(NotificationCompat.Action.Builder(null, getString(R.string.stop), pendingIntent).build())
+ .setOngoing(true)
+ .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
+ .build()
+ ServiceCompat.startForeground(
+ this, NotificationType.LockTaskMode.id, notification,
+ if (Build.VERSION.SDK_INT < 34) 0 else ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST
+ )
+ coroutineScope.launch {
+ val am = getSystemService(ActivityManager::class.java)
+ delay(3000)
+ while (am.lockTaskModeState == ActivityManager.LOCK_TASK_MODE_LOCKED) {
+ delay(1000)
+ }
+ stop()
+ }
+ return START_NOT_STICKY
+ }
+
+ fun stop() {
+ unregisterReceiver(stopReceiver)
+ stopSelf()
+ }
+
+ fun stopLockTask() {
+ val features = Privilege.DPM.getLockTaskFeatures(Privilege.DAR)
+ val packages = Privilege.DPM.getLockTaskPackages(Privilege.DAR)
+ Privilege.DPM.setLockTaskPackages(Privilege.DAR, arrayOf())
+ Privilege.DPM.setLockTaskPackages(Privilege.DAR, packages)
+ Privilege.DPM.setLockTaskFeatures(Privilege.DAR, features)
+ }
+
+ companion object {
+ const val STOP_ACTION = "com.bintianqi.owndroid.action.STOP_LOCK_TASK_MODE"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt
index 446d856..5aedebe 100644
--- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt
+++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt
@@ -137,6 +137,8 @@ import com.bintianqi.owndroid.dpm.LockTaskMode
import com.bintianqi.owndroid.dpm.LockTaskModeScreen
import com.bintianqi.owndroid.dpm.ManageAppGroups
import com.bintianqi.owndroid.dpm.ManageAppGroupsScreen
+import com.bintianqi.owndroid.dpm.ManagedConfiguration
+import com.bintianqi.owndroid.dpm.ManagedConfigurationScreen
import com.bintianqi.owndroid.dpm.MtePolicy
import com.bintianqi.owndroid.dpm.MtePolicyScreen
import com.bintianqi.owndroid.dpm.NearbyStreamingPolicy
@@ -155,7 +157,6 @@ import com.bintianqi.owndroid.dpm.OrganizationOwnedProfileScreen
import com.bintianqi.owndroid.dpm.OverrideApn
import com.bintianqi.owndroid.dpm.OverrideApnScreen
import com.bintianqi.owndroid.dpm.PackageFunctionScreen
-import com.bintianqi.owndroid.dpm.PackageFunctionScreenWithoutResult
import com.bintianqi.owndroid.dpm.Password
import com.bintianqi.owndroid.dpm.PasswordInfo
import com.bintianqi.owndroid.dpm.PasswordInfoScreen
@@ -253,6 +254,9 @@ class MainActivity : FragmentActivity() {
val launcher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {}
launcher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
+ registerPackageRemovedReceiver(this) {
+ vm.onPackageRemoved(it)
+ }
setContent {
var appLockDialog by rememberSaveable { mutableStateOf(false) }
val theme by vm.theme.collectAsStateWithLifecycle()
@@ -265,10 +269,6 @@ class MainActivity : FragmentActivity() {
}
}
- override fun onResume() {
- super.onResume()
- }
-
}
@ExperimentalMaterial3Api
@@ -285,7 +285,10 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
}
}
fun choosePackage() {
- navController.navigate(ApplicationsList(false))
+ navController.navigate(ApplicationsList(false, true))
+ }
+ fun chooseSinglePackage() {
+ navController.navigate(ApplicationsList(false, false))
}
fun navigateToAppGroups() {
navController.navigate(ManageAppGroups)
@@ -331,7 +334,7 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
DelegatedAdminsScreen(vm.delegatedAdmins, vm::getDelegatedAdmins, ::navigateUp, ::navigate)
}
composable{
- AddDelegatedAdminScreen(vm.chosenPackage, ::choosePackage, it.toRoute(),
+ AddDelegatedAdminScreen(vm.chosenPackage, ::chooseSinglePackage, it.toRoute(),
vm::setDelegatedAdmin, ::navigateUp)
}
composable { DeviceInfoScreen(vm, ::navigateUp) }
@@ -385,9 +388,11 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
vm::getNsNotificationPolicy, vm::setNsNotificationPolicy, ::navigateUp)
}
composable {
- LockTaskModeScreen(vm.chosenPackage, ::choosePackage, vm.lockTaskPackages,
+ LockTaskModeScreen(
+ vm.chosenPackage, ::chooseSinglePackage, ::choosePackage, vm.lockTaskPackages,
vm::getLockTaskPackages, vm::setLockTaskPackage, vm::startLockTaskMode,
- vm:: getLockTaskFeatures, vm::setLockTaskFeatures, ::navigateUp)
+ vm:: getLockTaskFeatures, vm::setLockTaskFeatures, ::navigateUp
+ )
}
composable {
CaCertScreen(vm.installedCaCerts, vm::getCaCerts, vm.selectedCaCert, vm::selectCaCert, vm::installCaCert, vm::parseCaCert,
@@ -435,7 +440,7 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
WifiSsidPolicyScreen(vm::getSsidPolicy, vm::setSsidPolicy, ::navigateUp)
}
composable {
- NetworkStatsScreen(vm.chosenPackage, ::choosePackage, vm::getPackageUid,
+ NetworkStatsScreen(vm.chosenPackage, ::chooseSinglePackage, vm::getPackageUid,
vm::queryNetworkStats, ::navigateUp) { navController.navigate(NetworkStatsViewer) }
}
composable {
@@ -446,7 +451,7 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
}
composable {
AlwaysOnVpnPackageScreen(vm::getAlwaysOnVpnPackage, vm::getAlwaysOnVpnLockdown,
- vm::setAlwaysOnVpn, vm.chosenPackage, ::choosePackage, ::navigateUp)
+ vm::setAlwaysOnVpn, vm.chosenPackage, ::chooseSinglePackage, ::navigateUp)
}
composable {
RecommendedGlobalProxyScreen(vm::setRecommendedGlobalProxy, ::navigateUp)
@@ -494,10 +499,10 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
composable { DeleteWorkProfileScreen(vm::wipeData, ::navigateUp) }
composable {
- val canSwitchView = (it.toRoute() as ApplicationsList).canSwitchView
+ val params = it.toRoute()
AppChooserScreen(
- canSwitchView, vm.installedPackages, vm.refreshPackagesProgress, { name ->
- if (canSwitchView) {
+ params, vm.installedPackages, vm.refreshPackagesProgress, { name ->
+ if (params.canSwitchView) {
if (name == null) {
navigateUp()
} else {
@@ -512,12 +517,12 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
navController.navigate(ApplicationsFeatures) {
popUpTo(Home)
}
- }, vm::refreshPackageList)
+ }, vm::refreshPackageList, vm::setPackageSuspended, vm::setPackageHidden)
}
composable {
ApplicationsFeaturesScreen(::navigateUp, ::navigate) {
SP.applicationsListView = true
- navController.navigate(ApplicationsList(true)) {
+ navController.navigate(ApplicationsList(true, true)) {
popUpTo(Home)
}
}
@@ -526,52 +531,72 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
ApplicationDetailsScreen(it.toRoute(), vm, ::navigateUp, ::navigate)
}
composable {
- PackageFunctionScreen(R.string.suspend, vm.suspendedPackages, vm::getSuspendedPackaged,
+ PackageFunctionScreen(
+ R.string.suspend, vm.suspendedPackages, vm::getSuspendedPackaged,
vm::setPackageSuspended, ::navigateUp, vm.chosenPackage, ::choosePackage,
- ::navigateToAppGroups, vm.appGroups, R.string.info_suspend_app)
+ ::navigateToAppGroups, vm.appGroups, R.string.info_suspend_app
+ )
}
composable {
- PackageFunctionScreen(R.string.hide, vm.hiddenPackages, vm::getHiddenPackages,
- vm::setPackageHidden, ::navigateUp, vm.chosenPackage, ::choosePackage, ::navigateToAppGroups, vm.appGroups)
+ PackageFunctionScreen(
+ R.string.hide, vm.hiddenPackages, vm::getHiddenPackages, vm::setPackageHidden,
+ ::navigateUp, vm.chosenPackage, ::choosePackage, ::navigateToAppGroups, vm.appGroups
+ )
}
composable {
- PackageFunctionScreenWithoutResult(R.string.block_uninstall, vm.ubPackages,
- vm::getUbPackages, vm::setPackageUb, ::navigateUp, vm.chosenPackage, ::choosePackage, ::navigateToAppGroups, vm.appGroups)
+ PackageFunctionScreen(
+ R.string.block_uninstall, vm.ubPackages, vm::getUbPackages, vm::setPackageUb,
+ ::navigateUp, vm.chosenPackage, ::choosePackage, ::navigateToAppGroups, vm.appGroups
+ )
}
composable {
- PackageFunctionScreenWithoutResult(R.string.disable_user_control, vm.ucdPackages,
- vm::getUcdPackages, vm::setPackageUcd, ::navigateUp, vm.chosenPackage,
- ::choosePackage, ::navigateToAppGroups, vm.appGroups, R.string.info_disable_user_control)
+ PackageFunctionScreen(
+ R.string.disable_user_control, vm.ucdPackages, vm::getUcdPackages,
+ vm::setPackageUcd, ::navigateUp, vm.chosenPackage, ::choosePackage,
+ ::navigateToAppGroups, vm.appGroups, R.string.info_disable_user_control
+ )
}
composable {
- PermissionsManagerScreen(vm.packagePermissions, vm::getPackagePermissions,
- vm::setPackagePermission, ::navigateUp, it.toRoute(), vm.chosenPackage, ::choosePackage)
+ PermissionsManagerScreen(
+ vm.packagePermissions, vm::getPackagePermissions, vm::setPackagePermission,
+ ::navigateUp, it.toRoute(), vm.chosenPackage, ::chooseSinglePackage
+ )
}
composable {
- PackageFunctionScreen(R.string.disable_metered_data, vm.mddPackages,
- vm::getMddPackages, vm::setPackageMdd, ::navigateUp, vm.chosenPackage,
- ::choosePackage, ::navigateToAppGroups, vm.appGroups)
+ PackageFunctionScreen(
+ R.string.disable_metered_data, vm.mddPackages, vm::getMddPackages,
+ vm::setPackageMdd, ::navigateUp, vm.chosenPackage, ::choosePackage,
+ ::navigateToAppGroups, vm.appGroups
+ )
}
composable {
- ClearAppStorageScreen(vm.chosenPackage, ::choosePackage, vm::clearAppData, ::navigateUp)
+ ClearAppStorageScreen(
+ vm.chosenPackage, ::chooseSinglePackage, vm::clearAppData, ::navigateUp
+ )
}
composable {
- UninstallAppScreen(vm.chosenPackage, ::choosePackage, vm::uninstallPackage, ::navigateUp)
+ UninstallAppScreen(
+ vm.chosenPackage, ::chooseSinglePackage, vm::uninstallPackage, ::navigateUp
+ )
}
composable {
- PackageFunctionScreenWithoutResult(R.string.keep_uninstalled_packages, vm.kuPackages,
- vm::getKuPackages, vm::setPackageKu, ::navigateUp, vm.chosenPackage,
- ::choosePackage, ::navigateToAppGroups, vm.appGroups,
- R.string.info_keep_uninstalled_apps)
+ PackageFunctionScreen(
+ R.string.keep_uninstalled_packages, vm.kuPackages, vm::getKuPackages,
+ vm::setPackageKu, ::navigateUp, vm.chosenPackage, ::choosePackage,
+ ::navigateToAppGroups, vm.appGroups, R.string.info_keep_uninstalled_apps
+ )
}
composable {
- InstallExistingAppScreen(vm.chosenPackage, ::choosePackage,
- vm::installExistingApp, ::navigateUp)
+ InstallExistingAppScreen(
+ vm.chosenPackage, ::chooseSinglePackage, vm::installExistingApp, ::navigateUp
+ )
}
composable {
- PackageFunctionScreenWithoutResult(R.string.cross_profile_apps, vm.cpPackages,
+ PackageFunctionScreen(
+ R.string.cross_profile_apps, vm.cpPackages,
vm::getCpPackages, vm::setPackageCp, ::navigateUp, vm.chosenPackage,
- ::choosePackage, ::navigateToAppGroups, vm.appGroups)
+ ::choosePackage, ::navigateToAppGroups, vm.appGroups
+ )
}
composable {
PackageFunctionScreen(R.string.cross_profile_widget, vm.cpwProviders,
@@ -579,28 +604,45 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
::choosePackage, ::navigateToAppGroups, vm.appGroups)
}
composable {
- CredentialManagerPolicyScreen(vm.chosenPackage, ::choosePackage,
- vm.cmPackages, vm::getCmPolicy, vm::setCmPackage, vm::setCmPolicy, ::navigateUp)
+ CredentialManagerPolicyScreen(
+ vm.chosenPackage, ::choosePackage, vm.cmPackages, vm::getCmPolicy,
+ vm::setCmPackage, vm::setCmPolicy, ::navigateUp
+ )
}
composable {
- PermittedAsAndImPackages(R.string.permitted_accessibility_services,
+ PermittedAsAndImPackages(
+ R.string.permitted_accessibility_services,
R.string.system_accessibility_always_allowed, vm.chosenPackage, ::choosePackage,
- vm.pasPackages, vm::getPasPackages, vm::setPasPackage, vm::setPasPolicy, ::navigateUp)
+ vm.pasPackages, vm::getPasPackages, vm::setPasPackage, vm::setPasPolicy,
+ ::navigateUp
+ )
}
composable {
- PermittedAsAndImPackages(R.string.permitted_ime, R.string.system_ime_always_allowed,
+ PermittedAsAndImPackages(
+ R.string.permitted_ime, R.string.system_ime_always_allowed,
vm.chosenPackage, ::choosePackage, vm.pimPackages, vm::getPimPackages,
- vm::setPimPackage, vm::setPimPolicy, ::navigateUp)
+ vm::setPimPackage, vm::setPimPolicy, ::navigateUp
+ )
}
composable {
- EnableSystemAppScreen(vm.chosenPackage, ::choosePackage, vm::enableSystemApp, ::navigateUp)
+ EnableSystemAppScreen(
+ vm.chosenPackage, ::chooseSinglePackage, vm::enableSystemApp, ::navigateUp
+ )
}
composable {
- SetDefaultDialerScreen(vm.chosenPackage, ::choosePackage, vm::setDefaultDialer, ::navigateUp)
+ SetDefaultDialerScreen(
+ vm.chosenPackage, ::chooseSinglePackage, vm::setDefaultDialer, ::navigateUp
+ )
+ }
+ composable {
+ ManagedConfigurationScreen(
+ it.toRoute(), vm.appRestrictions, vm::setAppRestrictions,
+ vm::clearAppRestrictions, ::navigateUp
+ )
}
composable {
ManageAppGroupsScreen(
- vm.appGroups,
+ vm.appGroups, vm::exportAppGroups, vm::importAppGroups,
{ id, name, apps -> navController.navigate(EditAppGroup(id, name, apps)) },
::navigateUp
)
@@ -751,7 +793,10 @@ private fun HomeScreen(onNavigate: (Any) -> Unit) {
}
if(privilege.device || privilege.profile) {
HomePageItem(R.string.applications, R.drawable.apps_fill0) {
- onNavigate(if(SP.applicationsListView) ApplicationsList(true) else ApplicationsFeatures)
+ onNavigate(
+ if (SP.applicationsListView) ApplicationsList(true, true)
+ else ApplicationsFeatures
+ )
}
if(VERSION.SDK_INT >= 24) {
HomePageItem(R.string.user_restriction, R.drawable.person_off) { onNavigate(UserRestriction) }
diff --git a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt
index 654ee05..47b1b95 100644
--- a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt
+++ b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt
@@ -25,6 +25,8 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
+import android.content.RestrictionEntry
+import android.content.RestrictionsManager
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInstaller
import android.content.pm.PackageManager
@@ -39,6 +41,7 @@ import android.net.wifi.WifiManager
import android.net.wifi.WifiSsid
import android.os.Binder
import android.os.Build.VERSION
+import android.os.Bundle
import android.os.HardwarePropertiesManager
import android.os.UserHandle
import android.os.UserManager
@@ -60,7 +63,9 @@ import com.bintianqi.owndroid.dpm.ApnConfig
import com.bintianqi.owndroid.dpm.ApnMvnoType
import com.bintianqi.owndroid.dpm.ApnProtocol
import com.bintianqi.owndroid.dpm.AppGroup
+import com.bintianqi.owndroid.dpm.AppRestriction
import com.bintianqi.owndroid.dpm.AppStatus
+import com.bintianqi.owndroid.dpm.BasicAppGroup
import com.bintianqi.owndroid.dpm.CaCertInfo
import com.bintianqi.owndroid.dpm.CreateUserResult
import com.bintianqi.owndroid.dpm.CreateWorkProfileOptions
@@ -115,6 +120,10 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.addJsonObject
+import kotlinx.serialization.json.buildJsonArray
+import kotlinx.serialization.json.put
import java.net.InetAddress
import java.security.MessageDigest
import java.security.cert.CertificateException
@@ -154,10 +163,10 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
return AppLockConfig(passwordHash?.ifEmpty { null }, SP.biometricsUnlock, SP.lockWhenLeaving)
}
fun setAppLockConfig(config: AppLockConfig) {
- SP.lockPasswordHash = if (config.password == null) {
- ""
- } else {
- config.password.hash()
+ if (config.password == null) {
+ SP.lockPasswordHash = ""
+ } else if (!config.password.isEmpty()) {
+ SP.lockPasswordHash = config.password.hash()
}
SP.biometricsUnlock = config.biometrics
SP.lockWhenLeaving = config.whenLeaving
@@ -196,6 +205,11 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
}
}
}
+ fun onPackageRemoved(name: String) {
+ installedPackages.update { list ->
+ list.filter { it.name != name }
+ }
+ }
fun getAppInfo(info: ApplicationInfo) =
AppInfo(info.packageName, info.loadLabel(PM).toString(), info.loadIcon(PM), info.flags)
fun getAppInfo(name: String): AppInfo {
@@ -215,10 +229,9 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
suspendedPackages.value = packages.map { getAppInfo(it) }
}
@RequiresApi(24)
- fun setPackageSuspended(name: String, status: Boolean): Boolean {
- val result = DPM.setPackagesSuspended(DAR, arrayOf(name), status)
+ fun setPackageSuspended(packages: List, status: Boolean) {
+ DPM.setPackagesSuspended(DAR, packages.toTypedArray(), status)
getSuspendedPackaged()
- return result.isEmpty()
}
val hiddenPackages = MutableStateFlow(emptyList())
@@ -227,10 +240,11 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
DPM.isApplicationHidden(DAR, it.packageName)
}.map { getAppInfo(it) }
}
- fun setPackageHidden(name: String, status: Boolean): Boolean {
- val result = DPM.setApplicationHidden(DAR, name, status)
+ fun setPackageHidden(packages: List, status: Boolean) {
+ for (name in packages) {
+ DPM.setApplicationHidden(DAR, name, status)
+ }
getHiddenPackages()
- return result
}
// Uninstall blocked packages
@@ -240,8 +254,10 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
DPM.isUninstallBlocked(DAR, it.packageName)
}.map { getAppInfo(it) }
}
- fun setPackageUb(name: String, status: Boolean) {
- DPM.setUninstallBlocked(DAR, name, status)
+ fun setPackageUb(packages: List, status: Boolean) {
+ for (name in packages) {
+ DPM.setUninstallBlocked(DAR, name, status)
+ }
getUbPackages()
}
@@ -254,16 +270,17 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
}
}
@RequiresApi(30)
- fun setPackageUcd(name: String, status: Boolean) {
+ fun setPackageUcd(packages: List, status: Boolean) {
DPM.setUserControlDisabledPackages(
DAR,
- ucdPackages.value.map { it.name }.run { if (status) plus(name) else minus(name) }
+ ucdPackages.value.map { it.name }.run {
+ if (status) plus(packages) else minus(packages)
+ }
)
getUcdPackages()
}
val packagePermissions = MutableStateFlow(emptyMap())
- @RequiresApi(23)
fun getPackagePermissions(name: String) {
if (name.isValidPackageName) {
packagePermissions.value = runtimePermissions.associate {
@@ -273,7 +290,6 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
packagePermissions.value = emptyMap()
}
}
- @RequiresApi(23)
fun setPackagePermission(name: String, permission: String, status: Int): Boolean {
val result = DPM.setPermissionGrantState(DAR, name, permission, status)
getPackagePermissions(name)
@@ -287,12 +303,13 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
mddPackages.value = DPM.getMeteredDataDisabledPackages(DAR).distinct().map { getAppInfo(it) }
}
@RequiresApi(28)
- fun setPackageMdd(name: String, status: Boolean): Boolean {
- val result = DPM.setMeteredDataDisabledPackages(
- DAR, mddPackages.value.map { it.name }.run { if (status) plus(name) else minus(name) }
+ fun setPackageMdd(packages: List, status: Boolean) {
+ DPM.setMeteredDataDisabledPackages(
+ DAR, mddPackages.value.map { it.name }.run {
+ if (status) plus(packages) else minus(packages)
+ }
)
getMddPackages()
- return result.isEmpty()
}
// Keep uninstalled packages
@@ -302,9 +319,11 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
kuPackages.value = DPM.getKeepUninstalledPackages(DAR)?.distinct()?.map { getAppInfo(it) } ?: emptyList()
}
@RequiresApi(28)
- fun setPackageKu(name: String, status: Boolean) {
+ fun setPackageKu(packages: List, status: Boolean) {
DPM.setKeepUninstalledPackages(
- DAR, kuPackages.value.map { it.name }.run { if (status) plus(name) else minus(name) }
+ DAR, kuPackages.value.map { it.name }.run {
+ if (status) plus(packages) else minus(packages)
+ }
)
getKuPackages()
}
@@ -316,10 +335,12 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
cpPackages.value = DPM.getCrossProfilePackages(DAR).map { getAppInfo(it) }
}
@RequiresApi(30)
- fun setPackageCp(name: String, status: Boolean) {
+ fun setPackageCp(packages: List, status: Boolean) {
DPM.setCrossProfilePackages(
DAR,
- cpPackages.value.map { it.name }.toSet().run { if (status) plus(name) else minus(name) }
+ cpPackages.value.map { it.name }.toSet().run {
+ if (status) plus(packages) else minus(packages)
+ }
)
getCpPackages()
}
@@ -329,14 +350,15 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
fun getCpwProviders() {
cpwProviders.value = DPM.getCrossProfileWidgetProviders(DAR).distinct().map { getAppInfo(it) }
}
- fun setCpwProvider(name: String, status: Boolean): Boolean {
- val result = if (status) {
- DPM.addCrossProfileWidgetProvider(DAR, name)
- } else {
- DPM.removeCrossProfileWidgetProvider(DAR, name)
+ fun setCpwProvider(packages: List, status: Boolean) {
+ for (name in packages) {
+ if (status) {
+ DPM.addCrossProfileWidgetProvider(DAR, name)
+ } else {
+ DPM.removeCrossProfileWidgetProvider(DAR, name)
+ }
}
getCpwProviders()
- return result
}
@RequiresApi(28)
@@ -347,6 +369,7 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
}
fun uninstallPackage(packageName: String, onComplete: (String?) -> Unit) {
+ val action = "com.bintianqi.owndroid.action.PACKAGE_UNINSTALLED"
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val statusExtra = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 999)
@@ -355,7 +378,7 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
context.startActivity(intent.getParcelableExtra(Intent.EXTRA_INTENT) as Intent?)
} else {
context.unregisterReceiver(this)
- if(statusExtra == PackageInstaller.STATUS_SUCCESS) {
+ if (statusExtra == PackageInstaller.STATUS_SUCCESS) {
onComplete(null)
} else {
onComplete(parsePackageInstallerMessage(context, intent))
@@ -364,16 +387,17 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
}
}
ContextCompat.registerReceiver(
- application, receiver, IntentFilter(AppInstallerViewModel.ACTION), null,
- null, ContextCompat.RECEIVER_EXPORTED
+ application, receiver, IntentFilter(action), null,
+ null, ContextCompat.RECEIVER_NOT_EXPORTED
)
+ val intent = Intent(action).setPackage(application.packageName)
val pi = if(VERSION.SDK_INT >= 34) {
PendingIntent.getBroadcast(
- application, 0, Intent(AppInstallerViewModel.ACTION),
+ application, 0, intent,
PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT or PendingIntent.FLAG_MUTABLE
).intentSender
} else {
- PendingIntent.getBroadcast(application, 0, Intent(AppInstallerViewModel.ACTION), PendingIntent.FLAG_MUTABLE).intentSender
+ PendingIntent.getBroadcast(application, 0, intent, PendingIntent.FLAG_MUTABLE).intentSender
}
application.getPackageInstaller().uninstall(packageName, pi)
}
@@ -392,9 +416,9 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
policy.policyType
} ?: -1
}
- fun setCmPackage(name: String, status: Boolean) {
- cmPackages.update { list ->
- if (status) list + getAppInfo(name) else list.filter { it.name != name }
+ fun setCmPackage(packages: List, status: Boolean) {
+ cmPackages.update {
+ updateAppInfoList(it, packages, status)
}
}
@RequiresApi(34)
@@ -405,6 +429,16 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
getCmPolicy()
}
+ fun updateAppInfoList(
+ origin: List, input: List, status: Boolean
+ ): List {
+ return if (status) {
+ origin + input.map { getAppInfo(it) }
+ } else {
+ origin.filter { it.name !in input }
+ }
+ }
+
// Permitted input method
val pimPackages = MutableStateFlow(emptyList())
fun getPimPackages(): Boolean {
@@ -413,9 +447,9 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
packages == null
}
}
- fun setPimPackage(name: String, status: Boolean) {
- pimPackages.update { packages ->
- if (status) packages + getAppInfo(name) else packages.filter { it.name != name }
+ fun setPimPackage(packages: List, status: Boolean) {
+ pimPackages.update {
+ updateAppInfoList(it, packages, status)
}
}
fun setPimPolicy(allowAll: Boolean): Boolean {
@@ -433,9 +467,9 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
packages == null
}
}
- fun setPasPackage(name: String, status: Boolean) {
- pasPackages.update { packages ->
- if (status) packages + getAppInfo(name) else packages.filter { it.name != name }
+ fun setPasPackage(packages: List, status: Boolean) {
+ pasPackages.update {
+ updateAppInfoList(it, packages, status)
}
}
fun setPasPolicy(allowAll: Boolean): Boolean {
@@ -457,14 +491,18 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
DPM.isUninstallBlocked(DAR, name),
if (VERSION.SDK_INT >= 30) name in DPM.getUserControlDisabledPackages(DAR) else false,
if (VERSION.SDK_INT >= 28) name in DPM.getMeteredDataDisabledPackages(DAR) else false,
- if (VERSION.SDK_INT >= 28) DPM.getKeepUninstalledPackages(DAR)?.contains(name) == true else false
+ if (VERSION.SDK_INT >= 28 && Privilege.status.value.device)
+ DPM.getKeepUninstalledPackages(DAR)?.contains(name) == true
+ else false
)
}
// Application details
@RequiresApi(24)
fun adSetPackageSuspended(name: String, status: Boolean) {
- DPM.setPackagesSuspended(DAR, arrayOf(name), status)
- appStatus.update { it.copy(suspend = DPM.isPackageSuspended(DAR, name)) }
+ try {
+ DPM.setPackagesSuspended(DAR, arrayOf(name), status)
+ appStatus.update { it.copy(suspend = DPM.isPackageSuspended(DAR, name)) }
+ } catch (_: Exception) {}
}
fun adSetPackageHidden(name: String, status: Boolean) {
DPM.setApplicationHidden(DAR, name, status)
@@ -510,6 +548,79 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
}
}
+ val appRestrictions = MutableStateFlow(emptyList())
+
+ fun getAppRestrictions(name: String) {
+ val rm = application.getSystemService(RestrictionsManager::class.java)
+ try {
+ val bundle = DPM.getApplicationRestrictions(DAR, name)
+ appRestrictions.value = rm.getManifestRestrictions(name)?.mapNotNull {
+ transformRestrictionEntry(it)
+ }?.map {
+ if (bundle.containsKey(it.key)) {
+ when (it) {
+ is AppRestriction.BooleanItem -> it.value = bundle.getBoolean(it.key)
+ is AppRestriction.StringItem -> it.value = bundle.getString(it.key)
+ is AppRestriction.IntItem -> it.value = bundle.getInt(it.key)
+ is AppRestriction.ChoiceItem -> it.value = bundle.getString(it.key)
+ is AppRestriction.MultiSelectItem -> it.value = bundle.getStringArray(it.key)
+ }
+ }
+ it
+ } ?: emptyList()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ appRestrictions.value = emptyList()
+ }
+ }
+
+ fun setAppRestrictions(name: String, item: AppRestriction) {
+ viewModelScope.launch(Dispatchers.IO) {
+ val bundle = transformAppRestriction(
+ appRestrictions.value.filter { it.key != item.key }.plus(item)
+ )
+ DPM.setApplicationRestrictions(DAR, name, bundle)
+ getAppRestrictions(name)
+ }
+ }
+
+ fun clearAppRestrictions(name: String) {
+ viewModelScope.launch(Dispatchers.IO) {
+ DPM.setApplicationRestrictions(DAR, name, Bundle())
+ getAppRestrictions(name)
+ }
+ }
+
+ fun transformRestrictionEntry(e: RestrictionEntry): AppRestriction? {
+ return when (e.type) {
+ RestrictionEntry.TYPE_INTEGER ->
+ AppRestriction.IntItem(e.key, e.title, e.description, null)
+ RestrictionEntry.TYPE_STRING ->
+ AppRestriction.StringItem(e.key, e.title, e.description, null)
+ RestrictionEntry.TYPE_BOOLEAN ->
+ AppRestriction.BooleanItem(e.key, e.title, e.description, null)
+ RestrictionEntry.TYPE_CHOICE -> AppRestriction.ChoiceItem(e.key, e.title,
+ e.description, e.choiceEntries, e.choiceValues, null)
+ RestrictionEntry.TYPE_MULTI_SELECT -> AppRestriction.MultiSelectItem(e.key, e.title,
+ e.description, e.choiceEntries, e.choiceValues, null)
+ else -> null
+ }
+ }
+
+ fun transformAppRestriction(list: List): Bundle {
+ val b = Bundle()
+ for (r in list) {
+ when (r) {
+ is AppRestriction.IntItem -> r.value?.let { b.putInt(r.key, it) }
+ is AppRestriction.StringItem -> r.value?.let { b.putString(r.key, it) }
+ is AppRestriction.BooleanItem -> r.value?.let { b.putBoolean(r.key, it) }
+ is AppRestriction.ChoiceItem -> r.value?.let { b.putString(r.key, it) }
+ is AppRestriction.MultiSelectItem -> r.value?.let { b.putStringArray(r.key, r.value) }
+ }
+ }
+ return b
+ }
+
val appGroups = MutableStateFlow(emptyList())
init {
getAppGroups()
@@ -527,6 +638,20 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
group.filter { it.id != id }
}
}
+ fun exportAppGroups(uri: Uri) {
+ application.contentResolver.openOutputStream(uri)!!.use {
+ val list: List = appGroups.value
+ it.write(Json.encodeToString(list).encodeToByteArray())
+ }
+ }
+ fun importAppGroups(uri: Uri) {
+ application.contentResolver.openInputStream(uri)!!.use {
+ Json.decodeFromString>(it.readBytes().decodeToString())
+ }.forEach {
+ myRepo.setAppGroup(null, it.name, it.apps)
+ }
+ getAppGroups()
+ }
@RequiresApi(24)
fun reboot() {
@@ -584,16 +709,17 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
statusBarDisabled = if (VERSION.SDK_INT >= 34 &&
privilege.run { device || (profile && affiliated) })
DPM.isStatusBarDisabled else false,
- autoTimeEnabled = if (VERSION.SDK_INT >= 30 && privilege.run { device || org })
+ autoTimeEnabled = if (VERSION.SDK_INT >= 30 && (privilege.device || privilege.org))
DPM.getAutoTimeEnabled(DAR) else false,
- autoTimeZoneEnabled = if (VERSION.SDK_INT >= 30 && privilege.run { device || org })
+ autoTimeZoneEnabled = if (VERSION.SDK_INT >= 30 && (privilege.device || privilege.org))
DPM.getAutoTimeZoneEnabled(DAR) else false,
autoTimeRequired = if (VERSION.SDK_INT < 30) DPM.autoTimeRequired else false,
masterVolumeMuted = DPM.isMasterVolumeMuted(DAR),
backupServiceEnabled = if (VERSION.SDK_INT >= 26) DPM.isBackupServiceEnabled(DAR) else false,
- btContactSharingDisabled = if (VERSION.SDK_INT >= 23 && privilege.work)
+ btContactSharingDisabled = if (privilege.work)
DPM.getBluetoothContactSharingDisabled(DAR) else false,
- commonCriteriaMode = if (VERSION.SDK_INT >= 30) DPM.isCommonCriteriaModeEnabled(DAR) else false,
+ commonCriteriaMode = if (VERSION.SDK_INT >= 30 && (privilege.device || privilege.org))
+ DPM.isCommonCriteriaModeEnabled(DAR) else false,
usbSignalEnabled = if (VERSION.SDK_INT >= 31) DPM.isUsbDataSignalingEnabled else false,
canDisableUsbSignal = if (VERSION.SDK_INT >= 31) DPM.canUsbDataSignalingBeDisabled() else false
)
@@ -609,7 +735,6 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
it.copy(screenCaptureDisabled = DPM.getScreenCaptureDisabled(null))
}
}
- @RequiresApi(23)
fun setStatusBarDisabled(disabled: Boolean) {
val result = DPM.setStatusBarDisabled(DAR, disabled)
if (result) systemOptionsStatus.update { it.copy(statusBarDisabled = disabled) }
@@ -643,7 +768,6 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
it.copy(backupServiceEnabled = DPM.isBackupServiceEnabled(DAR))
}
}
- @RequiresApi(23)
fun setBtContactSharingDisabled(disabled: Boolean) {
DPM.setBluetoothContactSharingDisabled(DAR, disabled)
systemOptionsStatus.update {
@@ -662,7 +786,6 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
DPM.isUsbDataSignalingEnabled = enabled
systemOptionsStatus.update { it.copy(usbSignalEnabled = DPM.isUsbDataSignalingEnabled) }
}
- @RequiresApi(23)
fun setKeyguardDisabled(disabled: Boolean): Boolean {
return DPM.setKeyguardDisabled(DAR, disabled)
}
@@ -732,11 +855,9 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
fun setContentProtectionPolicy(policy: Int) {
DPM.setContentProtectionPolicy(DAR, policy)
}
- @RequiresApi(23)
fun getPermissionPolicy(): Int {
return DPM.getPermissionPolicy(DAR)
}
- @RequiresApi(23)
fun setPermissionPolicy(policy: Int) {
DPM.setPermissionPolicy(DAR, policy)
}
@@ -784,19 +905,35 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
getLockTaskPackages()
}
@RequiresApi(28)
- fun startLockTaskMode(packageName: String, activity: String): Boolean {
+ fun startLockTaskMode(
+ packageName: String, activity: String, clearTask: Boolean, showNotification: Boolean
+ ): Boolean {
if (!DPM.isLockTaskPermitted(packageName)) {
val list = lockTaskPackages.value.map { it.name } + packageName
DPM.setLockTaskPackages(DAR, list.toTypedArray())
getLockTaskPackages()
}
+ if (showNotification) {
+ DPM.setLockTaskFeatures(
+ DAR,
+ DPM.getLockTaskFeatures(DAR) or
+ DevicePolicyManager.LOCK_TASK_FEATURE_NOTIFICATIONS or
+ DevicePolicyManager.LOCK_TASK_FEATURE_HOME
+ )
+ }
val options = ActivityOptions.makeBasic().setLockTaskEnabled(true)
val intent = if(activity.isNotEmpty()) {
Intent().setComponent(ComponentName(packageName, activity))
} else PM.getLaunchIntentForPackage(packageName)
if (intent != null) {
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ intent.addFlags(
+ Intent.FLAG_ACTIVITY_NEW_TASK
+ or (if (clearTask) Intent.FLAG_ACTIVITY_CLEAR_TASK else 0)
+ )
application.startActivity(intent, options.toBundle())
+ if (showNotification) {
+ application.startForegroundService(Intent(application, LockTaskService::class.java))
+ }
return true
} else {
return false
@@ -906,14 +1043,12 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
}
}
}
- @RequiresApi(23)
fun getSystemUpdatePolicy(): SystemUpdatePolicyInfo {
val policy = DPM.systemUpdatePolicy
return SystemUpdatePolicyInfo(
policy?.policyType ?: -1, policy?.installWindowStart ?: 0, policy?.installWindowEnd ?: 0
)
}
- @RequiresApi(23)
fun setSystemUpdatePolicy(info: SystemUpdatePolicyInfo) {
val policy = when (info.type) {
SystemUpdatePolicy.TYPE_INSTALL_AUTOMATIC -> SystemUpdatePolicy.createAutomaticInstallPolicy()
@@ -1227,18 +1362,11 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
}
fun createWorkProfile(options: CreateWorkProfileOptions): Intent {
val intent = Intent(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE)
- if (VERSION.SDK_INT >= 23) {
- intent.putExtra(
- DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME,
- MyAdminComponent
- )
- } else {
- intent.putExtra(
- DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME,
- application.packageName
- )
- }
- if (options.migrateAccount && VERSION.SDK_INT >= 22) {
+ intent.putExtra(
+ DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME,
+ MyAdminComponent
+ )
+ if (options.migrateAccount) {
intent.putExtra(
DevicePolicyManager.EXTRA_PROVISIONING_ACCOUNT_TO_MIGRATE,
Account(options.accountName, options.accountType)
@@ -1315,10 +1443,10 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
return UserInformation(
if (VERSION.SDK_INT >= 24) UserManager.supportsMultipleUsers() else false,
if (VERSION.SDK_INT >= 31) UserManager.isHeadlessSystemUserMode() else false,
- if (VERSION.SDK_INT >= 23) UM.isSystemUser else false,
+ UM.isSystemUser,
if (VERSION.SDK_INT >= 34) UM.isAdminUser else false,
if (VERSION.SDK_INT >= 25) UM.isDemoUser else false,
- if (VERSION.SDK_INT >= 23) UM.getUserCreationTime(uh) else 0,
+ UM.getUserCreationTime(uh),
if (VERSION.SDK_INT >= 28) DPM.isLogoutEnabled else false,
if (VERSION.SDK_INT >= 28) DPM.isEphemeralUser(DAR) else false,
if (VERSION.SDK_INT >= 28) DPM.isAffiliatedUser else false,
@@ -1354,6 +1482,7 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
UserManager.USER_OPERATION_ERROR_UNKNOWN -> R.string.unknown_error
UserManager.USER_OPERATION_ERROR_MANAGED_PROFILE-> R.string.fail_managed_profile
UserManager.USER_OPERATION_ERROR_MAX_RUNNING_USERS -> R.string.limit_reached
+ UserManager.USER_OPERATION_ERROR_MAX_USERS -> R.string.limit_reached
UserManager.USER_OPERATION_ERROR_CURRENT_USER -> R.string.fail_current_user
else -> R.string.unknown
}
@@ -1392,7 +1521,6 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
fun setProfileName(name: String) {
DPM.setProfileName(DAR, name)
}
- @RequiresApi(23)
fun setUserIcon(bitmap: Bitmap) {
DPM.setUserIcon(DAR, bitmap)
}
@@ -1530,7 +1658,6 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
return PM.getPackageUid(name, 0)
}
var networkStatsData = emptyList()
- @RequiresApi(23)
fun readNetworkStats(stats: NetworkStats): List {
val list = mutableListOf()
while (stats.hasNextBucket()) {
@@ -1541,7 +1668,6 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
stats.close()
return list
}
- @RequiresApi(23)
fun readNetworkStatsBucket(bucket: NetworkStats.Bucket): NetworkStatsData {
return NetworkStatsData(
bucket.rxBytes, bucket.rxPackets, bucket.txBytes, bucket.txPackets,
@@ -1823,7 +1949,12 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
}
@RequiresApi(26)
fun setRpToken(token: String): Boolean {
- return DPM.setResetPasswordToken(DAR, token.encodeToByteArray())
+ return try {
+ DPM.setResetPasswordToken(DAR, token.encodeToByteArray())
+ } catch (e: Exception) {
+ e.printStackTrace()
+ false
+ }
}
@RequiresApi(26)
fun clearRpToken(): Boolean {
diff --git a/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt b/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt
index ec7b1f7..4f34429 100644
--- a/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt
+++ b/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt
@@ -6,7 +6,9 @@ import android.graphics.drawable.Drawable
import android.os.Build
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
-import androidx.compose.foundation.clickable
+import androidx.compose.foundation.background
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -22,7 +24,14 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.List
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material.icons.filled.MoreVert
+import androidx.compose.material.icons.filled.Refresh
+import androidx.compose.material.icons.outlined.Clear
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
@@ -36,6 +45,7 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
@@ -45,8 +55,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
@@ -66,17 +78,20 @@ data class AppInfo(
private fun searchInString(query: String, content: String)
= query.split(' ').all { content.contains(it, true) }
-@Serializable data class ApplicationsList(val canSwitchView: Boolean)
+@Serializable data class ApplicationsList(val canSwitchView: Boolean, val multiSelect: Boolean)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
fun AppChooserScreen(
- canSwitchView: Boolean, packageList: MutableStateFlow>,
+ params: ApplicationsList, packageList: MutableStateFlow>,
refreshProgress: MutableStateFlow, onChoosePackage: (String?) -> Unit,
- onSwitchView: () -> Unit, onRefresh: () -> Unit
+ onSwitchView: () -> Unit, onRefresh: () -> Unit,
+ setPackagesSuspend: (List, Boolean) -> Unit,
+ setPackagesHidden: (List, Boolean) -> Unit,
) {
val packages by packageList.collectAsStateWithLifecycle()
val context = LocalContext.current
+ val hf = LocalHapticFeedback.current
val progress by refreshProgress.collectAsStateWithLifecycle()
var system by rememberSaveable { mutableStateOf(false) }
var query by rememberSaveable { mutableStateOf("") }
@@ -85,6 +100,7 @@ fun AppChooserScreen(
system == (it.flags and ApplicationInfo.FLAG_SYSTEM != 0) &&
(query.isEmpty() || (searchInString(query, it.label) || searchInString(query, it.name)))
}
+ val selectedPackages = remember { mutableStateListOf() }
val focusMgr = LocalFocusManager.current
LaunchedEffect(Unit) {
if(packages.size <= 1) onRefresh()
@@ -101,18 +117,86 @@ fun AppChooserScreen(
system = !system
context.popToast(if(system) R.string.show_system_app else R.string.show_user_app)
}) {
- Icon(painter = painterResource(R.drawable.filter_alt_fill0), contentDescription = null)
+ Icon(painterResource(R.drawable.filter_alt_fill0), null)
}
- IconButton(onRefresh, enabled = progress == 1F) {
- Icon(painter = painterResource(R.drawable.refresh_fill0), contentDescription = null)
+ if (selectedPackages.isEmpty()) {
+ IconButton(onRefresh, enabled = progress == 1F) {
+ Icon(Icons.Default.Refresh, null)
+ }
+ if (params.canSwitchView) IconButton(onSwitchView) {
+ Icon(Icons.AutoMirrored.Default.List, null)
+ }
}
- if (canSwitchView) IconButton(onSwitchView) {
- Icon(Icons.AutoMirrored.Default.List, null)
+ }
+ if (selectedPackages.isNotEmpty()) {
+ if (params.canSwitchView) {
+ var dropdown by remember { mutableStateOf(false) }
+ Box {
+ IconButton({
+ dropdown = !dropdown
+ }) {
+ Icon(Icons.Default.MoreVert, null)
+ }
+ DropdownMenu(dropdown, { dropdown = false }) {
+ if (Build.VERSION.SDK_INT >= 24) {
+ DropdownMenuItem(
+ { Text(stringResource(R.string.suspend)) },
+ {
+ setPackagesSuspend(selectedPackages.map { it.name }, true)
+ dropdown = false
+ selectedPackages.clear()
+ },
+ leadingIcon = {
+ Icon(painterResource(R.drawable.block_fill0), null)
+ }
+ )
+ DropdownMenuItem(
+ { Text(stringResource(R.string.unsuspend)) },
+ {
+ setPackagesSuspend(selectedPackages.map { it.name }, false)
+ dropdown = false
+ selectedPackages.clear()
+ },
+ leadingIcon = {
+ Icon(painterResource(R.drawable.enable_fill0), null)
+ }
+ )
+ }
+ DropdownMenuItem(
+ { Text(stringResource(R.string.hide)) },
+ {
+ setPackagesHidden(selectedPackages.map { it.name }, true)
+ dropdown = false
+ selectedPackages.clear()
+ },
+ leadingIcon = {
+ Icon(painterResource(R.drawable.visibility_off_fill0), null)
+ }
+ )
+ DropdownMenuItem(
+ { Text(stringResource(R.string.unhide)) },
+ {
+ setPackagesHidden(selectedPackages.map { it.name }, false)
+ dropdown = false
+ selectedPackages.clear()
+ },
+ leadingIcon = {
+ Icon(painterResource(R.drawable.visibility_fill0), null)
+ }
+ )
+ }
+ }
+ } else {
+ FilledIconButton({
+ onChoosePackage(selectedPackages.joinToString("\n") { it.name })
+ }) {
+ Icon(Icons.Default.Check, null)
+ }
}
}
},
title = {
- if(searchMode) {
+ if (searchMode) {
val fr = remember { FocusRequester() }
LaunchedEffect(Unit) { fr.requestFocus() }
OutlinedTextField(
@@ -122,19 +206,20 @@ fun AppChooserScreen(
keyboardActions = KeyboardActions { focusMgr.clearFocus() },
placeholder = { Text(stringResource(R.string.search)) },
trailingIcon = {
- Icon(
- painter = painterResource(R.drawable.close_fill0),
- contentDescription = null,
- modifier = Modifier.clickable {
- focusMgr.clearFocus()
- query = ""
- searchMode = false
- }
- )
+ IconButton({
+ query = ""
+ searchMode = false
+ }) {
+ Icon(Icons.Outlined.Clear, null)
+ }
},
textStyle = typography.bodyLarge,
modifier = Modifier.fillMaxWidth().focusRequester(fr)
)
+ } else {
+ if (selectedPackages.isNotEmpty()) {
+ Text(selectedPackages.size.toString())
+ }
}
},
navigationIcon = {
@@ -156,10 +241,24 @@ fun AppChooserScreen(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
- .clickable {
- focusMgr.clearFocus()
- onChoosePackage(it.name)
- }
+ .combinedClickable(onLongClick = {
+ if (params.multiSelect && it !in selectedPackages) {
+ selectedPackages += it
+ hf.performHapticFeedback(HapticFeedbackType.LongPress)
+ }
+ }, onClick = {
+ if (selectedPackages.isEmpty()) {
+ focusMgr.clearFocus()
+ onChoosePackage(it.name)
+ } else {
+ if (it in selectedPackages) selectedPackages -= it
+ else selectedPackages += it
+ }
+ })
+ .background(
+ if (it in selectedPackages) MaterialTheme.colorScheme.primaryContainer
+ else MaterialTheme.colorScheme.background
+ )
.padding(horizontal = 8.dp, vertical = 10.dp)
.animateItem()
) {
diff --git a/app/src/main/java/com/bintianqi/owndroid/Receiver.kt b/app/src/main/java/com/bintianqi/owndroid/Receiver.kt
index b542132..f5cd676 100644
--- a/app/src/main/java/com/bintianqi/owndroid/Receiver.kt
+++ b/app/src/main/java/com/bintianqi/owndroid/Receiver.kt
@@ -1,31 +1,17 @@
package com.bintianqi.owndroid
-import android.app.NotificationManager
-import android.app.PendingIntent
import android.app.admin.DeviceAdminReceiver
-import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.Binder
import android.os.Build.VERSION
import android.os.UserHandle
import android.os.UserManager
-import androidx.core.app.NotificationCompat
import com.bintianqi.owndroid.dpm.handlePrivilegeChange
import com.bintianqi.owndroid.dpm.retrieveNetworkLogs
import com.bintianqi.owndroid.dpm.retrieveSecurityLogs
class Receiver : DeviceAdminReceiver() {
- override fun onReceive(context: Context, intent: Intent) {
- super.onReceive(context, intent)
- if(VERSION.SDK_INT >= 26 && intent.action == "com.bintianqi.owndroid.action.STOP_LOCK_TASK_MODE") {
- val receiver = ComponentName(context, this::class.java)
- val packages = Privilege.DPM.getLockTaskPackages(receiver)
- Privilege.DPM.setLockTaskPackages(receiver, arrayOf())
- Privilege.DPM.setLockTaskPackages(receiver, packages)
- }
- }
-
override fun onEnabled(context: Context, intent: Intent) {
super.onEnabled(context, intent)
Privilege.updateStatus()
@@ -56,25 +42,6 @@ class Receiver : DeviceAdminReceiver() {
}
}
- override fun onLockTaskModeEntering(context: Context, intent: Intent, pkg: String) {
- super.onLockTaskModeEntering(context, intent, pkg)
- val stopIntent = Intent(context, this::class.java)
- .setAction("com.bintianqi.owndroid.action.STOP_LOCK_TASK_MODE")
- val pendingIntent = PendingIntent.getBroadcast(context, 0, stopIntent, PendingIntent.FLAG_IMMUTABLE)
- val notification = NotificationCompat.Builder(context, MyNotificationChannel.LockTaskMode.id)
- .setContentTitle(context.getText(R.string.lock_task_mode))
- .setSmallIcon(R.drawable.lock_fill0)
- .addAction(NotificationCompat.Action.Builder(null, context.getString(R.string.stop), pendingIntent).build())
- .build()
- val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- nm.notify(NotificationType.LockTaskMode.id, notification)
- }
-
- override fun onLockTaskModeExiting(context: Context, intent: Intent) {
- super.onLockTaskModeExiting(context, intent)
- NotificationUtils.cancel(context, NotificationType.LockTaskMode)
- }
-
override fun onPasswordChanged(context: Context, intent: Intent, userHandle: UserHandle) {
super.onPasswordChanged(context, intent, userHandle)
sendUserRelatedNotification(context, userHandle, NotificationType.PasswordChanged)
diff --git a/app/src/main/java/com/bintianqi/owndroid/Settings.kt b/app/src/main/java/com/bintianqi/owndroid/Settings.kt
index a004147..a005535 100644
--- a/app/src/main/java/com/bintianqi/owndroid/Settings.kt
+++ b/app/src/main/java/com/bintianqi/owndroid/Settings.kt
@@ -232,7 +232,7 @@ fun AppLockSettingsScreen(
config: AppLockConfig, setConfig: (AppLockConfig) -> Unit,
onNavigateUp: () -> Unit
) = MyScaffold(R.string.app_lock, onNavigateUp) {
- var password by rememberSaveable { mutableStateOf(config.password ?: "") }
+ var password by rememberSaveable { mutableStateOf("") }
var confirmPassword by rememberSaveable { mutableStateOf("") }
var allowBiometrics by rememberSaveable { mutableStateOf(config.biometrics) }
var lockWhenLeaving by rememberSaveable { mutableStateOf(config.whenLeaving) }
diff --git a/app/src/main/java/com/bintianqi/owndroid/Utils.kt b/app/src/main/java/com/bintianqi/owndroid/Utils.kt
index 700658d..c545970 100644
--- a/app/src/main/java/com/bintianqi/owndroid/Utils.kt
+++ b/app/src/main/java/com/bintianqi/owndroid/Utils.kt
@@ -1,9 +1,12 @@
package com.bintianqi.owndroid
+import android.content.BroadcastReceiver
import android.content.ClipData
import android.content.ClipboardManager
import android.content.ComponentName
import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
import android.content.pm.PackageInfo
import android.net.Uri
import android.os.Build
@@ -159,4 +162,20 @@ fun Modifier.clickableTextField(onClick: () -> Unit) =
fun adaptiveInsets(): WindowInsets {
val navbar = WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)
return WindowInsets.ime.union(navbar).union(WindowInsets.displayCutout)
-}
\ No newline at end of file
+}
+
+fun registerPackageRemovedReceiver(
+ ctx: Context, callback: (String) -> Unit
+) {
+ val br = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ callback(intent.data!!.schemeSpecificPart)
+ }
+ }
+ val filter = IntentFilter()
+ filter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED)
+ filter.addDataScheme("package")
+ ctx.registerReceiver(br, filter)
+}
+
+fun parsePackageNames(input: String) = input.split('\n').filter { it.isNotEmpty() }
diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt
index c4ffd1b..1f501dd 100644
--- a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt
+++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt
@@ -5,8 +5,11 @@ import android.app.admin.DevicePolicyManager.PERMISSION_GRANT_STATE_DENIED
import android.app.admin.DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED
import android.app.admin.PackagePolicy
import android.content.Intent
+import android.net.Uri
import android.os.Build.VERSION
import android.os.Looper
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@@ -21,14 +24,17 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.List
@@ -37,10 +43,14 @@ import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.outlined.CheckCircle
+import androidx.compose.material.icons.outlined.Clear
import androidx.compose.material.icons.outlined.Delete
+import androidx.compose.material.icons.outlined.Search
import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.Checkbox
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -53,7 +63,17 @@ import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
+import androidx.compose.material3.SegmentedButton
+import androidx.compose.material3.SegmentedButtonDefaults
+import androidx.compose.material3.SingleChoiceSegmentedButtonRow
+import androidx.compose.material3.SnackbarDuration
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.SnackbarResult
+import androidx.compose.material3.Surface
+import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
@@ -65,21 +85,26 @@ import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bintianqi.owndroid.AppInfo
@@ -90,6 +115,7 @@ import com.bintianqi.owndroid.MyViewModel
import com.bintianqi.owndroid.Privilege
import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.adaptiveInsets
+import com.bintianqi.owndroid.parsePackageNames
import com.bintianqi.owndroid.showOperationResultToast
import com.bintianqi.owndroid.ui.FullWidthRadioButtonItem
import com.bintianqi.owndroid.ui.FunctionItem
@@ -103,7 +129,10 @@ import com.google.accompanist.drawablepainter.rememberDrawablePainter
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
+import sh.calvin.reorderable.ReorderableItem
+import sh.calvin.reorderable.rememberReorderableLazyListState
val String.isValidPackageName
get() = Regex("""^(?:[a-zA-Z]\w*\.)+[a-zA-Z]\w*$""").matches(this)
@@ -111,13 +140,18 @@ val String.isValidPackageName
@Composable
fun LazyItemScope.ApplicationItem(info: AppInfo, onClear: () -> Unit) {
Row(
- Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 6.dp).animateItem(),
+ Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 8.dp, vertical = 6.dp)
+ .animateItem(),
Arrangement.SpaceBetween, Alignment.CenterVertically
) {
Row(Modifier.weight(1F), verticalAlignment = Alignment.CenterVertically) {
Image(
painter = rememberDrawablePainter(info.icon), contentDescription = null,
- modifier = Modifier.padding(start = 12.dp, end = 18.dp).size(30.dp)
+ modifier = Modifier
+ .padding(start = 12.dp, end = 18.dp)
+ .size(30.dp)
)
Column {
Text(info.label)
@@ -137,13 +171,16 @@ fun PackageNameTextField(
) {
val fm = LocalFocusManager.current
OutlinedTextField(
- value, onValueChange, Modifier.fillMaxWidth().then(modifier),
+ value, onValueChange, Modifier
+ .fillMaxWidth()
+ .then(modifier),
label = { Text(stringResource(R.string.package_name)) },
trailingIcon = {
IconButton(onChoosePackage) {
Icon(Icons.AutoMirrored.Default.List, null)
}
},
+ isError = value.isNotEmpty() && !value.isValidPackageName,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Done),
keyboardActions = KeyboardActions { fm.clearFocus() }
)
@@ -186,9 +223,7 @@ fun ApplicationsFeaturesScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Un
if(VERSION.SDK_INT >= 30 && (privilege.device || (VERSION.SDK_INT >= 33 && privilege.profile))) {
FunctionItem(R.string.disable_user_control, icon = R.drawable.do_not_touch_fill0) { onNavigate(DisableUserControl) }
}
- if(VERSION.SDK_INT >= 23) {
- FunctionItem(R.string.permissions, icon = R.drawable.shield_fill0) { onNavigate(PermissionsManager()) }
- }
+ FunctionItem(R.string.permissions, icon = R.drawable.shield_fill0) { onNavigate(PermissionsManager()) }
if(VERSION.SDK_INT >= 28) {
FunctionItem(R.string.disable_metered_data, icon = R.drawable.money_off_fill0) { onNavigate(DisableMeteredData) }
}
@@ -248,12 +283,20 @@ fun ApplicationDetailsScreen(
var dialog by rememberSaveable { mutableIntStateOf(0) } // 1: clear storage, 2: uninstall
val info = vm.getAppInfo(packageName)
val status by vm.appStatus.collectAsStateWithLifecycle()
- LaunchedEffect(Unit) { vm.getAppStatus(packageName) }
+ val appRestrictions by vm.appRestrictions.collectAsStateWithLifecycle()
+ LaunchedEffect(Unit) {
+ vm.getAppStatus(packageName)
+ vm.getAppRestrictions(packageName)
+ }
MySmallTitleScaffold(R.string.place_holder, onNavigateUp, 0.dp) {
- Column(Modifier.align(Alignment.CenterHorizontally).padding(top = 16.dp), horizontalAlignment = Alignment.CenterHorizontally) {
+ Column(Modifier
+ .align(Alignment.CenterHorizontally)
+ .padding(top = 16.dp), horizontalAlignment = Alignment.CenterHorizontally) {
Image(rememberDrawablePainter(info.icon), null, Modifier.size(50.dp))
Text(info.label, Modifier.padding(top = 4.dp))
- Text(info.name, Modifier.alpha(0.7F).padding(bottom = 8.dp), style = typography.bodyMedium)
+ Text(info.name, Modifier
+ .alpha(0.7F)
+ .padding(bottom = 8.dp), style = typography.bodyMedium)
}
FunctionItem(R.string.permissions, icon = R.drawable.shield_fill0) { onNavigate(PermissionsManager(packageName)) }
if(VERSION.SDK_INT >= 24) SwitchItem(
@@ -285,13 +328,21 @@ fun ApplicationDetailsScreen(
state = status.keepUninstalled,
onCheckedChange = { vm.adSetPackageKu(packageName, it) }
)
+ if (appRestrictions.isNotEmpty()) {
+ FunctionItem(R.string.managed_configuration, icon = R.drawable.description_fill0) {
+ onNavigate(ManagedConfiguration(packageName))
+ }
+ }
if(VERSION.SDK_INT >= 28) FunctionItem(R.string.clear_app_storage, icon = R.drawable.mop_fill0) { dialog = 1 }
FunctionItem(R.string.uninstall, icon = R.drawable.delete_fill0) { dialog = 2 }
Spacer(Modifier.height(BottomPadding))
}
if(dialog == 1 && VERSION.SDK_INT >= 28)
ClearAppStorageDialog(packageName, vm::clearAppData) { dialog = 0 }
- if(dialog == 2) UninstallAppDialog(packageName, vm::uninstallPackage) { dialog = 0 }
+ if(dialog == 2) UninstallAppDialog(packageName, vm::uninstallPackage) {
+ dialog = 0
+ if (it) onNavigateUp()
+ }
}
@Serializable object Suspend
@@ -304,7 +355,6 @@ fun ApplicationDetailsScreen(
@Serializable data class PermissionsManager(val packageName: String? = null)
-@RequiresApi(23)
@Composable
fun PermissionsManagerScreen(
packagePermissions: MutableStateFlow