unplugged-system/packages/modules/Permission/service/java/com/android/safetycenter/SafetyCenterBroadcastDispatcher.java

406 lines
17 KiB
Java

/*
* 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.
*
* <p>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.
*
* <p>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<String> safetySourceIds) {
List<Broadcast> broadcasts = mSafetyCenterConfigReader.getBroadcasts();
BroadcastOptions broadcastOptions = createBroadcastOptions();
String broadcastId =
mSafetyCenterRefreshTracker.reportRefreshInProgress(
refreshReason, userProfileGroup);
boolean hasSentAtLeastOneBroadcast = false;
for (int i = 0; i < broadcasts.size(); i++) {
Broadcast broadcast = broadcasts.get(i);
hasSentAtLeastOneBroadcast |=
sendRefreshSafetySourcesBroadcast(
broadcast,
broadcastOptions,
refreshReason,
userProfileGroup,
broadcastId,
safetySourceIds);
}
if (!hasSentAtLeastOneBroadcast) {
mSafetyCenterRefreshTracker.clearRefresh(broadcastId);
return null;
}
return broadcastId;
}
private boolean sendRefreshSafetySourcesBroadcast(
Broadcast broadcast,
BroadcastOptions broadcastOptions,
@RefreshReason int refreshReason,
UserProfileGroup userProfileGroup,
String broadcastId,
@Nullable List<String> requiredSourceIds) {
boolean hasSentAtLeastOneBroadcast = false;
int requestType = RefreshReasons.toRefreshRequestType(refreshReason);
String packageName = broadcast.getPackageName();
Set<String> deniedSourceIds = getRefreshDeniedSourceIds(refreshReason);
SparseArray<List<String>> userIdsToSourceIds =
getUserIdsToSourceIds(broadcast, userProfileGroup, refreshReason);
for (int i = 0; i < userIdsToSourceIds.size(); i++) {
int userId = userIdsToSourceIds.keyAt(i);
List<String> sourceIds = userIdsToSourceIds.valueAt(i);
if (!deniedSourceIds.isEmpty()) {
sourceIds = new ArrayList<>(sourceIds);
sourceIds.removeAll(deniedSourceIds);
}
if (requiredSourceIds != null) {
sourceIds = new ArrayList<>(sourceIds);
sourceIds.retainAll(requiredSourceIds);
}
if (sourceIds.isEmpty()) {
continue;
}
Intent intent = createRefreshIntent(requestType, packageName, sourceIds, broadcastId);
boolean broadcastWasSent =
sendBroadcastIfResolves(intent, UserHandle.of(userId), broadcastOptions);
if (broadcastWasSent) {
mSafetyCenterRefreshTracker.reportSourceRefreshesInFlight(
broadcastId, sourceIds, userId);
}
hasSentAtLeastOneBroadcast |= broadcastWasSent;
}
return hasSentAtLeastOneBroadcast;
}
/**
* Triggers an {@link SafetyCenterManager#ACTION_SAFETY_CENTER_ENABLED_CHANGED} broadcast for
* all safety sources.
*
* <p>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<Broadcast> broadcasts = mSafetyCenterConfigReader.getBroadcasts();
BroadcastOptions broadcastOptions = createBroadcastOptions();
List<UserProfileGroup> userProfileGroups =
UserProfileGroup.getAllUserProfileGroups(mContext);
for (int i = 0; i < broadcasts.size(); i++) {
Broadcast broadcast = broadcasts.get(i);
sendEnabledChangedBroadcast(broadcast, broadcastOptions, userProfileGroups);
}
Intent implicitIntent = createImplicitEnabledChangedIntent();
sendBroadcast(implicitIntent, UserHandle.SYSTEM, READ_SAFETY_CENTER_STATUS, null);
}
private void sendEnabledChangedBroadcast(
Broadcast broadcast,
BroadcastOptions broadcastOptions,
List<UserProfileGroup> userProfileGroups) {
Intent intent = createExplicitEnabledChangedIntent(broadcast.getPackageName());
// The same ENABLED reason is used here for both enable and disable events. It is not sent
// externally and is only used internally to filter safety sources in the methods of the
// Broadcast class.
int refreshReason = REFRESH_REASON_SAFETY_CENTER_ENABLED;
for (int i = 0; i < userProfileGroups.size(); i++) {
UserProfileGroup userProfileGroup = userProfileGroups.get(i);
SparseArray<List<String>> 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<String> sourceIdsToRefresh,
String broadcastId) {
String[] sourceIdsArray = sourceIdsToRefresh.toArray(new String[0]);
return createBroadcastIntent(ACTION_REFRESH_SAFETY_SOURCES)
.putExtra(EXTRA_REFRESH_SAFETY_SOURCES_REQUEST_TYPE, requestType)
.putExtra(EXTRA_REFRESH_SAFETY_SOURCE_IDS, sourceIdsArray)
.putExtra(EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID, broadcastId)
.setPackage(packageName);
}
private static Intent createBroadcastIntent(String intentAction) {
return new Intent(intentAction).setFlags(FLAG_RECEIVER_FOREGROUND);
}
// TODO(b/193460475): Remove when tooling supports SystemApi to public API.
@SuppressLint("NewApi")
private static BroadcastOptions createBroadcastOptions() {
BroadcastOptions broadcastOptions = BroadcastOptions.makeBasic();
Duration allowListDuration = SafetyCenterFlags.getFgsAllowlistDuration();
// This call requires the START_FOREGROUND_SERVICES_FROM_BACKGROUND permission.
final long callingId = Binder.clearCallingIdentity();
try {
broadcastOptions.setTemporaryAppAllowlist(
allowListDuration.toMillis(),
TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED,
REASON_REFRESH_SAFETY_SOURCES,
"Safety Center is requesting data from safety sources");
} finally {
Binder.restoreCallingIdentity(callingId);
}
return broadcastOptions;
}
/** Returns the list of source IDs for which refreshing is denied for the given reason. */
private static Set<String> getRefreshDeniedSourceIds(@RefreshReason int refreshReason) {
if (RefreshReasons.isBackgroundRefresh(refreshReason)) {
return SafetyCenterFlags.getBackgroundRefreshDeniedSourceIds();
} else {
return Collections.emptySet();
}
}
/**
* Returns a flattened mapping from user IDs to lists of source IDs for those users. The map is
* in the form of a {@link SparseArray} where the int keys are user IDs and the values are the
* lists of source IDs.
*
* <p>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.
*
* <p>Every value present is a non-empty list, but the overall result may be empty.
*/
private SparseArray<List<String>> getUserIdsToSourceIds(
Broadcast broadcast,
UserProfileGroup userProfileGroup,
@RefreshReason int refreshReason) {
int[] managedProfileIds = userProfileGroup.getManagedRunningProfilesUserIds();
SparseArray<List<String>> result = new SparseArray<>(managedProfileIds.length + 1);
List<String> profileParentSources =
getSourceIdsForRefreshReason(
refreshReason,
broadcast.getSourceIdsForProfileParent(),
broadcast.getSourceIdsForProfileParentOnPageOpen(),
userProfileGroup.getProfileParentUserId());
if (!profileParentSources.isEmpty()) {
result.put(userProfileGroup.getProfileParentUserId(), profileParentSources);
}
for (int i = 0; i < managedProfileIds.length; i++) {
List<String> managedProfileSources =
getSourceIdsForRefreshReason(
refreshReason,
broadcast.getSourceIdsForManagedProfiles(),
broadcast.getSourceIdsForManagedProfilesOnPageOpen(),
managedProfileIds[i]);
if (!managedProfileSources.isEmpty()) {
result.put(managedProfileIds[i], managedProfileSources);
}
}
return result;
}
/**
* Returns the sources to refresh for the given {@code refreshReason}.
*
* <p>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<String> getSourceIdsForRefreshReason(
@RefreshReason int refreshReason,
List<String> allSourceIds,
List<String> pageOpenSourceIds,
@UserIdInt int userId) {
if (refreshReason != REFRESH_REASON_PAGE_OPEN) {
return allSourceIds;
}
List<String> sourceIds = new ArrayList<>();
ArraySet<String> flagAllowListedSourceIds =
SafetyCenterFlags.getOverrideRefreshOnPageOpenSourceIds();
for (int i = 0; i < allSourceIds.size(); i++) {
String sourceId = allSourceIds.get(i);
if (pageOpenSourceIds.contains(sourceId)
|| flagAllowListedSourceIds.contains(sourceId)
|| mSafetyCenterDataManager.getSafetySourceDataInternal(
SafetySourceKey.of(sourceId, userId))
== null) {
sourceIds.add(sourceId);
}
}
return unmodifiableList(sourceIds);
}
}