Merge "[SPA] Add biometric authentication for package modification" into udc-qpr-dev

This commit is contained in:
Treehugger Robot
2025-02-04 20:46:20 -08:00
committed by Android (Google) Code Review
6 changed files with 219 additions and 43 deletions

View File

@@ -118,6 +118,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;
@@ -1353,4 +1354,18 @@ public final class Utils extends com.android.settingslib.Utils {
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<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

@@ -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

View File

@@ -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()
}
}
}

View File

@@ -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

View File

@@ -50,6 +50,7 @@ public class ShadowUtils {
private static ArraySet<String> 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;
}
}

View File

@@ -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