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