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 android.app.NotificationChannel;
20 import android.app.NotificationChannelGroup;
21 import android.app.NotificationManager;
22 import android.content.Context;
23 import android.os.Binder;
24 import android.os.UserHandle;
25 import android.safetycenter.SafetySourceData;
26 import android.safetycenter.SafetySourceIssue;
27 import android.util.Log;
28 
29 import androidx.annotation.Nullable;
30 
31 import com.android.permission.util.UserUtils;
32 import com.android.safetycenter.SafetyCenterFlags;
33 import com.android.safetycenter.resources.SafetyCenterResourcesApk;
34 
35 import java.util.List;
36 
37 /**
38  * Class responsible for creating and updating Safety Center's notification channels.
39  *
40  * @hide
41  */
42 public final class SafetyCenterNotificationChannels {
43 
44     private static final String TAG = "SafetyCenterNC";
45 
46     private static final String CHANNEL_GROUP_ID = "safety_center_channels";
47     private static final String CHANNEL_ID_INFORMATION = "safety_center_information";
48     private static final String CHANNEL_ID_RECOMMENDATION = "safety_center_recommendation";
49     private static final String CHANNEL_ID_CRITICAL_WARNING = "safety_center_critical_warning";
50 
51     private final SafetyCenterResourcesApk mSafetyCenterResourcesApk;
52 
SafetyCenterNotificationChannels(SafetyCenterResourcesApk safetyCenterResourcesApk)53     public SafetyCenterNotificationChannels(SafetyCenterResourcesApk safetyCenterResourcesApk) {
54         mSafetyCenterResourcesApk = safetyCenterResourcesApk;
55     }
56 
57     /** Returns a {@link NotificationManager} which will send notifications to the given user. */
58     @Nullable
getNotificationManagerForUser( Context baseContext, UserHandle userHandle)59     static NotificationManager getNotificationManagerForUser(
60             Context baseContext, UserHandle userHandle) {
61         Context contextAsUser = getContextAsUser(baseContext, userHandle);
62         NotificationManager notificationManager =
63                 (contextAsUser != null)
64                         ? contextAsUser.getSystemService(NotificationManager.class)
65                         : null;
66         if (notificationManager == null) {
67             Log.w(
68                     TAG,
69                     "Could not retrieve NotificationManager for user id: "
70                             + userHandle.getIdentifier());
71         }
72         return notificationManager;
73     }
74 
75     @Nullable
getContextAsUser(Context baseContext, UserHandle userHandle)76     static Context getContextAsUser(Context baseContext, UserHandle userHandle) {
77         // This call requires the INTERACT_ACROSS_USERS permission.
78         final long callingId = Binder.clearCallingIdentity();
79         try {
80             return baseContext.createContextAsUser(userHandle, /* flags= */ 0);
81         } catch (RuntimeException e) {
82             Log.w(TAG, "Could not create Context as user id: " + userHandle.getIdentifier(), e);
83             return null;
84         } finally {
85             Binder.restoreCallingIdentity(callingId);
86         }
87     }
88 
89     /**
90      * Returns the ID of the appropriate {@link NotificationChannel} for a notification about the
91      * given {@code issue} after ensuring that channel has been created.
92      */
93     @Nullable
getCreatedChannelId(NotificationManager notificationManager, SafetySourceIssue issue)94     String getCreatedChannelId(NotificationManager notificationManager, SafetySourceIssue issue) {
95         try {
96             createAllChannelsWithoutCallingIdentity(notificationManager);
97         } catch (RuntimeException e) {
98             Log.w(TAG, "Unable to create notification channels", e);
99             return null;
100         }
101         return getChannelIdForIssue(issue);
102     }
103 
104     /**
105      * Creates all Safety Center {@link NotificationChannel}s instances and their group, for all
106      * current users, dropping any calling identity so those channels can be unblockable.
107      */
createAllChannelsForAllUsers(Context context)108     public void createAllChannelsForAllUsers(Context context) {
109         if (!SafetyCenterFlags.getNotificationsEnabled()) {
110             // TODO(b/284271124): Decide what to do with existing channels if flag gets toggled.
111             Log.i(
112                     TAG,
113                     "Not creating notification channels because Safety Center notifications are"
114                             + " disabled");
115             return;
116         }
117 
118         List<UserHandle> users = UserUtils.getUserHandles(context);
119         for (int i = 0; i < users.size(); i++) {
120             createAllChannelsForUser(context, users.get(i));
121         }
122     }
123 
124     /**
125      * Creates all Safety Center {@link NotificationChannel}s instances and their group for the
126      * given {@link UserHandle}, dropping any calling identity so those channels can be unblockable.
127      */
createAllChannelsForUser(Context context, UserHandle user)128     public void createAllChannelsForUser(Context context, UserHandle user) {
129         if (!SafetyCenterFlags.getNotificationsEnabled()) {
130             // TODO(b/284271124): Decide what to do with existing channels if flag gets toggled.
131             Log.i(
132                     TAG,
133                     "Not creating notification channels because Safety Center notifications are"
134                             + " disabled");
135             return;
136         }
137 
138         NotificationManager notificationManager = getNotificationManagerForUser(context, user);
139         if (notificationManager == null) {
140             return;
141         }
142 
143         try {
144             createAllChannelsWithoutCallingIdentity(notificationManager);
145         } catch (RuntimeException e) {
146             Log.w(
147                     TAG,
148                     "Error creating notification channels for user id: " + user.getIdentifier(),
149                     e);
150         }
151     }
152 
153     @Nullable
getChannelIdForIssue(SafetySourceIssue issue)154     private String getChannelIdForIssue(SafetySourceIssue issue) {
155         int issueSeverityLevel = issue.getSeverityLevel();
156         switch (issueSeverityLevel) {
157             case SafetySourceData.SEVERITY_LEVEL_INFORMATION:
158                 return CHANNEL_ID_INFORMATION;
159             case SafetySourceData.SEVERITY_LEVEL_RECOMMENDATION:
160                 return CHANNEL_ID_RECOMMENDATION;
161             case SafetySourceData.SEVERITY_LEVEL_CRITICAL_WARNING:
162                 return CHANNEL_ID_CRITICAL_WARNING;
163             case SafetySourceData.SEVERITY_LEVEL_UNSPECIFIED:
164                 Log.w(TAG, "SafetySourceData.SeverityLevel is unspecified for issue: " + issue);
165                 return null;
166         }
167 
168         Log.w(
169                 TAG,
170                 "Unexpected SafetySourceData.SeverityLevel: "
171                         + issueSeverityLevel
172                         + ", for issue: "
173                         + issue);
174         return null;
175     }
176 
177     /**
178      * Creates all Safety Center {@link NotificationChannel}s instances and their group using the
179      * given {@link NotificationManager}, dropping any calling identity so those channels can be
180      * unblockable. Throws a {@link RuntimeException} if any channel is malformed and could not be
181      * created.
182      */
createAllChannelsWithoutCallingIdentity(NotificationManager notificationManager)183     private void createAllChannelsWithoutCallingIdentity(NotificationManager notificationManager) {
184         // Clearing calling identity to be able to make unblockable system notification channels and
185         // call this for other users with the INTERACT_ACROSS_USERS permission.
186         final long callingId = Binder.clearCallingIdentity();
187         try {
188             notificationManager.createNotificationChannelGroup(getChannelGroupDefinition());
189             notificationManager.createNotificationChannel(getInformationChannelDefinition());
190             notificationManager.createNotificationChannel(getRecommendationChannelDefinition());
191             notificationManager.createNotificationChannel(getCriticalWarningChannelDefinition());
192         } finally {
193             Binder.restoreCallingIdentity(callingId);
194         }
195     }
196 
clearAllChannelsWithoutCallingIdentity(NotificationManager notificationManager)197     private void clearAllChannelsWithoutCallingIdentity(NotificationManager notificationManager) {
198         // Clearing calling identity to do this for other users with the INTERACT_ACROSS_USERS
199         // permission.
200         final long callingId = Binder.clearCallingIdentity();
201         try {
202             notificationManager.deleteNotificationChannelGroup(CHANNEL_GROUP_ID);
203         } finally {
204             Binder.restoreCallingIdentity(callingId);
205         }
206     }
207 
getChannelGroupDefinition()208     private NotificationChannelGroup getChannelGroupDefinition() {
209         return new NotificationChannelGroup(
210                 CHANNEL_GROUP_ID, getString("notification_channel_group_name"));
211     }
212 
getInformationChannelDefinition()213     private NotificationChannel getInformationChannelDefinition() {
214         NotificationChannel channel =
215                 new NotificationChannel(
216                         CHANNEL_ID_INFORMATION,
217                         getString("notification_channel_name_information"),
218                         NotificationManager.IMPORTANCE_LOW);
219         channel.setGroup(CHANNEL_GROUP_ID);
220         channel.setBlockable(true);
221         return channel;
222     }
223 
getRecommendationChannelDefinition()224     private NotificationChannel getRecommendationChannelDefinition() {
225         NotificationChannel channel =
226                 new NotificationChannel(
227                         CHANNEL_ID_RECOMMENDATION,
228                         getString("notification_channel_name_recommendation"),
229                         NotificationManager.IMPORTANCE_DEFAULT);
230         channel.setGroup(CHANNEL_GROUP_ID);
231         channel.setBlockable(false);
232         return channel;
233     }
234 
getCriticalWarningChannelDefinition()235     private NotificationChannel getCriticalWarningChannelDefinition() {
236         NotificationChannel channel =
237                 new NotificationChannel(
238                         CHANNEL_ID_CRITICAL_WARNING,
239                         getString("notification_channel_name_critical_warning"),
240                         NotificationManager.IMPORTANCE_HIGH);
241         channel.setGroup(CHANNEL_GROUP_ID);
242         channel.setBlockable(false);
243         return channel;
244     }
245 
getString(String name)246     private String getString(String name) {
247         return mSafetyCenterResourcesApk.getStringByName(name);
248     }
249 }
250