• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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.nearby.halfsheet.constants.Constant.ACTION_FAST_PAIR;
20 import static com.android.server.nearby.fastpair.UserActionHandler.EXTRA_FAST_PAIR_SECRET;
21 import static com.android.server.nearby.fastpair.UserActionHandler.EXTRA_ITEM_ID;
22 import static com.android.server.nearby.fastpair.notification.FastPairNotificationManager.DEVICES_WITHIN_REACH_CHANNEL_ID;
23 
24 import static com.google.common.io.BaseEncoding.base16;
25 
26 import android.annotation.Nullable;
27 import android.app.Notification;
28 import android.app.PendingIntent;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.graphics.drawable.Icon;
32 import android.os.SystemClock;
33 import android.provider.Settings;
34 
35 import com.android.nearby.halfsheet.R;
36 import com.android.server.nearby.common.fastpair.IconUtils;
37 import com.android.server.nearby.fastpair.HalfSheetResources;
38 import com.android.server.nearby.fastpair.cache.DiscoveryItem;
39 
40 import service.proto.Cache;
41 
42 /**
43  * Collection of utilities to create {@link Notification} objects that are displayed through {@link
44  * FastPairNotificationManager}.
45  */
46 public class FastPairNotifications {
47 
48     private final Context mContext;
49     private final HalfSheetResources mResources;
50     /**
51      * Note: Idea copied from Google.
52      *
53      * <p>Request code used for notification pending intents (executed on tap, dismiss).
54      *
55      * <p>Android only keeps one PendingIntent instance if it thinks multiple pending intents match.
56      * As comparing PendingIntents/Intents does not inspect the data in the extras, multiple pending
57      * intents can conflict. This can have surprising consequences (see b/68702692#comment8).
58      *
59      * <p>We also need to avoid conflicts with notifications started by an earlier launch of the app
60      * so use the truncated uptime of when the class was instantiated. The uptime will only overflow
61      * every ~50 days, and even then chances of conflict will be rare.
62      */
63     private static int sRequestCode = (int) SystemClock.elapsedRealtime();
64 
FastPairNotifications(Context context, HalfSheetResources resources)65     public FastPairNotifications(Context context, HalfSheetResources resources) {
66         this.mContext = context;
67         this.mResources = resources;
68     }
69 
70     /**
71      * Creates the initial "Your saved device is available" notification when subsequent pairing
72      * is available.
73      * @param item discovered item which contains title and item id
74      * @param accountKey used for generating intent for pairing
75      */
discoveryNotification(DiscoveryItem item, byte[] accountKey)76     public Notification discoveryNotification(DiscoveryItem item, byte[] accountKey) {
77         Notification.Builder builder =
78                 newBaseBuilder(item)
79                         .setContentTitle(mResources.getString(R.string.fast_pair_your_device))
80                         .setContentText(item.getTitle())
81                         .setContentIntent(getPairIntent(item.getCopyOfStoredItem(), accountKey))
82                         .setCategory(Notification.CATEGORY_RECOMMENDATION)
83                         .setAutoCancel(false);
84         return builder.build();
85     }
86 
87     /**
88      * Creates the in progress "Connecting" notification when the device and headset are paring.
89      */
progressNotification(DiscoveryItem item)90     public Notification progressNotification(DiscoveryItem item) {
91         String summary = mResources.getString(R.string.common_connecting);
92         Notification.Builder builder =
93                 newBaseBuilder(item)
94                         .setTickerForAccessibility(summary)
95                         .setCategory(Notification.CATEGORY_PROGRESS)
96                         .setContentTitle(mResources.getString(R.string.fast_pair_your_device))
97                         .setContentText(summary)
98                         // Intermediate progress bar.
99                         .setProgress(0, 0, true)
100                         // Tapping does not dismiss this.
101                         .setAutoCancel(false);
102 
103         return builder.build();
104     }
105 
106     /**
107      * Creates paring failed notification.
108      */
showPairingFailedNotification(DiscoveryItem item, byte[] accountKey)109     public Notification showPairingFailedNotification(DiscoveryItem item, byte[] accountKey) {
110         String couldNotPair = mResources.getString(R.string.fast_pair_unable_to_connect);
111         String notificationContent;
112         if (accountKey != null) {
113             notificationContent = mResources.getString(
114                     R.string.fast_pair_turn_on_bt_device_pairing_mode);
115         } else {
116             notificationContent =
117                     mResources.getString(R.string.fast_pair_unable_to_connect_description);
118         }
119         Notification.Builder builder =
120                 newBaseBuilder(item)
121                         .setTickerForAccessibility(couldNotPair)
122                         .setCategory(Notification.CATEGORY_ERROR)
123                         .setContentTitle(couldNotPair)
124                         .setContentText(notificationContent)
125                         .setContentIntent(getBluetoothSettingsIntent())
126                         // Dismissing completes the attempt.
127                         .setDeleteIntent(getBluetoothSettingsIntent());
128         return builder.build();
129 
130     }
131 
132     /**
133      * Creates paring successfully notification.
134      */
pairingSucceededNotification( int batteryLevel, @Nullable String deviceName, String modelName, DiscoveryItem item)135     public Notification pairingSucceededNotification(
136             int batteryLevel,
137             @Nullable String deviceName,
138             String modelName,
139             DiscoveryItem item) {
140         final String contentText;
141         StringBuilder contentTextBuilder = new StringBuilder();
142         contentTextBuilder.append(modelName);
143         if (batteryLevel >= 0 && batteryLevel <= 100) {
144             contentTextBuilder
145                     .append("\n")
146                     .append(mResources.getString(R.string.common_battery_level, batteryLevel));
147         }
148         String pairingComplete =
149                 deviceName == null
150                         ? mResources.getString(R.string.fast_pair_device_ready)
151                         : mResources.getString(
152                                 R.string.fast_pair_device_ready_with_device_name, deviceName);
153         contentText = contentTextBuilder.toString();
154         Notification.Builder builder =
155                 newBaseBuilder(item)
156                         .setTickerForAccessibility(pairingComplete)
157                         .setCategory(Notification.CATEGORY_STATUS)
158                         .setContentTitle(pairingComplete)
159                         .setContentText(contentText);
160 
161         return builder.build();
162     }
163 
getPairIntent(Cache.StoredDiscoveryItem item, byte[] accountKey)164     private PendingIntent getPairIntent(Cache.StoredDiscoveryItem item, byte[] accountKey) {
165         Intent intent =
166                 new Intent(ACTION_FAST_PAIR)
167                         .putExtra(EXTRA_ITEM_ID, item.getId())
168                         // Encode account key as a string instead of bytes so that it can be passed
169                         // to the string representation of the intent.
170                         .putExtra(EXTRA_FAST_PAIR_SECRET, base16().encode(accountKey))
171                         .setPackage(mContext.getPackageName());
172         return PendingIntent.getBroadcast(mContext, sRequestCode++, intent,
173                 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
174     }
175 
getBluetoothSettingsIntent()176     private PendingIntent getBluetoothSettingsIntent() {
177         Intent intent = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS);
178         return PendingIntent.getActivity(mContext, sRequestCode++, intent,
179                 PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE);
180     }
181 
newBaseBuilder(DiscoveryItem item)182     private LargeHeadsUpNotificationBuilder newBaseBuilder(DiscoveryItem item) {
183         LargeHeadsUpNotificationBuilder builder =
184                 (LargeHeadsUpNotificationBuilder)
185                         (new LargeHeadsUpNotificationBuilder(
186                                 mContext,
187                                 DEVICES_WITHIN_REACH_CHANNEL_ID,
188                                 /* largeIcon= */ true)
189                                 .setIsDevice(true)
190                                 // Tapping does not dismiss this.
191                                 .setSmallIcon(Icon.createWithResource(
192                                         mResources.getResourcesContext(),
193                                         R.drawable.quantum_ic_devices_other_vd_theme_24)))
194                                 .setLargeIcon(IconUtils.addWhiteCircleBackground(
195                                         mResources.getResourcesContext(), item.getIcon()))
196                                 // Dismissible.
197                                 .setOngoing(false)
198                                 // Timestamp is not relevant, hide it.
199                                 .setShowWhen(false)
200                                 .setColor(mResources.getColor(R.color.discovery_activity_accent))
201                                 .setLocalOnly(true)
202                                 // don't show these notifications on wear devices
203                                 .setAutoCancel(true);
204 
205         return builder;
206     }
207 }
208