feat: app groups importing and exporting (#222)

Show group name in app group operation dialog (#224)
This commit is contained in:
BinTianqi
2026-01-15 13:04:52 +08:00
parent 2b58e56bbf
commit 3da523051b
7 changed files with 94 additions and 4 deletions

View File

@@ -642,7 +642,7 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) {
} }
composable<ManageAppGroups> { composable<ManageAppGroups> {
ManageAppGroupsScreen( ManageAppGroupsScreen(
vm.appGroups, vm.appGroups, vm::exportAppGroups, vm::importAppGroups,
{ id, name, apps -> navController.navigate(EditAppGroup(id, name, apps)) }, { id, name, apps -> navController.navigate(EditAppGroup(id, name, apps)) },
::navigateUp ::navigateUp
) )

View File

@@ -65,6 +65,7 @@ import com.bintianqi.owndroid.dpm.ApnProtocol
import com.bintianqi.owndroid.dpm.AppGroup import com.bintianqi.owndroid.dpm.AppGroup
import com.bintianqi.owndroid.dpm.AppRestriction import com.bintianqi.owndroid.dpm.AppRestriction
import com.bintianqi.owndroid.dpm.AppStatus import com.bintianqi.owndroid.dpm.AppStatus
import com.bintianqi.owndroid.dpm.BasicAppGroup
import com.bintianqi.owndroid.dpm.CaCertInfo import com.bintianqi.owndroid.dpm.CaCertInfo
import com.bintianqi.owndroid.dpm.CreateUserResult import com.bintianqi.owndroid.dpm.CreateUserResult
import com.bintianqi.owndroid.dpm.CreateWorkProfileOptions import com.bintianqi.owndroid.dpm.CreateWorkProfileOptions
@@ -119,6 +120,10 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext 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.net.InetAddress
import java.security.MessageDigest import java.security.MessageDigest
import java.security.cert.CertificateException import java.security.cert.CertificateException
@@ -632,6 +637,20 @@ class MyViewModel(application: Application): AndroidViewModel(application) {
group.filter { it.id != id } 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) @RequiresApi(24)
fun reboot() { fun reboot() {

View File

@@ -5,8 +5,11 @@ import android.app.admin.DevicePolicyManager.PERMISSION_GRANT_STATE_DENIED
import android.app.admin.DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED import android.app.admin.DevicePolicyManager.PERMISSION_GRANT_STATE_GRANTED
import android.app.admin.PackagePolicy import android.app.admin.PackagePolicy
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Build.VERSION import android.os.Build.VERSION
import android.os.Looper import android.os.Looper
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
@@ -904,6 +907,8 @@ fun PackageFunctionScreen(
if (dialog) AlertDialog( if (dialog) AlertDialog(
text = { text = {
Column { Column {
Text(selectedGroup!!.name, style = typography.titleLarge)
Spacer(Modifier.height(6.dp))
Button({ Button({
onSet(selectedGroup!!.apps, true) onSet(selectedGroup!!.apps, true)
dialog = false dialog = false
@@ -927,22 +932,68 @@ fun PackageFunctionScreen(
) )
} }
class AppGroup(val id: Int, val name: String, val apps: List<String>) @Serializable
open class BasicAppGroup(open val name: String, open val apps: List<String>)
class AppGroup(
val id: Int, override val name: String, override val apps: List<String>
) : BasicAppGroup(name, apps)
@Serializable object ManageAppGroups @Serializable object ManageAppGroups
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun ManageAppGroupsScreen( fun ManageAppGroupsScreen(
appGroups: StateFlow<List<AppGroup>>, appGroups: StateFlow<List<AppGroup>>, exportData: (Uri) -> Unit, importData: (Uri) -> Unit,
navigateToEditScreen: (Int?, String, List<String>) -> Unit, navigateUp: () -> Unit navigateToEditScreen: (Int?, String, List<String>) -> Unit, navigateUp: () -> Unit
) { ) {
val groups by appGroups.collectAsStateWithLifecycle() val groups by appGroups.collectAsStateWithLifecycle()
val exportLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.CreateDocument("application/json")
) {
if (it != null) exportData(it)
}
val importLauncher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) {
if (it != null) importData(it)
}
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
{ Text(stringResource(R.string.app_group)) }, { Text(stringResource(R.string.app_group)) },
navigationIcon = { NavIcon(navigateUp) } navigationIcon = { NavIcon(navigateUp) },
actions = {
var dropdown by remember { mutableStateOf(false) }
Box {
IconButton({
dropdown = true
}) {
Icon(Icons.Default.MoreVert, null)
}
DropdownMenu(dropdown, { dropdown = false }) {
DropdownMenuItem(
{ Text(stringResource(R.string.export)) },
{
exportLauncher.launch("owndroid_app_groups")
dropdown = false
},
leadingIcon = {
Icon(painterResource(R.drawable.file_export_fill0), null)
}
)
DropdownMenuItem(
{ Text(stringResource(R.string.import_str)) },
{
importLauncher.launch(arrayOf("application/json"))
dropdown = false
},
leadingIcon = {
Icon(painterResource(R.drawable.file_open_fill0), null)
}
)
}
}
}
) )
}, },
floatingActionButton = { floatingActionButton = {

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

@@ -376,6 +376,7 @@
<string name="search">搜索</string> <string name="search">搜索</string>
<string name="app_group">应用组</string> <string name="app_group">应用组</string>
<string name="manage_app_groups">管理组</string> <string name="manage_app_groups">管理组</string>
<string name="import_str">导入</string>
<string name="edit_app_group">编辑组</string> <string name="edit_app_group">编辑组</string>
<string name="add_to_list">添加到列表</string> <string name="add_to_list">添加到列表</string>
<string name="remove_from_list">从列表中移除</string> <string name="remove_from_list">从列表中移除</string>

View File

@@ -410,6 +410,7 @@
<string name="search">Search</string> <string name="search">Search</string>
<string name="app_group">App group</string> <string name="app_group">App group</string>
<string name="manage_app_groups">Manage groups</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="edit_app_group">Edit group</string>
<string name="add_to_list">Add to list</string> <string name="add_to_list">Add to list</string>
<string name="remove_from_list">Remove from list</string> <string name="remove_from_list">Remove from list</string>