Merge "Add unit test for limiting the sending of the PACKAGE_CHANGED broadcast" into main
diff --git a/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java b/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java
index a28e3c1..52e8c52 100644
--- a/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java
+++ b/services/core/java/com/android/server/pm/PackageMonitorCallbackHelper.java
@@ -38,6 +38,7 @@
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import java.util.ArrayList;
@@ -45,7 +46,8 @@
/** Helper class to handle PackageMonitorCallback and notify the registered client. This is mainly
* used by PackageMonitor to improve the broadcast latency. */
-class PackageMonitorCallbackHelper {
+@VisibleForTesting
+public class PackageMonitorCallbackHelper {
private static final boolean DEBUG = false;
private static final String TAG = "PackageMonitorCallbackHelper";
diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/BroadcastHelperTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/BroadcastHelperTest.java
new file mode 100644
index 0000000..1be5cef
--- /dev/null
+++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/BroadcastHelperTest.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2024 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 static android.content.pm.Flags.FLAG_REDUCE_BROADCASTS_FOR_COMPONENT_STATE_CHANGES;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.ActivityManager;
+import android.app.ActivityManagerInternal;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.Message;
+import android.os.UserHandle;
+import android.platform.test.annotations.AppModeFull;
+import android.platform.test.annotations.AppModeNonSdkSandbox;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.internal.pm.parsing.pkg.AndroidPackageInternal;
+import com.android.internal.pm.pkg.component.ParsedActivity;
+import com.android.server.pm.pkg.PackageStateInternal;
+
+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.MockitoAnnotations;
+
+import java.util.ArrayList;
+
+@AppModeFull
+@AppModeNonSdkSandbox
+@RunWith(AndroidJUnit4.class)
+public class BroadcastHelperTest {
+ private static final String TAG = "BroadcastHelperTest";
+ private static final String PACKAGE_CHANGED_TEST_PACKAGE_NAME = "testpackagename";
+ private static final String PACKAGE_CHANGED_TEST_MAIN_ACTIVITY =
+ PACKAGE_CHANGED_TEST_PACKAGE_NAME + ".MainActivity";
+ private static final String PERMISSION_PACKAGE_CHANGED_BROADCAST_ON_COMPONENT_STATE_CHANGED =
+ "android.permission.INTERNAL_RECEIVE_PACKAGE_CHANGED_BROADCAST_ON_COMPONENT_STATE_CHANGED";
+
+ @Rule
+ public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+ @Mock
+ ActivityManagerInternal mMockActivityManagerInternal;
+ @Mock
+ AndroidPackageInternal mMockAndroidPackageInternal;
+ @Mock
+ Computer mMockSnapshot;
+ @Mock
+ Handler mMockHandler;
+ @Mock
+ PackageManagerServiceInjector mMockPackageManagerServiceInjector;
+ @Mock
+ PackageMonitorCallbackHelper mMockPackageMonitorCallbackHelper;
+ @Mock
+ PackageStateInternal mMockPackageStateInternal;
+ @Mock
+ ParsedActivity mMockParsedActivity;
+ @Mock
+ UserManagerInternal mMockUserManagerInternal;
+
+ private Context mContext;
+ private BroadcastHelper mBroadcastHelper;
+
+ @Before
+ public void setup() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mContext = InstrumentationRegistry.getInstrumentation().getContext();
+
+ when(mMockHandler.sendMessageAtTime(any(Message.class), anyLong())).thenAnswer(
+ i -> {
+ ((Message) i.getArguments()[0]).getCallback().run();
+ return true;
+ });
+ when(mMockPackageManagerServiceInjector.getActivityManagerInternal()).thenReturn(
+ mMockActivityManagerInternal);
+ when(mMockPackageManagerServiceInjector.getContext()).thenReturn(mContext);
+ when(mMockPackageManagerServiceInjector.getHandler()).thenReturn(mMockHandler);
+ when(mMockPackageManagerServiceInjector.getPackageMonitorCallbackHelper()).thenReturn(
+ mMockPackageMonitorCallbackHelper);
+ when(mMockPackageManagerServiceInjector.getUserManagerInternal()).thenReturn(
+ mMockUserManagerInternal);
+
+ mBroadcastHelper = new BroadcastHelper(mMockPackageManagerServiceInjector);
+ }
+
+ @RequiresFlagsEnabled(FLAG_REDUCE_BROADCASTS_FOR_COMPONENT_STATE_CHANGES)
+ @Test
+ public void changeNonExportedComponent_sendPackageChangedBroadcastToSystem_withPermission()
+ throws Exception {
+ changeComponentAndSendPackageChangedBroadcast(false /* changeExportedComponent */);
+
+ ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
+ verify(mMockActivityManagerInternal).broadcastIntentWithCallback(
+ captor.capture(), eq(null),
+ eq(new String[]{PERMISSION_PACKAGE_CHANGED_BROADCAST_ON_COMPONENT_STATE_CHANGED}),
+ anyInt(), eq(null), eq(null), eq(null));
+ Intent intent = captor.getValue();
+ assertNotNull(intent);
+ assertThat(intent.getPackage()).isEqualTo("android");
+ }
+
+ @RequiresFlagsEnabled(FLAG_REDUCE_BROADCASTS_FOR_COMPONENT_STATE_CHANGES)
+ @Test
+ public void changeNonExportedComponent_sendPackageChangedBroadcastToApplicationItself()
+ throws Exception {
+ changeComponentAndSendPackageChangedBroadcast(false /* changeExportedComponent */);
+
+ ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
+ verify(mMockActivityManagerInternal).broadcastIntentWithCallback(captor.capture(), eq(null),
+ eq(null), anyInt(), eq(null), eq(null), eq(null));
+ Intent intent = captor.getValue();
+ assertNotNull(intent);
+ assertThat(intent.getPackage()).isEqualTo(PACKAGE_CHANGED_TEST_PACKAGE_NAME);
+ }
+
+ @Test
+ public void changeExportedComponent_sendPackageChangedBroadcastToAll() throws Exception {
+ changeComponentAndSendPackageChangedBroadcast(true /* changeExportedComponent */);
+
+ ArgumentCaptor<Intent> captor = ArgumentCaptor.forClass(Intent.class);
+ verify(mMockActivityManagerInternal).broadcastIntentWithCallback(captor.capture(), eq(null),
+ eq(null), anyInt(), eq(null), eq(null), eq(null));
+ Intent intent = captor.getValue();
+ assertNotNull(intent);
+ assertNull(intent.getPackage());
+ }
+
+ private void changeComponentAndSendPackageChangedBroadcast(boolean changeExportedComponent) {
+ when(mMockSnapshot.getPackageStateInternal(eq(PACKAGE_CHANGED_TEST_PACKAGE_NAME),
+ anyInt())).thenReturn(mMockPackageStateInternal);
+ when(mMockSnapshot.isInstantAppInternal(any(), anyInt(), anyInt())).thenReturn(false);
+ when(mMockSnapshot.getVisibilityAllowLists(any(), any())).thenReturn(null);
+ when(mMockPackageStateInternal.getPkg()).thenReturn(mMockAndroidPackageInternal);
+
+ when(mMockParsedActivity.getClassName()).thenReturn(
+ PACKAGE_CHANGED_TEST_MAIN_ACTIVITY);
+ when(mMockParsedActivity.isExported()).thenReturn(changeExportedComponent);
+ ArrayList<ParsedActivity> parsedActivities = new ArrayList<>();
+ parsedActivities.add(mMockParsedActivity);
+
+ when(mMockAndroidPackageInternal.getReceivers()).thenReturn(new ArrayList<>());
+ when(mMockAndroidPackageInternal.getProviders()).thenReturn(new ArrayList<>());
+ when(mMockAndroidPackageInternal.getServices()).thenReturn(new ArrayList<>());
+ when(mMockAndroidPackageInternal.getActivities()).thenReturn(parsedActivities);
+
+ doNothing().when(mMockPackageMonitorCallbackHelper).notifyPackageChanged(any(),
+ anyBoolean(), any(), anyInt(), any(), any(), any(), any(), any());
+ when(mMockActivityManagerInternal.broadcastIntentWithCallback(any(), any(), any(), anyInt(),
+ any(), any(), any())).thenReturn(ActivityManager.BROADCAST_SUCCESS);
+
+ ArrayList<String> componentNames = new ArrayList<>();
+ componentNames.add(PACKAGE_CHANGED_TEST_MAIN_ACTIVITY);
+
+ mBroadcastHelper.sendPackageChangedBroadcast(mMockSnapshot,
+ PACKAGE_CHANGED_TEST_PACKAGE_NAME, true /* dontKillApp */, componentNames,
+ UserHandle.USER_SYSTEM, "test" /* reason */);
+ }
+}