feat: multi-select in Package chooser (#215)

Update dependencies
Set minSdk to 23 since many compose libraries requires it
This commit is contained in:
BinTianqi
2025-12-27 13:58:07 +08:00
parent 5f95aa81c0
commit 434e9c1b25
10 changed files with 381 additions and 185 deletions

View File

@@ -24,7 +24,7 @@ android {
defaultConfig {
applicationId = "com.bintianqi.owndroid"
minSdk = 21
minSdk = 23
targetSdk = 36
versionCode = 41
versionName = "7.2"

View File

@@ -157,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
@@ -290,7 +289,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)
@@ -336,7 +338,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) }
@@ -390,9 +392,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,
@@ -440,7 +444,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> {
@@ -451,7 +455,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)
@@ -499,10 +503,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 {
@@ -517,12 +521,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)
}
}
@@ -531,52 +535,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,
@@ -584,24 +608,35 @@ 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(
@@ -762,7 +797,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

@@ -224,10 +224,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>())
@@ -236,10 +235,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
@@ -249,8 +249,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()
}
@@ -263,10 +265,12 @@ 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()
}
@@ -296,12 +300,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
@@ -311,9 +316,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()
}
@@ -325,10 +332,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()
}
@@ -338,14 +347,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)
@@ -401,9 +411,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)
@@ -414,6 +424,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 {
@@ -422,9 +442,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 {
@@ -442,9 +462,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 {

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,8 +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
@@ -37,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
@@ -46,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
@@ -67,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("") }
@@ -86,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()
@@ -102,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(
@@ -133,6 +216,10 @@ fun AppChooserScreen(
textStyle = typography.bodyLarge,
modifier = Modifier.fillMaxWidth().focusRequester(fr)
)
} else {
if (selectedPackages.isNotEmpty()) {
Text(selectedPackages.size.toString())
}
}
},
navigationIcon = {
@@ -154,10 +241,24 @@ fun AppChooserScreen(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable {
focusMgr.clearFocus()
onChoosePackage(it.name)
}
.combinedClickable(onLongClick = {
if (params.multiSelect) {
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

@@ -177,3 +177,5 @@ fun registerPackageRemovedReceiver(
filter.addDataScheme("package")
ctx.registerReceiver(br, filter)
}
fun parsePackageNames(input: String) = input.split('\n').filter { it.isNotEmpty() }

View File

@@ -107,6 +107,7 @@ import com.bintianqi.owndroid.MyViewModel
import com.bintianqi.owndroid.Privilege
import com.bintianqi.owndroid.R
import com.bintianqi.owndroid.adaptiveInsets
import com.bintianqi.owndroid.parsePackageNames
import com.bintianqi.owndroid.showOperationResultToast
import com.bintianqi.owndroid.ui.FullWidthRadioButtonItem
import com.bintianqi.owndroid.ui.FunctionItem
@@ -130,13 +131,18 @@ val String.isValidPackageName
@Composable
fun LazyItemScope.ApplicationItem(info: AppInfo, onClear: () -> Unit) {
Row(
Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 6.dp).animateItem(),
Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 6.dp)
.animateItem(),
Arrangement.SpaceBetween, Alignment.CenterVertically
) {
Row(Modifier.weight(1F), verticalAlignment = Alignment.CenterVertically) {
Image(
painter = rememberDrawablePainter(info.icon), contentDescription = null,
modifier = Modifier.padding(start = 12.dp, end = 18.dp).size(30.dp)
modifier = Modifier
.padding(start = 12.dp, end = 18.dp)
.size(30.dp)
)
Column {
Text(info.label)
@@ -156,7 +162,9 @@ fun PackageNameTextField(
) {
val fm = LocalFocusManager.current
OutlinedTextField(
value, onValueChange, Modifier.fillMaxWidth().then(modifier),
value, onValueChange, Modifier
.fillMaxWidth()
.then(modifier),
label = { Text(stringResource(R.string.package_name)) },
trailingIcon = {
IconButton(onChoosePackage) {
@@ -273,10 +281,14 @@ fun ApplicationDetailsScreen(
if (VERSION.SDK_INT >= 23) vm.getAppRestrictions(packageName)
}
MySmallTitleScaffold(R.string.place_holder, onNavigateUp, 0.dp) {
Column(Modifier.align(Alignment.CenterHorizontally).padding(top = 16.dp), horizontalAlignment = Alignment.CenterHorizontally) {
Column(Modifier
.align(Alignment.CenterHorizontally)
.padding(top = 16.dp), horizontalAlignment = Alignment.CenterHorizontally) {
Image(rememberDrawablePainter(info.icon), null, Modifier.size(50.dp))
Text(info.label, Modifier.padding(top = 4.dp))
Text(info.name, Modifier.alpha(0.7F).padding(bottom = 8.dp), style = typography.bodyMedium)
Text(info.name, Modifier
.alpha(0.7F)
.padding(bottom = 8.dp), style = typography.bodyMedium)
}
FunctionItem(R.string.permissions, icon = R.drawable.shield_fill0) { onNavigate(PermissionsManager(packageName)) }
if(VERSION.SDK_INT >= 24) SwitchItem(
@@ -401,7 +413,7 @@ fun PermissionsManagerScreen(
Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.background(if(selected) colorScheme.primaryContainer else Color.Transparent)
.background(if (selected) colorScheme.primaryContainer else Color.Transparent)
.clickable { changeState(status) }
.padding(vertical = 16.dp, horizontal = 12.dp),
Arrangement.SpaceBetween, Alignment.CenterVertically,
@@ -609,14 +621,16 @@ fun InstallExistingAppScreen(
fun CredentialManagerPolicyScreen(
chosenPackage: Channel<String>, onChoosePackage: () -> Unit,
cmPackages: MutableStateFlow<List<AppInfo>>, getCmPolicy: () -> Int,
setCmPackage: (String, Boolean) -> Unit, setCmPolicy: (Int) -> Unit, onNavigateUp: () -> Unit
setCmPackage: (List<String>, Boolean) -> Unit, setCmPolicy: (Int) -> Unit,
onNavigateUp: () -> Unit
) {
val context = LocalContext.current
var policy by rememberSaveable { mutableIntStateOf(getCmPolicy()) }
val packages by cmPackages.collectAsStateWithLifecycle()
var packageName by rememberSaveable { mutableStateOf("") }
var input by rememberSaveable { mutableStateOf("") }
val inputPackages = parsePackageNames(input)
LaunchedEffect(Unit) {
packageName = chosenPackage.receive()
input = chosenPackage.receive()
}
MyLazyScaffold(R.string.credential_manager_policy, onNavigateUp) {
item {
@@ -631,20 +645,20 @@ fun CredentialManagerPolicyScreen(
Spacer(Modifier.padding(vertical = 4.dp))
}
if (policy != -1) items(packages, { it.name }) {
ApplicationItem(it) { setCmPackage(it.name, false) }
ApplicationItem(it) { setCmPackage(listOf(it.name), false) }
}
item {
Column(Modifier.padding(horizontal = HorizontalPadding)) {
if (policy != -1) {
PackageNameTextField(packageName, onChoosePackage,
Modifier.padding(vertical = 8.dp)) { packageName = it }
PackageNameTextField(input, onChoosePackage,
Modifier.padding(vertical = 8.dp)) { input = it }
Button(
{
setCmPackage(packageName, true)
packageName = ""
setCmPackage(inputPackages, true)
input = ""
},
Modifier.fillMaxWidth(),
enabled = packageName.isValidPackageName
inputPackages.all { it.isValidPackageName }
) {
Text(stringResource(R.string.add))
}
@@ -672,33 +686,37 @@ fun CredentialManagerPolicyScreen(
fun PermittedAsAndImPackages(
title: Int, note: Int, chosenPackage: Channel<String>, onChoosePackage: () -> Unit,
packagesState: MutableStateFlow<List<AppInfo>>, getPackages: () -> Boolean,
setPackage: (String, Boolean) -> Unit, setPolicy: (Boolean) -> Boolean, onNavigateUp: () -> Unit
setPackage: (List<String>, Boolean) -> Unit, setPolicy: (Boolean) -> Boolean,
onNavigateUp: () -> Unit
) {
val context = LocalContext.current
val packages by packagesState.collectAsStateWithLifecycle()
var packageName by rememberSaveable { mutableStateOf("") }
var input by rememberSaveable { mutableStateOf("") }
val inputPackages = parsePackageNames(input)
var allowAll by rememberSaveable { mutableStateOf(getPackages()) }
LaunchedEffect(Unit) {
packageName = chosenPackage.receive()
input = chosenPackage.receive()
}
MyLazyScaffold(title, onNavigateUp) {
item {
SwitchItem(R.string.allow_all, state = allowAll, onCheckedChange = { allowAll = it })
}
if (!allowAll) items(packages, { it.name }) {
ApplicationItem(it) { setPackage(it.name, false) }
ApplicationItem(it) { setPackage(listOf(it.name), false) }
}
item {
if (!allowAll) {
PackageNameTextField(packageName, onChoosePackage,
Modifier.padding(HorizontalPadding, 8.dp)) { packageName = it }
PackageNameTextField(input, onChoosePackage,
Modifier.padding(HorizontalPadding, 8.dp)) { input = it }
Button(
{
setPackage(packageName, true)
packageName = ""
setPackage(inputPackages, true)
input = ""
},
Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding),
packageName.isValidPackageName
Modifier
.fillMaxWidth()
.padding(horizontal = HorizontalPadding),
inputPackages.all { it.isValidPackageName }
) {
Text(stringResource(R.string.add))
}
@@ -707,7 +725,10 @@ fun PermittedAsAndImPackages(
{
context.showOperationResultToast(setPolicy(allowAll))
},
Modifier.fillMaxWidth().padding(top = 8.dp).padding(horizontal = HorizontalPadding)
Modifier
.fillMaxWidth()
.padding(top = 8.dp)
.padding(horizontal = HorizontalPadding)
) {
Text(stringResource(R.string.apply))
}
@@ -777,35 +798,23 @@ fun SetDefaultDialerScreen(
}
}
@Composable
fun PackageFunctionScreenWithoutResult(
title: Int, packagesState: MutableStateFlow<List<AppInfo>>, onGet: () -> Unit,
onSet: (String, Boolean) -> Unit, onNavigateUp: () -> Unit,
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, navigateToGroups, appGroups, notes
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PackageFunctionScreen(
title: Int, packagesState: MutableStateFlow<List<AppInfo>>, onGet: () -> Unit,
onSet: (String, Boolean) -> Boolean?, onNavigateUp: () -> Unit,
onSet: (List<String>, Boolean) -> Unit, onNavigateUp: () -> Unit,
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 input by rememberSaveable { mutableStateOf("") }
val inputPackages = parsePackageNames(input)
var dialog by remember { mutableStateOf(false) }
var selectedGroup by remember { mutableStateOf<AppGroup?>(null) }
LaunchedEffect(Unit) {
onGet()
packageName = chosenPackage.receive()
input = chosenPackage.receive()
}
Scaffold(
topBar = {
@@ -848,21 +857,23 @@ fun PackageFunctionScreen(
LazyColumn(Modifier.padding(paddingValues)) {
items(packages, { it.name }) {
ApplicationItem(it) {
onSet(it.name, false)
onSet(listOf(it.name), false)
}
}
item {
PackageNameTextField(packageName, onChoosePackage,
Modifier.padding(HorizontalPadding, 8.dp)) { packageName = it }
PackageNameTextField(input, onChoosePackage,
Modifier.padding(HorizontalPadding, 8.dp)) { input = it }
Button(
{
if (onSet(packageName, true) != false) {
packageName = ""
}
onSet(inputPackages, true)
input = ""
},
Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding).padding(bottom = 10.dp),
packageName.isValidPackageName &&
packages.find { it.name == packageName } == null
Modifier
.fillMaxWidth()
.padding(horizontal = HorizontalPadding)
.padding(bottom = 10.dp),
inputPackages.all { it.isValidPackageName } &&
packages.none { it.name in inputPackages }
) {
Text(stringResource(R.string.add))
}
@@ -875,17 +886,13 @@ fun PackageFunctionScreen(
text = {
Column {
Button({
selectedGroup!!.apps.forEach {
onSet(it, true)
}
onSet(selectedGroup!!.apps, true)
dialog = false
}) {
Text(stringResource(R.string.add_to_list))
}
Button({
selectedGroup!!.apps.forEach {
onSet(it, false)
}
onSet(selectedGroup!!.apps, false)
dialog = false
}) {
Text(stringResource(R.string.remove_from_list))
@@ -930,9 +937,12 @@ fun ManageAppGroupsScreen(
LazyColumn(Modifier.padding(paddingValues)) {
items(groups, { it.id }) {
Column(
Modifier.fillMaxWidth().clickable {
navigateToEditScreen(it.id, it.name, it.apps)
}.padding(HorizontalPadding, 8.dp)
Modifier
.fillMaxWidth()
.clickable {
navigateToEditScreen(it.id, it.name, it.apps)
}
.padding(HorizontalPadding, 8.dp)
) {
Text(it.name)
Text(
@@ -957,9 +967,10 @@ fun EditAppGroupScreen(
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("") }
var input by rememberSaveable { mutableStateOf("") }
val inputPackages = parsePackageNames(input)
LaunchedEffect(Unit) {
packageName = chosenPackage.receive()
input = chosenPackage.receive()
}
Scaffold(
topBar = {
@@ -992,7 +1003,9 @@ fun EditAppGroupScreen(
LazyColumn(Modifier.padding(paddingValues)) {
item {
OutlinedTextField(
name, { name = it }, Modifier.fillMaxWidth().padding(HorizontalPadding, 8.dp),
name, { name = it }, Modifier
.fillMaxWidth()
.padding(HorizontalPadding, 8.dp),
label = { Text(stringResource(R.string.name)) }
)
}
@@ -1002,15 +1015,18 @@ fun EditAppGroupScreen(
}
}
item {
PackageNameTextField(packageName, onChoosePackage,
Modifier.padding(HorizontalPadding, 8.dp)) { packageName = it }
PackageNameTextField(input, onChoosePackage,
Modifier.padding(HorizontalPadding, 8.dp)) { input = it }
Button(
{
list += packageName
packageName = ""
list += inputPackages
input = ""
},
Modifier.fillMaxWidth().padding(horizontal = HorizontalPadding).padding(bottom = 10.dp),
packageName.isValidPackageName && packageName !in list
Modifier
.fillMaxWidth()
.padding(horizontal = HorizontalPadding)
.padding(bottom = 10.dp),
inputPackages.all { it.isValidPackageName && it !in list }
) {
Text(stringResource(R.string.add))
}
@@ -1053,7 +1069,9 @@ fun ManagedConfigurationScreen(
}
OutlinedTextField(
searchKeyword, { searchKeyword = it },
Modifier.fillMaxWidth().focusRequester(fr),
Modifier
.fillMaxWidth()
.focusRequester(fr),
textStyle = typography.bodyLarge,
placeholder = { Text(stringResource(R.string.search)) },
trailingIcon = {
@@ -1092,9 +1110,12 @@ fun ManagedConfigurationScreen(
LazyColumn(Modifier.padding(paddingValues)) {
items(displayRestrictions, { it.key }) { entry ->
Row(
Modifier.fillMaxWidth().clickable {
dialog = entry
}.padding(HorizontalPadding, 8.dp),
Modifier
.fillMaxWidth()
.clickable {
dialog = entry
}
.padding(HorizontalPadding, 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
val iconId = when (entry) {
@@ -1239,7 +1260,9 @@ fun ManagedConfigurationDialog(
}
}
Row(
Modifier.fillMaxWidth().padding(bottom = 4.dp),
Modifier
.fillMaxWidth()
.padding(bottom = 4.dp),
Arrangement.SpaceBetween, Alignment.CenterVertically
) {
Text(stringResource(R.string.specify_value))
@@ -1277,9 +1300,12 @@ fun ManagedConfigurationDialog(
is AppRestriction.ChoiceItem -> itemsIndexed(restriction.entryValues) { index, value ->
val label = restriction.entries.getOrNull(index)
Row(
Modifier.fillMaxWidth().clickable {
input = value
}.padding(8.dp, 4.dp)
Modifier
.fillMaxWidth()
.clickable {
input = value
}
.padding(8.dp, 4.dp)
) {
RadioButton(input == value, { input = value })
Spacer(Modifier.width(8.dp))
@@ -1298,10 +1324,13 @@ fun ManagedConfigurationDialog(
) { index, entry ->
ReorderableItem(reorderableListState, entry.value) {
Row(
Modifier.fillMaxWidth().clickable {
val old = multiSelectList[index]
multiSelectList[index] = old.copy(selected = !old.selected)
}.padding(8.dp, 4.dp),
Modifier
.fillMaxWidth()
.clickable {
val old = multiSelectList[index]
multiSelectList[index] = old.copy(selected = !old.selected)
}
.padding(8.dp, 4.dp),
Arrangement.SpaceBetween, Alignment.CenterVertically
) {
Row(Modifier.weight(1F), verticalAlignment = Alignment.CenterVertically) {
@@ -1325,7 +1354,9 @@ fun ManagedConfigurationDialog(
}
}
item {
Row(Modifier.fillMaxWidth().padding(top = 4.dp), Arrangement.End) {
Row(Modifier
.fillMaxWidth()
.padding(top = 4.dp), Arrangement.End) {
TextButton({
setRestriction(null)
}, Modifier.padding(end = 4.dp)) {

View File

@@ -1147,7 +1147,7 @@ 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, Boolean) -> Boolean,
@@ -1191,9 +1191,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)
}
@@ -1231,7 +1231,7 @@ private fun StartLockTaskMode(
R.string.lock_task_mode_start_clear_task, clearTask
) { clearTask = it }
FullWidthCheckBoxItem(
R.string.lock_taso_mode_show_notification, showNotification
R.string.lock_task_mode_show_notification, showNotification
) { showNotification = it }
Row(
Modifier

View File

@@ -174,7 +174,7 @@
<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_taso_mode_show_notification">显示通知以退出</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>
@@ -344,7 +344,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>

View File

@@ -202,7 +202,7 @@
<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_taso_mode_show_notification">Show a notification to exit</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-->
@@ -378,7 +378,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>

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"
@@ -42,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" }