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