diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index d8d648bf8b30c..764fa0218023f 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -15442,16 +15442,55 @@ public class PackageManagerService extends IPackageManager.Stub null); } - private void sendPackagesSuspendedForUser(String[] pkgList, int[] uidList, int userId, + @VisibleForTesting(visibility = Visibility.PRIVATE) + void sendPackagesSuspendedForUser(String[] pkgList, int[] uidList, int userId, boolean suspended) { - final Bundle extras = new Bundle(3); - extras.putStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST, pkgList); - extras.putIntArray(Intent.EXTRA_CHANGED_UID_LIST, uidList); - sendPackageBroadcast( - suspended ? Intent.ACTION_PACKAGES_SUSPENDED - : Intent.ACTION_PACKAGES_UNSUSPENDED, - null, extras, Intent.FLAG_RECEIVER_REGISTERED_ONLY, null, null, - new int[] {userId}, null, null, null); + final List> pkgsToSend = new ArrayList(pkgList.length); + final List uidsToSend = new ArrayList(pkgList.length); + final List> allowListsToSend = new ArrayList(pkgList.length); + final int[] userIds = new int[] {userId}; + // Get allow lists for the pkg in the pkgList. Merge into the existed pkgs and uids if + // allow lists are the same. + synchronized (mLock) { + for (int i = 0; i < pkgList.length; i++) { + final String pkgName = pkgList[i]; + final int uid = uidList[i]; + SparseArray allowList = mAppsFilter.getVisibilityAllowList( + getPackageSettingInternal(pkgName, Process.SYSTEM_UID), + userIds, mSettings.getPackagesLocked()); + if (allowList == null) { + allowList = new SparseArray<>(0); + } + boolean merged = false; + for (int j = 0; j < allowListsToSend.size(); j++) { + if (Arrays.equals(allowListsToSend.get(j).get(userId), allowList.get(userId))) { + pkgsToSend.get(j).add(pkgName); + uidsToSend.get(j).add(uid); + merged = true; + break; + } + } + if (!merged) { + pkgsToSend.add(new ArrayList<>(Arrays.asList(pkgName))); + uidsToSend.add(IntArray.wrap(new int[] {uid})); + allowListsToSend.add(allowList); + } + } + } + + for (int i = 0; i < pkgsToSend.size(); i++) { + final Bundle extras = new Bundle(3); + extras.putStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST, + pkgsToSend.get(i).toArray(new String[pkgsToSend.get(i).size()])); + extras.putIntArray(Intent.EXTRA_CHANGED_UID_LIST, uidsToSend.get(i).toArray()); + final SparseArray allowList = allowListsToSend.get(i).size() == 0 + ? null : allowListsToSend.get(i); + sendPackageBroadcast( + suspended ? Intent.ACTION_PACKAGES_SUSPENDED + : Intent.ACTION_PACKAGES_UNSUSPENDED, + null, extras, Intent.FLAG_RECEIVER_REGISTERED_ONLY, null, null, + userIds, null, allowList, null); + } } /** diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index b5765b50e7468..0ddb6cd50d4a6 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -104,8 +104,8 @@ import com.android.server.FgThread; import com.android.server.LocalServices; import com.android.server.SystemConfig; import com.android.server.pm.PackageManagerShellCommandDataLoader.Metadata; -import com.android.server.pm.verify.domain.DomainVerificationShell; import com.android.server.pm.permission.LegacyPermissionManagerInternal; +import com.android.server.pm.verify.domain.DomainVerificationShell; import dalvik.system.DexFile; @@ -2251,8 +2251,8 @@ class PackageManagerShellCommand extends ShellCommand { } } - final String packageName = getNextArg(); - if (packageName == null) { + final List packageNames = getRemainingArgs(); + if (packageNames.isEmpty()) { pw.println("Error: package name not specified"); return 1; } @@ -2270,12 +2270,15 @@ class PackageManagerShellCommand extends ShellCommand { try { final int translatedUserId = translateUserId(userId, UserHandle.USER_NULL, "runSuspend"); - mInterface.setPackagesSuspendedAsUser(new String[]{packageName}, suspendedState, - ((appExtras.size() > 0) ? appExtras : null), + mInterface.setPackagesSuspendedAsUser(packageNames.toArray(new String[] {}), + suspendedState, ((appExtras.size() > 0) ? appExtras : null), ((launcherExtras.size() > 0) ? launcherExtras : null), info, callingPackage, translatedUserId); - pw.println("Package " + packageName + " new suspended state: " - + mInterface.isPackageSuspendedForUser(packageName, translatedUserId)); + for (int i = 0; i < packageNames.size(); i++) { + final String packageName = packageNames.get(i); + pw.println("Package " + packageName + " new suspended state: " + + mInterface.isPackageSuspendedForUser(packageName, translatedUserId)); + } return 0; } catch (RemoteException | IllegalArgumentException e) { pw.println(e.toString()); @@ -3643,11 +3646,11 @@ class PackageManagerShellCommand extends ShellCommand { pw.println(" hide [--user USER_ID] PACKAGE_OR_COMPONENT"); pw.println(" unhide [--user USER_ID] PACKAGE_OR_COMPONENT"); pw.println(""); - pw.println(" suspend [--user USER_ID] TARGET-PACKAGE"); - pw.println(" Suspends the specified package (as user)."); + pw.println(" suspend [--user USER_ID] PACKAGE [PACKAGE...]"); + pw.println(" Suspends the specified package(s) (as user)."); pw.println(""); - pw.println(" unsuspend [--user USER_ID] TARGET-PACKAGE"); - pw.println(" Unsuspends the specified package (as user)."); + pw.println(" unsuspend [--user USER_ID] PACKAGE [PACKAGE...]"); + pw.println(" Unsuspends the specified package(s) (as user)."); pw.println(""); pw.println(" grant [--user USER_ID] PACKAGE PERMISSION"); pw.println(" revoke [--user USER_ID] PACKAGE PERMISSION"); diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/SuspendPackagesBroadcastTest.kt b/services/tests/mockingservicestests/src/com/android/server/pm/SuspendPackagesBroadcastTest.kt new file mode 100644 index 0000000000000..7a6110bdbda38 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/pm/SuspendPackagesBroadcastTest.kt @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.pm + +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.util.SparseArray +import com.android.server.testutils.any +import com.android.server.testutils.eq +import com.android.server.testutils.nullable +import com.android.server.testutils.whenever +import com.android.server.utils.WatchedArrayMap +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Captor +import org.mockito.Mockito.spy +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@RunWith(JUnit4::class) +class SuspendPackagesBroadcastTest { + + companion object { + const val TEST_PACKAGE_1 = "com.android.test.package1" + const val TEST_PACKAGE_2 = "com.android.test.package2" + const val TEST_USER_ID = 0 + } + + lateinit var pms: PackageManagerService + lateinit var packageSetting1: PackageSetting + lateinit var packageSetting2: PackageSetting + lateinit var packagesToSuspend: Array + lateinit var uidsToSuspend: IntArray + + @Captor + lateinit var bundleCaptor: ArgumentCaptor + + @Rule + @JvmField + val rule = MockSystemRule() + + @Before + @Throws(Exception::class) + fun setup() { + MockitoAnnotations.initMocks(this) + rule.system().stageNominalSystemState() + pms = spy(createPackageManagerService(TEST_PACKAGE_1, TEST_PACKAGE_2)) + packageSetting1 = pms.getPackageSetting(TEST_PACKAGE_1)!! + packageSetting2 = pms.getPackageSetting(TEST_PACKAGE_2)!! + packagesToSuspend = arrayOf(TEST_PACKAGE_1, TEST_PACKAGE_2) + uidsToSuspend = intArrayOf(packageSetting1.appId, packageSetting2.appId) + } + + @Test + @Throws(Exception::class) + fun sendPackagesSuspendedForUser_withSameVisibilityAllowList() { + mockAllowList(packageSetting1, allowList(10001, 10002, 10003)) + mockAllowList(packageSetting2, allowList(10001, 10002, 10003)) + + pms.sendPackagesSuspendedForUser( + packagesToSuspend, uidsToSuspend, TEST_USER_ID, /* suspended = */ true) + verify(pms).sendPackageBroadcast(any(), nullable(), bundleCaptor.capture(), + anyInt(), nullable(), nullable(), any(), nullable(), any(), nullable()) + + var changedPackages = bundleCaptor.value.getStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST) + var changedUids = bundleCaptor.value.getIntArray(Intent.EXTRA_CHANGED_UID_LIST) + assertThat(changedPackages).asList().containsExactly(TEST_PACKAGE_1, TEST_PACKAGE_2) + assertThat(changedUids).asList().containsExactly( + packageSetting1.appId, packageSetting2.appId) + } + + @Test + @Throws(Exception::class) + fun sendPackagesSuspendedForUser_withDifferentVisibilityAllowList() { + mockAllowList(packageSetting1, allowList(10001, 10002, 10003)) + mockAllowList(packageSetting2, allowList(10001, 10002, 10007)) + + pms.sendPackagesSuspendedForUser( + packagesToSuspend, uidsToSuspend, TEST_USER_ID, /* suspended = */ true) + verify(pms, times(2)).sendPackageBroadcast(any(), nullable(), bundleCaptor.capture(), + anyInt(), nullable(), nullable(), any(), nullable(), any(), nullable()) + + bundleCaptor.allValues.forEach { + var changedPackages = it.getStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST) + var changedUids = it.getIntArray(Intent.EXTRA_CHANGED_UID_LIST) + assertThat(changedPackages?.size).isEqualTo(1) + assertThat(changedUids?.size).isEqualTo(1) + assertThat(changedPackages?.get(0)).isAnyOf(TEST_PACKAGE_1, TEST_PACKAGE_2) + assertThat(changedUids?.get(0)).isAnyOf(packageSetting1.appId, packageSetting2.appId) + } + } + + @Test + @Throws(Exception::class) + fun sendPackagesSuspendedForUser_withNullVisibilityAllowList() { + mockAllowList(packageSetting1, allowList(10001, 10002, 10003)) + mockAllowList(packageSetting2, null) + + pms.sendPackagesSuspendedForUser( + packagesToSuspend, uidsToSuspend, TEST_USER_ID, /* suspended = */ true) + verify(pms, times(2)).sendPackageBroadcast(any(), nullable(), bundleCaptor.capture(), + anyInt(), nullable(), nullable(), any(), nullable(), nullable(), nullable()) + + bundleCaptor.allValues.forEach { + var changedPackages = it.getStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST) + var changedUids = it.getIntArray(Intent.EXTRA_CHANGED_UID_LIST) + assertThat(changedPackages?.size).isEqualTo(1) + assertThat(changedUids?.size).isEqualTo(1) + assertThat(changedPackages?.get(0)).isAnyOf(TEST_PACKAGE_1, TEST_PACKAGE_2) + assertThat(changedUids?.get(0)).isAnyOf(packageSetting1.appId, packageSetting2.appId) + } + } + + private fun allowList(vararg uids: Int) = SparseArray().apply { + this.put(TEST_USER_ID, uids) + } + + private fun mockAllowList(pkgSetting: PackageSetting, list: SparseArray?) { + whenever(rule.mocks().injector.appsFilter.getVisibilityAllowList(eq(pkgSetting), + any(IntArray::class.java), any() as WatchedArrayMap)) + .thenReturn(list) + } + + private fun createPackageManagerService(vararg stageExistingPackages: String): + PackageManagerService { + stageExistingPackages.forEach { + rule.system().stageScanExistingPackage(it, 1L, + rule.system().dataAppDirectory) + } + var pms = PackageManagerService(rule.mocks().injector, + false /*coreOnly*/, + false /*factoryTest*/, + MockSystem.DEFAULT_VERSION_INFO.fingerprint, + false /*isEngBuild*/, + false /*isUserDebugBuild*/, + Build.VERSION_CODES.CUR_DEVELOPMENT, + Build.VERSION.INCREMENTAL) + rule.system().validateFinalState() + return pms + } +} diff --git a/services/tests/servicestests/utils-mockito/com/android/server/testutils/MockitoUtils.kt b/services/tests/servicestests/utils-mockito/com/android/server/testutils/MockitoUtils.kt index c6e35cf84355c..e9329053c7405 100644 --- a/services/tests/servicestests/utils-mockito/com/android/server/testutils/MockitoUtils.kt +++ b/services/tests/servicestests/utils-mockito/com/android/server/testutils/MockitoUtils.kt @@ -93,3 +93,24 @@ inline fun mockThrowOnUnmocked(block: T.() -> Unit = {}) = spyThrowOnUnmocked(null, block) inline fun nullable() = ArgumentMatchers.nullable(T::class.java) + +/** + * Returns Mockito.any() as nullable type to avoid java.lang.IllegalStateException when + * null is returned. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +fun any(type: Class): T = Mockito.any(type) + +/** + * Wrapper around [Mockito.any] for generic types. + */ +inline fun any() = any(T::class.java) + +/** + * Returns Mockito.eq() as nullable type to avoid java.lang.IllegalStateException when + * null is returned. + * + * Generic T is nullable because implicitly bounded by Any?. + */ +fun eq(obj: T): T = Mockito.eq(obj)