[Fast Pair] Add FastpairNotificationManager logic for subsequent pairing

e2e video: https://photos.app.goo.gl/WawkcRm9eND3HfBb7

Test: -m
Fix: 247152236
Ignore-AOSP-First: nearby_not_in_aosp_yet
Change-Id: If2913e5072f130425315d203eb7eb03236968a68
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/FastPairAdvHandler.java b/nearby/service/java/com/android/server/nearby/fastpair/FastPairAdvHandler.java
index 7564716..412b738 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/FastPairAdvHandler.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/FastPairAdvHandler.java
@@ -36,6 +36,7 @@
 import com.android.server.nearby.fastpair.cache.DiscoveryItem;
 import com.android.server.nearby.fastpair.cache.FastPairCacheManager;
 import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
+import com.android.server.nearby.fastpair.notification.FastPairNotificationManager;
 import com.android.server.nearby.provider.FastPairDataProvider;
 import com.android.server.nearby.util.ArrayUtils;
 import com.android.server.nearby.util.DataUtils;
@@ -215,15 +216,11 @@
                 Log.d(TAG, "bloom filter is recognized in the cache");
                 continue;
             }
-
-            if (mIsFirst) {
-                mIsFirst = false;
-                pair(account, scannedDevice, recognizedDevice);
-            }
+            showSubsequentNotification(account, scannedDevice, recognizedDevice);
         }
     }
 
-    private void pair(Account account, FastPairDevice scannedDevice,
+    private void showSubsequentNotification(Account account, FastPairDevice scannedDevice,
             Data.FastPairDeviceWithAccountKey recognizedDevice) {
         // Get full info from api the initial request will only return
         // part of the info due to size limit.
@@ -242,12 +239,13 @@
                         .setMacAddress(
                                 scannedDevice.getBluetoothAddress())
                         .build();
-
-        // Connect and show notification
-        Locator.get(mContext, FastPairController.class).pair(
-                new DiscoveryItem(mContext, storedDiscoveryItem),
-                devicesWithAccountKeys.get(0).getAccountKey().toByteArray(),
-                /* companionApp= */ null);
+        // Show notification
+        FastPairNotificationManager fastPairNotificationManager =
+                Locator.get(mContext, FastPairNotificationManager.class);
+        DiscoveryItem item = new DiscoveryItem(mContext, storedDiscoveryItem);
+        Locator.get(mContext, FastPairCacheManager.class).saveDiscoveryItem(item);
+        fastPairNotificationManager.showDiscoveryNotification(item,
+                devicesWithAccountKeys.get(0).getAccountKey().toByteArray());
     }
 
     // Battery advertisement format:
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/FastPairController.java b/nearby/service/java/com/android/server/nearby/fastpair/FastPairController.java
index e1db7e5..447d199 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/FastPairController.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/FastPairController.java
@@ -129,11 +129,8 @@
                                 Log.d(TAG, "Incorrect state, ignore pairing");
                                 return;
                             }
-                            boolean useLargeNotifications =
-                                    item.getAuthenticationPublicKeySecp256R1() != null;
                             FastPairNotificationManager fastPairNotificationManager =
-                                    new FastPairNotificationManager(mContext, item,
-                                            useLargeNotifications);
+                                    Locator.get(mContext, FastPairNotificationManager.class);
                             FastPairHalfSheetManager fastPairHalfSheetManager =
                                     Locator.get(mContext, FastPairHalfSheetManager.class);
                             mFastPairCacheManager.saveDiscoveryItem(item);
@@ -169,7 +166,7 @@
             @Nullable byte[] accountKey,
             @Nullable String companionApp) {
         FastPairNotificationManager fastPairNotificationManager =
-                new FastPairNotificationManager(mContext, item, false);
+                Locator.get(mContext, FastPairNotificationManager.class);
         FastPairHalfSheetManager fastPairHalfSheetManager =
                 Locator.get(mContext, FastPairHalfSheetManager.class);
         PairingProgressHandlerBase pairingProgressHandlerBase =
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/FastPairManager.java b/nearby/service/java/com/android/server/nearby/fastpair/FastPairManager.java
index ea8c386..1ef98e5 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/FastPairManager.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/FastPairManager.java
@@ -16,6 +16,7 @@
 
 package com.android.server.nearby.fastpair;
 
+import static com.android.nearby.halfsheet.constants.Constant.ACTION_FAST_PAIR;
 import static com.android.nearby.halfsheet.constants.Constant.ACTION_FAST_PAIR_HALF_SHEET_BAN_STATE_RESET;
 import static com.android.nearby.halfsheet.constants.Constant.ACTION_FAST_PAIR_HALF_SHEET_CANCEL;
 import static com.android.nearby.halfsheet.constants.Constant.ACTION_HALF_SHEET_FOREGROUND_STATE;
@@ -23,6 +24,8 @@
 import static com.android.nearby.halfsheet.constants.FastPairConstants.EXTRA_MODEL_ID;
 import static com.android.server.nearby.fastpair.Constant.TAG;
 
+import static com.google.common.io.BaseEncoding.base16;
+
 import android.annotation.Nullable;
 import android.annotation.WorkerThread;
 import android.app.KeyguardManager;
@@ -103,6 +106,7 @@
     final IntentFilter mIntentFilter;
     final Locator mLocator;
     private boolean mScanEnabled;
+    private final FastPairCacheManager mFastPairCacheManager;
 
     private final BroadcastReceiver mScreenBroadcastReceiver = new BroadcastReceiver() {
         @Override
@@ -147,9 +151,29 @@
                     }
                     Locator.get(mLocatorContextWrapper, FastPairHalfSheetManager.class)
                             .dismiss(modelId);
-
+                    break;
+                case ACTION_FAST_PAIR:
+                    Log.d(TAG, "onReceive: ACTION_FAST_PAIR");
+                    String itemId = intent.getStringExtra(UserActionHandler.EXTRA_ITEM_ID);
+                    String accountKeyString = intent
+                            .getStringExtra(UserActionHandler.EXTRA_FAST_PAIR_SECRET);
+                    if (itemId == null || accountKeyString == null) {
+                        Log.d(TAG, "skip pair action, item id "
+                                + "or fast pair account key not found");
+                        break;
+                    }
+                    try {
+                        FastPairController controller =
+                                Locator.getFromContextWrapper(mLocatorContextWrapper,
+                                        FastPairController.class);
+                        if (mFastPairCacheManager != null) {
+                            controller.pair(mFastPairCacheManager.getDiscoveryItem(itemId),
+                                    base16().decode(accountKeyString), /* companionApp= */ null);
+                        }
+                    } catch (IllegalStateException e) {
+                        Log.e(TAG, "Cannot find FastPairController class", e);
+                    }
             }
-
         }
     };
 
@@ -160,6 +184,8 @@
         mLocator.bind(new FastPairModule());
         Rpcs.GetObservedDeviceResponse getObservedDeviceResponse =
                 Rpcs.GetObservedDeviceResponse.newBuilder().build();
+        mFastPairCacheManager =
+                Locator.getFromContextWrapper(mLocatorContextWrapper, FastPairCacheManager.class);
     }
 
     final ScanCallback mScanCallback = new ScanCallback() {
@@ -199,11 +225,11 @@
         mIntentFilter.addAction(ACTION_FAST_PAIR_HALF_SHEET_CANCEL);
         mIntentFilter.addAction(ACTION_FAST_PAIR_HALF_SHEET_BAN_STATE_RESET);
         mIntentFilter.addAction(ACTION_HALF_SHEET_FOREGROUND_STATE);
+        mIntentFilter.addAction(ACTION_FAST_PAIR);
 
         mLocatorContextWrapper.getContext().registerReceiver(mScreenBroadcastReceiver,
                 mIntentFilter, Context.RECEIVER_EXPORTED);
 
-        Locator.getFromContextWrapper(mLocatorContextWrapper, FastPairCacheManager.class);
         // Default false for now.
         mScanEnabled = NearbyManager.isFastPairScanEnabled(mLocatorContextWrapper.getContext());
         registerFastPairScanChangeContentObserver(mLocatorContextWrapper.getContentResolver());
@@ -307,6 +333,13 @@
                     context, item.getMacAddress(),
                     prefsBuilder.build(),
                     null);
+            connection.setOnPairedCallback(
+                    address -> {
+                        Log.v(TAG, "connection on paired callback;");
+                        // TODO(b/259150992) add fill Bluetooth metadata values logic
+                        pairingProgressHandlerBase.onPairedCallbackCalled(
+                                connection, accountKey, footprints, address);
+                    });
             pairingProgressHandlerBase.onPairingSetupCompleted();
 
             FastPairConnection.SharedSecret sharedSecret;
@@ -474,7 +507,7 @@
         NearbyManager nearbyManager = getNearbyManager();
         if (nearbyManager == null) {
             Log.w(TAG, "invalidateScan: "
-                    + "failed to start or stop scannning because NearbyManager is null.");
+                    + "failed to start or stop scanning because NearbyManager is null.");
             return;
         }
         if (mScanEnabled) {
@@ -504,8 +537,7 @@
                 processBackgroundTask(new Runnable() {
                     @Override
                     public void run() {
-                        mLocatorContextWrapper.getLocator().get(FastPairCacheManager.class)
-                                .removeStoredFastPairItem(device.getAddress());
+                        mFastPairCacheManager.removeStoredFastPairItem(device.getAddress());
                     }
                 });
             }
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/FastPairModule.java b/nearby/service/java/com/android/server/nearby/fastpair/FastPairModule.java
index d7946d1..1df4723 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/FastPairModule.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/FastPairModule.java
@@ -24,6 +24,7 @@
 import com.android.server.nearby.fastpair.cache.FastPairCacheManager;
 import com.android.server.nearby.fastpair.footprint.FootprintsDeviceManager;
 import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
+import com.android.server.nearby.fastpair.notification.FastPairNotificationManager;
 
 import java.time.Clock;
 import java.time.Instant;
@@ -52,6 +53,9 @@
             locator.bind(FastPairHalfSheetManager.class, new FastPairHalfSheetManager(context));
         } else if (type.equals(FastPairAdvHandler.class)) {
             locator.bind(FastPairAdvHandler.class, new FastPairAdvHandler(context));
+        } else if (type.equals(FastPairNotificationManager.class)) {
+            locator.bind(FastPairNotificationManager.class,
+                    new FastPairNotificationManager(context));
         } else if (type.equals(Clock.class)) {
             locator.bind(Clock.class, new Clock() {
                 @Override
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/notification/FastPairNotificationManager.java b/nearby/service/java/com/android/server/nearby/fastpair/notification/FastPairNotificationManager.java
index 65ad05e..c74249c 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/notification/FastPairNotificationManager.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/notification/FastPairNotificationManager.java
@@ -16,56 +16,166 @@
 
 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.
      */
-    public FastPairNotificationManager(Context context, DiscoveryItem item, boolean useLargeIcon,
-            int notificationId) {
+    @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, DiscoveryItem item, boolean useLargeIcon) {
+    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() {}
+    public void showConnectingNotification(DiscoveryItem item) {
+        disableShowDiscoveryNotification();
+        cancel(PAIR_FAILURE_NOTIFICATION_ID);
+        show(mNotificationId, mNotifications.progressNotification(item));
+    }
 
     /**
-     * Shows success notification
+     * Shows when Fast Pair successfully pairs the headset.
      */
     public void showPairingSucceededNotification(
-            @Nullable String companionApp,
+            DiscoveryItem item,
             int batteryLevel,
-            @Nullable String deviceName,
-            String address) {
-
+            @Nullable String deviceName) {
+        enableShowDiscoveryNotification();
+        cancel(mNotificationId);
+        show(PAIR_SUCCESS_NOTIFICATION_ID,
+                mNotifications
+                        .pairingSucceededNotification(
+                                batteryLevel, deviceName, item.getTitle(), item));
     }
 
     /**
      * Shows failed notification.
      */
-    public void showPairingFailedNotification(byte[] accountKey) {
-
+    public synchronized void showPairingFailedNotification(DiscoveryItem item, byte[] accountKey) {
+        enableShowDiscoveryNotification();
+        cancel(mNotificationId);
+        show(PAIR_FAILURE_NOTIFICATION_ID,
+                mNotifications.showPairingFailedNotification(item, accountKey));
     }
 
     /**
@@ -73,4 +183,98 @@
      */
     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);
+        }
+    }
 }
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/NotificationPairingProgressHandler.java b/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/NotificationPairingProgressHandler.java
index d469c45..5317673 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/NotificationPairingProgressHandler.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/NotificationPairingProgressHandler.java
@@ -20,7 +20,6 @@
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothManager;
 import android.content.Context;
-import android.text.TextUtils;
 import android.util.Log;
 
 import com.android.server.nearby.common.bluetooth.fastpair.FastPairConnection;
@@ -68,7 +67,7 @@
     @Override
     public void onReadyToPair() {
         super.onReadyToPair();
-        mFastPairNotificationManager.showConnectingNotification();
+        mFastPairNotificationManager.showConnectingNotification(mItem);
     }
 
     @Override
@@ -79,32 +78,24 @@
             String address) {
         String deviceName = super.onPairedCallbackCalled(connection, accountKey, footprints,
                 address);
-
         int batteryLevel = -1;
 
         BluetoothManager bluetoothManager = mContext.getSystemService(BluetoothManager.class);
         BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();
-        if (bluetoothAdapter != null) {
-            // Need to check battery level here set that to -1 for now
-            batteryLevel = -1;
+        if (address != null && bluetoothAdapter != null) {
+            batteryLevel = bluetoothAdapter.getRemoteDevice(address).getBatteryLevel();
         } else {
-            Log.v(
-                    "NotificationPairingProgressHandler",
-                    "onPairedCallbackCalled getBatteryLevel failed,"
-                            + " adapter is null");
+            Log.v(TAG, "onPairedCallbackCalled getBatteryLevel failed");
         }
-        mFastPairNotificationManager.showPairingSucceededNotification(
-                !TextUtils.isEmpty(mCompanionApp) ? mCompanionApp : null,
-                batteryLevel,
-                deviceName,
-                address);
+        mFastPairNotificationManager
+                .showPairingSucceededNotification(mItem, batteryLevel, deviceName);
         return deviceName;
     }
 
     @Override
     public void onPairingFailed(Throwable throwable) {
         super.onPairingFailed(throwable);
-        mFastPairNotificationManager.showPairingFailedNotification(mAccountKey);
+        mFastPairNotificationManager.showPairingFailedNotification(mItem, mAccountKey);
         mFastPairNotificationManager.notifyPairingProcessDone(
                 /* success= */ false,
                 /* forceNotify= */ false,
@@ -115,6 +106,19 @@
     @Override
     public void onPairingSuccess(String address) {
         super.onPairingSuccess(address);
+        int batteryLevel = -1;
+
+        BluetoothManager bluetoothManager = mContext.getSystemService(BluetoothManager.class);
+        BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();
+        String deviceName = null;
+        if (address != null && bluetoothAdapter != null) {
+            deviceName = bluetoothAdapter.getRemoteDevice(address).getName();
+            batteryLevel = bluetoothAdapter.getRemoteDevice(address).getBatteryLevel();
+        } else {
+            Log.v(TAG, "onPairedCallbackCalled getBatteryLevel failed");
+        }
+        mFastPairNotificationManager
+                .showPairingSucceededNotification(mItem, batteryLevel, deviceName);
         mFastPairNotificationManager.notifyPairingProcessDone(
                 /* success= */ true,
                 /* forceNotify= */ false,
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairAdvHandlerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairAdvHandlerTest.java
index 0544efc..900b618 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairAdvHandlerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairAdvHandlerTest.java
@@ -177,7 +177,8 @@
 
         DiscoveryItem discoveryItem =
                 new DiscoveryItem(mLocatorContextWrapper, STORED_DISCOVERY_ITEM);
-        verify(mFastPairController).pair(eq(discoveryItem), eq(ACCOUNT_KEY), eq(null));
+        verify(mFastPairNotificationManager).showDiscoveryNotification(eq(discoveryItem),
+                eq(ACCOUNT_KEY));
         verify(mFastPairHalfSheetManager, never()).showHalfSheet(any());
     }
 
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationBuilderTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationBuilderTest.java
index b644c91..d995969 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationBuilderTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationBuilderTest.java
@@ -23,7 +23,6 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import android.app.Notification;
@@ -69,7 +68,6 @@
         mResolveInfoList = new ArrayList<>();
         mResolveInfo.activityInfo = new ActivityInfo();
         mApplicationInfo = new ApplicationInfo();
-        mPackageManager = mock(PackageManager.class);
 
         when(mContext.getResources()).thenReturn(mResources);
         when(mContext.getApplicationInfo())
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationManagerTest.java
index 4fb6b37..9670a3f 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationManagerTest.java
@@ -18,38 +18,62 @@
 
 import static org.mockito.Mockito.when;
 
+import android.app.NotificationManager;
 import android.content.Context;
+import android.content.res.Resources;
 
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.server.nearby.common.locator.Locator;
 import com.android.server.nearby.common.locator.LocatorContextWrapper;
+import com.android.server.nearby.fastpair.HalfSheetResources;
+import com.android.server.nearby.fastpair.cache.DiscoveryItem;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import service.proto.Cache;
+
 public class FastPairNotificationManagerTest {
 
-    @Mock private Context mContext;
-    private static final boolean USE_LARGE_ICON = true;
+    @Mock
+    private Context mContext;
+    @Mock
+    NotificationManager mNotificationManager;
+    @Mock
+    Resources mResources;
+    @Mock
+    private LocatorContextWrapper mLocatorContextWrapper;
+    @Mock
+    private Locator mLocator;
+
     private static final int NOTIFICATION_ID = 1;
-    private static final String COMPANION_APP = "companionApp";
     private static final int BATTERY_LEVEL = 1;
     private static final String DEVICE_NAME = "deviceName";
-    private static final String ADDRESS = "address";
     private FastPairNotificationManager mFastPairNotificationManager;
-    private LocatorContextWrapper mLocatorContextWrapper;
+    private DiscoveryItem mItem;
 
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
-        mLocatorContextWrapper = new LocatorContextWrapper(mContext);
         when(mContext.getContentResolver()).thenReturn(
                 InstrumentationRegistry.getInstrumentation().getContext().getContentResolver());
+        when(mLocatorContextWrapper.getResources()).thenReturn(mResources);
+        when(mLocatorContextWrapper.getLocator()).thenReturn(mLocator);
+        HalfSheetResources.setResourcesContextForTest(mLocatorContextWrapper);
+        // Real context is needed
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        FastPairNotificationManager fastPairNotificationManager =
         mFastPairNotificationManager =
-                new FastPairNotificationManager(mLocatorContextWrapper, null,
-                        USE_LARGE_ICON, NOTIFICATION_ID);
+                new FastPairNotificationManager(context, NOTIFICATION_ID, mNotificationManager,
+                        new HalfSheetResources(mLocatorContextWrapper));
+        mLocator.overrideBindingForTest(FastPairNotificationManager.class,
+                fastPairNotificationManager);
+
+        mItem = new DiscoveryItem(mLocatorContextWrapper,
+                Cache.StoredDiscoveryItem.newBuilder().setTitle("Device Name").build());
     }
 
     @Test
@@ -60,17 +84,18 @@
 
     @Test
     public void  showConnectingNotification() {
-        mFastPairNotificationManager.showConnectingNotification();
+        mFastPairNotificationManager.showConnectingNotification(mItem);
     }
 
     @Test
     public void   showPairingFailedNotification() {
-        mFastPairNotificationManager.showPairingFailedNotification(new byte[]{1});
+        mFastPairNotificationManager
+                .showPairingFailedNotification(mItem, new byte[]{1});
     }
 
     @Test
     public void  showPairingSucceededNotification() {
-        mFastPairNotificationManager.showPairingSucceededNotification(COMPANION_APP,
-                BATTERY_LEVEL, DEVICE_NAME, ADDRESS);
+        mFastPairNotificationManager
+                .showPairingSucceededNotification(mItem, BATTERY_LEVEL, DEVICE_NAME);
     }
 }
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/NotificationPairingProgressHandlerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/NotificationPairingProgressHandlerTest.java
index 24f296c..5c61ddb 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/NotificationPairingProgressHandlerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/NotificationPairingProgressHandlerTest.java
@@ -20,13 +20,18 @@
 
 import static org.mockito.Mockito.when;
 
+import android.app.NotificationManager;
 import android.bluetooth.BluetoothManager;
+import android.content.Context;
+import android.content.res.Resources;
 
 import androidx.annotation.Nullable;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.server.nearby.common.bluetooth.fastpair.FastPairConnection;
 import com.android.server.nearby.common.locator.Locator;
 import com.android.server.nearby.common.locator.LocatorContextWrapper;
+import com.android.server.nearby.fastpair.HalfSheetResources;
 import com.android.server.nearby.fastpair.cache.DiscoveryItem;
 import com.android.server.nearby.fastpair.cache.FastPairCacheManager;
 import com.android.server.nearby.fastpair.footprint.FootprintsDeviceManager;
@@ -62,6 +67,10 @@
     FootprintsDeviceManager mFootprintsDeviceManager;
     @Mock
     android.bluetooth.BluetoothManager mBluetoothManager;
+    @Mock
+    NotificationManager mNotificationManager;
+    @Mock
+    Resources mResources;
 
     private static final String MAC_ADDRESS = "00:11:22:33:44:55";
     private static final byte[] ACCOUNT_KEY = new byte[]{0x01, 0x02};
@@ -76,6 +85,9 @@
         when(mContextWrapper.getSystemService(BluetoothManager.class))
                 .thenReturn(mBluetoothManager);
         when(mContextWrapper.getLocator()).thenReturn(mLocator);
+        when(mContextWrapper.getResources()).thenReturn(mResources);
+        HalfSheetResources.setResourcesContextForTest(mContextWrapper);
+
         mLocator.overrideBindingForTest(FastPairCacheManager.class,
                 mFastPairCacheManager);
         mLocator.overrideBindingForTest(Clock.class, mClock);
@@ -126,11 +138,17 @@
 
     private NotificationPairingProgressHandler createProgressHandler(
             @Nullable byte[] accountKey, DiscoveryItem fastPairItem) {
-        FastPairNotificationManager fastPairNotificationManager =
-                new FastPairNotificationManager(mContextWrapper, fastPairItem, true);
         FastPairHalfSheetManager fastPairHalfSheetManager =
                 new FastPairHalfSheetManager(mContextWrapper);
+        // Real context is needed
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        FastPairNotificationManager fastPairNotificationManager =
+                new FastPairNotificationManager(context, 1234, mNotificationManager,
+                        new HalfSheetResources(mContextWrapper));
         mLocator.overrideBindingForTest(FastPairHalfSheetManager.class, fastPairHalfSheetManager);
+        mLocator.overrideBindingForTest(FastPairNotificationManager.class,
+                fastPairNotificationManager);
+
         NotificationPairingProgressHandler mNotificationPairingProgressHandler =
                 new NotificationPairingProgressHandler(
                         mContextWrapper,
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBaseTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBaseTest.java
index a3eb50c..6d769df 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBaseTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBaseTest.java
@@ -20,16 +20,21 @@
 
 import static org.mockito.Mockito.when;
 
+import android.app.NotificationManager;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothManager;
+import android.content.Context;
+import android.content.res.Resources;
 
 import androidx.annotation.Nullable;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.server.nearby.common.bluetooth.fastpair.FastPairConnection;
 import com.android.server.nearby.common.bluetooth.fastpair.Preferences;
 import com.android.server.nearby.common.locator.Locator;
 import com.android.server.nearby.common.locator.LocatorContextWrapper;
+import com.android.server.nearby.fastpair.HalfSheetResources;
 import com.android.server.nearby.fastpair.cache.DiscoveryItem;
 import com.android.server.nearby.fastpair.cache.FastPairCacheManager;
 import com.android.server.nearby.fastpair.footprint.FootprintsDeviceManager;
@@ -64,6 +69,10 @@
     FastPairConnection mFastPairConnection;
     @Mock
     BluetoothManager mBluetoothManager;
+    @Mock
+    Resources mResources;
+    @Mock
+    NotificationManager mNotificationManager;
 
     private static final String MAC_ADDRESS = "00:11:22:33:44:55";
     private static final byte[] ACCOUNT_KEY = new byte[]{0x01, 0x02};
@@ -81,6 +90,9 @@
         mLocator.overrideBindingForTest(FastPairCacheManager.class,
                 mFastPairCacheManager);
         mLocator.overrideBindingForTest(Clock.class, mClock);
+        when(mContextWrapper.getResources()).thenReturn(mResources);
+        HalfSheetResources.setResourcesContextForTest(mContextWrapper);
+
         sBluetoothDevice =
                 BluetoothAdapter.getDefaultAdapter().getRemoteDevice("00:11:22:33:44:55");
         sDiscoveryItem = FakeDiscoveryItems.newFastPairDiscoveryItem(mContextWrapper);
@@ -205,10 +217,17 @@
 
     private PairingProgressHandlerBase createProgressHandler(
             @Nullable byte[] accountKey, DiscoveryItem fastPairItem, boolean isRetroactivePair) {
-        FastPairNotificationManager fastPairNotificationManager =
-                new FastPairNotificationManager(mContextWrapper, fastPairItem, true);
+
         FastPairHalfSheetManager fastPairHalfSheetManager =
                 new FastPairHalfSheetManager(mContextWrapper);
+        // Real context is needed
+        Context context = InstrumentationRegistry.getInstrumentation().getContext();
+        FastPairNotificationManager fastPairNotificationManager =
+                new FastPairNotificationManager(context, 1234, mNotificationManager,
+                        new HalfSheetResources(mContextWrapper));
+
+        mLocator.overrideBindingForTest(FastPairNotificationManager.class,
+                fastPairNotificationManager);
         mLocator.overrideBindingForTest(FastPairHalfSheetManager.class, fastPairHalfSheetManager);
         PairingProgressHandlerBase pairingProgressHandlerBase =
                 PairingProgressHandlerBase.create(