diff options
| -rw-r--r-- | core/java/android/app/BroadcastStickyCache.java | 215 | ||||
| -rw-r--r-- | core/java/android/app/ContextImpl.java | 21 | ||||
| -rw-r--r-- | core/java/android/app/TEST_MAPPING | 4 | ||||
| -rw-r--r-- | services/core/java/com/android/server/am/BroadcastController.java | 44 | ||||
| -rw-r--r-- | services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java | 54 | ||||
| -rw-r--r-- | tests/broadcasts/unit/Android.bp | 44 | ||||
| -rw-r--r-- | tests/broadcasts/unit/AndroidManifest.xml | 27 | ||||
| -rw-r--r-- | tests/broadcasts/unit/AndroidTest.xml | 29 | ||||
| -rw-r--r-- | tests/broadcasts/unit/OWNERS | 2 | ||||
| -rw-r--r-- | tests/broadcasts/unit/TEST_MAPPING | 7 | ||||
| -rw-r--r-- | tests/broadcasts/unit/src/android/app/BroadcastStickyCacheTest.java | 231 |
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)); + } +} |