diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 8a48c02..4e2288c 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -13,6 +13,8 @@ jobs:
steps:
- name: Check out repository
uses: actions/checkout@v4
+ with:
+ ref: 'master'
- name: Set up JDK 21
uses: actions/setup-java@v4
diff --git a/Readme-en.md b/Readme-en.md
deleted file mode 100644
index 6265fe8..0000000
--- a/Readme-en.md
+++ /dev/null
@@ -1,139 +0,0 @@
-[日本語](Readme-ja.md) | [简体中文](Readme.md)
-
-# OwnDroid
-
-Use Android's DevicePolicyManager API to manage your device.
-
-## Download
-
-- [IzzyOnDroid F-Droid Repository](https://apt.izzysoft.de/fdroid/index/apk/com.bintianqi.owndroid)
-- [Releases on GitHub](https://github.com/BinTianqi/OwnDroid/releases)
-
-> [!NOTE]
-> ColorOS users should download testkey version from releases on GitHub
-
-## Features
-
-- System: disable camera, disable screenshot, master volume mute, disable USB signal, lock task mode, wipe data...
-- Network: add/modify/delete Wi-Fi, network stats, network logging...
-- Applications: suspend/hide app, block app uninstallation, grant/revoke permissions, clear app storage, install/uninstall app...
-- User restriction: disable SMS, disable outgoing call, disable bluetooth, disable NFC, disable USB file transfer, disable app installing/uninstalling...
-- Users: user information, create/start/switch/stop/delete user...
-- Password and keyguard: reset password, set screen timeout...
-
-## Working modes
-
-- Device owner (recommended)
-
- Activating methods:
- - Shizuku
- - Dhizuku
- - Root
- - ADB shell command `dpm set-device-owner com.bintianqi.owndroid/.Receiver`
-- [Dhizuku](https://github.com/iamr0s/Dhizuku)
-- Work profile
-
-## FAQ
-
-### Already some accounts on the device
-
-```text
-java.lang.IllegalStateException: Not allowed to set the device owner because there are already some accounts on the device
-```
-
-Solutions:
-- Freeze apps who hold those accounts.
-- Delete these accounts.
-
-### Already several users on the device
-
-```text
-java.lang.IllegalStateException: Not allowed to set the device owner because there are already several users on the device
-```
-
-Solutions:
-- Delete secondary users.
-
-> [!NOTE]
-> Some systems have features such as app cloning and children space, which are usually users.
-
-### MIUI & HyperOS
-
-```text
-java.lang.SecurityException: Neither user 2000 nor current process has android.permission.MANAGE_DEVICE_ADMINS.
-```
-
-Solutions:
-- Enable `USB debugging (Security setting)` in developer options.
-- Or execute activating command in root shell.
-
-### ColorOS
-
-```text
-java.lang.IllegalStateException: Unexpected @ProvisioningPreCondition
-```
-
-Solution: Use OwnDroid testkey version
-
-### Samsung
-
-```text
-user limit reached
-```
-
-Samsung restricts Android's multiple users feature. There is currently no solution.
-
-## API
-
-| ID | Extras | Minimum Android version |
-|--------------------------|------------------------|:-----------------------:|
-| `HIDE` | `package` | |
-| `UNHIDE` | `package` | |
-| `SUSPEND` | `package` | 7 |
-| `UNSUSPEND` | `package` | 7 |
-| `ADD_USER_RESTRICTION` | `restriction` | |
-| `CLEAR_USER_RESTRICTION` | `restriction` | |
-| `SET_PERMISSION_DEFAULT` | `package` `permission` | 6 |
-| `SET_PERMISSION_GRANTED` | `package` `permission` | 6 |
-| `SET_PERMISSION_DENIED` | `package` `permission` | 6 |
-| `LOCK` | | |
-| `REBOOT` | | 7 |
-
-[Available user restrictions](https://developer.android.com/reference/android/os/UserManager#constants_1)
-
-```shell
-# An example of hiding app in ADB shell
-am broadcast -a com.bintianqi.owndroid.action.HIDE -n com.bintianqi.owndroid/.ApiReceiver --es key abcdefg --es package com.example.app
-```
-
-```kotlin
-// An example of hiding app in Kotlin
-val intent = Intent("com.bintianqi.owndroid.action.HIDE")
- .setComponent(ComponentName("com.bintianqi.owndroid", "com.bintianqi.owndroid.ApiReceiver"))
- .putExtra("key", "abcdefg")
- .putExtra("package", "com.example.app")
-context.sendBroadcast(intent)
-```
-
-## Build
-
-You can use Gradle in command line to build OwnDroid.
-```shell
-# Use testkey for signing (default)
-./gradlew build
-# Use your custom .jks key for signing
-./gradlew build -PStoreFile="/path/to/your/jks/file" -PStorePassword="YOUR_KEYSTORE_PASSWORD" -PKeyPassword="YOUR_KEY_PASSWORD" -PKeyAlias="YOUR_KEY_ALIAS"
-```
-(Use `./gradlew.bat` instead on Windows)
-
-## License
-
-[License.md](LICENSE.md)
-
-> Copyright (C) 2024 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.
->
-> This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
->
-> You should have received a copy of the GNU General Public License along with this program. If not, see .
diff --git a/Readme-ja.md b/Readme-ja.md
index b72641b..4a8c3d5 100644
--- a/Readme-ja.md
+++ b/Readme-ja.md
@@ -1,4 +1,4 @@
-[English](Readme-en.md) | [简体中文](Readme.md)
+[English](Readme.md) | [简体中文](Readme-zh_CN.md)
> [!important]
> The Japanese readme need update
diff --git a/Readme-zh_CN.md b/Readme-zh_CN.md
new file mode 100644
index 0000000..01e7af0
--- /dev/null
+++ b/Readme-zh_CN.md
@@ -0,0 +1,151 @@
+[English](Readme.md) | [日本語](Readme-ja.md)
+
+# OwnDroid
+
+使用安卓的设备策略管理器API管理你的设备。
+
+## 下载
+
+- [IzzyOnDroid F-Droid Repository](https://apt.izzysoft.de/fdroid/index/apk/com.bintianqi.owndroid)
+- [Releases on GitHub](https://github.com/BinTianqi/OwnDroid/releases)
+
+> [!NOTE]
+> ColorOS用户应在GitHub上的releases下载testkey版本
+
+## 功能
+
+- 系统:禁用摄像头、禁止截屏、全局静音、禁用USB信号、锁定任务模式、清除数据...
+- 网络:添加/修改/删除 Wi-Fi、网络统计、网络日志...
+- 应用:挂起/隐藏应用、阻止应用卸载、授予/撤销权限、清除应用存储、安装/卸载应用...
+- 用户限制:禁止发送短信、禁止拨出电话、禁用蓝牙、禁用NFC、禁用USB文件传输、禁止安装/卸载应用...
+- 用户:用户信息、创建/启动/切换/停止/删除用户...
+- 密码与锁屏:重置密码、设置屏幕超时...
+
+## 工作模式
+
+- Device owner(推荐)
+
+ 激活方式:
+ - Shizuku
+ - Dhizuku
+ - Root
+ - ADB shell命令 `dpm set-device-owner com.bintianqi.owndroid/.Receiver`
+- [Dhizuku](https://github.com/iamr0s/Dhizuku)
+- 工作资料
+
+## FAQ
+
+### 设备上有账号
+
+```text
+java.lang.IllegalStateException: Not allowed to set the device owner because there are already some accounts on the device
+```
+
+解决办法:
+- 冻结持有这些账号的app。
+- 删除这些账号。
+
+### 设备上有多个用户
+
+```text
+java.lang.IllegalStateException: Not allowed to set the device owner because there are already several users on the device
+```
+
+解决办法:
+- 删除次级用户。
+
+> [!NOTE]
+> 一些系统有应用克隆、儿童空间等功能,它们通常是用户。
+
+#### Device owner 已存在
+
+```text
+java.lang.IllegalStateException: Trying to set the device owner (com.bintianqi.owndroid/.Receiver), but device owner (xxx) is already set.
+```
+
+一个设备只能存在一个device owner,请先停用已存在的device owner。
+
+### MIUI & HyperOS
+
+```text
+java.lang.SecurityException: Neither user 2000 nor current process has android.permission.MANAGE_DEVICE_ADMINS.
+```
+
+解决办法: 在开发者设置中打开`USB调试(安全设置)`,或在root命令行中执行激活命令。
+
+### ColorOS
+
+```text
+java.lang.IllegalStateException: Unexpected @ProvisioningPreCondition
+```
+
+解决办法:使用 OwnDroid testkey 版本
+
+### 三星
+
+```text
+user limit reached
+```
+
+三星限制了多用户功能,暂无解决办法。
+
+## API
+
+OwnDroid提供了一个基于Intent的API。你需要在设置中设置密钥并启用API。括号中的数字是最小的安卓版本。
+
+- HIDE(package: String)
+- UNHIDE(package: String)
+- SUSPEND(package: String) (7)
+- UNSUSPEND(package: String) (7)
+- ADD_USER_RESTRICTION(restriction: Boolean)
+- CLEAR_USER_RESTRICTION(restriction: Boolean)
+- SET_PERMISSION_DEFAULT(package: String, permission: String) (6)
+- SET_PERMISSION_GRANTED(package: String, permission: String) (6)
+- SET_PERMISSION_DENIED(package: String, permission: String) (6)
+- SET_SCREEN_CAPTURE_DISABLED()
+- SET_SCREEN_CAPTURE_ENABLED()
+- SET_CAMERA_DISABLED()
+- SET_CAMERA_ENABLED()
+- SET_USB_DISABLED() (12)
+- SET_USB_ENABLED() (12)
+- LOCK()
+- REBOOT() (7)
+
+```shell
+# 一个在ADB shell中隐藏app的示例
+am broadcast -a com.bintianqi.owndroid.action.HIDE -n com.bintianqi.owndroid/.ApiReceiver --es key abcdefg --es package com.example.app
+```
+
+```kotlin
+// 一个在Kotlin中隐藏app的示例
+val intent = Intent("com.bintianqi.owndroid.action.HIDE")
+ .setComponent(ComponentName("com.bintianqi.owndroid", "com.bintianqi.owndroid.ApiReceiver"))
+ .putExtra("key", "abcdefg")
+ .putExtra("package", "com.example.app")
+context.sendBroadcast(intent)
+```
+
+[可用的用户限制](https://developer.android.google.cn/reference/android/os/UserManager#constants_1)
+
+## 构建
+
+你可以在命令行中使用Gradle以构建OwnDroid
+```shell
+# 使用testkey签名(默认)
+./gradlew build
+# 使用你的jks密钥签名
+./gradlew build -PStoreFile="/path/to/your/jks/file" -PStorePassword="YOUR_KEYSTORE_PASSWORD" -PKeyPassword="YOUR_KEY_PASSWORD" -PKeyAlias="YOUR_KEY_ALIAS"
+```
+(在Windows系统中应使用`./gradlew.bat`)
+
+## 许可证
+
+[License.md](LICENSE.md)
+
+> Copyright (C) 2024 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.
+>
+> This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+>
+> You should have received a copy of the GNU General Public License along with this program. If not, see .
diff --git a/Readme.md b/Readme.md
index d3309b0..2a2b9a0 100644
--- a/Readme.md
+++ b/Readme.md
@@ -1,61 +1,69 @@
-[English](Readme-en.md) | [日本語](Readme-ja.md)
+[简体中文](Readme-zh_CN.md) | [日本語](Readme-ja.md)
# OwnDroid
-使用安卓的设备策略管理器API管理你的设备。
+Use Android's DevicePolicyManager API to manage your device.
-## 下载
+## Download
- [IzzyOnDroid F-Droid Repository](https://apt.izzysoft.de/fdroid/index/apk/com.bintianqi.owndroid)
- [Releases on GitHub](https://github.com/BinTianqi/OwnDroid/releases)
> [!NOTE]
-> ColorOS用户应在GitHub上的releases下载testkey版本
+> ColorOS users should download testkey version from releases on GitHub
-## 功能
+## Features
-- 系统:禁用摄像头、禁止截屏、全局静音、禁用USB信号、锁定任务模式、清除数据...
-- 网络:添加/修改/删除 Wi-Fi、网络统计、网络日志...
-- 应用:挂起/隐藏应用、阻止应用卸载、授予/撤销权限、清除应用存储、安装/卸载应用...
-- 用户限制:禁止发送短信、禁止拨出电话、禁用蓝牙、禁用NFC、禁用USB文件传输、禁止安装/卸载应用...
-- 用户:用户信息、创建/启动/切换/停止/删除用户...
-- 密码与锁屏:重置密码、设置屏幕超时...
+- System: disable camera, disable screenshot, master volume mute, disable USB signal, lock task mode, wipe data...
+- Network: add/modify/delete Wi-Fi, network stats, network logging...
+- Applications: suspend/hide app, block app uninstallation, grant/revoke permissions, clear app storage, install/uninstall app...
+- User restriction: disable SMS, disable outgoing call, disable bluetooth, disable NFC, disable USB file transfer, disable app installing/uninstalling...
+- Users: user information, create/start/switch/stop/delete user...
+- Password and keyguard: reset password, set screen timeout...
-## 工作模式
+## Working modes
-- Device owner(推荐)
+- Device owner (recommended)
- 激活方式:
+ Activating methods:
- Shizuku
- Dhizuku
- Root
- - ADB shell命令 `dpm set-device-owner com.bintianqi.owndroid/.Receiver`
+ - ADB shell command `dpm set-device-owner com.bintianqi.owndroid/.Receiver`
- [Dhizuku](https://github.com/iamr0s/Dhizuku)
-- 工作资料
+- Work profile
## FAQ
-### 设备上有账号
+### Already some accounts on the device
```text
java.lang.IllegalStateException: Not allowed to set the device owner because there are already some accounts on the device
```
-解决办法:
-- 冻结持有这些账号的app。
-- 删除这些账号。
+Solutions:
+- Freeze apps who hold those accounts.
+- Delete these accounts.
-### 设备上有多个用户
+### Already several users on the device
```text
java.lang.IllegalStateException: Not allowed to set the device owner because there are already several users on the device
```
-解决办法:
-- 删除次级用户。
+Solutions:
+- Delete secondary users.
> [!NOTE]
-> 一些系统有应用克隆、儿童空间等功能,它们通常是用户。
+> Some systems have features such as app cloning and children space, which are usually users.
+
+#### 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.
+```
+
+Only 1 device owner can exist on a device. Please deactivate the existing device owner first.
### MIUI & HyperOS
@@ -63,7 +71,9 @@ java.lang.IllegalStateException: Not allowed to set the device owner because the
java.lang.SecurityException: Neither user 2000 nor current process has android.permission.MANAGE_DEVICE_ADMINS.
```
-解决办法: 在开发者设置中打开`USB调试(安全设置)`,或在root命令行中执行激活命令。
+Solutions:
+- Enable `USB debugging (Security setting)` in developer options.
+- Or execute activating command in root shell.
### ColorOS
@@ -71,41 +81,45 @@ java.lang.SecurityException: Neither user 2000 nor current process has android.p
java.lang.IllegalStateException: Unexpected @ProvisioningPreCondition
```
-解决办法:使用 OwnDroid testkey 版本
+Solution: Use OwnDroid testkey version
-### 三星
+### Samsung
```text
user limit reached
```
-三星限制了多用户功能,暂无解决办法。
+Samsung restricts Android's multiple users feature. There is currently no solution.
## API
-| ID | Extra | 最小安卓版本 |
-|--------------------------|------------------------|:------:|
-| `HIDE` | `package` | |
-| `UNHIDE` | `package` | |
-| `SUSPEND` | `package` | 7 |
-| `UNSUSPEND` | `package` | 7 |
-| `ADD_USER_RESTRICTION` | `restriction` | |
-| `CLEAR_USER_RESTRICTION` | `restriction` | |
-| `SET_PERMISSION_DEFAULT` | `package` `permission` | 6 |
-| `SET_PERMISSION_GRANTED` | `package` `permission` | 6 |
-| `SET_PERMISSION_DENIED` | `package` `permission` | 6 |
-| `LOCK` | | |
-| `REBOOT` | | 7 |
+OwnDroid provides an Intent-based API. You need to set the API key in settings and enable the API. The numbers in brackets represent the minimum Android version required.
-[可用的用户限制](https://developer.android.google.cn/reference/android/os/UserManager#constants_1)
+- HIDE(package: String)
+- UNHIDE(package: String)
+- SUSPEND(package: String) (7)
+- UNSUSPEND(package: String) (7)
+- ADD_USER_RESTRICTION(restriction: Boolean)
+- CLEAR_USER_RESTRICTION(restriction: Boolean)
+- SET_PERMISSION_DEFAULT(package: String, permission: String) (6)
+- SET_PERMISSION_GRANTED(package: String, permission: String) (6)
+- SET_PERMISSION_DENIED(package: String, permission: String) (6)
+- SET_SCREEN_CAPTURE_DISABLED()
+- SET_SCREEN_CAPTURE_ENABLED()
+- SET_CAMERA_DISABLED()
+- SET_CAMERA_ENABLED()
+- SET_USB_DISABLED() (12)
+- SET_USB_ENABLED() (12)
+- LOCK()
+- REBOOT() (7)
```shell
-# 一个在ADB shell中隐藏app的示例
+# An example of hiding app in ADB shell
am broadcast -a com.bintianqi.owndroid.action.HIDE -n com.bintianqi.owndroid/.ApiReceiver --es key abcdefg --es package com.example.app
```
```kotlin
-// 一个在Kotlin中隐藏app的示例
+// An example of hiding app in Kotlin
val intent = Intent("com.bintianqi.owndroid.action.HIDE")
.setComponent(ComponentName("com.bintianqi.owndroid", "com.bintianqi.owndroid.ApiReceiver"))
.putExtra("key", "abcdefg")
@@ -113,18 +127,20 @@ val intent = Intent("com.bintianqi.owndroid.action.HIDE")
context.sendBroadcast(intent)
```
-## 构建
+[Available user restrictions](https://developer.android.com/reference/android/os/UserManager#constants_1)
-你可以在命令行中使用Gradle以构建OwnDroid
+## Build
+
+You can use Gradle in command line to build OwnDroid.
```shell
-# 使用testkey签名(默认)
+# Use testkey for signing (default)
./gradlew build
-# 使用你的jks密钥签名
+# Use your custom .jks key for signing
./gradlew build -PStoreFile="/path/to/your/jks/file" -PStorePassword="YOUR_KEYSTORE_PASSWORD" -PKeyPassword="YOUR_KEY_PASSWORD" -PKeyAlias="YOUR_KEY_ALIAS"
```
-(在Windows系统中应使用`./gradlew.bat`)
+(Use `./gradlew.bat` instead on Windows)
-## 许可证
+## License
[License.md](LICENSE.md)
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index d94f989..84c2009 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -26,8 +26,8 @@ android {
applicationId = "com.bintianqi.owndroid"
minSdk = 21
targetSdk = 36
- versionCode = 40
- versionName = "7.1"
+ versionCode = 41
+ versionName = "7.2"
multiDexEnabled = false
}
@@ -44,6 +44,10 @@ android {
debug {
signingConfig = signingConfigs.getByName("defaultSignature")
}
+ create("fastDebug") {
+ initWith(getByName("debug"))
+ isDebuggable = false
+ }
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
@@ -91,6 +95,7 @@ dependencies {
implementation(libs.accompanist.permissions)
implementation(libs.androidx.material3)
implementation(libs.androidx.navigation.compose)
+ implementation(libs.material.icons.core)
implementation(libs.shizuku.provider)
implementation(libs.shizuku.api)
implementation(libs.dhizuku.api)
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index b19edd8..48b55e8 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -24,3 +24,5 @@
-dontwarn android.app.ActivityThread
-dontwarn android.app.ContextImpl
-dontwarn android.app.LoadedApk
+
+-keep class com.bintianqi.owndroid.MyViewModel { *; }
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f71f941..506c110 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -12,7 +12,7 @@
-
+
+
@@ -60,11 +61,6 @@
-
Privilege.DPM.setApplicationHidden(Privilege.DAR, app, true)
"UNHIDE" -> Privilege.DPM.setApplicationHidden(Privilege.DAR, app, false)
- "SUSPEND" -> Privilege.DPM.setPackagesSuspended(Privilege.DAR, arrayOf(app), true).isEmpty()
- "UNSUSPEND" -> Privilege.DPM.setPackagesSuspended(Privilege.DAR, arrayOf(app), false).isEmpty()
- "ADD_USER_RESTRICTION" -> { Privilege.DPM.addUserRestriction(Privilege.DAR, restriction); true }
- "CLEAR_USER_RESTRICTION" -> { Privilege.DPM.clearUserRestriction(Privilege.DAR, restriction); true }
+ "SUSPEND" -> Privilege.DPM.setPackagesSuspended(Privilege.DAR, arrayOf(app), true)
+ "UNSUSPEND" -> Privilege.DPM.setPackagesSuspended(Privilege.DAR, arrayOf(app), false)
+ "ADD_USER_RESTRICTION" -> { Privilege.DPM.addUserRestriction(Privilege.DAR, restriction) }
+ "CLEAR_USER_RESTRICTION" -> { Privilege.DPM.clearUserRestriction(Privilege.DAR, restriction) }
"SET_PERMISSION_DEFAULT" -> {
Privilege.DPM.setPermissionGrantState(
Privilege.DAR, app!!, permission!!,
@@ -45,14 +44,30 @@ class ApiReceiver: BroadcastReceiver() {
DevicePolicyManager.PERMISSION_GRANT_STATE_DENIED
)
}
- "LOCK" -> { Privilege.DPM.lockNow(); true }
- "REBOOT" -> { Privilege.DPM.reboot(Privilege.DAR); true }
+ "LOCK" -> { Privilege.DPM.lockNow() }
+ "REBOOT" -> { Privilege.DPM.reboot(Privilege.DAR) }
+ "SET_CAMERA_DISABLED" -> {
+ Privilege.DPM.setCameraDisabled(Privilege.DAR, true)
+ }
+ "SET_CAMERA_ENABLED" -> {
+ Privilege.DPM.setCameraDisabled(Privilege.DAR, false)
+ }
+ "SET_USB_DISABLED" -> {
+ Privilege.DPM.isUsbDataSignalingEnabled = false
+ }
+ "SET_USB_ENABLED" -> {
+ Privilege.DPM.isUsbDataSignalingEnabled = true
+ }
+ "SET_SCREEN_CAPTURE_DISABLED" -> {
+ Privilege.DPM.setScreenCaptureDisabled(Privilege.DAR, true)
+ }
+ "SET_SCREEN_CAPTURE_ENABLED" -> {
+ Privilege.DPM.setScreenCaptureDisabled(Privilege.DAR, false)
+ }
else -> {
log += "\nInvalid action"
- false
}
}
- log += "\nsuccess: $ok"
} catch(e: Exception) {
e.printStackTrace()
val message = (e::class.qualifiedName ?: "Exception") + ": " + (e.message ?: "")
diff --git a/app/src/main/java/com/bintianqi/owndroid/AppInstallerActivity.kt b/app/src/main/java/com/bintianqi/owndroid/AppInstallerActivity.kt
index f37be6c..ca6b948 100644
--- a/app/src/main/java/com/bintianqi/owndroid/AppInstallerActivity.kt
+++ b/app/src/main/java/com/bintianqi/owndroid/AppInstallerActivity.kt
@@ -1,401 +1,29 @@
package com.bintianqi.owndroid
-import android.app.Application
-import android.app.PendingIntent
-import android.content.BroadcastReceiver
-import android.content.Context
-import android.content.Intent
-import android.content.IntentFilter
-import android.content.pm.PackageInfo
-import android.content.pm.PackageInstaller
-import android.net.Uri
-import android.os.Build
import android.os.Bundle
-import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
-import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.ColumnScope
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.heightIn
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.pager.HorizontalPager
-import androidx.compose.foundation.pager.rememberPagerState
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Add
-import androidx.compose.material.icons.filled.Check
-import androidx.compose.material.icons.filled.Clear
-import androidx.compose.material.icons.filled.PlayArrow
-import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.CircularProgressIndicator
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.ExtendedFloatingActionButton
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Scaffold
-import androidx.compose.material3.Tab
-import androidx.compose.material3.TabRow
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
-import androidx.compose.material3.TopAppBar
-import androidx.compose.runtime.Composable
+import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
-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.platform.LocalContext
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import androidx.core.content.ContextCompat
-import androidx.core.net.toUri
import androidx.fragment.app.FragmentActivity
-import androidx.lifecycle.AndroidViewModel
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.lifecycle.viewModelScope
-import com.bintianqi.owndroid.dpm.parsePackageInstallerMessage
-import com.bintianqi.owndroid.ui.FullWidthCheckBoxItem
-import com.bintianqi.owndroid.ui.FullWidthRadioButtonItem
+import com.bintianqi.owndroid.ui.AppInstaller
import com.bintianqi.owndroid.ui.theme.OwnDroidTheme
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.update
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-import java.net.URLDecoder
class AppInstallerActivity:FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
super.onCreate(savedInstanceState)
- val myVm by viewModels()
val vm by viewModels()
vm.initialize(intent)
+ val theme = ThemeSettings(SP.materialYou, SP.darkTheme, SP.blackTheme)
setContent {
- val theme by myVm.theme.collectAsStateWithLifecycle()
OwnDroidTheme(theme) {
- val installing by vm.installing.collectAsStateWithLifecycle()
- val options by vm.options.collectAsStateWithLifecycle()
- val packages by vm.packages.collectAsStateWithLifecycle()
- val writtenPackages by vm.writtenPackages.collectAsStateWithLifecycle()
- val writingPackage by vm.writingPackage.collectAsStateWithLifecycle()
- val result by vm.result.collectAsStateWithLifecycle()
+ val uiState by vm.uiState.collectAsState()
AppInstaller(
- installing, options, { if(!installing) vm.options.value = it },
- packages, { uri -> vm.packages.update { it.minus(uri) } },
- { uris -> vm.packages.update { it.plus(uris) } },
- vm::startInstall, writtenPackages, writingPackage,
- result, { vm.result.value = null }
+ uiState, vm::onPackagesAdd, vm::onPackageRemove, vm::startInstall, vm::closeResultDialog
)
}
}
}
}
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Preview
-@Composable
-private fun AppInstaller(
- installing: Boolean = false,
- options: SessionParamsOptions = SessionParamsOptions(),
- onOptionsChange: (SessionParamsOptions) -> Unit = {},
- packages: Set = setOf("https://example.com".toUri()),
- onPackageRemove: (Uri) -> Unit = {},
- onPackageChoose: (List) -> Unit = {},
- onStartInstall: () -> Unit = {},
- writtenPackages: Set = setOf("https://example.com".toUri()),
- writingPackage: Uri? = null,
- result: Intent? = null,
- onResultDialogClose: () -> Unit = {}
-) {
- var appLockDialog by rememberSaveable { mutableStateOf(false) }
- val coroutine = rememberCoroutineScope()
- Scaffold(
- topBar = {
- TopAppBar(
- title = { Text(stringResource(R.string.app_installer)) }
- )
- },
- floatingActionButton = {
- if(packages.isNotEmpty()) ExtendedFloatingActionButton(
- text = { Text(stringResource(R.string.start)) },
- icon = {
- if(installing) CircularProgressIndicator(modifier = Modifier.size(24.dp))
- else Icon(Icons.Default.PlayArrow, null)
- },
- onClick = {
- if(SP.lockPasswordHash.isNullOrEmpty()) onStartInstall() else appLockDialog = true
- },
- expanded = !installing
- )
- }
- ) { paddingValues ->
- var tab by remember { mutableIntStateOf(0) }
- val pagerState = rememberPagerState { 2 }
- val scrollState = rememberScrollState()
- tab = pagerState.targetPage
- Column(modifier = Modifier.padding(paddingValues)) {
- TabRow(tab) {
- Tab(
- tab == 0,
- onClick = {
- coroutine.launch { scrollState.animateScrollTo(0) }
- coroutine.launch { pagerState.animateScrollToPage(0) }
- },
- text = { Text(stringResource(R.string.packages)) }
- )
- Tab(
- tab == 1,
- onClick = {
- coroutine.launch { scrollState.animateScrollTo(0) }
- coroutine.launch { pagerState.animateScrollToPage(1) }
- },
- text = { Text(stringResource(R.string.options)) }
- )
- }
- HorizontalPager(pagerState) { page ->
- Column(modifier = Modifier.fillMaxSize().verticalScroll(scrollState).padding(top = 8.dp)) {
- if(page == 0) Packages(installing, packages, onPackageRemove, onPackageChoose, writtenPackages, writingPackage)
- else Options(options, onOptionsChange)
- }
- }
- ResultDialog(result, onResultDialogClose)
- }
- }
- if(appLockDialog) {
- AppLockDialog({
- appLockDialog = false
- onStartInstall()
- }) { appLockDialog = false }
- }
-}
-
-
-@Composable
-private fun ColumnScope.Packages(
- installing: Boolean,
- packages: Set, onRemove: (Uri) -> Unit, onChoose: (List) -> Unit,
- writtenPackages: Set, writingPackage: Uri?
-) {
- val chooseSplitPackage = rememberLauncherForActivityResult(ActivityResultContracts.GetMultipleContents(), onChoose)
- packages.forEach {
- PackageItem(
- it, installing,
- { onRemove(it) }, it in writtenPackages, it == writingPackage
- )
- }
- AnimatedVisibility(!installing) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp).clickable {
- chooseSplitPackage.launch(APK_MIME)
- }.padding(vertical = 12.dp)
- ) {
- Icon(Icons.Default.Add, null, modifier = Modifier.padding(horizontal = 10.dp))
- Text(stringResource(R.string.add_packages), style = MaterialTheme.typography.titleMedium)
- }
- }
-}
-
-
-@Composable
-private fun PackageItem(uri: Uri, installing: Boolean, onRemove: () -> Unit, isWritten: Boolean, isWriting: Boolean) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween,
- modifier = Modifier.fillMaxWidth().padding(start = 8.dp, end = 6.dp, bottom = 6.dp).heightIn(min = 40.dp)
- ) {
- Text(
- URLDecoder.decode(URLDecoder.decode(uri.path ?: uri.toString())),
- style = MaterialTheme.typography.bodyMedium,
- modifier = Modifier.fillMaxWidth(0.85F)
- )
- if(!installing) IconButton(onRemove) {
- Icon(Icons.Default.Clear, contentDescription = stringResource(R.string.remove))
- }
- if(isWritten) Icon(Icons.Default.Check, null, Modifier.padding(end = 8.dp), MaterialTheme.colorScheme.secondary)
- if(isWriting) CircularProgressIndicator(Modifier.padding(end = 8.dp).size(24.dp))
- }
-}
-
-data class SessionParamsOptions(
- val mode: Int = PackageInstaller.SessionParams.MODE_FULL_INSTALL,
- val keepOriginalEnabledSetting: Boolean = false,
- val noKill: Boolean = false,
- val location: Int = PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY,
-)
-
-@Composable
-private fun ColumnScope.Options(options: SessionParamsOptions, onChange: (SessionParamsOptions) -> Unit) {
- Text(
- stringResource(R.string.mode), modifier = Modifier.padding(top = 10.dp, start = 8.dp, bottom = 4.dp),
- style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary
- )
- FullWidthRadioButtonItem(R.string.full_install, options.mode == PackageInstaller.SessionParams.MODE_FULL_INSTALL) {
- onChange(options.copy(mode = PackageInstaller.SessionParams.MODE_FULL_INSTALL, noKill = false))
- }
- FullWidthRadioButtonItem(R.string.inherit_existing, options.mode == PackageInstaller.SessionParams.MODE_INHERIT_EXISTING) {
- onChange(options.copy(mode = PackageInstaller.SessionParams.MODE_INHERIT_EXISTING))
- }
- if(Build.VERSION.SDK_INT >= 34) {
- AnimatedVisibility(options.mode == PackageInstaller.SessionParams.MODE_INHERIT_EXISTING) {
- FullWidthCheckBoxItem(R.string.dont_kill_app, options.noKill) {
- onChange(options.copy(noKill = it))
- }
- }
- FullWidthCheckBoxItem(R.string.keep_original_enabled_setting, options.keepOriginalEnabledSetting) {
- onChange(options.copy(keepOriginalEnabledSetting = it))
- }
- }
- Text(
- stringResource(R.string.install_location), modifier = Modifier.padding(top = 10.dp, start = 8.dp, bottom = 4.dp),
- style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.primary
- )
- FullWidthRadioButtonItem(R.string.auto, options.location == PackageInfo.INSTALL_LOCATION_AUTO) {
- onChange(options.copy(location = PackageInfo.INSTALL_LOCATION_AUTO))
- }
- FullWidthRadioButtonItem(R.string.internal_only, options.location == PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY) {
- onChange(options.copy(location = PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY))
- }
- FullWidthRadioButtonItem(R.string.prefer_external, options.location == PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL) {
- onChange(options.copy(location = PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL))
- }
-}
-
-@Composable
-private fun ResultDialog(result: Intent?, onDialogClose: () -> Unit) {
- if(result != null) {
- val status = result.getIntExtra(PackageInstaller.EXTRA_STATUS, 999)
- AlertDialog(
- title = {
- val text = if(status == PackageInstaller.STATUS_SUCCESS) R.string.success else R.string.failure
- Text(stringResource(text))
- },
- text = {
- val context = LocalContext.current
- Text(parsePackageInstallerMessage(context, result))
- },
- confirmButton = {
- TextButton(onDialogClose) {
- Text(stringResource(R.string.confirm))
- }
- },
- onDismissRequest = onDialogClose
- )
- }
-}
-
-class AppInstallerViewModel(application: Application): AndroidViewModel(application) {
- fun initialize(intent: Intent) {
- intent.data?.let { uri -> packages.update { it + uri } }
- intent.getParcelableExtra(Intent.EXTRA_STREAM)?.let { uri -> packages.update { it + uri } }
- intent.getParcelableArrayExtra(Intent.EXTRA_STREAM)?.forEach { uri -> packages.update { it + (uri as Uri) } }
- intent.clipData?.let { clipData ->
- for(i in 0..clipData.itemCount) {
- packages.update { it + clipData.getItemAt(i).uri }
- }
- }
- }
- val installing = MutableStateFlow(false)
- val result = MutableStateFlow(null)
- val packages = MutableStateFlow(setOf())
-
- val options = MutableStateFlow(SessionParamsOptions())
-
- val writtenPackages = MutableStateFlow(setOf())
- val writingPackage = MutableStateFlow(null)
- private fun getSessionParams(): PackageInstaller.SessionParams {
- return PackageInstaller.SessionParams(options.value.mode).apply {
- if(Build.VERSION.SDK_INT >= 34) {
- if(options.value.keepOriginalEnabledSetting) setApplicationEnabledSettingPersistent()
- setDontKillApp(options.value.noKill)
- }
- setInstallLocation(options.value.location)
- }
- }
- fun startInstall() {
- if(installing.value) return
- installing.value = true
- viewModelScope.launch(Dispatchers.IO) {
- val context = getApplication()
- val packageInstaller = context.packageManager.packageInstaller
- val sessionId = packageInstaller.createSession(getSessionParams())
- val session = packageInstaller.openSession(sessionId)
- try {
- packages.value.forEach { splitPackageUri ->
- withContext(Dispatchers.Main) { writingPackage.value = splitPackageUri }
- session.openWrite(splitPackageUri.hashCode().toString(), 0, -1).use { splitPackageOut ->
- context.contentResolver.openInputStream(splitPackageUri)!!.use { splitPackageIn ->
- splitPackageIn.copyTo(splitPackageOut)
- }
- session.fsync(splitPackageOut)
- }
- withContext(Dispatchers.Main) { writtenPackages.update { it.plus(splitPackageUri) } }
- }
- withContext(Dispatchers.Main) { writingPackage.value = null }
- } catch(e: Exception) {
- e.printStackTrace()
- session.abandon()
- return@launch
- }
- val receiver = object : BroadcastReceiver() {
- override fun onReceive(context: Context, intent: Intent) {
- val statusExtra = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 999)
- if(statusExtra == PackageInstaller.STATUS_PENDING_USER_ACTION) {
- @SuppressWarnings("UnsafeIntentLaunch")
- context.startActivity(
- (intent.getParcelableExtra(Intent.EXTRA_INTENT) as Intent?)
- ?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- )
- } else {
- result.value = intent
- writtenPackages.value = setOf()
- if(statusExtra == PackageInstaller.STATUS_SUCCESS) {
- packages.value = setOf()
- }
- installing.value = false
- context.unregisterReceiver(this)
- }
- }
- }
- ContextCompat.registerReceiver(
- context, receiver, IntentFilter(ACTION), null,
- null, ContextCompat.RECEIVER_EXPORTED
- )
- val pi = if(Build.VERSION.SDK_INT >= 34) {
- PendingIntent.getBroadcast(
- context, sessionId, Intent(ACTION),
- PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT or PendingIntent.FLAG_MUTABLE
- ).intentSender
- } else {
- PendingIntent.getBroadcast(context, sessionId, Intent(ACTION), PendingIntent.FLAG_MUTABLE).intentSender
- }
- session.commit(pi)
- }
- }
-
- override fun onCleared() {
- super.onCleared()
- viewModelScope.cancel()
- }
- companion object {
- const val ACTION = "com.bintianqi.owndroid.action.PACKAGE_INSTALLER_SESSION_STATUS_CHANGED"
- }
-}
diff --git a/app/src/main/java/com/bintianqi/owndroid/AppInstallerViewModel.kt b/app/src/main/java/com/bintianqi/owndroid/AppInstallerViewModel.kt
new file mode 100644
index 0000000..db89514
--- /dev/null
+++ b/app/src/main/java/com/bintianqi/owndroid/AppInstallerViewModel.kt
@@ -0,0 +1,150 @@
+package com.bintianqi.owndroid
+
+import android.app.Application
+import android.app.PendingIntent
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.PackageInfo
+import android.content.pm.PackageInstaller
+import android.net.Uri
+import android.os.Build
+import androidx.core.content.ContextCompat
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.application
+import androidx.lifecycle.viewModelScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.serialization.Serializable
+
+class AppInstallerViewModel(application: Application): AndroidViewModel(application) {
+ val uiState = MutableStateFlow(UiState())
+ data class UiState(
+ val packages: List = emptyList(),
+ val installing: Boolean = false,
+ val packageWriting: Int = -1,
+ val result: Intent? = null
+ )
+
+ fun initialize(intent: Intent) {
+ val list = mutableListOf()
+ intent.data?.let { list += it }
+ 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) {
+ list += clipData.getItemAt(i).uri
+ }
+ }
+ uiState.update { it.copy(it.packages + list.distinct()) }
+ }
+
+ fun onPackagesAdd(packages: List) {
+ uiState.update {
+ it.copy(packages = it.packages.plus(packages).distinct())
+ }
+ }
+
+ fun onPackageRemove(uri: Uri) {
+ uiState.update {
+ it.copy(packages = it.packages.minus(uri))
+ }
+ }
+
+ private fun getSessionParams(options: SessionParamsOptions): PackageInstaller.SessionParams {
+ return PackageInstaller.SessionParams(options.mode).apply {
+ if(Build.VERSION.SDK_INT >= 34) {
+ if(options.keepOriginalEnabledSetting) setApplicationEnabledSettingPersistent()
+ setDontKillApp(options.noKill)
+ }
+ setInstallLocation(options.location)
+ }
+ }
+
+ fun startInstall(options: SessionParamsOptions) {
+ if (uiState.value.installing) return
+ viewModelScope.launch(Dispatchers.IO) {
+ installPackages(options)
+ }
+ }
+
+ private fun installPackages(options: SessionParamsOptions) {
+ val packageInstaller = application.packageManager.packageInstaller
+ val sessionId = packageInstaller.createSession(getSessionParams(options))
+ val session = packageInstaller.openSession(sessionId)
+ try {
+ uiState.update { it.copy(packageWriting = 0) }
+ uiState.value.packages.forEach { uri ->
+ session.openWrite(uri.hashCode().toString(), 0, -1).use { splitPackageOut ->
+ application.contentResolver.openInputStream(uri)!!.use { splitPackageIn ->
+ splitPackageIn.copyTo(splitPackageOut)
+ }
+ session.fsync(splitPackageOut)
+ }
+ uiState.update { it.copy(packageWriting = it.packageWriting + 1) }
+ }
+ } catch(e: Exception) {
+ e.printStackTrace()
+ session.abandon()
+ uiState.update { it.copy(installing = false, packageWriting = -1) }
+ return
+ }
+ ContextCompat.registerReceiver(
+ application, Receiver(), IntentFilter(ACTION), null,
+ null, ContextCompat.RECEIVER_EXPORTED
+ )
+ val pi = if(Build.VERSION.SDK_INT >= 34) {
+ PendingIntent.getBroadcast(
+ application, sessionId, Intent(ACTION),
+ PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT or PendingIntent.FLAG_MUTABLE
+ ).intentSender
+ } else {
+ PendingIntent.getBroadcast(application, sessionId, Intent(ACTION), PendingIntent.FLAG_MUTABLE).intentSender
+ }
+ session.commit(pi)
+ }
+
+ inner class Receiver() : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ val statusExtra = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 999)
+ if (statusExtra == PackageInstaller.STATUS_PENDING_USER_ACTION) {
+ @SuppressWarnings("UnsafeIntentLaunch")
+ context.startActivity(
+ (intent.getParcelableExtra(Intent.EXTRA_INTENT) as Intent?)
+ ?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ )
+ } else {
+ uiState.update { it.copy(result = intent) }
+ context.unregisterReceiver(this)
+ }
+ }
+ }
+
+ fun closeResultDialog() {
+ if (uiState.value.result?.getIntExtra(PackageInstaller.EXTRA_STATUS, 999) == PackageInstaller.STATUS_SUCCESS) {
+ uiState.update { it.copy(emptyList(), packageWriting = -1, result = null) }
+ } else {
+ uiState.update { it.copy(packageWriting = -1, result = null) }
+ }
+ }
+
+ override fun onCleared() {
+ super.onCleared()
+ viewModelScope.cancel()
+ }
+ companion object {
+ const val ACTION = "com.bintianqi.owndroid.action.PACKAGE_INSTALLER_SESSION_STATUS_CHANGED"
+ }
+}
+
+@Serializable
+data class SessionParamsOptions(
+ val mode: Int = PackageInstaller.SessionParams.MODE_FULL_INSTALL,
+ val keepOriginalEnabledSetting: Boolean = false,
+ val noKill: Boolean = false,
+ val location: Int = PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY,
+)
diff --git a/app/src/main/java/com/bintianqi/owndroid/AppLock.kt b/app/src/main/java/com/bintianqi/owndroid/AppLock.kt
index d3c0052..820c554 100644
--- a/app/src/main/java/com/bintianqi/owndroid/AppLock.kt
+++ b/app/src/main/java/com/bintianqi/owndroid/AppLock.kt
@@ -26,9 +26,12 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+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.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
@@ -44,8 +47,9 @@ import androidx.compose.ui.window.DialogProperties
fun AppLockDialog(onSucceed: () -> Unit, onDismiss: () -> Unit) = Dialog(onDismiss, DialogProperties(true, false)) {
val context = LocalContext.current
val fm = LocalFocusManager.current
- var input by remember { mutableStateOf("") }
- var isError by remember { mutableStateOf(false) }
+ val fr = remember { FocusRequester() }
+ var input by rememberSaveable { mutableStateOf("") }
+ var isError by rememberSaveable { mutableStateOf(false) }
fun unlock() {
if(input.hash() == SP.lockPasswordHash) {
fm.clearFocus()
@@ -55,14 +59,18 @@ fun AppLockDialog(onSucceed: () -> Unit, onDismiss: () -> Unit) = Dialog(onDismi
}
}
LaunchedEffect(Unit) {
- if (Build.VERSION.SDK_INT >= 28 && SP.biometricsUnlock) startBiometricsUnlock(context, onSucceed)
+ if (Build.VERSION.SDK_INT >= 28 && SP.biometricsUnlock) {
+ startBiometricsUnlock(context, onSucceed)
+ } else {
+ fr.requestFocus()
+ }
}
BackHandler(onBack = onDismiss)
Card(Modifier.pointerInput(Unit) { detectTapGestures(onTap = { fm.clearFocus() }) }, shape = RoundedCornerShape(16.dp)) {
Column(Modifier.padding(12.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
OutlinedTextField(
- input, { input = it; isError = false }, Modifier.width(200.dp),
+ input, { input = it; isError = false }, Modifier.width(200.dp).focusRequester(fr),
label = { Text(stringResource(R.string.password)) }, isError = isError,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password, imeAction = if(input.length >= 4) ImeAction.Go else ImeAction.Done
diff --git a/app/src/main/java/com/bintianqi/owndroid/DhizukuServer.kt b/app/src/main/java/com/bintianqi/owndroid/DhizukuServer.kt
index 488a19f..51bce41 100644
--- a/app/src/main/java/com/bintianqi/owndroid/DhizukuServer.kt
+++ b/app/src/main/java/com/bintianqi/owndroid/DhizukuServer.kt
@@ -9,7 +9,6 @@ import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
-import androidx.activity.viewModels
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.size
import androidx.compose.material3.AlertDialog
@@ -20,11 +19,11 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.bintianqi.owndroid.ui.theme.OwnDroidTheme
import com.google.accompanist.drawablepainter.rememberDrawablePainter
import com.rosan.dhizuku.aidl.IDhizukuClient
@@ -34,13 +33,9 @@ import com.rosan.dhizuku.server_api.DhizukuService
import com.rosan.dhizuku.shared.DhizukuVariables
import kotlinx.coroutines.delay
import kotlinx.serialization.Serializable
-import kotlinx.serialization.encodeToString
-import kotlinx.serialization.json.Json
private const val TAG = "DhizukuServer"
-const val DHIZUKU_CLIENTS_FILE = "dhizuku_clients.json"
-
class MyDhizukuProvider(): DhizukuProvider() {
override fun onCreateService(client: IDhizukuClient): DhizukuService? {
Log.d(TAG, "Creating MyDhizukuService")
@@ -57,8 +52,6 @@ class MyDhizukuService(context: Context, admin: ComponentName, client: IDhizukuC
pm.getNameForUid(callingUid) ?: return false,
if (Build.VERSION.SDK_INT >= 28) PackageManager.GET_SIGNING_CERTIFICATES else PackageManager.GET_SIGNATURES
)
- val file = mContext.filesDir.resolve(DHIZUKU_CLIENTS_FILE)
- val clients = Json.decodeFromString>(file.readText())
val signature = getPackageSignature(packageInfo)
val requiredPermission = when (func) {
"remote_transact", "remote_process" -> func
@@ -66,9 +59,10 @@ class MyDhizukuService(context: Context, admin: ComponentName, client: IDhizukuC
"get_delegated_scopes", "set_delegated_scopes" -> "delegated_scopes"
else -> "other"
}
- val hasPermission = clients.find {
- callingUid == it.uid && signature == it.signature && requiredPermission in it.permissions
- } != null
+ val hasPermission = (mContext.applicationContext as MyApplication).myRepo
+ .checkDhizukuClientPermission(
+ callingUid, signature, requiredPermission
+ )
Log.d(TAG, "UID $callingUid, PID $callingPid, required permission: $requiredPermission, has permission: $hasPermission")
return hasPermission
}
@@ -98,26 +92,19 @@ class DhizukuActivity : ComponentActivity() {
val icon = appInfo.loadIcon(packageManager)
val label = appInfo.loadLabel(packageManager).toString()
fun close(grantPermission: Boolean) {
- val file = filesDir.resolve(DHIZUKU_CLIENTS_FILE)
- val json = Json { ignoreUnknownKeys = true }
- val clients = json.decodeFromString>(file.readText())
- val index = clients.indexOfFirst { it.uid == uid }
val clientInfo = DhizukuClientInfo(
uid, getPackageSignature(packageInfo), if (grantPermission) DhizukuPermissions else emptyList()
)
- if (index == -1) clients += clientInfo
- else clients[index] = clientInfo
- file.writeText(Json.encodeToString(clients))
+ (application as MyApplication).myRepo.setDhizukuClient(clientInfo)
finish()
listener.onRequestPermission(
if (grantPermission) PackageManager.PERMISSION_GRANTED else PackageManager.PERMISSION_DENIED
)
}
- val vm by viewModels()
enableEdgeToEdge()
+ val theme = ThemeSettings(SP.materialYou, SP.darkTheme, SP.blackTheme)
setContent {
- var appLockDialog by remember { mutableStateOf(false) }
- val theme by vm.theme.collectAsStateWithLifecycle()
+ var appLockDialog by rememberSaveable { mutableStateOf(false) }
OwnDroidTheme(theme) {
if (!appLockDialog) AlertDialog(
icon = {
diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt
index 53ee8f6..446d856 100644
--- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt
+++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt
@@ -1,9 +1,12 @@
package com.bintianqi.owndroid
+import android.Manifest
+import android.content.pm.PackageManager
import android.os.Build.VERSION
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
+import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
@@ -11,10 +14,9 @@ import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.ime
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@@ -62,8 +64,6 @@ import com.bintianqi.owndroid.dpm.AddApnSetting
import com.bintianqi.owndroid.dpm.AddApnSettingScreen
import com.bintianqi.owndroid.dpm.AddDelegatedAdmin
import com.bintianqi.owndroid.dpm.AddDelegatedAdminScreen
-import com.bintianqi.owndroid.dpm.AddNetwork
-import com.bintianqi.owndroid.dpm.AddNetworkScreen
import com.bintianqi.owndroid.dpm.AddPreferentialNetworkServiceConfig
import com.bintianqi.owndroid.dpm.AddPreferentialNetworkServiceConfigScreen
import com.bintianqi.owndroid.dpm.AffiliationId
@@ -79,7 +79,6 @@ import com.bintianqi.owndroid.dpm.AutoTimePolicyScreen
import com.bintianqi.owndroid.dpm.AutoTimeZonePolicy
import com.bintianqi.owndroid.dpm.AutoTimeZonePolicyScreen
import com.bintianqi.owndroid.dpm.BlockUninstall
-import com.bintianqi.owndroid.dpm.BlockUninstallScreen
import com.bintianqi.owndroid.dpm.CaCert
import com.bintianqi.owndroid.dpm.CaCertScreen
import com.bintianqi.owndroid.dpm.ChangeTime
@@ -101,9 +100,7 @@ import com.bintianqi.owndroid.dpm.CredentialManagerPolicyScreen
import com.bintianqi.owndroid.dpm.CrossProfileIntentFilter
import com.bintianqi.owndroid.dpm.CrossProfileIntentFilterScreen
import com.bintianqi.owndroid.dpm.CrossProfilePackages
-import com.bintianqi.owndroid.dpm.CrossProfilePackagesScreen
import com.bintianqi.owndroid.dpm.CrossProfileWidgetProviders
-import com.bintianqi.owndroid.dpm.CrossProfileWidgetProvidersScreen
import com.bintianqi.owndroid.dpm.DelegatedAdmins
import com.bintianqi.owndroid.dpm.DelegatedAdminsScreen
import com.bintianqi.owndroid.dpm.DeleteWorkProfile
@@ -115,9 +112,9 @@ import com.bintianqi.owndroid.dpm.DhizukuServerSettingsScreen
import com.bintianqi.owndroid.dpm.DisableAccountManagement
import com.bintianqi.owndroid.dpm.DisableAccountManagementScreen
import com.bintianqi.owndroid.dpm.DisableMeteredData
-import com.bintianqi.owndroid.dpm.DisableMeteredDataScreen
import com.bintianqi.owndroid.dpm.DisableUserControl
-import com.bintianqi.owndroid.dpm.DisableUserControlScreen
+import com.bintianqi.owndroid.dpm.EditAppGroup
+import com.bintianqi.owndroid.dpm.EditAppGroupScreen
import com.bintianqi.owndroid.dpm.EnableSystemApp
import com.bintianqi.owndroid.dpm.EnableSystemAppScreen
import com.bintianqi.owndroid.dpm.FrpPolicy
@@ -125,13 +122,11 @@ import com.bintianqi.owndroid.dpm.FrpPolicyScreen
import com.bintianqi.owndroid.dpm.HardwareMonitor
import com.bintianqi.owndroid.dpm.HardwareMonitorScreen
import com.bintianqi.owndroid.dpm.Hide
-import com.bintianqi.owndroid.dpm.HideScreen
import com.bintianqi.owndroid.dpm.InstallExistingApp
import com.bintianqi.owndroid.dpm.InstallExistingAppScreen
import com.bintianqi.owndroid.dpm.InstallSystemUpdate
import com.bintianqi.owndroid.dpm.InstallSystemUpdateScreen
import com.bintianqi.owndroid.dpm.KeepUninstalledPackages
-import com.bintianqi.owndroid.dpm.KeepUninstalledPackagesScreen
import com.bintianqi.owndroid.dpm.Keyguard
import com.bintianqi.owndroid.dpm.KeyguardDisabledFeatures
import com.bintianqi.owndroid.dpm.KeyguardDisabledFeaturesScreen
@@ -140,6 +135,8 @@ import com.bintianqi.owndroid.dpm.LockScreenInfo
import com.bintianqi.owndroid.dpm.LockScreenInfoScreen
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.MtePolicy
import com.bintianqi.owndroid.dpm.MtePolicyScreen
import com.bintianqi.owndroid.dpm.NearbyStreamingPolicy
@@ -157,6 +154,8 @@ import com.bintianqi.owndroid.dpm.OrganizationOwnedProfile
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
@@ -166,10 +165,10 @@ import com.bintianqi.owndroid.dpm.PermissionPolicyScreen
import com.bintianqi.owndroid.dpm.PermissionsManager
import com.bintianqi.owndroid.dpm.PermissionsManagerScreen
import com.bintianqi.owndroid.dpm.PermittedAccessibilityServices
-import com.bintianqi.owndroid.dpm.PermittedAccessibilityServicesScreen
+import com.bintianqi.owndroid.dpm.PermittedAsAndImPackages
import com.bintianqi.owndroid.dpm.PermittedInputMethods
-import com.bintianqi.owndroid.dpm.PermittedInputMethodsScreen
import com.bintianqi.owndroid.dpm.PreferentialNetworkService
+import com.bintianqi.owndroid.dpm.PreferentialNetworkServiceInfo
import com.bintianqi.owndroid.dpm.PreferentialNetworkServiceScreen
import com.bintianqi.owndroid.dpm.PrivateDns
import com.bintianqi.owndroid.dpm.PrivateDnsScreen
@@ -184,7 +183,6 @@ import com.bintianqi.owndroid.dpm.ResetPassword
import com.bintianqi.owndroid.dpm.ResetPasswordScreen
import com.bintianqi.owndroid.dpm.ResetPasswordToken
import com.bintianqi.owndroid.dpm.ResetPasswordTokenScreen
-import com.bintianqi.owndroid.dpm.Restriction
import com.bintianqi.owndroid.dpm.SecurityLogging
import com.bintianqi.owndroid.dpm.SecurityLoggingScreen
import com.bintianqi.owndroid.dpm.SetDefaultDialer
@@ -195,7 +193,6 @@ import com.bintianqi.owndroid.dpm.SupportMessageScreen
import com.bintianqi.owndroid.dpm.Suspend
import com.bintianqi.owndroid.dpm.SuspendPersonalApp
import com.bintianqi.owndroid.dpm.SuspendPersonalAppScreen
-import com.bintianqi.owndroid.dpm.SuspendScreen
import com.bintianqi.owndroid.dpm.SystemManager
import com.bintianqi.owndroid.dpm.SystemManagerScreen
import com.bintianqi.owndroid.dpm.SystemOptions
@@ -205,6 +202,8 @@ import com.bintianqi.owndroid.dpm.TransferOwnership
import com.bintianqi.owndroid.dpm.TransferOwnershipScreen
import com.bintianqi.owndroid.dpm.UninstallApp
import com.bintianqi.owndroid.dpm.UninstallAppScreen
+import com.bintianqi.owndroid.dpm.UpdateNetwork
+import com.bintianqi.owndroid.dpm.UpdateNetworkScreen
import com.bintianqi.owndroid.dpm.UserInfo
import com.bintianqi.owndroid.dpm.UserInfoScreen
import com.bintianqi.owndroid.dpm.UserOperation
@@ -222,8 +221,6 @@ import com.bintianqi.owndroid.dpm.UsersOptions
import com.bintianqi.owndroid.dpm.UsersOptionsScreen
import com.bintianqi.owndroid.dpm.UsersScreen
import com.bintianqi.owndroid.dpm.WiFi
-import com.bintianqi.owndroid.dpm.WifiAuthKeypair
-import com.bintianqi.owndroid.dpm.WifiAuthKeypairScreen
import com.bintianqi.owndroid.dpm.WifiScreen
import com.bintianqi.owndroid.dpm.WifiSecurityLevel
import com.bintianqi.owndroid.dpm.WifiSecurityLevelScreen
@@ -235,7 +232,7 @@ import com.bintianqi.owndroid.dpm.WorkModesScreen
import com.bintianqi.owndroid.dpm.WorkProfile
import com.bintianqi.owndroid.dpm.WorkProfileScreen
import com.bintianqi.owndroid.dpm.dhizukuErrorStatus
-import com.bintianqi.owndroid.ui.Animations
+import com.bintianqi.owndroid.ui.NavTransition
import com.bintianqi.owndroid.ui.theme.OwnDroidTheme
import kotlinx.serialization.Serializable
import java.util.Locale
@@ -249,6 +246,13 @@ class MainActivity : FragmentActivity() {
val locale = context.resources?.configuration?.locale
zhCN = locale == Locale.SIMPLIFIED_CHINESE || locale == Locale.CHINESE || locale == Locale.CHINA
val vm by viewModels()
+ if (
+ VERSION.SDK_INT >= 33 &&
+ checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED
+ ) {
+ val launcher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {}
+ launcher.launch(Manifest.permission.POST_NOTIFICATIONS)
+ }
setContent {
var appLockDialog by rememberSaveable { mutableStateOf(false) }
val theme by vm.theme.collectAsStateWithLifecycle()
@@ -275,7 +279,17 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
val focusMgr = LocalFocusManager.current
val lifecycleOwner = LocalLifecycleOwner.current
fun navigateUp() { navController.navigateUp() }
- fun navigate(destination: Any) { navController.navigate(destination) }
+ fun navigate(destination: Any) {
+ navController.navigate(destination) {
+ launchSingleTop = true
+ }
+ }
+ fun choosePackage() {
+ navController.navigate(ApplicationsList(false))
+ }
+ fun navigateToAppGroups() {
+ navController.navigate(ManageAppGroups)
+ }
LaunchedEffect(Unit) {
if(!Privilege.status.value.activated) {
navController.navigate(WorkModes(false)) {
@@ -290,92 +304,215 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
.fillMaxSize()
.background(colorScheme.background)
.pointerInput(Unit) { detectTapGestures(onTap = { focusMgr.clearFocus() }) },
- enterTransition = Animations.navHostEnterTransition,
- exitTransition = Animations.navHostExitTransition,
- popEnterTransition = Animations.navHostPopEnterTransition,
- popExitTransition = Animations.navHostPopExitTransition
+ enterTransition = { NavTransition.enterTransition },
+ exitTransition = { NavTransition.exitTransition },
+ popEnterTransition = { NavTransition.popEnterTransition },
+ popExitTransition = { NavTransition.popExitTransition }
) {
composable { HomeScreen(::navigate) }
composable {
- WorkModesScreen(it.toRoute(), ::navigateUp, {
+ WorkModesScreen(vm, it.toRoute(), ::navigateUp, {
navController.navigate(Home) {
popUpTo { inclusive = true }
}
+ }, {
+ navController.navigate(WorkModes(false)) {
+ popUpTo(Home) { inclusive = true }
+ }
}, ::navigate)
}
- composable { DhizukuServerSettingsScreen(::navigateUp) }
+ composable {
+ DhizukuServerSettingsScreen(vm.dhizukuClients, vm::getDhizukuClients,
+ vm::updateDhizukuClient, vm::getDhizukuServerEnabled, vm::setDhizukuServerEnabled,
+ ::navigateUp)
+ }
- composable { DelegatedAdminsScreen(::navigateUp, ::navigate) }
- composable{ AddDelegatedAdminScreen(it.toRoute(), ::navigateUp) }
- composable { DeviceInfoScreen(::navigateUp) }
- composable { LockScreenInfoScreen(::navigateUp) }
- composable { SupportMessageScreen(::navigateUp) }
+ composable {
+ DelegatedAdminsScreen(vm.delegatedAdmins, vm::getDelegatedAdmins, ::navigateUp, ::navigate)
+ }
+ composable{
+ AddDelegatedAdminScreen(vm.chosenPackage, ::choosePackage, it.toRoute(),
+ vm::setDelegatedAdmin, ::navigateUp)
+ }
+ composable { DeviceInfoScreen(vm, ::navigateUp) }
+ composable {
+ LockScreenInfoScreen(vm::getLockScreenInfo, vm::setLockScreenInfo, ::navigateUp)
+ }
+ composable {
+ SupportMessageScreen(vm::getShortSupportMessage, vm::getLongSupportMessage,
+ vm::setShortSupportMessage, vm::setLongSupportMessage, ::navigateUp)
+ }
composable {
- TransferOwnershipScreen(::navigateUp) {
+ TransferOwnershipScreen(vm.deviceAdminReceivers, vm::getDeviceAdminReceivers,
+ vm::transferOwnership, ::navigateUp) {
navController.navigate(WorkModes(false)) {
popUpTo(Home) { inclusive = true }
}
}
}
- composable { SystemManagerScreen(::navigateUp, ::navigate) }
- composable { SystemOptionsScreen(::navigateUp) }
- composable { KeyguardScreen(::navigateUp) }
- composable { HardwareMonitorScreen(::navigateUp) }
- composable { ChangeTimeScreen(::navigateUp) }
- composable { ChangeTimeZoneScreen(::navigateUp) }
- composable { AutoTimePolicyScreen(::navigateUp) }
- composable { AutoTimeZonePolicyScreen(::navigateUp) }
+ composable { SystemManagerScreen(vm, ::navigateUp, ::navigate) }
+ composable { SystemOptionsScreen(vm, ::navigateUp) }
+ composable {
+ KeyguardScreen(vm::setKeyguardDisabled, vm::lockScreen, ::navigateUp)
+ }
+ composable {
+ HardwareMonitorScreen(vm.hardwareProperties, vm::getHardwareProperties,
+ vm::setHpRefreshInterval, ::navigateUp)
+ }
+ composable { ChangeTimeScreen(vm::setTime, ::navigateUp) }
+ composable { ChangeTimeZoneScreen(vm::setTimeZone, ::navigateUp) }
+ composable {
+ AutoTimePolicyScreen(vm::getAutoTimePolicy, vm::setAutoTimePolicy, ::navigateUp)
+ }
+ composable {
+ AutoTimeZonePolicyScreen(vm::getAutoTimeZonePolicy, vm::setAutoTimeZonePolicy,
+ ::navigateUp)
+ }
//composable<> { KeyPairs(::navigateUp) }
- composable { ContentProtectionPolicyScreen(::navigateUp) }
- composable { PermissionPolicyScreen(::navigateUp) }
- composable { MtePolicyScreen(::navigateUp) }
- composable { NearbyStreamingPolicyScreen(::navigateUp) }
- composable { LockTaskModeScreen(::navigateUp) }
- composable { CaCertScreen(::navigateUp) }
- composable { SecurityLoggingScreen(::navigateUp) }
- composable { DisableAccountManagementScreen(::navigateUp) }
- composable { SystemUpdatePolicyScreen(::navigateUp) }
- composable { InstallSystemUpdateScreen(::navigateUp) }
- composable { FrpPolicyScreen(::navigateUp) }
- composable { WipeDataScreen(::navigateUp) }
+ composable {
+ ContentProtectionPolicyScreen(vm::getContentProtectionPolicy,
+ vm::setContentProtectionPolicy, ::navigateUp)
+ }
+ composable {
+ PermissionPolicyScreen(vm::getPermissionPolicy, vm::setPermissionPolicy, ::navigateUp)
+ }
+ composable {
+ MtePolicyScreen(vm::getMtePolicy, vm::setMtePolicy, ::navigateUp)
+ }
+ composable {
+ NearbyStreamingPolicyScreen(vm::getNsAppPolicy, vm::setNsAppPolicy,
+ vm::getNsNotificationPolicy, vm::setNsNotificationPolicy, ::navigateUp)
+ }
+ composable {
+ LockTaskModeScreen(vm.chosenPackage, ::choosePackage, vm.lockTaskPackages,
+ vm::getLockTaskPackages, vm::setLockTaskPackage, vm::startLockTaskMode,
+ vm:: getLockTaskFeatures, vm::setLockTaskFeatures, ::navigateUp)
+ }
+ composable {
+ CaCertScreen(vm.installedCaCerts, vm::getCaCerts, vm.selectedCaCert, vm::selectCaCert, vm::installCaCert, vm::parseCaCert,
+ vm::exportCaCert, vm::uninstallCaCert, vm::uninstallAllCaCerts, ::navigateUp)
+ }
+ composable {
+ SecurityLoggingScreen(vm::getSecurityLoggingEnabled, vm::setSecurityLoggingEnabled,
+ vm::exportSecurityLogs, vm::getSecurityLogsCount, vm::deleteSecurityLogs,
+ vm::getPreRebootSecurityLogs, vm::exportPreRebootSecurityLogs, ::navigateUp)
+ }
+ composable {
+ DisableAccountManagementScreen(vm.mdAccountTypes, vm::getMdAccountTypes,
+ vm::setMdAccountType, ::navigateUp)
+ }
+ composable {
+ SystemUpdatePolicyScreen(vm::getSystemUpdatePolicy, vm::setSystemUpdatePolicy,
+ vm::getPendingSystemUpdate, ::navigateUp)
+ }
+ composable {
+ InstallSystemUpdateScreen(vm::installSystemUpdate, ::navigateUp)
+ }
+ composable {
+ FrpPolicyScreen(vm.getFrpPolicy(), vm::setFrpPolicy, ::navigateUp)
+ }
+ composable { WipeDataScreen(vm::wipeData, ::navigateUp) }
composable { NetworkScreen(::navigateUp, ::navigate) }
- composable { WifiScreen(::navigateUp, ::navigate) { navController.navigate(AddNetwork, it)} }
- composable { NetworkOptionsScreen(::navigateUp) }
- composable { AddNetworkScreen(it.arguments!!, ::navigateUp) }
- composable { WifiSecurityLevelScreen(::navigateUp) }
- composable { WifiSsidPolicyScreen(::navigateUp) }
- composable { NetworkStatsScreen(::navigateUp, ::navigate) }
- composable(mapOf(serializableNavTypePair>())) {
- NetworkStatsViewerScreen(it.toRoute(), ::navigateUp)
+ composable {
+ WifiScreen(vm, ::navigateUp, ::navigate) { navController.navigate(UpdateNetwork(it)) }
+ }
+ composable {
+ NetworkOptionsScreen(vm::getLanEnabled, vm::setLanEnabled, ::navigateUp)
+ }
+ composable {
+ val info = vm.configuredNetworks.collectAsStateWithLifecycle().value[
+ (it.toRoute() as UpdateNetwork).index
+ ]
+ UpdateNetworkScreen(info, vm::setWifi, ::navigateUp)
+ }
+ composable {
+ WifiSecurityLevelScreen(vm::getMinimumWifiSecurityLevel,
+ vm::setMinimumWifiSecurityLevel, ::navigateUp)
+ }
+ composable {
+ WifiSsidPolicyScreen(vm::getSsidPolicy, vm::setSsidPolicy, ::navigateUp)
+ }
+ composable {
+ NetworkStatsScreen(vm.chosenPackage, ::choosePackage, vm::getPackageUid,
+ vm::queryNetworkStats, ::navigateUp) { navController.navigate(NetworkStatsViewer) }
+ }
+ composable {
+ NetworkStatsViewerScreen(vm.networkStatsData, vm::clearNetworkStats, ::navigateUp)
+ }
+ composable {
+ PrivateDnsScreen(vm::getPrivateDns, vm::setPrivateDns, ::navigateUp)
+ }
+ composable {
+ AlwaysOnVpnPackageScreen(vm::getAlwaysOnVpnPackage, vm::getAlwaysOnVpnLockdown,
+ vm::setAlwaysOnVpn, vm.chosenPackage, ::choosePackage, ::navigateUp)
+ }
+ composable {
+ RecommendedGlobalProxyScreen(vm::setRecommendedGlobalProxy, ::navigateUp)
+ }
+ composable {
+ NetworkLoggingScreen(vm::getNetworkLoggingEnabled, vm::setNetworkLoggingEnabled,
+ vm::getNetworkLogsCount, vm::exportNetworkLogs, vm::deleteNetworkLogs, ::navigateUp)
+ }
+ //composable { WifiAuthKeypairScreen(::navigateUp) }
+ composable {
+ PreferentialNetworkServiceScreen(vm::getPnsEnabled, vm::setPnsEnabled, vm.pnsConfigs,
+ vm::getPnsConfigs, ::navigateUp, ::navigate)
+ }
+ composable {
+ val info = vm.pnsConfigs.collectAsStateWithLifecycle().value.getOrNull(
+ it.toRoute().index
+ ) ?: PreferentialNetworkServiceInfo()
+ AddPreferentialNetworkServiceConfigScreen(info, vm::setPnsConfig, ::navigateUp)
+ }
+ composable {
+ OverrideApnScreen(vm.apnConfigs, vm::getApnConfigs, vm::getApnEnabled,
+ vm::setApnEnabled, ::navigateUp) { navController.navigate(AddApnSetting(it)) }
+ }
+ composable {
+ val origin = vm.apnConfigs.collectAsStateWithLifecycle().value.getOrNull((it.toRoute() as AddApnSetting).index)
+ AddApnSettingScreen(vm::setApnConfig, vm::removeApnConfig, origin, ::navigateUp)
}
- composable { PrivateDnsScreen(::navigateUp) }
- composable { AlwaysOnVpnPackageScreen(::navigateUp) }
- composable { RecommendedGlobalProxyScreen(::navigateUp) }
- composable { NetworkLoggingScreen(::navigateUp) }
- composable { WifiAuthKeypairScreen(::navigateUp) }
- composable { PreferentialNetworkServiceScreen(::navigateUp, ::navigate) }
- composable { AddPreferentialNetworkServiceConfigScreen(it.toRoute(), ::navigateUp) }
- composable { OverrideApnScreen(::navigateUp) { navController.navigate(AddApnSetting, it) } }
- composable { AddApnSettingScreen(it.arguments?.getParcelable("setting"), ::navigateUp) }
composable { WorkProfileScreen(::navigateUp, ::navigate) }
- composable { OrganizationOwnedProfileScreen(::navigateUp) }
- composable { CreateWorkProfileScreen(::navigateUp) }
- composable { SuspendPersonalAppScreen(::navigateUp) }
- composable { CrossProfileIntentFilterScreen(::navigateUp) }
- composable { DeleteWorkProfileScreen(::navigateUp) }
+ composable {
+ OrganizationOwnedProfileScreen(vm::activateOrgProfileByShizuku, ::navigateUp)
+ }
+ composable {
+ CreateWorkProfileScreen(vm::createWorkProfile, ::navigateUp)
+ }
+ composable {
+ SuspendPersonalAppScreen(
+ vm::getPersonalAppsSuspendedReason, vm::setPersonalAppsSuspended,
+ vm::getProfileMaxTimeOff, vm::setProfileMaxTimeOff, ::navigateUp
+ )
+ }
+ composable {
+ CrossProfileIntentFilterScreen(vm::addCrossProfileIntentFilter, ::navigateUp)
+ }
+ composable { DeleteWorkProfileScreen(vm::wipeData, ::navigateUp) }
composable {
- AppChooserScreen(it.toRoute(), { dest ->
- if(dest == null) navigateUp() else navigate(ApplicationDetails(dest))
+ val canSwitchView = (it.toRoute() as ApplicationsList).canSwitchView
+ AppChooserScreen(
+ canSwitchView, vm.installedPackages, vm.refreshPackagesProgress, { name ->
+ if (canSwitchView) {
+ if (name == null) {
+ navigateUp()
+ } else {
+ navigate(ApplicationDetails(name))
+ }
+ } else {
+ if (name != null) vm.chosenPackage.trySend(name)
+ navigateUp()
+ }
}, {
SP.applicationsListView = false
navController.navigate(ApplicationsFeatures) {
popUpTo(Home)
}
- })
+ }, vm::refreshPackageList)
}
composable {
ApplicationsFeaturesScreen(::navigateUp, ::navigate) {
@@ -385,63 +522,165 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
}
}
}
- composable { ApplicationDetailsScreen(it.toRoute(), ::navigateUp, ::navigate) }
- composable { SuspendScreen(::navigateUp) }
- composable { HideScreen(::navigateUp) }
- composable { BlockUninstallScreen(::navigateUp) }
- composable { DisableUserControlScreen(::navigateUp) }
- composable { PermissionsManagerScreen(::navigateUp, it.toRoute()) }
- composable { DisableMeteredDataScreen(::navigateUp) }
- composable { ClearAppStorageScreen(::navigateUp) }
- composable { UninstallAppScreen(::navigateUp) }
- composable { KeepUninstalledPackagesScreen(::navigateUp) }
- composable { InstallExistingAppScreen(::navigateUp) }
- composable { CrossProfilePackagesScreen(::navigateUp) }
- composable { CrossProfileWidgetProvidersScreen(::navigateUp) }
- composable { CredentialManagerPolicyScreen(::navigateUp) }
- composable { PermittedAccessibilityServicesScreen(::navigateUp) }
- composable { PermittedInputMethodsScreen(::navigateUp) }
- composable { EnableSystemAppScreen(::navigateUp) }
- composable { SetDefaultDialerScreen(::navigateUp) }
+ composable {
+ ApplicationDetailsScreen(it.toRoute(), vm, ::navigateUp, ::navigate)
+ }
+ composable {
+ PackageFunctionScreen(R.string.suspend, vm.suspendedPackages, vm::getSuspendedPackaged,
+ vm::setPackageSuspended, ::navigateUp, vm.chosenPackage, ::choosePackage,
+ ::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)
+ }
+ composable {
+ PackageFunctionScreenWithoutResult(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)
+ }
+ composable {
+ PermissionsManagerScreen(vm.packagePermissions, vm::getPackagePermissions,
+ vm::setPackagePermission, ::navigateUp, it.toRoute(), vm.chosenPackage, ::choosePackage)
+ }
+ composable {
+ 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)
+ }
+ composable {
+ UninstallAppScreen(vm.chosenPackage, ::choosePackage, 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)
+ }
+ composable {
+ InstallExistingAppScreen(vm.chosenPackage, ::choosePackage,
+ vm::installExistingApp, ::navigateUp)
+ }
+ composable {
+ PackageFunctionScreenWithoutResult(R.string.cross_profile_apps, vm.cpPackages,
+ vm::getCpPackages, vm::setPackageCp, ::navigateUp, vm.chosenPackage,
+ ::choosePackage, ::navigateToAppGroups, vm.appGroups)
+ }
+ composable {
+ PackageFunctionScreen(R.string.cross_profile_widget, vm.cpwProviders,
+ vm::getCpwProviders, vm::setCpwProvider, ::navigateUp, vm.chosenPackage,
+ ::choosePackage, ::navigateToAppGroups, vm.appGroups)
+ }
+ composable {
+ CredentialManagerPolicyScreen(vm.chosenPackage, ::choosePackage,
+ vm.cmPackages, vm::getCmPolicy, vm::setCmPackage, vm::setCmPolicy, ::navigateUp)
+ }
+ composable {
+ PermittedAsAndImPackages(R.string.permitted_accessibility_services,
+ R.string.system_accessibility_always_allowed, vm.chosenPackage, ::choosePackage,
+ vm.pasPackages, vm::getPasPackages, vm::setPasPackage, vm::setPasPolicy, ::navigateUp)
+ }
+ composable {
+ PermittedAsAndImPackages(R.string.permitted_ime, R.string.system_ime_always_allowed,
+ vm.chosenPackage, ::choosePackage, vm.pimPackages, vm::getPimPackages,
+ vm::setPimPackage, vm::setPimPolicy, ::navigateUp)
+ }
+ composable {
+ EnableSystemAppScreen(vm.chosenPackage, ::choosePackage, vm::enableSystemApp, ::navigateUp)
+ }
+ composable {
+ SetDefaultDialerScreen(vm.chosenPackage, ::choosePackage, vm::setDefaultDialer, ::navigateUp)
+ }
+ composable {
+ ManageAppGroupsScreen(
+ vm.appGroups,
+ { id, name, apps -> navController.navigate(EditAppGroup(id, name, apps)) },
+ ::navigateUp
+ )
+ }
+ composable {
+ EditAppGroupScreen(
+ it.toRoute(), vm::getAppInfo, ::navigateUp, vm::setAppGroup,
+ vm::deleteAppGroup, ::choosePackage, vm.chosenPackage
+ )
+ }
composable {
- UserRestrictionScreen(::navigateUp) {
- navigate(it)
- }
+ UserRestrictionScreen(vm::getUserRestrictions, ::navigateUp, ::navigate)
}
composable {
- UserRestrictionEditorScreen(::navigateUp)
+ UserRestrictionEditorScreen(vm.userRestrictions, vm::setUserRestriction, ::navigateUp)
}
- composable(mapOf(serializableNavTypePair>())) {
- UserRestrictionOptionsScreen(it.toRoute(), ::navigateUp)
+ composable {
+ UserRestrictionOptionsScreen(it.toRoute(), vm.userRestrictions,
+ vm::setUserRestriction, vm::createUserRestrictionShortcut, ::navigateUp)
}
- composable { UsersScreen(::navigateUp, ::navigate) }
- composable { UserInfoScreen(::navigateUp) }
- composable { UsersOptionsScreen(::navigateUp) }
- composable { UserOperationScreen(::navigateUp) }
- composable { CreateUserScreen(::navigateUp) }
- composable { ChangeUsernameScreen(::navigateUp) }
- composable { UserSessionMessageScreen(::navigateUp) }
- composable { AffiliationIdScreen(::navigateUp) }
+ composable { UsersScreen(vm, ::navigateUp, ::navigate) }
+ composable { UserInfoScreen(vm::getUserInformation, ::navigateUp) }
+ composable {
+ UsersOptionsScreen(vm::getLogoutEnabled, vm::setLogoutEnabled, ::navigateUp)
+ }
+ composable {
+ UserOperationScreen(vm::getUserIdentifiers, vm::doUserOperation,
+ vm::createUserOperationShortcut, ::navigateUp)
+ }
+ composable { CreateUserScreen(vm::createUser, ::navigateUp) }
+ composable { ChangeUsernameScreen(vm::setProfileName, ::navigateUp) }
+ composable {
+ UserSessionMessageScreen(vm::getUserSessionMessages, vm::setStartUserSessionMessage,
+ vm::setEndUserSessionMessage, ::navigateUp)
+ }
+ composable {
+ AffiliationIdScreen(vm.affiliationIds, vm::getAffiliationIds, vm::setAffiliationId,
+ ::navigateUp)
+ }
- composable { PasswordScreen(::navigateUp, ::navigate) }
- composable { PasswordInfoScreen(::navigateUp) }
- composable { ResetPasswordTokenScreen(::navigateUp) }
- composable { ResetPasswordScreen(::navigateUp) }
- composable { RequiredPasswordComplexityScreen(::navigateUp) }
- composable { KeyguardDisabledFeaturesScreen(::navigateUp) }
+ composable { PasswordScreen(vm, ::navigateUp, ::navigate) }
+ composable {
+ PasswordInfoScreen(vm::getPasswordComplexity, vm::isPasswordComplexitySufficient,
+ vm::isUsingUnifiedPassword, ::navigateUp)
+ }
+ composable {
+ ResetPasswordTokenScreen(vm::getRpTokenState, vm::setRpToken,
+ vm::createActivateRpTokenIntent, vm::clearRpToken, ::navigateUp)
+ }
+ composable { ResetPasswordScreen(vm::resetPassword, ::navigateUp) }
+ composable {
+ RequiredPasswordComplexityScreen(vm::getRequiredPasswordComplexity,
+ vm::setRequiredPasswordComplexity, ::navigateUp)
+ }
+ composable {
+ KeyguardDisabledFeaturesScreen(vm::getKeyguardDisableConfig,
+ vm::setKeyguardDisableConfig, ::navigateUp)
+ }
composable { RequiredPasswordQualityScreen(::navigateUp) }
composable { SettingsScreen(::navigateUp, ::navigate) }
- composable { SettingsOptionsScreen(::navigateUp) }
- composable {
- val theme by vm.theme.collectAsStateWithLifecycle()
- AppearanceScreen(::navigateUp, theme, vm::changeTheme)
+ composable {
+ SettingsOptionsScreen(vm::getDisplayDangerousFeatures, vm::getShortcutsEnabled,
+ vm::setDisplayDangerousFeatures, vm::setShortcutsEnabled, ::navigateUp)
+ }
+ composable {
+ AppearanceScreen(::navigateUp, vm.theme, vm::changeTheme)
+ }
+ composable {
+ AppLockSettingsScreen(vm.getAppLockConfig(), vm::setAppLockConfig, ::navigateUp)
+ }
+ composable {
+ ApiSettings(vm::getApiEnabled, vm::setApiKey, ::navigateUp)
+ }
+ composable {
+ NotificationsScreen(vm.enabledNotifications, vm::getEnabledNotifications,
+ vm::setNotificationEnabled, ::navigateUp)
}
- composable { AppLockSettingsScreen(::navigateUp) }
- composable { ApiSettings(::navigateUp) }
- composable { NotificationsScreen(::navigateUp) }
composable { AboutScreen(::navigateUp) }
}
DisposableEffect(lifecycleOwner) {
@@ -495,9 +734,12 @@ private fun HomeScreen(onNavigate: (Any) -> Unit) {
scrollBehavior = sb
)
},
- contentWindowInsets = WindowInsets.ime
+ contentWindowInsets = adaptiveInsets()
) {
- Column(Modifier.fillMaxSize().padding(it).verticalScroll(rememberScrollState())) {
+ Column(Modifier
+ .fillMaxSize()
+ .padding(it)
+ .verticalScroll(rememberScrollState())) {
if(privilege.device || privilege.profile) {
HomePageItem(R.string.system, R.drawable.android_fill0) { onNavigate(SystemManager) }
HomePageItem(R.string.network, R.drawable.wifi_fill0) { onNavigate(Network) }
@@ -517,7 +759,7 @@ private fun HomeScreen(onNavigate: (Any) -> Unit) {
HomePageItem(R.string.users,R.drawable.manage_accounts_fill0) { onNavigate(Users) }
HomePageItem(R.string.password_and_keyguard, R.drawable.password_fill0) { onNavigate(Password) }
}
- Spacer(Modifier.padding(vertical = 20.dp))
+ Spacer(Modifier.height(BottomPadding))
}
}
}
diff --git a/app/src/main/java/com/bintianqi/owndroid/MyApplication.kt b/app/src/main/java/com/bintianqi/owndroid/MyApplication.kt
index 9372942..d6105c3 100644
--- a/app/src/main/java/com/bintianqi/owndroid/MyApplication.kt
+++ b/app/src/main/java/com/bintianqi/owndroid/MyApplication.kt
@@ -5,12 +5,15 @@ import android.os.Build.VERSION
import org.lsposed.hiddenapibypass.HiddenApiBypass
class MyApplication : Application() {
+ lateinit var myRepo: MyRepository
override fun onCreate() {
super.onCreate()
if (VERSION.SDK_INT >= 28) HiddenApiBypass.setHiddenApiExemptions("")
SP = SharedPrefs(applicationContext)
+ val dbHelper = MyDbHelper(this)
+ myRepo = MyRepository(dbHelper)
Privilege.initialize(applicationContext)
- Privilege.updateStatus()
+ NotificationUtils.createChannels(this)
}
}
diff --git a/app/src/main/java/com/bintianqi/owndroid/MyDbHelper.kt b/app/src/main/java/com/bintianqi/owndroid/MyDbHelper.kt
new file mode 100644
index 0000000..dfa0805
--- /dev/null
+++ b/app/src/main/java/com/bintianqi/owndroid/MyDbHelper.kt
@@ -0,0 +1,37 @@
+package com.bintianqi.owndroid
+
+import android.content.Context
+import android.database.sqlite.SQLiteDatabase
+import android.database.sqlite.SQLiteOpenHelper
+
+class MyDbHelper(context: Context): SQLiteOpenHelper(context, "data", null, 4) {
+ override fun onCreate(db: SQLiteDatabase) {
+ db.execSQL(DHIZUKU_CLIENTS_TABLE)
+ db.execSQL(SECURITY_LOGS_TABLE)
+ db.execSQL(NETWORK_LOGS_TABLE)
+ db.execSQL(APP_GROUPS_TABLE)
+ }
+ override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
+ if (oldVersion < 2) {
+ db.execSQL(SECURITY_LOGS_TABLE)
+ }
+ if (oldVersion < 3) {
+ db.execSQL(NETWORK_LOGS_TABLE)
+ }
+ if (oldVersion < 4) {
+ db.execSQL(APP_GROUPS_TABLE)
+ }
+ }
+ companion object {
+ const val DHIZUKU_CLIENTS_TABLE = "CREATE TABLE dhizuku_clients (uid INTEGER PRIMARY KEY," +
+ "signature TEXT, permissions TEXT)"
+ const val SECURITY_LOGS_TABLE = "CREATE TABLE security_logs (id INTEGER, tag INTEGER," +
+ "level INTEGER, time INTEGER, data TEXT)"
+ const val NETWORK_LOGS_TABLE = "CREATE TABLE network_logs (id INTEGER, package INTEGER," +
+ "time INTEGER, type TEXT, host TEXT, count INTEGER, addresses TEXT," +
+ "address TEXT, port INTEGER)"
+ const val APP_GROUPS_TABLE = "CREATE TABLE app_groups(" +
+ "id INTEGER PRIMARY KEY AUTOINCREMENT," +
+ "name TEXT, apps TEXT)"
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/bintianqi/owndroid/MyRepository.kt b/app/src/main/java/com/bintianqi/owndroid/MyRepository.kt
new file mode 100644
index 0000000..62f7c57
--- /dev/null
+++ b/app/src/main/java/com/bintianqi/owndroid/MyRepository.kt
@@ -0,0 +1,251 @@
+package com.bintianqi.owndroid
+
+import android.app.admin.SecurityLog
+import android.content.ContentValues
+import android.database.DatabaseUtils
+import android.database.sqlite.SQLiteDatabase
+import android.os.Build.VERSION
+import androidx.annotation.RequiresApi
+import androidx.core.database.getIntOrNull
+import androidx.core.database.getLongOrNull
+import androidx.core.database.getStringOrNull
+import com.bintianqi.owndroid.dpm.AppGroup
+import com.bintianqi.owndroid.dpm.NetworkLog
+import com.bintianqi.owndroid.dpm.SecurityEvent
+import com.bintianqi.owndroid.dpm.SecurityEventWithData
+import com.bintianqi.owndroid.dpm.transformSecurityEventData
+import kotlinx.serialization.json.ClassDiscriminatorMode
+import kotlinx.serialization.json.Json
+import java.io.OutputStream
+
+class MyRepository(val dbHelper: MyDbHelper) {
+ fun getDhizukuClients(): List {
+ val list = mutableListOf()
+ dbHelper.readableDatabase.rawQuery("SELECT * FROM dhizuku_clients", null).use { cursor ->
+ while (cursor.moveToNext()) {
+ list += DhizukuClientInfo(
+ cursor.getInt(0), cursor.getString(1),
+ cursor.getString(2).split(",").filter { it.isNotEmpty() }
+ )
+ }
+ }
+ return list
+ }
+ fun checkDhizukuClientPermission(uid: Int, signature: String?, permission: String): Boolean {
+ val cursor = if (signature == null) {
+ dbHelper.readableDatabase.rawQuery(
+ "SELECT permissions FROM dhizuku_clients WHERE uid = $uid AND signature IS NULL",
+ null
+ )
+ } else {
+ dbHelper.readableDatabase.rawQuery(
+ "SELECT permissions FROM dhizuku_clients WHERE uid = $uid AND signature = ?",
+ arrayOf(signature)
+ )
+ }
+ return cursor.use {
+ it.moveToNext() && permission in it.getString(0).split(",")
+ }
+ }
+ fun setDhizukuClient(info: DhizukuClientInfo) {
+ val cv = ContentValues()
+ cv.put("uid", info.uid)
+ cv.put("signature", info.signature)
+ cv.put("permissions", info.permissions.joinToString(","))
+ dbHelper.writableDatabase.insertWithOnConflict("dhizuku_clients", null, cv,
+ SQLiteDatabase.CONFLICT_REPLACE)
+ }
+ fun deleteDhizukuClient(info: DhizukuClientInfo) {
+ dbHelper.writableDatabase.delete("dhizuku_clients", "uid = ${info.uid}", null)
+ }
+
+ fun getSecurityLogsCount(): Long {
+ return DatabaseUtils.queryNumEntries(dbHelper.readableDatabase, "security_logs")
+ }
+ @RequiresApi(24)
+ fun writeSecurityLogs(events: List) {
+ val db = dbHelper.writableDatabase
+ val json = Json {
+ classDiscriminatorMode = ClassDiscriminatorMode.NONE
+ }
+ val statement = db.compileStatement("INSERT INTO security_logs VALUES (?, ?, ?, ?, ?)")
+ db.beginTransaction()
+ events.forEach { event ->
+ try {
+ if (VERSION.SDK_INT >= 28) {
+ statement.bindLong(1, event.id)
+ statement.bindLong(3, event.logLevel.toLong())
+ } else {
+ statement.bindNull(1)
+ statement.bindNull(3)
+ }
+ statement.bindLong(2, event.tag.toLong())
+ statement.bindLong(4, event.timeNanos / 1000000)
+ val dataObject = transformSecurityEventData(event.tag, event.data)
+ if (dataObject == null) {
+ statement.bindNull(5)
+ } else {
+ statement.bindString(5, json.encodeToString(dataObject))
+ }
+ statement.executeInsert()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ } finally {
+ statement.clearBindings()
+ }
+ }
+ db.setTransactionSuccessful()
+ db.endTransaction()
+ statement.close()
+ }
+ fun exportSecurityLogs(stream: OutputStream) {
+ var offset = 0
+ val json = Json {
+ explicitNulls = false
+ }
+ var addComma = false
+ val bw = stream.bufferedWriter()
+ bw.write("[")
+ while (true) {
+ dbHelper.readableDatabase.rawQuery(
+ "SELECT * FROM security_logs LIMIT ? OFFSET ?",
+ arrayOf(100.toString(), offset.toString())
+ ).use { cursor ->
+ if (cursor.count == 0) {
+ break
+ }
+ while (cursor.moveToNext()) {
+ if (addComma) bw.write(",")
+ addComma = true
+ val event = SecurityEvent(
+ cursor.getLong(0), cursor.getInt(1), cursor.getInt(2), cursor.getLong(3),
+ cursor.getStringOrNull(4)?.let { json.decodeFromString(it) }
+ )
+ bw.write(json.encodeToString(event))
+ }
+ offset += 100
+ }
+ }
+ bw.write("]")
+ bw.close()
+ }
+ @RequiresApi(24)
+ fun exportPRSecurityLogs(logs: List, stream: OutputStream) {
+ val bw = stream.bufferedWriter()
+ bw.write("[")
+ val json = Json {
+ explicitNulls = false
+ classDiscriminatorMode = ClassDiscriminatorMode.NONE
+ }
+ var addComma = false
+ logs.forEach { log ->
+ try {
+ if (addComma) bw.write(",")
+ addComma = true
+ val event = SecurityEventWithData(
+ if (VERSION.SDK_INT >= 28) log.id else null, log.tag,
+ if (VERSION.SDK_INT >= 28) log.logLevel else null, log.timeNanos / 1000000,
+ transformSecurityEventData(log.tag, log.data)
+ )
+ bw.write(json.encodeToString(event))
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ bw.write("]")
+ bw.close()
+ }
+ fun deleteSecurityLogs() {
+ dbHelper.writableDatabase.execSQL("DELETE FROM security_logs")
+ }
+
+ fun getNetworkLogsCount(): Long {
+ return DatabaseUtils.queryNumEntries(dbHelper.readableDatabase, "network_logs")
+ }
+ fun writeNetworkLogs(logs: List) {
+ val db = dbHelper.writableDatabase
+ val statement = db.compileStatement(
+ "INSERT INTO network_logs VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
+ )
+ db.beginTransaction()
+ logs.forEach { event ->
+ if (event.id == null) statement.bindNull(1)
+ else statement.bindLong(1, event.id)
+ statement.bindString(2, event.packageName)
+ statement.bindLong(3, event.time)
+ statement.bindString(4, event.type)
+ if (event.host == null) statement.bindNull(5)
+ else statement.bindString(5, event.host)
+ if (event.count == null) statement.bindNull(6)
+ else statement.bindLong(6, event.count.toLong())
+ if (event.addresses == null) statement.bindNull(7)
+ else statement.bindString(7, event.addresses.joinToString(","))
+ if (event.address == null) statement.bindNull(8)
+ else statement.bindString(8, event.address)
+ if (event.port == null) statement.bindNull(9)
+ else statement.bindLong(9, event.port.toLong())
+ statement.executeInsert()
+ statement.clearBindings()
+ }
+ db.setTransactionSuccessful()
+ db.endTransaction()
+ statement.close()
+ }
+ fun exportNetworkLogs(stream: OutputStream) {
+ val bw = stream.bufferedWriter()
+ val json = Json {
+ explicitNulls = false
+ }
+ var offset = 0
+ var addComma = false
+ bw.write("[")
+ while (true) {
+ val cursor = dbHelper.readableDatabase.rawQuery(
+ "SELECT * FROM network_logs LIMIT ? OFFSET ?",
+ arrayOf(100.toString(), offset.toString())
+ )
+ if (cursor.count == 0) break
+ while (cursor.moveToNext()) {
+ if (addComma) bw.write(",")
+ addComma = true
+ val log = NetworkLog(
+ cursor.getLongOrNull(0), cursor.getString(1), cursor.getLong(2),
+ cursor.getString(3), cursor.getStringOrNull(4), cursor.getIntOrNull(5),
+ cursor.getStringOrNull(6)?.split(',')?.filter { it.isNotEmpty() },
+ cursor.getStringOrNull(7), cursor.getIntOrNull(8)
+ )
+ bw.write(json.encodeToString(log))
+ offset += 100
+ }
+ cursor.close()
+ }
+ bw.write("]")
+ bw.close()
+ }
+ fun deleteNetworkLogs() {
+ dbHelper.writableDatabase.execSQL("DELETE FROM network_logs")
+ }
+
+ fun getAppGroups(): List {
+ val list = mutableListOf()
+ dbHelper.readableDatabase.rawQuery("SELECT * FROM app_groups", null).use {
+ while (it.moveToNext()) {
+ list += AppGroup(it.getInt(0), it.getString(1), it.getString(2).split(','))
+ }
+ }
+ return list
+ }
+ fun setAppGroup(id: Int?, name: String, apps: List) {
+ val cv = ContentValues()
+ cv.put("name", name)
+ cv.put("apps", apps.joinToString(","))
+ if (id == null) {
+ dbHelper.writableDatabase.insert("app_groups", null, cv)
+ } else {
+ dbHelper.writableDatabase.update("app_groups", cv, "id = ?", arrayOf(id.toString()))
+ }
+ }
+ fun deleteAppGroup(id: Int) {
+ dbHelper.writableDatabase.delete("app_groups", "id = ?", arrayOf(id.toString()))
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt
index db9be42..654ee05 100644
--- a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt
+++ b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt
@@ -1,10 +1,134 @@
package com.bintianqi.owndroid
+import android.accounts.Account
+import android.annotation.SuppressLint
+import android.app.ActivityOptions
import android.app.Application
+import android.app.KeyguardManager
+import android.app.PendingIntent
+import android.app.admin.DeviceAdminInfo
+import android.app.admin.DeviceAdminReceiver
+import android.app.admin.DevicePolicyManager
+import android.app.admin.DevicePolicyManager.InstallSystemUpdateCallback
+import android.app.admin.FactoryResetProtectionPolicy
+import android.app.admin.IDevicePolicyManager
+import android.app.admin.PackagePolicy
+import android.app.admin.PreferentialNetworkServiceConfig
+import android.app.admin.SecurityLog
+import android.app.admin.SystemUpdateInfo
+import android.app.admin.SystemUpdatePolicy
+import android.app.admin.WifiSsidPolicy
+import android.app.usage.NetworkStats
+import android.app.usage.NetworkStatsManager
+import android.content.BroadcastReceiver
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageInstaller
+import android.content.pm.PackageManager
+import android.graphics.Bitmap
+import android.net.IpConfiguration
+import android.net.LinkAddress
+import android.net.ProxyInfo
+import android.net.StaticIpConfiguration
+import android.net.Uri
+import android.net.wifi.WifiConfiguration
+import android.net.wifi.WifiManager
+import android.net.wifi.WifiSsid
+import android.os.Binder
+import android.os.Build.VERSION
+import android.os.HardwarePropertiesManager
+import android.os.UserHandle
+import android.os.UserManager
+import android.telephony.data.ApnSetting
+import androidx.annotation.RequiresApi
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.core.content.ContextCompat
+import androidx.core.graphics.drawable.toDrawable
+import androidx.core.net.toUri
import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.application
+import androidx.lifecycle.viewModelScope
+import com.bintianqi.owndroid.Privilege.DAR
+import com.bintianqi.owndroid.Privilege.DPM
+import com.bintianqi.owndroid.dpm.ACTIVATE_DEVICE_OWNER_COMMAND
+import com.bintianqi.owndroid.dpm.ApnAuthType
+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.AppStatus
+import com.bintianqi.owndroid.dpm.CaCertInfo
+import com.bintianqi.owndroid.dpm.CreateUserResult
+import com.bintianqi.owndroid.dpm.CreateWorkProfileOptions
+import com.bintianqi.owndroid.dpm.DelegatedAdmin
+import com.bintianqi.owndroid.dpm.DeviceAdmin
+import com.bintianqi.owndroid.dpm.FrpPolicyInfo
+import com.bintianqi.owndroid.dpm.HardwareProperties
+import com.bintianqi.owndroid.dpm.IntentFilterDirection
+import com.bintianqi.owndroid.dpm.IntentFilterOptions
+import com.bintianqi.owndroid.dpm.IpMode
+import com.bintianqi.owndroid.dpm.KeyguardDisableConfig
+import com.bintianqi.owndroid.dpm.KeyguardDisableMode
+import com.bintianqi.owndroid.dpm.NetworkStatsData
+import com.bintianqi.owndroid.dpm.NetworkStatsTarget
+import com.bintianqi.owndroid.dpm.PasswordComplexity
+import com.bintianqi.owndroid.dpm.PendingSystemUpdateInfo
+import com.bintianqi.owndroid.dpm.PreferentialNetworkServiceInfo
+import com.bintianqi.owndroid.dpm.PrivateDnsConfiguration
+import com.bintianqi.owndroid.dpm.PrivateDnsMode
+import com.bintianqi.owndroid.dpm.ProxyMode
+import com.bintianqi.owndroid.dpm.ProxyType
+import com.bintianqi.owndroid.dpm.QueryNetworkStatsParams
+import com.bintianqi.owndroid.dpm.RecommendedProxyConf
+import com.bintianqi.owndroid.dpm.RpTokenState
+import com.bintianqi.owndroid.dpm.SsidPolicy
+import com.bintianqi.owndroid.dpm.SsidPolicyType
+import com.bintianqi.owndroid.dpm.SystemOptionsStatus
+import com.bintianqi.owndroid.dpm.SystemUpdatePolicyInfo
+import com.bintianqi.owndroid.dpm.UserIdentifier
+import com.bintianqi.owndroid.dpm.UserInformation
+import com.bintianqi.owndroid.dpm.UserOperationType
+import com.bintianqi.owndroid.dpm.WifiInfo
+import com.bintianqi.owndroid.dpm.WifiSecurity
+import com.bintianqi.owndroid.dpm.WifiStatus
+import com.bintianqi.owndroid.dpm.activateOrgProfileCommand
+import com.bintianqi.owndroid.dpm.delegatedScopesList
+import com.bintianqi.owndroid.dpm.doUserOperationWithContext
+import com.bintianqi.owndroid.dpm.getPackageInstaller
+import com.bintianqi.owndroid.dpm.handlePrivilegeChange
+import com.bintianqi.owndroid.dpm.isValidPackageName
+import com.bintianqi.owndroid.dpm.parsePackageInstallerMessage
+import com.bintianqi.owndroid.dpm.runtimePermissions
+import com.bintianqi.owndroid.dpm.temperatureTypes
+import com.rosan.dhizuku.api.Dhizuku
+import com.rosan.dhizuku.api.DhizukuRequestPermissionListener
+import com.topjohnwu.superuser.Shell
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.BufferOverflow
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.net.InetAddress
+import java.security.MessageDigest
+import java.security.cert.CertificateException
+import java.security.cert.CertificateFactory
+import java.security.cert.X509Certificate
+import java.time.ZoneId
+import java.time.ZonedDateTime
+import java.util.concurrent.Executors
+import kotlin.reflect.jvm.jvmErasure
class MyViewModel(application: Application): AndroidViewModel(application) {
+ val myRepo = getApplication().myRepo
+ val PM = application.packageManager
+
val theme = MutableStateFlow(ThemeSettings(SP.materialYou, SP.darkTheme, SP.blackTheme))
fun changeTheme(newTheme: ThemeSettings) {
theme.value = newTheme
@@ -12,6 +136,1770 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
SP.darkTheme = newTheme.darkTheme
SP.blackTheme = newTheme.blackTheme
}
+ fun getDisplayDangerousFeatures(): Boolean {
+ return SP.displayDangerousFeatures
+ }
+ fun getShortcutsEnabled(): Boolean {
+ return SP.shortcuts
+ }
+ fun setDisplayDangerousFeatures(state: Boolean) {
+ SP.displayDangerousFeatures = state
+ }
+ fun setShortcutsEnabled(enabled: Boolean) {
+ SP.shortcuts = enabled
+ ShortcutUtils.setAllShortcuts(application, enabled)
+ }
+ fun getAppLockConfig(): AppLockConfig {
+ val passwordHash = SP.lockPasswordHash
+ return AppLockConfig(passwordHash?.ifEmpty { null }, SP.biometricsUnlock, SP.lockWhenLeaving)
+ }
+ fun setAppLockConfig(config: AppLockConfig) {
+ SP.lockPasswordHash = if (config.password == null) {
+ ""
+ } else {
+ config.password.hash()
+ }
+ SP.biometricsUnlock = config.biometrics
+ SP.lockWhenLeaving = config.whenLeaving
+ }
+ fun getApiEnabled(): Boolean {
+ return SP.apiKeyHash?.isNotEmpty() ?: false
+ }
+ fun setApiKey(key: String) {
+ SP.apiKeyHash = if (key.isEmpty()) "" else key.hash()
+ }
+ val enabledNotifications = MutableStateFlow(emptyList())
+ fun getEnabledNotifications() {
+ val list = SP.notifications?.split(',')?.mapNotNull { it.toIntOrNull() }
+ enabledNotifications.value = list ?: NotificationType.entries.map { it.id }
+ }
+ fun setNotificationEnabled(type: NotificationType, enabled: Boolean) {
+ enabledNotifications.update { list ->
+ if (enabled) list.plus(type.id) else list.minus(type.id)
+ }
+ SP.notifications = enabledNotifications.value.joinToString(",") { it.toString() }
+ }
+
+ val chosenPackage = Channel(1, BufferOverflow.DROP_LATEST)
+
+ val installedPackages = MutableStateFlow(emptyList())
+ val refreshPackagesProgress = MutableStateFlow(0F)
+ fun refreshPackageList() {
+ viewModelScope.launch(Dispatchers.IO) {
+ installedPackages.value = emptyList()
+ val apps = PM.getInstalledApplications(getInstalledAppsFlags)
+ apps.forEachIndexed { index, info ->
+ installedPackages.update {
+ it + getAppInfo(info)
+ }
+ refreshPackagesProgress.value = (index + 1).toFloat() / apps.size
+ }
+ }
+ }
+ fun getAppInfo(info: ApplicationInfo) =
+ AppInfo(info.packageName, info.loadLabel(PM).toString(), info.loadIcon(PM), info.flags)
+ fun getAppInfo(name: String): AppInfo {
+ return try {
+ getAppInfo(PM.getApplicationInfo(name, getInstalledAppsFlags))
+ } catch (_: PackageManager.NameNotFoundException) {
+ AppInfo(name, "???", Color.Transparent.toArgb().toDrawable(), 0)
+ }
+ }
+
+ val suspendedPackages = MutableStateFlow(emptyList())
+ @RequiresApi(24)
+ fun getSuspendedPackaged() {
+ val packages = PM.getInstalledApplications(getInstalledAppsFlags).filter {
+ DPM.isPackageSuspended(DAR, it.packageName)
+ }
+ suspendedPackages.value = packages.map { getAppInfo(it) }
+ }
+ @RequiresApi(24)
+ fun setPackageSuspended(name: String, status: Boolean): Boolean {
+ val result = DPM.setPackagesSuspended(DAR, arrayOf(name), status)
+ getSuspendedPackaged()
+ return result.isEmpty()
+ }
+
+ val hiddenPackages = MutableStateFlow(emptyList())
+ fun getHiddenPackages() {
+ hiddenPackages.value = PM.getInstalledApplications(getInstalledAppsFlags).filter {
+ DPM.isApplicationHidden(DAR, it.packageName)
+ }.map { getAppInfo(it) }
+ }
+ fun setPackageHidden(name: String, status: Boolean): Boolean {
+ val result = DPM.setApplicationHidden(DAR, name, status)
+ getHiddenPackages()
+ return result
+ }
+
+ // Uninstall blocked packages
+ val ubPackages = MutableStateFlow(emptyList())
+ fun getUbPackages() {
+ ubPackages.value = PM.getInstalledApplications(getInstalledAppsFlags).filter {
+ DPM.isUninstallBlocked(DAR, it.packageName)
+ }.map { getAppInfo(it) }
+ }
+ fun setPackageUb(name: String, status: Boolean) {
+ DPM.setUninstallBlocked(DAR, name, status)
+ getUbPackages()
+ }
+
+ // User control disabled packages
+ val ucdPackages = MutableStateFlow(emptyList())
+ @RequiresApi(30)
+ fun getUcdPackages() {
+ ucdPackages.value = DPM.getUserControlDisabledPackages(DAR).distinct().map {
+ getAppInfo(it)
+ }
+ }
+ @RequiresApi(30)
+ fun setPackageUcd(name: String, status: Boolean) {
+ DPM.setUserControlDisabledPackages(
+ DAR,
+ ucdPackages.value.map { it.name }.run { if (status) plus(name) else minus(name) }
+ )
+ getUcdPackages()
+ }
+
+ val packagePermissions = MutableStateFlow(emptyMap())
+ @RequiresApi(23)
+ fun getPackagePermissions(name: String) {
+ if (name.isValidPackageName) {
+ packagePermissions.value = runtimePermissions.associate {
+ it.id to DPM.getPermissionGrantState(DAR, name, it.id)
+ }
+ } else {
+ packagePermissions.value = emptyMap()
+ }
+ }
+ @RequiresApi(23)
+ fun setPackagePermission(name: String, permission: String, status: Int): Boolean {
+ val result = DPM.setPermissionGrantState(DAR, name, permission, status)
+ getPackagePermissions(name)
+ return result
+ }
+
+ // Metered data disabled packages
+ val mddPackages = MutableStateFlow(emptyList())
+ @RequiresApi(28)
+ fun getMddPackages() {
+ 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) }
+ )
+ getMddPackages()
+ return result.isEmpty()
+ }
+
+ // Keep uninstalled packages
+ val kuPackages = MutableStateFlow(emptyList())
+ @RequiresApi(28)
+ fun getKuPackages() {
+ kuPackages.value = DPM.getKeepUninstalledPackages(DAR)?.distinct()?.map { getAppInfo(it) } ?: emptyList()
+ }
+ @RequiresApi(28)
+ fun setPackageKu(name: String, status: Boolean) {
+ DPM.setKeepUninstalledPackages(
+ DAR, kuPackages.value.map { it.name }.run { if (status) plus(name) else minus(name) }
+ )
+ getKuPackages()
+ }
+
+ // Cross profile packages
+ val cpPackages = MutableStateFlow(emptyList())
+ @RequiresApi(30)
+ fun getCpPackages() {
+ cpPackages.value = DPM.getCrossProfilePackages(DAR).map { getAppInfo(it) }
+ }
+ @RequiresApi(30)
+ fun setPackageCp(name: String, status: Boolean) {
+ DPM.setCrossProfilePackages(
+ DAR,
+ cpPackages.value.map { it.name }.toSet().run { if (status) plus(name) else minus(name) }
+ )
+ getCpPackages()
+ }
+
+ // Cross-profile widget providers
+ val cpwProviders = MutableStateFlow(emptyList())
+ 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)
+ }
+ getCpwProviders()
+ return result
+ }
+
+ @RequiresApi(28)
+ fun clearAppData(name: String, callback: (Boolean) -> Unit) {
+ DPM.clearApplicationUserData(DAR, name, Executors.newSingleThreadExecutor()) { _, result ->
+ callback(result)
+ }
+ }
+
+ fun uninstallPackage(packageName: String, onComplete: (String?) -> Unit) {
+ val receiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ val statusExtra = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 999)
+ if(statusExtra == PackageInstaller.STATUS_PENDING_USER_ACTION) {
+ @SuppressWarnings("UnsafeIntentLaunch")
+ context.startActivity(intent.getParcelableExtra(Intent.EXTRA_INTENT) as Intent?)
+ } else {
+ context.unregisterReceiver(this)
+ if(statusExtra == PackageInstaller.STATUS_SUCCESS) {
+ onComplete(null)
+ } else {
+ onComplete(parsePackageInstallerMessage(context, intent))
+ }
+ }
+ }
+ }
+ ContextCompat.registerReceiver(
+ application, receiver, IntentFilter(AppInstallerViewModel.ACTION), null,
+ null, ContextCompat.RECEIVER_EXPORTED
+ )
+ val pi = if(VERSION.SDK_INT >= 34) {
+ PendingIntent.getBroadcast(
+ application, 0, Intent(AppInstallerViewModel.ACTION),
+ PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT or PendingIntent.FLAG_MUTABLE
+ ).intentSender
+ } else {
+ PendingIntent.getBroadcast(application, 0, Intent(AppInstallerViewModel.ACTION), PendingIntent.FLAG_MUTABLE).intentSender
+ }
+ application.getPackageInstaller().uninstall(packageName, pi)
+ }
+
+ @RequiresApi(28)
+ fun installExistingApp(name: String): Boolean {
+ return DPM.installExistingPackage(DAR, name)
+ }
+
+ // Credential manager policy
+ val cmPackages = MutableStateFlow(emptyList())
+ @RequiresApi(34)
+ fun getCmPolicy(): Int {
+ return DPM.credentialManagerPolicy?.let { policy ->
+ cmPackages.value = policy.packageNames.distinct().map { getAppInfo(it) }
+ policy.policyType
+ } ?: -1
+ }
+ fun setCmPackage(name: String, status: Boolean) {
+ cmPackages.update { list ->
+ if (status) list + getAppInfo(name) else list.filter { it.name != name }
+ }
+ }
+ @RequiresApi(34)
+ fun setCmPolicy(type: Int) {
+ DPM.credentialManagerPolicy = if (type != -1 && cmPackages.value.isNotEmpty()) {
+ PackagePolicy(type, cmPackages.value.map { it.name }.toSet())
+ } else null
+ getCmPolicy()
+ }
+
+ // Permitted input method
+ val pimPackages = MutableStateFlow(emptyList())
+ fun getPimPackages(): Boolean {
+ return DPM.getPermittedInputMethods(DAR).let { packages ->
+ pimPackages.value = packages?.distinct()?.map { getAppInfo(it) } ?: emptyList()
+ packages == null
+ }
+ }
+ fun setPimPackage(name: String, status: Boolean) {
+ pimPackages.update { packages ->
+ if (status) packages + getAppInfo(name) else packages.filter { it.name != name }
+ }
+ }
+ fun setPimPolicy(allowAll: Boolean): Boolean {
+ val result = DPM.setPermittedInputMethods(
+ DAR, if (allowAll) null else pimPackages.value.map { it.name })
+ getPimPackages()
+ return result
+ }
+
+ // Permitted accessibility services
+ val pasPackages = MutableStateFlow(emptyList())
+ fun getPasPackages(): Boolean {
+ return DPM.getPermittedAccessibilityServices(DAR).let { packages ->
+ pasPackages.value = packages?.distinct()?.map { getAppInfo(it) } ?: emptyList()
+ packages == null
+ }
+ }
+ fun setPasPackage(name: String, status: Boolean) {
+ pasPackages.update { packages ->
+ if (status) packages + getAppInfo(name) else packages.filter { it.name != name }
+ }
+ }
+ fun setPasPolicy(allowAll: Boolean): Boolean {
+ val result = DPM.setPermittedAccessibilityServices(
+ DAR, if (allowAll) null else pasPackages.value.map { it.name })
+ getPasPackages()
+ return result
+ }
+
+ fun enableSystemApp(name: String) {
+ DPM.enableSystemApp(DAR, name)
+ }
+
+ val appStatus = MutableStateFlow(AppStatus(false, false, false, false, false, false))
+ fun getAppStatus(name: String) {
+ appStatus.value = AppStatus(
+ if (VERSION.SDK_INT >= 24) DPM.isPackageSuspended(DAR, name) else false,
+ DPM.isApplicationHidden(DAR, name),
+ 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
+ )
+ }
+ // 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)) }
+ }
+ fun adSetPackageHidden(name: String, status: Boolean) {
+ DPM.setApplicationHidden(DAR, name, status)
+ appStatus.update { it.copy(hide = DPM.isApplicationHidden(DAR, name)) }
+ }
+ fun adSetPackageUb(name: String, status: Boolean) {
+ DPM.setUninstallBlocked(DAR, name, status)
+ appStatus.update { it.copy(uninstallBlocked = DPM.isUninstallBlocked(DAR, name)) }
+ }
+ @RequiresApi(30)
+ fun adSetPackageUcd(name: String, status: Boolean) {
+ DPM.setUserControlDisabledPackages(DAR,
+ DPM.getUserControlDisabledPackages(DAR).run { if (status) plus(name) else minus(name) })
+ appStatus.update {
+ it.copy(userControlDisabled = name in DPM.getUserControlDisabledPackages(DAR))
+ }
+ }
+ @RequiresApi(28)
+ fun adSetPackageMdd(name: String, status: Boolean) {
+ DPM.setMeteredDataDisabledPackages(DAR,
+ DPM.getMeteredDataDisabledPackages(DAR).run { if (status) plus(name) else minus(name) })
+ appStatus.update {
+ it.copy(meteredDataDisabled = name in DPM.getMeteredDataDisabledPackages(DAR))
+ }
+ }
+ @RequiresApi(28)
+ fun adSetPackageKu(name: String, status: Boolean) {
+ DPM.setKeepUninstalledPackages(DAR,
+ DPM.getKeepUninstalledPackages(DAR)?.run { if (status) plus(name) else minus(name) } ?: emptyList())
+ appStatus.update {
+ it.copy(keepUninstalled = DPM.getKeepUninstalledPackages(DAR)?.contains(name) == true )
+ }
+ }
+
+ @RequiresApi(34)
+ fun setDefaultDialer(name: String): Boolean {
+ return try {
+ DPM.setDefaultDialerApplication(name)
+ true
+ } catch (e: IllegalArgumentException) {
+ e.printStackTrace()
+ false
+ }
+ }
+
+ val appGroups = MutableStateFlow(emptyList())
+ init {
+ getAppGroups()
+ }
+ fun getAppGroups() {
+ appGroups.value = myRepo.getAppGroups()
+ }
+ fun setAppGroup(id: Int?, name: String, apps: List) {
+ myRepo.setAppGroup(id, name, apps)
+ getAppGroups()
+ }
+ fun deleteAppGroup(id: Int) {
+ myRepo.deleteAppGroup(id)
+ appGroups.update { group ->
+ group.filter { it.id != id }
+ }
+ }
+
+ @RequiresApi(24)
+ fun reboot() {
+ DPM.reboot(DAR)
+ }
+ @RequiresApi(24)
+ fun requestBugReport(): Boolean {
+ return try {
+ DPM.requestBugreport(DAR)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ false
+ }
+ }
+ @SuppressLint("PrivateApi")
+ @RequiresApi(24)
+ fun getOrgName(): String {
+ return try {
+ DPM.getOrganizationName(DAR)?.toString() ?: ""
+ } catch (_: Exception) {
+ try {
+ val method = DevicePolicyManager::class.java.getDeclaredMethod(
+ "getDeviceOwnerOrganizationName"
+ )
+ method.isAccessible = true
+ (method.invoke(DPM) as CharSequence).toString()
+ } catch (_: Exception) {
+ ""
+ }
+ }
+ }
+ @RequiresApi(24)
+ fun setOrgName(name: String) {
+ DPM.setOrganizationName(DAR, name)
+ }
+ @RequiresApi(31)
+ fun setOrgId(id: String): Boolean {
+ return try {
+ DPM.setOrganizationId(id)
+ true
+ } catch (_: IllegalStateException) {
+ false
+ }
+ }
+ @RequiresApi(31)
+ fun getEnrollmentSpecificId(): String {
+ return DPM.enrollmentSpecificId
+ }
+ val systemOptionsStatus = MutableStateFlow(SystemOptionsStatus())
+ fun getSystemOptionsStatus() {
+ val privilege = Privilege.status.value
+ systemOptionsStatus.value = SystemOptionsStatus(
+ cameraDisabled = DPM.getCameraDisabled(null),
+ screenCaptureDisabled = DPM.getScreenCaptureDisabled(null),
+ 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 })
+ DPM.getAutoTimeEnabled(DAR) else false,
+ autoTimeZoneEnabled = if (VERSION.SDK_INT >= 30 && privilege.run { device || 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)
+ DPM.getBluetoothContactSharingDisabled(DAR) else false,
+ commonCriteriaMode = if (VERSION.SDK_INT >= 30) 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
+ )
+ }
+ fun setCameraDisabled(disabled: Boolean) {
+ DPM.setCameraDisabled(DAR, disabled)
+ ShortcutUtils.setShortcut(application, MyShortcut.DisableCamera, !disabled)
+ systemOptionsStatus.update { it.copy(cameraDisabled = DPM.getCameraDisabled(null)) }
+ }
+ fun setScreenCaptureDisabled(disabled: Boolean) {
+ DPM.setScreenCaptureDisabled(DAR, disabled)
+ systemOptionsStatus.update {
+ 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) }
+ }
+ @RequiresApi(30)
+ fun setAutoTimeEnabled(enabled: Boolean) {
+ DPM.setAutoTimeEnabled(DAR, enabled)
+ systemOptionsStatus.update { it.copy(autoTimeEnabled = DPM.getAutoTimeEnabled(DAR)) }
+ }
+ @RequiresApi(30)
+ fun setAutoTimeZoneEnabled(enabled: Boolean) {
+ DPM.setAutoTimeZoneEnabled(DAR, enabled)
+ systemOptionsStatus.update {
+ it.copy(autoTimeZoneEnabled = DPM.getAutoTimeZoneEnabled(DAR))
+ }
+ }
+ @Suppress("DEPRECATION")
+ fun setAutoTimeRequired(required: Boolean) {
+ DPM.setAutoTimeRequired(DAR, required)
+ systemOptionsStatus.update { it.copy(autoTimeRequired = DPM.autoTimeRequired) }
+ }
+ fun setMasterVolumeMuted(muted: Boolean) {
+ DPM.setMasterVolumeMuted(DAR, muted)
+ ShortcutUtils.setShortcut(application, MyShortcut.Mute, !muted)
+ systemOptionsStatus.update { it.copy(masterVolumeMuted = DPM.isMasterVolumeMuted(DAR)) }
+ }
+ @RequiresApi(26)
+ fun setBackupServiceEnabled(enabled: Boolean) {
+ DPM.setBackupServiceEnabled(DAR, enabled)
+ systemOptionsStatus.update {
+ it.copy(backupServiceEnabled = DPM.isBackupServiceEnabled(DAR))
+ }
+ }
+ @RequiresApi(23)
+ fun setBtContactSharingDisabled(disabled: Boolean) {
+ DPM.setBluetoothContactSharingDisabled(DAR, disabled)
+ systemOptionsStatus.update {
+ it.copy(btContactSharingDisabled = DPM.getBluetoothContactSharingDisabled(DAR))
+ }
+ }
+ @RequiresApi(30)
+ fun setCommonCriteriaModeEnabled(enabled: Boolean) {
+ DPM.setCommonCriteriaModeEnabled(DAR, enabled)
+ systemOptionsStatus.update {
+ it.copy(commonCriteriaMode = DPM.isCommonCriteriaModeEnabled(DAR))
+ }
+ }
+ @RequiresApi(31)
+ fun setUsbSignalEnabled(enabled: Boolean) {
+ DPM.isUsbDataSignalingEnabled = enabled
+ systemOptionsStatus.update { it.copy(usbSignalEnabled = DPM.isUsbDataSignalingEnabled) }
+ }
+ @RequiresApi(23)
+ fun setKeyguardDisabled(disabled: Boolean): Boolean {
+ return DPM.setKeyguardDisabled(DAR, disabled)
+ }
+ fun lockScreen(evictKey: Boolean) {
+ if (VERSION.SDK_INT >= 26 && Privilege.status.value.work) {
+ DPM.lockNow(if (evictKey) DevicePolicyManager.FLAG_EVICT_CREDENTIAL_ENCRYPTION_KEY else 0)
+ } else {
+ DPM.lockNow()
+ }
+ }
+ val hardwareProperties = MutableStateFlow(HardwareProperties())
+ var hpRefreshInterval = 1000L
+ fun setHpRefreshInterval(interval: Float) {
+ hpRefreshInterval = (interval * 1000).toLong()
+ }
+ @RequiresApi(24)
+ suspend fun getHardwareProperties() {
+ val hpm = application.getSystemService(HardwarePropertiesManager::class.java)
+ while (true) {
+ val properties = HardwareProperties(
+ temperatureTypes.map { (type, _) ->
+ type to hpm.getDeviceTemperatures(type, HardwarePropertiesManager.TEMPERATURE_CURRENT).toList()
+ }.toMap(),
+ hpm.cpuUsages.map { it.active to it.total },
+ hpm.fanSpeeds.toList()
+ )
+ if (properties.cpuUsages.isEmpty() && properties.fanSpeeds.isEmpty() &&
+ properties.temperatures.isEmpty()) {
+ break
+ }
+ hardwareProperties.value = properties
+ delay(hpRefreshInterval)
+ }
+ }
+ @RequiresApi(28)
+ fun setTime(time: Long, useCurrentTz: Boolean): Boolean {
+ val offset = if (useCurrentTz) {
+ ZonedDateTime.now(ZoneId.systemDefault()).offset.totalSeconds * 1000L
+ } else 0L
+ return DPM.setTime(DAR, time - offset)
+ }
+ @RequiresApi(28)
+ fun setTimeZone(tz: String): Boolean {
+ return DPM.setTimeZone(DAR, tz)
+ }
+ @RequiresApi(36)
+ fun getAutoTimePolicy(): Int {
+ return DPM.autoTimePolicy
+ }
+ @RequiresApi(36)
+ fun setAutoTimePolicy(policy: Int) {
+ DPM.autoTimePolicy = policy
+ }
+ @RequiresApi(36)
+ fun getAutoTimeZonePolicy(): Int {
+ return DPM.autoTimeZonePolicy
+ }
+ @RequiresApi(36)
+ fun setAutoTimeZonePolicy(policy: Int) {
+ DPM.autoTimeZonePolicy = policy
+ }
+ @RequiresApi(35)
+ fun getContentProtectionPolicy(): Int {
+ return DPM.getContentProtectionPolicy(DAR)
+ }
+ @RequiresApi(35)
+ 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)
+ }
+ @RequiresApi(34)
+ fun getMtePolicy(): Int {
+ return DPM.mtePolicy
+ }
+ @RequiresApi(34)
+ fun setMtePolicy(policy: Int): Boolean {
+ return try {
+ DPM.mtePolicy = policy
+ true
+ } catch (_: UnsupportedOperationException) {
+ false
+ }
+ }
+ @RequiresApi(31)
+ fun getNsAppPolicy(): Int {
+ return DPM.nearbyAppStreamingPolicy
+ }
+ @RequiresApi(31)
+ fun setNsAppPolicy(policy: Int) {
+ DPM.nearbyAppStreamingPolicy = policy
+ }
+ @RequiresApi(31)
+ fun getNsNotificationPolicy(): Int {
+ return DPM.nearbyNotificationStreamingPolicy
+ }
+ @RequiresApi(31)
+ fun setNsNotificationPolicy(policy: Int) {
+ DPM.nearbyNotificationStreamingPolicy = policy
+ }
+ val lockTaskPackages = MutableStateFlow(emptyList())
+ @RequiresApi(26)
+ fun getLockTaskPackages() {
+ lockTaskPackages.value = DPM.getLockTaskPackages(DAR).map { getAppInfo(it) }
+ }
+ @RequiresApi(26)
+ fun setLockTaskPackage(name: String, status: Boolean) {
+ DPM.setLockTaskPackages(DAR,
+ lockTaskPackages.value.map { it.name }
+ .run { if (status) plus(name) else minus(name) }
+ .toTypedArray()
+ )
+ getLockTaskPackages()
+ }
+ @RequiresApi(28)
+ fun startLockTaskMode(packageName: String, activity: String): Boolean {
+ if (!DPM.isLockTaskPermitted(packageName)) {
+ val list = lockTaskPackages.value.map { it.name } + packageName
+ DPM.setLockTaskPackages(DAR, list.toTypedArray())
+ getLockTaskPackages()
+ }
+ 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)
+ application.startActivity(intent, options.toBundle())
+ return true
+ } else {
+ return false
+ }
+ }
+ @RequiresApi(28)
+ fun getLockTaskFeatures(): Int {
+ return DPM.getLockTaskFeatures(DAR)
+ }
+ @RequiresApi(28)
+ fun setLockTaskFeatures(flags: Int): String? {
+ try {
+ DPM.setLockTaskFeatures(DAR, flags)
+ return null
+ } catch (e: IllegalArgumentException) {
+ return e.message
+ }
+ }
+ val installedCaCerts = MutableStateFlow(emptyList())
+ val selectedCaCert = MutableStateFlow(null)
+ fun getCaCerts() {
+ installedCaCerts.value = DPM.getInstalledCaCerts(DAR).mapNotNull { parseCaCert(it) }
+ }
+ fun selectCaCert(cert: CaCertInfo) {
+ selectedCaCert.value = cert
+ }
+ fun parseCaCert(uri: Uri) {
+ try {
+ application.contentResolver.openInputStream(uri)?.use {
+ selectedCaCert.value = parseCaCert(it.readBytes())
+ }
+ } catch(e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ fun parseCaCert(bytes: ByteArray): CaCertInfo? {
+ val hash = MessageDigest.getInstance("SHA-256").digest(bytes).toHexString()
+ return try {
+ val factory = CertificateFactory.getInstance("X.509")
+ val cert = factory.generateCertificate(bytes.inputStream()) as X509Certificate
+ CaCertInfo(
+ hash, cert.serialNumber.toString(16),
+ cert.issuerX500Principal.name, cert.subjectX500Principal.name,
+ cert.notBefore.time, cert.notAfter.time, bytes
+ )
+ } catch (e: CertificateException) {
+ e.printStackTrace()
+ null
+ }
+ }
+ fun installCaCert(): Boolean {
+ val result = DPM.installCaCert(DAR, selectedCaCert.value!!.bytes)
+ if (result) getCaCerts()
+ return result
+ }
+ fun uninstallCaCert() {
+ DPM.uninstallCaCert(DAR, selectedCaCert.value!!.bytes)
+ getCaCerts()
+ }
+ fun uninstallAllCaCerts() {
+ DPM.uninstallAllUserCaCerts(DAR)
+ getCaCerts()
+ }
+ fun exportCaCert(uri: Uri) {
+ application.contentResolver.openOutputStream(uri)?.use {
+ it.write(selectedCaCert.value!!.bytes)
+ }
+ }
+ val mdAccountTypes = MutableStateFlow(emptyList())
+ fun getMdAccountTypes() {
+ mdAccountTypes.value = DPM.accountTypesWithManagementDisabled?.toList() ?: emptyList()
+ }
+ fun setMdAccountType(type: String, disabled: Boolean) {
+ DPM.setAccountManagementDisabled(DAR, type, disabled)
+ getMdAccountTypes()
+ }
+ @RequiresApi(30)
+ fun getFrpPolicy(): FrpPolicyInfo {
+ return try {
+ val policy = DPM.getFactoryResetProtectionPolicy(DAR)
+ FrpPolicyInfo(
+ true, policy != null, policy?.isFactoryResetProtectionEnabled ?: false,
+ policy?.factoryResetProtectionAccounts ?: emptyList()
+ )
+ } catch (_: UnsupportedOperationException) {
+ FrpPolicyInfo(false, false, false, emptyList())
+ }
+ }
+ @RequiresApi(30)
+ fun setFrpPolicy(info: FrpPolicyInfo) {
+ val policy = if (info.usePolicy) {
+ FactoryResetProtectionPolicy.Builder()
+ .setFactoryResetProtectionEnabled(info.enabled)
+ .setFactoryResetProtectionAccounts(info.accounts)
+ .build()
+ } else null
+ DPM.setFactoryResetProtectionPolicy(DAR, policy)
+ }
+ fun wipeData(wipeDevice: Boolean, flags: Int, reason: String) {
+ if (wipeDevice && VERSION.SDK_INT >= 34) {
+ DPM.wipeDevice(flags)
+ } else {
+ if(VERSION.SDK_INT >= 28 && reason.isNotEmpty()) {
+ DPM.wipeData(flags, reason)
+ } else {
+ DPM.wipeData(flags)
+ }
+ }
+ }
+ @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()
+ SystemUpdatePolicy.TYPE_INSTALL_WINDOWED ->
+ SystemUpdatePolicy.createWindowedInstallPolicy(info.start, info.end)
+ SystemUpdatePolicy.TYPE_POSTPONE -> SystemUpdatePolicy.createPostponeInstallPolicy()
+ else -> null
+ }
+ DPM.setSystemUpdatePolicy(DAR, policy)
+ }
+ @RequiresApi(26)
+ fun getPendingSystemUpdate(): PendingSystemUpdateInfo {
+ val update = DPM.getPendingSystemUpdate(DAR)
+ return PendingSystemUpdateInfo(update != null, update?.receivedTime ?: 0,
+ update?.securityPatchState == SystemUpdateInfo.SECURITY_PATCH_STATE_TRUE)
+ }
+ @RequiresApi(29)
+ fun installSystemUpdate(uri: Uri, callback: (String) -> Unit) {
+ val callback = object: InstallSystemUpdateCallback() {
+ override fun onInstallUpdateError(errorCode: Int, errorMessage: String) {
+ super.onInstallUpdateError(errorCode, errorMessage)
+ val errDetail = when(errorCode) {
+ UPDATE_ERROR_BATTERY_LOW -> R.string.battery_low
+ UPDATE_ERROR_UPDATE_FILE_INVALID -> R.string.update_file_invalid
+ UPDATE_ERROR_INCORRECT_OS_VERSION -> R.string.incorrect_os_ver
+ UPDATE_ERROR_FILE_NOT_FOUND -> R.string.file_not_exist
+ else -> R.string.unknown_error
+ }
+ callback(application.getString(errDetail) + "\n$errorMessage")
+ }
+ }
+ DPM.installSystemUpdate(DAR, uri, application.mainExecutor, callback)
+ }
+ @RequiresApi(24)
+ fun getSecurityLoggingEnabled(): Boolean {
+ return DPM.isSecurityLoggingEnabled(DAR)
+ }
+ @RequiresApi(24)
+ fun setSecurityLoggingEnabled(enabled: Boolean) {
+ DPM.setSecurityLoggingEnabled(DAR, enabled)
+ }
+ fun exportSecurityLogs(uri: Uri, callback: () -> Unit) {
+ viewModelScope.launch(Dispatchers.IO) {
+ application.contentResolver.openOutputStream(uri)?.use {
+ myRepo.exportSecurityLogs(it)
+ }
+ withContext(Dispatchers.Main) {
+ callback()
+ }
+ }
+ }
+ fun getSecurityLogsCount(): Int {
+ return myRepo.getSecurityLogsCount().toInt()
+ }
+ fun deleteSecurityLogs() {
+ myRepo.deleteSecurityLogs()
+ }
+ var preRebootSecurityLogs = emptyList()
+ @RequiresApi(24)
+ fun getPreRebootSecurityLogs(): Boolean {
+ if (preRebootSecurityLogs.isNotEmpty()) return true
+ return try {
+ val logs = DPM.retrievePreRebootSecurityLogs(DAR)
+ if (logs != null && logs.isNotEmpty()) {
+ preRebootSecurityLogs = logs
+ true
+ } else false
+ } catch (_: SecurityException) {
+ false
+ }
+ }
+ @RequiresApi(24)
+ fun exportPreRebootSecurityLogs(uri: Uri, callback: () -> Unit) {
+ viewModelScope.launch(Dispatchers.IO) {
+ val stream = application.contentResolver.openOutputStream(uri) ?: return@launch
+ myRepo.exportPRSecurityLogs(preRebootSecurityLogs, stream)
+ stream.close()
+ withContext(Dispatchers.Main) { callback() }
+ }
+ }
+
+ @RequiresApi(24)
+ fun isCreatingWorkProfileAllowed(): Boolean {
+ return DPM.isProvisioningAllowed(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE)
+ }
+ fun activateDoByShizuku(callback: (Boolean, String?) -> Unit) {
+ viewModelScope.launch(Dispatchers.IO) {
+ useShizuku(application) { service ->
+ try {
+ val result = IUserService.Stub.asInterface(service)
+ .execute(ACTIVATE_DEVICE_OWNER_COMMAND)
+ if (result == null || result.getInt("code", -1) != 0) {
+ callback(false, null)
+ } else {
+ Privilege.updateStatus()
+ handlePrivilegeChange(application)
+ callback(
+ true, result.getString("output") + "\n" + result.getString("error")
+ )
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ callback(false, null)
+ }
+ }
+ }
+ }
+ fun activateDoByRoot(callback: (Boolean, String?) -> Unit) {
+ Shell.getShell { shell ->
+ if(shell.isRoot) {
+ val result = Shell.cmd(ACTIVATE_DEVICE_OWNER_COMMAND).exec()
+ val output = result.out.joinToString("\n") + "\n" + result.err.joinToString("\n")
+ if (result.isSuccess) {
+ Privilege.updateStatus()
+ handlePrivilegeChange(application)
+ }
+ callback(result.isSuccess, output)
+ } else {
+ callback(false, application.getString(R.string.permission_denied))
+ }
+ }
+ }
+ @RequiresApi(28)
+ fun activateDoByDhizuku(callback: (Boolean, String?) -> Unit) {
+ DPM.transferOwnership(DAR, MyAdminComponent, null)
+ SP.dhizuku = false
+ Privilege.initialize(application)
+ handlePrivilegeChange(application)
+ callback(true, null)
+ }
+ fun activateDhizukuMode(callback: (Boolean, String?) -> Unit) {
+ fun onSucceed() {
+ SP.dhizuku = true
+ Privilege.initialize(application)
+ handlePrivilegeChange(application)
+ callback(true, null)
+ }
+ if (Dhizuku.init(application)) {
+ if (Dhizuku.isPermissionGranted()) {
+ onSucceed()
+ } else {
+ Dhizuku.requestPermission(object : DhizukuRequestPermissionListener() {
+ override fun onRequestPermission(grantResult: Int) {
+ if (grantResult == PackageManager.PERMISSION_GRANTED) onSucceed()
+ else callback(false, application.getString(R.string.dhizuku_permission_not_granted))
+ }
+ })
+ }
+ } else {
+ callback(false, application.getString(R.string.failed_to_init_dhizuku))
+ }
+ }
+ fun clearDeviceOwner() {
+ DPM.clearDeviceOwnerApp(application.packageName)
+ }
+ @RequiresApi(24)
+ fun clearProfileOwner() {
+ DPM.clearProfileOwner(MyAdminComponent)
+ }
+ fun deactivateDhizukuMode() {
+ SP.dhizuku = false
+ Privilege.initialize(application)
+ }
+ val dhizukuClients = MutableStateFlow(emptyList>())
+ fun getDhizukuClients() {
+ viewModelScope.launch(Dispatchers.IO) {
+ dhizukuClients.value = myRepo.getDhizukuClients().mapNotNull {
+ val packageName = PM.getNameForUid(it.uid)
+ if (packageName == null) {
+ myRepo.deleteDhizukuClient(it)
+ null
+ } else {
+ it to getAppInfo(packageName)
+ }
+ }
+ }
+ }
+ fun getDhizukuServerEnabled(): Boolean {
+ return SP.dhizukuServer
+ }
+ fun setDhizukuServerEnabled(status: Boolean) {
+ SP.dhizukuServer = status
+ }
+ fun updateDhizukuClient(info: DhizukuClientInfo) {
+ myRepo.setDhizukuClient(info)
+ dhizukuClients.update { list ->
+ val ml = list.toMutableList()
+ val index = ml.indexOfFirst { it.first.uid == info.uid }
+ ml[index] = info to ml[index].second
+ ml
+ }
+ }
+ @RequiresApi(24)
+ fun getLockScreenInfo(): String {
+ return DPM.deviceOwnerLockScreenInfo?.toString() ?: ""
+ }
+ @RequiresApi(24)
+ fun setLockScreenInfo(text: String) {
+ DPM.setDeviceOwnerLockScreenInfo(DAR, text)
+ }
+ val delegatedAdmins = MutableStateFlow(emptyList())
+ @RequiresApi(26)
+ fun getDelegatedAdmins() {
+ val list = mutableListOf()
+ delegatedScopesList.forEach { scope ->
+ DPM.getDelegatePackages(DAR, scope.id)?.forEach { pkg ->
+ val index = list.indexOfFirst { it.app.name == pkg }
+ if (index == -1) {
+ list += DelegatedAdmin(getAppInfo(pkg), listOf(scope.id))
+ } else {
+ list[index] = DelegatedAdmin(list[index].app, list[index].scopes + scope.id)
+ }
+ }
+ }
+ delegatedAdmins.value = list
+ }
+ @RequiresApi(26)
+ fun setDelegatedAdmin(name: String, scopes: List) {
+ DPM.setDelegatedScopes(DAR, name, scopes)
+ getDelegatedAdmins()
+ }
+ @RequiresApi(34)
+ fun getDeviceFinanced(): Boolean {
+ return DPM.isDeviceFinanced
+ }
+ @RequiresApi(33)
+ fun getDpmRh(): String? {
+ return DPM.devicePolicyManagementRoleHolderPackage
+ }
+ fun getStorageEncryptionStatus(): Int {
+ return DPM.storageEncryptionStatus
+ }
+ @RequiresApi(28)
+ fun getDeviceIdAttestationSupported(): Boolean {
+ return DPM.isDeviceIdAttestationSupported
+ }
+ @RequiresApi(30)
+ fun getUniqueDeviceAttestationSupported(): Boolean {
+ return DPM.isUniqueDeviceAttestationSupported
+ }
+ fun getActiveAdmins(): String {
+ return DPM.activeAdmins?.joinToString("\n") {
+ it.flattenToShortString()
+ } ?: application.getString(R.string.none)
+ }
+ @RequiresApi(24)
+ fun getShortSupportMessage(): String {
+ return DPM.getShortSupportMessage(DAR)?.toString() ?: ""
+ }
+ @RequiresApi(24)
+ fun getLongSupportMessage(): String {
+ return DPM.getLongSupportMessage(DAR)?.toString() ?: ""
+ }
+ @RequiresApi(24)
+ fun setShortSupportMessage(text: String?) {
+ DPM.setShortSupportMessage(DAR, text)
+ }
+ @RequiresApi(24)
+ fun setLongSupportMessage(text: String?) {
+ DPM.setLongSupportMessage(DAR, text)
+ }
+ val deviceAdminReceivers = MutableStateFlow(emptyList())
+ fun getDeviceAdminReceivers() {
+ viewModelScope.launch(Dispatchers.IO) {
+ deviceAdminReceivers.value = PM.queryBroadcastReceivers(
+ Intent(DeviceAdminReceiver.ACTION_DEVICE_ADMIN_ENABLED),
+ PackageManager.GET_META_DATA
+ ).mapNotNull {
+ try {
+ DeviceAdminInfo(application, it)
+ } catch(_: Exception) {
+ null
+ }
+ }.filter {
+ it.isVisible && it.packageName != "com.bintianqi.owndroid" &&
+ it.activityInfo.applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM == 0
+ }.map {
+ DeviceAdmin(getAppInfo(it.packageName), it.component)
+ }
+ }
+ }
+ @RequiresApi(28)
+ fun transferOwnership(component: ComponentName) {
+ DPM.transferOwnership(DAR, component, null)
+ Privilege.updateStatus()
+ }
+ val userRestrictions = MutableStateFlow(emptyMap())
+ @RequiresApi(24)
+ fun getUserRestrictions() {
+ val bundle = DPM.getUserRestrictions(DAR)
+ userRestrictions.value = bundle.keySet().associateWith { bundle.getBoolean(it) }
+ }
+ fun setUserRestriction(name: String, state: Boolean): Boolean {
+ return try {
+ if (state) {
+ DPM.addUserRestriction(DAR, name)
+ } else {
+ DPM.clearUserRestriction(DAR, name)
+ }
+ userRestrictions.update { it.plus(name to state) }
+ ShortcutUtils.updateUserRestrictionShortcut(application, name, !state, true)
+ true
+ } catch (_: SecurityException) {
+ false
+ }
+ }
+ fun createUserRestrictionShortcut(id: String): Boolean {
+ return ShortcutUtils.setUserRestrictionShortcut(
+ application, id, userRestrictions.value[id] ?: true
+ )
+ }
+ 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_ACCOUNT_TO_MIGRATE,
+ Account(options.accountName, options.accountType)
+ )
+ if (VERSION.SDK_INT >= 26) {
+ intent.putExtra(
+ DevicePolicyManager.EXTRA_PROVISIONING_KEEP_ACCOUNT_ON_MIGRATION,
+ options.keepAccount
+ )
+ }
+ }
+ if (VERSION.SDK_INT >= 24) {
+ intent.putExtra(
+ DevicePolicyManager.EXTRA_PROVISIONING_SKIP_ENCRYPTION,
+ options.skipEncrypt
+ )
+ }
+ if (VERSION.SDK_INT >= 33) {
+ intent.putExtra(DevicePolicyManager.EXTRA_PROVISIONING_ALLOW_OFFLINE, options.offline)
+ }
+ return intent
+ }
+ fun activateOrgProfileByShizuku(callback: (Boolean) -> Unit) {
+ viewModelScope.launch(Dispatchers.IO) {
+ var succeed = false
+ useShizuku(application) { service ->
+ val result = IUserService.Stub.asInterface(service).execute(activateOrgProfileCommand)
+ succeed = result?.getInt("code", -1) == 0
+ callback(succeed)
+ }
+ if (succeed) Privilege.updateStatus()
+ }
+ }
+ @RequiresApi(30)
+ fun getPersonalAppsSuspendedReason(): Int {
+ return DPM.getPersonalAppsSuspendedReasons(DAR)
+ }
+ @RequiresApi(30)
+ fun setPersonalAppsSuspended(suspended: Boolean) {
+ DPM.setPersonalAppsSuspended(DAR, suspended)
+ }
+ @RequiresApi(30)
+ fun getProfileMaxTimeOff(): Long {
+ return DPM.getManagedProfileMaximumTimeOff(DAR)
+ }
+ @RequiresApi(30)
+ fun setProfileMaxTimeOff(time: Long) {
+ DPM.setManagedProfileMaximumTimeOff(DAR, time)
+ }
+ fun addCrossProfileIntentFilter(options: IntentFilterOptions) {
+ val filter = IntentFilter(options.action)
+ if (options.category.isNotEmpty()) filter.addCategory(options.category)
+ if (options.mimeType.isNotEmpty()) filter.addDataType(options.mimeType)
+ val flags = when(options.direction) {
+ IntentFilterDirection.ToManaged -> DevicePolicyManager.FLAG_PARENT_CAN_ACCESS_MANAGED
+ IntentFilterDirection.ToParent -> DevicePolicyManager.FLAG_MANAGED_CAN_ACCESS_PARENT
+ IntentFilterDirection.Both -> DevicePolicyManager.FLAG_PARENT_CAN_ACCESS_MANAGED or
+ DevicePolicyManager.FLAG_MANAGED_CAN_ACCESS_PARENT
+ }
+ DPM.addCrossProfileIntentFilter(DAR, filter, flags)
+ }
+
+ val UM = application.getSystemService(Context.USER_SERVICE) as UserManager
+ @RequiresApi(28)
+ fun getLogoutEnabled(): Boolean {
+ return DPM.isLogoutEnabled
+ }
+ @RequiresApi(28)
+ fun setLogoutEnabled(enabled: Boolean) {
+ DPM.setLogoutEnabled(DAR, enabled)
+ }
+ fun getUserInformation(): UserInformation {
+ val uh = Binder.getCallingUserHandle()
+ 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,
+ 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,
+ 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,
+ UM.getSerialNumberForUser(uh)
+ )
+ }
+ @Suppress("PrivateApi")
+ @RequiresApi(28)
+ fun getUserIdentifiers(): List {
+ return DPM.getSecondaryUsers(DAR)?.mapNotNull {
+ try {
+ val field = UserHandle::class.java.getDeclaredField("mHandle")
+ field.isAccessible = true
+ UserIdentifier(field.get(it) as Int, UM.getSerialNumberForUser(it))
+ } catch (e: Exception) {
+ e.printStackTrace()
+ null
+ }
+ } ?: emptyList()
+ }
+ fun doUserOperation(type: UserOperationType, id: Int, isUserId: Boolean): Boolean {
+ return doUserOperationWithContext(application, type, id, isUserId)
+ }
+ fun createUserOperationShortcut(type: UserOperationType, id: Int, isUserId: Boolean): Boolean {
+ val serial = if (isUserId && VERSION.SDK_INT >= 24) {
+ UM.getSerialNumberForUser(UserHandle.getUserHandleForUid(id * 100000))
+ } else id
+ return ShortcutUtils.setUserOperationShortcut(application, type, serial.toInt())
+ }
+ fun getUserOperationResultText(code: Int): Int {
+ return when (code) {
+ UserManager.USER_OPERATION_SUCCESS -> R.string.success
+ 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_CURRENT_USER -> R.string.fail_current_user
+ else -> R.string.unknown
+ }
+ }
+ @RequiresApi(24)
+ fun createUser(name: String, flags: Int, callback: (CreateUserResult) -> Unit) {
+ viewModelScope.launch(Dispatchers.IO) {
+ try {
+ val uh = DPM.createAndManageUser(DAR, name, DAR, null, flags)
+ if (uh == null) {
+ callback(CreateUserResult(R.string.failed))
+ } else {
+ callback(CreateUserResult(R.string.succeeded, UM.getSerialNumberForUser(uh)))
+ }
+ } catch (e: Exception) {
+ e.printStackTrace()
+ if (VERSION.SDK_INT >= 28 && e is UserManager.UserOperationException) {
+ callback(CreateUserResult(getUserOperationResultText(e.userOperationResult)))
+ } else {
+ callback(CreateUserResult(R.string.error))
+ }
+ }
+ }
+ }
+ val affiliationIds = MutableStateFlow(emptyList())
+ @RequiresApi(26)
+ fun getAffiliationIds() {
+ affiliationIds.value = DPM.getAffiliationIds(DAR).toList()
+ }
+ @RequiresApi(26)
+ fun setAffiliationId(id: String, state: Boolean) {
+ val newList = affiliationIds.value.run { if (state) plus(id) else minus(id) }
+ DPM.setAffiliationIds(DAR, newList.toSet())
+ affiliationIds.value = newList
+ }
+ fun setProfileName(name: String) {
+ DPM.setProfileName(DAR, name)
+ }
+ @RequiresApi(23)
+ fun setUserIcon(bitmap: Bitmap) {
+ DPM.setUserIcon(DAR, bitmap)
+ }
+ @RequiresApi(28)
+ fun getUserSessionMessages(): Pair {
+ return (DPM.getStartUserSessionMessage(DAR)?.toString() ?: "") to
+ (DPM.getEndUserSessionMessage(DAR)?.toString() ?: "")
+ }
+ @RequiresApi(28)
+ fun setStartUserSessionMessage(message: String?) {
+ DPM.setStartUserSessionMessage(DAR, message)
+ }
+ @RequiresApi(28)
+ fun setEndUserSessionMessage(message: String?) {
+ DPM.setEndUserSessionMessage(DAR, message)
+ }
+ @RequiresApi(28)
+ fun logoutUser(): Int {
+ return getUserOperationResultText(DPM.logoutUser(DAR))
+ }
+
+ val WM = application.getSystemService(Context.WIFI_SERVICE) as WifiManager
+ // Lockdown admin configured networks
+ @RequiresApi(30)
+ fun getLanEnabled(): Boolean {
+ return DPM.hasLockdownAdminConfiguredNetworks(DAR)
+ }
+ @RequiresApi(30)
+ fun setLanEnabled(state: Boolean) {
+ DPM.setConfiguredNetworksLockdownState(DAR, state)
+ }
+ fun setWifiEnabled(enabled: Boolean): Boolean {
+ return WM.setWifiEnabled(enabled)
+ }
+ fun disconnectWifi(): Boolean {
+ return WM.disconnect()
+ }
+ fun reconnectWifi(): Boolean {
+ return WM.reconnect()
+ }
+ @RequiresApi(24)
+ fun getWifiMac(): String? {
+ return DPM.getWifiMacAddress(DAR)
+ }
+ val configuredNetworks = MutableStateFlow(emptyList())
+ fun getConfiguredNetworks() {
+ configuredNetworks.value = WM.configuredNetworks.distinctBy { it.networkId }.map { conf ->
+ WifiInfo(
+ conf.networkId, conf.SSID.removeSurrounding("\""), null, conf.BSSID ?: "", null,
+ WifiStatus.entries.find { it.id == conf.status }!!, null, "", null, null, null, null
+ )
+ }
+ }
+ fun enableNetwork(id: Int): Boolean {
+ return WM.enableNetwork(id, false)
+ }
+ fun disableNetwork(id: Int): Boolean {
+ return WM.disableNetwork(id)
+ }
+ fun removeNetwork(id: Int): Boolean{
+ return WM.removeNetwork(id)
+ }
+ fun setWifi(info: WifiInfo): Boolean {
+ val conf = WifiConfiguration()
+ conf.SSID = "\"" + info.ssid + "\""
+ info.hiddenSsid?.let { conf.hiddenSSID = it }
+ if (VERSION.SDK_INT >= 30) info.security?.let { conf.setSecurityParams(it.id) }
+ if (info.security == WifiSecurity.Psk) conf.preSharedKey = info.password
+ if (VERSION.SDK_INT >= 33) info.macRandomization?.let { conf.macRandomizationSetting = it.id }
+ if (VERSION.SDK_INT >= 33 && info.ipMode != null) {
+ val ipConf = if (info.ipMode == IpMode.Static && info.ipConf != null) {
+ val constructor = LinkAddress::class.constructors.find {
+ it.parameters.size == 1 && it.parameters[0].type.jvmErasure == String::class
+ }
+ val address = constructor!!.call(info.ipConf.address)
+ val staticIpConf = StaticIpConfiguration.Builder()
+ .setIpAddress(address)
+ .setGateway(InetAddress.getByName(info.ipConf.gateway))
+ .setDnsServers(info.ipConf.dns.map { InetAddress.getByName(it) })
+ .build()
+ IpConfiguration.Builder().setStaticIpConfiguration(staticIpConf).build()
+ } else null
+ conf.setIpConfiguration(ipConf)
+ }
+ if (VERSION.SDK_INT >= 26 && info.proxyMode != null) {
+ val proxy = if (info.proxyMode == ProxyMode.Http) {
+ info.proxyConf?.let {
+ ProxyInfo.buildDirectProxy(it.host, it.port, it.exclude)
+ }
+ } else null
+ conf.httpProxy = proxy
+ }
+ val result = if (info.id != -1) {
+ conf.networkId = info.id
+ WM.updateNetwork(conf)
+ } else {
+ WM.addNetwork(conf)
+ }
+ if (result != -1) {
+ when (info.status) {
+ WifiStatus.Current -> WM.enableNetwork(result, true)
+ WifiStatus.Enabled -> WM.enableNetwork(result, false)
+ WifiStatus.Disabled -> WM.disableNetwork(result)
+ }
+ }
+ return result != -1
+ }
+ @RequiresApi(33)
+ fun getMinimumWifiSecurityLevel(): Int {
+ return DPM.minimumRequiredWifiSecurityLevel
+ }
+ @RequiresApi(33)
+ fun setMinimumWifiSecurityLevel(level: Int) {
+ DPM.minimumRequiredWifiSecurityLevel = level
+ }
+ @RequiresApi(33)
+ fun getSsidPolicy(): SsidPolicy {
+ val policy = DPM.wifiSsidPolicy
+ return SsidPolicy(
+ SsidPolicyType.entries.find { it.id == policy?.policyType } ?: SsidPolicyType.None,
+ policy?.ssids?.map { it.bytes.decodeToString() } ?: emptyList()
+ )
+ }
+ @RequiresApi(33)
+ fun setSsidPolicy(policy: SsidPolicy) {
+ val newPolicy = if (policy.type != SsidPolicyType.None) {
+ WifiSsidPolicy(
+ policy.type.id, policy.list.map { WifiSsid.fromBytes(it.encodeToByteArray()) }.toSet()
+ )
+ } else null
+ DPM.wifiSsidPolicy = newPolicy
+ }
+ @RequiresApi(24)
+ fun getPackageUid(name: String): Int {
+ return PM.getPackageUid(name, 0)
+ }
+ var networkStatsData = emptyList()
+ @RequiresApi(23)
+ fun readNetworkStats(stats: NetworkStats): List {
+ val list = mutableListOf