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