Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 432146ce authored by mayankkk's avatar mayankkk
Browse files

Cache Sticky broadcast Intents in the client side.

When a client makes a sticky broadcast intent query,
cache the result on the client side using IpcDataCache to avoid having
to make a call to the system_server process for
subsequent queries.

Bug: 356148006
Test: atest StickyBroadcastCacheTest.java
Flag: android.app.use_sticky_bcast_cache
Change-Id: I714661e2fdbb1734aae26d159af6fe033cbc878d
parent a963c7a6
Loading
Loading
Loading
Loading
+215 −0
Original line number Diff line number Diff line
/*
 * 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) {
    }
}
+17 −4
Original line number Diff line number Diff line
@@ -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,
            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
+4 −0
Original line number Diff line number Diff line
@@ -177,6 +177,10 @@
        {
            "file_patterns": ["(/|^)AppOpsManager.java"],
            "name": "CtsAppOpsTestCases"
        },
        {
            "file_patterns": ["(/|^)BroadcastStickyCache.java"],
            "name": "BroadcastUnitTests"
        }
    ]
}
+42 −2
Original line number Diff line number Diff line
@@ -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;
@@ -182,6 +183,13 @@ class BroadcastController {
    @GuardedBy("mService")
    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
+33 −21
Original line number Diff line number Diff line
@@ -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,6 +962,10 @@ public class ActivityManagerServiceTest {
    @Test
    @SuppressWarnings("GuardedBy")
    public void testBroadcastStickyIntent_verifyTypeNotResolved() throws Exception {
        MockitoSession mockitoSession =
                ExtendedMockito.mockitoSession().mockStatic(IpcDataCache.class).startMocking();

        try {
            final Intent intent = new Intent(TEST_ACTION1);
            final Uri uri = new Uri.Builder()
                    .scheme(SCHEME_CONTENT)
@@ -971,6 +978,8 @@ public class ActivityManagerServiceTest {
                    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);
@@ -981,6 +990,9 @@ public class ActivityManagerServiceTest {
                    Context.RECEIVER_EXPORTED);
            assertNotNull(resultIntent);
            verify(mContentResolver, never()).getType(any());
        } finally {
            mockitoSession.finishMocking();
        }
    }

    @SuppressWarnings("GuardedBy")
Loading