Merge branch 'dev'

This commit is contained in:
BinTianqi
2026-02-04 12:57:27 +08:00
35 changed files with 1389 additions and 403 deletions

View File

@@ -72,12 +72,6 @@ jobs:
with:
path: artifacts
- name: Download telegram-bot-api
run: |
mkdir ./binaries
wget "https://github.com/jakbin/telegram-bot-api-binary/releases/download/latest/telegram-bot-api" -O ./binaries/telegram-bot-api
chmod +x ./binaries/telegram-bot-api
- name: Start API Server & Upload
env:
COMMIT_MESSAGE: |+
@@ -91,8 +85,7 @@ jobs:
mv ./$RELEASE_TEST_PWD/app-release.apk ./$RELEASE_TEST_PWD.apk && rm -rf ./$RELEASE_TEST_PWD
export RELEASE_SIGNED_PWD=$(find . -name "*release-signed*")
mv ./$RELEASE_SIGNED_PWD/app-release.apk ./$RELEASE_SIGNED_PWD.apk && rm -rf ./$RELEASE_SIGNED_PWD
../binaries/telegram-bot-api --api-id=${{ secrets.TELEGRAM_API_APP_ID }} --api-hash=${{ secrets.TELEGRAM_API_HASH }} --local 2>&1 > /dev/null &
export token=${{ secrets.TELEGRAM_BOT_KEY }}
curl -v "http://127.0.0.1:8081/bot$token/sendMediaGroup?chat_id=-1002203528169&media=%5B%7B%22type%22%3A%22document%22%2C%22media%22%3A%22attach%3A%2F%2FreleaseTest%22%7D%2C%7B%22type%22%3A%22document%22%2C%22media%22%3A%22attach%3A%2F%2FreleaseSigned%22%2C%22parse_mode%22%3A%22HTML%22%2C%22caption%22%3A${ESCAPED}%7D%5D" \
curl -v "http://api.telegram.org/bot$token/sendMediaGroup?chat_id=-1002203528169&media=%5B%7B%22type%22%3A%22document%22%2C%22media%22%3A%22attach%3A%2F%2FreleaseTest%22%7D%2C%7B%22type%22%3A%22document%22%2C%22media%22%3A%22attach%3A%2F%2FreleaseSigned%22%2C%22parse_mode%22%3A%22HTML%22%2C%22caption%22%3A${ESCAPED}%7D%5D" \
-F releaseTest="@$RELEASE_TEST_PWD.apk" \
-F releaseSigned="@$RELEASE_SIGNED_PWD.apk"

View File

@@ -57,7 +57,7 @@ java.lang.IllegalStateException: Not allowed to set the device owner because the
> [!NOTE]
> 一些系统有应用克隆、儿童空间等功能,它们通常是用户。
#### Device owner 已存在
### Device owner 已存在
```text
java.lang.IllegalStateException: Trying to set the device owner (com.bintianqi.owndroid/.Receiver), but device owner (xxx) is already set.
@@ -142,7 +142,7 @@ context.sendBroadcast(intent)
[License.md](LICENSE.md)
> Copyright (C) 2024 BinTianqi
> Copyright (C) 2026 BinTianqi
>
> This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
>

View File

@@ -57,7 +57,7 @@ Solutions:
> [!NOTE]
> Some systems have features such as app cloning and children space, which are usually users.
#### Device owner is already set
### Device owner is already set
```text
java.lang.IllegalStateException: Trying to set the device owner (com.bintianqi.owndroid/.Receiver), but device owner (xxx) is already set.
@@ -144,7 +144,7 @@ You can use Gradle in command line to build OwnDroid.
[License.md](LICENSE.md)
> Copyright (C) 2024 BinTianqi
> Copyright (C) 2026 BinTianqi
>
> This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
>

View File

@@ -24,10 +24,10 @@ android {
defaultConfig {
applicationId = "com.bintianqi.owndroid"
minSdk = 21
minSdk = 23
targetSdk = 36
versionCode = 41
versionName = "7.2"
versionCode = 42
versionName = "7.3"
multiDexEnabled = false
}
@@ -103,6 +103,7 @@ dependencies {
implementation(libs.androidx.fragment)
implementation(libs.hiddenApiBypass)
implementation(libs.libsu)
implementation(libs.reoderable)
implementation(libs.serialization)
implementation(kotlin("reflect"))
}

View File

@@ -12,6 +12,8 @@
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
<uses-sdk tools:overrideLibrary="rikka.shizuku.provider,rikka.shizuku.api,rikka.shizuku.shared,rikka.shizuku.aidl,com.rosan.dhizuku.server_api,com.rosan.dhizuku.api"/>
<uses-feature android:name="android.software.device_admin"/>
<application
@@ -81,7 +83,7 @@
android:name=".Receiver"
android:description="@string/app_name"
android:permission="android.permission.BIND_DEVICE_ADMIN"
android:exported="true">
android:exported="false">
<meta-data
android:name="android.app.device_admin"
android:resource="@xml/device_admin"/>
@@ -99,6 +101,12 @@
android:name=".ApiReceiver"
android:exported="true">
</receiver>
<service
android:name=".LockTaskService"
android:exported="false"
android:foregroundServiceType="specialUse" />
<provider
android:name="rikka.shizuku.ShizukuProvider"
android:authorities="${applicationId}.shizuku"

View File

@@ -16,6 +16,7 @@ class AppInstallerActivity:FragmentActivity() {
super.onCreate(savedInstanceState)
val vm by viewModels<AppInstallerViewModel>()
vm.initialize(intent)
vm.registerInstallerReceiver(this)
val theme = ThemeSettings(SP.materialYou, SP.darkTheme, SP.blackTheme)
setContent {
OwnDroidTheme(theme) {

View File

@@ -36,13 +36,19 @@ class AppInstallerViewModel(application: Application): AndroidViewModel(applicat
intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)?.let { list += it }
intent.getParcelableArrayExtra(Intent.EXTRA_STREAM)?.forEach { list += it as Uri }
intent.clipData?.let { clipData ->
for(i in 0..clipData.itemCount - 1) {
for(i in 0..<clipData.itemCount) {
list += clipData.getItemAt(i).uri
}
}
uiState.update { it.copy(it.packages + list.distinct()) }
}
fun registerInstallerReceiver(context: Context) {
ContextCompat.registerReceiver(
context, Receiver(), IntentFilter(ACTION), ContextCompat.RECEIVER_NOT_EXPORTED
)
}
fun onPackagesAdd(packages: List<Uri>) {
uiState.update {
it.copy(packages = it.packages.plus(packages).distinct())
@@ -93,17 +99,14 @@ class AppInstallerViewModel(application: Application): AndroidViewModel(applicat
uiState.update { it.copy(installing = false, packageWriting = -1) }
return
}
ContextCompat.registerReceiver(
application, Receiver(), IntentFilter(ACTION), null,
null, ContextCompat.RECEIVER_EXPORTED
)
val intent = Intent(ACTION).setPackage(application.packageName)
val pi = if(Build.VERSION.SDK_INT >= 34) {
PendingIntent.getBroadcast(
application, sessionId, Intent(ACTION),
application, sessionId, intent,
PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT or PendingIntent.FLAG_MUTABLE
).intentSender
} else {
PendingIntent.getBroadcast(application, sessionId, Intent(ACTION), PendingIntent.FLAG_MUTABLE).intentSender
PendingIntent.getBroadcast(application, sessionId, intent, PendingIntent.FLAG_MUTABLE).intentSender
}
session.commit(pi)
}
@@ -119,7 +122,6 @@ class AppInstallerViewModel(application: Application): AndroidViewModel(applicat
)
} else {
uiState.update { it.copy(result = intent) }
context.unregisterReceiver(this)
}
}
}

View File

@@ -3,6 +3,7 @@ package com.bintianqi.owndroid
import android.content.Context
import android.hardware.biometrics.BiometricPrompt
import android.hardware.biometrics.BiometricPrompt.AuthenticationCallback
import android.hardware.fingerprint.FingerprintManager
import android.os.Build
import android.os.CancellationSignal
import androidx.activity.compose.BackHandler
@@ -19,6 +20,7 @@ import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -39,6 +41,8 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
@@ -50,6 +54,7 @@ fun AppLockDialog(onSucceed: () -> Unit, onDismiss: () -> Unit) = Dialog(onDismi
val fr = remember { FocusRequester() }
var input by rememberSaveable { mutableStateOf("") }
var isError by rememberSaveable { mutableStateOf(false) }
var showPassword by remember { mutableStateOf(false) }
fun unlock() {
if(input.hash() == SP.lockPasswordHash) {
fm.clearFocus()
@@ -75,7 +80,18 @@ fun AppLockDialog(onSucceed: () -> Unit, onDismiss: () -> Unit) = Dialog(onDismi
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password, imeAction = if(input.length >= 4) ImeAction.Go else ImeAction.Done
),
keyboardActions = KeyboardActions({ fm.clearFocus() }, { unlock() })
keyboardActions = KeyboardActions({ fm.clearFocus() }, { unlock() }),
visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(),
trailingIcon = {
IconButton(onClick = { showPassword = !showPassword }) {
Icon(
painter = painterResource(
id = if (showPassword) R.drawable.visibility_fill0 else R.drawable.visibility_off_fill0
),
contentDescription = if (showPassword) "Hide password" else "Show password"
)
}
}
)
if(Build.VERSION.SDK_INT >= 28 && SP.biometricsUnlock) {
FilledTonalIconButton({ startBiometricsUnlock(context, onSucceed) }, Modifier.padding(start = 4.dp)) {
@@ -83,7 +99,7 @@ fun AppLockDialog(onSucceed: () -> Unit, onDismiss: () -> Unit) = Dialog(onDismi
}
}
}
Button(::unlock, Modifier.align(Alignment.End).padding(top = 8.dp), input.length >= 4) {
Button(::unlock, Modifier.align(Alignment.End).padding(top = 8.dp)) {
Text(stringResource(R.string.unlock))
}
}
@@ -92,6 +108,7 @@ fun AppLockDialog(onSucceed: () -> Unit, onDismiss: () -> Unit) = Dialog(onDismi
@RequiresApi(28)
fun startBiometricsUnlock(context: Context, onSucceed: () -> Unit) {
context.getSystemService(FingerprintManager::class.java) ?: return
val callback = object : AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult?) {
super.onAuthenticationSucceeded(result)

View File

@@ -0,0 +1,84 @@
package com.bintianqi.owndroid
import android.app.ActivityManager
import android.app.PendingIntent
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@RequiresApi(28)
class LockTaskService: Service() {
val coroutineScope = CoroutineScope(Dispatchers.IO)
override fun onBind(intent: Intent?): IBinder? = null
val stopReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
coroutineScope.cancel()
stopLockTask()
stop()
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
val filter = IntentFilter(STOP_ACTION)
ContextCompat.registerReceiver(
this, stopReceiver, filter, ContextCompat.RECEIVER_NOT_EXPORTED
)
val pendingIntent = PendingIntent.getBroadcast(
this, 0, Intent(STOP_ACTION).setPackage(this.packageName), PendingIntent.FLAG_IMMUTABLE
)
val notification = NotificationCompat.Builder(this, MyNotificationChannel.LockTaskMode.id)
.setContentTitle(getText(R.string.lock_task_mode))
.setSmallIcon(R.drawable.lock_fill0)
.addAction(NotificationCompat.Action.Builder(null, getString(R.string.stop), pendingIntent).build())
.setOngoing(true)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.build()
ServiceCompat.startForeground(
this, NotificationType.LockTaskMode.id, notification,
if (Build.VERSION.SDK_INT < 34) 0 else ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST
)
coroutineScope.launch {
val am = getSystemService(ActivityManager::class.java)
delay(3000)
while (am.lockTaskModeState == ActivityManager.LOCK_TASK_MODE_LOCKED) {
delay(1000)
}
stop()
}
return START_NOT_STICKY
}
fun stop() {
unregisterReceiver(stopReceiver)
stopSelf()
}
fun stopLockTask() {
val features = Privilege.DPM.getLockTaskFeatures(Privilege.DAR)
val packages = Privilege.DPM.getLockTaskPackages(Privilege.DAR)
Privilege.DPM.setLockTaskPackages(Privilege.DAR, arrayOf())
Privilege.DPM.setLockTaskPackages(Privilege.DAR, packages)
Privilege.DPM.setLockTaskFeatures(Privilege.DAR, features)
}
companion object {
const val STOP_ACTION = "com.bintianqi.owndroid.action.STOP_LOCK_TASK_MODE"
}
}

View File

@@ -137,6 +137,8 @@ import com.bintianqi.owndroid.dpm.LockTaskMode
import com.bintianqi.owndroid.dpm.LockTaskModeScreen
import com.bintianqi.owndroid.dpm.ManageAppGroups
import com.bintianqi.owndroid.dpm.ManageAppGroupsScreen
import com.bintianqi.owndroid.dpm.ManagedConfiguration
import com.bintianqi.owndroid.dpm.ManagedConfigurationScreen
import com.bintianqi.owndroid.dpm.MtePolicy
import com.bintianqi.owndroid.dpm.MtePolicyScreen
import com.bintianqi.owndroid.dpm.NearbyStreamingPolicy
@@ -155,7 +157,6 @@ import com.bintianqi.owndroid.dpm.OrganizationOwnedProfileScreen
import com.bintianqi.owndroid.dpm.OverrideApn
import com.bintianqi.owndroid.dpm.OverrideApnScreen
import com.bintianqi.owndroid.dpm.PackageFunctionScreen
import com.bintianqi.owndroid.dpm.PackageFunctionScreenWithoutResult
import com.bintianqi.owndroid.dpm.Password
import com.bintianqi.owndroid.dpm.PasswordInfo
import com.bintianqi.owndroid.dpm.PasswordInfoScreen
@@ -253,6 +254,9 @@ class MainActivity : FragmentActivity() {
val launcher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {}
launcher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
registerPackageRemovedReceiver(this) {
vm.onPackageRemoved(it)
}
setContent {
var appLockDialog by rememberSaveable { mutableStateOf(false) }
val theme by vm.theme.collectAsStateWithLifecycle()
@@ -265,10 +269,6 @@ class MainActivity : FragmentActivity() {
}
}
override fun onResume() {
super.onResume()
}
}
@ExperimentalMaterial3Api
@@ -285,7 +285,10 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
}
}
fun choosePackage() {
navController.navigate(ApplicationsList(false))
navController.navigate(ApplicationsList(false, true))
}
fun chooseSinglePackage() {
navController.navigate(ApplicationsList(false, false))
}
fun navigateToAppGroups() {
navController.navigate(ManageAppGroups)
@@ -331,7 +334,7 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
DelegatedAdminsScreen(vm.delegatedAdmins, vm::getDelegatedAdmins, ::navigateUp, ::navigate)
}
composable<AddDelegatedAdmin>{
AddDelegatedAdminScreen(vm.chosenPackage, ::choosePackage, it.toRoute(),
AddDelegatedAdminScreen(vm.chosenPackage, ::chooseSinglePackage, it.toRoute(),
vm::setDelegatedAdmin, ::navigateUp)
}
composable<DeviceInfo> { DeviceInfoScreen(vm, ::navigateUp) }
@@ -385,9 +388,11 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
vm::getNsNotificationPolicy, vm::setNsNotificationPolicy, ::navigateUp)
}
composable<LockTaskMode> {
LockTaskModeScreen(vm.chosenPackage, ::choosePackage, vm.lockTaskPackages,
LockTaskModeScreen(
vm.chosenPackage, ::chooseSinglePackage, ::choosePackage, vm.lockTaskPackages,
vm::getLockTaskPackages, vm::setLockTaskPackage, vm::startLockTaskMode,
vm:: getLockTaskFeatures, vm::setLockTaskFeatures, ::navigateUp)
vm:: getLockTaskFeatures, vm::setLockTaskFeatures, ::navigateUp
)
}
composable<CaCert> {
CaCertScreen(vm.installedCaCerts, vm::getCaCerts, vm.selectedCaCert, vm::selectCaCert, vm::installCaCert, vm::parseCaCert,
@@ -435,7 +440,7 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
WifiSsidPolicyScreen(vm::getSsidPolicy, vm::setSsidPolicy, ::navigateUp)
}
composable<QueryNetworkStats> {
NetworkStatsScreen(vm.chosenPackage, ::choosePackage, vm::getPackageUid,
NetworkStatsScreen(vm.chosenPackage, ::chooseSinglePackage, vm::getPackageUid,
vm::queryNetworkStats, ::navigateUp) { navController.navigate(NetworkStatsViewer) }
}
composable<NetworkStatsViewer> {
@@ -446,7 +451,7 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
}
composable<AlwaysOnVpnPackage> {
AlwaysOnVpnPackageScreen(vm::getAlwaysOnVpnPackage, vm::getAlwaysOnVpnLockdown,
vm::setAlwaysOnVpn, vm.chosenPackage, ::choosePackage, ::navigateUp)
vm::setAlwaysOnVpn, vm.chosenPackage, ::chooseSinglePackage, ::navigateUp)
}
composable<RecommendedGlobalProxy> {
RecommendedGlobalProxyScreen(vm::setRecommendedGlobalProxy, ::navigateUp)
@@ -494,10 +499,10 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
composable<DeleteWorkProfile> { DeleteWorkProfileScreen(vm::wipeData, ::navigateUp) }
composable<ApplicationsList> {
val canSwitchView = (it.toRoute() as ApplicationsList).canSwitchView
val params = it.toRoute<ApplicationsList>()
AppChooserScreen(
canSwitchView, vm.installedPackages, vm.refreshPackagesProgress, { name ->
if (canSwitchView) {
params, vm.installedPackages, vm.refreshPackagesProgress, { name ->
if (params.canSwitchView) {
if (name == null) {
navigateUp()
} else {
@@ -512,12 +517,12 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
navController.navigate(ApplicationsFeatures) {
popUpTo(Home)
}
}, vm::refreshPackageList)
}, vm::refreshPackageList, vm::setPackageSuspended, vm::setPackageHidden)
}
composable<ApplicationsFeatures> {
ApplicationsFeaturesScreen(::navigateUp, ::navigate) {
SP.applicationsListView = true
navController.navigate(ApplicationsList(true)) {
navController.navigate(ApplicationsList(true, true)) {
popUpTo(Home)
}
}
@@ -526,52 +531,72 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
ApplicationDetailsScreen(it.toRoute(), vm, ::navigateUp, ::navigate)
}
composable<Suspend> {
PackageFunctionScreen(R.string.suspend, vm.suspendedPackages, vm::getSuspendedPackaged,
PackageFunctionScreen(
R.string.suspend, vm.suspendedPackages, vm::getSuspendedPackaged,
vm::setPackageSuspended, ::navigateUp, vm.chosenPackage, ::choosePackage,
::navigateToAppGroups, vm.appGroups, R.string.info_suspend_app)
::navigateToAppGroups, vm.appGroups, R.string.info_suspend_app
)
}
composable<Hide> {
PackageFunctionScreen(R.string.hide, vm.hiddenPackages, vm::getHiddenPackages,
vm::setPackageHidden, ::navigateUp, vm.chosenPackage, ::choosePackage, ::navigateToAppGroups, vm.appGroups)
PackageFunctionScreen(
R.string.hide, vm.hiddenPackages, vm::getHiddenPackages, vm::setPackageHidden,
::navigateUp, vm.chosenPackage, ::choosePackage, ::navigateToAppGroups, vm.appGroups
)
}
composable<BlockUninstall> {
PackageFunctionScreenWithoutResult(R.string.block_uninstall, vm.ubPackages,
vm::getUbPackages, vm::setPackageUb, ::navigateUp, vm.chosenPackage, ::choosePackage, ::navigateToAppGroups, vm.appGroups)
PackageFunctionScreen(
R.string.block_uninstall, vm.ubPackages, vm::getUbPackages, vm::setPackageUb,
::navigateUp, vm.chosenPackage, ::choosePackage, ::navigateToAppGroups, vm.appGroups
)
}
composable<DisableUserControl> {
PackageFunctionScreenWithoutResult(R.string.disable_user_control, vm.ucdPackages,
vm::getUcdPackages, vm::setPackageUcd, ::navigateUp, vm.chosenPackage,
::choosePackage, ::navigateToAppGroups, vm.appGroups, R.string.info_disable_user_control)
PackageFunctionScreen(
R.string.disable_user_control, vm.ucdPackages, vm::getUcdPackages,
vm::setPackageUcd, ::navigateUp, vm.chosenPackage, ::choosePackage,
::navigateToAppGroups, vm.appGroups, R.string.info_disable_user_control
)
}
composable<PermissionsManager> {
PermissionsManagerScreen(vm.packagePermissions, vm::getPackagePermissions,
vm::setPackagePermission, ::navigateUp, it.toRoute(), vm.chosenPackage, ::choosePackage)
PermissionsManagerScreen(
vm.packagePermissions, vm::getPackagePermissions, vm::setPackagePermission,
::navigateUp, it.toRoute(), vm.chosenPackage, ::chooseSinglePackage
)
}
composable<DisableMeteredData> {
PackageFunctionScreen(R.string.disable_metered_data, vm.mddPackages,
vm::getMddPackages, vm::setPackageMdd, ::navigateUp, vm.chosenPackage,
::choosePackage, ::navigateToAppGroups, vm.appGroups)
PackageFunctionScreen(
R.string.disable_metered_data, vm.mddPackages, vm::getMddPackages,
vm::setPackageMdd, ::navigateUp, vm.chosenPackage, ::choosePackage,
::navigateToAppGroups, vm.appGroups
)
}
composable<ClearAppStorage> {
ClearAppStorageScreen(vm.chosenPackage, ::choosePackage, vm::clearAppData, ::navigateUp)
ClearAppStorageScreen(
vm.chosenPackage, ::chooseSinglePackage, vm::clearAppData, ::navigateUp
)
}
composable<UninstallApp> {
UninstallAppScreen(vm.chosenPackage, ::choosePackage, vm::uninstallPackage, ::navigateUp)
UninstallAppScreen(
vm.chosenPackage, ::chooseSinglePackage, vm::uninstallPackage, ::navigateUp
)
}
composable<KeepUninstalledPackages> {
PackageFunctionScreenWithoutResult(R.string.keep_uninstalled_packages, vm.kuPackages,
vm::getKuPackages, vm::setPackageKu, ::navigateUp, vm.chosenPackage,
::choosePackage, ::navigateToAppGroups, vm.appGroups,
R.string.info_keep_uninstalled_apps)
PackageFunctionScreen(
R.string.keep_uninstalled_packages, vm.kuPackages, vm::getKuPackages,
vm::setPackageKu, ::navigateUp, vm.chosenPackage, ::choosePackage,
::navigateToAppGroups, vm.appGroups, R.string.info_keep_uninstalled_apps
)
}
composable<InstallExistingApp> {
InstallExistingAppScreen(vm.chosenPackage, ::choosePackage,
vm::installExistingApp, ::navigateUp)
InstallExistingAppScreen(
vm.chosenPackage, ::chooseSinglePackage, vm::installExistingApp, ::navigateUp
)
}
composable<CrossProfilePackages> {
PackageFunctionScreenWithoutResult(R.string.cross_profile_apps, vm.cpPackages,
PackageFunctionScreen(
R.string.cross_profile_apps, vm.cpPackages,
vm::getCpPackages, vm::setPackageCp, ::navigateUp, vm.chosenPackage,
::choosePackage, ::navigateToAppGroups, vm.appGroups)
::choosePackage, ::navigateToAppGroups, vm.appGroups
)
}
composable<CrossProfileWidgetProviders> {
PackageFunctionScreen(R.string.cross_profile_widget, vm.cpwProviders,
@@ -579,28 +604,45 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
::choosePackage, ::navigateToAppGroups, vm.appGroups)
}
composable<CredentialManagerPolicy> {
CredentialManagerPolicyScreen(vm.chosenPackage, ::choosePackage,
vm.cmPackages, vm::getCmPolicy, vm::setCmPackage, vm::setCmPolicy, ::navigateUp)
CredentialManagerPolicyScreen(
vm.chosenPackage, ::choosePackage, vm.cmPackages, vm::getCmPolicy,
vm::setCmPackage, vm::setCmPolicy, ::navigateUp
)
}
composable<PermittedAccessibilityServices> {
PermittedAsAndImPackages(R.string.permitted_accessibility_services,
PermittedAsAndImPackages(
R.string.permitted_accessibility_services,
R.string.system_accessibility_always_allowed, vm.chosenPackage, ::choosePackage,
vm.pasPackages, vm::getPasPackages, vm::setPasPackage, vm::setPasPolicy, ::navigateUp)
vm.pasPackages, vm::getPasPackages, vm::setPasPackage, vm::setPasPolicy,
::navigateUp
)
}
composable<PermittedInputMethods> {
PermittedAsAndImPackages(R.string.permitted_ime, R.string.system_ime_always_allowed,
PermittedAsAndImPackages(
R.string.permitted_ime, R.string.system_ime_always_allowed,
vm.chosenPackage, ::choosePackage, vm.pimPackages, vm::getPimPackages,
vm::setPimPackage, vm::setPimPolicy, ::navigateUp)
vm::setPimPackage, vm::setPimPolicy, ::navigateUp
)
}
composable<EnableSystemApp> {
EnableSystemAppScreen(vm.chosenPackage, ::choosePackage, vm::enableSystemApp, ::navigateUp)
EnableSystemAppScreen(
vm.chosenPackage, ::chooseSinglePackage, vm::enableSystemApp, ::navigateUp
)
}
composable<SetDefaultDialer> {
SetDefaultDialerScreen(vm.chosenPackage, ::choosePackage, vm::setDefaultDialer, ::navigateUp)
SetDefaultDialerScreen(
vm.chosenPackage, ::chooseSinglePackage, vm::setDefaultDialer, ::navigateUp
)
}
composable<ManagedConfiguration> {
ManagedConfigurationScreen(
it.toRoute(), vm.appRestrictions, vm::setAppRestrictions,
vm::clearAppRestrictions, ::navigateUp
)
}
composable<ManageAppGroups> {
ManageAppGroupsScreen(
vm.appGroups,
vm.appGroups, vm::exportAppGroups, vm::importAppGroups,
{ id, name, apps -> navController.navigate(EditAppGroup(id, name, apps)) },
::navigateUp
)
@@ -751,7 +793,10 @@ private fun HomeScreen(onNavigate: (Any) -> Unit) {
}
if(privilege.device || privilege.profile) {
HomePageItem(R.string.applications, R.drawable.apps_fill0) {
onNavigate(if(SP.applicationsListView) ApplicationsList(true) else ApplicationsFeatures)
onNavigate(
if (SP.applicationsListView) ApplicationsList(true, true)
else ApplicationsFeatures
)
}
if(VERSION.SDK_INT >= 24) {
HomePageItem(R.string.user_restriction, R.drawable.person_off) { onNavigate(UserRestriction) }

View File

@@ -25,6 +25,8 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.RestrictionEntry
import android.content.RestrictionsManager
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInstaller
import android.content.pm.PackageManager
@@ -39,6 +41,7 @@ import android.net.wifi.WifiManager
import android.net.wifi.WifiSsid
import android.os.Binder
import android.os.Build.VERSION
import android.os.Bundle
import android.os.HardwarePropertiesManager
import android.os.UserHandle
import android.os.UserManager
@@ -60,7 +63,9 @@ import com.bintianqi.owndroid.dpm.ApnConfig
import com.bintianqi.owndroid.dpm.ApnMvnoType
import com.bintianqi.owndroid.dpm.ApnProtocol
import com.bintianqi.owndroid.dpm.AppGroup
import com.bintianqi.owndroid.dpm.AppRestriction
import com.bintianqi.owndroid.dpm.AppStatus
import com.bintianqi.owndroid.dpm.BasicAppGroup
import com.bintianqi.owndroid.dpm.CaCertInfo
import com.bintianqi.owndroid.dpm.CreateUserResult
import com.bintianqi.owndroid.dpm.CreateWorkProfileOptions
@@ -115,6 +120,10 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.addJsonObject
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.put
import java.net.InetAddress
import java.security.MessageDigest
import java.security.cert.CertificateException
@@ -154,10 +163,10 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
return AppLockConfig(passwordHash?.ifEmpty { null }, SP.biometricsUnlock, SP.lockWhenLeaving)
}
fun setAppLockConfig(config: AppLockConfig) {
SP.lockPasswordHash = if (config.password == null) {
""
} else {
config.password.hash()
if (config.password == null) {
SP.lockPasswordHash = ""
} else if (!config.password.isEmpty()) {
SP.lockPasswordHash = config.password.hash()
}
SP.biometricsUnlock = config.biometrics
SP.lockWhenLeaving = config.whenLeaving
@@ -196,6 +205,11 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
}
}
}
fun onPackageRemoved(name: String) {
installedPackages.update { list ->
list.filter { it.name != name }
}
}
fun getAppInfo(info: ApplicationInfo) =
AppInfo(info.packageName, info.loadLabel(PM).toString(), info.loadIcon(PM), info.flags)
fun getAppInfo(name: String): AppInfo {
@@ -215,10 +229,9 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
suspendedPackages.value = packages.map { getAppInfo(it) }
}
@RequiresApi(24)
fun setPackageSuspended(name: String, status: Boolean): Boolean {
val result = DPM.setPackagesSuspended(DAR, arrayOf(name), status)
fun setPackageSuspended(packages: List<String>, status: Boolean) {
DPM.setPackagesSuspended(DAR, packages.toTypedArray(), status)
getSuspendedPackaged()
return result.isEmpty()
}
val hiddenPackages = MutableStateFlow(emptyList<AppInfo>())
@@ -227,10 +240,11 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
DPM.isApplicationHidden(DAR, it.packageName)
}.map { getAppInfo(it) }
}
fun setPackageHidden(name: String, status: Boolean): Boolean {
val result = DPM.setApplicationHidden(DAR, name, status)
fun setPackageHidden(packages: List<String>, status: Boolean) {
for (name in packages) {
DPM.setApplicationHidden(DAR, name, status)
}
getHiddenPackages()
return result
}
// Uninstall blocked packages
@@ -240,8 +254,10 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
DPM.isUninstallBlocked(DAR, it.packageName)
}.map { getAppInfo(it) }
}
fun setPackageUb(name: String, status: Boolean) {
DPM.setUninstallBlocked(DAR, name, status)
fun setPackageUb(packages: List<String>, status: Boolean) {
for (name in packages) {
DPM.setUninstallBlocked(DAR, name, status)
}
getUbPackages()
}
@@ -254,16 +270,17 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
}
}
@RequiresApi(30)
fun setPackageUcd(name: String, status: Boolean) {
fun setPackageUcd(packages: List<String>, status: Boolean) {
DPM.setUserControlDisabledPackages(
DAR,
ucdPackages.value.map { it.name }.run { if (status) plus(name) else minus(name) }
ucdPackages.value.map { it.name }.run {
if (status) plus(packages) else minus(packages)
}
)
getUcdPackages()
}
val packagePermissions = MutableStateFlow(emptyMap<String, Int>())
@RequiresApi(23)
fun getPackagePermissions(name: String) {
if (name.isValidPackageName) {
packagePermissions.value = runtimePermissions.associate {
@@ -273,7 +290,6 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
packagePermissions.value = emptyMap()
}
}
@RequiresApi(23)
fun setPackagePermission(name: String, permission: String, status: Int): Boolean {
val result = DPM.setPermissionGrantState(DAR, name, permission, status)
getPackagePermissions(name)
@@ -287,12 +303,13 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
mddPackages.value = DPM.getMeteredDataDisabledPackages(DAR).distinct().map { getAppInfo(it) }
}
@RequiresApi(28)
fun setPackageMdd(name: String, status: Boolean): Boolean {
val result = DPM.setMeteredDataDisabledPackages(
DAR, mddPackages.value.map { it.name }.run { if (status) plus(name) else minus(name) }
fun setPackageMdd(packages: List<String>, status: Boolean) {
DPM.setMeteredDataDisabledPackages(
DAR, mddPackages.value.map { it.name }.run {
if (status) plus(packages) else minus(packages)
}
)
getMddPackages()
return result.isEmpty()
}
// Keep uninstalled packages
@@ -302,9 +319,11 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
kuPackages.value = DPM.getKeepUninstalledPackages(DAR)?.distinct()?.map { getAppInfo(it) } ?: emptyList()
}
@RequiresApi(28)
fun setPackageKu(name: String, status: Boolean) {
fun setPackageKu(packages: List<String>, status: Boolean) {
DPM.setKeepUninstalledPackages(
DAR, kuPackages.value.map { it.name }.run { if (status) plus(name) else minus(name) }
DAR, kuPackages.value.map { it.name }.run {
if (status) plus(packages) else minus(packages)
}
)
getKuPackages()
}
@@ -316,10 +335,12 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
cpPackages.value = DPM.getCrossProfilePackages(DAR).map { getAppInfo(it) }
}
@RequiresApi(30)
fun setPackageCp(name: String, status: Boolean) {
fun setPackageCp(packages: List<String>, status: Boolean) {
DPM.setCrossProfilePackages(
DAR,
cpPackages.value.map { it.name }.toSet().run { if (status) plus(name) else minus(name) }
cpPackages.value.map { it.name }.toSet().run {
if (status) plus(packages) else minus(packages)
}
)
getCpPackages()
}
@@ -329,14 +350,15 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
fun getCpwProviders() {
cpwProviders.value = DPM.getCrossProfileWidgetProviders(DAR).distinct().map { getAppInfo(it) }
}
fun setCpwProvider(name: String, status: Boolean): Boolean {
val result = if (status) {
DPM.addCrossProfileWidgetProvider(DAR, name)
} else {
DPM.removeCrossProfileWidgetProvider(DAR, name)
fun setCpwProvider(packages: List<String>, status: Boolean) {
for (name in packages) {
if (status) {
DPM.addCrossProfileWidgetProvider(DAR, name)
} else {
DPM.removeCrossProfileWidgetProvider(DAR, name)
}
}
getCpwProviders()
return result
}
@RequiresApi(28)
@@ -347,6 +369,7 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
}
fun uninstallPackage(packageName: String, onComplete: (String?) -> Unit) {
val action = "com.bintianqi.owndroid.action.PACKAGE_UNINSTALLED"
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val statusExtra = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, 999)
@@ -355,7 +378,7 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
context.startActivity(intent.getParcelableExtra(Intent.EXTRA_INTENT) as Intent?)
} else {
context.unregisterReceiver(this)
if(statusExtra == PackageInstaller.STATUS_SUCCESS) {
if (statusExtra == PackageInstaller.STATUS_SUCCESS) {
onComplete(null)
} else {
onComplete(parsePackageInstallerMessage(context, intent))
@@ -364,16 +387,17 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
}
}
ContextCompat.registerReceiver(
application, receiver, IntentFilter(AppInstallerViewModel.ACTION), null,
null, ContextCompat.RECEIVER_EXPORTED
application, receiver, IntentFilter(action), null,
null, ContextCompat.RECEIVER_NOT_EXPORTED
)
val intent = Intent(action).setPackage(application.packageName)
val pi = if(VERSION.SDK_INT >= 34) {
PendingIntent.getBroadcast(
application, 0, Intent(AppInstallerViewModel.ACTION),
application, 0, intent,
PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT or PendingIntent.FLAG_MUTABLE
).intentSender
} else {
PendingIntent.getBroadcast(application, 0, Intent(AppInstallerViewModel.ACTION), PendingIntent.FLAG_MUTABLE).intentSender
PendingIntent.getBroadcast(application, 0, intent, PendingIntent.FLAG_MUTABLE).intentSender
}
application.getPackageInstaller().uninstall(packageName, pi)
}
@@ -392,9 +416,9 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
policy.policyType
} ?: -1
}
fun setCmPackage(name: String, status: Boolean) {
cmPackages.update { list ->
if (status) list + getAppInfo(name) else list.filter { it.name != name }
fun setCmPackage(packages: List<String>, status: Boolean) {
cmPackages.update {
updateAppInfoList(it, packages, status)
}
}
@RequiresApi(34)
@@ -405,6 +429,16 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
getCmPolicy()
}
fun updateAppInfoList(
origin: List<AppInfo>, input: List<String>, status: Boolean
): List<AppInfo> {
return if (status) {
origin + input.map { getAppInfo(it) }
} else {
origin.filter { it.name !in input }
}
}
// Permitted input method
val pimPackages = MutableStateFlow(emptyList<AppInfo>())
fun getPimPackages(): Boolean {
@@ -413,9 +447,9 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
packages == null
}
}
fun setPimPackage(name: String, status: Boolean) {
pimPackages.update { packages ->
if (status) packages + getAppInfo(name) else packages.filter { it.name != name }
fun setPimPackage(packages: List<String>, status: Boolean) {
pimPackages.update {
updateAppInfoList(it, packages, status)
}
}
fun setPimPolicy(allowAll: Boolean): Boolean {
@@ -433,9 +467,9 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
packages == null
}
}
fun setPasPackage(name: String, status: Boolean) {
pasPackages.update { packages ->
if (status) packages + getAppInfo(name) else packages.filter { it.name != name }
fun setPasPackage(packages: List<String>, status: Boolean) {
pasPackages.update {
updateAppInfoList(it, packages, status)
}
}
fun setPasPolicy(allowAll: Boolean): Boolean {
@@ -457,14 +491,18 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
DPM.isUninstallBlocked(DAR, name),
if (VERSION.SDK_INT >= 30) name in DPM.getUserControlDisabledPackages(DAR) else false,
if (VERSION.SDK_INT >= 28) name in DPM.getMeteredDataDisabledPackages(DAR) else false,
if (VERSION.SDK_INT >= 28) DPM.getKeepUninstalledPackages(DAR)?.contains(name) == true else false
if (VERSION.SDK_INT >= 28 && Privilege.status.value.device)
DPM.getKeepUninstalledPackages(DAR)?.contains(name) == true
else false
)
}
// Application details
@RequiresApi(24)
fun adSetPackageSuspended(name: String, status: Boolean) {
DPM.setPackagesSuspended(DAR, arrayOf(name), status)
appStatus.update { it.copy(suspend = DPM.isPackageSuspended(DAR, name)) }
try {
DPM.setPackagesSuspended(DAR, arrayOf(name), status)
appStatus.update { it.copy(suspend = DPM.isPackageSuspended(DAR, name)) }
} catch (_: Exception) {}
}
fun adSetPackageHidden(name: String, status: Boolean) {
DPM.setApplicationHidden(DAR, name, status)
@@ -510,6 +548,79 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
}
}
val appRestrictions = MutableStateFlow(emptyList<AppRestriction>())
fun getAppRestrictions(name: String) {
val rm = application.getSystemService(RestrictionsManager::class.java)
try {
val bundle = DPM.getApplicationRestrictions(DAR, name)
appRestrictions.value = rm.getManifestRestrictions(name)?.mapNotNull {
transformRestrictionEntry(it)
}?.map {
if (bundle.containsKey(it.key)) {
when (it) {
is AppRestriction.BooleanItem -> it.value = bundle.getBoolean(it.key)
is AppRestriction.StringItem -> it.value = bundle.getString(it.key)
is AppRestriction.IntItem -> it.value = bundle.getInt(it.key)
is AppRestriction.ChoiceItem -> it.value = bundle.getString(it.key)
is AppRestriction.MultiSelectItem -> it.value = bundle.getStringArray(it.key)
}
}
it
} ?: emptyList()
} catch (e: Exception) {
e.printStackTrace()
appRestrictions.value = emptyList()
}
}
fun setAppRestrictions(name: String, item: AppRestriction) {
viewModelScope.launch(Dispatchers.IO) {
val bundle = transformAppRestriction(
appRestrictions.value.filter { it.key != item.key }.plus(item)
)
DPM.setApplicationRestrictions(DAR, name, bundle)
getAppRestrictions(name)
}
}
fun clearAppRestrictions(name: String) {
viewModelScope.launch(Dispatchers.IO) {
DPM.setApplicationRestrictions(DAR, name, Bundle())
getAppRestrictions(name)
}
}
fun transformRestrictionEntry(e: RestrictionEntry): AppRestriction? {
return when (e.type) {
RestrictionEntry.TYPE_INTEGER ->
AppRestriction.IntItem(e.key, e.title, e.description, null)
RestrictionEntry.TYPE_STRING ->
AppRestriction.StringItem(e.key, e.title, e.description, null)
RestrictionEntry.TYPE_BOOLEAN ->
AppRestriction.BooleanItem(e.key, e.title, e.description, null)
RestrictionEntry.TYPE_CHOICE -> AppRestriction.ChoiceItem(e.key, e.title,
e.description, e.choiceEntries, e.choiceValues, null)
RestrictionEntry.TYPE_MULTI_SELECT -> AppRestriction.MultiSelectItem(e.key, e.title,
e.description, e.choiceEntries, e.choiceValues, null)
else -> null
}
}
fun transformAppRestriction(list: List<AppRestriction>): Bundle {
val b = Bundle()
for (r in list) {
when (r) {
is AppRestriction.IntItem -> r.value?.let { b.putInt(r.key, it) }
is AppRestriction.StringItem -> r.value?.let { b.putString(r.key, it) }
is AppRestriction.BooleanItem -> r.value?.let { b.putBoolean(r.key, it) }
is AppRestriction.ChoiceItem -> r.value?.let { b.putString(r.key, it) }
is AppRestriction.MultiSelectItem -> r.value?.let { b.putStringArray(r.key, r.value) }
}
}
return b
}
val appGroups = MutableStateFlow(emptyList<AppGroup>())
init {
getAppGroups()
@@ -527,6 +638,20 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
group.filter { it.id != id }
}
}
fun exportAppGroups(uri: Uri) {
application.contentResolver.openOutputStream(uri)!!.use {
val list: List<BasicAppGroup> = appGroups.value
it.write(Json.encodeToString(list).encodeToByteArray())
}
}
fun importAppGroups(uri: Uri) {
application.contentResolver.openInputStream(uri)!!.use {
Json.decodeFromString<List<BasicAppGroup>>(it.readBytes().decodeToString())
}.forEach {
myRepo.setAppGroup(null, it.name, it.apps)
}
getAppGroups()
}
@RequiresApi(24)
fun reboot() {
@@ -584,16 +709,17 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
statusBarDisabled = if (VERSION.SDK_INT >= 34 &&
privilege.run { device || (profile && affiliated) })
DPM.isStatusBarDisabled else false,
autoTimeEnabled = if (VERSION.SDK_INT >= 30 && privilege.run { device || org })
autoTimeEnabled = if (VERSION.SDK_INT >= 30 && (privilege.device || privilege.org))
DPM.getAutoTimeEnabled(DAR) else false,
autoTimeZoneEnabled = if (VERSION.SDK_INT >= 30 && privilege.run { device || org })
autoTimeZoneEnabled = if (VERSION.SDK_INT >= 30 && (privilege.device || privilege.org))
DPM.getAutoTimeZoneEnabled(DAR) else false,
autoTimeRequired = if (VERSION.SDK_INT < 30) DPM.autoTimeRequired else false,
masterVolumeMuted = DPM.isMasterVolumeMuted(DAR),
backupServiceEnabled = if (VERSION.SDK_INT >= 26) DPM.isBackupServiceEnabled(DAR) else false,
btContactSharingDisabled = if (VERSION.SDK_INT >= 23 && privilege.work)
btContactSharingDisabled = if (privilege.work)
DPM.getBluetoothContactSharingDisabled(DAR) else false,
commonCriteriaMode = if (VERSION.SDK_INT >= 30) DPM.isCommonCriteriaModeEnabled(DAR) else false,
commonCriteriaMode = if (VERSION.SDK_INT >= 30 && (privilege.device || privilege.org))
DPM.isCommonCriteriaModeEnabled(DAR) else false,
usbSignalEnabled = if (VERSION.SDK_INT >= 31) DPM.isUsbDataSignalingEnabled else false,
canDisableUsbSignal = if (VERSION.SDK_INT >= 31) DPM.canUsbDataSignalingBeDisabled() else false
)
@@ -609,7 +735,6 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
it.copy(screenCaptureDisabled = DPM.getScreenCaptureDisabled(null))
}
}
@RequiresApi(23)
fun setStatusBarDisabled(disabled: Boolean) {
val result = DPM.setStatusBarDisabled(DAR, disabled)
if (result) systemOptionsStatus.update { it.copy(statusBarDisabled = disabled) }
@@ -643,7 +768,6 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
it.copy(backupServiceEnabled = DPM.isBackupServiceEnabled(DAR))
}
}
@RequiresApi(23)
fun setBtContactSharingDisabled(disabled: Boolean) {
DPM.setBluetoothContactSharingDisabled(DAR, disabled)
systemOptionsStatus.update {
@@ -662,7 +786,6 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
DPM.isUsbDataSignalingEnabled = enabled
systemOptionsStatus.update { it.copy(usbSignalEnabled = DPM.isUsbDataSignalingEnabled) }
}
@RequiresApi(23)
fun setKeyguardDisabled(disabled: Boolean): Boolean {
return DPM.setKeyguardDisabled(DAR, disabled)
}
@@ -732,11 +855,9 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
fun setContentProtectionPolicy(policy: Int) {
DPM.setContentProtectionPolicy(DAR, policy)
}
@RequiresApi(23)
fun getPermissionPolicy(): Int {
return DPM.getPermissionPolicy(DAR)
}
@RequiresApi(23)
fun setPermissionPolicy(policy: Int) {
DPM.setPermissionPolicy(DAR, policy)
}
@@ -784,19 +905,35 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
getLockTaskPackages()
}
@RequiresApi(28)
fun startLockTaskMode(packageName: String, activity: String): Boolean {
fun startLockTaskMode(
packageName: String, activity: String, clearTask: Boolean, showNotification: Boolean
): Boolean {
if (!DPM.isLockTaskPermitted(packageName)) {
val list = lockTaskPackages.value.map { it.name } + packageName
DPM.setLockTaskPackages(DAR, list.toTypedArray())
getLockTaskPackages()
}
if (showNotification) {
DPM.setLockTaskFeatures(
DAR,
DPM.getLockTaskFeatures(DAR) or
DevicePolicyManager.LOCK_TASK_FEATURE_NOTIFICATIONS or
DevicePolicyManager.LOCK_TASK_FEATURE_HOME
)
}
val options = ActivityOptions.makeBasic().setLockTaskEnabled(true)
val intent = if(activity.isNotEmpty()) {
Intent().setComponent(ComponentName(packageName, activity))
} else PM.getLaunchIntentForPackage(packageName)
if (intent != null) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK
or (if (clearTask) Intent.FLAG_ACTIVITY_CLEAR_TASK else 0)
)
application.startActivity(intent, options.toBundle())
if (showNotification) {
application.startForegroundService(Intent(application, LockTaskService::class.java))
}
return true
} else {
return false
@@ -906,14 +1043,12 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
}
}
}
@RequiresApi(23)
fun getSystemUpdatePolicy(): SystemUpdatePolicyInfo {
val policy = DPM.systemUpdatePolicy
return SystemUpdatePolicyInfo(
policy?.policyType ?: -1, policy?.installWindowStart ?: 0, policy?.installWindowEnd ?: 0
)
}
@RequiresApi(23)
fun setSystemUpdatePolicy(info: SystemUpdatePolicyInfo) {
val policy = when (info.type) {
SystemUpdatePolicy.TYPE_INSTALL_AUTOMATIC -> SystemUpdatePolicy.createAutomaticInstallPolicy()
@@ -1227,18 +1362,11 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
}
fun createWorkProfile(options: CreateWorkProfileOptions): Intent {
val intent = Intent(DevicePolicyManager.ACTION_PROVISION_MANAGED_PROFILE)
if (VERSION.SDK_INT >= 23) {
intent.putExtra(
DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME,
MyAdminComponent
)
} else {
intent.putExtra(
DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_PACKAGE_NAME,
application.packageName
)
}
if (options.migrateAccount && VERSION.SDK_INT >= 22) {
intent.putExtra(
DevicePolicyManager.EXTRA_PROVISIONING_DEVICE_ADMIN_COMPONENT_NAME,
MyAdminComponent
)
if (options.migrateAccount) {
intent.putExtra(
DevicePolicyManager.EXTRA_PROVISIONING_ACCOUNT_TO_MIGRATE,
Account(options.accountName, options.accountType)
@@ -1315,10 +1443,10 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
return UserInformation(
if (VERSION.SDK_INT >= 24) UserManager.supportsMultipleUsers() else false,
if (VERSION.SDK_INT >= 31) UserManager.isHeadlessSystemUserMode() else false,
if (VERSION.SDK_INT >= 23) UM.isSystemUser else false,
UM.isSystemUser,
if (VERSION.SDK_INT >= 34) UM.isAdminUser else false,
if (VERSION.SDK_INT >= 25) UM.isDemoUser else false,
if (VERSION.SDK_INT >= 23) UM.getUserCreationTime(uh) else 0,
UM.getUserCreationTime(uh),
if (VERSION.SDK_INT >= 28) DPM.isLogoutEnabled else false,
if (VERSION.SDK_INT >= 28) DPM.isEphemeralUser(DAR) else false,
if (VERSION.SDK_INT >= 28) DPM.isAffiliatedUser else false,
@@ -1354,6 +1482,7 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
UserManager.USER_OPERATION_ERROR_UNKNOWN -> R.string.unknown_error
UserManager.USER_OPERATION_ERROR_MANAGED_PROFILE-> R.string.fail_managed_profile
UserManager.USER_OPERATION_ERROR_MAX_RUNNING_USERS -> R.string.limit_reached
UserManager.USER_OPERATION_ERROR_MAX_USERS -> R.string.limit_reached
UserManager.USER_OPERATION_ERROR_CURRENT_USER -> R.string.fail_current_user
else -> R.string.unknown
}
@@ -1392,7 +1521,6 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
fun setProfileName(name: String) {
DPM.setProfileName(DAR, name)
}
@RequiresApi(23)
fun setUserIcon(bitmap: Bitmap) {
DPM.setUserIcon(DAR, bitmap)
}
@@ -1530,7 +1658,6 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
return PM.getPackageUid(name, 0)
}
var networkStatsData = emptyList<NetworkStatsData>()
@RequiresApi(23)
fun readNetworkStats(stats: NetworkStats): List<NetworkStatsData> {
val list = mutableListOf<NetworkStatsData>()
while (stats.hasNextBucket()) {
@@ -1541,7 +1668,6 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
stats.close()
return list
}
@RequiresApi(23)
fun readNetworkStatsBucket(bucket: NetworkStats.Bucket): NetworkStatsData {
return NetworkStatsData(
bucket.rxBytes, bucket.rxPackets, bucket.txBytes, bucket.txPackets,
@@ -1823,7 +1949,12 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
}
@RequiresApi(26)
fun setRpToken(token: String): Boolean {
return DPM.setResetPasswordToken(DAR, token.encodeToByteArray())
return try {
DPM.setResetPasswordToken(DAR, token.encodeToByteArray())
} catch (e: Exception) {
e.printStackTrace()
false
}
}
@RequiresApi(26)
fun clearRpToken(): Boolean {

View File

@@ -6,7 +6,9 @@ import android.graphics.drawable.Drawable
import android.os.Build
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -22,7 +24,14 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.outlined.Clear
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
@@ -36,6 +45,7 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
@@ -45,8 +55,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
@@ -66,17 +78,20 @@ data class AppInfo(
private fun searchInString(query: String, content: String)
= query.split(' ').all { content.contains(it, true) }
@Serializable data class ApplicationsList(val canSwitchView: Boolean)
@Serializable data class ApplicationsList(val canSwitchView: Boolean, val multiSelect: Boolean)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
fun AppChooserScreen(
canSwitchView: Boolean, packageList: MutableStateFlow<List<AppInfo>>,
params: ApplicationsList, packageList: MutableStateFlow<List<AppInfo>>,
refreshProgress: MutableStateFlow<Float>, onChoosePackage: (String?) -> Unit,
onSwitchView: () -> Unit, onRefresh: () -> Unit
onSwitchView: () -> Unit, onRefresh: () -> Unit,
setPackagesSuspend: (List<String>, Boolean) -> Unit,
setPackagesHidden: (List<String>, Boolean) -> Unit,
) {
val packages by packageList.collectAsStateWithLifecycle()
val context = LocalContext.current
val hf = LocalHapticFeedback.current
val progress by refreshProgress.collectAsStateWithLifecycle()
var system by rememberSaveable { mutableStateOf(false) }
var query by rememberSaveable { mutableStateOf("") }
@@ -85,6 +100,7 @@ fun AppChooserScreen(
system == (it.flags and ApplicationInfo.FLAG_SYSTEM != 0) &&
(query.isEmpty() || (searchInString(query, it.label) || searchInString(query, it.name)))
}
val selectedPackages = remember { mutableStateListOf<AppInfo>() }
val focusMgr = LocalFocusManager.current
LaunchedEffect(Unit) {
if(packages.size <= 1) onRefresh()
@@ -101,18 +117,86 @@ fun AppChooserScreen(
system = !system
context.popToast(if(system) R.string.show_system_app else R.string.show_user_app)
}) {
Icon(painter = painterResource(R.drawable.filter_alt_fill0), contentDescription = null)
Icon(painterResource(R.drawable.filter_alt_fill0), null)
}
IconButton(onRefresh, enabled = progress == 1F) {
Icon(painter = painterResource(R.drawable.refresh_fill0), contentDescription = null)
if (selectedPackages.isEmpty()) {
IconButton(onRefresh, enabled = progress == 1F) {
Icon(Icons.Default.Refresh, null)
}
if (params.canSwitchView) IconButton(onSwitchView) {
Icon(Icons.AutoMirrored.Default.List, null)
}
}
if (canSwitchView) IconButton(onSwitchView) {
Icon(Icons.AutoMirrored.Default.List, null)
}
if (selectedPackages.isNotEmpty()) {
if (params.canSwitchView) {
var dropdown by remember { mutableStateOf(false) }
Box {
IconButton({
dropdown = !dropdown
}) {
Icon(Icons.Default.MoreVert, null)
}
DropdownMenu(dropdown, { dropdown = false }) {
if (Build.VERSION.SDK_INT >= 24) {
DropdownMenuItem(
{ Text(stringResource(R.string.suspend)) },
{
setPackagesSuspend(selectedPackages.map { it.name }, true)
dropdown = false
selectedPackages.clear()
},
leadingIcon = {
Icon(painterResource(R.drawable.block_fill0), null)
}
)
DropdownMenuItem(
{ Text(stringResource(R.string.unsuspend)) },
{
setPackagesSuspend(selectedPackages.map { it.name }, false)
dropdown = false
selectedPackages.clear()
},
leadingIcon = {
Icon(painterResource(R.drawable.enable_fill0), null)
}
)
}
DropdownMenuItem(
{ Text(stringResource(R.string.hide)) },
{
setPackagesHidden(selectedPackages.map { it.name }, true)
dropdown = false
selectedPackages.clear()
},
leadingIcon = {
Icon(painterResource(R.drawable.visibility_off_fill0), null)
}
)
DropdownMenuItem(
{ Text(stringResource(R.string.unhide)) },
{
setPackagesHidden(selectedPackages.map { it.name }, false)
dropdown = false
selectedPackages.clear()
},
leadingIcon = {
Icon(painterResource(R.drawable.visibility_fill0), null)
}
)
}
}
} else {
FilledIconButton({
onChoosePackage(selectedPackages.joinToString("\n") { it.name })
}) {
Icon(Icons.Default.Check, null)
}
}
}
},
title = {
if(searchMode) {
if (searchMode) {
val fr = remember { FocusRequester() }
LaunchedEffect(Unit) { fr.requestFocus() }
OutlinedTextField(
@@ -122,19 +206,20 @@ fun AppChooserScreen(
keyboardActions = KeyboardActions { focusMgr.clearFocus() },
placeholder = { Text(stringResource(R.string.search)) },
trailingIcon = {
Icon(
painter = painterResource(R.drawable.close_fill0),
contentDescription = null,
modifier = Modifier.clickable {
focusMgr.clearFocus()
query = ""
searchMode = false
}
)
IconButton({
query = ""
searchMode = false
}) {
Icon(Icons.Outlined.Clear, null)
}
},
textStyle = typography.bodyLarge,
modifier = Modifier.fillMaxWidth().focusRequester(fr)
)
} else {
if (selectedPackages.isNotEmpty()) {
Text(selectedPackages.size.toString())
}
}
},
navigationIcon = {
@@ -156,10 +241,24 @@ fun AppChooserScreen(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable {
focusMgr.clearFocus()
onChoosePackage(it.name)
}
.combinedClickable(onLongClick = {
if (params.multiSelect && it !in selectedPackages) {
selectedPackages += it
hf.performHapticFeedback(HapticFeedbackType.LongPress)
}
}, onClick = {
if (selectedPackages.isEmpty()) {
focusMgr.clearFocus()
onChoosePackage(it.name)
} else {
if (it in selectedPackages) selectedPackages -= it
else selectedPackages += it
}
})
.background(
if (it in selectedPackages) MaterialTheme.colorScheme.primaryContainer
else MaterialTheme.colorScheme.background
)
.padding(horizontal = 8.dp, vertical = 10.dp)
.animateItem()
) {

View File

@@ -1,31 +1,17 @@
package com.bintianqi.owndroid
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.admin.DeviceAdminReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.Binder
import android.os.Build.VERSION
import android.os.UserHandle
import android.os.UserManager
import androidx.core.app.NotificationCompat
import com.bintianqi.owndroid.dpm.handlePrivilegeChange
import com.bintianqi.owndroid.dpm.retrieveNetworkLogs
import com.bintianqi.owndroid.dpm.retrieveSecurityLogs
class Receiver : DeviceAdminReceiver() {
override fun onReceive(context: Context, intent: Intent) {
super.onReceive(context, intent)
if(VERSION.SDK_INT >= 26 && intent.action == "com.bintianqi.owndroid.action.STOP_LOCK_TASK_MODE") {
val receiver = ComponentName(context, this::class.java)
val packages = Privilege.DPM.getLockTaskPackages(receiver)
Privilege.DPM.setLockTaskPackages(receiver, arrayOf())
Privilege.DPM.setLockTaskPackages(receiver, packages)
}
}
override fun onEnabled(context: Context, intent: Intent) {
super.onEnabled(context, intent)
Privilege.updateStatus()
@@ -56,25 +42,6 @@ class Receiver : DeviceAdminReceiver() {
}
}
override fun onLockTaskModeEntering(context: Context, intent: Intent, pkg: String) {
super.onLockTaskModeEntering(context, intent, pkg)
val stopIntent = Intent(context, this::class.java)
.setAction("com.bintianqi.owndroid.action.STOP_LOCK_TASK_MODE")
val pendingIntent = PendingIntent.getBroadcast(context, 0, stopIntent, PendingIntent.FLAG_IMMUTABLE)
val notification = NotificationCompat.Builder(context, MyNotificationChannel.LockTaskMode.id)
.setContentTitle(context.getText(R.string.lock_task_mode))
.setSmallIcon(R.drawable.lock_fill0)
.addAction(NotificationCompat.Action.Builder(null, context.getString(R.string.stop), pendingIntent).build())
.build()
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
nm.notify(NotificationType.LockTaskMode.id, notification)
}
override fun onLockTaskModeExiting(context: Context, intent: Intent) {
super.onLockTaskModeExiting(context, intent)
NotificationUtils.cancel(context, NotificationType.LockTaskMode)
}
override fun onPasswordChanged(context: Context, intent: Intent, userHandle: UserHandle) {
super.onPasswordChanged(context, intent, userHandle)
sendUserRelatedNotification(context, userHandle, NotificationType.PasswordChanged)

View File

@@ -232,7 +232,7 @@ fun AppLockSettingsScreen(
config: AppLockConfig, setConfig: (AppLockConfig) -> Unit,
onNavigateUp: () -> Unit
) = MyScaffold(R.string.app_lock, onNavigateUp) {
var password by rememberSaveable { mutableStateOf(config.password ?: "") }
var password by rememberSaveable { mutableStateOf("") }
var confirmPassword by rememberSaveable { mutableStateOf("") }
var allowBiometrics by rememberSaveable { mutableStateOf(config.biometrics) }
var lockWhenLeaving by rememberSaveable { mutableStateOf(config.whenLeaving) }

View File

@@ -1,9 +1,12 @@
package com.bintianqi.owndroid
import android.content.BroadcastReceiver
import android.content.ClipData
import android.content.ClipboardManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageInfo
import android.net.Uri
import android.os.Build
@@ -159,4 +162,20 @@ fun Modifier.clickableTextField(onClick: () -> Unit) =
fun adaptiveInsets(): WindowInsets {
val navbar = WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)
return WindowInsets.ime.union(navbar).union(WindowInsets.displayCutout)
}
}
fun registerPackageRemovedReceiver(
ctx: Context, callback: (String) -> Unit
) {
val br = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
callback(intent.data!!.schemeSpecificPart)
}
}
val filter = IntentFilter()
filter.addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED)
filter.addDataScheme("package")
ctx.registerReceiver(br, filter)
}
fun parsePackageNames(input: String) = input.split('\n').filter { it.isNotEmpty() }

File diff suppressed because it is too large Load Diff

View File

@@ -152,7 +152,7 @@ fun NetworkScreen(onNavigateUp: () -> Unit, onNavigate: (Any) -> Unit) {
if(VERSION.SDK_INT >= 30) {
FunctionItem(R.string.options, icon = R.drawable.tune_fill0) { onNavigate(NetworkOptions) }
}
if (VERSION.SDK_INT >= 23 && !privilege.dhizuku)
if (!privilege.dhizuku)
FunctionItem(R.string.network_stats, icon = R.drawable.query_stats_fill0) { onNavigate(QueryNetworkStats) }
if(VERSION.SDK_INT >= 29 && privilege.device) {
FunctionItem(R.string.private_dns, icon = R.drawable.dns_fill0) { onNavigate(PrivateDns) }
@@ -186,9 +186,9 @@ fun NetworkOptionsScreen(
) {
val privilege by Privilege.status.collectAsStateWithLifecycle()
var dialog by rememberSaveable { mutableIntStateOf(0) }
var lanEnabled by rememberSaveable { mutableStateOf(getLanEnabled()) }
MyScaffold(R.string.options, onNavigateUp, 0.dp) {
if(VERSION.SDK_INT >= 30 && (privilege.device || privilege.org)) {
var lanEnabled by rememberSaveable { mutableStateOf(getLanEnabled()) }
SwitchItem(R.string.lockdown_admin_configured_network, icon = R.drawable.wifi_password_fill0,
state = lanEnabled,
onCheckedChange = {
@@ -937,7 +937,6 @@ enum class NetworkStatsState(val id: Int, val text: Int) {
Default(NetworkStats.Bucket.STATE_DEFAULT, R.string.default_str),
Foreground(NetworkStats.Bucket.STATE_FOREGROUND, R.string.foreground)
}
@RequiresApi(23)
enum class NetworkStatsUID(val uid: Int, val text: Int) {
All(NetworkStats.Bucket.UID_ALL, R.string.all),
Removed(NetworkStats.Bucket.UID_REMOVED, R.string.uninstalled),
@@ -952,7 +951,6 @@ data class QueryNetworkStatsParams(
@Serializable object QueryNetworkStats
@OptIn(ExperimentalMaterial3Api::class)
@RequiresApi(23)
@Composable
fun NetworkStatsScreen(
chosenPackage: Channel<String>, onChoosePackage: () -> Unit, getUid: (String) -> Int,
@@ -1270,7 +1268,6 @@ data class NetworkStatsData(
@Serializable object NetworkStatsViewer
@RequiresApi(23)
@Composable
fun NetworkStatsViewerScreen(
data: List<NetworkStatsData>, clearData: () -> Unit, onNavigateUp: () -> Unit

View File

@@ -97,7 +97,9 @@ fun PasswordScreen(vm: MyViewModel,onNavigateUp: () -> Unit, onNavigate: (Any) -
if(privilege.device) {
FunctionItem(R.string.max_time_to_lock, icon = R.drawable.schedule_fill0) { dialog = 1 }
FunctionItem(R.string.pwd_expiration_timeout, icon = R.drawable.lock_clock_fill0) { dialog = 3 }
FunctionItem(R.string.max_pwd_fail, icon = R.drawable.no_encryption_fill0) { dialog = 4 }
if (SP.displayDangerousFeatures) {
FunctionItem(R.string.max_pwd_fail, icon = R.drawable.no_encryption_fill0) { dialog = 4 }
}
}
if(VERSION.SDK_INT >= 26) {
FunctionItem(R.string.required_strong_auth_timeout, icon = R.drawable.fingerprint_off_fill0) { dialog = 2 }
@@ -333,12 +335,10 @@ fun ResetPasswordScreen(resetPassword: (String, String, Int) -> Boolean, onNavig
visualTransformation = PasswordVisualTransformation()
)
Spacer(Modifier.padding(vertical = 5.dp))
if(VERSION.SDK_INT >= 23) {
CheckBoxItem(
R.string.do_not_ask_credentials_on_boot,
flags and RESET_PASSWORD_DO_NOT_ASK_CREDENTIALS_ON_BOOT != 0
) { flags = flags xor RESET_PASSWORD_DO_NOT_ASK_CREDENTIALS_ON_BOOT }
}
CheckBoxItem(
R.string.do_not_ask_credentials_on_boot,
flags and RESET_PASSWORD_DO_NOT_ASK_CREDENTIALS_ON_BOOT != 0
) { flags = flags xor RESET_PASSWORD_DO_NOT_ASK_CREDENTIALS_ON_BOOT }
CheckBoxItem(
R.string.reset_password_require_entry,
flags and RESET_PASSWORD_REQUIRE_ENTRY != 0

View File

@@ -72,7 +72,6 @@ import androidx.compose.material3.PrimaryTabRow
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Slider
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TimePicker
@@ -109,9 +108,9 @@ import com.bintianqi.owndroid.MyViewModel
import com.bintianqi.owndroid.Privilege
import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.SP
import com.bintianqi.owndroid.adaptiveInsets
import com.bintianqi.owndroid.clickableTextField
import com.bintianqi.owndroid.formatDate
import com.bintianqi.owndroid.adaptiveInsets
import com.bintianqi.owndroid.popToast
import com.bintianqi.owndroid.showOperationResultToast
import com.bintianqi.owndroid.ui.CheckBoxItem
@@ -172,16 +171,14 @@ fun SystemManagerScreen(
FunctionItem(R.string.key_pairs, icon = R.drawable.key_vertical_fill0) { navCtrl.navigate("KeyPairs") }*/
if(VERSION.SDK_INT >= 35 && (privilege.device || (privilege.profile && privilege.affiliated)))
FunctionItem(R.string.content_protection_policy, icon = R.drawable.search_fill0) { onNavigate(ContentProtectionPolicy) }
if(VERSION.SDK_INT >= 23) {
FunctionItem(R.string.permission_policy, icon = R.drawable.key_fill0) { onNavigate(PermissionPolicy) }
}
FunctionItem(R.string.permission_policy, icon = R.drawable.key_fill0) { onNavigate(PermissionPolicy) }
if(VERSION.SDK_INT >= 34 && privilege.device) {
FunctionItem(R.string.mte_policy, icon = R.drawable.memory_fill0) { onNavigate(MtePolicy) }
}
if(VERSION.SDK_INT >= 31) {
FunctionItem(R.string.nearby_streaming_policy, icon = R.drawable.share_fill0) { onNavigate(NearbyStreamingPolicy) }
}
if (VERSION.SDK_INT >= 28 && privilege.device && !privilege.dhizuku) {
if (VERSION.SDK_INT >= 28 && privilege.device) {
FunctionItem(R.string.lock_task_mode, icon = R.drawable.lock_fill0) { onNavigate(LockTaskMode) }
}
FunctionItem(R.string.ca_cert, icon = R.drawable.license_fill0) { onNavigate(CaCert) }
@@ -205,7 +202,7 @@ fun SystemManagerScreen(
FunctionItem(R.string.support_messages, icon = R.drawable.chat_fill0) { onNavigate(SupportMessage) }
}
FunctionItem(R.string.disable_account_management, icon = R.drawable.account_circle_fill0) { onNavigate(DisableAccountManagement) }
if(VERSION.SDK_INT >= 23 && (privilege.device || privilege.org)) {
if (privilege.device || privilege.org) {
FunctionItem(R.string.system_update_policy, icon = R.drawable.system_update_fill0) { onNavigate(SetSystemUpdatePolicy) }
}
if(VERSION.SDK_INT >= 29 && (privilege.device || privilege.org)) {
@@ -369,7 +366,7 @@ fun SystemOptionsScreen(vm: MyViewModel, onNavigateUp: () -> Unit) {
SwitchItem(R.string.enable_usb_signal, status.usbSignalEnabled,
vm::setUsbSignalEnabled, R.drawable.usb_fill0)
}
if (VERSION.SDK_INT >= 23 && VERSION.SDK_INT < 34) {
if (VERSION.SDK_INT < 34) {
Row(
Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding),
verticalAlignment = Alignment.CenterVertically
@@ -414,8 +411,8 @@ fun KeyguardScreen(
val context = LocalContext.current
val privilege by Privilege.status.collectAsStateWithLifecycle()
MyScaffold(R.string.keyguard, onNavigateUp) {
if (VERSION.SDK_INT >= 23 && (privilege.device ||
(VERSION.SDK_INT >= 28 && privilege.profile && privilege.affiliated))) {
if (privilege.device ||
(VERSION.SDK_INT >= 28 && privilege.profile && privilege.affiliated)) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
@@ -436,7 +433,7 @@ fun KeyguardScreen(
Notes(R.string.info_disable_keyguard)
Spacer(Modifier.padding(vertical = 12.dp))
}
if(VERSION.SDK_INT >= 23) Text(text = stringResource(R.string.lock_now), style = typography.headlineLarge)
Text(text = stringResource(R.string.lock_now), style = typography.headlineLarge)
Spacer(Modifier.padding(vertical = 2.dp))
var evictKey by rememberSaveable { mutableStateOf(false) }
Button(
@@ -1008,7 +1005,6 @@ fun ContentProtectionPolicyScreen(
@Serializable object PermissionPolicy
@RequiresApi(23)
@Composable
fun PermissionPolicyScreen(
getPolicy: () -> Int, setPolicy: (Int) -> Unit, onNavigateUp: () -> Unit
@@ -1148,9 +1144,10 @@ fun NearbyStreamingPolicyScreen(
@RequiresApi(28)
@Composable
fun LockTaskModeScreen(
chosenPackage: Channel<String>, onChoosePackage: () -> Unit,
chosenPackage: Channel<String>, chooseSinglePackage: () -> Unit, choosePackage: () -> Unit,
lockTaskPackages: StateFlow<List<AppInfo>>, getLockTaskPackages: () -> Unit,
setLockTaskPackage: (String, Boolean) -> Unit, startLockTaskMode: (String, String) -> Boolean,
setLockTaskPackage: (String, Boolean) -> Unit,
startLockTaskMode: (String, String, Boolean, Boolean) -> Boolean,
getLockTaskFeatures: () -> Int, setLockTaskFeature: (Int) -> String?, onNavigateUp: () -> Unit
) {
val coroutine = rememberCoroutineScope()
@@ -1175,7 +1172,7 @@ fun LockTaskModeScreen(
.fillMaxSize()
.padding(paddingValues)
) {
TabRow(tabIndex) {
PrimaryTabRow(tabIndex) {
Tab(
tabIndex == 0, onClick = { coroutine.launch { pagerState.animateScrollToPage(0) } },
text = { Text(stringResource(R.string.start)) }
@@ -1191,9 +1188,9 @@ fun LockTaskModeScreen(
}
HorizontalPager(pagerState, verticalAlignment = Alignment.Top) { page ->
if(page == 0) {
StartLockTaskMode(startLockTaskMode, chosenPackage, onChoosePackage)
StartLockTaskMode(startLockTaskMode, chosenPackage, chooseSinglePackage)
} else if (page == 1) {
LockTaskPackages(chosenPackage, onChoosePackage, lockTaskPackages, setLockTaskPackage)
LockTaskPackages(chosenPackage, choosePackage, lockTaskPackages, setLockTaskPackage)
} else {
LockTaskFeatures(getLockTaskFeatures, setLockTaskFeature)
}
@@ -1205,29 +1202,39 @@ fun LockTaskModeScreen(
@RequiresApi(28)
@Composable
private fun StartLockTaskMode(
startLockTaskMode: (String, String) -> Boolean,
startLockTaskMode: (String, String, Boolean, Boolean) -> Boolean,
chosenPackage: Channel<String>, onChoosePackage: () -> Unit
) {
val context = LocalContext.current
val focusMgr = LocalFocusManager.current
val privilege by Privilege.status.collectAsStateWithLifecycle()
var packageName by rememberSaveable { mutableStateOf("") }
var activity by rememberSaveable { mutableStateOf("") }
var specifyActivity by rememberSaveable { mutableStateOf(false) }
var clearTask by rememberSaveable { mutableStateOf(true) }
var showNotification by rememberSaveable { mutableStateOf(true) }
LaunchedEffect(Unit) {
packageName = chosenPackage.receive()
}
Column(
Modifier
.fillMaxWidth()
.padding(horizontal = HorizontalPadding)
.verticalScroll(rememberScrollState())
) {
Spacer(Modifier.height(5.dp))
PackageNameTextField(packageName, onChoosePackage) { packageName = it }
PackageNameTextField(
packageName, onChoosePackage, Modifier.padding(HorizontalPadding, 8.dp)
) { packageName = it }
FullWidthCheckBoxItem(
R.string.lock_task_mode_start_clear_task, clearTask
) { clearTask = it }
FullWidthCheckBoxItem(
R.string.lock_task_mode_show_notification, showNotification
) { showNotification = it }
Row(
Modifier
.fillMaxWidth()
.padding(vertical = 4.dp), verticalAlignment = Alignment.CenterVertically
.padding(start = 4.dp, top = 4.dp, end = HorizontalPadding, bottom = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(specifyActivity, {
specifyActivity = it
@@ -1246,16 +1253,17 @@ private fun StartLockTaskMode(
Button(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 5.dp),
.padding(horizontal = HorizontalPadding),
onClick = {
val result = startLockTaskMode(packageName, activity)
val result = startLockTaskMode(packageName, activity, clearTask, showNotification)
if (!result) context.showOperationResultToast(false)
},
enabled = packageName.isNotBlank() && (!specifyActivity || activity.isNotBlank())
) {
Text(stringResource(R.string.start))
}
Notes(R.string.info_start_lock_task_mode)
Spacer(Modifier.height(5.dp))
if (!privilege.dhizuku) Notes(R.string.info_start_lock_task_mode)
}
}
@@ -1774,7 +1782,7 @@ fun WipeDataScreen(
FullWidthCheckBoxItem(R.string.wipe_external_storage, flag and WIPE_EXTERNAL_STORAGE != 0) {
flag = flag xor WIPE_EXTERNAL_STORAGE
}
if(VERSION.SDK_INT >= 22 && privilege.device) FullWidthCheckBoxItem(
if (privilege.device) FullWidthCheckBoxItem(
R.string.wipe_reset_protection_data, flag and WIPE_RESET_PROTECTION_DATA != 0) {
flag = flag xor WIPE_RESET_PROTECTION_DATA
}
@@ -1828,7 +1836,7 @@ fun WipeDataScreen(
text = {
Text(
text = stringResource(
if(VERSION.SDK_INT >= 23 && userManager.isSystemUser) R.string.wipe_data_warning
if (userManager.isSystemUser) R.string.wipe_data_warning
else R.string.info_wipe_data_in_managed_user
),
color = colorScheme.error
@@ -1869,7 +1877,6 @@ data class PendingSystemUpdateInfo(val exists: Boolean, val time: Long, val secu
@Serializable object SetSystemUpdatePolicy
@RequiresApi(23)
@Composable
fun SystemUpdatePolicyScreen(
getPolicy: () -> SystemUpdatePolicyInfo, setPolicy: (SystemUpdatePolicyInfo) -> Unit,

View File

@@ -30,12 +30,12 @@ import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuAnchorType
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MenuAnchorType
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
@@ -105,25 +105,23 @@ fun UsersScreen(vm: MyViewModel, onNavigateUp: () -> Unit, onNavigate: (Any) ->
FunctionItem(R.string.create_user, icon = R.drawable.person_add_fill0) { onNavigate(CreateUser) }
}
FunctionItem(R.string.change_username, icon = R.drawable.edit_fill0) { onNavigate(ChangeUsername) }
if(VERSION.SDK_INT >= 23) {
var changeUserIconDialog by remember { mutableStateOf(false) }
var bitmap: Bitmap? by remember { mutableStateOf(null) }
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
if(it != null) uriToStream(context, it) { stream ->
bitmap = BitmapFactory.decodeStream(stream)
if(bitmap != null) changeUserIconDialog = true
}
var changeUserIconDialog by remember { mutableStateOf(false) }
var bitmap: Bitmap? by remember { mutableStateOf(null) }
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
if(it != null) uriToStream(context, it) { stream ->
bitmap = BitmapFactory.decodeStream(stream)
if(bitmap != null) changeUserIconDialog = true
}
FunctionItem(R.string.change_user_icon, icon = R.drawable.account_circle_fill0) {
context.popToast(R.string.select_an_image)
launcher.launch("image/*")
}
if (changeUserIconDialog) ChangeUserIconDialog(
bitmap!!, {
vm.setUserIcon(bitmap!!)
changeUserIconDialog = false
}) { changeUserIconDialog = false }
}
FunctionItem(R.string.change_user_icon, icon = R.drawable.account_circle_fill0) {
context.popToast(R.string.select_an_image)
launcher.launch("image/*")
}
if (changeUserIconDialog) ChangeUserIconDialog(
bitmap!!, {
vm.setUserIcon(bitmap!!)
changeUserIconDialog = false
}) { changeUserIconDialog = false }
if(VERSION.SDK_INT >= 28 && privilege.device) {
FunctionItem(R.string.user_session_msg, icon = R.drawable.notifications_fill0) { onNavigate(UserSessionMessage) }
}
@@ -191,7 +189,7 @@ fun UserInfoScreen(getInfo: () -> UserInformation, onNavigateUp: () -> Unit) {
if (VERSION.SDK_INT >= 24) InfoItem(R.string.support_multiuser, info.multiUser.yesOrNo)
if (VERSION.SDK_INT >= 31) InfoItem(R.string.headless_system_user_mode, info.headless.yesOrNo, true) { infoDialog = 1 }
Spacer(Modifier.height(8.dp))
if (VERSION.SDK_INT >= 23) InfoItem(R.string.system_user, info.system.yesOrNo)
InfoItem(R.string.system_user, info.system.yesOrNo)
if (VERSION.SDK_INT >= 34) InfoItem(R.string.admin_user, info.admin.yesOrNo)
if (VERSION.SDK_INT >= 25) InfoItem(R.string.demo_user, info.demo.yesOrNo)
if (info.time != 0L) InfoItem(R.string.creation_time, formatDate(info.time))
@@ -264,7 +262,7 @@ fun UserOperationScreen(
input, { input = it },
Modifier
.fillMaxWidth()
.menuAnchor(MenuAnchorType.PrimaryEditable)
.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable)
.padding(top = 4.dp, bottom = 8.dp),
label = {
Text(stringResource(if(useUserId) R.string.user_id else R.string.serial_number))
@@ -586,7 +584,6 @@ fun UserSessionMessageScreen(
}
}
@RequiresApi(23)
@Composable
private fun ChangeUserIconDialog(bitmap: Bitmap, onSet: () -> Unit, onClose: () -> Unit) {
AlertDialog(

View File

@@ -101,31 +101,29 @@ fun CreateWorkProfileScreen(
var migrateAccountName by remember { mutableStateOf("") }
var migrateAccountType by remember { mutableStateOf("") }
var keepAccount by remember { mutableStateOf(true) }
if (VERSION.SDK_INT >= 22) {
FullWidthCheckBoxItem(R.string.migrate_account, migrateAccount) { migrateAccount = it }
AnimatedVisibility(migrateAccount) {
val fr = FocusRequester()
Column(modifier = Modifier.padding(start = 10.dp)) {
OutlinedTextField(
value = migrateAccountName, onValueChange = { migrateAccountName = it },
label = { Text(stringResource(R.string.account_name)) },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions { fr.requestFocus() },
modifier = Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding)
)
OutlinedTextField(
value = migrateAccountType, onValueChange = { migrateAccountType = it },
label = { Text(stringResource(R.string.account_type)) },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions { focusMgr.clearFocus() },
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = HorizontalPadding)
.focusRequester(fr)
)
if(VERSION.SDK_INT >= 26) {
FullWidthCheckBoxItem(R.string.keep_account, keepAccount) { keepAccount = it }
}
FullWidthCheckBoxItem(R.string.migrate_account, migrateAccount) { migrateAccount = it }
AnimatedVisibility(migrateAccount) {
val fr = FocusRequester()
Column(modifier = Modifier.padding(start = 10.dp)) {
OutlinedTextField(
value = migrateAccountName, onValueChange = { migrateAccountName = it },
label = { Text(stringResource(R.string.account_name)) },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions { fr.requestFocus() },
modifier = Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding)
)
OutlinedTextField(
value = migrateAccountType, onValueChange = { migrateAccountType = it },
label = { Text(stringResource(R.string.account_type)) },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions { focusMgr.clearFocus() },
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = HorizontalPadding)
.focusRequester(fr)
)
if(VERSION.SDK_INT >= 26) {
FullWidthCheckBoxItem(R.string.keep_account, keepAccount) { keepAccount = it }
}
}
}

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M680,600q-17,0 -28.5,-11.5T640,560v-160q0,-17 11.5,-28.5T680,360h120q17,0 28.5,11.5T840,400v40h-60v-20h-80v120h80v-20h60v40q0,17 -11.5,28.5T800,600L680,600ZM380,600v-240h160q17,0 28.5,11.5T580,400v40q0,17 -11.5,28.5T540,480q17,0 28.5,11.5T580,520v40q0,17 -11.5,28.5T540,600L380,600ZM440,450h80v-30h-80v30ZM440,540h80v-30h-80v30ZM120,600v-200q0,-17 11.5,-28.5T160,360h120q17,0 28.5,11.5T320,400v200h-60v-60h-80v60h-60ZM180,480h80v-60h-80v60Z"
android:fillColor="#000000"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="m424,648 l282,-282 -56,-56 -226,226 -114,-114 -56,56 170,170ZM200,840q-33,0 -56.5,-23.5T120,760v-560q0,-33 23.5,-56.5T200,120h560q33,0 56.5,23.5T840,200v560q0,33 -23.5,56.5T760,840L200,840ZM200,760h560v-560L200,200v560ZM200,200v560,-560Z"
android:fillColor="#000000"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M360,800q-33,0 -56.5,-23.5T280,720q0,-33 23.5,-56.5T360,640q33,0 56.5,23.5T440,720q0,33 -23.5,56.5T360,800ZM600,800q-33,0 -56.5,-23.5T520,720q0,-33 23.5,-56.5T600,640q33,0 56.5,23.5T680,720q0,33 -23.5,56.5T600,800ZM360,560q-33,0 -56.5,-23.5T280,480q0,-33 23.5,-56.5T360,400q33,0 56.5,23.5T440,480q0,33 -23.5,56.5T360,560ZM600,560q-33,0 -56.5,-23.5T520,480q0,-33 23.5,-56.5T600,400q33,0 56.5,23.5T680,480q0,33 -23.5,56.5T600,560ZM360,320q-33,0 -56.5,-23.5T280,240q0,-33 23.5,-56.5T360,160q33,0 56.5,23.5T440,240q0,33 -23.5,56.5T360,320ZM600,320q-33,0 -56.5,-23.5T520,240q0,-33 23.5,-56.5T600,160q33,0 56.5,23.5T680,240q0,33 -23.5,56.5T600,320Z"
android:fillColor="#000000"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M480,480ZM202,895l-56,-57 118,-118h-90v-80h226v226h-80v-89L202,895ZM480,880v-80h240v-440L520,360v-200L240,160v400h-80v-400q0,-33 23.5,-56.5T240,80h320l240,240v480q0,33 -23.5,56.5T720,880L480,880Z"
android:fillColor="#000000"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M240,880q-33,0 -56.5,-23.5T160,800v-640q0,-33 23.5,-56.5T240,80h320l240,240v240h-80v-200L520,360v-200L240,160v640h360v80L240,880ZM878,895L760,777v89h-80v-226h226v80h-90l118,118 -56,57ZM240,800v-640,640Z"
android:fillColor="#000000"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M220,600v-180h-60v-60h120v240h-60ZM360,600v-100q0,-17 11.5,-28.5T400,460h80v-40L360,420v-60h140q17,0 28.5,11.5T540,400v60q0,17 -11.5,28.5T500,500h-80v40h120v60L360,600ZM600,600v-60h120v-40h-80v-40h80v-40L600,420v-60h140q17,0 28.5,11.5T780,400v160q0,17 -11.5,28.5T740,600L600,600Z"
android:fillColor="#000000"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M480,680q83,0 141.5,-58.5T680,480q0,-83 -58.5,-141.5T480,280q-83,0 -141.5,58.5T280,480q0,83 58.5,141.5T480,680ZM480,880q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q83,0 156,31.5T763,197q54,54 85.5,127T880,480q0,83 -31.5,156T763,763q-54,54 -127,85.5T480,880ZM480,800q134,0 227,-93t93,-227q0,-134 -93,-227t-227,-93q-134,0 -227,93t-93,227q0,134 93,227t227,93Z"
android:fillColor="#000000"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M280,720q-100,0 -170,-70T40,480q0,-100 70,-170t170,-70h400q100,0 170,70t70,170q0,100 -70,170t-170,70L280,720ZM280,640h400q66,0 113,-47t47,-113q0,-66 -47,-113t-113,-47L280,320q-66,0 -113,47t-47,113q0,66 47,113t113,47ZM280,600q50,0 85,-35t35,-85q0,-50 -35,-85t-85,-35q-50,0 -85,35t-35,85q0,50 35,85t85,35ZM480,480Z"
android:fillColor="#000000"/>
</vector>

View File

@@ -0,0 +1,11 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:tint="#000000"
android:viewportHeight="960"
android:viewportWidth="960"
android:width="24dp">
<path
android:fillColor="@android:color/white"
android:pathData="M480,640Q555,640 607.5,587.5Q660,535 660,460Q660,385 607.5,332.5Q555,280 480,280Q405,280 352.5,332.5Q300,385 300,460Q300,535 352.5,587.5Q405,640 480,640ZM480,568Q435,568 403.5,536.5Q372,505 372,460Q372,415 403.5,383.5Q435,352 480,352Q525,352 556.5,383.5Q588,415 588,460Q588,505 556.5,536.5Q525,568 480,568ZM480,760Q334,760 214,678.5Q94,597 40,460Q94,323 214,241.5Q334,160 480,160Q626,160 746,241.5Q866,323 920,460Q866,597 746,678.5Q626,760 480,760ZM480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460ZM480,680Q593,680 687.5,620.5Q782,561 832,460Q782,359 687.5,299.5Q593,240 480,240Q367,240 272.5,299.5Q178,359 128,460Q178,561 272.5,620.5Q367,680 480,680Z"/>
</vector>

View File

@@ -278,7 +278,6 @@
<string name="network_logging">Сетевой журнал</string>
<string name="delete_logs">Удалить журналы</string>
<string name="export_logs">Экспортировать журналы</string>
<string name="wifi_auth_keypair">Пара ключей Wi-Fi</string>
<string name="preferential_network_service">Предпочтительная сетевая служба</string>
<string name="add_config">Add config</string> <!--TODO-->
<string name="network_id">Идентификатор сети</string>

View File

@@ -307,7 +307,6 @@
<string name="network_logging">Ağ Kayıtları</string>
<string name="delete_logs">Kayıtları Sil</string>
<string name="export_logs">Kayıtları Dışa Aktar</string>
<string name="wifi_auth_keypair">Wi-Fi Kimlik Doğrulama Anahtar Çifti</string>
<string name="preferential_network_service">Tercihli Ağ Servisi</string>
<string name="add_config">Yapılandırma Ekle</string>
<string name="network_id">Ağ Kimliği</string>

View File

@@ -68,6 +68,7 @@
<string name="timeout">超时</string>
<string name="continue_str">继续</string>
<string name="exit">退出</string>
<string name="undo">撤销</string>
<!--Permissions-->
<string name="profile_owner">Profile owner</string>
@@ -173,6 +174,8 @@
<string name="nearby_notification_streaming">附近通知传输</string>
<string name="enable_if_secure_enough">在足够安全时启用</string>
<string name="lock_task_mode">锁定任务模式</string>
<string name="lock_task_mode_start_clear_task">清除任务(新实例)</string>
<string name="lock_task_mode_show_notification">显示通知以退出</string>
<string name="app_not_allowed">应用未被允许</string>
<string name="disable_all">禁用全部</string>
<string name="ltf_sys_info">允许状态栏信息</string>
@@ -293,7 +296,6 @@
<string name="network_logs_collected">网络日志已收集</string>
<string name="delete_logs">删除日志</string>
<string name="export_logs">导出日志</string>
<string name="wifi_auth_keypair">Wi-Fi密钥对</string>
<string name="preferential_network_service">首选网络服务</string>
<string name="add_config">添加配置</string>
<string name="network_id">网络ID</string>
@@ -343,7 +345,9 @@
<string name="show_user_app">显示用户应用</string>
<string name="show_system_app">显示系统应用</string>
<string name="suspend">挂起</string>
<string name="unsuspend">取消挂起</string>
<string name="hide">隐藏</string>
<string name="unhide">取消隐藏</string>
<string name="always_on_vpn">VPN保持打开</string>
<string name="enable_lockdown">启用锁定</string>
<string name="clear_current_config">清除当前配置</string>
@@ -362,6 +366,7 @@
<string name="permitted_ime">许可的输入法</string>
<string name="keep_uninstalled_packages">卸载后保留的应用</string>
<string name="clear_app_storage">清除应用存储</string>
<string name="clear_app_storage_confirmation">你确定要删除应用数据吗?</string>
<string name="set_default_dialer">设置默认拨号器</string>
<string name="uninstall_app">卸载应用</string>
<string name="install_app">安装应用</string>
@@ -371,9 +376,14 @@
<string name="search">搜索</string>
<string name="app_group">应用组</string>
<string name="manage_app_groups">管理组</string>
<string name="import_str">导入</string>
<string name="edit_app_group">编辑组</string>
<string name="add_to_list">添加到列表</string>
<string name="remove_from_list">从列表中移除</string>
<string name="managed_configuration">托管配置</string>
<string name="clear_configurations">清除配置</string>
<string name="specify_value">指定值</string>
<string name="package_removed">移除了 %1$s</string>
<!--UserRestriction-->
<string name="user_restriction">用户限制</string>

View File

@@ -73,6 +73,7 @@
<string name="timeout">Timeout</string>
<string name="continue_str">Continue</string>
<string name="exit">Exit</string>
<string name="undo">Undo</string>
<!--Permissions-->
<string name="profile_owner">Profile owner</string>
@@ -102,7 +103,7 @@
<string name="org_name">Organization name</string>
<string name="disable_account_management">Disable account management</string>
<string name="account_type">Account type</string>
<string name="transfer_ownership">Transfer Ownership</string>
<string name="transfer_ownership">Transfer ownership</string>
<string name="lock_screen_info">Lock screen info</string>
<string name="support_messages">Support Messages</string>
<string name="short_support_msg">Short message</string>
@@ -201,6 +202,8 @@
<string name="nearby_notification_streaming">Nearby notification streaming policy</string>
<string name="enable_if_secure_enough">Same managed account only</string>
<string name="lock_task_mode">Lock task mode</string>
<string name="lock_task_mode_start_clear_task">Clear task (start fresh)</string>
<string name="lock_task_mode_show_notification">Show a notification to exit</string>
<string name="app_not_allowed">App is not allowed</string>
<string name="disable_all">Disable all</string>
<!--ltf: lock task feature-->
@@ -327,7 +330,6 @@
<string name="network_logs_collected">Network logs collected</string>
<string name="delete_logs">Delete logs</string>
<string name="export_logs">Export logs</string>
<string name="wifi_auth_keypair">Wi-Fi keypair</string>
<string name="preferential_network_service">Preferential network service</string>
<string name="add_config">Add config</string>
<string name="network_id">Network ID</string>
@@ -377,7 +379,9 @@
<string name="show_user_app">Show user apps</string>
<string name="show_system_app">Show system apps</string>
<string name="suspend">Suspend</string>
<string name="unsuspend">Unsuspend</string>
<string name="hide">Hide</string>
<string name="unhide">Unhide</string>
<string name="always_on_vpn">Always-on VPN</string>
<string name="enable_lockdown">Enable lockdown</string>
<string name="clear_current_config">Clear current config</string>
@@ -397,6 +401,7 @@
<string name="permitted_ime">Permitted IME</string>
<string name="keep_uninstalled_packages">Keep uninstalled packages</string>
<string name="clear_app_storage">Clear app storage</string>
<string name="clear_app_storage_confirmation">Are you sure to delete app data?</string>
<string name="set_default_dialer">Set default dialer</string>
<string name="uninstall_app">Uninstall app</string>
<string name="install_app">Install app</string>
@@ -405,9 +410,14 @@
<string name="search">Search</string>
<string name="app_group">App group</string>
<string name="manage_app_groups">Manage groups</string>
<string name="import_str">Import</string>
<string name="edit_app_group">Edit group</string>
<string name="add_to_list">Add to list</string>
<string name="remove_from_list">Remove from list</string>
<string name="managed_configuration">Managed configuration</string>
<string name="clear_configurations">Clear configurations</string>
<string name="specify_value">Specify value</string>
<string name="package_removed">Removed %1$s</string>
<!--UserRestriction-->
<string name="user_restriction">User restriction</string>

View File

@@ -1,9 +1,9 @@
[versions]
agp = "8.13.1"
kotlin = "2.2.21"
agp = "8.13.2"
kotlin = "2.3.0"
navigation-compose = "2.9.6"
composeBom = "2025.11.00"
composeBom = "2025.12.01"
accompanist-drawablepainter = "0.37.3"
accompanist-permissions = "0.37.3"
shizuku = "13.1.5"
@@ -12,6 +12,7 @@ dhizuku = "2.5.4"
dhizuku-server = "0.0.10"
hiddenApiBypass = "6.1"
libsu = "6.0.0"
reoderable = "3.0.0"
serialization = "1.9.0"
[libraries]
@@ -33,6 +34,7 @@ dhizuku-api = { module = "io.github.iamr0s:Dhizuku-API", version.ref = "dhizuku"
dhizuku-server-api = { group = "io.github.iamr0s", name = "Dhizuku-SERVER_API", version.ref = "dhizuku-server" }
hiddenApiBypass = { module = "org.lsposed.hiddenapibypass:hiddenapibypass", version.ref = "hiddenApiBypass" }
libsu = { module = "com.github.topjohnwu.libsu:core", version.ref = "libsu" }
reoderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reoderable" }
serialization = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" }
@@ -40,4 +42,4 @@ serialization = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version = "2.2.21" }
serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version = "2.3.0" }