blob: 3b4eef8a5fa09190b794d8857c2c1191b3ff6acd [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.nearby.fastpair.notification;
import static com.android.nearby.halfsheet.constants.Constant.ACTION_FAST_PAIR;
import static com.android.server.nearby.fastpair.UserActionHandler.EXTRA_FAST_PAIR_SECRET;
import static com.android.server.nearby.fastpair.UserActionHandler.EXTRA_ITEM_ID;
import static com.android.server.nearby.fastpair.notification.FastPairNotificationManager.DEVICES_WITHIN_REACH_CHANNEL_ID;
import static com.google.common.io.BaseEncoding.base16;
import android.annotation.Nullable;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Icon;
import android.os.SystemClock;
import android.provider.Settings;
import com.android.nearby.halfsheet.R;
import com.android.server.nearby.common.fastpair.IconUtils;
import com.android.server.nearby.fastpair.HalfSheetResources;
import com.android.server.nearby.fastpair.cache.DiscoveryItem;
import service.proto.Cache;
/**
* Collection of utilities to create {@link Notification} objects that are displayed through {@link
* FastPairNotificationManager}.
*/
public class FastPairNotifications {
private final Context mContext;
private final HalfSheetResources mResources;
/**
* Note: Idea copied from Google.
*
* <p>Request code used for notification pending intents (executed on tap, dismiss).
*
* <p>Android only keeps one PendingIntent instance if it thinks multiple pending intents match.
* As comparing PendingIntents/Intents does not inspect the data in the extras, multiple pending
* intents can conflict. This can have surprising consequences (see b/68702692#comment8).
*
* <p>We also need to avoid conflicts with notifications started by an earlier launch of the app
* so use the truncated uptime of when the class was instantiated. The uptime will only overflow
* every ~50 days, and even then chances of conflict will be rare.
*/
private static int sRequestCode = (int) SystemClock.elapsedRealtime();
public FastPairNotifications(Context context, HalfSheetResources resources) {
this.mContext = context;
this.mResources = resources;
}
/**
* Creates the initial "Your saved device is available" notification when subsequent pairing
* is available.
* @param item discovered item which contains title and item id
* @param accountKey used for generating intent for pairing
*/
public Notification discoveryNotification(DiscoveryItem item, byte[] accountKey) {
Notification.Builder builder =
newBaseBuilder(item)
.setContentTitle(mResources.getString(R.string.fast_pair_your_device))
.setContentText(item.getTitle())
.setContentIntent(getPairIntent(item.getCopyOfStoredItem(), accountKey))
.setCategory(Notification.CATEGORY_RECOMMENDATION)
.setAutoCancel(false);
return builder.build();
}
/**
* Creates the in progress "Connecting" notification when the device and headset are paring.
*/
public Notification progressNotification(DiscoveryItem item) {
String summary = mResources.getString(R.string.common_connecting);
Notification.Builder builder =
newBaseBuilder(item)
.setTickerForAccessibility(summary)
.setCategory(Notification.CATEGORY_PROGRESS)
.setContentTitle(mResources.getString(R.string.fast_pair_your_device))
.setContentText(summary)
// Intermediate progress bar.
.setProgress(0, 0, true)
// Tapping does not dismiss this.
.setAutoCancel(false);
return builder.build();
}
/**
* Creates paring failed notification.
*/
public Notification showPairingFailedNotification(DiscoveryItem item, byte[] accountKey) {
String couldNotPair = mResources.getString(R.string.fast_pair_unable_to_connect);
String notificationContent;
if (accountKey != null) {
notificationContent = mResources.getString(
R.string.fast_pair_turn_on_bt_device_pairing_mode);
} else {
notificationContent =
mResources.getString(R.string.fast_pair_unable_to_connect_description);
}
Notification.Builder builder =
newBaseBuilder(item)
.setTickerForAccessibility(couldNotPair)
.setCategory(Notification.CATEGORY_ERROR)
.setContentTitle(couldNotPair)
.setContentText(notificationContent)
.setContentIntent(getBluetoothSettingsIntent())
// Dismissing completes the attempt.
.setDeleteIntent(getBluetoothSettingsIntent());
return builder.build();
}
/**
* Creates paring successfully notification.
*/
public Notification pairingSucceededNotification(
int batteryLevel,
@Nullable String deviceName,
String modelName,
DiscoveryItem item) {
final String contentText;
StringBuilder contentTextBuilder = new StringBuilder();
contentTextBuilder.append(modelName);
if (batteryLevel >= 0 && batteryLevel <= 100) {
contentTextBuilder
.append("\n")
.append(mResources.getString(R.string.common_battery_level, batteryLevel));
}
String pairingComplete =
deviceName == null
? mResources.getString(R.string.fast_pair_device_ready)
: mResources.getString(
R.string.fast_pair_device_ready_with_device_name, deviceName);
contentText = contentTextBuilder.toString();
Notification.Builder builder =
newBaseBuilder(item)
.setTickerForAccessibility(pairingComplete)
.setCategory(Notification.CATEGORY_STATUS)
.setContentTitle(pairingComplete)
.setContentText(contentText);
return builder.build();
}
private PendingIntent getPairIntent(Cache.StoredDiscoveryItem item, byte[] accountKey) {
Intent intent =
new Intent(ACTION_FAST_PAIR)
.putExtra(EXTRA_ITEM_ID, item.getId())
// Encode account key as a string instead of bytes so that it can be passed
// to the string representation of the intent.
.putExtra(EXTRA_FAST_PAIR_SECRET, base16().encode(accountKey))
.setPackage(mContext.getPackageName());
return PendingIntent.getBroadcast(mContext, sRequestCode++, intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
}
private PendingIntent getBluetoothSettingsIntent() {
Intent intent = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS);
return PendingIntent.getActivity(mContext, sRequestCode++, intent,
PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_IMMUTABLE);
}
private LargeHeadsUpNotificationBuilder newBaseBuilder(DiscoveryItem item) {
LargeHeadsUpNotificationBuilder builder =
(LargeHeadsUpNotificationBuilder)
(new LargeHeadsUpNotificationBuilder(
mContext,
DEVICES_WITHIN_REACH_CHANNEL_ID,
/* largeIcon= */ true)
.setIsDevice(true)
// Tapping does not dismiss this.
.setSmallIcon(Icon.createWithResource(
mResources.getResourcesContext(),
R.drawable.quantum_ic_devices_other_vd_theme_24)))
.setLargeIcon(IconUtils.addWhiteCircleBackground(
mResources.getResourcesContext(), item.getIcon()))
// Dismissible.
.setOngoing(false)
// Timestamp is not relevant, hide it.
.setShowWhen(false)
.setColor(mResources.getColor(R.color.discovery_activity_accent))
.setLocalOnly(true)
// don't show these notifications on wear devices
.setAutoCancel(true);
return builder;
}
}