From 90983daa411891840f2ad1d4f89b6a1901b8f0b5 Mon Sep 17 00:00:00 2001 From: Chaohui Wang Date: Fri, 17 Mar 2023 16:26:34 +0800 Subject: [PATCH] Fix not displaying "Allow restricted settings" "Allow restricted settings" is missed from SPA, added to SPA to fix this issue. Also make the system call in app info more options async to improve performance. Fix: 273678047 Test: Unit test Test: By the following steps, 1. Install an app with accessibility feature from Chrome 2. Go Accessibility page and click on the disabled grey app 3. Go to the app info page, click more options 4. Make sure "Allow restricted settings" is displayed Change-Id: I4adbe2335a32e6a7c4ebe155715684d768e5d1ef --- .../appinfo/AppInfoDashboardFragment.java | 3 +- .../app/appinfo/AppInfoSettingsMoreOptions.kt | 93 +++++++++++++++---- tests/spa_unit/AndroidManifest.xml | 2 + .../appinfo/AppInfoSettingsMoreOptionsTest.kt | 43 +++++++++ 4 files changed, 122 insertions(+), 19 deletions(-) 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) {