diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f6a280d..9ee1a2c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -24,7 +24,7 @@ android { defaultConfig { applicationId = "com.bintianqi.owndroid" - minSdk = 21 + minSdk = 23 targetSdk = 36 versionCode = 41 versionName = "7.2" diff --git a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt index f7c81d2..0324052 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MainActivity.kt @@ -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{ - AddDelegatedAdminScreen(vm.chosenPackage, ::choosePackage, it.toRoute(), + AddDelegatedAdminScreen(vm.chosenPackage, ::chooseSinglePackage, it.toRoute(), vm::setDelegatedAdmin, ::navigateUp) } composable { DeviceInfoScreen(vm, ::navigateUp) } @@ -390,9 +392,11 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) { vm::getNsNotificationPolicy, vm::setNsNotificationPolicy, ::navigateUp) } composable { - 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 { 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 { - NetworkStatsScreen(vm.chosenPackage, ::choosePackage, vm::getPackageUid, + NetworkStatsScreen(vm.chosenPackage, ::chooseSinglePackage, vm::getPackageUid, vm::queryNetworkStats, ::navigateUp) { navController.navigate(NetworkStatsViewer) } } composable { @@ -451,7 +455,7 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) { } composable { AlwaysOnVpnPackageScreen(vm::getAlwaysOnVpnPackage, vm::getAlwaysOnVpnLockdown, - vm::setAlwaysOnVpn, vm.chosenPackage, ::choosePackage, ::navigateUp) + vm::setAlwaysOnVpn, vm.chosenPackage, ::chooseSinglePackage, ::navigateUp) } composable { RecommendedGlobalProxyScreen(vm::setRecommendedGlobalProxy, ::navigateUp) @@ -499,10 +503,10 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) { composable { DeleteWorkProfileScreen(vm::wipeData, ::navigateUp) } composable { - val canSwitchView = (it.toRoute() as ApplicationsList).canSwitchView + val params = it.toRoute() 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 { 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - ClearAppStorageScreen(vm.chosenPackage, ::choosePackage, vm::clearAppData, ::navigateUp) + ClearAppStorageScreen( + vm.chosenPackage, ::chooseSinglePackage, vm::clearAppData, ::navigateUp + ) } composable { - UninstallAppScreen(vm.chosenPackage, ::choosePackage, vm::uninstallPackage, ::navigateUp) + UninstallAppScreen( + vm.chosenPackage, ::chooseSinglePackage, vm::uninstallPackage, ::navigateUp + ) } composable { - 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 { - InstallExistingAppScreen(vm.chosenPackage, ::choosePackage, - vm::installExistingApp, ::navigateUp) + InstallExistingAppScreen( + vm.chosenPackage, ::chooseSinglePackage, vm::installExistingApp, ::navigateUp + ) } composable { - 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 { PackageFunctionScreen(R.string.cross_profile_widget, vm.cpwProviders, @@ -584,24 +608,35 @@ fun Home(vm: MyViewModel, onLock: () -> Unit) { ::choosePackage, ::navigateToAppGroups, vm.appGroups) } composable { - 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 { - 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 { - 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 { - EnableSystemAppScreen(vm.chosenPackage, ::choosePackage, vm::enableSystemApp, ::navigateUp) + EnableSystemAppScreen( + vm.chosenPackage, ::chooseSinglePackage, vm::enableSystemApp, ::navigateUp + ) } composable { - SetDefaultDialerScreen(vm.chosenPackage, ::choosePackage, vm::setDefaultDialer, ::navigateUp) + SetDefaultDialerScreen( + vm.chosenPackage, ::chooseSinglePackage, vm::setDefaultDialer, ::navigateUp + ) } composable { 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) } diff --git a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt index 2b9debb..496f61e 100644 --- a/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt +++ b/app/src/main/java/com/bintianqi/owndroid/MyViewModel.kt @@ -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, status: Boolean) { + DPM.setPackagesSuspended(DAR, packages.toTypedArray(), status) getSuspendedPackaged() - return result.isEmpty() } val hiddenPackages = MutableStateFlow(emptyList()) @@ -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, 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, 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, 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, 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, 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, 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, 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, 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, input: List, status: Boolean + ): List { + return if (status) { + origin + input.map { getAppInfo(it) } + } else { + origin.filter { it.name !in input } + } + } + // Permitted input method val pimPackages = MutableStateFlow(emptyList()) 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, 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, status: Boolean) { + pasPackages.update { + updateAppInfoList(it, packages, status) } } fun setPasPolicy(allowAll: Boolean): Boolean { diff --git a/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt b/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt index 271e234..8c58ff1 100644 --- a/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt +++ b/app/src/main/java/com/bintianqi/owndroid/PackageChooser.kt @@ -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>, + params: ApplicationsList, packageList: MutableStateFlow>, refreshProgress: MutableStateFlow, onChoosePackage: (String?) -> Unit, - onSwitchView: () -> Unit, onRefresh: () -> Unit + onSwitchView: () -> Unit, onRefresh: () -> Unit, + setPackagesSuspend: (List, Boolean) -> Unit, + setPackagesHidden: (List, 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() } 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() ) { diff --git a/app/src/main/java/com/bintianqi/owndroid/Utils.kt b/app/src/main/java/com/bintianqi/owndroid/Utils.kt index 8dd3858..c545970 100644 --- a/app/src/main/java/com/bintianqi/owndroid/Utils.kt +++ b/app/src/main/java/com/bintianqi/owndroid/Utils.kt @@ -177,3 +177,5 @@ fun registerPackageRemovedReceiver( filter.addDataScheme("package") ctx.registerReceiver(br, filter) } + +fun parsePackageNames(input: String) = input.split('\n').filter { it.isNotEmpty() } diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt index 235516b..02d1426 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/Applications.kt @@ -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, onChoosePackage: () -> Unit, cmPackages: MutableStateFlow>, getCmPolicy: () -> Int, - setCmPackage: (String, Boolean) -> Unit, setCmPolicy: (Int) -> Unit, onNavigateUp: () -> Unit + setCmPackage: (List, 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, onChoosePackage: () -> Unit, packagesState: MutableStateFlow>, getPackages: () -> Boolean, - setPackage: (String, Boolean) -> Unit, setPolicy: (Boolean) -> Boolean, onNavigateUp: () -> Unit + setPackage: (List, 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>, onGet: () -> Unit, - onSet: (String, Boolean) -> Unit, onNavigateUp: () -> Unit, - chosenPackage: Channel, onChoosePackage: () -> Unit, - navigateToGroups: () -> Unit, appGroups: StateFlow>, 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>, onGet: () -> Unit, - onSet: (String, Boolean) -> Boolean?, onNavigateUp: () -> Unit, + onSet: (List, Boolean) -> Unit, onNavigateUp: () -> Unit, chosenPackage: Channel, onChoosePackage: () -> Unit, navigateToGroups: () -> Unit, appGroups: StateFlow>, 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(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)) { diff --git a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt index 64bcbab..c6eaa0c 100644 --- a/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt +++ b/app/src/main/java/com/bintianqi/owndroid/dpm/System.kt @@ -1147,7 +1147,7 @@ fun NearbyStreamingPolicyScreen( @RequiresApi(28) @Composable fun LockTaskModeScreen( - chosenPackage: Channel, onChoosePackage: () -> Unit, + chosenPackage: Channel, chooseSinglePackage: () -> Unit, choosePackage: () -> Unit, lockTaskPackages: StateFlow>, 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 diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 3b95803..1be5e09 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -174,7 +174,7 @@ 在足够安全时启用 锁定任务模式 清除任务(新实例) - 显示通知以退出 + 显示通知以退出 应用未被允许 禁用全部 允许状态栏信息 @@ -344,7 +344,9 @@ 显示用户应用 显示系统应用 挂起 + 取消挂起 隐藏 + 取消隐藏 VPN保持打开 启用锁定 清除当前配置 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9add4f7..8ac92ed 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -202,7 +202,7 @@ Same managed account only Lock task mode Clear task (start fresh) - Show a notification to exit + Show a notification to exit App is not allowed Disable all @@ -378,7 +378,9 @@ Show user apps Show system apps Suspend + Unsuspend Hide + Unhide Always-on VPN Enable lockdown Clear current config diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1090da1..14feb9a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }