From 27315b3b921152b09a6a5d9128664cf30d98a087 Mon Sep 17 00:00:00 2001 From: Shraddha Basantwani Date: Mon, 2 Dec 2024 09:36:05 +0000 Subject: [PATCH] [SPA] Add biometric authentication for package modification Add an extra step of Lock Screen for disabling, force-stopping or uninstalling updates for protected packages Bug: 352504490, 344865740 Test: atest AppButtonsPreferenceControllerTest PackageInfoPresenterTest Flag: EXEMPT High Security Bug (cherry picked from https://googleplex-android-review.googlesource.com/q/commit:32e388ad3199de3c062bb2e2db5d3239f934d0eb) Merged-In: I0c494e307b02229d751de118abcc89e4e61a6861 Change-Id: I0c494e307b02229d751de118abcc89e4e61a6861 --- src/com/android/settings/Utils.java | 16 +++ .../AppButtonsPreferenceController.java | 60 +++++--- .../spa/app/appinfo/PackageInfoPresenter.kt | 38 +++-- .../AppButtonsPreferenceControllerTest.java | 3 + .../testutils/shadow/ShadowUtils.java | 11 ++ .../app/appinfo/PackageInfoPresenterTest.kt | 135 +++++++++++++++--- 6 files changed, 220 insertions(+), 43 deletions(-) diff --git a/src/com/android/settings/Utils.java b/src/com/android/settings/Utils.java index 68b1a48a380..91a4f611f94 100644 --- a/src/com/android/settings/Utils.java +++ b/src/com/android/settings/Utils.java @@ -116,6 +116,7 @@ import com.android.settings.password.ChooseLockSettingsHelper; import com.android.settingslib.widget.ActionBarShadowController; import com.android.settingslib.widget.AdaptiveIcon; +import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Locale; @@ -1295,4 +1296,19 @@ public final class Utils extends com.android.settingslib.Utils { com.android.internal.R.bool.config_dreamsOnlyEnabledForDockUser); return dreamsSupported && (!dreamsOnlyEnabledForDockUser || canCurrentUserDream(context)); } + + /** + * Returns {@code true} if the supplied package is a protected package. Otherwise, returns + * {@code false}. + * + * @param context the context + * @param packageName the package name + */ + public static boolean isProtectedPackage( + @NonNull Context context, @NonNull String packageName) { + final List protectedPackageNames = Arrays.asList(context.getResources() + .getStringArray(com.android.internal.R.array + .config_biometric_protected_package_names)); + return protectedPackageNames != null && protectedPackageNames.contains(packageName); + } } diff --git a/src/com/android/settings/applications/appinfo/AppButtonsPreferenceController.java b/src/com/android/settings/applications/appinfo/AppButtonsPreferenceController.java index ff191ab4476..ffaf898c8af 100644 --- a/src/com/android/settings/applications/appinfo/AppButtonsPreferenceController.java +++ b/src/com/android/settings/applications/appinfo/AppButtonsPreferenceController.java @@ -52,6 +52,7 @@ import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.Utils; import com.android.settings.applications.ApplicationFeatureProvider; +import com.android.settings.applications.appinfo.AppInfoDashboardFragment; import com.android.settings.applications.specialaccess.deviceadmin.DeviceAdminAdd; import com.android.settings.core.BasePreferenceController; import com.android.settings.core.InstrumentedPreferenceFragment; @@ -239,13 +240,21 @@ public class AppButtonsPreferenceController extends BasePreferenceController imp } else if ((mAppEntry.info.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { if (mAppEntry.info.enabled && !isDisabledUntilUsed()) { showDialogInner(ButtonActionDialogFragment.DialogType.DISABLE); + } else if (mAppEntry.info.enabled) { + requireAuthAndExecute(() -> { + mMetricsFeatureProvider.action( + mActivity, + SettingsEnums.ACTION_SETTINGS_DISABLE_APP, + getPackageNameForMetric()); + AsyncTask.execute(new DisableChangerRunnable(mPm, + mAppEntry.info.packageName, + PackageManager.COMPONENT_ENABLED_STATE_DEFAULT)); + }); } else { mMetricsFeatureProvider.action( mActivity, - mAppEntry.info.enabled - ? SettingsEnums.ACTION_SETTINGS_DISABLE_APP - : SettingsEnums.ACTION_SETTINGS_ENABLE_APP, - getPackageNameForMetric()); + SettingsEnums.ACTION_SETTINGS_ENABLE_APP, + getPackageNameForMetric()); AsyncTask.execute(new DisableChangerRunnable(mPm, mAppEntry.info.packageName, PackageManager.COMPONENT_ENABLED_STATE_DEFAULT)); } @@ -288,16 +297,33 @@ public class AppButtonsPreferenceController extends BasePreferenceController imp } } + /** + * Runs the given action with restricted lock authentication if it is a protected package. + * + * @param action The action to run. + */ + private void requireAuthAndExecute(Runnable action) { + if (Utils.isProtectedPackage(mContext, mAppEntry.info.packageName)) { + AppInfoDashboardFragment.showLockScreen(mContext, () -> action.run()); + } else { + action.run(); + } + } + public void handleDialogClick(int id) { switch (id) { case ButtonActionDialogFragment.DialogType.DISABLE: - mMetricsFeatureProvider.action(mActivity, - SettingsEnums.ACTION_SETTINGS_DISABLE_APP); - AsyncTask.execute(new DisableChangerRunnable(mPm, mAppEntry.info.packageName, - PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER)); + requireAuthAndExecute(() -> { + mMetricsFeatureProvider.action(mActivity, + SettingsEnums.ACTION_SETTINGS_DISABLE_APP); + AsyncTask.execute(new DisableChangerRunnable(mPm, mAppEntry.info.packageName, + PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER)); + }); break; case ButtonActionDialogFragment.DialogType.FORCE_STOP: - forceStopPackage(mAppEntry.info.packageName); + requireAuthAndExecute(() -> { + forceStopPackage(mAppEntry.info.packageName); + }); break; } } @@ -525,14 +551,16 @@ public class AppButtonsPreferenceController extends BasePreferenceController imp @VisibleForTesting void uninstallPkg(String packageName, boolean allUsers) { - stopListeningToPackageRemove(); - // Create new intent to launch Uninstaller activity - Uri packageUri = Uri.parse("package:" + packageName); - Intent uninstallIntent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri); - uninstallIntent.putExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, allUsers); + requireAuthAndExecute(() -> { + stopListeningToPackageRemove(); + // Create new intent to launch Uninstaller activity + Uri packageUri = Uri.parse("package:" + packageName); + Intent uninstallIntent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri); + uninstallIntent.putExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, allUsers); - mMetricsFeatureProvider.action(mActivity, SettingsEnums.ACTION_SETTINGS_UNINSTALL_APP); - mFragment.startActivityForResult(uninstallIntent, mRequestUninstall); + mMetricsFeatureProvider.action(mActivity, SettingsEnums.ACTION_SETTINGS_UNINSTALL_APP); + mFragment.startActivityForResult(uninstallIntent, mRequestUninstall); + }); } @VisibleForTesting diff --git a/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt b/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt index 52c8ad7b160..ca9b2ebcf5c 100644 --- a/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt +++ b/src/com/android/settings/spa/app/appinfo/PackageInfoPresenter.kt @@ -25,6 +25,8 @@ import android.content.pm.PackageManager import android.os.UserHandle import android.util.Log import androidx.compose.runtime.Composable +import com.android.settings.Utils +import com.android.settings.applications.appinfo.AppInfoDashboardFragment import com.android.settings.overlay.FeatureFactory import com.android.settings.spa.app.startUninstallActivity import com.android.settingslib.spa.framework.compose.LocalNavController @@ -87,6 +89,16 @@ class PackageInfoPresenter( } } + private fun requireAuthAndExecute(action: () -> Unit) { + if (Utils.isProtectedPackage(context, packageName)) { + AppInfoDashboardFragment.showLockScreen(context) { + action() + } + } else { + action() + } + } + /** Enables this package. */ fun enable() { logAction(SettingsEnums.ACTION_SETTINGS_ENABLE_APP) @@ -101,18 +113,22 @@ class PackageInfoPresenter( /** Disables this package. */ fun disable() { logAction(SettingsEnums.ACTION_SETTINGS_DISABLE_APP) - coroutineScope.launch(Dispatchers.IO) { - userPackageManager.setApplicationEnabledSetting( - packageName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER, 0 - ) - reloadPackageInfo() + requireAuthAndExecute { + coroutineScope.launch(Dispatchers.IO) { + userPackageManager.setApplicationEnabledSetting( + packageName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER, 0 + ) + reloadPackageInfo() + } } } /** Starts the uninstallation activity. */ fun startUninstallActivity(forAllUsers: Boolean = false) { logAction(SettingsEnums.ACTION_SETTINGS_UNINSTALL_APP) - context.startUninstallActivity(packageName, userHandle, forAllUsers) + requireAuthAndExecute { + context.startUninstallActivity(packageName, userHandle, forAllUsers) + } } /** Clears this instant app. */ @@ -127,10 +143,12 @@ class PackageInfoPresenter( /** Force stops this package. */ fun forceStop() { logAction(SettingsEnums.ACTION_APP_FORCE_STOP) - coroutineScope.launch(Dispatchers.Default) { - Log.d(TAG, "Stopping package $packageName") - context.activityManager.forceStopPackageAsUser(packageName, userId) - reloadPackageInfo() + requireAuthAndExecute { + coroutineScope.launch(Dispatchers.Default) { + Log.d(TAG, "Stopping package $packageName") + context.activityManager.forceStopPackageAsUser(packageName, userId) + reloadPackageInfo() + } } } diff --git a/tests/robotests/src/com/android/settings/applications/appinfo/AppButtonsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/applications/appinfo/AppButtonsPreferenceControllerTest.java index 54c2d6ea8f0..02692fe7837 100644 --- a/tests/robotests/src/com/android/settings/applications/appinfo/AppButtonsPreferenceControllerTest.java +++ b/tests/robotests/src/com/android/settings/applications/appinfo/AppButtonsPreferenceControllerTest.java @@ -56,6 +56,7 @@ import com.android.settings.R; import com.android.settings.SettingsActivity; import com.android.settings.core.InstrumentedPreferenceFragment; import com.android.settings.testutils.FakeFeatureFactory; +import com.android.settings.testutils.shadow.ShadowUtils; import com.android.settingslib.applications.AppUtils; import com.android.settingslib.applications.ApplicationsState; import com.android.settingslib.applications.instantapps.InstantAppDataProvider; @@ -81,6 +82,7 @@ import org.robolectric.util.ReflectionHelpers; import java.util.Set; +@Config(shadows = {ShadowUtils.class}) @RunWith(RobolectricTestRunner.class) public class AppButtonsPreferenceControllerTest { @@ -164,6 +166,7 @@ public class AppButtonsPreferenceControllerTest { @After public void tearDown() { ShadowAppUtils.reset(); + ShadowUtils.reset(); } @Test diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowUtils.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowUtils.java index 5f8c434fc9f..1dd381f3570 100644 --- a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowUtils.java +++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowUtils.java @@ -50,6 +50,7 @@ public class ShadowUtils { private static ArraySet sResultLinks = new ArraySet<>(); private static boolean sIsBatteryPresent; private static boolean sIsMultipleBiometricsSupported; + private static boolean sIsProtectedPackage; @Implementation protected static int enforceSameOwner(Context context, int userId) { @@ -82,6 +83,7 @@ public class ShadowUtils { sResultLinks = new ArraySet<>(); sIsBatteryPresent = true; sIsMultipleBiometricsSupported = false; + sIsProtectedPackage = false; } public static void setIsDemoUser(boolean isDemoUser) { @@ -188,4 +190,13 @@ public class ShadowUtils { public static void setIsMultipleBiometricsSupported(boolean isMultipleBiometricsSupported) { sIsMultipleBiometricsSupported = isMultipleBiometricsSupported; } + + @Implementation + protected static boolean isProtectedPackage(Context context, String packageName) { + return sIsProtectedPackage; + } + + public static void setIsProtectedPackage(boolean isProtectedPackage) { + sIsProtectedPackage = isProtectedPackage; + } } diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/PackageInfoPresenterTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/PackageInfoPresenterTest.kt index 3bfa90e2d9e..f8026cc5dae 100644 --- a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/PackageInfoPresenterTest.kt +++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/PackageInfoPresenterTest.kt @@ -17,12 +17,15 @@ package com.android.settings.spa.app.appinfo import android.app.ActivityManager +import android.app.KeyguardManager import android.app.settings.SettingsEnums import android.content.Context import android.content.Intent import android.content.pm.PackageManager import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.dx.mockito.inline.extended.ExtendedMockito +import com.android.settings.Utils import com.android.settings.testutils.FakeFeatureFactory import com.android.settings.testutils.mockAsUser import com.android.settingslib.spaprivileged.framework.common.activityManager @@ -31,26 +34,26 @@ import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.test.runTest +import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Mock +import org.mockito.MockitoSession import org.mockito.Mockito.any import org.mockito.Mockito.doNothing import org.mockito.Mockito.verify import org.mockito.Spy import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoRule +import org.mockito.quality.Strictness import org.mockito.Mockito.`when` as whenever @OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) class PackageInfoPresenterTest { - @get:Rule - val mockito: MockitoRule = MockitoJUnit.rule() - @Spy private val context: Context = ApplicationProvider.getApplicationContext() @@ -63,16 +66,38 @@ class PackageInfoPresenterTest { @Mock private lateinit var packageManagers: IPackageManagers + @Mock + private lateinit var keyguardManager: KeyguardManager + + private lateinit var mockSession: MockitoSession + private val fakeFeatureFactory = FakeFeatureFactory() private val metricsFeatureProvider = fakeFeatureFactory.metricsFeatureProvider + private var isUserAuthenticated: Boolean = false + @Before fun setUp() { + mockSession = ExtendedMockito.mockitoSession() + .initMocks(this) + .mockStatic(Utils::class.java) + .strictness(Strictness.LENIENT) + .startMocking() + context.mockAsUser() whenever(context.packageManager).thenReturn(packageManager) whenever(context.activityManager).thenReturn(activityManager) + whenever(context.getSystemService(KeyguardManager::class.java)).thenReturn(keyguardManager) + whenever(Utils.isProtectedPackage(context, PACKAGE_NAME)).thenReturn(false) } + @After + fun tearDown() { + mockSession.finishMocking() + isUserAuthenticated = false + } + + @Test fun enable() = runTest { coroutineScope { @@ -97,10 +122,23 @@ class PackageInfoPresenterTest { packageInfoPresenter.disable() } - verifyAction(SettingsEnums.ACTION_SETTINGS_DISABLE_APP) - verify(packageManager).setApplicationEnabledSetting( - PACKAGE_NAME, PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER, 0 - ) + verifyDisablePackage() + } + + @Test + fun disable_protectedPackage() = runTest { + mockProtectedPackage() + setAuthPassesAutomatically() + + coroutineScope { + val packageInfoPresenter = + PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, this, packageManagers) + + packageInfoPresenter.disable() + } + + verifyUserAuthenticated() + verifyDisablePackage() } @Test @@ -111,14 +149,22 @@ class PackageInfoPresenterTest { packageInfoPresenter.startUninstallActivity() - verifyAction(SettingsEnums.ACTION_SETTINGS_UNINSTALL_APP) - val intentCaptor = ArgumentCaptor.forClass(Intent::class.java) - verify(context).startActivityAsUser(intentCaptor.capture(), any()) - with(intentCaptor.value) { - assertThat(action).isEqualTo(Intent.ACTION_UNINSTALL_PACKAGE) - assertThat(data?.schemeSpecificPart).isEqualTo(PACKAGE_NAME) - assertThat(getBooleanExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, true)).isEqualTo(false) - } + verifyUninstallPackage() + } + + @Test + fun startUninstallActivity_protectedPackage() = runTest { + mockProtectedPackage() + setAuthPassesAutomatically() + + doNothing().`when`(context).startActivityAsUser(any(), any()) + val packageInfoPresenter = + PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, this, packageManagers) + + packageInfoPresenter.startUninstallActivity() + + verifyUserAuthenticated() + verifyUninstallPackage() } @Test @@ -143,8 +189,23 @@ class PackageInfoPresenterTest { packageInfoPresenter.forceStop() } - verifyAction(SettingsEnums.ACTION_APP_FORCE_STOP) - verify(activityManager).forceStopPackageAsUser(PACKAGE_NAME, USER_ID) + verifyForceStop() + } + + @Test + fun forceStop_protectedPackage() = runTest { + mockProtectedPackage() + setAuthPassesAutomatically() + + coroutineScope { + val packageInfoPresenter = + PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, this, packageManagers) + + packageInfoPresenter.forceStop() + } + + verifyUserAuthenticated() + verifyForceStop() } @Test @@ -161,6 +222,46 @@ class PackageInfoPresenterTest { verify(metricsFeatureProvider).action(context, category, PACKAGE_NAME) } + private fun verifyDisablePackage() { + verifyAction(SettingsEnums.ACTION_SETTINGS_DISABLE_APP) + verify(packageManager).setApplicationEnabledSetting( + PACKAGE_NAME, PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER, 0 + ) + } + + private fun verifyUninstallPackage() { + verifyAction(SettingsEnums.ACTION_SETTINGS_UNINSTALL_APP) + val intentCaptor = ArgumentCaptor.forClass(Intent::class.java) + verify(context).startActivityAsUser(intentCaptor.capture(), any()) + with(intentCaptor.value) { + assertThat(action).isEqualTo(Intent.ACTION_UNINSTALL_PACKAGE) + assertThat(data?.schemeSpecificPart).isEqualTo(PACKAGE_NAME) + assertThat(getBooleanExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, true)).isEqualTo(false) + } + } + + private fun verifyForceStop() { + verifyAction(SettingsEnums.ACTION_APP_FORCE_STOP) + verify(activityManager).forceStopPackageAsUser(PACKAGE_NAME, USER_ID) + } + + private fun setAuthPassesAutomatically() { + whenever(keyguardManager.isKeyguardSecure).thenReturn(mockUserAuthentication()) + } + + private fun mockUserAuthentication() : Boolean { + isUserAuthenticated = true + return false + } + + private fun mockProtectedPackage() { + whenever(Utils.isProtectedPackage(context, PACKAGE_NAME)).thenReturn(true) + } + + private fun verifyUserAuthenticated() { + assertThat(isUserAuthenticated).isTrue() + } + private companion object { const val PACKAGE_NAME = "package.name" const val USER_ID = 0