blob: c74249c20c1e9c5bb42b91e704efb1d8f9debbd7 [file] [log] [blame]
/*
* Copyright (C) 2021 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.server.nearby.fastpair.Constant.TAG;
import android.annotation.Nullable;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationChannelGroup;
import android.app.NotificationManager;
import android.content.Context;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.nearby.halfsheet.R;
import com.android.server.nearby.fastpair.HalfSheetResources;
import com.android.server.nearby.fastpair.cache.DiscoveryItem;
import com.google.common.base.Objects;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import java.util.concurrent.TimeUnit;
/**
* Responsible for show notification logic.
*/
public class FastPairNotificationManager {
private static int sInstanceId = 0;
// Notification channel group ID for Devices notification channels.
private static final String DEVICES_CHANNEL_GROUP_ID = "DEVICES_CHANNEL_GROUP_ID";
// These channels are rebranded string because they are migrated from different channel ID they
// should not be changed.
// Channel ID for channel "Devices within reach".
static final String DEVICES_WITHIN_REACH_CHANNEL_ID = "DEVICES_WITHIN_REACH_REBRANDED";
// Channel ID for channel "Devices".
static final String DEVICES_CHANNEL_ID = "DEVICES_REBRANDED";
// Channel ID for channel "Devices with your account".
public static final String DEVICES_WITH_YOUR_ACCOUNT_CHANNEL_ID = "DEVICES_WITH_YOUR_ACCOUNT";
// Default channel importance for channel "Devices within reach".
private static final int DEFAULT_DEVICES_WITHIN_REACH_CHANNEL_IMPORTANCE =
NotificationManager.IMPORTANCE_HIGH;
// Default channel importance for channel "Devices".
private static final int DEFAULT_DEVICES_CHANNEL_IMPORTANCE =
NotificationManager.IMPORTANCE_LOW;
// Default channel importance for channel "Devices with your account".
private static final int DEFAULT_DEVICES_WITH_YOUR_ACCOUNT_CHANNEL_IMPORTANCE =
NotificationManager.IMPORTANCE_MIN;
/** Fixed notification ID that won't duplicated with {@code notificationId}. */
private static final int MAGIC_PAIR_NOTIFICATION_ID = "magic_pair_notification_id".hashCode();
/** Fixed notification ID that won't duplicated with {@code mNotificationId}. */
@VisibleForTesting
static final int PAIR_SUCCESS_NOTIFICATION_ID = MAGIC_PAIR_NOTIFICATION_ID - 1;
/** Fixed notification ID for showing the pairing failure notification. */
@VisibleForTesting static final int PAIR_FAILURE_NOTIFICATION_ID =
MAGIC_PAIR_NOTIFICATION_ID - 3;
/**
* The amount of delay enforced between notifications. The system only allows 10 notifications /
* second, but delays in the binder IPC can cause overlap.
*/
private static final long MIN_NOTIFICATION_DELAY_MILLIS = 300;
// To avoid a (really unlikely) race where the user pairs and succeeds quickly more than once,
// use a unique ID per session, so we can delay cancellation without worrying.
// This is for connecting related notifications only. Discovery notification will use item id
// as notification id.
@VisibleForTesting
final int mNotificationId;
private HalfSheetResources mResources;
private final FastPairNotifications mNotifications;
private boolean mDiscoveryNotificationEnable = true;
// A static cache that remembers all recently shown notifications. We use this to throttle
// ourselves from showing notifications too rapidly. If we attempt to show a notification faster
// than once every 100ms, the later notifications will be dropped and we'll show stale state.
// Maps from Key -> Uptime Millis
private final Cache<Key, Long> mNotificationCache =
CacheBuilder.newBuilder()
.maximumSize(100)
.expireAfterWrite(MIN_NOTIFICATION_DELAY_MILLIS, TimeUnit.MILLISECONDS)
.build();
private NotificationManager mNotificationManager;
/**
* FastPair notification manager that handle notification ui for fast pair.
*/
@VisibleForTesting
public FastPairNotificationManager(Context context, int notificationId,
NotificationManager notificationManager, HalfSheetResources resources) {
mNotificationId = notificationId;
mNotificationManager = notificationManager;
mResources = resources;
mNotifications = new FastPairNotifications(context, mResources);
configureDevicesNotificationChannels();
}
/**
* FastPair notification manager that handle notification ui for fast pair.
*/
public FastPairNotificationManager(Context context, int notificationId) {
this(context, notificationId, context.getSystemService(NotificationManager.class),
new HalfSheetResources(context));
}
/**
* FastPair notification manager that handle notification ui for fast pair.
*/
public FastPairNotificationManager(Context context) {
this(context, /* notificationId= */ MAGIC_PAIR_NOTIFICATION_ID + sInstanceId);
sInstanceId++;
}
/**
* Shows the notification when found saved device. A notification will be like
* "Your saved device is available."
* This uses item id as notification Id. This should be disabled when connecting starts.
*/
public void showDiscoveryNotification(DiscoveryItem item, byte[] accountKey) {
if (mDiscoveryNotificationEnable) {
Log.v(TAG, "the discovery notification is disabled");
return;
}
show(item.getId().hashCode(), mNotifications.discoveryNotification(item, accountKey));
}
/**
* Shows pairing in progress notification.
*/
public void showConnectingNotification(DiscoveryItem item) {
disableShowDiscoveryNotification();
cancel(PAIR_FAILURE_NOTIFICATION_ID);
show(mNotificationId, mNotifications.progressNotification(item));
}
/**
* Shows when Fast Pair successfully pairs the headset.
*/
public void showPairingSucceededNotification(
DiscoveryItem item,
int batteryLevel,
@Nullable String deviceName) {
enableShowDiscoveryNotification();
cancel(mNotificationId);
show(PAIR_SUCCESS_NOTIFICATION_ID,
mNotifications
.pairingSucceededNotification(
batteryLevel, deviceName, item.getTitle(), item));
}
/**
* Shows failed notification.
*/
public synchronized void showPairingFailedNotification(DiscoveryItem item, byte[] accountKey) {
enableShowDiscoveryNotification();
cancel(mNotificationId);
show(PAIR_FAILURE_NOTIFICATION_ID,
mNotifications.showPairingFailedNotification(item, accountKey));
}
/**
* Notify the pairing process is done.
*/
public void notifyPairingProcessDone(boolean success, boolean forceNotify,
String privateAddress, String publicAddress) {}
/** Enables the discovery notification when pairing is in progress */
public void enableShowDiscoveryNotification() {
Log.v(TAG, "enabling discovery notification");
mDiscoveryNotificationEnable = true;
}
/** Disables the discovery notification when pairing is in progress */
public synchronized void disableShowDiscoveryNotification() {
Log.v(TAG, "disabling discovery notification");
mDiscoveryNotificationEnable = false;
}
private void show(int id, Notification notification) {
mNotificationManager.notify(id, notification);
}
/**
* Configures devices related notification channels, including "Devices" and "Devices within
* reach" channels.
*/
private void configureDevicesNotificationChannels() {
mNotificationManager.createNotificationChannelGroup(
new NotificationChannelGroup(
DEVICES_CHANNEL_GROUP_ID,
mResources.get().getString(R.string.common_devices)));
mNotificationManager.createNotificationChannel(
createNotificationChannel(
DEVICES_WITHIN_REACH_CHANNEL_ID,
mResources.get().getString(R.string.devices_within_reach_channel_name),
DEFAULT_DEVICES_WITHIN_REACH_CHANNEL_IMPORTANCE,
DEVICES_CHANNEL_GROUP_ID));
mNotificationManager.createNotificationChannel(
createNotificationChannel(
DEVICES_CHANNEL_ID,
mResources.get().getString(R.string.common_devices),
DEFAULT_DEVICES_CHANNEL_IMPORTANCE,
DEVICES_CHANNEL_GROUP_ID));
mNotificationManager.createNotificationChannel(
createNotificationChannel(
DEVICES_WITH_YOUR_ACCOUNT_CHANNEL_ID,
mResources.get().getString(R.string.devices_with_your_account_channel_name),
DEFAULT_DEVICES_WITH_YOUR_ACCOUNT_CHANNEL_IMPORTANCE,
DEVICES_CHANNEL_GROUP_ID));
}
private NotificationChannel createNotificationChannel(
String channelId, String channelName, int channelImportance, String channelGroupId) {
NotificationChannel channel =
new NotificationChannel(channelId, channelName, channelImportance);
channel.setGroup(channelGroupId);
if (channelImportance >= NotificationManager.IMPORTANCE_HIGH) {
channel.setSound(/* sound= */ null, /* audioAttributes= */ null);
// Disable vibration. Otherwise, the silent sound triggers a vibration if your
// ring volume is set to vibrate (aka turned down all the way).
channel.enableVibration(false);
}
return channel;
}
/** Cancel a previously shown notification. */
public void cancel(int id) {
try {
mNotificationManager.cancel(id);
} catch (SecurityException e) {
Log.e(TAG, "Failed to cancel notification " + id, e);
}
mNotificationCache.invalidate(new Key(id));
}
private static final class Key {
@Nullable final String mTag;
final int mId;
Key(int id) {
this.mTag = null;
this.mId = id;
}
@Override
public boolean equals(@Nullable Object o) {
if (o instanceof Key) {
Key that = (Key) o;
return Objects.equal(mTag, that.mTag) && (mId == that.mId);
}
return false;
}
@Override
public int hashCode() {
return Objects.hashCode(mTag == null ? 0 : mTag, mId);
}
}
}