/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.safetycenter; import static android.Manifest.permission.READ_SAFETY_CENTER_STATUS; import static android.Manifest.permission.SEND_SAFETY_CENTER_UPDATE; import static android.content.Intent.FLAG_RECEIVER_FOREGROUND; import static android.os.Build.VERSION_CODES.TIRAMISU; import static android.os.PowerExemptionManager.REASON_REFRESH_SAFETY_SOURCES; import static android.os.PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED; import static android.safetycenter.SafetyCenterManager.ACTION_REFRESH_SAFETY_SOURCES; import static android.safetycenter.SafetyCenterManager.ACTION_SAFETY_CENTER_ENABLED_CHANGED; import static android.safetycenter.SafetyCenterManager.EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID; import static android.safetycenter.SafetyCenterManager.EXTRA_REFRESH_SAFETY_SOURCES_REQUEST_TYPE; import static android.safetycenter.SafetyCenterManager.EXTRA_REFRESH_SAFETY_SOURCE_IDS; import static android.safetycenter.SafetyCenterManager.REFRESH_REASON_PAGE_OPEN; import static android.safetycenter.SafetyCenterManager.REFRESH_REASON_SAFETY_CENTER_ENABLED; import static java.util.Collections.unmodifiableList; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.annotation.UserIdInt; import android.app.BroadcastOptions; import android.content.Context; import android.content.Intent; import android.os.Binder; import android.os.UserHandle; import android.safetycenter.SafetyCenterManager; import android.safetycenter.SafetyCenterManager.RefreshReason; import android.safetycenter.SafetyCenterManager.RefreshRequestType; import android.safetycenter.SafetySourceData; import android.util.ArraySet; import android.util.Log; import android.util.SparseArray; import androidx.annotation.RequiresApi; import com.android.permission.util.PackageUtils; import com.android.safetycenter.SafetyCenterConfigReader.Broadcast; import com.android.safetycenter.data.SafetyCenterDataManager; import java.time.Duration; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; import javax.annotation.concurrent.NotThreadSafe; /** * A class that dispatches SafetyCenter broadcasts. * *
This class isn't thread safe. Thread safety must be handled by the caller. */ @RequiresApi(TIRAMISU) @NotThreadSafe final class SafetyCenterBroadcastDispatcher { private static final String TAG = "SafetyCenterBroadcastDi"; private final Context mContext; private final SafetyCenterConfigReader mSafetyCenterConfigReader; private final SafetyCenterRefreshTracker mSafetyCenterRefreshTracker; private final SafetyCenterDataManager mSafetyCenterDataManager; SafetyCenterBroadcastDispatcher( Context context, SafetyCenterConfigReader safetyCenterConfigReader, SafetyCenterRefreshTracker safetyCenterRefreshTracker, SafetyCenterDataManager safetyCenterDataManager) { mContext = context; mSafetyCenterConfigReader = safetyCenterConfigReader; mSafetyCenterRefreshTracker = safetyCenterRefreshTracker; mSafetyCenterDataManager = safetyCenterDataManager; } /** * Triggers a refresh of safety sources by sending them broadcasts with action {@link * SafetyCenterManager#ACTION_REFRESH_SAFETY_SOURCES}, and returns the associated broadcast id. * *
Returns {@code null} if no broadcast was sent.
     *
     * @param safetySourceIds list of IDs to specify the safety sources to be refreshed or a {@code
     *     null} value to refresh all safety sources.
     */
    @Nullable
    String sendRefreshSafetySources(
            @RefreshReason int refreshReason,
            UserProfileGroup userProfileGroup,
            @Nullable List This method also sends an implicit broadcast globally (which requires the {@link
     * android.Manifest.permission#READ_SAFETY_CENTER_STATUS} permission).
     */
    // TODO(b/227310195): Consider adding a boolean extra to the intent instead of having clients
    //  rely on SafetyCenterManager#isSafetyCenterEnabled()?
    void sendEnabledChanged() {
        List The set of user IDs (keys) is the profile parent user ID of {@code userProfileGroup} plus
     * the (possibly empty) set of running managed profile user IDs in that group.
     *
     *  Every value present is a non-empty list, but the overall result may be empty.
     */
    private SparseArray For {@link SafetyCenterManager#REFRESH_REASON_PAGE_OPEN}, returns a copy of {@code
     * allSourceIds} filtered to contain only sources that have refreshOnPageOpenAllowed in the XML
     * config, or are in the safety_center_override_refresh_on_page_open_sources flag, or don't have
     * any {@link SafetySourceData} provided.
     */
    private List> userIdsToSourceIds =
                getUserIdsToSourceIds(broadcast, userProfileGroup, refreshReason);
        for (int i = 0; i < userIdsToSourceIds.size(); i++) {
            int userId = userIdsToSourceIds.keyAt(i);
            List
> userIdsToSourceIds =
                    getUserIdsToSourceIds(broadcast, userProfileGroup, refreshReason);
            for (int j = 0; j < userIdsToSourceIds.size(); j++) {
                int userId = userIdsToSourceIds.keyAt(j);
                sendBroadcastIfResolves(intent, UserHandle.of(userId), broadcastOptions);
            }
        }
    }
    private boolean sendBroadcastIfResolves(
            Intent intent, UserHandle userHandle, @Nullable BroadcastOptions broadcastOptions) {
        if (!doesBroadcastResolve(intent, userHandle)) {
            Log.w(
                    TAG,
                    "No receiver for intent targeting "
                            + intent.getPackage()
                            + " and user "
                            + userHandle);
            return false;
        }
        Log.v(
                TAG,
                "Found receiver for intent targeting "
                        + intent.getPackage()
                        + " and user "
                        + userHandle);
        sendBroadcast(intent, userHandle, SEND_SAFETY_CENTER_UPDATE, broadcastOptions);
        return true;
    }
    // TODO(b/193460475): Remove when tooling supports SystemApi to public API.
    @SuppressLint("NewApi")
    private void sendBroadcast(
            Intent intent,
            UserHandle userHandle,
            String permission,
            @Nullable BroadcastOptions broadcastOptions) {
        // This call requires the INTERACT_ACROSS_USERS permission.
        final long callingId = Binder.clearCallingIdentity();
        try {
            mContext.sendBroadcastAsUser(
                    intent,
                    userHandle,
                    permission,
                    broadcastOptions == null ? null : broadcastOptions.toBundle());
        } finally {
            Binder.restoreCallingIdentity(callingId);
        }
    }
    private boolean doesBroadcastResolve(Intent broadcastIntent, UserHandle userHandle) {
        return !PackageUtils.queryUnfilteredBroadcastReceiversAsUser(
                        broadcastIntent, 0, userHandle.getIdentifier(), mContext)
                .isEmpty();
    }
    private static Intent createExplicitEnabledChangedIntent(String packageName) {
        return createImplicitEnabledChangedIntent().setPackage(packageName);
    }
    private static Intent createImplicitEnabledChangedIntent() {
        return createBroadcastIntent(ACTION_SAFETY_CENTER_ENABLED_CHANGED);
    }
    private static Intent createRefreshIntent(
            @RefreshRequestType int requestType,
            String packageName,
            List
> getUserIdsToSourceIds(
            Broadcast broadcast,
            UserProfileGroup userProfileGroup,
            @RefreshReason int refreshReason) {
        int[] managedProfileIds = userProfileGroup.getManagedRunningProfilesUserIds();
        SparseArray
> result = new SparseArray<>(managedProfileIds.length + 1);
        List