/* * 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.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.EXTRA_REFRESH_REQUEST_TYPE_FETCH_FRESH_DATA; import static android.safetycenter.SafetyCenterManager.EXTRA_REFRESH_REQUEST_TYPE_GET_DATA; import static android.safetycenter.SafetyCenterManager.EXTRA_REFRESH_SAFETY_SOURCES_REQUEST_TYPE; import static android.safetycenter.SafetyCenterManager.REFRESH_REASON_PAGE_OPEN; import static android.safetycenter.SafetyCenterManager.REFRESH_REASON_RESCAN_BUTTON_CLICK; import static android.safetycenter.config.SafetySource.SAFETY_SOURCE_TYPE_STATIC; import android.annotation.NonNull; import android.app.BroadcastOptions; import android.content.Context; import android.content.Intent; import android.os.UserHandle; import android.safetycenter.SafetyCenterManager.RefreshReason; import android.safetycenter.config.SafetyCenterConfig; import android.safetycenter.config.SafetySource; import android.util.Log; import androidx.annotation.RequiresApi; import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; /** * Class to manage and track refresh broadcasts sent by {@link SafetyCenterService}. * *

This class isn't thread safe. Thread safety must be handled by the caller. */ @RequiresApi(TIRAMISU) final class SafetyCenterRefreshManager { private static final String TAG = "SafetyCenterRefreshMana"; /** * Time for which an app, upon receiving a particular broadcast, will be placed on a temporary * power allowlist allowing it to start a foreground service from the background. */ // TODO(b/219553295): Use a Device Config value instead, so that this duration can be // easily adjusted. private static final Duration ALLOWLIST_DURATION = Duration.ofSeconds(20); @NonNull private final List mAdditionalSafetySourcePackageNames = new ArrayList<>(); @NonNull private final Context mContext; @NonNull private final SafetyCenterConfigReader mSafetyCenterConfigReader; /** * Creates a {@link SafetyCenterRefreshManager} using the given {@link Context} and {@link * SafetyCenterConfigReader}. */ SafetyCenterRefreshManager( @NonNull Context context, @NonNull SafetyCenterConfigReader safetyCenterConfigReader) { mContext = context; mSafetyCenterConfigReader = safetyCenterConfigReader; } /** Adds a package name representing a source to refresh. */ // TODO(b/218157907): Remove this method and use a SafetyCenterConfigReader field in // SafetyCenterRefreshManager instead once ag/16834483 is submitted. void addAdditionalSafetySourcePackageNames(@NonNull String packageName) { mAdditionalSafetySourcePackageNames.add(packageName); } /** Removes all additional package names representing sources to refresh. */ // TODO(b/218157907): Remove this method and use a SafetyCenterConfigReader field in // SafetyCenterRefreshManager instead once ag/16834483 is submitted. void clearAdditionalSafetySourcePackageNames() { mAdditionalSafetySourcePackageNames.clear(); } /** * Triggers a refresh of safety sources by sending them broadcasts with action {@link * android.safetycenter.SafetyCenterManager#ACTION_REFRESH_SAFETY_SOURCES}. */ void refreshSafetySources(@RefreshReason int refreshReason) { int requestType; switch (refreshReason) { case REFRESH_REASON_RESCAN_BUTTON_CLICK: requestType = EXTRA_REFRESH_REQUEST_TYPE_FETCH_FRESH_DATA; break; case REFRESH_REASON_PAGE_OPEN: requestType = EXTRA_REFRESH_REQUEST_TYPE_GET_DATA; break; default: throw new IllegalArgumentException("Invalid refresh reason: " + refreshReason); } SafetyCenterConfig safetyCenterConfig = mSafetyCenterConfigReader.getSafetyCenterConfig(); if (safetyCenterConfig == null) { Log.w(TAG, "SafetyCenterConfig unavailable, ignoring refresh"); return; } // TODO(b/219702252): Use a more efficient data structure for this. List safetySourcesToRefresh = safetyCenterConfig.getSafetySourcesGroups().stream() .flatMap(group -> group.getSafetySources().stream()) .filter( // Only send broadcasts to dynamic safety sources. source -> source.getType() != SAFETY_SOURCE_TYPE_STATIC) .collect(Collectors.toList()); sendRefreshBroadcastToSafetySources(safetySourcesToRefresh, requestType); sendRefreshBroadcastToAdditionalSafetySourceReceivers(requestType); } private void sendRefreshBroadcastToSafetySources( List safetySources, int requestType) { Intent broadcastIntent = new Intent(ACTION_REFRESH_SAFETY_SOURCES) .putExtra(EXTRA_REFRESH_SAFETY_SOURCES_REQUEST_TYPE, requestType) .setFlags(FLAG_RECEIVER_FOREGROUND); BroadcastOptions broadcastOptions = BroadcastOptions.makeBasic(); // The following operation requires START_FOREGROUND_SERVICES_FROM_BACKGROUND // permission. broadcastOptions.setTemporaryAppAllowlist( ALLOWLIST_DURATION.toMillis(), TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED, REASON_REFRESH_SAFETY_SOURCES, "Safety Center is requesting data from safety sources"); for (SafetySource source : safetySources) { Intent broadcastIntentForSource = new Intent(broadcastIntent).setPackage(source.getPackageName()); // TODO(b/215144069): Add cross profile support for safety sources which support // both personal and work profile. This implementation invokes // `sendBroadcastAsUser` in order to invoke the permission. // The following operation requires INTERACT_ACROSS_USERS permission. mContext.sendBroadcastAsUser( broadcastIntentForSource, UserHandle.CURRENT, SEND_SAFETY_CENTER_UPDATE, broadcastOptions.toBundle()); } } private void sendRefreshBroadcastToAdditionalSafetySourceReceivers(int requestType) { Intent broadcastIntent = new Intent(ACTION_REFRESH_SAFETY_SOURCES) .putExtra(EXTRA_REFRESH_SAFETY_SOURCES_REQUEST_TYPE, requestType) .setFlags(FLAG_RECEIVER_FOREGROUND); BroadcastOptions broadcastOptions = BroadcastOptions.makeBasic(); // The following operation requires START_FOREGROUND_SERVICES_FROM_BACKGROUND // permission. broadcastOptions.setTemporaryAppAllowlist( ALLOWLIST_DURATION.toMillis(), TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED, REASON_REFRESH_SAFETY_SOURCES, "Safety Center is requesting data from safety sources"); for (String packageName : mAdditionalSafetySourcePackageNames) { // The following operation requires INTERACT_ACROSS_USERS permission. mContext.sendBroadcastAsUser( new Intent(broadcastIntent).setPackage(packageName), UserHandle.CURRENT, SEND_SAFETY_CENTER_UPDATE, broadcastOptions.toBundle()); } } }