1 /* 2 * Copyright (C) 2024 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.server.healthconnect.notifications; 18 19 import android.annotation.Nullable; 20 import android.app.Notification; 21 import android.app.NotificationChannel; 22 import android.app.NotificationChannelGroup; 23 import android.app.NotificationManager; 24 import android.content.Context; 25 import android.os.Binder; 26 import android.os.UserHandle; 27 import android.util.Log; 28 import android.util.Slog; 29 30 import java.util.Objects; 31 32 /** 33 * Abstraction for the Health Connect NotificationSender 34 * 35 * @hide 36 */ 37 public final class HealthConnectNotificationSender { 38 39 private static final String TAG = "HealthConnectNotificationSender"; 40 41 private final Context mContext; 42 private final HealthConnectNotificationFactory mNotificationFactory; 43 private final int mFixedNotificationId; 44 private final String mNotificationTag; 45 private final String mChannelId; 46 private final String mChannelGroupId; 47 private final String mChannelNameResource; 48 private final String mChannelGroupNameResource; 49 private final boolean mIsEnabled; 50 HealthConnectNotificationSender(Builder builder)51 public HealthConnectNotificationSender(Builder builder) { 52 if (builder.mContext == null 53 || builder.mNotificationFactory == null 54 || builder.mNotificationTag == null 55 || builder.mChannelId == null 56 || builder.mChannelGroupId == null 57 || builder.mChannelNameResource == null 58 || builder.mChannelGroupNameResource == null) { 59 throw new IllegalArgumentException("Values cannot be null"); 60 } 61 this.mContext = builder.mContext; 62 this.mNotificationFactory = builder.mNotificationFactory; 63 this.mFixedNotificationId = builder.mFixedNotificationId; 64 this.mNotificationTag = builder.mNotificationTag; 65 this.mChannelId = builder.mChannelId; 66 this.mChannelGroupId = builder.mChannelGroupId; 67 this.mChannelNameResource = builder.mChannelNameResource; 68 this.mChannelGroupNameResource = builder.mChannelGroupNameResource; 69 this.mIsEnabled = builder.mIsEnabled; 70 } 71 72 public static final class Builder { 73 74 @Nullable private Context mContext; 75 @Nullable private HealthConnectNotificationFactory mNotificationFactory; 76 private int mFixedNotificationId; 77 @Nullable private String mNotificationTag; 78 @Nullable private String mChannelId; 79 @Nullable private String mChannelGroupId; 80 @Nullable private String mChannelNameResource; 81 @Nullable private String mChannelGroupNameResource; 82 private boolean mIsEnabled; 83 84 /** provide notification sender with context */ setContext(Context context)85 public Builder setContext(Context context) { 86 this.mContext = context; 87 return this; 88 } 89 90 /** 91 * set the current status of the notification sender e.g. may want to only enable 92 * notifications if a specific Flag is enabled 93 */ setIsEnabled(boolean isEnabled)94 public Builder setIsEnabled(boolean isEnabled) { 95 this.mIsEnabled = isEnabled; 96 return this; 97 } 98 99 /** provide notification sender with notification factory */ setNotificationFactory( HealthConnectNotificationFactory notificationFactory)100 public Builder setNotificationFactory( 101 HealthConnectNotificationFactory notificationFactory) { 102 this.mNotificationFactory = notificationFactory; 103 return this; 104 } 105 106 /** set notification ID */ setFixedNotificationId(int fixedNotificationId)107 public Builder setFixedNotificationId(int fixedNotificationId) { 108 this.mFixedNotificationId = fixedNotificationId; 109 return this; 110 } 111 112 /** set the identifying tag for notifications */ setNotificationTag(String notificationTag)113 public Builder setNotificationTag(String notificationTag) { 114 this.mNotificationTag = notificationTag; 115 return this; 116 } 117 118 /** set the notification channel ID */ setChannelId(String channelId)119 public Builder setChannelId(String channelId) { 120 this.mChannelId = channelId; 121 return this; 122 } 123 124 /** set the notification channel group ID */ setChannelGroupId(String channelGroupId)125 public Builder setChannelGroupId(String channelGroupId) { 126 this.mChannelGroupId = channelGroupId; 127 return this; 128 } 129 130 /** set the name of the notification channel */ setChannelNameResource(String channelNameResource)131 public Builder setChannelNameResource(String channelNameResource) { 132 this.mChannelNameResource = channelNameResource; 133 return this; 134 } 135 136 /** set the name of the notification channel group */ setChannelGroupNameResource(String channelGroupNameResource)137 public Builder setChannelGroupNameResource(String channelGroupNameResource) { 138 this.mChannelGroupNameResource = channelGroupNameResource; 139 return this; 140 } 141 142 /** build the notification sender */ build()143 public HealthConnectNotificationSender build() { 144 if (this.mChannelGroupId == null 145 || this.mChannelId == null 146 || this.mNotificationTag == null 147 || this.mNotificationFactory == null 148 || this.mContext == null 149 || this.mChannelNameResource == null 150 || this.mChannelGroupNameResource == null) { 151 throw new IllegalArgumentException("Cannot have null parameter."); 152 } 153 return new HealthConnectNotificationSender(this); 154 } 155 } 156 157 /** Creates a notification determined by the passed-in type and displays it to the user. */ sendNotificationAsUser( @ealthConnectNotificationType int notificationType, UserHandle userHandle)158 public void sendNotificationAsUser( 159 @HealthConnectNotificationType int notificationType, UserHandle userHandle) { 160 161 Slog.i(TAG, "Sending notification as user."); 162 163 if (!mIsEnabled) { 164 Slog.i(TAG, "Notifications have been disabled."); 165 return; 166 } 167 168 createNotificationChannel(userHandle); 169 Notification notification = mNotificationFactory.createNotification(notificationType); 170 if (notification == null) return; 171 NotificationManager notificationManager = getNotificationManagerForUser(userHandle); 172 notifyFromSystem(notificationManager, notification); 173 } 174 175 /** Cancels all Health Connect notifications on this channel. */ clearNotificationsAsUser(UserHandle userHandle)176 public void clearNotificationsAsUser(UserHandle userHandle) { 177 if (!mIsEnabled) return; 178 NotificationManager notificationManager = getNotificationManagerForUser(userHandle); 179 cancelFromSystem(notificationManager); 180 } 181 182 /** Returns a {@link NotificationManager} which will send notifications to the given user. */ getNotificationManagerForUser(UserHandle userHandle)183 private NotificationManager getNotificationManagerForUser(UserHandle userHandle) { 184 Context contextAsUser = mContext.createContextAsUser(userHandle, 0); 185 return Objects.requireNonNull(contextAsUser.getSystemService(NotificationManager.class)); 186 } 187 notifyFromSystem( NotificationManager notificationManager, Notification notification)188 private void notifyFromSystem( 189 NotificationManager notificationManager, Notification notification) { 190 final long callingId = Binder.clearCallingIdentity(); 191 try { 192 notificationManager.notify(mNotificationTag, mFixedNotificationId, notification); 193 } catch (Throwable e) { 194 Log.w(TAG, "Unable to send system notification", e); 195 } finally { 196 Binder.restoreCallingIdentity(callingId); 197 } 198 } 199 cancelFromSystem(NotificationManager notificationManager)200 private void cancelFromSystem(NotificationManager notificationManager) { 201 final long callingId = Binder.clearCallingIdentity(); 202 try { 203 notificationManager.cancel(mNotificationTag, mFixedNotificationId); 204 } catch (Throwable e) { 205 Log.w(TAG, "Unable to cancel system notification", e); 206 } finally { 207 Binder.restoreCallingIdentity(callingId); 208 } 209 } 210 createNotificationChannel(UserHandle userHandle)211 private void createNotificationChannel(UserHandle userHandle) { 212 CharSequence channelGroupName = 213 mNotificationFactory.getStringResource(mChannelGroupNameResource); 214 CharSequence channelName = mNotificationFactory.getStringResource(mChannelNameResource); 215 216 NotificationChannelGroup group = 217 new NotificationChannelGroup(mChannelGroupId, channelGroupName); 218 219 int importance = NotificationManager.IMPORTANCE_HIGH; 220 NotificationChannel notificationChannel = 221 new NotificationChannel(mChannelId, channelName, importance); 222 notificationChannel.setGroup(mChannelGroupId); 223 notificationChannel.setBlockable(false); 224 225 final long callingId = Binder.clearCallingIdentity(); 226 227 NotificationManager notificationManager = getNotificationManagerForUser(userHandle); 228 229 try { 230 notificationManager.createNotificationChannelGroup(group); 231 notificationManager.createNotificationChannel(notificationChannel); 232 } catch (Throwable e) { 233 Log.w(TAG, "Unable to create notification channel", e); 234 } finally { 235 Binder.restoreCallingIdentity(callingId); 236 } 237 } 238 239 /** @hide */ 240 public @interface HealthConnectNotificationType {} 241 } 242