• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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