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