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