summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/app/BroadcastStickyCache.java215
-rw-r--r--core/java/android/app/ContextImpl.java21
-rw-r--r--core/java/android/app/TEST_MAPPING4
-rw-r--r--services/core/java/com/android/server/am/BroadcastController.java44
-rw-r--r--services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java54
-rw-r--r--tests/broadcasts/unit/Android.bp44
-rw-r--r--tests/broadcasts/unit/AndroidManifest.xml27
-rw-r--r--tests/broadcasts/unit/AndroidTest.xml29
-rw-r--r--tests/broadcasts/unit/OWNERS2
-rw-r--r--tests/broadcasts/unit/TEST_MAPPING7
-rw-r--r--tests/broadcasts/unit/src/android/app/BroadcastStickyCacheTest.java231
11 files changed, 651 insertions, 27 deletions
diff --git a/core/java/android/app/BroadcastStickyCache.java b/core/java/android/app/BroadcastStickyCache.java
new file mode 100644
index 000000000000..fe2e10752355
--- /dev/null
+++ b/core/java/android/app/BroadcastStickyCache.java
@@ -0,0 +1,215 @@
+/*
+ * 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 android.app;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.content.Context.RegisterReceiverFlags;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.hardware.usb.UsbManager;
+import android.media.AudioManager;
+import android.net.ConnectivityManager;
+import android.net.TetheringManager;
+import android.net.nsd.NsdManager;
+import android.net.wifi.WifiManager;
+import android.net.wifi.p2p.WifiP2pManager;
+import android.os.IpcDataCache;
+import android.os.IpcDataCache.Config;
+import android.os.UpdateLock;
+import android.telephony.TelephonyManager;
+import android.util.ArrayMap;
+import android.view.WindowManagerPolicyConstants;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
+
+/** @hide */
+public class BroadcastStickyCache {
+
+ @VisibleForTesting
+ public static final String[] STICKY_BROADCAST_ACTIONS = {
+ AudioManager.ACTION_HDMI_AUDIO_PLUG,
+ AudioManager.ACTION_HEADSET_PLUG,
+ AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED,
+ AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED,
+ AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION,
+ AudioManager.RINGER_MODE_CHANGED_ACTION,
+ ConnectivityManager.CONNECTIVITY_ACTION,
+ Intent.ACTION_BATTERY_CHANGED,
+ Intent.ACTION_DEVICE_STORAGE_FULL,
+ Intent.ACTION_DEVICE_STORAGE_LOW,
+ Intent.ACTION_SIM_STATE_CHANGED,
+ NsdManager.ACTION_NSD_STATE_CHANGED,
+ TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED,
+ TetheringManager.ACTION_TETHER_STATE_CHANGED,
+ UpdateLock.UPDATE_LOCK_CHANGED,
+ UsbManager.ACTION_USB_STATE,
+ WifiManager.ACTION_WIFI_SCAN_AVAILABILITY_CHANGED,
+ WifiManager.NETWORK_STATE_CHANGED_ACTION,
+ WifiManager.SUPPLICANT_STATE_CHANGED_ACTION,
+ WifiManager.WIFI_STATE_CHANGED_ACTION,
+ WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION,
+ WindowManagerPolicyConstants.ACTION_HDMI_PLUGGED,
+ "android.net.conn.INET_CONDITION_ACTION" // ConnectivityManager.INET_CONDITION_ACTION
+ };
+
+ @VisibleForTesting
+ public static final ArrayMap<String, String> sActionApiNameMap = new ArrayMap<>();
+
+ private static final ArrayMap<String, IpcDataCache.Config> sActionConfigMap = new ArrayMap<>();
+
+ private static final ArrayMap<StickyBroadcastFilter, IpcDataCache<Void, Intent>>
+ sFilterCacheMap = new ArrayMap<>();
+
+ static {
+ sActionApiNameMap.put(AudioManager.ACTION_HDMI_AUDIO_PLUG, "hdmi_audio_plug");
+ sActionApiNameMap.put(AudioManager.ACTION_HEADSET_PLUG, "headset_plug");
+ sActionApiNameMap.put(AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED,
+ "sco_audio_state_changed");
+ sActionApiNameMap.put(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED,
+ "action_sco_audio_state_updated");
+ sActionApiNameMap.put(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION,
+ "internal_ringer_mode_changed_action");
+ sActionApiNameMap.put(AudioManager.RINGER_MODE_CHANGED_ACTION,
+ "ringer_mode_changed");
+ sActionApiNameMap.put(ConnectivityManager.CONNECTIVITY_ACTION,
+ "connectivity_change");
+ sActionApiNameMap.put(Intent.ACTION_BATTERY_CHANGED, "battery_changed");
+ sActionApiNameMap.put(Intent.ACTION_DEVICE_STORAGE_FULL, "device_storage_full");
+ sActionApiNameMap.put(Intent.ACTION_DEVICE_STORAGE_LOW, "device_storage_low");
+ sActionApiNameMap.put(Intent.ACTION_SIM_STATE_CHANGED, "sim_state_changed");
+ sActionApiNameMap.put(NsdManager.ACTION_NSD_STATE_CHANGED, "nsd_state_changed");
+ sActionApiNameMap.put(TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED,
+ "service_providers_updated");
+ sActionApiNameMap.put(TetheringManager.ACTION_TETHER_STATE_CHANGED,
+ "tether_state_changed");
+ sActionApiNameMap.put(UpdateLock.UPDATE_LOCK_CHANGED, "update_lock_changed");
+ sActionApiNameMap.put(UsbManager.ACTION_USB_STATE, "usb_state");
+ sActionApiNameMap.put(WifiManager.ACTION_WIFI_SCAN_AVAILABILITY_CHANGED,
+ "wifi_scan_availability_changed");
+ sActionApiNameMap.put(WifiManager.NETWORK_STATE_CHANGED_ACTION,
+ "network_state_change");
+ sActionApiNameMap.put(WifiManager.SUPPLICANT_STATE_CHANGED_ACTION,
+ "supplicant_state_change");
+ sActionApiNameMap.put(WifiManager.WIFI_STATE_CHANGED_ACTION, "wifi_state_changed");
+ sActionApiNameMap.put(
+ WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION, "wifi_p2p_state_changed");
+ sActionApiNameMap.put(
+ WindowManagerPolicyConstants.ACTION_HDMI_PLUGGED, "hdmi_plugged");
+ sActionApiNameMap.put(
+ "android.net.conn.INET_CONDITION_ACTION", "inet_condition_action");
+ }
+
+ /**
+ * Checks whether we can use caching for the given filter.
+ */
+ public static boolean useCache(@Nullable IntentFilter filter) {
+ return Flags.useStickyBcastCache()
+ && filter != null
+ && filter.safeCountActions() == 1
+ && ArrayUtils.contains(STICKY_BROADCAST_ACTIONS, filter.getAction(0));
+ }
+
+ public static void invalidateCache(@NonNull String action) {
+ if (!Flags.useStickyBcastCache()
+ || !ArrayUtils.contains(STICKY_BROADCAST_ACTIONS, action)) {
+ return;
+ }
+ IpcDataCache.invalidateCache(IpcDataCache.MODULE_SYSTEM,
+ sActionApiNameMap.get(action));
+ }
+
+ public static void invalidateAllCaches() {
+ for (int i = sActionApiNameMap.size() - 1; i >= 0; i--) {
+ IpcDataCache.invalidateCache(IpcDataCache.MODULE_SYSTEM,
+ sActionApiNameMap.valueAt(i));
+ }
+ }
+
+ /**
+ * Returns the cached {@link Intent} based on the filter, if exits otherwise
+ * fetches the value from the service.
+ */
+ @Nullable
+ public static Intent getIntent(
+ @NonNull IApplicationThread applicationThread,
+ @NonNull String mBasePackageName,
+ @Nullable String attributionTag,
+ @NonNull IntentFilter filter,
+ @Nullable String broadcastPermission,
+ @UserIdInt int userId,
+ @RegisterReceiverFlags int flags) {
+ IpcDataCache<Void, Intent> intentDataCache = findIpcDataCache(filter);
+
+ if (intentDataCache == null) {
+ final String action = filter.getAction(0);
+ final StickyBroadcastFilter stickyBroadcastFilter =
+ new StickyBroadcastFilter(filter, action);
+ final Config config = getConfig(action);
+
+ intentDataCache =
+ new IpcDataCache<>(config,
+ (query) -> ActivityManager.getService().registerReceiverWithFeature(
+ applicationThread,
+ mBasePackageName,
+ attributionTag,
+ /* receiverId= */ "null",
+ /* receiver= */ null,
+ filter,
+ broadcastPermission,
+ userId,
+ flags));
+ sFilterCacheMap.put(stickyBroadcastFilter, intentDataCache);
+ }
+ return intentDataCache.query(null);
+ }
+
+ @VisibleForTesting
+ public static void clearCacheForTest() {
+ sFilterCacheMap.clear();
+ }
+
+ @Nullable
+ private static IpcDataCache<Void, Intent> findIpcDataCache(
+ @NonNull IntentFilter filter) {
+ for (int i = sFilterCacheMap.size() - 1; i >= 0; i--) {
+ StickyBroadcastFilter existingFilter = sFilterCacheMap.keyAt(i);
+ if (filter.getAction(0).equals(existingFilter.action())
+ && IntentFilter.filterEquals(existingFilter.filter(), filter)) {
+ return sFilterCacheMap.valueAt(i);
+ }
+ }
+ return null;
+ }
+
+ @NonNull
+ private static IpcDataCache.Config getConfig(@NonNull String action) {
+ if (!sActionConfigMap.containsKey(action)) {
+ // We only need 1 entry per cache but just to be on the safer side we are choosing 32
+ // although we don't expect more than 1.
+ sActionConfigMap.put(action,
+ new Config(32, IpcDataCache.MODULE_SYSTEM, sActionApiNameMap.get(action)));
+ }
+
+ return sActionConfigMap.get(action);
+ }
+
+ @VisibleForTesting
+ private record StickyBroadcastFilter(@NonNull IntentFilter filter, @NonNull String action) {
+ }
+}
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index cd56957ed5d1..dcbdc2348fbc 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -1922,10 +1922,23 @@ class ContextImpl extends Context {
}
}
try {
- final Intent intent = ActivityManager.getService().registerReceiverWithFeature(
- mMainThread.getApplicationThread(), mBasePackageName, getAttributionTag(),
- AppOpsManager.toReceiverId(receiver), rd, filter, broadcastPermission, userId,
- flags);
+ final Intent intent;
+ if (receiver == null && BroadcastStickyCache.useCache(filter)) {
+ intent = BroadcastStickyCache.getIntent(
+ mMainThread.getApplicationThread(),
+ mBasePackageName,
+ getAttributionTag(),
+ filter,
+ broadcastPermission,
+ userId,
+ flags);
+ } else {
+ intent = ActivityManager.getService().registerReceiverWithFeature(
+ mMainThread.getApplicationThread(), mBasePackageName, getAttributionTag(),
+ AppOpsManager.toReceiverId(receiver), rd, filter, broadcastPermission,
+ userId, flags);
+ }
+
if (intent != null) {
intent.setExtrasClassLoader(getClassLoader());
// TODO: determine at registration time if caller is
diff --git a/core/java/android/app/TEST_MAPPING b/core/java/android/app/TEST_MAPPING
index 5ed1f4e35533..637187e01160 100644
--- a/core/java/android/app/TEST_MAPPING
+++ b/core/java/android/app/TEST_MAPPING
@@ -177,6 +177,10 @@
{
"file_patterns": ["(/|^)AppOpsManager.java"],
"name": "CtsAppOpsTestCases"
+ },
+ {
+ "file_patterns": ["(/|^)BroadcastStickyCache.java"],
+ "name": "BroadcastUnitTests"
}
]
}
diff --git a/services/core/java/com/android/server/am/BroadcastController.java b/services/core/java/com/android/server/am/BroadcastController.java
index c6cb67f4efa8..354f281551b2 100644
--- a/services/core/java/com/android/server/am/BroadcastController.java
+++ b/services/core/java/com/android/server/am/BroadcastController.java
@@ -57,6 +57,7 @@ import android.app.ApplicationExitInfo;
import android.app.ApplicationThreadConstants;
import android.app.BackgroundStartPrivileges;
import android.app.BroadcastOptions;
+import android.app.BroadcastStickyCache;
import android.app.IApplicationThread;
import android.app.compat.CompatChanges;
import android.appwidget.AppWidgetManager;
@@ -183,6 +184,13 @@ class BroadcastController {
final HashMap<IBinder, ReceiverList> mRegisteredReceivers = new HashMap<>();
/**
+ * If {@code false} invalidate the list of {@link android.os.IpcDataCache} present inside the
+ * {@link BroadcastStickyCache} class.
+ * The invalidation is required to start caching of the sticky broadcast in the client side.
+ */
+ private volatile boolean mAreStickyCachesInvalidated = false;
+
+ /**
* Resolver for broadcast intents to registered receivers.
* Holds BroadcastFilter (subclass of IntentFilter).
*/
@@ -288,6 +296,11 @@ class BroadcastController {
IIntentReceiver receiver, IntentFilter filter, String permission,
int userId, int flags) {
mService.enforceNotIsolatedCaller("registerReceiver");
+
+ if (!mAreStickyCachesInvalidated) {
+ BroadcastStickyCache.invalidateAllCaches();
+ mAreStickyCachesInvalidated = true;
+ }
ArrayList<StickyBroadcast> stickyBroadcasts = null;
ProcessRecord callerApp = null;
final boolean visibleToInstantApps =
@@ -700,6 +713,7 @@ class BroadcastController {
String[] excludedPackages, int appOp, Bundle bOptions,
boolean serialized, boolean sticky, int userId) {
mService.enforceNotIsolatedCaller("broadcastIntent");
+ final int result;
synchronized (mService) {
intent = verifyBroadcastLocked(intent);
@@ -722,7 +736,7 @@ class BroadcastController {
final long origId = Binder.clearCallingIdentity();
try {
- return broadcastIntentLocked(callerApp,
+ result = broadcastIntentLocked(callerApp,
callerApp != null ? callerApp.info.packageName : null, callingFeatureId,
intent, resolvedType, resultToApp, resultTo, resultCode, resultData,
resultExtras, requiredPermissions, excludedPermissions, excludedPackages,
@@ -733,6 +747,11 @@ class BroadcastController {
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
}
}
+
+ if (sticky && result == ActivityManager.BROADCAST_SUCCESS) {
+ BroadcastStickyCache.invalidateCache(intent.getAction());
+ }
+ return result;
}
// Not the binder call surface
@@ -743,6 +762,7 @@ class BroadcastController {
boolean serialized, boolean sticky, int userId,
BackgroundStartPrivileges backgroundStartPrivileges,
@Nullable int[] broadcastAllowList) {
+ final int result;
synchronized (mService) {
intent = verifyBroadcastLocked(intent);
@@ -750,7 +770,7 @@ class BroadcastController {
String[] requiredPermissions = requiredPermission == null ? null
: new String[] {requiredPermission};
try {
- return broadcastIntentLocked(null, packageName, featureId, intent, resolvedType,
+ result = broadcastIntentLocked(null, packageName, featureId, intent, resolvedType,
resultToApp, resultTo, resultCode, resultData, resultExtras,
requiredPermissions, null, null, OP_NONE, bOptions, serialized, sticky, -1,
uid, realCallingUid, realCallingPid, userId,
@@ -760,6 +780,11 @@ class BroadcastController {
Binder.restoreCallingIdentity(origId);
}
}
+
+ if (sticky && result == ActivityManager.BROADCAST_SUCCESS) {
+ BroadcastStickyCache.invalidateCache(intent.getAction());
+ }
+ return result;
}
@GuardedBy("mService")
@@ -1458,6 +1483,7 @@ class BroadcastController {
list.add(StickyBroadcast.create(new Intent(intent), deferUntilActive,
callingUid, callerAppProcessState, resolvedType));
}
+ BroadcastStickyCache.invalidateCache(intent.getAction());
}
}
@@ -1724,6 +1750,7 @@ class BroadcastController {
Slog.w(TAG, msg);
throw new SecurityException(msg);
}
+ final ArrayList<String> changedStickyBroadcasts = new ArrayList<>();
synchronized (mStickyBroadcasts) {
ArrayMap<String, ArrayList<StickyBroadcast>> stickies = mStickyBroadcasts.get(userId);
if (stickies != null) {
@@ -1740,12 +1767,16 @@ class BroadcastController {
if (list.size() <= 0) {
stickies.remove(intent.getAction());
}
+ changedStickyBroadcasts.add(intent.getAction());
}
if (stickies.size() <= 0) {
mStickyBroadcasts.remove(userId);
}
}
}
+ for (int i = changedStickyBroadcasts.size() - 1; i >= 0; --i) {
+ BroadcastStickyCache.invalidateCache(changedStickyBroadcasts.get(i));
+ }
}
void finishReceiver(IBinder caller, int resultCode, String resultData,
@@ -2124,9 +2155,18 @@ class BroadcastController {
}
void removeStickyBroadcasts(int userId) {
+ final ArrayList<String> changedStickyBroadcasts = new ArrayList<>();
synchronized (mStickyBroadcasts) {
+ final ArrayMap<String, ArrayList<StickyBroadcast>> stickies =
+ mStickyBroadcasts.get(userId);
+ if (stickies != null) {
+ changedStickyBroadcasts.addAll(stickies.keySet());
+ }
mStickyBroadcasts.remove(userId);
}
+ for (int i = changedStickyBroadcasts.size() - 1; i >= 0; --i) {
+ BroadcastStickyCache.invalidateCache(changedStickyBroadcasts.get(i));
+ }
}
@NeverCompile
diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java
index ace6aae1a8fc..d9332ec05697 100644
--- a/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java
@@ -47,8 +47,10 @@ import static com.android.server.am.Flags.FLAG_AVOID_RESOLVING_TYPE;
import static com.android.server.am.ProcessList.NETWORK_STATE_BLOCK;
import static com.android.server.am.ProcessList.NETWORK_STATE_NO_CHANGE;
import static com.android.server.am.ProcessList.NETWORK_STATE_UNBLOCK;
+
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
+
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
@@ -105,6 +107,7 @@ import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.IProgressListener;
+import android.os.IpcDataCache;
import android.os.Looper;
import android.os.Message;
import android.os.Parcel;
@@ -959,28 +962,37 @@ public class ActivityManagerServiceTest {
@Test
@SuppressWarnings("GuardedBy")
public void testBroadcastStickyIntent_verifyTypeNotResolved() throws Exception {
- final Intent intent = new Intent(TEST_ACTION1);
- final Uri uri = new Uri.Builder()
- .scheme(SCHEME_CONTENT)
- .authority(TEST_AUTHORITY)
- .path("green")
- .build();
- intent.setData(uri);
- broadcastIntent(intent, null, true, TEST_MIME_TYPE, USER_ALL);
- assertStickyBroadcasts(mAms.getStickyBroadcastsForTest(TEST_ACTION1, USER_ALL),
- StickyBroadcast.create(intent, false, Process.myUid(), PROCESS_STATE_UNKNOWN,
- TEST_MIME_TYPE));
- when(mContentResolver.getType(uri)).thenReturn(TEST_MIME_TYPE);
+ MockitoSession mockitoSession =
+ ExtendedMockito.mockitoSession().mockStatic(IpcDataCache.class).startMocking();
- addUidRecord(TEST_UID, TEST_PACKAGE);
- final ProcessRecord procRecord = mAms.getProcessRecordLocked(TEST_PACKAGE, TEST_UID);
- final IntentFilter intentFilter = new IntentFilter(TEST_ACTION1);
- intentFilter.addDataType(TEST_MIME_TYPE);
- final Intent resultIntent = mAms.registerReceiverWithFeature(procRecord.getThread(),
- TEST_PACKAGE, null, null, null, intentFilter, null, TEST_USER,
- Context.RECEIVER_EXPORTED);
- assertNotNull(resultIntent);
- verify(mContentResolver, never()).getType(any());
+ try {
+ final Intent intent = new Intent(TEST_ACTION1);
+ final Uri uri = new Uri.Builder()
+ .scheme(SCHEME_CONTENT)
+ .authority(TEST_AUTHORITY)
+ .path("green")
+ .build();
+ intent.setData(uri);
+ broadcastIntent(intent, null, true, TEST_MIME_TYPE, USER_ALL);
+ assertStickyBroadcasts(mAms.getStickyBroadcastsForTest(TEST_ACTION1, USER_ALL),
+ StickyBroadcast.create(intent, false, Process.myUid(), PROCESS_STATE_UNKNOWN,
+ TEST_MIME_TYPE));
+ when(mContentResolver.getType(uri)).thenReturn(TEST_MIME_TYPE);
+ ExtendedMockito.doNothing().when(
+ () -> IpcDataCache.invalidateCache(anyString(), anyString()));
+
+ addUidRecord(TEST_UID, TEST_PACKAGE);
+ final ProcessRecord procRecord = mAms.getProcessRecordLocked(TEST_PACKAGE, TEST_UID);
+ final IntentFilter intentFilter = new IntentFilter(TEST_ACTION1);
+ intentFilter.addDataType(TEST_MIME_TYPE);
+ final Intent resultIntent = mAms.registerReceiverWithFeature(procRecord.getThread(),
+ TEST_PACKAGE, null, null, null, intentFilter, null, TEST_USER,
+ Context.RECEIVER_EXPORTED);
+ assertNotNull(resultIntent);
+ verify(mContentResolver, never()).getType(any());
+ } finally {
+ mockitoSession.finishMocking();
+ }
}
@SuppressWarnings("GuardedBy")
diff --git a/tests/broadcasts/unit/Android.bp b/tests/broadcasts/unit/Android.bp
new file mode 100644
index 000000000000..9e15ac41d84b
--- /dev/null
+++ b/tests/broadcasts/unit/Android.bp
@@ -0,0 +1,44 @@
+// 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 {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+ default_team: "trendy_team_framework_backstage_power",
+}
+
+android_test {
+ name: "BroadcastUnitTests",
+ srcs: ["src/**/*.java"],
+ defaults: [
+ "modules-utils-extended-mockito-rule-defaults",
+ ],
+ static_libs: [
+ "androidx.test.runner",
+ "androidx.test.rules",
+ "androidx.test.ext.junit",
+ "mockito-target-extended-minus-junit4",
+ "truth",
+ "flag-junit",
+ "android.app.flags-aconfig-java",
+ "junit-params",
+ ],
+ certificate: "platform",
+ platform_apis: true,
+ test_suites: ["device-tests"],
+}
diff --git a/tests/broadcasts/unit/AndroidManifest.xml b/tests/broadcasts/unit/AndroidManifest.xml
new file mode 100644
index 000000000000..61eb230f7957
--- /dev/null
+++ b/tests/broadcasts/unit/AndroidManifest.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.broadcasts.unit" >
+
+ <application android:debuggable="true">
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.broadcasts.unit"
+ android:label="Broadcasts Unit Tests"/>
+</manifest> \ No newline at end of file
diff --git a/tests/broadcasts/unit/AndroidTest.xml b/tests/broadcasts/unit/AndroidTest.xml
new file mode 100644
index 000000000000..b91e4783b69e
--- /dev/null
+++ b/tests/broadcasts/unit/AndroidTest.xml
@@ -0,0 +1,29 @@
+<!-- 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.
+-->
+<configuration description="Runs Broadcasts tests">
+ <option name="test-suite-tag" value="apct" />
+ <option name="test-tag" value="BroadcastUnitTests" />
+
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="BroadcastUnitTests.apk" />
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.broadcasts.unit" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <option name="hidden-api-checks" value="false"/>
+ </test>
+</configuration> \ No newline at end of file
diff --git a/tests/broadcasts/unit/OWNERS b/tests/broadcasts/unit/OWNERS
new file mode 100644
index 000000000000..f1e450b7e5f9
--- /dev/null
+++ b/tests/broadcasts/unit/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 316181
+include platform/frameworks/base:/BROADCASTS_OWNERS \ No newline at end of file
diff --git a/tests/broadcasts/unit/TEST_MAPPING b/tests/broadcasts/unit/TEST_MAPPING
new file mode 100644
index 000000000000..b920e2586c86
--- /dev/null
+++ b/tests/broadcasts/unit/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+ "postsubmit": [
+ {
+ "name": "BroadcastUnitTests"
+ }
+ ]
+}
diff --git a/tests/broadcasts/unit/src/android/app/BroadcastStickyCacheTest.java b/tests/broadcasts/unit/src/android/app/BroadcastStickyCacheTest.java
new file mode 100644
index 000000000000..ad032fb2fba6
--- /dev/null
+++ b/tests/broadcasts/unit/src/android/app/BroadcastStickyCacheTest.java
@@ -0,0 +1,231 @@
+/*
+ * 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 android.app;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotNull;
+
+import static org.junit.Assert.assertFalse;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.AudioManager;
+import android.os.IpcDataCache;
+import android.os.RemoteException;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+
+import com.android.dx.mockito.inline.extended.ExtendedMockito;
+import com.android.internal.annotations.Keep;
+import com.android.modules.utils.testing.ExtendedMockitoRule;
+
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+@RunWith(JUnitParamsRunner.class)
+public class BroadcastStickyCacheTest {
+
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+ @Rule
+ public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this)
+ .mockStatic(IpcDataCache.class)
+ .mockStatic(ActivityManager.class)
+ .build();
+
+ @Mock
+ private IActivityManager mActivityManagerMock;
+
+ @Mock
+ private IApplicationThread mIApplicationThreadMock;
+
+ @Keep
+ private static Object stickyBroadcastList() {
+ return BroadcastStickyCache.STICKY_BROADCAST_ACTIONS;
+ }
+
+ @Before
+ public void setUp() {
+ BroadcastStickyCache.clearCacheForTest();
+
+ doNothing().when(() -> IpcDataCache.invalidateCache(anyString(), anyString()));
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_USE_STICKY_BCAST_CACHE)
+ public void useCache_flagDisabled_returnsFalse() {
+ assertFalse(BroadcastStickyCache.useCache(new IntentFilter(Intent.ACTION_BATTERY_CHANGED)));
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_USE_STICKY_BCAST_CACHE)
+ public void useCache_nullFilter_returnsFalse() {
+ assertFalse(BroadcastStickyCache.useCache(null));
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_USE_STICKY_BCAST_CACHE)
+ public void useCache_filterWithoutAction_returnsFalse() {
+ assertFalse(BroadcastStickyCache.useCache(new IntentFilter()));
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_USE_STICKY_BCAST_CACHE)
+ public void useCache_filterWithoutStickyBroadcastAction_returnsFalse() {
+ assertFalse(BroadcastStickyCache.useCache(new IntentFilter(Intent.ACTION_BOOT_COMPLETED)));
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_USE_STICKY_BCAST_CACHE)
+ public void invalidateCache_flagDisabled_cacheNotInvalidated() {
+ final String apiName = BroadcastStickyCache.sActionApiNameMap.get(
+ AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION);
+
+ BroadcastStickyCache.invalidateCache(
+ AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION);
+
+ ExtendedMockito.verify(
+ () -> IpcDataCache.invalidateCache(eq(IpcDataCache.MODULE_SYSTEM), eq(apiName)),
+ times(0));
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_USE_STICKY_BCAST_CACHE)
+ public void invalidateCache_broadcastNotSticky_cacheNotInvalidated() {
+ BroadcastStickyCache.invalidateCache(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+
+ ExtendedMockito.verify(
+ () -> IpcDataCache.invalidateCache(eq(IpcDataCache.MODULE_SYSTEM), anyString()),
+ times(0));
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_USE_STICKY_BCAST_CACHE)
+ public void invalidateCache_withStickyBroadcast_cacheInvalidated() {
+ final String apiName = BroadcastStickyCache.sActionApiNameMap.get(
+ Intent.ACTION_BATTERY_CHANGED);
+
+ BroadcastStickyCache.invalidateCache(Intent.ACTION_BATTERY_CHANGED);
+
+ ExtendedMockito.verify(
+ () -> IpcDataCache.invalidateCache(eq(IpcDataCache.MODULE_SYSTEM), eq(apiName)),
+ times(1));
+ }
+
+ @Test
+ public void invalidateAllCaches_cacheInvalidated() {
+ BroadcastStickyCache.invalidateAllCaches();
+
+ for (int i = BroadcastStickyCache.sActionApiNameMap.size() - 1; i > -1; i--) {
+ final String apiName = BroadcastStickyCache.sActionApiNameMap.valueAt(i);
+ ExtendedMockito.verify(() -> IpcDataCache.invalidateCache(anyString(),
+ eq(apiName)), times(1));
+ }
+ }
+
+ @Test
+ @Parameters(method = "stickyBroadcastList")
+ public void getIntent_createNewCache_verifyRegisterReceiverIsCalled(String action)
+ throws RemoteException {
+ setActivityManagerMock(action);
+ final IntentFilter filter = new IntentFilter(action);
+ final Intent intent = queryIntent(filter);
+
+ assertNotNull(intent);
+ assertEquals(intent.getAction(), action);
+ verify(mActivityManagerMock, times(1)).registerReceiverWithFeature(
+ eq(mIApplicationThreadMock), anyString(), anyString(), anyString(), any(),
+ eq(filter), anyString(), anyInt(), anyInt());
+ }
+
+ @Test
+ public void getIntent_querySameValueTwice_verifyRegisterReceiverIsCalledOnce()
+ throws RemoteException {
+ setActivityManagerMock(Intent.ACTION_DEVICE_STORAGE_LOW);
+ final Intent intent = queryIntent(new IntentFilter(Intent.ACTION_DEVICE_STORAGE_LOW));
+ final Intent cachedIntent = queryIntent(new IntentFilter(Intent.ACTION_DEVICE_STORAGE_LOW));
+
+ assertNotNull(intent);
+ assertEquals(intent.getAction(), Intent.ACTION_DEVICE_STORAGE_LOW);
+ assertNotNull(cachedIntent);
+ assertEquals(cachedIntent.getAction(), Intent.ACTION_DEVICE_STORAGE_LOW);
+
+ verify(mActivityManagerMock, times(1)).registerReceiverWithFeature(
+ eq(mIApplicationThreadMock), anyString(), anyString(), anyString(), any(),
+ any(), anyString(), anyInt(), anyInt());
+ }
+
+ @Test
+ public void getIntent_querySameActionWithDifferentFilter_verifyRegisterReceiverCalledTwice()
+ throws RemoteException {
+ setActivityManagerMock(Intent.ACTION_DEVICE_STORAGE_LOW);
+ final IntentFilter filter = new IntentFilter(Intent.ACTION_DEVICE_STORAGE_LOW);
+ final Intent intent = queryIntent(filter);
+
+ final IntentFilter newFilter = new IntentFilter(Intent.ACTION_DEVICE_STORAGE_LOW);
+ newFilter.addDataScheme("file");
+ final Intent newIntent = queryIntent(newFilter);
+
+ assertNotNull(intent);
+ assertEquals(intent.getAction(), Intent.ACTION_DEVICE_STORAGE_LOW);
+ assertNotNull(newIntent);
+ assertEquals(newIntent.getAction(), Intent.ACTION_DEVICE_STORAGE_LOW);
+
+ verify(mActivityManagerMock, times(1)).registerReceiverWithFeature(
+ eq(mIApplicationThreadMock), anyString(), anyString(), anyString(), any(),
+ eq(filter), anyString(), anyInt(), anyInt());
+
+ verify(mActivityManagerMock, times(1)).registerReceiverWithFeature(
+ eq(mIApplicationThreadMock), anyString(), anyString(), anyString(), any(),
+ eq(newFilter), anyString(), anyInt(), anyInt());
+ }
+
+ private Intent queryIntent(IntentFilter filter) {
+ return BroadcastStickyCache.getIntent(
+ mIApplicationThreadMock,
+ "android",
+ "android",
+ filter,
+ "system",
+ 0,
+ 0
+ );
+ }
+
+ private void setActivityManagerMock(String action) throws RemoteException {
+ when(ActivityManager.getService()).thenReturn(mActivityManagerMock);
+ when(mActivityManagerMock.registerReceiverWithFeature(any(), anyString(),
+ anyString(), anyString(), any(), any(), anyString(), anyInt(),
+ anyInt())).thenReturn(new Intent(action));
+ }
+}