• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.safetycenter;
18 
19 import static android.Manifest.permission.READ_SAFETY_CENTER_STATUS;
20 import static android.Manifest.permission.SEND_SAFETY_CENTER_UPDATE;
21 import static android.content.Intent.FLAG_RECEIVER_FOREGROUND;
22 import static android.os.Build.VERSION_CODES.TIRAMISU;
23 import static android.os.PowerExemptionManager.REASON_REFRESH_SAFETY_SOURCES;
24 import static android.os.PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED;
25 import static android.safetycenter.SafetyCenterManager.ACTION_REFRESH_SAFETY_SOURCES;
26 import static android.safetycenter.SafetyCenterManager.ACTION_SAFETY_CENTER_ENABLED_CHANGED;
27 import static android.safetycenter.SafetyCenterManager.EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID;
28 import static android.safetycenter.SafetyCenterManager.EXTRA_REFRESH_SAFETY_SOURCES_REQUEST_TYPE;
29 import static android.safetycenter.SafetyCenterManager.EXTRA_REFRESH_SAFETY_SOURCE_IDS;
30 import static android.safetycenter.SafetyCenterManager.REFRESH_REASON_PAGE_OPEN;
31 import static android.safetycenter.SafetyCenterManager.REFRESH_REASON_SAFETY_CENTER_ENABLED;
32 
33 import static java.util.Collections.unmodifiableList;
34 
35 import android.annotation.Nullable;
36 import android.annotation.SuppressLint;
37 import android.annotation.UserIdInt;
38 import android.app.BroadcastOptions;
39 import android.content.Context;
40 import android.content.Intent;
41 import android.os.Binder;
42 import android.os.UserHandle;
43 import android.safetycenter.SafetyCenterManager;
44 import android.safetycenter.SafetyCenterManager.RefreshReason;
45 import android.safetycenter.SafetyCenterManager.RefreshRequestType;
46 import android.safetycenter.SafetySourceData;
47 import android.util.ArraySet;
48 import android.util.Log;
49 import android.util.SparseArray;
50 
51 import androidx.annotation.RequiresApi;
52 
53 import com.android.permission.util.PackageUtils;
54 import com.android.safetycenter.SafetyCenterConfigReader.Broadcast;
55 import com.android.safetycenter.data.SafetyCenterDataManager;
56 
57 import java.time.Duration;
58 import java.util.ArrayList;
59 import java.util.Collections;
60 import java.util.List;
61 import java.util.Set;
62 
63 import javax.annotation.concurrent.NotThreadSafe;
64 
65 /**
66  * A class that dispatches SafetyCenter broadcasts.
67  *
68  * <p>This class isn't thread safe. Thread safety must be handled by the caller.
69  */
70 @RequiresApi(TIRAMISU)
71 @NotThreadSafe
72 final class SafetyCenterBroadcastDispatcher {
73     private static final String TAG = "SafetyCenterBroadcastDi";
74 
75     private final Context mContext;
76     private final SafetyCenterConfigReader mSafetyCenterConfigReader;
77     private final SafetyCenterRefreshTracker mSafetyCenterRefreshTracker;
78     private final SafetyCenterDataManager mSafetyCenterDataManager;
79 
SafetyCenterBroadcastDispatcher( Context context, SafetyCenterConfigReader safetyCenterConfigReader, SafetyCenterRefreshTracker safetyCenterRefreshTracker, SafetyCenterDataManager safetyCenterDataManager)80     SafetyCenterBroadcastDispatcher(
81             Context context,
82             SafetyCenterConfigReader safetyCenterConfigReader,
83             SafetyCenterRefreshTracker safetyCenterRefreshTracker,
84             SafetyCenterDataManager safetyCenterDataManager) {
85         mContext = context;
86         mSafetyCenterConfigReader = safetyCenterConfigReader;
87         mSafetyCenterRefreshTracker = safetyCenterRefreshTracker;
88         mSafetyCenterDataManager = safetyCenterDataManager;
89     }
90 
91     /**
92      * Triggers a refresh of safety sources by sending them broadcasts with action {@link
93      * SafetyCenterManager#ACTION_REFRESH_SAFETY_SOURCES}, and returns the associated broadcast id.
94      *
95      * <p>Returns {@code null} if no broadcast was sent.
96      *
97      * @param safetySourceIds list of IDs to specify the safety sources to be refreshed or a {@code
98      *     null} value to refresh all safety sources.
99      */
100     @Nullable
sendRefreshSafetySources( @efreshReason int refreshReason, UserProfileGroup userProfileGroup, @Nullable List<String> safetySourceIds)101     String sendRefreshSafetySources(
102             @RefreshReason int refreshReason,
103             UserProfileGroup userProfileGroup,
104             @Nullable List<String> safetySourceIds) {
105         List<Broadcast> broadcasts = mSafetyCenterConfigReader.getBroadcasts();
106         BroadcastOptions broadcastOptions = createBroadcastOptions();
107 
108         String broadcastId =
109                 mSafetyCenterRefreshTracker.reportRefreshInProgress(
110                         refreshReason, userProfileGroup);
111         boolean hasSentAtLeastOneBroadcast = false;
112 
113         for (int i = 0; i < broadcasts.size(); i++) {
114             Broadcast broadcast = broadcasts.get(i);
115 
116             hasSentAtLeastOneBroadcast |=
117                     sendRefreshSafetySourcesBroadcast(
118                             broadcast,
119                             broadcastOptions,
120                             refreshReason,
121                             userProfileGroup,
122                             broadcastId,
123                             safetySourceIds);
124         }
125 
126         if (!hasSentAtLeastOneBroadcast) {
127             mSafetyCenterRefreshTracker.clearRefresh(broadcastId);
128             return null;
129         }
130 
131         return broadcastId;
132     }
133 
sendRefreshSafetySourcesBroadcast( Broadcast broadcast, BroadcastOptions broadcastOptions, @RefreshReason int refreshReason, UserProfileGroup userProfileGroup, String broadcastId, @Nullable List<String> requiredSourceIds)134     private boolean sendRefreshSafetySourcesBroadcast(
135             Broadcast broadcast,
136             BroadcastOptions broadcastOptions,
137             @RefreshReason int refreshReason,
138             UserProfileGroup userProfileGroup,
139             String broadcastId,
140             @Nullable List<String> requiredSourceIds) {
141         boolean hasSentAtLeastOneBroadcast = false;
142         int requestType = RefreshReasons.toRefreshRequestType(refreshReason);
143         String packageName = broadcast.getPackageName();
144         Set<String> deniedSourceIds = getRefreshDeniedSourceIds(refreshReason);
145         SparseArray<List<String>> userIdsToSourceIds =
146                 getUserIdsToSourceIds(broadcast, userProfileGroup, refreshReason);
147 
148         for (int i = 0; i < userIdsToSourceIds.size(); i++) {
149             int userId = userIdsToSourceIds.keyAt(i);
150             List<String> sourceIds = userIdsToSourceIds.valueAt(i);
151 
152             if (!deniedSourceIds.isEmpty()) {
153                 sourceIds = new ArrayList<>(sourceIds);
154                 sourceIds.removeAll(deniedSourceIds);
155             }
156 
157             if (requiredSourceIds != null) {
158                 sourceIds = new ArrayList<>(sourceIds);
159                 sourceIds.retainAll(requiredSourceIds);
160             }
161 
162             if (sourceIds.isEmpty()) {
163                 continue;
164             }
165 
166             Intent intent = createRefreshIntent(requestType, packageName, sourceIds, broadcastId);
167             boolean broadcastWasSent =
168                     sendBroadcastIfResolves(intent, UserHandle.of(userId), broadcastOptions);
169             if (broadcastWasSent) {
170                 mSafetyCenterRefreshTracker.reportSourceRefreshesInFlight(
171                         broadcastId, sourceIds, userId);
172             }
173             hasSentAtLeastOneBroadcast |= broadcastWasSent;
174         }
175 
176         return hasSentAtLeastOneBroadcast;
177     }
178 
179     /**
180      * Triggers an {@link SafetyCenterManager#ACTION_SAFETY_CENTER_ENABLED_CHANGED} broadcast for
181      * all safety sources.
182      *
183      * <p>This method also sends an implicit broadcast globally (which requires the {@link
184      * android.Manifest.permission#READ_SAFETY_CENTER_STATUS} permission).
185      */
186     // TODO(b/227310195): Consider adding a boolean extra to the intent instead of having clients
187     //  rely on SafetyCenterManager#isSafetyCenterEnabled()?
sendEnabledChanged()188     void sendEnabledChanged() {
189         List<Broadcast> broadcasts = mSafetyCenterConfigReader.getBroadcasts();
190         BroadcastOptions broadcastOptions = createBroadcastOptions();
191         List<UserProfileGroup> userProfileGroups =
192                 UserProfileGroup.getAllUserProfileGroups(mContext);
193 
194         for (int i = 0; i < broadcasts.size(); i++) {
195             Broadcast broadcast = broadcasts.get(i);
196 
197             sendEnabledChangedBroadcast(broadcast, broadcastOptions, userProfileGroups);
198         }
199 
200         Intent implicitIntent = createImplicitEnabledChangedIntent();
201         sendBroadcast(implicitIntent, UserHandle.SYSTEM, READ_SAFETY_CENTER_STATUS, null);
202     }
203 
sendEnabledChangedBroadcast( Broadcast broadcast, BroadcastOptions broadcastOptions, List<UserProfileGroup> userProfileGroups)204     private void sendEnabledChangedBroadcast(
205             Broadcast broadcast,
206             BroadcastOptions broadcastOptions,
207             List<UserProfileGroup> userProfileGroups) {
208         Intent intent = createExplicitEnabledChangedIntent(broadcast.getPackageName());
209         // The same ENABLED reason is used here for both enable and disable events. It is not sent
210         // externally and is only used internally to filter safety sources in the methods of the
211         // Broadcast class.
212         int refreshReason = REFRESH_REASON_SAFETY_CENTER_ENABLED;
213 
214         for (int i = 0; i < userProfileGroups.size(); i++) {
215             UserProfileGroup userProfileGroup = userProfileGroups.get(i);
216             SparseArray<List<String>> userIdsToSourceIds =
217                     getUserIdsToSourceIds(broadcast, userProfileGroup, refreshReason);
218 
219             for (int j = 0; j < userIdsToSourceIds.size(); j++) {
220                 int userId = userIdsToSourceIds.keyAt(j);
221 
222                 sendBroadcastIfResolves(intent, UserHandle.of(userId), broadcastOptions);
223             }
224         }
225     }
226 
sendBroadcastIfResolves( Intent intent, UserHandle userHandle, @Nullable BroadcastOptions broadcastOptions)227     private boolean sendBroadcastIfResolves(
228             Intent intent, UserHandle userHandle, @Nullable BroadcastOptions broadcastOptions) {
229         if (!doesBroadcastResolve(intent, userHandle)) {
230             Log.w(
231                     TAG,
232                     "No receiver for intent targeting "
233                             + intent.getPackage()
234                             + " and user "
235                             + userHandle);
236             return false;
237         }
238         Log.v(
239                 TAG,
240                 "Found receiver for intent targeting "
241                         + intent.getPackage()
242                         + " and user "
243                         + userHandle);
244         sendBroadcast(intent, userHandle, SEND_SAFETY_CENTER_UPDATE, broadcastOptions);
245         return true;
246     }
247 
248     // TODO(b/193460475): Remove when tooling supports SystemApi to public API.
249     @SuppressLint("NewApi")
sendBroadcast( Intent intent, UserHandle userHandle, String permission, @Nullable BroadcastOptions broadcastOptions)250     private void sendBroadcast(
251             Intent intent,
252             UserHandle userHandle,
253             String permission,
254             @Nullable BroadcastOptions broadcastOptions) {
255         // This call requires the INTERACT_ACROSS_USERS permission.
256         final long callingId = Binder.clearCallingIdentity();
257         try {
258             mContext.sendBroadcastAsUser(
259                     intent,
260                     userHandle,
261                     permission,
262                     broadcastOptions == null ? null : broadcastOptions.toBundle());
263         } finally {
264             Binder.restoreCallingIdentity(callingId);
265         }
266     }
267 
doesBroadcastResolve(Intent broadcastIntent, UserHandle userHandle)268     private boolean doesBroadcastResolve(Intent broadcastIntent, UserHandle userHandle) {
269         return !PackageUtils.queryUnfilteredBroadcastReceiversAsUser(
270                         broadcastIntent, 0, userHandle.getIdentifier(), mContext)
271                 .isEmpty();
272     }
273 
createExplicitEnabledChangedIntent(String packageName)274     private static Intent createExplicitEnabledChangedIntent(String packageName) {
275         return createImplicitEnabledChangedIntent().setPackage(packageName);
276     }
277 
createImplicitEnabledChangedIntent()278     private static Intent createImplicitEnabledChangedIntent() {
279         return createBroadcastIntent(ACTION_SAFETY_CENTER_ENABLED_CHANGED);
280     }
281 
createRefreshIntent( @efreshRequestType int requestType, String packageName, List<String> sourceIdsToRefresh, String broadcastId)282     private static Intent createRefreshIntent(
283             @RefreshRequestType int requestType,
284             String packageName,
285             List<String> sourceIdsToRefresh,
286             String broadcastId) {
287         String[] sourceIdsArray = sourceIdsToRefresh.toArray(new String[0]);
288         return createBroadcastIntent(ACTION_REFRESH_SAFETY_SOURCES)
289                 .putExtra(EXTRA_REFRESH_SAFETY_SOURCES_REQUEST_TYPE, requestType)
290                 .putExtra(EXTRA_REFRESH_SAFETY_SOURCE_IDS, sourceIdsArray)
291                 .putExtra(EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID, broadcastId)
292                 .setPackage(packageName);
293     }
294 
createBroadcastIntent(String intentAction)295     private static Intent createBroadcastIntent(String intentAction) {
296         return new Intent(intentAction).setFlags(FLAG_RECEIVER_FOREGROUND);
297     }
298 
299     // TODO(b/193460475): Remove when tooling supports SystemApi to public API.
300     @SuppressLint("NewApi")
createBroadcastOptions()301     private static BroadcastOptions createBroadcastOptions() {
302         BroadcastOptions broadcastOptions = BroadcastOptions.makeBasic();
303         Duration allowListDuration = SafetyCenterFlags.getFgsAllowlistDuration();
304         // This call requires the START_FOREGROUND_SERVICES_FROM_BACKGROUND permission.
305         final long callingId = Binder.clearCallingIdentity();
306         try {
307             broadcastOptions.setTemporaryAppAllowlist(
308                     allowListDuration.toMillis(),
309                     TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED,
310                     REASON_REFRESH_SAFETY_SOURCES,
311                     "Safety Center is requesting data from safety sources");
312         } finally {
313             Binder.restoreCallingIdentity(callingId);
314         }
315         return broadcastOptions;
316     }
317 
318     /** Returns the list of source IDs for which refreshing is denied for the given reason. */
getRefreshDeniedSourceIds(@efreshReason int refreshReason)319     private static Set<String> getRefreshDeniedSourceIds(@RefreshReason int refreshReason) {
320         if (RefreshReasons.isBackgroundRefresh(refreshReason)) {
321             return SafetyCenterFlags.getBackgroundRefreshDeniedSourceIds();
322         } else {
323             return Collections.emptySet();
324         }
325     }
326 
327     /**
328      * Returns a flattened mapping from user IDs to lists of source IDs for those users. The map is
329      * in the form of a {@link SparseArray} where the int keys are user IDs and the values are the
330      * lists of source IDs.
331      *
332      * <p>The set of user IDs (keys) is the profile parent user ID of {@code userProfileGroup} plus
333      * the (possibly empty) set of running managed profile user IDs in that group.
334      *
335      * <p>Every value present is a non-empty list, but the overall result may be empty.
336      */
getUserIdsToSourceIds( Broadcast broadcast, UserProfileGroup userProfileGroup, @RefreshReason int refreshReason)337     private SparseArray<List<String>> getUserIdsToSourceIds(
338             Broadcast broadcast,
339             UserProfileGroup userProfileGroup,
340             @RefreshReason int refreshReason) {
341         int[] managedProfileIds = userProfileGroup.getManagedRunningProfilesUserIds();
342         SparseArray<List<String>> result = new SparseArray<>(managedProfileIds.length + 1);
343         List<String> profileParentSources =
344                 getSourceIdsForRefreshReason(
345                         refreshReason,
346                         broadcast.getSourceIdsForProfileParent(),
347                         broadcast.getSourceIdsForProfileParentOnPageOpen(),
348                         userProfileGroup.getProfileParentUserId());
349 
350         if (!profileParentSources.isEmpty()) {
351             result.put(userProfileGroup.getProfileParentUserId(), profileParentSources);
352         }
353 
354         for (int i = 0; i < managedProfileIds.length; i++) {
355             List<String> managedProfileSources =
356                     getSourceIdsForRefreshReason(
357                             refreshReason,
358                             broadcast.getSourceIdsForManagedProfiles(),
359                             broadcast.getSourceIdsForManagedProfilesOnPageOpen(),
360                             managedProfileIds[i]);
361 
362             if (!managedProfileSources.isEmpty()) {
363                 result.put(managedProfileIds[i], managedProfileSources);
364             }
365         }
366 
367         return result;
368     }
369 
370     /**
371      * Returns the sources to refresh for the given {@code refreshReason}.
372      *
373      * <p>For {@link SafetyCenterManager#REFRESH_REASON_PAGE_OPEN}, returns a copy of {@code
374      * allSourceIds} filtered to contain only sources that have refreshOnPageOpenAllowed in the XML
375      * config, or are in the safety_center_override_refresh_on_page_open_sources flag, or don't have
376      * any {@link SafetySourceData} provided.
377      */
getSourceIdsForRefreshReason( @efreshReason int refreshReason, List<String> allSourceIds, List<String> pageOpenSourceIds, @UserIdInt int userId)378     private List<String> getSourceIdsForRefreshReason(
379             @RefreshReason int refreshReason,
380             List<String> allSourceIds,
381             List<String> pageOpenSourceIds,
382             @UserIdInt int userId) {
383         if (refreshReason != REFRESH_REASON_PAGE_OPEN) {
384             return allSourceIds;
385         }
386 
387         List<String> sourceIds = new ArrayList<>();
388 
389         ArraySet<String> flagAllowListedSourceIds =
390                 SafetyCenterFlags.getOverrideRefreshOnPageOpenSourceIds();
391 
392         for (int i = 0; i < allSourceIds.size(); i++) {
393             String sourceId = allSourceIds.get(i);
394             if (pageOpenSourceIds.contains(sourceId)
395                     || flagAllowListedSourceIds.contains(sourceId)
396                     || mSafetyCenterDataManager.getSafetySourceDataInternal(
397                                     SafetySourceKey.of(sourceId, userId))
398                             == null) {
399                 sourceIds.add(sourceId);
400             }
401         }
402 
403         return unmodifiableList(sourceIds);
404     }
405 }
406