1 /* 2 * Copyright (C) 2021 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.nearby.fastpair.notification; 18 19 import static com.android.server.nearby.fastpair.Constant.TAG; 20 21 import android.annotation.Nullable; 22 import android.app.Notification; 23 import android.app.NotificationChannel; 24 import android.app.NotificationChannelGroup; 25 import android.app.NotificationManager; 26 import android.content.Context; 27 import android.util.Log; 28 29 import com.android.internal.annotations.VisibleForTesting; 30 import com.android.nearby.halfsheet.R; 31 import com.android.server.nearby.fastpair.HalfSheetResources; 32 import com.android.server.nearby.fastpair.cache.DiscoveryItem; 33 34 import com.google.common.base.Objects; 35 import com.google.common.cache.Cache; 36 import com.google.common.cache.CacheBuilder; 37 38 import java.util.concurrent.TimeUnit; 39 40 /** 41 * Responsible for show notification logic. 42 */ 43 public class FastPairNotificationManager { 44 45 private static int sInstanceId = 0; 46 // Notification channel group ID for Devices notification channels. 47 private static final String DEVICES_CHANNEL_GROUP_ID = "DEVICES_CHANNEL_GROUP_ID"; 48 // These channels are rebranded string because they are migrated from different channel ID they 49 // should not be changed. 50 // Channel ID for channel "Devices within reach". 51 static final String DEVICES_WITHIN_REACH_CHANNEL_ID = "DEVICES_WITHIN_REACH_REBRANDED"; 52 // Channel ID for channel "Devices". 53 static final String DEVICES_CHANNEL_ID = "DEVICES_REBRANDED"; 54 // Channel ID for channel "Devices with your account". 55 public static final String DEVICES_WITH_YOUR_ACCOUNT_CHANNEL_ID = "DEVICES_WITH_YOUR_ACCOUNT"; 56 57 // Default channel importance for channel "Devices within reach". 58 private static final int DEFAULT_DEVICES_WITHIN_REACH_CHANNEL_IMPORTANCE = 59 NotificationManager.IMPORTANCE_HIGH; 60 // Default channel importance for channel "Devices". 61 private static final int DEFAULT_DEVICES_CHANNEL_IMPORTANCE = 62 NotificationManager.IMPORTANCE_LOW; 63 // Default channel importance for channel "Devices with your account". 64 private static final int DEFAULT_DEVICES_WITH_YOUR_ACCOUNT_CHANNEL_IMPORTANCE = 65 NotificationManager.IMPORTANCE_MIN; 66 67 /** Fixed notification ID that won't duplicated with {@code notificationId}. */ 68 private static final int MAGIC_PAIR_NOTIFICATION_ID = "magic_pair_notification_id".hashCode(); 69 /** Fixed notification ID that won't duplicated with {@code mNotificationId}. */ 70 @VisibleForTesting 71 static final int PAIR_SUCCESS_NOTIFICATION_ID = MAGIC_PAIR_NOTIFICATION_ID - 1; 72 /** Fixed notification ID for showing the pairing failure notification. */ 73 @VisibleForTesting static final int PAIR_FAILURE_NOTIFICATION_ID = 74 MAGIC_PAIR_NOTIFICATION_ID - 3; 75 76 /** 77 * The amount of delay enforced between notifications. The system only allows 10 notifications / 78 * second, but delays in the binder IPC can cause overlap. 79 */ 80 private static final long MIN_NOTIFICATION_DELAY_MILLIS = 300; 81 82 // To avoid a (really unlikely) race where the user pairs and succeeds quickly more than once, 83 // use a unique ID per session, so we can delay cancellation without worrying. 84 // This is for connecting related notifications only. Discovery notification will use item id 85 // as notification id. 86 @VisibleForTesting 87 final int mNotificationId; 88 private HalfSheetResources mResources; 89 private final FastPairNotifications mNotifications; 90 private boolean mDiscoveryNotificationEnable = true; 91 // A static cache that remembers all recently shown notifications. We use this to throttle 92 // ourselves from showing notifications too rapidly. If we attempt to show a notification faster 93 // than once every 100ms, the later notifications will be dropped and we'll show stale state. 94 // Maps from Key -> Uptime Millis 95 private final Cache<Key, Long> mNotificationCache = 96 CacheBuilder.newBuilder() 97 .maximumSize(100) 98 .expireAfterWrite(MIN_NOTIFICATION_DELAY_MILLIS, TimeUnit.MILLISECONDS) 99 .build(); 100 private NotificationManager mNotificationManager; 101 102 /** 103 * FastPair notification manager that handle notification ui for fast pair. 104 */ 105 @VisibleForTesting FastPairNotificationManager(Context context, int notificationId, NotificationManager notificationManager, HalfSheetResources resources)106 public FastPairNotificationManager(Context context, int notificationId, 107 NotificationManager notificationManager, HalfSheetResources resources) { 108 mNotificationId = notificationId; 109 mNotificationManager = notificationManager; 110 mResources = resources; 111 mNotifications = new FastPairNotifications(context, mResources); 112 113 configureDevicesNotificationChannels(); 114 } 115 116 /** 117 * FastPair notification manager that handle notification ui for fast pair. 118 */ FastPairNotificationManager(Context context, int notificationId)119 public FastPairNotificationManager(Context context, int notificationId) { 120 this(context, notificationId, context.getSystemService(NotificationManager.class), 121 new HalfSheetResources(context)); 122 } 123 124 /** 125 * FastPair notification manager that handle notification ui for fast pair. 126 */ FastPairNotificationManager(Context context)127 public FastPairNotificationManager(Context context) { 128 this(context, /* notificationId= */ MAGIC_PAIR_NOTIFICATION_ID + sInstanceId); 129 130 sInstanceId++; 131 } 132 133 /** 134 * Shows the notification when found saved device. A notification will be like 135 * "Your saved device is available." 136 * This uses item id as notification Id. This should be disabled when connecting starts. 137 */ showDiscoveryNotification(DiscoveryItem item, byte[] accountKey)138 public void showDiscoveryNotification(DiscoveryItem item, byte[] accountKey) { 139 if (mDiscoveryNotificationEnable) { 140 Log.v(TAG, "the discovery notification is disabled"); 141 return; 142 } 143 144 show(item.getId().hashCode(), mNotifications.discoveryNotification(item, accountKey)); 145 } 146 147 /** 148 * Shows pairing in progress notification. 149 */ showConnectingNotification(DiscoveryItem item)150 public void showConnectingNotification(DiscoveryItem item) { 151 disableShowDiscoveryNotification(); 152 cancel(PAIR_FAILURE_NOTIFICATION_ID); 153 show(mNotificationId, mNotifications.progressNotification(item)); 154 } 155 156 /** 157 * Shows when Fast Pair successfully pairs the headset. 158 */ showPairingSucceededNotification( DiscoveryItem item, int batteryLevel, @Nullable String deviceName)159 public void showPairingSucceededNotification( 160 DiscoveryItem item, 161 int batteryLevel, 162 @Nullable String deviceName) { 163 enableShowDiscoveryNotification(); 164 cancel(mNotificationId); 165 show(PAIR_SUCCESS_NOTIFICATION_ID, 166 mNotifications 167 .pairingSucceededNotification( 168 batteryLevel, deviceName, item.getTitle(), item)); 169 } 170 171 /** 172 * Shows failed notification. 173 */ showPairingFailedNotification(DiscoveryItem item, byte[] accountKey)174 public synchronized void showPairingFailedNotification(DiscoveryItem item, byte[] accountKey) { 175 enableShowDiscoveryNotification(); 176 cancel(mNotificationId); 177 show(PAIR_FAILURE_NOTIFICATION_ID, 178 mNotifications.showPairingFailedNotification(item, accountKey)); 179 } 180 181 /** 182 * Notify the pairing process is done. 183 */ notifyPairingProcessDone(boolean success, boolean forceNotify, String privateAddress, String publicAddress)184 public void notifyPairingProcessDone(boolean success, boolean forceNotify, 185 String privateAddress, String publicAddress) {} 186 187 /** Enables the discovery notification when pairing is in progress */ enableShowDiscoveryNotification()188 public void enableShowDiscoveryNotification() { 189 Log.v(TAG, "enabling discovery notification"); 190 mDiscoveryNotificationEnable = true; 191 } 192 193 /** Disables the discovery notification when pairing is in progress */ disableShowDiscoveryNotification()194 public synchronized void disableShowDiscoveryNotification() { 195 Log.v(TAG, "disabling discovery notification"); 196 mDiscoveryNotificationEnable = false; 197 } 198 show(int id, Notification notification)199 private void show(int id, Notification notification) { 200 mNotificationManager.notify(id, notification); 201 } 202 203 /** 204 * Configures devices related notification channels, including "Devices" and "Devices within 205 * reach" channels. 206 */ configureDevicesNotificationChannels()207 private void configureDevicesNotificationChannels() { 208 mNotificationManager.createNotificationChannelGroup( 209 new NotificationChannelGroup( 210 DEVICES_CHANNEL_GROUP_ID, 211 mResources.get().getString(R.string.common_devices))); 212 mNotificationManager.createNotificationChannel( 213 createNotificationChannel( 214 DEVICES_WITHIN_REACH_CHANNEL_ID, 215 mResources.get().getString(R.string.devices_within_reach_channel_name), 216 DEFAULT_DEVICES_WITHIN_REACH_CHANNEL_IMPORTANCE, 217 DEVICES_CHANNEL_GROUP_ID)); 218 mNotificationManager.createNotificationChannel( 219 createNotificationChannel( 220 DEVICES_CHANNEL_ID, 221 mResources.get().getString(R.string.common_devices), 222 DEFAULT_DEVICES_CHANNEL_IMPORTANCE, 223 DEVICES_CHANNEL_GROUP_ID)); 224 mNotificationManager.createNotificationChannel( 225 createNotificationChannel( 226 DEVICES_WITH_YOUR_ACCOUNT_CHANNEL_ID, 227 mResources.get().getString(R.string.devices_with_your_account_channel_name), 228 DEFAULT_DEVICES_WITH_YOUR_ACCOUNT_CHANNEL_IMPORTANCE, 229 DEVICES_CHANNEL_GROUP_ID)); 230 } 231 createNotificationChannel( String channelId, String channelName, int channelImportance, String channelGroupId)232 private NotificationChannel createNotificationChannel( 233 String channelId, String channelName, int channelImportance, String channelGroupId) { 234 NotificationChannel channel = 235 new NotificationChannel(channelId, channelName, channelImportance); 236 channel.setGroup(channelGroupId); 237 if (channelImportance >= NotificationManager.IMPORTANCE_HIGH) { 238 channel.setSound(/* sound= */ null, /* audioAttributes= */ null); 239 // Disable vibration. Otherwise, the silent sound triggers a vibration if your 240 // ring volume is set to vibrate (aka turned down all the way). 241 channel.enableVibration(false); 242 } 243 244 return channel; 245 } 246 247 /** Cancel a previously shown notification. */ cancel(int id)248 public void cancel(int id) { 249 try { 250 mNotificationManager.cancel(id); 251 } catch (SecurityException e) { 252 Log.e(TAG, "Failed to cancel notification " + id, e); 253 } 254 mNotificationCache.invalidate(new Key(id)); 255 } 256 257 private static final class Key { 258 @Nullable final String mTag; 259 final int mId; 260 Key(int id)261 Key(int id) { 262 this.mTag = null; 263 this.mId = id; 264 } 265 266 @Override equals(@ullable Object o)267 public boolean equals(@Nullable Object o) { 268 if (o instanceof Key) { 269 Key that = (Key) o; 270 return Objects.equal(mTag, that.mTag) && (mId == that.mId); 271 } 272 return false; 273 } 274 275 @Override hashCode()276 public int hashCode() { 277 return Objects.hashCode(mTag == null ? 0 : mTag, mId); 278 } 279 } 280 } 281