summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/app/BroadcastStickyCache.java229
-rw-r--r--core/java/android/app/ContextImpl.java17
-rw-r--r--core/java/android/app/TEST_MAPPING4
-rw-r--r--core/java/android/app/activity_manager.aconfig11
-rw-r--r--services/core/java/com/android/server/am/BroadcastController.java34
-rw-r--r--tests/broadcasts/OWNERS2
-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/TEST_MAPPING15
-rw-r--r--tests/broadcasts/unit/src/android/app/BroadcastStickyCacheTest.java258
11 files changed, 663 insertions, 7 deletions
diff --git a/core/java/android/app/BroadcastStickyCache.java b/core/java/android/app/BroadcastStickyCache.java
new file mode 100644
index 000000000000..d6f061be3b00
--- /dev/null
+++ b/core/java/android/app/BroadcastStickyCache.java
@@ -0,0 +1,229 @@
+/*
+ * 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.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+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.SystemProperties;
+import android.os.UpdateLock;
+import android.telephony.TelephonyManager;
+import android.util.ArrayMap;
+import android.view.WindowManagerPolicyConstants;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
+
+import java.util.ArrayList;
+
+/** @hide */
+public class BroadcastStickyCache {
+
+ private static final String[] CACHED_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
+ };
+
+ @GuardedBy("sCachedStickyBroadcasts")
+ private static final ArrayList<CachedStickyBroadcast> sCachedStickyBroadcasts =
+ new ArrayList<>();
+
+ @GuardedBy("sCachedPropertyHandles")
+ private static final ArrayMap<String, SystemProperties.Handle> sCachedPropertyHandles =
+ new ArrayMap<>();
+
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public static boolean useCache(@Nullable IntentFilter filter) {
+ if (!shouldCache(filter)) {
+ return false;
+ }
+ synchronized (sCachedStickyBroadcasts) {
+ final CachedStickyBroadcast cachedStickyBroadcast = getValueUncheckedLocked(filter);
+ if (cachedStickyBroadcast == null) {
+ return false;
+ }
+ final long version = cachedStickyBroadcast.propertyHandle.getLong(-1 /* def */);
+ return version > 0 && cachedStickyBroadcast.version == version;
+ }
+ }
+
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ public static void add(@Nullable IntentFilter filter, @Nullable Intent intent) {
+ if (!shouldCache(filter)) {
+ return;
+ }
+ synchronized (sCachedStickyBroadcasts) {
+ CachedStickyBroadcast cachedStickyBroadcast = getValueUncheckedLocked(filter);
+ if (cachedStickyBroadcast == null) {
+ final String key = getKey(filter.getAction(0));
+ final SystemProperties.Handle handle = SystemProperties.find(key);
+ final long version = handle == null ? -1 : handle.getLong(-1 /* def */);
+ if (version == -1) {
+ return;
+ }
+ cachedStickyBroadcast = new CachedStickyBroadcast(filter, handle);
+ sCachedStickyBroadcasts.add(cachedStickyBroadcast);
+ cachedStickyBroadcast.intent = intent;
+ cachedStickyBroadcast.version = version;
+ } else {
+ cachedStickyBroadcast.intent = intent;
+ cachedStickyBroadcast.version = cachedStickyBroadcast.propertyHandle
+ .getLong(-1 /* def */);
+ }
+ }
+ }
+
+ private static boolean shouldCache(@Nullable IntentFilter filter) {
+ if (!Flags.useStickyBcastCache()) {
+ return false;
+ }
+ if (filter == null || filter.safeCountActions() != 1) {
+ return false;
+ }
+ if (!ArrayUtils.contains(CACHED_BROADCAST_ACTIONS, filter.getAction(0))) {
+ return false;
+ }
+ return true;
+ }
+
+ @VisibleForTesting
+ @NonNull
+ public static String getKey(@NonNull String action) {
+ return "cache_key.system_server.sticky_bcast." + action;
+ }
+
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+ @Nullable
+ public static Intent getIntentUnchecked(@NonNull IntentFilter filter) {
+ synchronized (sCachedStickyBroadcasts) {
+ final CachedStickyBroadcast cachedStickyBroadcast = getValueUncheckedLocked(filter);
+ return cachedStickyBroadcast.intent;
+ }
+ }
+
+ @GuardedBy("sCachedStickyBroadcasts")
+ @Nullable
+ private static CachedStickyBroadcast getValueUncheckedLocked(@NonNull IntentFilter filter) {
+ for (int i = sCachedStickyBroadcasts.size() - 1; i >= 0; --i) {
+ final CachedStickyBroadcast cachedStickyBroadcast = sCachedStickyBroadcasts.get(i);
+ if (IntentFilter.filterEquals(filter, cachedStickyBroadcast.filter)) {
+ return cachedStickyBroadcast;
+ }
+ }
+ return null;
+ }
+
+ public static void incrementVersion(@NonNull String action) {
+ if (!shouldIncrementVersion(action)) {
+ return;
+ }
+ final String key = getKey(action);
+ synchronized (sCachedPropertyHandles) {
+ SystemProperties.Handle handle = sCachedPropertyHandles.get(key);
+ final long version;
+ if (handle == null) {
+ handle = SystemProperties.find(key);
+ if (handle != null) {
+ sCachedPropertyHandles.put(key, handle);
+ }
+ }
+ version = handle == null ? 0 : handle.getLong(0 /* def */);
+ SystemProperties.set(key, String.valueOf(version + 1));
+ if (handle == null) {
+ sCachedPropertyHandles.put(key, SystemProperties.find(key));
+ }
+ }
+ }
+
+ public static void incrementVersionIfExists(@NonNull String action) {
+ if (!shouldIncrementVersion(action)) {
+ return;
+ }
+ final String key = getKey(action);
+ synchronized (sCachedPropertyHandles) {
+ final SystemProperties.Handle handle = sCachedPropertyHandles.get(key);
+ if (handle == null) {
+ return;
+ }
+ final long version = handle.getLong(0 /* def */);
+ SystemProperties.set(key, String.valueOf(version + 1));
+ }
+ }
+
+ private static boolean shouldIncrementVersion(@NonNull String action) {
+ if (!Flags.useStickyBcastCache()) {
+ return false;
+ }
+ if (!ArrayUtils.contains(CACHED_BROADCAST_ACTIONS, action)) {
+ return false;
+ }
+ return true;
+ }
+
+ @VisibleForTesting
+ public static void clearForTest() {
+ synchronized (sCachedStickyBroadcasts) {
+ sCachedStickyBroadcasts.clear();
+ }
+ synchronized (sCachedPropertyHandles) {
+ sCachedPropertyHandles.clear();
+ }
+ }
+
+ private static final class CachedStickyBroadcast {
+ @NonNull public final IntentFilter filter;
+ @Nullable public Intent intent;
+ @IntRange(from = 0) public long version;
+ @NonNull public final SystemProperties.Handle propertyHandle;
+
+ CachedStickyBroadcast(@NonNull IntentFilter filter,
+ @NonNull SystemProperties.Handle propertyHandle) {
+ this.filter = filter;
+ this.propertyHandle = propertyHandle;
+ }
+ }
+}
diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java
index 90fba2962a23..ecef0db46bb2 100644
--- a/core/java/android/app/ContextImpl.java
+++ b/core/java/android/app/ContextImpl.java
@@ -1921,10 +1921,19 @@ 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.getIntentUnchecked(filter);
+ } else {
+ intent = ActivityManager.getService().registerReceiverWithFeature(
+ mMainThread.getApplicationThread(), mBasePackageName, getAttributionTag(),
+ AppOpsManager.toReceiverId(receiver), rd, filter, broadcastPermission,
+ userId,
+ flags);
+ if (receiver == null) {
+ BroadcastStickyCache.add(filter, intent);
+ }
+ }
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/core/java/android/app/activity_manager.aconfig b/core/java/android/app/activity_manager.aconfig
index 38bd576d607a..56488e7cd296 100644
--- a/core/java/android/app/activity_manager.aconfig
+++ b/core/java/android/app/activity_manager.aconfig
@@ -147,3 +147,14 @@ flag {
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ namespace: "backstage_power"
+ name: "use_sticky_bcast_cache"
+ description: "Use cache for sticky broadcast intents"
+ is_fixed_read_only: true
+ bug: "356148006"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/services/core/java/com/android/server/am/BroadcastController.java b/services/core/java/com/android/server/am/BroadcastController.java
index f7085b4b26b4..15f1085b7125 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;
@@ -685,6 +686,7 @@ class BroadcastController {
boolean serialized, boolean sticky, int userId) {
mService.enforceNotIsolatedCaller("broadcastIntent");
+ int result;
synchronized (mService) {
intent = verifyBroadcastLocked(intent);
@@ -706,7 +708,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,
@@ -717,6 +719,10 @@ class BroadcastController {
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
}
}
+ if (sticky && result == ActivityManager.BROADCAST_SUCCESS) {
+ BroadcastStickyCache.incrementVersion(intent.getAction());
+ }
+ return result;
}
// Not the binder call surface
@@ -727,6 +733,7 @@ class BroadcastController {
boolean serialized, boolean sticky, int userId,
BackgroundStartPrivileges backgroundStartPrivileges,
@Nullable int[] broadcastAllowList) {
+ int result;
synchronized (mService) {
intent = verifyBroadcastLocked(intent);
@@ -734,7 +741,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,
@@ -744,6 +751,10 @@ class BroadcastController {
Binder.restoreCallingIdentity(origId);
}
}
+ if (sticky && result == ActivityManager.BROADCAST_SUCCESS) {
+ BroadcastStickyCache.incrementVersion(intent.getAction());
+ }
+ return result;
}
@GuardedBy("mService")
@@ -1442,6 +1453,7 @@ class BroadcastController {
list.add(StickyBroadcast.create(new Intent(intent), deferUntilActive,
callingUid, callerAppProcessState, resolvedType));
}
+ BroadcastStickyCache.incrementVersion(intent.getAction());
}
}
@@ -1708,6 +1720,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) {
@@ -1724,12 +1737,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.incrementVersionIfExists(changedStickyBroadcasts.get(i));
+ }
}
void finishReceiver(IBinder caller, int resultCode, String resultData,
@@ -1892,7 +1909,9 @@ class BroadcastController {
private void sendPackageBroadcastLocked(int cmd, String[] packages, int userId) {
mService.mProcessList.sendPackageBroadcastLocked(cmd, packages, userId);
- }private List<ResolveInfo> collectReceiverComponents(
+ }
+
+ private List<ResolveInfo> collectReceiverComponents(
Intent intent, String resolvedType, int callingUid, int callingPid,
int[] users, int[] broadcastAllowList) {
// TODO: come back and remove this assumption to triage all broadcasts
@@ -2108,9 +2127,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.incrementVersionIfExists(changedStickyBroadcasts.get(i));
+ }
}
@NeverCompile
diff --git a/tests/broadcasts/OWNERS b/tests/broadcasts/OWNERS
new file mode 100644
index 000000000000..d2e1f815e8dc
--- /dev/null
+++ b/tests/broadcasts/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 316181
+include platform/frameworks/base:/BROADCASTS_OWNERS
diff --git a/tests/broadcasts/unit/Android.bp b/tests/broadcasts/unit/Android.bp
new file mode 100644
index 000000000000..069247735cd4
--- /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"],
+}
+
+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",
+ ],
+ 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..e9c5248e4d98
--- /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/TEST_MAPPING b/tests/broadcasts/unit/TEST_MAPPING
new file mode 100644
index 000000000000..0e824c54e92e
--- /dev/null
+++ b/tests/broadcasts/unit/TEST_MAPPING
@@ -0,0 +1,15 @@
+{
+ "postsubmit": [
+ {
+ "name": "BroadcastUnitTests",
+ "options": [
+ {
+ "exclude-annotation": "androidx.test.filters.FlakyTest"
+ },
+ {
+ "exclude-annotation": "org.junit.Ignore"
+ }
+ ]
+ }
+ ]
+}
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..b7c412dea999
--- /dev/null
+++ b/tests/broadcasts/unit/src/android/app/BroadcastStickyCacheTest.java
@@ -0,0 +1,258 @@
+/*
+ * 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 android.content.Intent.ACTION_BATTERY_CHANGED;
+import static android.content.Intent.ACTION_DEVICE_STORAGE_LOW;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.BatteryManager;
+import android.os.Bundle;
+import android.os.SystemProperties;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+import android.util.ArrayMap;
+
+import androidx.annotation.GuardedBy;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.modules.utils.testing.ExtendedMockitoRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+@EnableFlags(Flags.FLAG_USE_STICKY_BCAST_CACHE)
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class BroadcastStickyCacheTest {
+ @ClassRule
+ public static final SetFlagsRule.ClassRule mClassRule = new SetFlagsRule.ClassRule();
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = mClassRule.createSetFlagsRule();
+
+ @Rule
+ public final ExtendedMockitoRule mExtendedMockitoRule = new ExtendedMockitoRule.Builder(this)
+ .mockStatic(SystemProperties.class)
+ .build();
+
+ private static final String PROP_KEY_BATTERY_CHANGED = BroadcastStickyCache.getKey(
+ ACTION_BATTERY_CHANGED);
+
+ private final TestSystemProps mTestSystemProps = new TestSystemProps();
+
+ @Before
+ public void setUp() {
+ doAnswer(invocation -> {
+ final String name = invocation.getArgument(0);
+ final long value = Long.parseLong(invocation.getArgument(1));
+ mTestSystemProps.add(name, value);
+ return null;
+ }).when(() -> SystemProperties.set(anyString(), anyString()));
+ doAnswer(invocation -> {
+ final String name = invocation.getArgument(0);
+ final TestSystemProps.Handle testHandle = mTestSystemProps.query(name);
+ if (testHandle == null) {
+ return null;
+ }
+ final SystemProperties.Handle handle = Mockito.mock(SystemProperties.Handle.class);
+ doAnswer(handleInvocation -> testHandle.getLong(-1)).when(handle).getLong(anyLong());
+ return handle;
+ }).when(() -> SystemProperties.find(anyString()));
+ }
+
+ @After
+ public void tearDown() {
+ mTestSystemProps.clear();
+ BroadcastStickyCache.clearForTest();
+ }
+
+ @Test
+ public void testUseCache_nullFilter() {
+ assertThat(BroadcastStickyCache.useCache(null)).isEqualTo(false);
+ }
+
+ @Test
+ public void testUseCache_noActions() {
+ final IntentFilter filter = new IntentFilter();
+ assertThat(BroadcastStickyCache.useCache(filter)).isEqualTo(false);
+ }
+
+ @Test
+ public void testUseCache_multipleActions() {
+ final IntentFilter filter = new IntentFilter();
+ filter.addAction(ACTION_DEVICE_STORAGE_LOW);
+ filter.addAction(ACTION_BATTERY_CHANGED);
+ assertThat(BroadcastStickyCache.useCache(filter)).isEqualTo(false);
+ }
+
+ @Test
+ public void testUseCache_valueNotSet() {
+ final IntentFilter filter = new IntentFilter(ACTION_BATTERY_CHANGED);
+ assertThat(BroadcastStickyCache.useCache(filter)).isEqualTo(false);
+ }
+
+ @Test
+ public void testUseCache() {
+ final IntentFilter filter = new IntentFilter(ACTION_BATTERY_CHANGED);
+ final Intent intent = new Intent(ACTION_BATTERY_CHANGED)
+ .putExtra(BatteryManager.EXTRA_LEVEL, 90);
+ BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED);
+ BroadcastStickyCache.add(filter, intent);
+ assertThat(BroadcastStickyCache.useCache(filter)).isEqualTo(true);
+ }
+
+ @Test
+ public void testUseCache_versionMismatch() {
+ final IntentFilter filter = new IntentFilter(ACTION_BATTERY_CHANGED);
+ final Intent intent = new Intent(ACTION_BATTERY_CHANGED)
+ .putExtra(BatteryManager.EXTRA_LEVEL, 90);
+ BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED);
+ BroadcastStickyCache.add(filter, intent);
+ BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED);
+
+ assertThat(BroadcastStickyCache.useCache(filter)).isEqualTo(false);
+ }
+
+ @Test
+ public void testAdd() {
+ final IntentFilter filter = new IntentFilter(ACTION_BATTERY_CHANGED);
+ Intent intent = new Intent(ACTION_BATTERY_CHANGED)
+ .putExtra(BatteryManager.EXTRA_LEVEL, 90);
+ BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED);
+ BroadcastStickyCache.add(filter, intent);
+ assertThat(BroadcastStickyCache.useCache(filter)).isEqualTo(true);
+ Intent actualIntent = BroadcastStickyCache.getIntentUnchecked(filter);
+ assertThat(actualIntent).isNotNull();
+ assertEquals(actualIntent, intent);
+
+ intent = new Intent(ACTION_BATTERY_CHANGED)
+ .putExtra(BatteryManager.EXTRA_LEVEL, 99);
+ BroadcastStickyCache.add(filter, intent);
+ actualIntent = BroadcastStickyCache.getIntentUnchecked(filter);
+ assertThat(actualIntent).isNotNull();
+ assertEquals(actualIntent, intent);
+ }
+
+ @Test
+ public void testIncrementVersion_propExists() {
+ SystemProperties.set(PROP_KEY_BATTERY_CHANGED, String.valueOf(100));
+
+ BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED);
+ assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(101);
+ BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED);
+ assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(102);
+ }
+
+ @Test
+ public void testIncrementVersion_propNotExists() {
+ // Verify that the property doesn't exist
+ assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(-1);
+
+ BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED);
+ assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(1);
+ BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED);
+ assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(2);
+ }
+
+ @Test
+ public void testIncrementVersionIfExists_propExists() {
+ BroadcastStickyCache.incrementVersion(ACTION_BATTERY_CHANGED);
+
+ BroadcastStickyCache.incrementVersionIfExists(ACTION_BATTERY_CHANGED);
+ assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(2);
+ BroadcastStickyCache.incrementVersionIfExists(ACTION_BATTERY_CHANGED);
+ assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(3);
+ }
+
+ @Test
+ public void testIncrementVersionIfExists_propNotExists() {
+ // Verify that the property doesn't exist
+ assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(-1);
+
+ BroadcastStickyCache.incrementVersionIfExists(ACTION_BATTERY_CHANGED);
+ assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(-1);
+ // Verify that property is not added as part of the querying.
+ BroadcastStickyCache.incrementVersionIfExists(ACTION_BATTERY_CHANGED);
+ assertThat(mTestSystemProps.get(PROP_KEY_BATTERY_CHANGED, -1 /* def */)).isEqualTo(-1);
+ }
+
+ private void assertEquals(Intent actualIntent, Intent expectedIntent) {
+ assertThat(actualIntent.getAction()).isEqualTo(expectedIntent.getAction());
+ assertEquals(actualIntent.getExtras(), expectedIntent.getExtras());
+ }
+
+ private void assertEquals(Bundle actualExtras, Bundle expectedExtras) {
+ assertWithMessage("Extras expected=%s, actual=%s", expectedExtras, actualExtras)
+ .that(actualExtras.kindofEquals(expectedExtras)).isTrue();
+ }
+
+ private static final class TestSystemProps {
+ @GuardedBy("mSysProps")
+ private final ArrayMap<String, Long> mSysProps = new ArrayMap<>();
+
+ public void add(String name, long value) {
+ synchronized (mSysProps) {
+ mSysProps.put(name, value);
+ }
+ }
+
+ public long get(String name, long defaultValue) {
+ synchronized (mSysProps) {
+ final int idx = mSysProps.indexOfKey(name);
+ return idx >= 0 ? mSysProps.valueAt(idx) : defaultValue;
+ }
+ }
+
+ public Handle query(String name) {
+ synchronized (mSysProps) {
+ return mSysProps.containsKey(name) ? new Handle(name) : null;
+ }
+ }
+
+ public void clear() {
+ synchronized (mSysProps) {
+ mSysProps.clear();
+ }
+ }
+
+ public class Handle {
+ private final String mName;
+
+ Handle(String name) {
+ mName = name;
+ }
+
+ public long getLong(long defaultValue) {
+ return get(mName, defaultValue);
+ }
+ }
+ }
+}