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 -> { else -> {
log += "\nInvalid action" log += "\nInvalid action"
false
} }
} }
} catch(e: Exception) { } 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.Icons
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton 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.DisableAccountManagementScreen
import com.bintianqi.owndroid.dpm.DisableMeteredData import com.bintianqi.owndroid.dpm.DisableMeteredData
import com.bintianqi.owndroid.dpm.DisableUserControl 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.EnableSystemApp
import com.bintianqi.owndroid.dpm.EnableSystemAppScreen import com.bintianqi.owndroid.dpm.EnableSystemAppScreen
import com.bintianqi.owndroid.dpm.FrpPolicy 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.LockScreenInfoScreen
import com.bintianqi.owndroid.dpm.LockTaskMode import com.bintianqi.owndroid.dpm.LockTaskMode
import com.bintianqi.owndroid.dpm.LockTaskModeScreen 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.MtePolicy
import com.bintianqi.owndroid.dpm.MtePolicyScreen import com.bintianqi.owndroid.dpm.MtePolicyScreen
import com.bintianqi.owndroid.dpm.NearbyStreamingPolicy import com.bintianqi.owndroid.dpm.NearbyStreamingPolicy
@@ -284,6 +287,9 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
fun choosePackage() { fun choosePackage() {
navController.navigate(ApplicationsList(false)) navController.navigate(ApplicationsList(false))
} }
fun navigateToAppGroups() {
navController.navigate(ManageAppGroups)
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
if(!Privilege.status.value.activated) { if(!Privilege.status.value.activated) {
navController.navigate(WorkModes(false)) { navController.navigate(WorkModes(false)) {
@@ -522,20 +528,20 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
composable<Suspend> { composable<Suspend> {
PackageFunctionScreen(R.string.suspend, vm.suspendedPackages, vm::getSuspendedPackaged, PackageFunctionScreen(R.string.suspend, vm.suspendedPackages, vm::getSuspendedPackaged,
vm::setPackageSuspended, ::navigateUp, vm.chosenPackage, ::choosePackage, vm::setPackageSuspended, ::navigateUp, vm.chosenPackage, ::choosePackage,
R.string.info_suspend_app) ::navigateToAppGroups, vm.appGroups, R.string.info_suspend_app)
} }
composable<Hide> { composable<Hide> {
PackageFunctionScreen(R.string.hide, vm.hiddenPackages, vm::getHiddenPackages, 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> { composable<BlockUninstall> {
PackageFunctionScreenWithoutResult(R.string.block_uninstall, vm.ubPackages, 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> { composable<DisableUserControl> {
PackageFunctionScreenWithoutResult(R.string.disable_user_control, vm.ucdPackages, PackageFunctionScreenWithoutResult(R.string.disable_user_control, vm.ucdPackages,
vm::getUcdPackages, vm::setPackageUcd, ::navigateUp, vm.chosenPackage, 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> { composable<PermissionsManager> {
PermissionsManagerScreen(vm.packagePermissions, vm::getPackagePermissions, PermissionsManagerScreen(vm.packagePermissions, vm::getPackagePermissions,
@@ -543,7 +549,8 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
} }
composable<DisableMeteredData> { composable<DisableMeteredData> {
PackageFunctionScreen(R.string.disable_metered_data, vm.mddPackages, 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> { composable<ClearAppStorage> {
ClearAppStorageScreen(vm.chosenPackage, ::choosePackage, vm::clearAppData, ::navigateUp) ClearAppStorageScreen(vm.chosenPackage, ::choosePackage, vm::clearAppData, ::navigateUp)
@@ -554,7 +561,8 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
composable<KeepUninstalledPackages> { composable<KeepUninstalledPackages> {
PackageFunctionScreenWithoutResult(R.string.keep_uninstalled_packages, vm.kuPackages, PackageFunctionScreenWithoutResult(R.string.keep_uninstalled_packages, vm.kuPackages,
vm::getKuPackages, vm::setPackageKu, ::navigateUp, vm.chosenPackage, 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> { composable<InstallExistingApp> {
InstallExistingAppScreen(vm.chosenPackage, ::choosePackage, InstallExistingAppScreen(vm.chosenPackage, ::choosePackage,
@@ -562,11 +570,13 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
} }
composable<CrossProfilePackages> { composable<CrossProfilePackages> {
PackageFunctionScreenWithoutResult(R.string.cross_profile_apps, vm.cpPackages, 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> { composable<CrossProfileWidgetProviders> {
PackageFunctionScreen(R.string.cross_profile_widget, vm.cpwProviders, 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> { composable<CredentialManagerPolicy> {
CredentialManagerPolicyScreen(vm.chosenPackage, ::choosePackage, CredentialManagerPolicyScreen(vm.chosenPackage, ::choosePackage,
@@ -588,6 +598,19 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
composable<SetDefaultDialer> { composable<SetDefaultDialer> {
SetDefaultDialerScreen(vm.chosenPackage, ::choosePackage, vm::setDefaultDialer, ::navigateUp) 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> { composable<UserRestriction> {
UserRestrictionScreen(vm::getUserRestrictions, ::navigateUp, ::navigate) UserRestrictionScreen(vm::getUserRestrictions, ::navigateUp, ::navigate)

View File

@@ -4,11 +4,12 @@ import android.content.Context
import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper 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) { override fun onCreate(db: SQLiteDatabase) {
db.execSQL(DHIZUKU_CLIENTS_TABLE) db.execSQL(DHIZUKU_CLIENTS_TABLE)
db.execSQL(SECURITY_LOGS_TABLE) db.execSQL(SECURITY_LOGS_TABLE)
db.execSQL(NETWORK_LOGS_TABLE) db.execSQL(NETWORK_LOGS_TABLE)
db.execSQL(APP_GROUPS_TABLE)
} }
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
if (oldVersion < 2) { if (oldVersion < 2) {
@@ -17,6 +18,9 @@ class MyDbHelper(context: Context): SQLiteOpenHelper(context, "data", null, 3) {
if (oldVersion < 3) { if (oldVersion < 3) {
db.execSQL(NETWORK_LOGS_TABLE) db.execSQL(NETWORK_LOGS_TABLE)
} }
if (oldVersion < 4) {
db.execSQL(APP_GROUPS_TABLE)
}
} }
companion object { companion object {
const val DHIZUKU_CLIENTS_TABLE = "CREATE TABLE dhizuku_clients (uid INTEGER PRIMARY KEY," + 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," + const val NETWORK_LOGS_TABLE = "CREATE TABLE network_logs (id INTEGER, package INTEGER," +
"time INTEGER, type TEXT, host TEXT, count INTEGER, addresses TEXT," + "time INTEGER, type TEXT, host TEXT, count INTEGER, addresses TEXT," +
"address TEXT, port INTEGER)" "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.getIntOrNull
import androidx.core.database.getLongOrNull import androidx.core.database.getLongOrNull
import androidx.core.database.getStringOrNull import androidx.core.database.getStringOrNull
import com.bintianqi.owndroid.dpm.AppGroup
import com.bintianqi.owndroid.dpm.NetworkLog import com.bintianqi.owndroid.dpm.NetworkLog
import com.bintianqi.owndroid.dpm.SecurityEvent import com.bintianqi.owndroid.dpm.SecurityEvent
import com.bintianqi.owndroid.dpm.SecurityEventWithData import com.bintianqi.owndroid.dpm.SecurityEventWithData
@@ -224,4 +225,27 @@ class MyRepository(val dbHelper: MyDbHelper) {
fun deleteNetworkLogs() { fun deleteNetworkLogs() {
dbHelper.writableDatabase.execSQL("DELETE FROM network_logs") 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.ApnConfig
import com.bintianqi.owndroid.dpm.ApnMvnoType import com.bintianqi.owndroid.dpm.ApnMvnoType
import com.bintianqi.owndroid.dpm.ApnProtocol import com.bintianqi.owndroid.dpm.ApnProtocol
import com.bintianqi.owndroid.dpm.AppGroup
import com.bintianqi.owndroid.dpm.AppStatus import com.bintianqi.owndroid.dpm.AppStatus
import com.bintianqi.owndroid.dpm.CaCertInfo import com.bintianqi.owndroid.dpm.CaCertInfo
import com.bintianqi.owndroid.dpm.CreateUserResult 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) @RequiresApi(24)
fun reboot() { fun reboot() {
DPM.reboot(DAR) DPM.reboot(DAR)

View File

@@ -23,8 +23,8 @@ object Privilege {
return return
} }
} }
} catch(_: Exception) { } catch(e: Exception) {
false e.printStackTrace()
} }
dhizukuErrorStatus.value = 2 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? { override fun restore(value: String): T? {
return Json.decodeFromString(serializer, value) return Json.decodeFromString(serializer, value)
} }
override fun SaverScope.save(value: T): String? { override fun SaverScope.save(value: T): String {
return Json.encodeToString(serializer, value) 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.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer 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.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
@@ -30,12 +32,20 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.List 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.Clear
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.outlined.CheckCircle import androidx.compose.material.icons.outlined.CheckCircle
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.LargeTopAppBar import androidx.compose.material3.LargeTopAppBar
@@ -46,12 +56,15 @@ import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -89,6 +102,7 @@ import com.bintianqi.owndroid.ui.SwitchItem
import com.google.accompanist.drawablepainter.rememberDrawablePainter import com.google.accompanist.drawablepainter.rememberDrawablePainter
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
val String.isValidPackageName val String.isValidPackageName
@@ -729,27 +743,69 @@ fun SetDefaultDialerScreen(
fun PackageFunctionScreenWithoutResult( fun PackageFunctionScreenWithoutResult(
title: Int, packagesState: MutableStateFlow<List<AppInfo>>, onGet: () -> Unit, title: Int, packagesState: MutableStateFlow<List<AppInfo>>, onGet: () -> Unit,
onSet: (String, Boolean) -> Unit, onNavigateUp: () -> 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( PackageFunctionScreen(
title, packagesState, onGet, { name, status -> onSet(name, status); null }, title, packagesState, onGet, { name, status -> onSet(name, status); null },
onNavigateUp, chosenPackage, onChoosePackage, notes onNavigateUp, chosenPackage, onChoosePackage, navigateToGroups, appGroups, notes
) )
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun PackageFunctionScreen( fun PackageFunctionScreen(
title: Int, packagesState: MutableStateFlow<List<AppInfo>>, onGet: () -> Unit, title: Int, packagesState: MutableStateFlow<List<AppInfo>>, onGet: () -> Unit,
onSet: (String, Boolean) -> Boolean?, onNavigateUp: () -> 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() val packages by packagesState.collectAsStateWithLifecycle()
var packageName by rememberSaveable { mutableStateOf("") } var packageName by rememberSaveable { mutableStateOf("") }
var selectedGroup by remember { mutableStateOf<AppGroup?>(null) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
onGet() onGet()
packageName = chosenPackage.receive() packageName = chosenPackage.receive()
} }
MyLazyScaffold(title, onNavigateUp) { 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 }) { items(packages, { it.name }) {
ApplicationItem(it) { ApplicationItem(it) {
onSet(it.name, false) onSet(it.name, false)
@@ -761,7 +817,6 @@ fun PackageFunctionScreen(
Button( Button(
{ {
if (onSet(packageName, true) != false) { if (onSet(packageName, true) != false) {
println("reset")
packageName = "" packageName = ""
} }
}, },
@@ -774,4 +829,152 @@ fun PackageFunctionScreen(
Spacer(Modifier.height(BottomPadding)) Spacer(Modifier.height(BottomPadding))
} }
} }
}
if (selectedGroup != null) AlertDialog(
text = {
Column {
Button({
selectedGroup!!.apps.forEach {
onSet(it, true)
}
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))
}
}
}
} }

View File

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

View File

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

View File

@@ -115,7 +115,6 @@
<!--Dhizuku--> <!--Dhizuku-->
<string name="failed_to_init_dhizuku">Dhizuku初始化失败</string> <string name="failed_to_init_dhizuku">Dhizuku初始化失败</string>
<string name="dhizuku_permission_not_granted">Dhizuku未授权</string> <string name="dhizuku_permission_not_granted">Dhizuku未授权</string>
<string name="dhizuku_mode_disabled">Dhizuku模式已禁用</string>
<!--System--> <!--System-->
<string name="system">系统</string> <string name="system">系统</string>
@@ -370,6 +369,11 @@
<string name="enable_system_app">启用系统应用</string> <string name="enable_system_app">启用系统应用</string>
<string name="keep_after_uninstall">卸载后保留</string> <string name="keep_after_uninstall">卸载后保留</string>
<string name="search">搜索</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--> <!--UserRestriction-->
<string name="user_restriction">用户限制</string> <string name="user_restriction">用户限制</string>

View File

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

View File

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