[SPA] Add biometric authentication for package modification

Add an extra step of Lock Screen for disabling, force-stopping or
uninstalling updates for protected packages

UI Change Details : https://drive.google.com/drive/folders/1w7gKTmCxQ_j-9GQnIpEfF5_gmQ27b8l_?resourcekey=0-brLdN8VfqVPGm2FMwfrmkQ&usp=drive_link
Bug: 352504490, 344865740
Test: atest AppButtonsPreferenceControllerTest PackageInfoPresenterTest
Flag: EXEMPT High Security Bug
Change-Id: I0c494e307b02229d751de118abcc89e4e61a6861
This commit is contained in:
Shraddha Basantwani
2024-12-02 09:36:05 +00:00
parent 4084b5603e
commit 32e388ad31
6 changed files with 218 additions and 47 deletions

View File

@@ -131,6 +131,7 @@ import com.android.settings.password.ConfirmDeviceCredentialActivity;
import com.android.settingslib.widget.ActionBarShadowController; import com.android.settingslib.widget.ActionBarShadowController;
import com.android.settingslib.widget.AdaptiveIcon; import com.android.settingslib.widget.AdaptiveIcon;
import java.util.Arrays;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@@ -1600,4 +1601,19 @@ public final class Utils extends com.android.settingslib.Utils {
pm.setComponentEnabledSetting(componentName, pm.setComponentEnabledSetting(componentName,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
} }
/**
* 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<String> protectedPackageNames = Arrays.asList(context.getResources()
.getStringArray(com.android.internal.R.array
.config_biometric_protected_package_names));
return protectedPackageNames != null && protectedPackageNames.contains(packageName);
}
} }

View File

@@ -53,6 +53,7 @@ import com.android.settings.R;
import com.android.settings.SettingsActivity; import com.android.settings.SettingsActivity;
import com.android.settings.Utils; import com.android.settings.Utils;
import com.android.settings.applications.ApplicationFeatureProvider; 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.applications.specialaccess.deviceadmin.DeviceAdminAdd;
import com.android.settings.core.BasePreferenceController; import com.android.settings.core.BasePreferenceController;
import com.android.settings.core.InstrumentedPreferenceFragment; import com.android.settings.core.InstrumentedPreferenceFragment;
@@ -240,12 +241,20 @@ public class AppButtonsPreferenceController extends BasePreferenceController imp
} else if ((mAppEntry.info.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { } else if ((mAppEntry.info.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
if (mAppEntry.info.enabled && !isDisabledUntilUsed()) { if (mAppEntry.info.enabled && !isDisabledUntilUsed()) {
showDialogInner(ButtonActionDialogFragment.DialogType.DISABLE); 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 { } else {
mMetricsFeatureProvider.action( mMetricsFeatureProvider.action(
mActivity, mActivity,
mAppEntry.info.enabled SettingsEnums.ACTION_SETTINGS_ENABLE_APP,
? SettingsEnums.ACTION_SETTINGS_DISABLE_APP
: SettingsEnums.ACTION_SETTINGS_ENABLE_APP,
getPackageNameForMetric()); getPackageNameForMetric());
AsyncTask.execute(new DisableChangerRunnable(mPm, mAppEntry.info.packageName, AsyncTask.execute(new DisableChangerRunnable(mPm, mAppEntry.info.packageName,
PackageManager.COMPONENT_ENABLED_STATE_DEFAULT)); PackageManager.COMPONENT_ENABLED_STATE_DEFAULT));
@@ -289,17 +298,34 @@ 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) { public void handleDialogClick(int id) {
switch (id) { switch (id) {
case ButtonActionDialogFragment.DialogType.DISABLE: case ButtonActionDialogFragment.DialogType.DISABLE:
requireAuthAndExecute(() -> {
mMetricsFeatureProvider.action(mActivity, mMetricsFeatureProvider.action(mActivity,
SettingsEnums.ACTION_SETTINGS_DISABLE_APP, SettingsEnums.ACTION_SETTINGS_DISABLE_APP,
getPackageNameForMetric()); getPackageNameForMetric());
AsyncTask.execute(new DisableChangerRunnable(mPm, mAppEntry.info.packageName, AsyncTask.execute(new DisableChangerRunnable(mPm, mAppEntry.info.packageName,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER)); PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER));
});
break; break;
case ButtonActionDialogFragment.DialogType.FORCE_STOP: case ButtonActionDialogFragment.DialogType.FORCE_STOP:
requireAuthAndExecute(() -> {
forceStopPackage(mAppEntry.info.packageName); forceStopPackage(mAppEntry.info.packageName);
});
break; break;
} }
} }
@@ -535,6 +561,7 @@ public class AppButtonsPreferenceController extends BasePreferenceController imp
@VisibleForTesting @VisibleForTesting
void uninstallPkg(String packageName, boolean allUsers) { void uninstallPkg(String packageName, boolean allUsers) {
requireAuthAndExecute(() -> {
stopListeningToPackageRemove(); stopListeningToPackageRemove();
// Create new intent to launch Uninstaller activity // Create new intent to launch Uninstaller activity
Uri packageUri = Uri.parse("package:" + packageName); Uri packageUri = Uri.parse("package:" + packageName);
@@ -543,6 +570,7 @@ public class AppButtonsPreferenceController extends BasePreferenceController imp
mMetricsFeatureProvider.action(mActivity, SettingsEnums.ACTION_SETTINGS_UNINSTALL_APP); mMetricsFeatureProvider.action(mActivity, SettingsEnums.ACTION_SETTINGS_UNINSTALL_APP);
mFragment.startActivityForResult(uninstallIntent, mRequestUninstall); mFragment.startActivityForResult(uninstallIntent, mRequestUninstall);
});
} }
@VisibleForTesting @VisibleForTesting

View File

@@ -29,6 +29,8 @@ import android.os.UserHandle
import android.util.Log import android.util.Log
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import com.android.settings.Utils
import com.android.settings.applications.appinfo.AppInfoDashboardFragment
import com.android.settings.flags.FeatureFlags import com.android.settings.flags.FeatureFlags
import com.android.settings.flags.FeatureFlagsImpl import com.android.settings.flags.FeatureFlagsImpl
import com.android.settings.overlay.FeatureFactory.Companion.featureFactory import com.android.settings.overlay.FeatureFactory.Companion.featureFactory
@@ -116,6 +118,16 @@ class PackageInfoPresenter(
private fun isForThisApp(intent: Intent) = packageName == intent.data?.schemeSpecificPart private fun isForThisApp(intent: Intent) = packageName == intent.data?.schemeSpecificPart
private fun requireAuthAndExecute(action: () -> Unit) {
if (Utils.isProtectedPackage(context, packageName)) {
AppInfoDashboardFragment.showLockScreen(context) {
action()
}
} else {
action()
}
}
/** Enables this package. */ /** Enables this package. */
fun enable() { fun enable() {
logAction(SettingsEnums.ACTION_SETTINGS_ENABLE_APP) logAction(SettingsEnums.ACTION_SETTINGS_ENABLE_APP)
@@ -129,18 +141,22 @@ class PackageInfoPresenter(
/** Disables this package. */ /** Disables this package. */
fun disable() { fun disable() {
logAction(SettingsEnums.ACTION_SETTINGS_DISABLE_APP) logAction(SettingsEnums.ACTION_SETTINGS_DISABLE_APP)
requireAuthAndExecute {
coroutineScope.launch(Dispatchers.IO) { coroutineScope.launch(Dispatchers.IO) {
userPackageManager.setApplicationEnabledSetting( userPackageManager.setApplicationEnabledSetting(
packageName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER, 0 packageName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER, 0
) )
} }
} }
}
/** Starts the uninstallation activity. */ /** Starts the uninstallation activity. */
fun startUninstallActivity(forAllUsers: Boolean = false) { fun startUninstallActivity(forAllUsers: Boolean = false) {
logAction(SettingsEnums.ACTION_SETTINGS_UNINSTALL_APP) logAction(SettingsEnums.ACTION_SETTINGS_UNINSTALL_APP)
requireAuthAndExecute {
context.startUninstallActivity(packageName, userHandle, forAllUsers) context.startUninstallActivity(packageName, userHandle, forAllUsers)
} }
}
/** Clears this instant app. */ /** Clears this instant app. */
fun clearInstantApp() { fun clearInstantApp() {
@@ -153,6 +169,7 @@ class PackageInfoPresenter(
/** Force stops this package. */ /** Force stops this package. */
fun forceStop() { fun forceStop() {
logAction(SettingsEnums.ACTION_APP_FORCE_STOP) logAction(SettingsEnums.ACTION_APP_FORCE_STOP)
requireAuthAndExecute {
coroutineScope.launch(Dispatchers.Default) { coroutineScope.launch(Dispatchers.Default) {
Log.d(TAG, "Stopping package $packageName") Log.d(TAG, "Stopping package $packageName")
if (android.app.Flags.appRestrictionsApi()) { if (android.app.Flags.appRestrictionsApi()) {
@@ -166,6 +183,7 @@ class PackageInfoPresenter(
context.activityManager.forceStopPackageAsUser(packageName, userId) context.activityManager.forceStopPackageAsUser(packageName, userId)
} }
} }
}
fun logAction(category: Int) { fun logAction(category: Int) {
metricsFeatureProvider.action(context, category, packageName) metricsFeatureProvider.action(context, category, packageName)

View File

@@ -60,6 +60,7 @@ import com.android.settings.R;
import com.android.settings.SettingsActivity; import com.android.settings.SettingsActivity;
import com.android.settings.core.InstrumentedPreferenceFragment; import com.android.settings.core.InstrumentedPreferenceFragment;
import com.android.settings.testutils.FakeFeatureFactory; import com.android.settings.testutils.FakeFeatureFactory;
import com.android.settings.testutils.shadow.ShadowUtils;
import com.android.settingslib.applications.AppUtils; import com.android.settingslib.applications.AppUtils;
import com.android.settingslib.applications.ApplicationsState; import com.android.settingslib.applications.ApplicationsState;
import com.android.settingslib.applications.instantapps.InstantAppDataProvider; import com.android.settingslib.applications.instantapps.InstantAppDataProvider;
@@ -85,6 +86,7 @@ import org.robolectric.util.ReflectionHelpers;
import java.util.Set; import java.util.Set;
@Config(shadows = {ShadowUtils.class})
@RunWith(RobolectricTestRunner.class) @RunWith(RobolectricTestRunner.class)
public class AppButtonsPreferenceControllerTest { public class AppButtonsPreferenceControllerTest {
@@ -168,6 +170,7 @@ public class AppButtonsPreferenceControllerTest {
@After @After
public void tearDown() { public void tearDown() {
ShadowAppUtils.reset(); ShadowAppUtils.reset();
ShadowUtils.reset();
} }
@Test @Test

View File

@@ -51,6 +51,7 @@ public class ShadowUtils {
private static boolean sIsBatteryPresent; private static boolean sIsBatteryPresent;
private static boolean sIsMultipleBiometricsSupported; private static boolean sIsMultipleBiometricsSupported;
private static boolean sIsPrivateProfile; private static boolean sIsPrivateProfile;
private static boolean sIsProtectedPackage;
@Implementation @Implementation
protected static int enforceSameOwner(Context context, int userId) { protected static int enforceSameOwner(Context context, int userId) {
@@ -84,6 +85,7 @@ public class ShadowUtils {
sIsBatteryPresent = true; sIsBatteryPresent = true;
sIsMultipleBiometricsSupported = false; sIsMultipleBiometricsSupported = false;
sIsPrivateProfile = false; sIsPrivateProfile = false;
sIsProtectedPackage = false;
} }
public static void setIsDemoUser(boolean isDemoUser) { public static void setIsDemoUser(boolean isDemoUser) {
@@ -199,4 +201,13 @@ public class ShadowUtils {
public static void setIsPrivateProfile(boolean isPrivateProfile) { public static void setIsPrivateProfile(boolean isPrivateProfile) {
sIsPrivateProfile = isPrivateProfile; sIsPrivateProfile = isPrivateProfile;
} }
@Implementation
protected static boolean isProtectedPackage(Context context, String packageName) {
return sIsProtectedPackage;
}
public static void setIsProtectedPackage(boolean isProtectedPackage) {
sIsProtectedPackage = isProtectedPackage;
}
} }

View File

@@ -17,6 +17,7 @@
package com.android.settings.spa.app.appinfo package com.android.settings.spa.app.appinfo
import android.app.ActivityManager import android.app.ActivityManager
import android.app.KeyguardManager
import android.app.settings.SettingsEnums import android.app.settings.SettingsEnums
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@@ -25,6 +26,8 @@ import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4 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.FakeFeatureFactory
import com.android.settings.testutils.mockAsUser import com.android.settings.testutils.mockAsUser
import com.android.settingslib.spaprivileged.framework.common.activityManager import com.android.settingslib.spaprivileged.framework.common.activityManager
@@ -33,8 +36,11 @@ import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
import org.junit.After
import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.MockitoSession
import org.mockito.kotlin.any import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doNothing import org.mockito.kotlin.doNothing
@@ -43,6 +49,7 @@ import org.mockito.kotlin.mock
import org.mockito.kotlin.spy import org.mockito.kotlin.spy
import org.mockito.kotlin.verify import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever import org.mockito.kotlin.whenever
import org.mockito.quality.Strictness
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class PackageInfoPresenterTest { class PackageInfoPresenterTest {
@@ -51,9 +58,14 @@ class PackageInfoPresenterTest {
private val mockActivityManager = mock<ActivityManager>() private val mockActivityManager = mock<ActivityManager>()
private val mockKeyguardManager = mock<KeyguardManager>()
private lateinit var mockSession: MockitoSession
private val context: Context = spy(ApplicationProvider.getApplicationContext()) { private val context: Context = spy(ApplicationProvider.getApplicationContext()) {
on { packageManager } doReturn mockPackageManager on { packageManager } doReturn mockPackageManager
on { activityManager } doReturn mockActivityManager on { activityManager } doReturn mockActivityManager
on { getSystemService(Context.KEYGUARD_SERVICE) } doReturn mockKeyguardManager
doNothing().whenever(mock).startActivityAsUser(any(), any()) doNothing().whenever(mock).startActivityAsUser(any(), any())
mock.mockAsUser() mock.mockAsUser()
} }
@@ -66,6 +78,24 @@ class PackageInfoPresenterTest {
private val packageInfoPresenter = private val packageInfoPresenter =
PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers) PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, TestScope(), packageManagers)
private var isUserAuthenticated: Boolean = false
@Before
fun setUp() {
mockSession = ExtendedMockito.mockitoSession()
.initMocks(this)
.mockStatic(Utils::class.java)
.strictness(Strictness.LENIENT)
.startMocking()
whenever(Utils.isProtectedPackage(context, PACKAGE_NAME)).thenReturn(false)
}
@After
fun tearDown() {
mockSession.finishMocking()
isUserAuthenticated = false
}
@Test @Test
fun isInterestedAppChange_packageChanged_isInterested() { fun isInterestedAppChange_packageChanged_isInterested() {
val intent = Intent(Intent.ACTION_PACKAGE_CHANGED).apply { val intent = Intent(Intent.ACTION_PACKAGE_CHANGED).apply {
@@ -129,25 +159,37 @@ class PackageInfoPresenterTest {
packageInfoPresenter.disable() packageInfoPresenter.disable()
delay(100) delay(100)
verifyAction(SettingsEnums.ACTION_SETTINGS_DISABLE_APP) verifyDisablePackage()
verify(mockPackageManager).setApplicationEnabledSetting( }
PACKAGE_NAME, PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER, 0
) @Test
fun disable_protectedPackage() = runBlocking {
mockProtectedPackage()
setAuthPassesAutomatically()
packageInfoPresenter.disable()
delay(100)
verifyUserAuthenticated()
verifyDisablePackage()
} }
@Test @Test
fun startUninstallActivity() = runBlocking { fun startUninstallActivity() = runBlocking {
packageInfoPresenter.startUninstallActivity() packageInfoPresenter.startUninstallActivity()
verifyAction(SettingsEnums.ACTION_SETTINGS_UNINSTALL_APP) verifyUninstallPackage()
val intent = argumentCaptor<Intent> {
verify(context).startActivityAsUser(capture(), any())
}.firstValue
with(intent) {
assertThat(action).isEqualTo(Intent.ACTION_UNINSTALL_PACKAGE)
assertThat(data?.schemeSpecificPart).isEqualTo(PACKAGE_NAME)
assertThat(getBooleanExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, true)).isEqualTo(false)
} }
@Test
fun startUninstallActivity_protectedPackage() = runBlocking {
mockProtectedPackage()
setAuthPassesAutomatically()
packageInfoPresenter.startUninstallActivity()
verifyUserAuthenticated()
verifyUninstallPackage()
} }
@Test @Test
@@ -164,8 +206,19 @@ class PackageInfoPresenterTest {
packageInfoPresenter.forceStop() packageInfoPresenter.forceStop()
delay(100) delay(100)
verifyAction(SettingsEnums.ACTION_APP_FORCE_STOP) verifyForceStop()
verify(mockActivityManager).forceStopPackageAsUser(PACKAGE_NAME, USER_ID) }
@Test
fun forceStop_protectedPackage() = runBlocking {
mockProtectedPackage()
setAuthPassesAutomatically()
packageInfoPresenter.forceStop()
delay(100)
verifyUserAuthenticated()
verifyForceStop()
} }
@Test @Test
@@ -179,6 +232,48 @@ class PackageInfoPresenterTest {
verify(metricsFeatureProvider).action(context, category, PACKAGE_NAME) verify(metricsFeatureProvider).action(context, category, PACKAGE_NAME)
} }
private fun verifyDisablePackage() {
verifyAction(SettingsEnums.ACTION_SETTINGS_DISABLE_APP)
verify(mockPackageManager).setApplicationEnabledSetting(
PACKAGE_NAME, PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER, 0
)
}
private fun verifyUninstallPackage() {
verifyAction(SettingsEnums.ACTION_SETTINGS_UNINSTALL_APP)
val intent = argumentCaptor<Intent> {
verify(context).startActivityAsUser(capture(), any())
}.firstValue
with(intent) {
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(mockActivityManager).forceStopPackageAsUser(PACKAGE_NAME, USER_ID)
}
private fun setAuthPassesAutomatically() {
whenever(mockKeyguardManager.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 { private companion object {
const val PACKAGE_NAME = "package.name" const val PACKAGE_NAME = "package.name"
const val USER_ID = 0 const val USER_ID = 0