• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 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.notifications;
18 
19 import static android.os.Build.VERSION_CODES.TIRAMISU;
20 import static android.safetycenter.SafetyEvent.SAFETY_EVENT_TYPE_RESOLVING_ACTION_SUCCEEDED;
21 
22 import static com.android.safetycenter.internaldata.SafetyCenterIds.toUserFriendlyString;
23 
24 import android.annotation.IntDef;
25 import android.annotation.Nullable;
26 import android.annotation.UserIdInt;
27 import android.app.Notification;
28 import android.app.NotificationManager;
29 import android.content.Context;
30 import android.os.Binder;
31 import android.os.UserHandle;
32 import android.safetycenter.SafetyEvent;
33 import android.safetycenter.SafetySourceIssue;
34 import android.safetycenter.config.SafetySource;
35 import android.util.ArrayMap;
36 import android.util.ArraySet;
37 import android.util.Log;
38 
39 import androidx.annotation.RequiresApi;
40 
41 import com.android.modules.utils.build.SdkLevel;
42 import com.android.permission.util.UserUtils;
43 import com.android.safetycenter.SafetyCenterFlags;
44 import com.android.safetycenter.SafetySourceIssueInfo;
45 import com.android.safetycenter.UserProfileGroup;
46 import com.android.safetycenter.data.SafetyCenterDataManager;
47 import com.android.safetycenter.internaldata.SafetyCenterIds;
48 import com.android.safetycenter.internaldata.SafetyCenterIssueKey;
49 import com.android.safetycenter.logging.SafetyCenterStatsdLogger;
50 import com.android.safetycenter.resources.SafetyCenterResourcesContext;
51 
52 import java.io.PrintWriter;
53 import java.lang.annotation.Retention;
54 import java.lang.annotation.RetentionPolicy;
55 import java.time.Duration;
56 import java.time.Instant;
57 import java.util.List;
58 
59 import javax.annotation.concurrent.NotThreadSafe;
60 
61 /**
62  * Class responsible for posting, updating and dismissing Safety Center notifications each time
63  * Safety Center's issues change.
64  *
65  * <p>This class isn't thread safe. Thread safety must be handled by the caller.
66  *
67  * @hide
68  */
69 @RequiresApi(TIRAMISU)
70 @NotThreadSafe
71 public final class SafetyCenterNotificationSender {
72 
73     private static final String TAG = "SafetyCenterNS";
74 
75     // We use a fixed notification ID because notifications are keyed by (tag, id) and it easier
76     // to differentiate our notifications using the tag
77     private static final int FIXED_NOTIFICATION_ID = 2345;
78 
79     private static final int NOTIFICATION_BEHAVIOR_INTERNAL_NEVER = 100;
80     private static final int NOTIFICATION_BEHAVIOR_INTERNAL_DELAYED = 200;
81     private static final int NOTIFICATION_BEHAVIOR_INTERNAL_IMMEDIATELY = 300;
82 
83     /**
84      * Internal notification behavior {@code @IntDef} which is related to the {@code
85      * SafetySourceIssue.NotificationBehavior} type introduced in Android U.
86      *
87      * <p>This definition is available on T+.
88      *
89      * <p>Unlike the U+/external {@code @IntDef}, this one has no "unspecified behavior" value. Any
90      * issues which have unspecified behavior are resolved to one of these specific behaviors based
91      * on their other properties.
92      */
93     @IntDef(
94             prefix = {"NOTIFICATION_BEHAVIOR_INTERNAL"},
95             value = {
96                 NOTIFICATION_BEHAVIOR_INTERNAL_NEVER,
97                 NOTIFICATION_BEHAVIOR_INTERNAL_DELAYED,
98                 NOTIFICATION_BEHAVIOR_INTERNAL_IMMEDIATELY
99             })
100     @Retention(RetentionPolicy.SOURCE)
101     private @interface NotificationBehaviorInternal {}
102 
103     private final Context mContext;
104 
105     private final SafetyCenterNotificationFactory mNotificationFactory;
106 
107     private final SafetyCenterDataManager mSafetyCenterDataManager;
108 
109     private final ArrayMap<SafetyCenterIssueKey, SafetySourceIssue> mNotifiedIssues =
110             new ArrayMap<>();
111 
SafetyCenterNotificationSender( Context context, SafetyCenterNotificationFactory notificationFactory, SafetyCenterDataManager safetyCenterDataManager)112     private SafetyCenterNotificationSender(
113             Context context,
114             SafetyCenterNotificationFactory notificationFactory,
115             SafetyCenterDataManager safetyCenterDataManager) {
116         mContext = context;
117         mNotificationFactory = notificationFactory;
118         mSafetyCenterDataManager = safetyCenterDataManager;
119     }
120 
newInstance( Context context, SafetyCenterResourcesContext resourcesContext, SafetyCenterNotificationChannels notificationChannels, SafetyCenterDataManager dataManager)121     public static SafetyCenterNotificationSender newInstance(
122             Context context,
123             SafetyCenterResourcesContext resourcesContext,
124             SafetyCenterNotificationChannels notificationChannels,
125             SafetyCenterDataManager dataManager) {
126         return new SafetyCenterNotificationSender(
127                 context,
128                 new SafetyCenterNotificationFactory(
129                         context, notificationChannels, resourcesContext),
130                 dataManager);
131     }
132 
133     /**
134      * Replaces an issue's notification with one displaying the success message of the {@link
135      * SafetySourceIssue.Action} that resolved that issue.
136      *
137      * <p>The given {@link SafetyEvent} have type {@link
138      * SafetyEvent#SAFETY_EVENT_TYPE_RESOLVING_ACTION_SUCCEEDED} and include issue and action IDs
139      * that correspond to a {@link SafetySourceIssue} for which a notification is currently
140      * displayed. Otherwise this method has no effect.
141      *
142      * @param sourceId of the source which reported the issue
143      * @param safetyEvent the source provided upon successful action resolution
144      * @param userId to which the source, issue and notification belong
145      */
notifyActionSuccess( String sourceId, SafetyEvent safetyEvent, @UserIdInt int userId)146     public void notifyActionSuccess(
147             String sourceId, SafetyEvent safetyEvent, @UserIdInt int userId) {
148         if (safetyEvent.getType() != SAFETY_EVENT_TYPE_RESOLVING_ACTION_SUCCEEDED) {
149             Log.w(TAG, "Received safety event of wrong type");
150             return;
151         }
152 
153         String sourceIssueId = safetyEvent.getSafetySourceIssueId();
154         if (sourceIssueId == null) {
155             Log.w(TAG, "Received safety event without a safety source issue id");
156             return;
157         }
158 
159         String sourceIssueActionId = safetyEvent.getSafetySourceIssueActionId();
160         if (sourceIssueActionId == null) {
161             Log.w(TAG, "Received safety event without a safety source issue action id");
162             return;
163         }
164 
165         SafetyCenterIssueKey issueKey =
166                 SafetyCenterIssueKey.newBuilder()
167                         .setSafetySourceId(sourceId)
168                         .setSafetySourceIssueId(sourceIssueId)
169                         .setUserId(userId)
170                         .build();
171         SafetySourceIssue notifiedIssue = mNotifiedIssues.get(issueKey);
172         if (notifiedIssue == null) {
173             Log.w(TAG, "No notification for this issue");
174             return;
175         }
176 
177         SafetySourceIssue.Action successfulAction = null;
178         for (int i = 0; i < notifiedIssue.getActions().size(); i++) {
179             if (notifiedIssue.getActions().get(i).getId().equals(sourceIssueActionId)) {
180                 successfulAction = notifiedIssue.getActions().get(i);
181             }
182         }
183         if (successfulAction == null) {
184             Log.w(TAG, "Successful action not found");
185             return;
186         }
187 
188         NotificationManager notificationManager = getNotificationManagerForUser(userId);
189 
190         if (notificationManager == null) {
191             return;
192         }
193 
194         Notification notification =
195                 mNotificationFactory.newNotificationForSuccessfulAction(
196                         notificationManager, notifiedIssue, successfulAction, userId);
197         if (notification == null) {
198             Log.w(TAG, "Could not create successful action notification");
199             return;
200         }
201         String tag = getNotificationTag(issueKey);
202         boolean wasPosted = notifyFromSystem(notificationManager, tag, notification);
203         if (wasPosted) {
204             // If the original issue notification was successfully replaced the key removed from
205             // mNotifiedIssues to prevent the success notification from being removed by
206             // cancelStaleNotifications below.
207             mNotifiedIssues.remove(issueKey);
208         }
209     }
210 
211     /** Updates Safety Center notifications for the given {@link UserProfileGroup}. */
updateNotifications(UserProfileGroup userProfileGroup)212     public void updateNotifications(UserProfileGroup userProfileGroup) {
213         updateNotifications(userProfileGroup.getProfileParentUserId());
214 
215         int[] managedProfileUserIds = userProfileGroup.getManagedProfilesUserIds();
216         for (int i = 0; i < managedProfileUserIds.length; i++) {
217             updateNotifications(managedProfileUserIds[i]);
218         }
219     }
220 
221     /**
222      * Updates Safety Center notifications, usually in response to a change in the issues for the
223      * given userId.
224      */
updateNotifications(@serIdInt int userId)225     public void updateNotifications(@UserIdInt int userId) {
226         if (!SafetyCenterFlags.getNotificationsEnabled()) {
227             return;
228         }
229 
230         NotificationManager notificationManager = getNotificationManagerForUser(userId);
231 
232         if (notificationManager == null) {
233             return;
234         }
235 
236         ArrayMap<SafetyCenterIssueKey, SafetySourceIssue> issuesToNotify =
237                 getIssuesToNotify(userId);
238 
239         // Post or update notifications for notifiable issues. We keep track of the "fresh" issues
240         // keys of those issues which were just notified because doing so allows us to cancel any
241         // notifications for other, non-fresh issues.
242         ArraySet<SafetyCenterIssueKey> freshIssueKeys = new ArraySet<>();
243         for (int i = 0; i < issuesToNotify.size(); i++) {
244             SafetyCenterIssueKey issueKey = issuesToNotify.keyAt(i);
245             SafetySourceIssue issue = issuesToNotify.valueAt(i);
246 
247             boolean unchanged = issue.equals(mNotifiedIssues.get(issueKey));
248             if (unchanged) {
249                 freshIssueKeys.add(issueKey);
250                 continue;
251             }
252 
253             boolean wasPosted = postNotificationForIssue(notificationManager, issue, issueKey);
254             if (wasPosted) {
255                 freshIssueKeys.add(issueKey);
256             }
257         }
258 
259         cancelStaleNotifications(notificationManager, userId, freshIssueKeys);
260     }
261 
262     /** Cancels all notifications previously posted by this class */
cancelAllNotifications()263     public void cancelAllNotifications() {
264         // Loop in reverse index order to be able to remove entries while iterating
265         for (int i = mNotifiedIssues.size() - 1; i >= 0; i--) {
266             SafetyCenterIssueKey issueKey = mNotifiedIssues.keyAt(i);
267             int userId = issueKey.getUserId();
268             NotificationManager notificationManager = getNotificationManagerForUser(userId);
269             if (notificationManager == null) {
270                 continue;
271             }
272             cancelNotificationFromSystem(notificationManager, getNotificationTag(issueKey));
273             mNotifiedIssues.removeAt(i);
274         }
275     }
276 
277     /** Dumps state for debugging purposes. */
dump(PrintWriter fout)278     public void dump(PrintWriter fout) {
279         int notifiedIssuesCount = mNotifiedIssues.size();
280         fout.println("NOTIFICATION SENDER (" + notifiedIssuesCount + " notified issues)");
281         for (int i = 0; i < notifiedIssuesCount; i++) {
282             SafetyCenterIssueKey key = mNotifiedIssues.keyAt(i);
283             SafetySourceIssue issue = mNotifiedIssues.valueAt(i);
284             fout.println("\t[" + i + "] " + toUserFriendlyString(key) + " -> " + issue);
285         }
286         fout.println();
287     }
288 
289     /** Get all of the key-issue pairs for which notifications should be posted or updated now. */
getIssuesToNotify( @serIdInt int userId)290     private ArrayMap<SafetyCenterIssueKey, SafetySourceIssue> getIssuesToNotify(
291             @UserIdInt int userId) {
292         ArrayMap<SafetyCenterIssueKey, SafetySourceIssue> result = new ArrayMap<>();
293         List<SafetySourceIssueInfo> allIssuesInfo =
294                 mSafetyCenterDataManager.getIssuesForUser(userId);
295 
296         for (int i = 0; i < allIssuesInfo.size(); i++) {
297             SafetySourceIssueInfo issueInfo = allIssuesInfo.get(i);
298             SafetyCenterIssueKey issueKey = issueInfo.getSafetyCenterIssueKey();
299             SafetySourceIssue issue = issueInfo.getSafetySourceIssue();
300 
301             if (!areNotificationsAllowedForSource(issueInfo.getSafetySource())) {
302                 continue;
303             }
304 
305             if (mSafetyCenterDataManager.isNotificationDismissedNow(
306                     issueKey, issue.getSeverityLevel())) {
307                 continue;
308             }
309 
310             // Get the notification behavior for this issue which determines whether we should
311             // send a notification about it now
312             int behavior = getBehavior(issue, issueKey);
313             if (behavior == NOTIFICATION_BEHAVIOR_INTERNAL_IMMEDIATELY) {
314                 result.put(issueKey, issue);
315             } else if (behavior == NOTIFICATION_BEHAVIOR_INTERNAL_DELAYED) {
316                 if (canNotifyDelayedIssueNow(issueKey)) {
317                     result.put(issueKey, issue);
318                 }
319                 // TODO(b/259094736): else handle delayed notifications using a scheduled job
320             }
321         }
322         return result;
323     }
324 
325     @NotificationBehaviorInternal
getBehavior(SafetySourceIssue issue, SafetyCenterIssueKey issueKey)326     private int getBehavior(SafetySourceIssue issue, SafetyCenterIssueKey issueKey) {
327         if (SdkLevel.isAtLeastU()) {
328             switch (issue.getNotificationBehavior()) {
329                 case SafetySourceIssue.NOTIFICATION_BEHAVIOR_NEVER:
330                     return NOTIFICATION_BEHAVIOR_INTERNAL_NEVER;
331                 case SafetySourceIssue.NOTIFICATION_BEHAVIOR_DELAYED:
332                     return NOTIFICATION_BEHAVIOR_INTERNAL_DELAYED;
333                 case SafetySourceIssue.NOTIFICATION_BEHAVIOR_IMMEDIATELY:
334                     return NOTIFICATION_BEHAVIOR_INTERNAL_IMMEDIATELY;
335             }
336         }
337         // On Android T all issues are assumed to have "unspecified" behavior
338         return getBehaviorForIssueWithUnspecifiedBehavior(issue, issueKey);
339     }
340 
341     @NotificationBehaviorInternal
getBehaviorForIssueWithUnspecifiedBehavior( SafetySourceIssue issue, SafetyCenterIssueKey issueKey)342     private int getBehaviorForIssueWithUnspecifiedBehavior(
343             SafetySourceIssue issue, SafetyCenterIssueKey issueKey) {
344         String flagKey = issueKey.getSafetySourceId() + "/" + issue.getIssueTypeId();
345         if (SafetyCenterFlags.getImmediateNotificationBehaviorIssues().contains(flagKey)) {
346             return NOTIFICATION_BEHAVIOR_INTERNAL_IMMEDIATELY;
347         } else {
348             return NOTIFICATION_BEHAVIOR_INTERNAL_NEVER;
349         }
350     }
351 
areNotificationsAllowedForSource(SafetySource safetySource)352     private boolean areNotificationsAllowedForSource(SafetySource safetySource) {
353         if (SdkLevel.isAtLeastU()) {
354             if (safetySource.areNotificationsAllowed()) {
355                 return true;
356             }
357         }
358         return SafetyCenterFlags.getNotificationsAllowedSourceIds().contains(safetySource.getId());
359     }
360 
canNotifyDelayedIssueNow(SafetyCenterIssueKey issueKey)361     private boolean canNotifyDelayedIssueNow(SafetyCenterIssueKey issueKey) {
362         Duration minNotificationsDelay = SafetyCenterFlags.getNotificationsMinDelay();
363         Instant threshold = Instant.now().minus(minNotificationsDelay);
364         Instant seenAt = mSafetyCenterDataManager.getIssueFirstSeenAt(issueKey);
365         return seenAt != null && seenAt.isBefore(threshold);
366     }
367 
postNotificationForIssue( NotificationManager notificationManager, SafetySourceIssue issue, SafetyCenterIssueKey key)368     private boolean postNotificationForIssue(
369             NotificationManager notificationManager,
370             SafetySourceIssue issue,
371             SafetyCenterIssueKey key) {
372         Notification notification =
373                 mNotificationFactory.newNotificationForIssue(notificationManager, issue, key);
374         if (notification == null) {
375             return false;
376         }
377         String tag = getNotificationTag(key);
378         boolean wasPosted = notifyFromSystem(notificationManager, tag, notification);
379         if (wasPosted) {
380             mNotifiedIssues.put(key, issue);
381             SafetyCenterStatsdLogger.writeNotificationPostedEvent(
382                     key.getSafetySourceId(),
383                     UserUtils.isManagedProfile(key.getUserId(), mContext),
384                     issue.getIssueTypeId(),
385                     issue.getSeverityLevel());
386         }
387         return wasPosted;
388     }
389 
cancelStaleNotifications( NotificationManager notificationManager, @UserIdInt int userId, ArraySet<SafetyCenterIssueKey> freshIssueKeys)390     private void cancelStaleNotifications(
391             NotificationManager notificationManager,
392             @UserIdInt int userId,
393             ArraySet<SafetyCenterIssueKey> freshIssueKeys) {
394         // Loop in reverse index order to be able to remove entries while iterating
395         for (int i = mNotifiedIssues.size() - 1; i >= 0; i--) {
396             SafetyCenterIssueKey key = mNotifiedIssues.keyAt(i);
397             if (key.getUserId() == userId && !freshIssueKeys.contains(key)) {
398                 String tag = getNotificationTag(key);
399                 cancelNotificationFromSystem(notificationManager, tag);
400                 mNotifiedIssues.removeAt(i);
401             }
402         }
403     }
404 
getNotificationTag(SafetyCenterIssueKey issueKey)405     private static String getNotificationTag(SafetyCenterIssueKey issueKey) {
406         // Base 64 encoding of the issueKey proto:
407         return SafetyCenterIds.encodeToString(issueKey);
408     }
409 
410     /** Returns a {@link NotificationManager} which will send notifications to the given user. */
411     @Nullable
getNotificationManagerForUser(@serIdInt int userId)412     private NotificationManager getNotificationManagerForUser(@UserIdInt int userId) {
413         return SafetyCenterNotificationChannels.getNotificationManagerForUser(
414                 mContext, UserHandle.of(userId));
415     }
416 
417     /**
418      * Sends a {@link Notification} from the system, dropping any calling identity. Returns {@code
419      * true} if successful or {@code false} otherwise.
420      *
421      * <p>The recipient of the notification depends on the {@link Context} of the given {@link
422      * NotificationManager}. Use {@link #getNotificationManagerForUser(int)} to send notifications
423      * to a specific user.
424      */
notifyFromSystem( NotificationManager notificationManager, @Nullable String tag, Notification notification)425     private boolean notifyFromSystem(
426             NotificationManager notificationManager,
427             @Nullable String tag,
428             Notification notification) {
429         // This call is needed to send a notification from the system and this also grants the
430         // necessary POST_NOTIFICATIONS permission.
431         final long callingId = Binder.clearCallingIdentity();
432         try {
433             // The fixed notification ID is OK because notifications are keyed by (tag, id)
434             notificationManager.notify(tag, FIXED_NOTIFICATION_ID, notification);
435             return true;
436         } catch (Throwable e) {
437             Log.w(TAG, "Unable to send system notification", e);
438             return false;
439         } finally {
440             Binder.restoreCallingIdentity(callingId);
441         }
442     }
443 
444     /**
445      * Cancels a {@link Notification} from the system, dropping any calling identity.
446      *
447      * <p>The recipient of the notification depends on the {@link Context} of the given {@link
448      * NotificationManager}. Use {@link #getNotificationManagerForUser(int)} to cancel notifications
449      * sent to a specific user.
450      */
cancelNotificationFromSystem( NotificationManager notificationManager, @Nullable String tag)451     private void cancelNotificationFromSystem(
452             NotificationManager notificationManager, @Nullable String tag) {
453         // This call is needed to cancel a notification previously sent from the system
454         final long callingId = Binder.clearCallingIdentity();
455         try {
456             notificationManager.cancel(tag, FIXED_NOTIFICATION_ID);
457         } catch (Throwable e) {
458             Log.w(TAG, "Unable to cancel system notification", e);
459         } finally {
460             Binder.restoreCallingIdentity(callingId);
461         }
462     }
463 }
464