Loading src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java +2 −1 Original line number Diff line number Diff line Loading @@ -437,7 +437,8 @@ public class AppInfoDashboardFragment extends DashboardFragment } } private static void showLockScreen(Context context, Runnable successRunnable) { /** Shows the lock screen if the keyguard is secured. */ public static void showLockScreen(Context context, Runnable successRunnable) { final KeyguardManager keyguardManager = context.getSystemService( KeyguardManager.class); Loading src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptions.kt +75 −18 Original line number Diff line number Diff line Loading @@ -16,17 +16,25 @@ package com.android.settings.spa.app.appinfo import android.app.AppOpsManager import android.content.Context import android.content.pm.ApplicationInfo import android.os.UserManager import android.widget.Toast import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import com.android.settings.R import com.android.settings.Utils import com.android.settings.applications.appinfo.AppInfoDashboardFragment import com.android.settingslib.spa.widget.scaffold.MoreOptionsAction import com.android.settingslib.spaprivileged.framework.common.appOpsManager import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager import com.android.settingslib.spaprivileged.framework.common.userManager import com.android.settingslib.spaprivileged.model.app.IPackageManagers Loading @@ -35,6 +43,8 @@ import com.android.settingslib.spaprivileged.model.app.userId import com.android.settingslib.spaprivileged.model.enterprise.Restrictions import com.android.settingslib.spaprivileged.template.scaffold.RestrictedMenuItem import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.withContext @Composable Loading @@ -44,13 +54,11 @@ fun AppInfoSettingsMoreOptions( packageManagers: IPackageManagers = PackageManagers, ) { val state = app.produceState(packageManagers).value ?: return when { // We don't allow uninstalling update for DO/PO if it's a system app, because it will clear // data on all users. We also don't allow uninstalling for all users if it's DO/PO for any // user. state.isProfileOrDeviceOwner -> return !state.shownUninstallUpdates && !state.shownUninstallForAllUsers -> return } var restrictedSettingsAllowed by rememberSaveable { mutableStateOf(false) } if (!state.shownUninstallUpdates && !state.shownUninstallForAllUsers && !(state.shouldShowAccessRestrictedSettings && !restrictedSettingsAllowed) ) return MoreOptionsAction { val restrictions = Restrictions(userId = app.userId, keys = listOf(UserManager.DISALLOW_APPS_CONTROL)) Loading @@ -70,13 +78,37 @@ fun AppInfoSettingsMoreOptions( packageInfoPresenter.startUninstallActivity(forAllUsers = true) } } if (state.shouldShowAccessRestrictedSettings && !restrictedSettingsAllowed) { MenuItem(text = stringResource(R.string.app_restricted_settings_lockscreen_title)) { app.allowRestrictedSettings(packageInfoPresenter.context) { restrictedSettingsAllowed = true } } } } } private fun ApplicationInfo.allowRestrictedSettings(context: Context, onSuccess: () -> Unit) { AppInfoDashboardFragment.showLockScreen(context) { context.appOpsManager.setMode( AppOpsManager.OP_ACCESS_RESTRICTED_SETTINGS, uid, packageName, AppOpsManager.MODE_ALLOWED, ) onSuccess() val toastString = context.getString( R.string.toast_allows_restricted_settings_successfully, loadLabel(context.packageManager), ) Toast.makeText(context, toastString, Toast.LENGTH_LONG).show() } } private data class AppInfoSettingsMoreOptionsState( val isProfileOrDeviceOwner: Boolean, val shownUninstallUpdates: Boolean, val shownUninstallForAllUsers: Boolean, val shouldShowAccessRestrictedSettings: Boolean, ) @Composable Loading @@ -86,18 +118,38 @@ private fun ApplicationInfo.produceState( val context = LocalContext.current return produceState<AppInfoSettingsMoreOptionsState?>(initialValue = null, this) { withContext(Dispatchers.IO) { value = AppInfoSettingsMoreOptionsState( isProfileOrDeviceOwner = Utils.isProfileOrDeviceOwner( context.userManager, context.devicePolicyManager, packageName ), shownUninstallUpdates = isShowUninstallUpdates(context), shownUninstallForAllUsers = isShowUninstallForAllUsers( value = getMoreOptionsState(context, packageManagers) } } } private suspend fun ApplicationInfo.getMoreOptionsState( context: Context, packageManagers: IPackageManagers, ) = coroutineScope { val shownUninstallUpdatesDeferred = async { isShowUninstallUpdates(context) } val shownUninstallForAllUsersDeferred = async { isShowUninstallForAllUsers( userManager = context.userManager, packageManagers = packageManagers, ), ) } val shouldShowAccessRestrictedSettingsDeferred = async { shouldShowAccessRestrictedSettings(context.appOpsManager) } val isProfileOrDeviceOwner = Utils.isProfileOrDeviceOwner(context.userManager, context.devicePolicyManager, packageName) AppInfoSettingsMoreOptionsState( // We don't allow uninstalling update for DO/PO if it's a system app, because it will clear // data on all users. shownUninstallUpdates = !isProfileOrDeviceOwner && shownUninstallUpdatesDeferred.await(), // We also don't allow uninstalling for all users if it's DO/PO for any user. shownUninstallForAllUsers = !isProfileOrDeviceOwner && shownUninstallForAllUsersDeferred.await(), shouldShowAccessRestrictedSettings = shouldShowAccessRestrictedSettingsDeferred.await(), ) } private fun ApplicationInfo.isShowUninstallUpdates(context: Context): Boolean = Loading @@ -116,3 +168,8 @@ private fun ApplicationInfo.isOtherUserHasInstallPackage( ): Boolean = userManager.aliveUsers .filter { it.id != userId } .any { packageManagers.isPackageInstalledAsUser(packageName, it.id) } private fun ApplicationInfo.shouldShowAccessRestrictedSettings(appOpsManager: AppOpsManager) = appOpsManager.noteOpNoThrow( AppOpsManager.OP_ACCESS_RESTRICTED_SETTINGS, uid, packageName, null, null ) == AppOpsManager.MODE_IGNORED tests/spa_unit/AndroidManifest.xml +2 −0 Original line number Diff line number Diff line Loading @@ -19,6 +19,8 @@ xmlns:tools="http://schemas.android.com/tools" package="com.android.settings.tests.spa_unit"> <uses-permission android:name="android.permission.MANAGE_APPOPS" /> <uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS" /> <uses-permission android:name="android.permission.WRITE_DEVICE_CONFIG" /> <application android:debuggable="true"> Loading tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptionsTest.kt +43 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package com.android.settings.spa.app.appinfo import android.app.AppOpsManager import android.app.KeyguardManager import android.app.admin.DevicePolicyManager import android.content.Context import android.content.pm.ApplicationInfo Loading @@ -27,6 +29,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.performClick import androidx.test.core.app.ApplicationProvider Loading @@ -36,6 +39,7 @@ import com.android.settings.R import com.android.settings.Utils import com.android.settingslib.spa.testutils.delay import com.android.settingslib.spa.testutils.waitUntilExists import com.android.settingslib.spaprivileged.framework.common.appOpsManager import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager import com.android.settingslib.spaprivileged.framework.common.userManager import com.android.settingslib.spaprivileged.model.app.IPackageManagers Loading @@ -46,6 +50,7 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.MockitoSession import org.mockito.Spy import org.mockito.quality.Strictness Loading Loading @@ -73,6 +78,12 @@ class AppInfoSettingsMoreOptionsTest { @Mock private lateinit var devicePolicyManager: DevicePolicyManager @Mock private lateinit var appOpsManager: AppOpsManager @Mock private lateinit var keyguardManager: KeyguardManager @Spy private var resources = context.resources Loading @@ -90,6 +101,9 @@ class AppInfoSettingsMoreOptionsTest { whenever(context.packageManager).thenReturn(packageManager) whenever(context.userManager).thenReturn(userManager) whenever(context.devicePolicyManager).thenReturn(devicePolicyManager) whenever(context.appOpsManager).thenReturn(appOpsManager) whenever(context.getSystemService(KeyguardManager::class.java)).thenReturn(keyguardManager) whenever(keyguardManager.isKeyguardSecure).thenReturn(false) whenever(Utils.isProfileOrDeviceOwner(userManager, devicePolicyManager, PACKAGE_NAME)) .thenReturn(false) } Loading Loading @@ -143,6 +157,35 @@ class AppInfoSettingsMoreOptionsTest { ) } @Test fun shouldShowAccessRestrictedSettings() { whenever( appOpsManager.noteOpNoThrow( AppOpsManager.OP_ACCESS_RESTRICTED_SETTINGS, UID, PACKAGE_NAME, null, null ) ).thenReturn(AppOpsManager.MODE_IGNORED) val app = ApplicationInfo().apply { packageName = PACKAGE_NAME uid = UID } setContent(app) composeTestRule.onRoot().performClick() composeTestRule.waitUntilExists( hasText(context.getString(R.string.app_restricted_settings_lockscreen_title)) ) composeTestRule .onNodeWithText(context.getString(R.string.app_restricted_settings_lockscreen_title)) .performClick() verify(appOpsManager).setMode( AppOpsManager.OP_ACCESS_RESTRICTED_SETTINGS, UID, PACKAGE_NAME, AppOpsManager.MODE_ALLOWED, ) } private fun setContent(app: ApplicationInfo) { composeTestRule.setContent { CompositionLocalProvider(LocalContext provides context) { Loading Loading
src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java +2 −1 Original line number Diff line number Diff line Loading @@ -437,7 +437,8 @@ public class AppInfoDashboardFragment extends DashboardFragment } } private static void showLockScreen(Context context, Runnable successRunnable) { /** Shows the lock screen if the keyguard is secured. */ public static void showLockScreen(Context context, Runnable successRunnable) { final KeyguardManager keyguardManager = context.getSystemService( KeyguardManager.class); Loading
src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptions.kt +75 −18 Original line number Diff line number Diff line Loading @@ -16,17 +16,25 @@ package com.android.settings.spa.app.appinfo import android.app.AppOpsManager import android.content.Context import android.content.pm.ApplicationInfo import android.os.UserManager import android.widget.Toast import androidx.compose.runtime.Composable import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import com.android.settings.R import com.android.settings.Utils import com.android.settings.applications.appinfo.AppInfoDashboardFragment import com.android.settingslib.spa.widget.scaffold.MoreOptionsAction import com.android.settingslib.spaprivileged.framework.common.appOpsManager import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager import com.android.settingslib.spaprivileged.framework.common.userManager import com.android.settingslib.spaprivileged.model.app.IPackageManagers Loading @@ -35,6 +43,8 @@ import com.android.settingslib.spaprivileged.model.app.userId import com.android.settingslib.spaprivileged.model.enterprise.Restrictions import com.android.settingslib.spaprivileged.template.scaffold.RestrictedMenuItem import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.withContext @Composable Loading @@ -44,13 +54,11 @@ fun AppInfoSettingsMoreOptions( packageManagers: IPackageManagers = PackageManagers, ) { val state = app.produceState(packageManagers).value ?: return when { // We don't allow uninstalling update for DO/PO if it's a system app, because it will clear // data on all users. We also don't allow uninstalling for all users if it's DO/PO for any // user. state.isProfileOrDeviceOwner -> return !state.shownUninstallUpdates && !state.shownUninstallForAllUsers -> return } var restrictedSettingsAllowed by rememberSaveable { mutableStateOf(false) } if (!state.shownUninstallUpdates && !state.shownUninstallForAllUsers && !(state.shouldShowAccessRestrictedSettings && !restrictedSettingsAllowed) ) return MoreOptionsAction { val restrictions = Restrictions(userId = app.userId, keys = listOf(UserManager.DISALLOW_APPS_CONTROL)) Loading @@ -70,13 +78,37 @@ fun AppInfoSettingsMoreOptions( packageInfoPresenter.startUninstallActivity(forAllUsers = true) } } if (state.shouldShowAccessRestrictedSettings && !restrictedSettingsAllowed) { MenuItem(text = stringResource(R.string.app_restricted_settings_lockscreen_title)) { app.allowRestrictedSettings(packageInfoPresenter.context) { restrictedSettingsAllowed = true } } } } } private fun ApplicationInfo.allowRestrictedSettings(context: Context, onSuccess: () -> Unit) { AppInfoDashboardFragment.showLockScreen(context) { context.appOpsManager.setMode( AppOpsManager.OP_ACCESS_RESTRICTED_SETTINGS, uid, packageName, AppOpsManager.MODE_ALLOWED, ) onSuccess() val toastString = context.getString( R.string.toast_allows_restricted_settings_successfully, loadLabel(context.packageManager), ) Toast.makeText(context, toastString, Toast.LENGTH_LONG).show() } } private data class AppInfoSettingsMoreOptionsState( val isProfileOrDeviceOwner: Boolean, val shownUninstallUpdates: Boolean, val shownUninstallForAllUsers: Boolean, val shouldShowAccessRestrictedSettings: Boolean, ) @Composable Loading @@ -86,18 +118,38 @@ private fun ApplicationInfo.produceState( val context = LocalContext.current return produceState<AppInfoSettingsMoreOptionsState?>(initialValue = null, this) { withContext(Dispatchers.IO) { value = AppInfoSettingsMoreOptionsState( isProfileOrDeviceOwner = Utils.isProfileOrDeviceOwner( context.userManager, context.devicePolicyManager, packageName ), shownUninstallUpdates = isShowUninstallUpdates(context), shownUninstallForAllUsers = isShowUninstallForAllUsers( value = getMoreOptionsState(context, packageManagers) } } } private suspend fun ApplicationInfo.getMoreOptionsState( context: Context, packageManagers: IPackageManagers, ) = coroutineScope { val shownUninstallUpdatesDeferred = async { isShowUninstallUpdates(context) } val shownUninstallForAllUsersDeferred = async { isShowUninstallForAllUsers( userManager = context.userManager, packageManagers = packageManagers, ), ) } val shouldShowAccessRestrictedSettingsDeferred = async { shouldShowAccessRestrictedSettings(context.appOpsManager) } val isProfileOrDeviceOwner = Utils.isProfileOrDeviceOwner(context.userManager, context.devicePolicyManager, packageName) AppInfoSettingsMoreOptionsState( // We don't allow uninstalling update for DO/PO if it's a system app, because it will clear // data on all users. shownUninstallUpdates = !isProfileOrDeviceOwner && shownUninstallUpdatesDeferred.await(), // We also don't allow uninstalling for all users if it's DO/PO for any user. shownUninstallForAllUsers = !isProfileOrDeviceOwner && shownUninstallForAllUsersDeferred.await(), shouldShowAccessRestrictedSettings = shouldShowAccessRestrictedSettingsDeferred.await(), ) } private fun ApplicationInfo.isShowUninstallUpdates(context: Context): Boolean = Loading @@ -116,3 +168,8 @@ private fun ApplicationInfo.isOtherUserHasInstallPackage( ): Boolean = userManager.aliveUsers .filter { it.id != userId } .any { packageManagers.isPackageInstalledAsUser(packageName, it.id) } private fun ApplicationInfo.shouldShowAccessRestrictedSettings(appOpsManager: AppOpsManager) = appOpsManager.noteOpNoThrow( AppOpsManager.OP_ACCESS_RESTRICTED_SETTINGS, uid, packageName, null, null ) == AppOpsManager.MODE_IGNORED
tests/spa_unit/AndroidManifest.xml +2 −0 Original line number Diff line number Diff line Loading @@ -19,6 +19,8 @@ xmlns:tools="http://schemas.android.com/tools" package="com.android.settings.tests.spa_unit"> <uses-permission android:name="android.permission.MANAGE_APPOPS" /> <uses-permission android:name="android.permission.UPDATE_APP_OPS_STATS" /> <uses-permission android:name="android.permission.WRITE_DEVICE_CONFIG" /> <application android:debuggable="true"> Loading
tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptionsTest.kt +43 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package com.android.settings.spa.app.appinfo import android.app.AppOpsManager import android.app.KeyguardManager import android.app.admin.DevicePolicyManager import android.content.Context import android.content.pm.ApplicationInfo Loading @@ -27,6 +29,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onRoot import androidx.compose.ui.test.performClick import androidx.test.core.app.ApplicationProvider Loading @@ -36,6 +39,7 @@ import com.android.settings.R import com.android.settings.Utils import com.android.settingslib.spa.testutils.delay import com.android.settingslib.spa.testutils.waitUntilExists import com.android.settingslib.spaprivileged.framework.common.appOpsManager import com.android.settingslib.spaprivileged.framework.common.devicePolicyManager import com.android.settingslib.spaprivileged.framework.common.userManager import com.android.settingslib.spaprivileged.model.app.IPackageManagers Loading @@ -46,6 +50,7 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.MockitoSession import org.mockito.Spy import org.mockito.quality.Strictness Loading Loading @@ -73,6 +78,12 @@ class AppInfoSettingsMoreOptionsTest { @Mock private lateinit var devicePolicyManager: DevicePolicyManager @Mock private lateinit var appOpsManager: AppOpsManager @Mock private lateinit var keyguardManager: KeyguardManager @Spy private var resources = context.resources Loading @@ -90,6 +101,9 @@ class AppInfoSettingsMoreOptionsTest { whenever(context.packageManager).thenReturn(packageManager) whenever(context.userManager).thenReturn(userManager) whenever(context.devicePolicyManager).thenReturn(devicePolicyManager) whenever(context.appOpsManager).thenReturn(appOpsManager) whenever(context.getSystemService(KeyguardManager::class.java)).thenReturn(keyguardManager) whenever(keyguardManager.isKeyguardSecure).thenReturn(false) whenever(Utils.isProfileOrDeviceOwner(userManager, devicePolicyManager, PACKAGE_NAME)) .thenReturn(false) } Loading Loading @@ -143,6 +157,35 @@ class AppInfoSettingsMoreOptionsTest { ) } @Test fun shouldShowAccessRestrictedSettings() { whenever( appOpsManager.noteOpNoThrow( AppOpsManager.OP_ACCESS_RESTRICTED_SETTINGS, UID, PACKAGE_NAME, null, null ) ).thenReturn(AppOpsManager.MODE_IGNORED) val app = ApplicationInfo().apply { packageName = PACKAGE_NAME uid = UID } setContent(app) composeTestRule.onRoot().performClick() composeTestRule.waitUntilExists( hasText(context.getString(R.string.app_restricted_settings_lockscreen_title)) ) composeTestRule .onNodeWithText(context.getString(R.string.app_restricted_settings_lockscreen_title)) .performClick() verify(appOpsManager).setMode( AppOpsManager.OP_ACCESS_RESTRICTED_SETTINGS, UID, PACKAGE_NAME, AppOpsManager.MODE_ALLOWED, ) } private fun setContent(app: ApplicationInfo) { composeTestRule.setContent { CompositionLocalProvider(LocalContext provides context) { Loading