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