diff --git a/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java b/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java index ed45c2be31e..e771ff47761 100644 --- a/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java +++ b/src/com/android/settings/applications/appinfo/AppInfoDashboardFragment.java @@ -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); diff --git a/src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptions.kt b/src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptions.kt index fbdde0b982c..7f7d8c544b4 100644 --- a/src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptions.kt +++ b/src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptions.kt @@ -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 @@ -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 @@ -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)) @@ -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 @@ -86,20 +118,40 @@ private fun ApplicationInfo.produceState( val context = LocalContext.current return produceState(initialValue = null, this) { withContext(Dispatchers.IO) { - value = AppInfoSettingsMoreOptionsState( - isProfileOrDeviceOwner = Utils.isProfileOrDeviceOwner( - context.userManager, context.devicePolicyManager, packageName - ), - shownUninstallUpdates = isShowUninstallUpdates(context), - shownUninstallForAllUsers = isShowUninstallForAllUsers( - userManager = context.userManager, - packageManagers = packageManagers, - ), - ) + 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 = isUpdatedSystemApp && context.userManager.isUserAdmin(userId) && !context.resources.getBoolean(R.bool.config_disable_uninstall_update) @@ -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 diff --git a/tests/spa_unit/AndroidManifest.xml b/tests/spa_unit/AndroidManifest.xml index ec777410080..5a7f5659ce6 100644 --- a/tests/spa_unit/AndroidManifest.xml +++ b/tests/spa_unit/AndroidManifest.xml @@ -19,6 +19,8 @@ xmlns:tools="http://schemas.android.com/tools" package="com.android.settings.tests.spa_unit"> + + diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptionsTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptionsTest.kt index 318debab238..71035163057 100644 --- a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptionsTest.kt +++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppInfoSettingsMoreOptionsTest.kt @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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) } @@ -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) {