App grouping (#195)

This commit is contained in:
BinTianqi
2025-11-19 18:30:12 +08:00
parent 97721892f3
commit 1dab0a08d2
13 changed files with 325 additions and 43 deletions

View File

@@ -66,7 +66,6 @@ class ApiReceiver: BroadcastReceiver() {
}
else -> {
log += "\nInvalid action"
false
}
}
} catch(e: Exception) {

View File

@@ -23,7 +23,6 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -114,6 +113,8 @@ import com.bintianqi.owndroid.dpm.DisableAccountManagement
import com.bintianqi.owndroid.dpm.DisableAccountManagementScreen
import com.bintianqi.owndroid.dpm.DisableMeteredData
import com.bintianqi.owndroid.dpm.DisableUserControl
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
@@ -134,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
@@ -284,6 +287,9 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
fun choosePackage() {
navController.navigate(ApplicationsList(false))
}
fun navigateToAppGroups() {
navController.navigate(ManageAppGroups)
}
LaunchedEffect(Unit) {
if(!Privilege.status.value.activated) {
navController.navigate(WorkModes(false)) {
@@ -522,20 +528,20 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
composable<Suspend> {
PackageFunctionScreen(R.string.suspend, vm.suspendedPackages, vm::getSuspendedPackaged,
vm::setPackageSuspended, ::navigateUp, vm.chosenPackage, ::choosePackage,
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)
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)
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, R.string.info_disable_user_control)
::choosePackage, ::navigateToAppGroups, vm.appGroups, R.string.info_disable_user_control)
}
composable<PermissionsManager> {
PermissionsManagerScreen(vm.packagePermissions, vm::getPackagePermissions,
@@ -543,7 +549,8 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
}
composable<DisableMeteredData> {
PackageFunctionScreen(R.string.disable_metered_data, vm.mddPackages,
vm::getMddPackages, vm::setPackageMdd, ::navigateUp, vm.chosenPackage, ::choosePackage)
vm::getMddPackages, vm::setPackageMdd, ::navigateUp, vm.chosenPackage,
::choosePackage, ::navigateToAppGroups, vm.appGroups)
}
composable<ClearAppStorage> {
ClearAppStorageScreen(vm.chosenPackage, ::choosePackage, vm::clearAppData, ::navigateUp)
@@ -554,7 +561,8 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
composable<KeepUninstalledPackages> {
PackageFunctionScreenWithoutResult(R.string.keep_uninstalled_packages, vm.kuPackages,
vm::getKuPackages, vm::setPackageKu, ::navigateUp, vm.chosenPackage,
::choosePackage, R.string.info_keep_uninstalled_apps)
::choosePackage, ::navigateToAppGroups, vm.appGroups,
R.string.info_keep_uninstalled_apps)
}
composable<InstallExistingApp> {
InstallExistingAppScreen(vm.chosenPackage, ::choosePackage,
@@ -562,11 +570,13 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
}
composable<CrossProfilePackages> {
PackageFunctionScreenWithoutResult(R.string.cross_profile_apps, vm.cpPackages,
vm::getCpPackages, vm::setPackageCp, ::navigateUp, vm.chosenPackage, ::choosePackage)
vm::getCpPackages, vm::setPackageCp, ::navigateUp, vm.chosenPackage,
::choosePackage, ::navigateToAppGroups, vm.appGroups)
}
composable<CrossProfileWidgetProviders> {
PackageFunctionScreen(R.string.cross_profile_widget, vm.cpwProviders,
vm::getCpwProviders, vm::setCpwProvider, ::navigateUp, vm.chosenPackage, ::choosePackage)
vm::getCpwProviders, vm::setCpwProvider, ::navigateUp, vm.chosenPackage,
::choosePackage, ::navigateToAppGroups, vm.appGroups)
}
composable<CredentialManagerPolicy> {
CredentialManagerPolicyScreen(vm.chosenPackage, ::choosePackage,
@@ -588,6 +598,19 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
composable<SetDefaultDialer> {
SetDefaultDialerScreen(vm.chosenPackage, ::choosePackage, vm::setDefaultDialer, ::navigateUp)
}
composable<ManageAppGroups> {
ManageAppGroupsScreen(
vm.appGroups,
{ id, name, apps -> navController.navigate(EditAppGroup(id, name, apps)) },
::navigateUp
)
}
composable<EditAppGroup> {
EditAppGroupScreen(
it.toRoute(), vm::getAppInfo, ::navigateUp, vm::setAppGroup,
vm::deleteAppGroup, ::choosePackage, vm.chosenPackage
)
}
composable<UserRestriction> {
UserRestrictionScreen(vm::getUserRestrictions, ::navigateUp, ::navigate)

View File

@@ -4,11 +4,12 @@ import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
class MyDbHelper(context: Context): SQLiteOpenHelper(context, "data", null, 3) {
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) {
@@ -17,6 +18,9 @@ class MyDbHelper(context: Context): SQLiteOpenHelper(context, "data", null, 3) {
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," +
@@ -26,5 +30,8 @@ class MyDbHelper(context: Context): SQLiteOpenHelper(context, "data", null, 3) {
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)"
}
}

View File

@@ -9,6 +9,7 @@ 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
@@ -224,4 +225,27 @@ class MyRepository(val dbHelper: MyDbHelper) {
fun deleteNetworkLogs() {
dbHelper.writableDatabase.execSQL("DELETE FROM network_logs")
}
fun getAppGroups(): List<AppGroup> {
val list = mutableListOf<AppGroup>()
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<String>) {
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()))
}
}

View File

@@ -59,6 +59,7 @@ 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
@@ -509,6 +510,24 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
}
}
val appGroups = MutableStateFlow(emptyList<AppGroup>())
init {
getAppGroups()
}
fun getAppGroups() {
appGroups.value = myRepo.getAppGroups()
}
fun setAppGroup(id: Int?, name: String, apps: List<String>) {
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)

View File

@@ -23,8 +23,8 @@ object Privilege {
return
}
}
} catch(_: Exception) {
false
} catch(e: Exception) {
e.printStackTrace()
}
dhizukuErrorStatus.value = 2
}

View File

@@ -135,7 +135,7 @@ class SerializableSaver<T>(val serializer: KSerializer<T>) : Saver<T, String> {
override fun restore(value: String): T? {
return Json.decodeFromString(serializer, value)
}
override fun SaverScope.save(value: T): String? {
override fun SaverScope.save(value: T): String {
return Json.encodeToString(serializer, value)
}
}

View File

@@ -12,6 +12,7 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -20,6 +21,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
@@ -30,12 +32,20 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.List
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.MoreVert
import androidx.compose.material.icons.outlined.CheckCircle
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LargeTopAppBar
@@ -46,12 +56,15 @@ import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf
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
@@ -89,6 +102,7 @@ import com.bintianqi.owndroid.ui.SwitchItem
import com.google.accompanist.drawablepainter.rememberDrawablePainter
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.serialization.Serializable
val String.isValidPackageName
@@ -729,49 +743,238 @@ fun SetDefaultDialerScreen(
fun PackageFunctionScreenWithoutResult(
title: Int, packagesState: MutableStateFlow<List<AppInfo>>, onGet: () -> Unit,
onSet: (String, Boolean) -> Unit, onNavigateUp: () -> Unit,
chosenPackage: Channel<String>, onChoosePackage: () -> Unit, notes: Int? = null
chosenPackage: Channel<String>, onChoosePackage: () -> Unit,
navigateToGroups: () -> Unit, appGroups: StateFlow<List<AppGroup>>, notes: Int? = null
) {
PackageFunctionScreen(
title, packagesState, onGet, { name, status -> onSet(name, status); null },
onNavigateUp, chosenPackage, onChoosePackage, notes
onNavigateUp, chosenPackage, onChoosePackage, navigateToGroups, appGroups, notes
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PackageFunctionScreen(
title: Int, packagesState: MutableStateFlow<List<AppInfo>>, onGet: () -> Unit,
onSet: (String, Boolean) -> Boolean?, onNavigateUp: () -> Unit,
chosenPackage: Channel<String>, onChoosePackage: () -> Unit, notes: Int? = null
chosenPackage: Channel<String>, onChoosePackage: () -> Unit,
navigateToGroups: () -> Unit, appGroups: StateFlow<List<AppGroup>>, notes: Int? = null
) {
val groups by appGroups.collectAsStateWithLifecycle()
val packages by packagesState.collectAsStateWithLifecycle()
var packageName by rememberSaveable { mutableStateOf("") }
var selectedGroup by remember { mutableStateOf<AppGroup?>(null) }
LaunchedEffect(Unit) {
onGet()
packageName = chosenPackage.receive()
}
MyLazyScaffold(title, onNavigateUp) {
items(packages, { it.name }) {
ApplicationItem(it) {
onSet(it.name, false)
Scaffold(
topBar = {
TopAppBar(
{ Text(stringResource(title)) },
navigationIcon = { NavIcon(onNavigateUp) },
actions = {
var expand by remember { mutableStateOf(false) }
Box {
IconButton({
expand = true
}) {
Icon(Icons.Default.MoreVert, null)
}
DropdownMenu(expand, { expand = false }) {
groups.forEach {
DropdownMenuItem(
{ Text("(${it.apps.size}) ${it.name}") },
{
selectedGroup = it
expand = false
}
)
}
if (groups.isNotEmpty()) HorizontalDivider()
DropdownMenuItem(
{ Text(stringResource(R.string.manage_app_groups)) },
{
navigateToGroups()
expand = false
}
)
}
}
}
)
}
) { paddingValues ->
LazyColumn(Modifier.padding(paddingValues)) {
items(packages, { it.name }) {
ApplicationItem(it) {
onSet(it.name, false)
}
}
item {
PackageNameTextField(packageName, onChoosePackage,
Modifier.padding(HorizontalPadding, 8.dp)) { packageName = it }
Button(
{
if (onSet(packageName, true) != false) {
packageName = ""
}
},
Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding).padding(bottom = 10.dp),
packageName.isValidPackageName
) {
Text(stringResource(R.string.add))
}
if (notes != null) Notes(notes, HorizontalPadding)
Spacer(Modifier.height(BottomPadding))
}
}
item {
PackageNameTextField(packageName, onChoosePackage,
Modifier.padding(HorizontalPadding, 8.dp)) { packageName = it }
Button(
{
if (onSet(packageName, true) != false) {
println("reset")
packageName = ""
}
if (selectedGroup != null) AlertDialog(
text = {
Column {
Button({
selectedGroup!!.apps.forEach {
onSet(it, true)
}
},
Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding).padding(bottom = 10.dp),
packageName.isValidPackageName
) {
Text(stringResource(R.string.add))
selectedGroup = null
}) {
Text(stringResource(R.string.add_to_list))
}
Button({
selectedGroup!!.apps.forEach {
onSet(it, false)
}
selectedGroup = null
}) {
Text(stringResource(R.string.remove_from_list))
}
}
},
confirmButton = {
TextButton({ selectedGroup = null }) {
Text(stringResource(R.string.cancel))
}
},
onDismissRequest = { selectedGroup = null }
)
}
class AppGroup(val id: Int, val name: String, val apps: List<String>)
@Serializable object ManageAppGroups
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ManageAppGroupsScreen(
appGroups: StateFlow<List<AppGroup>>,
navigateToEditScreen: (Int?, String, List<String>) -> Unit, navigateUp: () -> Unit
) {
val groups by appGroups.collectAsStateWithLifecycle()
Scaffold(
topBar = {
TopAppBar(
{ Text(stringResource(R.string.app_group)) },
navigationIcon = { NavIcon(navigateUp) }
)
},
floatingActionButton = {
FloatingActionButton({
navigateToEditScreen(null, "", emptyList())
}) {
Icon(Icons.Default.Add, null)
}
}
) { paddingValues ->
LazyColumn(Modifier.padding(paddingValues)) {
items(groups, { it.id }) {
Column(
Modifier.fillMaxWidth().clickable {
navigateToEditScreen(it.id, it.name, it.apps)
}.padding(HorizontalPadding, 8.dp)
) {
Text(it.name)
Text(
it.apps.size.toString() + " apps", Modifier.alpha(0.7F),
style = typography.bodyMedium
)
}
}
}
}
}
@Serializable class EditAppGroup(val id: Int?, val name: String, val apps: List<String>)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EditAppGroupScreen(
params: EditAppGroup, getAppInfo: (String) -> AppInfo, navigateUp: () -> Unit,
setGroup: (Int?, String, List<String>) -> Unit, deleteGroup: (Int) -> Unit,
onChoosePackage: () -> Unit, chosenPackage: Channel<String>
) {
var name by rememberSaveable { mutableStateOf(params.name) }
val list = rememberSaveable { mutableStateListOf(*params.apps.toTypedArray()) }
val appInfoList = list.map { getAppInfo(it) }
var packageName by rememberSaveable { mutableStateOf("") }
LaunchedEffect(Unit) {
packageName = chosenPackage.receive()
}
Scaffold(
topBar = {
TopAppBar(
{ Text(stringResource(R.string.edit_app_group)) },
navigationIcon = {
NavIcon(navigateUp)
},
actions = {
if (params.id != null) IconButton({
deleteGroup(params.id)
navigateUp()
}) {
Icon(Icons.Outlined.Delete, null)
}
IconButton(
{
setGroup(params.id, name, list)
navigateUp()
},
enabled = name.isNotBlank() && list.isNotEmpty()
) {
Icon(Icons.Default.Check, null)
}
}
)
},
contentWindowInsets = adaptiveInsets()
) { paddingValues ->
LazyColumn(Modifier.padding(paddingValues)) {
item {
OutlinedTextField(
name, { name = it }, Modifier.fillMaxWidth().padding(HorizontalPadding, 8.dp),
label = { Text(stringResource(R.string.name)) }
)
}
items(appInfoList, { it.name }) {
ApplicationItem(it) {
list -= it.name
}
}
item {
PackageNameTextField(packageName, onChoosePackage,
Modifier.padding(HorizontalPadding, 8.dp)) { packageName = it }
Button(
{
list += packageName
packageName = ""
},
Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding).padding(bottom = 10.dp),
packageName.isValidPackageName
) {
Text(stringResource(R.string.add))
}
Spacer(Modifier.height(BottomPadding))
}
if (notes != null) Notes(notes, HorizontalPadding)
Spacer(Modifier.height(BottomPadding))
}
}
}

View File

@@ -112,7 +112,6 @@
<!--Dhizuku-->
<string name="failed_to_init_dhizuku">Не удалось инициализировать Dhizuku</string>
<string name="dhizuku_permission_not_granted">Разрешение Dhizuku не предоставлено</string>
<string name="dhizuku_mode_disabled">Режим Dhizuku отключен</string>
<!--Системные-->
<string name="system">Система</string>

View File

@@ -118,7 +118,6 @@
<!--Dhizuku-->
<string name="failed_to_init_dhizuku">Dhizuku Başlatılamadı</string>
<string name="dhizuku_permission_not_granted">Dhizuku İzni Verilmedi</string>
<string name="dhizuku_mode_disabled">Dhizuku Modu Devre Dışı</string>
<!--System-->
<string name="system">Sistem</string>

View File

@@ -115,7 +115,6 @@
<!--Dhizuku-->
<string name="failed_to_init_dhizuku">Dhizuku初始化失败</string>
<string name="dhizuku_permission_not_granted">Dhizuku未授权</string>
<string name="dhizuku_mode_disabled">Dhizuku模式已禁用</string>
<!--System-->
<string name="system">系统</string>
@@ -370,6 +369,11 @@
<string name="enable_system_app">启用系统应用</string>
<string name="keep_after_uninstall">卸载后保留</string>
<string name="search">搜索</string>
<string name="app_group">应用组</string>
<string name="manage_app_groups">管理组</string>
<string name="edit_app_group">编辑组</string>
<string name="add_to_list">添加到列表</string>
<string name="remove_from_list">从列表中移除</string>
<!--UserRestriction-->
<string name="user_restriction">用户限制</string>

View File

@@ -122,7 +122,6 @@
<string name="dhizuku" translatable="false">Dhizuku</string>
<string name="failed_to_init_dhizuku">Failed to initialize Dhizuku</string>
<string name="dhizuku_permission_not_granted">Dhizuku permission not granted</string>
<string name="dhizuku_mode_disabled">Dhizuku mode disabled</string>
<string name="shizuku" translatable="false">Shizuku</string>
@@ -404,6 +403,11 @@
<string name="install_existing_app">Install existing app</string>
<string name="keep_after_uninstall">Keep after uninstall</string>
<string name="search">Search</string>
<string name="app_group">App group</string>
<string name="manage_app_groups">Manage groups</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>
<!--UserRestriction-->
<string name="user_restriction">User restriction</string>

View File

@@ -7,3 +7,4 @@ kotlin.code.style=official
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx1536M"
org.gradle.configuration-cache=true