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.SafetyCenterManager.EXTRA_SAFETY_SOURCE_ID; 20 import static android.safetycenter.SafetyCenterManager.EXTRA_SAFETY_SOURCE_ISSUE_ID; 21 import static android.safetycenter.SafetyCenterManager.EXTRA_SAFETY_SOURCE_USER_HANDLE; 22 23 import static com.android.safetycenter.notifications.SafetyCenterNotificationChannels.getContextAsUser; 24 25 import android.annotation.ColorInt; 26 import android.annotation.UserIdInt; 27 import android.app.Notification; 28 import android.app.NotificationChannel; 29 import android.app.NotificationManager; 30 import android.app.PendingIntent; 31 import android.content.Context; 32 import android.content.Intent; 33 import android.graphics.drawable.Icon; 34 import android.os.Bundle; 35 import android.os.UserHandle; 36 import android.safetycenter.SafetySourceData; 37 import android.safetycenter.SafetySourceIssue; 38 import android.text.TextUtils; 39 40 import androidx.annotation.Nullable; 41 42 import com.android.modules.utils.build.SdkLevel; 43 import com.android.safetycenter.PendingIntentFactory; 44 import com.android.safetycenter.internaldata.SafetyCenterIds; 45 import com.android.safetycenter.internaldata.SafetyCenterIssueActionId; 46 import com.android.safetycenter.internaldata.SafetyCenterIssueKey; 47 import com.android.safetycenter.resources.SafetyCenterResourcesApk; 48 49 import java.time.Duration; 50 import java.util.List; 51 52 /** 53 * Factory that builds {@link Notification} objects from {@link SafetySourceIssue} instances with 54 * appropriate {@link PendingIntent}s for click and dismiss callbacks. 55 */ 56 final class SafetyCenterNotificationFactory { 57 58 private static final int OPEN_SAFETY_CENTER_REQUEST_CODE = 1221; 59 private static final Duration SUCCESS_NOTIFICATION_TIMEOUT = Duration.ofSeconds(10); 60 61 private final Context mContext; 62 private final SafetyCenterNotificationChannels mNotificationChannels; 63 private final SafetyCenterResourcesApk mSafetyCenterResourcesApk; 64 SafetyCenterNotificationFactory( Context context, SafetyCenterNotificationChannels notificationChannels, SafetyCenterResourcesApk safetyCenterResourcesApk)65 SafetyCenterNotificationFactory( 66 Context context, 67 SafetyCenterNotificationChannels notificationChannels, 68 SafetyCenterResourcesApk safetyCenterResourcesApk) { 69 mContext = context; 70 mNotificationChannels = notificationChannels; 71 mSafetyCenterResourcesApk = safetyCenterResourcesApk; 72 } 73 74 /** 75 * Creates and returns a new {@link Notification} for a successful action, or {@code null} if 76 * none could be created. 77 * 78 * <p>The provided {@link NotificationManager} is used to create or update the {@link 79 * NotificationChannel} for the notification. 80 */ 81 @Nullable newNotificationForSuccessfulAction( NotificationManager notificationManager, SafetySourceIssue issue, SafetySourceIssue.Action action, @UserIdInt int userId)82 Notification newNotificationForSuccessfulAction( 83 NotificationManager notificationManager, 84 SafetySourceIssue issue, 85 SafetySourceIssue.Action action, 86 @UserIdInt int userId) { 87 if (action.getSuccessMessage() == null) { 88 return null; 89 } 90 91 String channelId = mNotificationChannels.getCreatedChannelId(notificationManager, issue); 92 if (channelId == null) { 93 return null; 94 } 95 96 PendingIntent contentIntent = newSafetyCenterPendingIntent(userId); 97 if (contentIntent == null) { 98 return null; 99 } 100 101 Notification.Builder builder = 102 new Notification.Builder(mContext, channelId) 103 .setSmallIcon( 104 getNotificationIcon(SafetySourceData.SEVERITY_LEVEL_INFORMATION)) 105 .setExtras(getNotificationExtras()) 106 .setContentTitle(action.getSuccessMessage()) 107 .setShowWhen(true) 108 .setTimeoutAfter(SUCCESS_NOTIFICATION_TIMEOUT.toMillis()) 109 .setContentIntent(contentIntent) 110 .setAutoCancel(true); 111 112 Integer color = getNotificationColor(SafetySourceData.SEVERITY_LEVEL_INFORMATION); 113 if (color != null) { 114 builder.setColor(color); 115 } 116 117 return builder.build(); 118 } 119 120 /** 121 * Creates and returns a new {@link Notification} instance corresponding to the given issue, or 122 * {@code null} if none could be created. 123 * 124 * <p>The provided {@link NotificationManager} is used to create or update the {@link 125 * NotificationChannel} for the notification. 126 */ 127 @Nullable newNotificationForIssue( NotificationManager notificationManager, SafetySourceIssue issue, SafetyCenterIssueKey issueKey)128 Notification newNotificationForIssue( 129 NotificationManager notificationManager, 130 SafetySourceIssue issue, 131 SafetyCenterIssueKey issueKey) { 132 String channelId = mNotificationChannels.getCreatedChannelId(notificationManager, issue); 133 if (channelId == null) { 134 return null; 135 } 136 137 CharSequence title = issue.getTitle(); 138 CharSequence text = issue.getSummary(); 139 List<SafetySourceIssue.Action> issueActions = issue.getActions(); 140 141 if (SdkLevel.isAtLeastU()) { 142 SafetySourceIssue.Notification customNotification = issue.getCustomNotification(); 143 if (customNotification != null) { 144 title = customNotification.getTitle(); 145 text = customNotification.getText(); 146 issueActions = customNotification.getActions(); 147 } 148 } 149 150 PendingIntent contentIntent = newSafetyCenterPendingIntent(issueKey); 151 if (contentIntent == null) { 152 return null; 153 } 154 155 Notification.Builder builder = 156 new Notification.Builder(mContext, channelId) 157 .setSmallIcon(getNotificationIcon(issue.getSeverityLevel())) 158 .setExtras(getNotificationExtras()) 159 .setShowWhen(true) 160 .setContentTitle(title) 161 .setContentText(text) 162 .setContentIntent(contentIntent) 163 .setDeleteIntent( 164 SafetyCenterNotificationReceiver.newNotificationDismissedIntent( 165 mContext, issueKey)); 166 167 Integer color = getNotificationColor(issue.getSeverityLevel()); 168 if (color != null) { 169 builder.setColor(color); 170 } 171 172 for (int i = 0; i < issueActions.size(); i++) { 173 Notification.Action notificationAction = 174 toNotificationAction(issueKey, issueActions.get(i)); 175 builder.addAction(notificationAction); 176 } 177 178 if (issue.getSeverityLevel() == SafetySourceData.SEVERITY_LEVEL_INFORMATION) { 179 builder.setAutoCancel(true); 180 } 181 182 return builder.build(); 183 } 184 185 /** 186 * Returns a {@link PendingIntent} to open Safety Center, navigating to a specific issue, or 187 * {@code null} if no such intent can be created. 188 */ 189 @Nullable newSafetyCenterPendingIntent(SafetyCenterIssueKey issueKey)190 private PendingIntent newSafetyCenterPendingIntent(SafetyCenterIssueKey issueKey) { 191 UserHandle userHandle = UserHandle.of(issueKey.getUserId()); 192 Context userContext = getContextAsUser(mContext, userHandle); 193 if (userContext == null) { 194 return null; 195 } 196 197 Intent intent = newSafetyCenterIntent(); 198 // Set the encoded issue key as the intent's identifier to ensure the PendingIntents of 199 // different notifications do not collide: 200 intent.setIdentifier(SafetyCenterIds.encodeToString(issueKey)); 201 intent.putExtra(EXTRA_SAFETY_SOURCE_ID, issueKey.getSafetySourceId()); 202 intent.putExtra(EXTRA_SAFETY_SOURCE_ISSUE_ID, issueKey.getSafetySourceIssueId()); 203 intent.putExtra(EXTRA_SAFETY_SOURCE_USER_HANDLE, userHandle); 204 205 return PendingIntentFactory.getActivityPendingIntent( 206 userContext, OPEN_SAFETY_CENTER_REQUEST_CODE, intent, PendingIntent.FLAG_IMMUTABLE); 207 } 208 209 /** 210 * Returns a {@link PendingIntent} to open Safety Center, or {@code null} if no such intent can 211 * be created. 212 */ 213 @Nullable newSafetyCenterPendingIntent(@serIdInt int userId)214 private PendingIntent newSafetyCenterPendingIntent(@UserIdInt int userId) { 215 Context userContext = getContextAsUser(mContext, UserHandle.of(userId)); 216 if (userContext == null) { 217 return null; 218 } 219 return PendingIntentFactory.getActivityPendingIntent( 220 userContext, 221 OPEN_SAFETY_CENTER_REQUEST_CODE, 222 newSafetyCenterIntent(), 223 PendingIntent.FLAG_IMMUTABLE); 224 } 225 newSafetyCenterIntent()226 private static Intent newSafetyCenterIntent() { 227 Intent intent = new Intent(Intent.ACTION_SAFETY_CENTER); 228 // This extra is defined in the PermissionController APK, cannot be referenced directly: 229 intent.putExtra("navigation_source_intent_extra", "NOTIFICATION"); 230 return intent; 231 } 232 getNotificationIcon(@afetySourceData.SeverityLevel int severityLevel)233 private Icon getNotificationIcon(@SafetySourceData.SeverityLevel int severityLevel) { 234 String iconResName = "ic_notification_badge_general"; 235 if (severityLevel == SafetySourceData.SEVERITY_LEVEL_CRITICAL_WARNING) { 236 iconResName = "ic_notification_badge_critical"; 237 } 238 Icon icon = mSafetyCenterResourcesApk.getIconByDrawableName(iconResName); 239 if (icon == null) { 240 // In case it was impossible to fetch the above drawable for any reason use this 241 // fallback which should be present on all Android devices: 242 icon = Icon.createWithResource(mContext, android.R.drawable.ic_dialog_alert); 243 } 244 return icon; 245 } 246 247 @ColorInt 248 @Nullable getNotificationColor(@afetySourceData.SeverityLevel int severityLevel)249 private Integer getNotificationColor(@SafetySourceData.SeverityLevel int severityLevel) { 250 String colorResName = "notification_tint_normal"; 251 if (severityLevel == SafetySourceData.SEVERITY_LEVEL_CRITICAL_WARNING) { 252 colorResName = "notification_tint_critical"; 253 } 254 return mSafetyCenterResourcesApk.getColorByName(colorResName); 255 } 256 getNotificationExtras()257 private Bundle getNotificationExtras() { 258 Bundle extras = new Bundle(); 259 String appName = 260 mSafetyCenterResourcesApk.getStringByName("notification_channel_group_name"); 261 if (!TextUtils.isEmpty(appName)) { 262 extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, appName); 263 } 264 return extras; 265 } 266 toNotificationAction( SafetyCenterIssueKey issueKey, SafetySourceIssue.Action issueAction)267 private Notification.Action toNotificationAction( 268 SafetyCenterIssueKey issueKey, SafetySourceIssue.Action issueAction) { 269 PendingIntent pendingIntent = getPendingIntentForAction(issueKey, issueAction); 270 return new Notification.Action.Builder( 271 /* icon= */ null, issueAction.getLabel(), pendingIntent) 272 .build(); 273 } 274 getPendingIntentForAction( SafetyCenterIssueKey issueKey, SafetySourceIssue.Action issueAction)275 private PendingIntent getPendingIntentForAction( 276 SafetyCenterIssueKey issueKey, SafetySourceIssue.Action issueAction) { 277 if (issueAction.willResolve()) { 278 return getReceiverPendingIntentForResolvingAction(issueKey, issueAction); 279 } else { 280 return getDirectPendingIntentForNonResolvingAction(issueAction); 281 } 282 } 283 getReceiverPendingIntentForResolvingAction( SafetyCenterIssueKey issueKey, SafetySourceIssue.Action issueAction)284 private PendingIntent getReceiverPendingIntentForResolvingAction( 285 SafetyCenterIssueKey issueKey, SafetySourceIssue.Action issueAction) { 286 // We do not use the action's PendingIntent directly here instead we build a new PI which 287 // will be handled by our SafetyCenterNotificationReceiver which will in turn dispatch 288 // the source-provided action PI. This ensures that action execution is consistent across 289 // between Safety Center UI and notifications, for example executing an action from a 290 // notification will send an "action in-flight" update to any current listeners. 291 SafetyCenterIssueActionId issueActionId = 292 SafetyCenterIssueActionId.newBuilder() 293 .setSafetyCenterIssueKey(issueKey) 294 .setSafetySourceIssueActionId(issueAction.getId()) 295 .build(); 296 return SafetyCenterNotificationReceiver.newNotificationActionClickedIntent( 297 mContext, issueActionId); 298 } 299 getDirectPendingIntentForNonResolvingAction( SafetySourceIssue.Action issueAction)300 private PendingIntent getDirectPendingIntentForNonResolvingAction( 301 SafetySourceIssue.Action issueAction) { 302 return issueAction.getPendingIntent(); 303 } 304 } 305