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.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;
@@ -1353,4 +1354,18 @@ public final class Utils extends com.android.settingslib.Utils {
return dreamsSupported && (!dreamsOnlyEnabledForDockUser || canCurrentUserDream(context)); 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.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;
@@ -239,13 +240,21 @@ 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 getPackageNameForMetric());
: SettingsEnums.ACTION_SETTINGS_ENABLE_APP,
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));
} }
@@ -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) { public void handleDialogClick(int id) {
switch (id) { switch (id) {
case ButtonActionDialogFragment.DialogType.DISABLE: case ButtonActionDialogFragment.DialogType.DISABLE:
mMetricsFeatureProvider.action(mActivity, requireAuthAndExecute(() -> {
SettingsEnums.ACTION_SETTINGS_DISABLE_APP); mMetricsFeatureProvider.action(mActivity,
AsyncTask.execute(new DisableChangerRunnable(mPm, mAppEntry.info.packageName, SettingsEnums.ACTION_SETTINGS_DISABLE_APP);
PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER)); AsyncTask.execute(new DisableChangerRunnable(mPm, mAppEntry.info.packageName,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER));
});
break; break;
case ButtonActionDialogFragment.DialogType.FORCE_STOP: case ButtonActionDialogFragment.DialogType.FORCE_STOP:
forceStopPackage(mAppEntry.info.packageName); requireAuthAndExecute(() -> {
forceStopPackage(mAppEntry.info.packageName);
});
break; break;
} }
} }
@@ -525,14 +551,16 @@ public class AppButtonsPreferenceController extends BasePreferenceController imp
@VisibleForTesting @VisibleForTesting
void uninstallPkg(String packageName, boolean allUsers) { void uninstallPkg(String packageName, boolean allUsers) {
stopListeningToPackageRemove(); requireAuthAndExecute(() -> {
// Create new intent to launch Uninstaller activity stopListeningToPackageRemove();
Uri packageUri = Uri.parse("package:" + packageName); // Create new intent to launch Uninstaller activity
Intent uninstallIntent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri); Uri packageUri = Uri.parse("package:" + packageName);
uninstallIntent.putExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, allUsers); 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); mMetricsFeatureProvider.action(mActivity, SettingsEnums.ACTION_SETTINGS_UNINSTALL_APP);
mFragment.startActivityForResult(uninstallIntent, mRequestUninstall); mFragment.startActivityForResult(uninstallIntent, mRequestUninstall);
});
} }
@VisibleForTesting @VisibleForTesting

View File

@@ -25,6 +25,8 @@ import android.content.pm.PackageManager
import android.os.UserHandle import android.os.UserHandle
import android.util.Log import android.util.Log
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.overlay.FeatureFactory import com.android.settings.overlay.FeatureFactory
import com.android.settings.spa.app.startUninstallActivity import com.android.settings.spa.app.startUninstallActivity
import com.android.settingslib.spa.framework.compose.LocalNavController 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. */ /** Enables this package. */
fun enable() { fun enable() {
logAction(SettingsEnums.ACTION_SETTINGS_ENABLE_APP) logAction(SettingsEnums.ACTION_SETTINGS_ENABLE_APP)
@@ -101,18 +113,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)
coroutineScope.launch(Dispatchers.IO) { requireAuthAndExecute {
userPackageManager.setApplicationEnabledSetting( coroutineScope.launch(Dispatchers.IO) {
packageName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER, 0 userPackageManager.setApplicationEnabledSetting(
) packageName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER, 0
reloadPackageInfo() )
reloadPackageInfo()
}
} }
} }
/** 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)
context.startUninstallActivity(packageName, userHandle, forAllUsers) requireAuthAndExecute {
context.startUninstallActivity(packageName, userHandle, forAllUsers)
}
} }
/** Clears this instant app. */ /** Clears this instant app. */
@@ -127,10 +143,12 @@ 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)
coroutineScope.launch(Dispatchers.Default) { requireAuthAndExecute {
Log.d(TAG, "Stopping package $packageName") coroutineScope.launch(Dispatchers.Default) {
context.activityManager.forceStopPackageAsUser(packageName, userId) Log.d(TAG, "Stopping package $packageName")
reloadPackageInfo() 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.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;
@@ -81,6 +82,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 {
@@ -164,6 +166,7 @@ public class AppButtonsPreferenceControllerTest {
@After @After
public void tearDown() { public void tearDown() {
ShadowAppUtils.reset(); ShadowAppUtils.reset();
ShadowUtils.reset();
} }
@Test @Test

View File

@@ -50,6 +50,7 @@ public class ShadowUtils {
private static ArraySet<String> sResultLinks = new ArraySet<>(); private static ArraySet<String> sResultLinks = new ArraySet<>();
private static boolean sIsBatteryPresent; private static boolean sIsBatteryPresent;
private static boolean sIsMultipleBiometricsSupported; private static boolean sIsMultipleBiometricsSupported;
private static boolean sIsProtectedPackage;
@Implementation @Implementation
protected static int enforceSameOwner(Context context, int userId) { protected static int enforceSameOwner(Context context, int userId) {
@@ -82,6 +83,7 @@ public class ShadowUtils {
sResultLinks = new ArraySet<>(); sResultLinks = new ArraySet<>();
sIsBatteryPresent = true; sIsBatteryPresent = true;
sIsMultipleBiometricsSupported = false; sIsMultipleBiometricsSupported = false;
sIsProtectedPackage = false;
} }
public static void setIsDemoUser(boolean isDemoUser) { public static void setIsDemoUser(boolean isDemoUser) {
@@ -188,4 +190,13 @@ public class ShadowUtils {
public static void setIsMultipleBiometricsSupported(boolean isMultipleBiometricsSupported) { public static void setIsMultipleBiometricsSupported(boolean isMultipleBiometricsSupported) {
sIsMultipleBiometricsSupported = 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 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
import android.content.pm.PackageManager import android.content.pm.PackageManager
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
@@ -31,26 +34,26 @@ import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor import org.mockito.ArgumentCaptor
import org.mockito.Mock import org.mockito.Mock
import org.mockito.MockitoSession
import org.mockito.Mockito.any import org.mockito.Mockito.any
import org.mockito.Mockito.doNothing import org.mockito.Mockito.doNothing
import org.mockito.Mockito.verify import org.mockito.Mockito.verify
import org.mockito.Spy import org.mockito.Spy
import org.mockito.junit.MockitoJUnit import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule import org.mockito.junit.MockitoRule
import org.mockito.quality.Strictness
import org.mockito.Mockito.`when` as whenever import org.mockito.Mockito.`when` as whenever
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class PackageInfoPresenterTest { class PackageInfoPresenterTest {
@get:Rule
val mockito: MockitoRule = MockitoJUnit.rule()
@Spy @Spy
private val context: Context = ApplicationProvider.getApplicationContext() private val context: Context = ApplicationProvider.getApplicationContext()
@@ -63,16 +66,38 @@ class PackageInfoPresenterTest {
@Mock @Mock
private lateinit var packageManagers: IPackageManagers private lateinit var packageManagers: IPackageManagers
@Mock
private lateinit var keyguardManager: KeyguardManager
private lateinit var mockSession: MockitoSession
private val fakeFeatureFactory = FakeFeatureFactory() private val fakeFeatureFactory = FakeFeatureFactory()
private val metricsFeatureProvider = fakeFeatureFactory.metricsFeatureProvider private val metricsFeatureProvider = fakeFeatureFactory.metricsFeatureProvider
private var isUserAuthenticated: Boolean = false
@Before @Before
fun setUp() { fun setUp() {
mockSession = ExtendedMockito.mockitoSession()
.initMocks(this)
.mockStatic(Utils::class.java)
.strictness(Strictness.LENIENT)
.startMocking()
context.mockAsUser() context.mockAsUser()
whenever(context.packageManager).thenReturn(packageManager) whenever(context.packageManager).thenReturn(packageManager)
whenever(context.activityManager).thenReturn(activityManager) 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 @Test
fun enable() = runTest { fun enable() = runTest {
coroutineScope { coroutineScope {
@@ -97,10 +122,23 @@ class PackageInfoPresenterTest {
packageInfoPresenter.disable() packageInfoPresenter.disable()
} }
verifyAction(SettingsEnums.ACTION_SETTINGS_DISABLE_APP) verifyDisablePackage()
verify(packageManager).setApplicationEnabledSetting( }
PACKAGE_NAME, PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER, 0
) @Test
fun disable_protectedPackage() = runTest {
mockProtectedPackage()
setAuthPassesAutomatically()
coroutineScope {
val packageInfoPresenter =
PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, this, packageManagers)
packageInfoPresenter.disable()
}
verifyUserAuthenticated()
verifyDisablePackage()
} }
@Test @Test
@@ -111,14 +149,22 @@ class PackageInfoPresenterTest {
packageInfoPresenter.startUninstallActivity() packageInfoPresenter.startUninstallActivity()
verifyAction(SettingsEnums.ACTION_SETTINGS_UNINSTALL_APP) verifyUninstallPackage()
val intentCaptor = ArgumentCaptor.forClass(Intent::class.java) }
verify(context).startActivityAsUser(intentCaptor.capture(), any())
with(intentCaptor.value) { @Test
assertThat(action).isEqualTo(Intent.ACTION_UNINSTALL_PACKAGE) fun startUninstallActivity_protectedPackage() = runTest {
assertThat(data?.schemeSpecificPart).isEqualTo(PACKAGE_NAME) mockProtectedPackage()
assertThat(getBooleanExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, true)).isEqualTo(false) setAuthPassesAutomatically()
}
doNothing().`when`(context).startActivityAsUser(any(), any())
val packageInfoPresenter =
PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, this, packageManagers)
packageInfoPresenter.startUninstallActivity()
verifyUserAuthenticated()
verifyUninstallPackage()
} }
@Test @Test
@@ -143,8 +189,23 @@ class PackageInfoPresenterTest {
packageInfoPresenter.forceStop() packageInfoPresenter.forceStop()
} }
verifyAction(SettingsEnums.ACTION_APP_FORCE_STOP) verifyForceStop()
verify(activityManager).forceStopPackageAsUser(PACKAGE_NAME, USER_ID) }
@Test
fun forceStop_protectedPackage() = runTest {
mockProtectedPackage()
setAuthPassesAutomatically()
coroutineScope {
val packageInfoPresenter =
PackageInfoPresenter(context, PACKAGE_NAME, USER_ID, this, packageManagers)
packageInfoPresenter.forceStop()
}
verifyUserAuthenticated()
verifyForceStop()
} }
@Test @Test
@@ -161,6 +222,46 @@ 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(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 { 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