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_INCLUDE_STOPPED_PACKAGES;
22 import static android.content.Intent.FLAG_RECEIVER_FOREGROUND;
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.UserIdInt;
36 import android.app.BroadcastOptions;
37 import android.content.Context;
38 import android.content.Intent;
39 import android.os.Binder;
40 import android.os.UserHandle;
41 import android.safetycenter.SafetyCenterManager;
42 import android.safetycenter.SafetyCenterManager.RefreshReason;
43 import android.safetycenter.SafetySourceData;
44 import android.util.ArraySet;
45 import android.util.Log;
46 import android.util.SparseArray;
47 
48 import androidx.annotation.Nullable;
49 
50 import com.android.permission.util.PackageUtils;
51 import com.android.safetycenter.SafetyCenterConfigReader.Broadcast;
52 import com.android.safetycenter.UserProfileGroup.ProfileType;
53 import com.android.safetycenter.data.SafetyCenterDataManager;
54 
55 import java.time.Duration;
56 import java.util.ArrayList;
57 import java.util.Collections;
58 import java.util.List;
59 import java.util.Set;
60 
61 import javax.annotation.concurrent.NotThreadSafe;
62 
63 /**
64  * A class that dispatches SafetyCenter broadcasts.
65  *
66  * <p>This class isn't thread safe. Thread safety must be handled by the caller.
67  */
68 @NotThreadSafe
69 final class SafetyCenterBroadcastDispatcher {
70     private static final String TAG = "SafetyCenterBroadcastDi";
71 
72     private final Context mContext;
73     private final SafetyCenterConfigReader mSafetyCenterConfigReader;
74     private final SafetyCenterRefreshTracker mSafetyCenterRefreshTracker;
75     private final SafetyCenterDataManager mSafetyCenterDataManager;
76 
SafetyCenterBroadcastDispatcher( Context context, SafetyCenterConfigReader safetyCenterConfigReader, SafetyCenterRefreshTracker safetyCenterRefreshTracker, SafetyCenterDataManager safetyCenterDataManager)77     SafetyCenterBroadcastDispatcher(
78             Context context,
79             SafetyCenterConfigReader safetyCenterConfigReader,
80             SafetyCenterRefreshTracker safetyCenterRefreshTracker,
81             SafetyCenterDataManager safetyCenterDataManager) {
82         mContext = context;
83         mSafetyCenterConfigReader = safetyCenterConfigReader;
84         mSafetyCenterRefreshTracker = safetyCenterRefreshTracker;
85         mSafetyCenterDataManager = safetyCenterDataManager;
86     }
87 
88     /**
89      * Triggers a refresh of safety sources by sending them broadcasts with action {@link
90      * SafetyCenterManager#ACTION_REFRESH_SAFETY_SOURCES}, and returns the associated broadcast id.
91      *
92      * <p>Returns {@code null} if no broadcast was sent.
93      *
94      * @param safetySourceIds list of IDs to specify the safety sources to be refreshed or a {@code
95      *     null} value to refresh all safety sources.
96      */
97     @Nullable
sendRefreshSafetySources( @efreshReason int refreshReason, UserProfileGroup userProfileGroup, @Nullable List<String> safetySourceIds)98     String sendRefreshSafetySources(
99             @RefreshReason int refreshReason,
100             UserProfileGroup userProfileGroup,
101             @Nullable List<String> safetySourceIds) {
102         List<Broadcast> broadcasts = mSafetyCenterConfigReader.getBroadcasts();
103         BroadcastOptions broadcastOptions = createBroadcastOptions();
104 
105         String broadcastId =
106                 mSafetyCenterRefreshTracker.reportRefreshInProgress(
107                         refreshReason, userProfileGroup);
108         boolean hasSentAtLeastOneBroadcast = false;
109 
110         for (int i = 0; i < broadcasts.size(); i++) {
111             Broadcast broadcast = broadcasts.get(i);
112 
113             hasSentAtLeastOneBroadcast |=
114                     sendRefreshSafetySourcesBroadcast(
115                             broadcast,
116                             broadcastOptions,
117                             refreshReason,
118                             userProfileGroup,
119                             broadcastId,
120                             safetySourceIds);
121         }
122 
123         if (!hasSentAtLeastOneBroadcast) {
124             mSafetyCenterRefreshTracker.clearRefresh(broadcastId);
125             return null;
126         }
127 
128         return broadcastId;
129     }
130 
sendRefreshSafetySourcesBroadcast( Broadcast broadcast, BroadcastOptions broadcastOptions, @RefreshReason int refreshReason, UserProfileGroup userProfileGroup, String broadcastId, @Nullable List<String> requiredSourceIds)131     private boolean sendRefreshSafetySourcesBroadcast(
132             Broadcast broadcast,
133             BroadcastOptions broadcastOptions,
134             @RefreshReason int refreshReason,
135             UserProfileGroup userProfileGroup,
136             String broadcastId,
137             @Nullable List<String> requiredSourceIds) {
138         boolean hasSentAtLeastOneBroadcast = false;
139         String packageName = broadcast.getPackageName();
140         Set<String> deniedSourceIds = getRefreshDeniedSourceIds(refreshReason);
141         SparseArray<List<String>> userIdsToSourceIds =
142                 getUserIdsToSourceIds(broadcast, userProfileGroup, refreshReason);
143 
144         for (int i = 0; i < userIdsToSourceIds.size(); i++) {
145             int userId = userIdsToSourceIds.keyAt(i);
146             List<String> sourceIds = userIdsToSourceIds.valueAt(i);
147 
148             if (!deniedSourceIds.isEmpty()) {
149                 sourceIds = new ArrayList<>(sourceIds);
150                 sourceIds.removeAll(deniedSourceIds);
151             }
152 
153             if (requiredSourceIds != null) {
154                 sourceIds = new ArrayList<>(sourceIds);
155                 sourceIds.retainAll(requiredSourceIds);
156             }
157 
158             if (sourceIds.isEmpty()) {
159                 continue;
160             }
161 
162             Intent intent = createRefreshIntent(refreshReason, packageName, sourceIds, broadcastId);
163             boolean broadcastWasSent =
164                     sendBroadcastIfResolves(intent, UserHandle.of(userId), broadcastOptions);
165             if (broadcastWasSent) {
166                 mSafetyCenterRefreshTracker.reportSourceRefreshesInFlight(
167                         broadcastId, sourceIds, userId);
168             }
169             hasSentAtLeastOneBroadcast |= broadcastWasSent;
170         }
171 
172         return hasSentAtLeastOneBroadcast;
173     }
174 
175     /**
176      * Triggers an {@link SafetyCenterManager#ACTION_SAFETY_CENTER_ENABLED_CHANGED} broadcast for
177      * all safety sources.
178      *
179      * <p>This method also sends an implicit broadcast globally (which requires the {@link
180      * android.Manifest.permission#READ_SAFETY_CENTER_STATUS} permission).
181      */
182     // TODO(b/227310195): Consider adding a boolean extra to the intent instead of having clients
183     //  rely on SafetyCenterManager#isSafetyCenterEnabled()?
sendEnabledChanged()184     void sendEnabledChanged() {
185         List<Broadcast> broadcasts = mSafetyCenterConfigReader.getBroadcasts();
186         BroadcastOptions broadcastOptions = createBroadcastOptions();
187         List<UserProfileGroup> userProfileGroups =
188                 UserProfileGroup.getAllUserProfileGroups(mContext);
189 
190         for (int i = 0; i < broadcasts.size(); i++) {
191             Broadcast broadcast = broadcasts.get(i);
192 
193             sendEnabledChangedBroadcast(broadcast, broadcastOptions, userProfileGroups);
194         }
195 
196         Intent implicitIntent = createImplicitEnabledChangedIntent();
197         sendBroadcast(
198                 implicitIntent,
199                 UserHandle.SYSTEM,
200                 READ_SAFETY_CENTER_STATUS,
201                 /* broadcastOptions= */ 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 
210         for (int i = 0; i < userProfileGroups.size(); i++) {
211             UserProfileGroup userProfileGroup = userProfileGroups.get(i);
212             SparseArray<List<String>> userIdsToSourceIds =
213                     getUserIdsToSourceIds(
214                             broadcast,
215                             userProfileGroup,
216                             // The same ENABLED reason is used here for both enable and disable
217                             // events. It is not sent externally and is only used internally to
218                             // filter safety sources in the methods of the Broadcast class.
219                             REFRESH_REASON_SAFETY_CENTER_ENABLED);
220 
221             for (int j = 0; j < userIdsToSourceIds.size(); j++) {
222                 int userId = userIdsToSourceIds.keyAt(j);
223 
224                 sendBroadcastIfResolves(intent, UserHandle.of(userId), broadcastOptions);
225             }
226         }
227     }
228 
sendBroadcastIfResolves( Intent intent, UserHandle userHandle, @Nullable BroadcastOptions broadcastOptions)229     private boolean sendBroadcastIfResolves(
230             Intent intent, UserHandle userHandle, @Nullable BroadcastOptions broadcastOptions) {
231         if (!doesBroadcastResolve(intent, userHandle)) {
232             Log.w(
233                     TAG,
234                     "No receiver for intent targeting: "
235                             + intent.getPackage()
236                             + ", and user id: "
237                             + userHandle.getIdentifier());
238             return false;
239         }
240         Log.v(
241                 TAG,
242                 "Found receiver for intent targeting: "
243                         + intent.getPackage()
244                         + ", and user id: "
245                         + userHandle.getIdentifier());
246         sendBroadcast(intent, userHandle, SEND_SAFETY_CENTER_UPDATE, broadcastOptions);
247         return true;
248     }
249 
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, /* flags= */ 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( @efreshReason int refreshReason, String packageName, List<String> sourceIdsToRefresh, String broadcastId)282     private static Intent createRefreshIntent(
283             @RefreshReason int refreshReason,
284             String packageName,
285             List<String> sourceIdsToRefresh,
286             String broadcastId) {
287         String[] sourceIdsArray = sourceIdsToRefresh.toArray(new String[0]);
288         int requestType = RefreshReasons.toRefreshRequestType(refreshReason);
289         Intent refreshIntent =
290                 createBroadcastIntent(ACTION_REFRESH_SAFETY_SOURCES)
291                         .putExtra(EXTRA_REFRESH_SAFETY_SOURCES_REQUEST_TYPE, requestType)
292                         .putExtra(EXTRA_REFRESH_SAFETY_SOURCE_IDS, sourceIdsArray)
293                         .putExtra(EXTRA_REFRESH_SAFETY_SOURCES_BROADCAST_ID, broadcastId)
294                         .setPackage(packageName);
295         boolean isUserInitiated = !RefreshReasons.isBackgroundRefresh(refreshReason);
296         if (isUserInitiated) {
297             return refreshIntent.addFlags(FLAG_INCLUDE_STOPPED_PACKAGES);
298         }
299         return refreshIntent;
300     }
301 
createBroadcastIntent(String intentAction)302     private static Intent createBroadcastIntent(String intentAction) {
303         return new Intent(intentAction).addFlags(FLAG_RECEIVER_FOREGROUND);
304     }
305 
createBroadcastOptions()306     private static BroadcastOptions createBroadcastOptions() {
307         BroadcastOptions broadcastOptions = BroadcastOptions.makeBasic();
308         Duration allowListDuration = SafetyCenterFlags.getFgsAllowlistDuration();
309         // This call requires the START_FOREGROUND_SERVICES_FROM_BACKGROUND permission.
310         final long callingId = Binder.clearCallingIdentity();
311         try {
312             broadcastOptions.setTemporaryAppAllowlist(
313                     allowListDuration.toMillis(),
314                     TEMPORARY_ALLOW_LIST_TYPE_FOREGROUND_SERVICE_ALLOWED,
315                     REASON_REFRESH_SAFETY_SOURCES,
316                     "Safety Center is requesting data from safety sources");
317         } finally {
318             Binder.restoreCallingIdentity(callingId);
319         }
320         return broadcastOptions;
321     }
322 
323     /** Returns the list of source IDs for which refreshing is denied for the given reason. */
getRefreshDeniedSourceIds(@efreshReason int refreshReason)324     private static Set<String> getRefreshDeniedSourceIds(@RefreshReason int refreshReason) {
325         if (RefreshReasons.isBackgroundRefresh(refreshReason)) {
326             return SafetyCenterFlags.getBackgroundRefreshDeniedSourceIds();
327         } else {
328             return Collections.emptySet();
329         }
330     }
331 
332     /**
333      * Returns a flattened mapping from user IDs to lists of source IDs for those users. The map is
334      * in the form of a {@link SparseArray} where the int keys are user IDs and the values are the
335      * lists of source IDs.
336      *
337      * <p>The set of user IDs (keys) is the profile parent user ID of {@code userProfileGroup} plus
338      * all the other types of running profiles:
339      * <ol>
340      *     <li>The (possibly empty) set of running managed profile user IDs in that group.
341      *     <li>The (possibly empty) set of running private profile user ID in that group.
342      * </ol>
343      * <p>Every value present is a non-empty list, but the overall result may be empty.
344      */
getUserIdsToSourceIds( Broadcast broadcast, UserProfileGroup userProfileGroup, @RefreshReason int refreshReason)345     private SparseArray<List<String>> getUserIdsToSourceIds(
346             Broadcast broadcast,
347             UserProfileGroup userProfileGroup,
348             @RefreshReason int refreshReason) {
349         SparseArray<List<String>> result =
350                 new SparseArray<>(userProfileGroup.getNumRunningProfiles());
351         for (int profilTypeIdx = 0;
352                 profilTypeIdx < ProfileType.ALL_PROFILE_TYPES.length;
353                 ++profilTypeIdx) {
354             @ProfileType int profileType = ProfileType.ALL_PROFILE_TYPES[profilTypeIdx];
355             int[] runningProfiles = userProfileGroup.getRunningProfilesOfType(profileType);
356             for (int profileIdx = 0; profileIdx < runningProfiles.length; ++profileIdx) {
357                 List<String> profileSources =
358                         getSourceIdsForRefreshReason(
359                                 refreshReason,
360                                 broadcast.getSourceIdsForProfileType(profileType),
361                                 broadcast.getSourceIdsOnPageOpenForProfileType(profileType),
362                                 runningProfiles[profileIdx]);
363 
364                 if (!profileSources.isEmpty()) {
365                     result.put(runningProfiles[profileIdx], profileSources);
366                 }
367             }
368         }
369 
370         return result;
371     }
372 
373     /**
374      * Returns the sources to refresh for the given {@code refreshReason}.
375      *
376      * <p>For {@link SafetyCenterManager#REFRESH_REASON_PAGE_OPEN}, returns a copy of {@code
377      * allSourceIds} filtered to contain only sources that have refreshOnPageOpenAllowed in the XML
378      * config, or are in the safety_center_override_refresh_on_page_open_sources flag, or don't have
379      * any {@link SafetySourceData} provided.
380      */
getSourceIdsForRefreshReason( @efreshReason int refreshReason, List<String> allSourceIds, List<String> pageOpenSourceIds, @UserIdInt int userId)381     private List<String> getSourceIdsForRefreshReason(
382             @RefreshReason int refreshReason,
383             List<String> allSourceIds,
384             List<String> pageOpenSourceIds,
385             @UserIdInt int userId) {
386         if (refreshReason != REFRESH_REASON_PAGE_OPEN) {
387             return allSourceIds;
388         }
389 
390         List<String> sourceIds = new ArrayList<>();
391 
392         ArraySet<String> flagAllowListedSourceIds =
393                 SafetyCenterFlags.getOverrideRefreshOnPageOpenSourceIds();
394 
395         for (int i = 0; i < allSourceIds.size(); i++) {
396             String sourceId = allSourceIds.get(i);
397             if (pageOpenSourceIds.contains(sourceId)
398                     || flagAllowListedSourceIds.contains(sourceId)
399                     || mSafetyCenterDataManager.getSafetySourceDataInternal(
400                                     SafetySourceKey.of(sourceId, userId))
401                             == null) {
402                 sourceIds.add(sourceId);
403             }
404         }
405 
406         return unmodifiableList(sourceIds);
407     }
408 }
409