Merge "Send BLE callback after phone is unlocked if the app is not directBootAware." into main
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index edf9fd1..2d59309 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -23,7 +23,6 @@
 import static android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED;
 import static android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE;
 import static android.Manifest.permission.USE_COMPANION_TRANSPORTS;
-import static android.companion.DevicePresenceEvent.EVENT_BT_CONNECTED;
 import static android.content.pm.PackageManager.CERT_INPUT_SHA256;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.os.Process.SYSTEM_UID;
@@ -53,7 +52,6 @@
 import android.app.AppOpsManager;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
-import android.bluetooth.BluetoothDevice;
 import android.companion.AssociationInfo;
 import android.companion.AssociationRequest;
 import android.companion.IAssociationRequestCallback;
@@ -76,7 +74,6 @@
 import android.os.Environment;
 import android.os.Parcel;
 import android.os.ParcelFileDescriptor;
-import android.os.ParcelUuid;
 import android.os.PowerExemptionManager;
 import android.os.PowerManagerInternal;
 import android.os.RemoteException;
@@ -118,9 +115,7 @@
 import java.io.File;
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
-import java.util.Arrays;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 
@@ -250,38 +245,9 @@
 
     @Override
     public void onUserUnlocked(@NonNull TargetUser user) {
+        Slog.i(TAG, "onUserUnlocked() user=" + user);
         // Notify and bind the app after the phone is unlocked.
-        final int userId = user.getUserIdentifier();
-        final Set<BluetoothDevice> blueToothDevices =
-                mDevicePresenceProcessor.getPendingConnectedDevices().get(userId);
-
-        final List<ObservableUuid> observableUuids =
-                mObservableUuidStore.getObservableUuidsForUser(userId);
-
-        if (blueToothDevices != null) {
-            for (BluetoothDevice bluetoothDevice : blueToothDevices) {
-                final ParcelUuid[] bluetoothDeviceUuids = bluetoothDevice.getUuids();
-
-                final List<ParcelUuid> deviceUuids = ArrayUtils.isEmpty(bluetoothDeviceUuids)
-                        ? Collections.emptyList() : Arrays.asList(bluetoothDeviceUuids);
-
-                for (AssociationInfo ai :
-                        mAssociationStore.getActiveAssociationsByAddress(
-                                bluetoothDevice.getAddress())) {
-                    Slog.i(TAG, "onUserUnlocked, device id( " + ai.getId() + " ) is connected");
-                    mDevicePresenceProcessor.onBluetoothCompanionDeviceConnected(ai.getId());
-                }
-
-                for (ObservableUuid observableUuid : observableUuids) {
-                    if (deviceUuids.contains(observableUuid.getUuid())) {
-                        Slog.i(TAG, "onUserUnlocked, UUID( "
-                                + observableUuid.getUuid() + " ) is connected");
-                        mDevicePresenceProcessor.onDevicePresenceEventByUuid(
-                                observableUuid, EVENT_BT_CONNECTED);
-                    }
-                }
-            }
-        }
+        mDevicePresenceProcessor.sendDevicePresenceEventOnUnlocked(user.getUserIdentifier());
     }
 
     private void onPackageRemoveOrDataClearedInternal(
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
index a789384..daa8fdb 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
@@ -80,25 +80,6 @@
         final int associationId;
 
         try {
-            if ("simulate-device-event".equals(cmd) && Flags.devicePresence()) {
-                associationId = getNextIntArgRequired();
-                int event = getNextIntArgRequired();
-                mDevicePresenceProcessor.simulateDeviceEvent(associationId, event);
-                return 0;
-            }
-
-            if ("simulate-device-uuid-event".equals(cmd) && Flags.devicePresence()) {
-                String uuid = getNextArgRequired();
-                String packageName = getNextArgRequired();
-                int userId = getNextIntArgRequired();
-                int event = getNextIntArgRequired();
-                ObservableUuid observableUuid = new ObservableUuid(
-                        userId, ParcelUuid.fromString(uuid), packageName,
-                        System.currentTimeMillis());
-                mDevicePresenceProcessor.simulateDeviceEventByUuid(observableUuid, event);
-                return 0;
-            }
-
             switch (cmd) {
                 case "list": {
                     final int userId = getNextIntArgRequired();
@@ -167,6 +148,51 @@
                     mDevicePresenceProcessor.simulateDeviceEvent(associationId, /* event */ 1);
                     break;
 
+                case "simulate-device-event": {
+                    if (Flags.devicePresence()) {
+                        associationId = getNextIntArgRequired();
+                        int event = getNextIntArgRequired();
+                        mDevicePresenceProcessor.simulateDeviceEvent(associationId, event);
+                    }
+                    break;
+                }
+
+                case "simulate-device-uuid-event": {
+                    if (Flags.devicePresence()) {
+                        String uuid = getNextArgRequired();
+                        String packageName = getNextArgRequired();
+                        int userId = getNextIntArgRequired();
+                        int event = getNextIntArgRequired();
+                        ObservableUuid observableUuid = new ObservableUuid(
+                                userId, ParcelUuid.fromString(uuid), packageName,
+                                System.currentTimeMillis());
+                        mDevicePresenceProcessor.simulateDeviceEventByUuid(observableUuid, event);
+                    }
+                    break;
+                }
+
+                case "simulate-device-event-device-locked": {
+                    if (Flags.devicePresence()) {
+                        associationId = getNextIntArgRequired();
+                        int userId = getNextIntArgRequired();
+                        int event = getNextIntArgRequired();
+                        String uuid = getNextArgRequired();
+                        ParcelUuid parcelUuid =
+                                uuid.equals("null") ? null : ParcelUuid.fromString(uuid);
+                        mDevicePresenceProcessor.simulateDeviceEventOnDeviceLocked(
+                                associationId, userId, event, parcelUuid);
+                    }
+                    break;
+                }
+
+                case "simulate-device-event-device-unlocked": {
+                    if (Flags.devicePresence()) {
+                        int userId = getNextIntArgRequired();
+                        mDevicePresenceProcessor.simulateDeviceEventOnUserUnlocked(userId);
+                    }
+                    break;
+                }
+
                 case "get-backup-payload": {
                     final int userId = getNextIntArgRequired();
                     byte[] payload = mBackupRestoreProcessor.getBackupPayload(userId);
@@ -478,6 +504,17 @@
             pw.println("      Make CDM act as if the given DEVICE is BT disconnected base"
                     + "on the UUID");
             pw.println("      USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY.");
+
+            pw.println("  simulate-device-event-device-locked"
+                    + " ASSOCIATION_ID USER_ID DEVICE_EVENT PARCEL_UUID");
+            pw.println("  Simulate device event when the device is locked");
+            pw.println("  USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY.");
+
+            pw.println("  simulate-device-event-device-unlocked USER_ID");
+            pw.println("  Simulate device unlocked for given user. This will send corresponding");
+            pw.println("  callback after simulate-device-event-device-locked");
+            pw.println("  command has been called.");
+            pw.println("  USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY.");
         }
 
         pw.println("  remove-inactive-associations");
diff --git a/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java b/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java
index 9c37881..407b9da 100644
--- a/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java
+++ b/services/companion/java/com/android/server/companion/presence/BleCompanionDeviceScanner.java
@@ -73,9 +73,9 @@
     private static final String TAG = "CDM_BleCompanionDeviceScanner";
 
     interface Callback {
-        void onBleCompanionDeviceFound(int associationId);
+        void onBleCompanionDeviceFound(int associationId, int userId);
 
-        void onBleCompanionDeviceLost(int associationId);
+        void onBleCompanionDeviceLost(int associationId, int userId);
     }
 
     private final @NonNull AssociationStore mAssociationStore;
@@ -259,7 +259,7 @@
         if (DEBUG) Log.d(TAG, "  > associations=" + Arrays.toString(associations.toArray()));
 
         for (AssociationInfo association : associations) {
-            mCallback.onBleCompanionDeviceFound(association.getId());
+            mCallback.onBleCompanionDeviceFound(association.getId(), association.getUserId());
         }
     }
 
@@ -272,7 +272,7 @@
         if (DEBUG) Log.d(TAG, "  > associations=" + Arrays.toString(associations.toArray()));
 
         for (AssociationInfo association : associations) {
-            mCallback.onBleCompanionDeviceLost(association.getId());
+            mCallback.onBleCompanionDeviceLost(association.getId(), association.getUserId());
         }
     }
 
diff --git a/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java b/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java
index 2d345c4..e1a8db4 100644
--- a/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java
+++ b/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java
@@ -34,20 +34,15 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.Log;
-import android.util.Slog;
-import android.util.SparseArray;
 
-import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.ArrayUtils;
 import com.android.server.companion.association.AssociationStore;
 
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 
 @SuppressLint("LongLogTag")
 public class BluetoothCompanionDeviceConnectionListener
@@ -56,14 +51,13 @@
     private static final String TAG = "CDM_BluetoothCompanionDeviceConnectionListener";
 
     interface Callback {
-        void onBluetoothCompanionDeviceConnected(int associationId);
+        void onBluetoothCompanionDeviceConnected(int associationId, int userId);
 
-        void onBluetoothCompanionDeviceDisconnected(int associationId);
+        void onBluetoothCompanionDeviceDisconnected(int associationId, int userId);
 
         void onDevicePresenceEventByUuid(ObservableUuid uuid, int event);
     }
 
-    private final UserManager mUserManager;
     private final @NonNull AssociationStore mAssociationStore;
     private final @NonNull Callback mCallback;
     /** A set of ALL connected BT device (not only companion.) */
@@ -71,21 +65,12 @@
 
     private final @NonNull ObservableUuidStore mObservableUuidStore;
 
-    /**
-     * A structure hold the connected BT devices that are pending to be reported to the companion
-     * app when the user unlocks the local device per userId.
-     */
-    @GuardedBy("mPendingConnectedDevices")
-    @NonNull
-    final SparseArray<Set<BluetoothDevice>> mPendingConnectedDevices = new SparseArray<>();
-
     BluetoothCompanionDeviceConnectionListener(UserManager userManager,
             @NonNull AssociationStore associationStore,
             @NonNull ObservableUuidStore observableUuidStore, @NonNull Callback callback) {
         mAssociationStore = associationStore;
         mObservableUuidStore = observableUuidStore;
         mCallback = callback;
-        mUserManager = userManager;
     }
 
     public void init(@NonNull BluetoothAdapter btAdapter) {
@@ -111,19 +96,8 @@
             if (DEBUG) Log.w(TAG, "Device " + btDeviceToString(device) + " is already connected.");
             return;
         }
-        // Try to bind and notify the app after the phone is unlocked.
-        if (!mUserManager.isUserUnlockingOrUnlocked(UserHandle.myUserId())) {
-            Slog.i(TAG, "Current user is not in unlocking or unlocked stage yet. Notify "
-                        + "the application when the phone is unlocked");
-            synchronized (mPendingConnectedDevices) {
-                Set<BluetoothDevice> bluetoothDevices = mPendingConnectedDevices.get(
-                        userId, new HashSet<>());
-                bluetoothDevices.add(device);
-                mPendingConnectedDevices.put(userId, bluetoothDevices);
-            }
-        } else {
-            onDeviceConnectivityChanged(device, true);
-        }
+
+        onDeviceConnectivityChanged(device, true);
     }
 
     /**
@@ -149,19 +123,6 @@
             return;
         }
 
-        // Do not need to report the connectivity since the user is not unlock the phone so
-        // that cdm is not bind with the app yet.
-        if (!mUserManager.isUserUnlockingOrUnlocked(userId)) {
-            synchronized (mPendingConnectedDevices) {
-                Set<BluetoothDevice> bluetoothDevices = mPendingConnectedDevices.get(userId);
-                if (bluetoothDevices != null) {
-                    bluetoothDevices.remove(device);
-                }
-            }
-
-            return;
-        }
-
         onDeviceConnectivityChanged(device, false);
     }
 
@@ -190,16 +151,15 @@
             if (!association.isNotifyOnDeviceNearby()) continue;
             final int id = association.getId();
             if (connected) {
-                mCallback.onBluetoothCompanionDeviceConnected(id);
+                mCallback.onBluetoothCompanionDeviceConnected(id, association.getUserId());
             } else {
-                mCallback.onBluetoothCompanionDeviceDisconnected(id);
+                mCallback.onBluetoothCompanionDeviceDisconnected(id, association.getUserId());
             }
         }
 
         for (ObservableUuid uuid : observableUuids) {
             if (deviceUuids.contains(uuid.getUuid())) {
-                mCallback.onDevicePresenceEventByUuid(
-                        uuid, connected ? EVENT_BT_CONNECTED
+                mCallback.onDevicePresenceEventByUuid(uuid, connected ? EVENT_BT_CONNECTED
                                 : EVENT_BT_DISCONNECTED);
             }
         }
@@ -210,7 +170,8 @@
         if (DEBUG) Log.d(TAG, "onAssociation_Added() " + association);
 
         if (mAllConnectedDevices.containsKey(association.getDeviceMacAddress())) {
-            mCallback.onBluetoothCompanionDeviceConnected(association.getId());
+            mCallback.onBluetoothCompanionDeviceConnected(
+                    association.getId(), association.getUserId());
         }
     }
 
diff --git a/services/companion/java/com/android/server/companion/presence/DevicePresenceProcessor.java b/services/companion/java/com/android/server/companion/presence/DevicePresenceProcessor.java
index 642460e..092886c 100644
--- a/services/companion/java/com/android/server/companion/presence/DevicePresenceProcessor.java
+++ b/services/companion/java/com/android/server/companion/presence/DevicePresenceProcessor.java
@@ -35,7 +35,6 @@
 import android.annotation.TestApi;
 import android.annotation.UserIdInt;
 import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothDevice;
 import android.companion.AssociationInfo;
 import android.companion.DeviceNotAssociatedException;
 import android.companion.DevicePresenceEvent;
@@ -59,8 +58,10 @@
 import com.android.server.companion.association.AssociationStore;
 
 import java.io.PrintWriter;
+import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Objects;
 import java.util.Set;
 
 /**
@@ -97,6 +98,8 @@
     private final BleCompanionDeviceScanner mBleScanner;
     @NonNull
     private final PowerManagerInternal mPowerManagerInternal;
+    @NonNull
+    private final UserManager mUserManager;
 
     // NOTE: Same association may appear in more than one of the following sets at the same time.
     // (E.g. self-managed devices that have MAC addresses, could be reported as present by their
@@ -129,6 +132,14 @@
     private final BleDeviceDisappearedScheduler mBleDeviceDisappearedScheduler =
             new BleDeviceDisappearedScheduler();
 
+    /**
+     * A structure hold the DevicePresenceEvents that are pending to be reported to the companion
+     * app when the user unlocks the local device per userId.
+     */
+    @GuardedBy("mPendingDevicePresenceEvents")
+    public final SparseArray<List<DevicePresenceEvent>> mPendingDevicePresenceEvents =
+            new SparseArray<>();
+
     public DevicePresenceProcessor(@NonNull Context context,
             @NonNull CompanionAppBinder companionAppBinder,
             UserManager userManager,
@@ -139,6 +150,7 @@
         mCompanionAppBinder = companionAppBinder;
         mAssociationStore = associationStore;
         mObservableUuidStore = observableUuidStore;
+        mUserManager = userManager;
         mBtConnectionListener = new BluetoothCompanionDeviceConnectionListener(userManager,
                 associationStore, mObservableUuidStore,
                 /* BluetoothCompanionDeviceConnectionListener.Callback */ this);
@@ -461,7 +473,14 @@
     }
 
     @Override
-    public void onBluetoothCompanionDeviceConnected(int associationId) {
+    public void onBluetoothCompanionDeviceConnected(int associationId, int userId) {
+        Slog.i(TAG, "onBluetoothCompanionDeviceConnected: "
+                + "associationId( " + associationId + " )");
+        if (!mUserManager.isUserUnlockingOrUnlocked(userId)) {
+            onDeviceLocked(associationId, userId, EVENT_BT_CONNECTED, /* ParcelUuid */ null);
+            return;
+        }
+
         synchronized (mBtDisconnectedDevices) {
             // A device is considered reconnected within 10 seconds if a pending BLE lost report is
             // followed by a detected Bluetooth connection.
@@ -484,9 +503,15 @@
     }
 
     @Override
-    public void onBluetoothCompanionDeviceDisconnected(int associationId) {
+    public void onBluetoothCompanionDeviceDisconnected(int associationId, int userId) {
         Slog.i(TAG, "onBluetoothCompanionDeviceDisconnected "
                 + "associationId( " + associationId + " )");
+
+        if (!mUserManager.isUserUnlockingOrUnlocked(userId)) {
+            onDeviceLocked(associationId, userId, EVENT_BT_DISCONNECTED, /* ParcelUuid */ null);
+            return;
+        }
+
         // Start BLE scanning when the device is disconnected.
         mBleScanner.startScan();
 
@@ -503,7 +528,13 @@
 
 
     @Override
-    public void onBleCompanionDeviceFound(int associationId) {
+    public void onBleCompanionDeviceFound(int associationId, int userId) {
+        Slog.i(TAG, "onBleCompanionDeviceFound " + "associationId( " + associationId + " )");
+        if (!mUserManager.isUserUnlockingOrUnlocked(userId)) {
+            onDeviceLocked(associationId, userId, EVENT_BLE_APPEARED, /* ParcelUuid */ null);
+            return;
+        }
+
         onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_APPEARED);
         synchronized (mBtDisconnectedDevices) {
             final boolean isCurrentPresent = mBtDisconnectedDevicesBlePresence.get(associationId);
@@ -514,7 +545,13 @@
     }
 
     @Override
-    public void onBleCompanionDeviceLost(int associationId) {
+    public void onBleCompanionDeviceLost(int associationId, int userId) {
+        Slog.i(TAG, "onBleCompanionDeviceLost " + "associationId( " + associationId + " )");
+        if (!mUserManager.isUserUnlockingOrUnlocked(userId)) {
+            onDeviceLocked(associationId, userId, EVENT_BLE_APPEARED, /* ParcelUuid */ null);
+            return;
+        }
+
         onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_DISAPPEARED);
     }
 
@@ -529,18 +566,20 @@
         // Make sure the association exists.
         enforceAssociationExists(associationId);
 
+        final AssociationInfo associationInfo = mAssociationStore.getAssociationById(associationId);
+
         switch (event) {
             case EVENT_BLE_APPEARED:
                 simulateDeviceAppeared(associationId, event);
                 break;
             case EVENT_BT_CONNECTED:
-                onBluetoothCompanionDeviceConnected(associationId);
+                onBluetoothCompanionDeviceConnected(associationId, associationInfo.getUserId());
                 break;
             case EVENT_BLE_DISAPPEARED:
                 simulateDeviceDisappeared(associationId, event);
                 break;
             case EVENT_BT_DISCONNECTED:
-                onBluetoothCompanionDeviceDisconnected(associationId);
+                onBluetoothCompanionDeviceDisconnected(associationId, associationInfo.getUserId());
                 break;
             default:
                 throw new IllegalArgumentException("Event: " + event + "is not supported");
@@ -558,6 +597,29 @@
         onDevicePresenceEventByUuid(uuid, event);
     }
 
+    /** FOR DEBUGGING AND/OR TESTING PURPOSES ONLY. */
+    @TestApi
+    public void simulateDeviceEventOnDeviceLocked(
+            int associationId, int userId, int event, ParcelUuid uuid) {
+        // IMPORTANT: this API should only be invoked via the
+        // 'companiondevice simulate-device-event-device-locked' Shell command,
+        // so the only uid-s allowed to make this call are SHELL and ROOT.
+        // No other caller (including SYSTEM!) should be allowed.
+        enforceCallerShellOrRoot();
+        onDeviceLocked(associationId, userId, event, uuid);
+    }
+
+    /** FOR DEBUGGING AND/OR TESTING PURPOSES ONLY. */
+    @TestApi
+    public void simulateDeviceEventOnUserUnlocked(int userId) {
+        // IMPORTANT: this API should only be invoked via the
+        // 'companiondevice simulate-device-event-device-unlocked' Shell command,
+        // so the only uid-s allowed to make this call are SHELL and ROOT.
+        // No other caller (including SYSTEM!) should be allowed.
+        enforceCallerShellOrRoot();
+        sendDevicePresenceEventOnUnlocked(userId);
+    }
+
     private void simulateDeviceAppeared(int associationId, int state) {
         onDevicePresenceEvent(mSimulated, associationId, state);
         mSchedulerHelper.scheduleOnDeviceGoneCallForSimulatedDevicePresence(associationId);
@@ -660,8 +722,13 @@
                 + "]...");
 
         final ParcelUuid parcelUuid = uuid.getUuid();
-        final String packageName = uuid.getPackageName();
         final int userId = uuid.getUserId();
+        if (!mUserManager.isUserUnlockingOrUnlocked(userId)) {
+            onDeviceLocked(/* associationId */ -1, userId, eventType, parcelUuid);
+            return;
+        }
+
+        final String packageName = uuid.getPackageName();
         final DevicePresenceEvent event = new DevicePresenceEvent(NO_ASSOCIATION, eventType,
                 parcelUuid);
 
@@ -882,14 +949,6 @@
         // what's needed.
     }
 
-    /**
-     * Return a set of devices that pending to report connectivity
-     */
-    public SparseArray<Set<BluetoothDevice>> getPendingConnectedDevices() {
-        synchronized (mBtConnectionListener.mPendingConnectedDevices) {
-            return mBtConnectionListener.mPendingConnectedDevices;
-        }
-    }
 
     private static void enforceCallerShellOrRoot() {
         final int callingUid = Binder.getCallingUid();
@@ -919,6 +978,114 @@
     }
 
     /**
+     * Store the positive DevicePresenceEvent in the cache if the current device is still
+     * locked.
+     * Remove the current DevicePresenceEvent if there's a negative event occurs.
+     */
+    private void onDeviceLocked(int associationId, int userId, int event, ParcelUuid uuid) {
+        switch (event) {
+            case EVENT_BLE_APPEARED, EVENT_BT_CONNECTED -> {
+                // Try to bind and notify the app after the phone is unlocked.
+                Slog.i(TAG, "Current user is not in unlocking or unlocked stage yet. "
+                        + "Notify the application when the phone is unlocked");
+                synchronized (mPendingDevicePresenceEvents) {
+                    final DevicePresenceEvent devicePresenceEvent = new DevicePresenceEvent(
+                            associationId, event, uuid);
+                    List<DevicePresenceEvent> deviceEvents = mPendingDevicePresenceEvents.get(
+                            userId, new ArrayList<>());
+                    deviceEvents.add(devicePresenceEvent);
+                    mPendingDevicePresenceEvents.put(userId, deviceEvents);
+                }
+            }
+            case EVENT_BLE_DISAPPEARED -> {
+                synchronized (mPendingDevicePresenceEvents) {
+                    List<DevicePresenceEvent> deviceEvents = mPendingDevicePresenceEvents
+                            .get(userId);
+                    if (deviceEvents != null) {
+                        deviceEvents.removeIf(deviceEvent ->
+                                deviceEvent.getEvent() == EVENT_BLE_APPEARED
+                                && Objects.equals(deviceEvent.getUuid(), uuid)
+                                && deviceEvent.getAssociationId() == associationId);
+                    }
+                }
+            }
+            case EVENT_BT_DISCONNECTED -> {
+                // Do not need to report the event since the user is not unlock the
+                // phone so that cdm is not bind with the app yet.
+                synchronized (mPendingDevicePresenceEvents) {
+                    List<DevicePresenceEvent> deviceEvents = mPendingDevicePresenceEvents
+                            .get(userId);
+                    if (deviceEvents != null) {
+                        deviceEvents.removeIf(deviceEvent ->
+                                deviceEvent.getEvent() == EVENT_BT_CONNECTED
+                                && Objects.equals(deviceEvent.getUuid(), uuid)
+                                && deviceEvent.getAssociationId() == associationId);
+                    }
+                }
+            }
+            default -> Slog.e(TAG, "Event: " + event + "is not supported");
+        }
+    }
+
+    /**
+     * Send the device presence event by userID when the device is unlocked.
+     */
+    public void sendDevicePresenceEventOnUnlocked(int userId) {
+        final List<DevicePresenceEvent> deviceEvents = getPendingDevicePresenceEventsByUserId(
+                userId);
+        final List<ObservableUuid> observableUuids =
+                mObservableUuidStore.getObservableUuidsForUser(userId);
+        // Notify and bind the app after the phone is unlocked.
+        for (DevicePresenceEvent deviceEvent : deviceEvents) {
+            boolean isUuid = deviceEvent.getUuid() != null;
+            if (isUuid) {
+                for (ObservableUuid uuid : observableUuids) {
+                    if (uuid.getUuid().equals(deviceEvent.getUuid())) {
+                        onDevicePresenceEventByUuid(uuid, EVENT_BT_CONNECTED);
+                    }
+                }
+            } else {
+                int event = deviceEvent.getEvent();
+                int associationId = deviceEvent.getAssociationId();
+                final AssociationInfo associationInfo = mAssociationStore.getAssociationById(
+                        associationId);
+
+                if (associationInfo == null) {
+                    return;
+                }
+
+                switch(event) {
+                    case EVENT_BLE_APPEARED:
+                        onBleCompanionDeviceFound(
+                                associationInfo.getId(), associationInfo.getUserId());
+                        break;
+                    case EVENT_BT_CONNECTED:
+                        onBluetoothCompanionDeviceConnected(
+                                associationInfo.getId(), associationInfo.getUserId());
+                        break;
+                    default:
+                        Slog.e(TAG, "Event: " + event + "is not supported");
+                        break;
+                }
+            }
+        }
+
+        clearPendingDevicePresenceEventsByUserId(userId);
+    }
+
+    private List<DevicePresenceEvent> getPendingDevicePresenceEventsByUserId(int userId) {
+        synchronized (mPendingDevicePresenceEvents) {
+            return mPendingDevicePresenceEvents.get(userId, new ArrayList<>());
+        }
+    }
+
+    private void clearPendingDevicePresenceEventsByUserId(int userId) {
+        synchronized (mPendingDevicePresenceEvents) {
+            mPendingDevicePresenceEvents.get(userId).clear();
+        }
+    }
+
+    /**
      * Dumps system information about devices that are marked as "present".
      */
     public void dump(@NonNull PrintWriter out) {