Introduce CDM device presence base on UUID

1. Introduce a new API startObservingDevicePresence` that
able to observing the devices base on association id or
uuid.

2. Introduce a new request called DevicePresenceRequest allows
app to be able setUuid or setAssociationId. Not that,
caller can only use one setter. And only system app is able
to use setUuid.

2. Introduce a new callback that that system app
will be recieve when device is connected / disconnected.

3. Introduce a new Permission REQUEST_OBSERVE_DEVICE_UUID_PRESENCE
that with signature protection level.

4. Introduce a new ObservingDevicePresenceResult with
a new callback onDeviceEvent(ObservingDevicePresenceResult)

5. Deprecate the startObservingDevicePresence(MacAddress).

Test: cts
Bug: 316015619

Change-Id: I4e311866d7640218ed6dd9937911f984e88355ef
diff --git a/core/api/current.txt b/core/api/current.txt
index 15c054f..44eda70 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -273,6 +273,7 @@
     field public static final String REQUEST_IGNORE_BATTERY_OPTIMIZATIONS = "android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS";
     field public static final String REQUEST_INSTALL_PACKAGES = "android.permission.REQUEST_INSTALL_PACKAGES";
     field public static final String REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE = "android.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE";
+    field @FlaggedApi("android.companion.flags.device_presence") public static final String REQUEST_OBSERVE_DEVICE_UUID_PRESENCE = "android.permission.REQUEST_OBSERVE_DEVICE_UUID_PRESENCE";
     field public static final String REQUEST_PASSWORD_COMPLEXITY = "android.permission.REQUEST_PASSWORD_COMPLEXITY";
     field @Deprecated public static final String RESTART_PACKAGES = "android.permission.RESTART_PACKAGES";
     field public static final String RUN_USER_INITIATED_JOBS = "android.permission.RUN_USER_INITIATED_JOBS";
@@ -9694,8 +9695,10 @@
     method public void requestNotificationAccess(android.content.ComponentName);
     method @FlaggedApi("android.companion.association_tag") public void setAssociationTag(int, @NonNull String);
     method @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) public void startObservingDevicePresence(@NonNull String) throws android.companion.DeviceNotAssociatedException;
+    method @FlaggedApi("android.companion.device_presence") @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) public void startObservingDevicePresence(@NonNull android.companion.ObservingDevicePresenceRequest);
     method public void startSystemDataTransfer(int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.companion.CompanionException>) throws android.companion.DeviceNotAssociatedException;
     method @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) public void stopObservingDevicePresence(@NonNull String) throws android.companion.DeviceNotAssociatedException;
+    method @FlaggedApi("android.companion.device_presence") @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) public void stopObservingDevicePresence(@NonNull android.companion.ObservingDevicePresenceRequest);
     field public static final String EXTRA_ASSOCIATION = "android.companion.extra.ASSOCIATION";
     field @Deprecated public static final String EXTRA_DEVICE = "android.companion.extra.DEVICE";
     field public static final int FLAG_CALL_METADATA = 1; // 0x1
@@ -9723,13 +9726,7 @@
     method @MainThread public void onDeviceAppeared(@NonNull android.companion.AssociationInfo);
     method @Deprecated @MainThread public void onDeviceDisappeared(@NonNull String);
     method @MainThread public void onDeviceDisappeared(@NonNull android.companion.AssociationInfo);
-    method @FlaggedApi("android.companion.device_presence") @MainThread public void onDeviceEvent(@NonNull android.companion.AssociationInfo, int);
-    field @FlaggedApi("android.companion.device_presence") public static final int DEVICE_EVENT_BLE_APPEARED = 0; // 0x0
-    field @FlaggedApi("android.companion.device_presence") public static final int DEVICE_EVENT_BLE_DISAPPEARED = 1; // 0x1
-    field @FlaggedApi("android.companion.device_presence") public static final int DEVICE_EVENT_BT_CONNECTED = 2; // 0x2
-    field @FlaggedApi("android.companion.device_presence") public static final int DEVICE_EVENT_BT_DISCONNECTED = 3; // 0x3
-    field @FlaggedApi("android.companion.device_presence") public static final int DEVICE_EVENT_SELF_MANAGED_APPEARED = 4; // 0x4
-    field @FlaggedApi("android.companion.device_presence") public static final int DEVICE_EVENT_SELF_MANAGED_DISAPPEARED = 5; // 0x5
+    method @FlaggedApi("android.companion.device_presence") @MainThread public void onDevicePresenceEvent(@NonNull android.companion.DevicePresenceEvent);
     field public static final String SERVICE_INTERFACE = "android.companion.CompanionDeviceService";
   }
 
@@ -9742,6 +9739,38 @@
   public class DeviceNotAssociatedException extends java.lang.RuntimeException {
   }
 
+  @FlaggedApi("android.companion.device_presence") public final class DevicePresenceEvent implements android.os.Parcelable {
+    ctor public DevicePresenceEvent(int, int, @Nullable android.os.ParcelUuid);
+    method public int describeContents();
+    method public int getAssociationId();
+    method public int getEvent();
+    method @Nullable public android.os.ParcelUuid getUuid();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.companion.DevicePresenceEvent> CREATOR;
+    field public static final int EVENT_BLE_APPEARED = 0; // 0x0
+    field public static final int EVENT_BLE_DISAPPEARED = 1; // 0x1
+    field public static final int EVENT_BT_CONNECTED = 2; // 0x2
+    field public static final int EVENT_BT_DISCONNECTED = 3; // 0x3
+    field public static final int EVENT_SELF_MANAGED_APPEARED = 4; // 0x4
+    field public static final int EVENT_SELF_MANAGED_DISAPPEARED = 5; // 0x5
+    field public static final int NO_ASSOCIATION = -1; // 0xffffffff
+  }
+
+  @FlaggedApi("android.companion.device_presence") public final class ObservingDevicePresenceRequest implements android.os.Parcelable {
+    method public int describeContents();
+    method public int getAssociationId();
+    method @Nullable public android.os.ParcelUuid getUuid();
+    method public void writeToParcel(@NonNull android.os.Parcel, int);
+    field @NonNull public static final android.os.Parcelable.Creator<android.companion.ObservingDevicePresenceRequest> CREATOR;
+  }
+
+  public static final class ObservingDevicePresenceRequest.Builder {
+    ctor public ObservingDevicePresenceRequest.Builder();
+    method @NonNull public android.companion.ObservingDevicePresenceRequest build();
+    method @NonNull public android.companion.ObservingDevicePresenceRequest.Builder setAssociationId(int);
+    method @NonNull @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_DEVICE_UUID_PRESENCE) public android.companion.ObservingDevicePresenceRequest.Builder setUuid(@NonNull android.os.ParcelUuid);
+  }
+
   public final class WifiDeviceFilter implements android.companion.DeviceFilter<android.net.wifi.ScanResult> {
     method public int describeContents();
     method public void writeToParcel(@NonNull android.os.Parcel, int);
diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java
index 879656a..c4b1c72 100644
--- a/core/java/android/companion/CompanionDeviceManager.java
+++ b/core/java/android/companion/CompanionDeviceManager.java
@@ -1061,6 +1061,7 @@
         }
     }
 
+    // TODO(b/315163162) Add @Deprecated keyword after 24Q2 cut.
     /**
      * Register to receive callbacks whenever the associated device comes in and out of range.
      *
@@ -1117,7 +1118,7 @@
                             callingUid, callingPid);
         }
     }
-
+    // TODO(b/315163162) Add @Deprecated keyword after 24Q2 cut.
     /**
      * Unregister for receiving callbacks whenever the associated device comes in and out of range.
      *
@@ -1160,6 +1161,64 @@
     }
 
     /**
+     * Register to receive callbacks whenever the associated device comes in and out of range.
+     *
+     * <p>The app doesn't need to remain running in order to receive its callbacks.</p>
+     *
+     * <p>Calling app must check for feature presence of
+     * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} before calling this API.</p>
+     *
+     * <p>For Bluetooth LE devices, this is based on scanning for device with the given address.
+     * The system will scan for the device when Bluetooth is ON or Bluetooth scanning is ON.</p>
+     *
+     * <p>For Bluetooth classic devices this is triggered when the device connects/disconnects.</p>
+     *
+     * <p>WiFi devices are not supported.</p>
+     *
+     * <p>If a Bluetooth LE device wants to use a rotating mac address, it is recommended to use
+     * Resolvable Private Address, and ensure the device is bonded to the phone so that android OS
+     * is able to resolve the address.</p>
+     *
+     * @param request A request for setting the types of device for observing device presence.
+     *
+     * @see ObservingDevicePresenceRequest.Builder
+     * @see CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)
+     */
+    @FlaggedApi(Flags.FLAG_DEVICE_PRESENCE)
+    @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE)
+    public void startObservingDevicePresence(@NonNull ObservingDevicePresenceRequest request) {
+        Objects.requireNonNull(request, "request cannot be null");
+
+        try {
+            mService.startObservingDevicePresence(
+                    request, mContext.getOpPackageName(), mContext.getUserId());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Unregister for receiving callbacks whenever the associated device comes in and out of range.
+     *
+     * Calling app must check for feature presence of
+     * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} before calling this API.
+     *
+     * @param request A request for setting the types of device for observing device presence.
+     */
+    @FlaggedApi(Flags.FLAG_DEVICE_PRESENCE)
+    @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE)
+    public void stopObservingDevicePresence(@NonNull ObservingDevicePresenceRequest request) {
+        Objects.requireNonNull(request, "request cannot be null");
+
+        try {
+            mService.stopObservingDevicePresence(
+                    request, mContext.getOpPackageName(), mContext.getUserId());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Dispatch a message to system for processing. It should only be called by
      * {@link CompanionDeviceService#dispatchMessageToSystem(int, int, byte[])}
      *
diff --git a/core/java/android/companion/CompanionDeviceService.java b/core/java/android/companion/CompanionDeviceService.java
index 4d0267c..5ad2348 100644
--- a/core/java/android/companion/CompanionDeviceService.java
+++ b/core/java/android/companion/CompanionDeviceService.java
@@ -18,7 +18,6 @@
 package android.companion;
 
 import android.annotation.FlaggedApi;
-import android.annotation.IntDef;
 import android.annotation.MainThread;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -33,8 +32,6 @@
 
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
 import java.util.Objects;
 import java.util.concurrent.Executor;
 
@@ -123,62 +120,6 @@
      */
     public static final String SERVICE_INTERFACE = "android.companion.CompanionDeviceService";
 
-    /** @hide */
-    @IntDef(prefix = {"DEVICE_EVENT"}, value = {
-            DEVICE_EVENT_BLE_APPEARED,
-            DEVICE_EVENT_BLE_DISAPPEARED,
-            DEVICE_EVENT_BT_CONNECTED,
-            DEVICE_EVENT_BT_DISCONNECTED,
-            DEVICE_EVENT_SELF_MANAGED_APPEARED,
-            DEVICE_EVENT_SELF_MANAGED_DISAPPEARED
-    })
-
-    @Retention(RetentionPolicy.SOURCE)
-    public @interface DeviceEvent {}
-
-    /**
-     * Companion app receives {@link #onDeviceEvent(AssociationInfo, int)} callback
-     * with this event if the device comes into BLE range.
-     */
-    @FlaggedApi(Flags.FLAG_DEVICE_PRESENCE)
-    public static final int DEVICE_EVENT_BLE_APPEARED = 0;
-
-    /**
-     * Companion app receives {@link #onDeviceEvent(AssociationInfo, int)} callback
-     * with this event if the device is no longer in BLE range.
-     */
-    @FlaggedApi(Flags.FLAG_DEVICE_PRESENCE)
-    public static final int DEVICE_EVENT_BLE_DISAPPEARED = 1;
-
-    /**
-     * Companion app receives {@link #onDeviceEvent(AssociationInfo, int)} callback
-     * with this event when the bluetooth device is connected.
-     */
-    @FlaggedApi(Flags.FLAG_DEVICE_PRESENCE)
-    public static final int DEVICE_EVENT_BT_CONNECTED = 2;
-
-    /**
-     * Companion app receives {@link #onDeviceEvent(AssociationInfo, int)} callback
-     * with this event if the bluetooth device is disconnected.
-     */
-    @FlaggedApi(Flags.FLAG_DEVICE_PRESENCE)
-    public static final int DEVICE_EVENT_BT_DISCONNECTED = 3;
-
-    /**
-     * A companion app for a self-managed device will receive the callback
-     * {@link #onDeviceEvent(AssociationInfo, int)} if it reports that a device has appeared on its
-     * own.
-     */
-    @FlaggedApi(Flags.FLAG_DEVICE_PRESENCE)
-    public static final int DEVICE_EVENT_SELF_MANAGED_APPEARED = 4;
-
-    /**
-     * A companion app for a self-managed device will receive the callback
-     * {@link #onDeviceEvent(AssociationInfo, int)} if it reports that a device has disappeared on
-     * its own.
-     */
-    @FlaggedApi(Flags.FLAG_DEVICE_PRESENCE)
-    public static final int DEVICE_EVENT_SELF_MANAGED_DISAPPEARED = 5;
 
     private final Stub mRemote = new Stub();
 
@@ -306,6 +247,7 @@
                 .detachSystemDataTransport(associationId);
     }
 
+    // TODO(b/315163162) Add @Deprecated keyword after 24Q2 cut.
     /**
      * Called by system whenever a device associated with this app is connected.
      *
@@ -318,6 +260,7 @@
         }
     }
 
+    // TODO(b/315163162) Add @Deprecated keyword after 24Q2 cut.
     /**
      * Called by system whenever a device associated with this app is disconnected.
      *
@@ -331,27 +274,13 @@
     }
 
     /**
-     *  Called by the system during device events.
+     * Called by the system during device events.
      *
-     *  <p>E.g. Event {@link #DEVICE_EVENT_BLE_APPEARED} will be called when the associated
-     *  companion device comes into BLE range.
-     *  <p>Event {@link #DEVICE_EVENT_BLE_DISAPPEARED} will be called when the associated
-     *  companion device is no longer in BLE range.
-     *  <p> Event {@link #DEVICE_EVENT_BT_CONNECTED} will be called when the associated
-     *  companion device is connected.
-     *  <p>Event {@link #DEVICE_EVENT_BT_DISCONNECTED} will be called when the associated
-     *  companion device is disconnected.
-     *  Note that app must receive {@link #DEVICE_EVENT_BLE_APPEARED} first before
-     *  {@link #DEVICE_EVENT_BLE_DISAPPEARED} and {@link #DEVICE_EVENT_BT_CONNECTED}
-     *  before {@link #DEVICE_EVENT_BT_DISCONNECTED}.
-     *
-     * @param associationInfo A record for the companion device.
-     * @param event Associated companion device's event.
+     * @see CompanionDeviceManager#startObservingDevicePresence(ObservingDevicePresenceRequest)
      */
     @FlaggedApi(Flags.FLAG_DEVICE_PRESENCE)
     @MainThread
-    public void onDeviceEvent(@NonNull AssociationInfo associationInfo,
-            @DeviceEvent int event) {
+    public void onDevicePresenceEvent(@NonNull DevicePresenceEvent event) {
         // Do nothing. Companion apps can override this function.
     }
 
@@ -390,9 +319,10 @@
         }
 
         @Override
-        public void onDeviceEvent(AssociationInfo associationInfo, int event) {
-            mMainHandler.postAtFrontOfQueue(
-                    () -> mService.onDeviceEvent(associationInfo, event));
+        public void onDevicePresenceEvent(DevicePresenceEvent event) {
+            if (Flags.devicePresence()) {
+                mMainHandler.postAtFrontOfQueue(() -> mService.onDevicePresenceEvent(event));
+            }
         }
     }
 }
diff --git a/core/java/android/companion/DevicePresenceEvent.aidl b/core/java/android/companion/DevicePresenceEvent.aidl
new file mode 100644
index 0000000..1521574
--- /dev/null
+++ b/core/java/android/companion/DevicePresenceEvent.aidl
@@ -0,0 +1,19 @@
+ /*
+  * Copyright (C) 2024 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 android.companion;
+
+ parcelable DevicePresenceEvent;
diff --git a/core/java/android/companion/DevicePresenceEvent.java b/core/java/android/companion/DevicePresenceEvent.java
new file mode 100644
index 0000000..30439a5
--- /dev/null
+++ b/core/java/android/companion/DevicePresenceEvent.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2024 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 android.companion;
+
+import android.annotation.FlaggedApi;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.ParcelUuid;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Event for observing device presence.
+ *
+ * @see CompanionDeviceManager#startObservingDevicePresence(ObservingDevicePresenceRequest)
+ * @see ObservingDevicePresenceRequest.Builder#setUuid(ParcelUuid)
+ * @see ObservingDevicePresenceRequest.Builder#setAssociationId(int)
+ */
+@FlaggedApi(Flags.FLAG_DEVICE_PRESENCE)
+public final class DevicePresenceEvent implements Parcelable {
+
+    /** @hide */
+    @IntDef(prefix = {"EVENT"}, value = {
+            EVENT_BLE_APPEARED,
+            EVENT_BLE_DISAPPEARED,
+            EVENT_BT_CONNECTED,
+            EVENT_BT_DISCONNECTED,
+            EVENT_SELF_MANAGED_APPEARED,
+            EVENT_SELF_MANAGED_DISAPPEARED
+    })
+
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Event {}
+
+    /**
+     * Indicate observing device presence base on the ParcelUuid but not association id.
+     */
+    public static final int NO_ASSOCIATION = -1;
+
+    /**
+     * Companion app receives
+     * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)} callback
+     * with this event if the device comes into BLE range.
+     */
+    public static final int EVENT_BLE_APPEARED = 0;
+
+    /**
+     * Companion app receives
+     * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)} callback
+     * with this event if the device is no longer in BLE range.
+     */
+    public static final int EVENT_BLE_DISAPPEARED = 1;
+
+    /**
+     * Companion app receives
+     * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)} callback
+     * with this event when the bluetooth device is connected.
+     */
+    public static final int EVENT_BT_CONNECTED = 2;
+
+    /**
+     * Companion app receives
+     * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)} callback
+     * with this event if the bluetooth device is disconnected.
+     */
+    public static final int EVENT_BT_DISCONNECTED = 3;
+
+    /**
+     * A companion app for a self-managed device will receive the callback
+     * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)}
+     * if it reports that a device has appeared on its
+     * own.
+     */
+    public static final int EVENT_SELF_MANAGED_APPEARED = 4;
+
+    /**
+     * A companion app for a self-managed device will receive the callback
+     * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)} if it reports
+     * that a device has disappeared on its own.
+     */
+    public static final int EVENT_SELF_MANAGED_DISAPPEARED = 5;
+    private final int mAssociationId;
+    private final int mEvent;
+    @Nullable
+    private final ParcelUuid mUuid;
+
+    private static final int PARCEL_UUID_NULL = 0;
+
+    private static final int PARCEL_UUID_NOT_NULL = 1;
+
+    /**
+     * Create a new DevicePresenceEvent.
+     */
+    public DevicePresenceEvent(
+            int associationId, @Event int event, @Nullable ParcelUuid uuid) {
+        mAssociationId = associationId;
+        mEvent = event;
+        mUuid = uuid;
+    }
+
+    /**
+     * @return The association id has been used to observe device presence.
+     *
+     * Caller will receive the valid association id if only if using
+     * {@link ObservingDevicePresenceRequest.Builder#setAssociationId(int)}, otherwise
+     * return {@link #NO_ASSOCIATION}.
+     *
+     * @see ObservingDevicePresenceRequest.Builder#setAssociationId(int)
+     */
+    public int getAssociationId() {
+        return mAssociationId;
+    }
+
+    /**
+     * @return Associated companion device's event.
+     */
+    public int getEvent() {
+        return mEvent;
+    }
+
+    /**
+     * @return The ParcelUuid has been used to observe device presence.
+     *
+     * Caller will receive the ParcelUuid if only if using
+     * {@link ObservingDevicePresenceRequest.Builder#setUuid(ParcelUuid)}, otherwise return null.
+     *
+     * @see ObservingDevicePresenceRequest.Builder#setUuid(ParcelUuid)
+     */
+
+    @Nullable
+    public ParcelUuid getUuid() {
+        return mUuid;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mAssociationId);
+        dest.writeInt(mEvent);
+        if (mUuid == null) {
+            // Write 0 to the parcel to indicate the ParcelUuid is null.
+            dest.writeInt(PARCEL_UUID_NULL);
+        } else {
+            dest.writeInt(PARCEL_UUID_NOT_NULL);
+            mUuid.writeToParcel(dest, flags);
+        }
+    }
+
+    @Override
+    public boolean equals(@Nullable Object o) {
+        if (this == o) return true;
+        if (!(o instanceof DevicePresenceEvent that)) return false;
+
+        return Objects.equals(mUuid, that.mUuid)
+                && mAssociationId == that.mAssociationId
+                && mEvent == that.mEvent;
+    }
+
+    @Override
+    public String toString() {
+        return "ObservingDevicePresenceResult { "
+                + "Association Id= " + mAssociationId + ","
+                + "ParcelUuid= " + mUuid + ","
+                + "Event= " + mEvent + "}";
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mAssociationId, mEvent, mUuid);
+    }
+
+    @NonNull
+    public static final Parcelable.Creator<DevicePresenceEvent> CREATOR =
+            new Parcelable.Creator<DevicePresenceEvent>() {
+                @Override
+                public DevicePresenceEvent[] newArray(int size) {
+                    return new DevicePresenceEvent[size];
+                }
+
+                @Override
+                public DevicePresenceEvent createFromParcel(@NonNull Parcel in) {
+                    return new DevicePresenceEvent(in);
+                }
+            };
+
+    private DevicePresenceEvent(@NonNull Parcel in) {
+        mAssociationId = in.readInt();
+        mEvent = in.readInt();
+        if (in.readInt() == PARCEL_UUID_NULL) {
+            mUuid = null;
+        } else {
+            mUuid = ParcelUuid.CREATOR.createFromParcel(in);
+        }
+    }
+}
diff --git a/core/java/android/companion/ICompanionDeviceManager.aidl b/core/java/android/companion/ICompanionDeviceManager.aidl
index 22689f3..57d59e5 100644
--- a/core/java/android/companion/ICompanionDeviceManager.aidl
+++ b/core/java/android/companion/ICompanionDeviceManager.aidl
@@ -24,8 +24,11 @@
 import android.companion.ISystemDataTransferCallback;
 import android.companion.AssociationInfo;
 import android.companion.AssociationRequest;
+import android.companion.ObservingDevicePresenceRequest;
 import android.companion.datatransfer.PermissionSyncRequest;
 import android.content.ComponentName;
+import android.os.ParcelUuid;
+
 
 /**
  * Interface for communication with the core companion device manager service.
@@ -132,4 +135,10 @@
     byte[] getBackupPayload(int userId);
 
     void applyRestoredPayload(in byte[] payload, int userId);
+
+    @EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE")
+    void startObservingDevicePresence(in ObservingDevicePresenceRequest request, in String packageName, int userId);
+
+    @EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE")
+    void stopObservingDevicePresence(in ObservingDevicePresenceRequest request, in String packageName, int userId);
 }
diff --git a/core/java/android/companion/ICompanionDeviceService.aidl b/core/java/android/companion/ICompanionDeviceService.aidl
index 2a311bf..f5401d2 100644
--- a/core/java/android/companion/ICompanionDeviceService.aidl
+++ b/core/java/android/companion/ICompanionDeviceService.aidl
@@ -17,10 +17,12 @@
 package android.companion;
 
 import android.companion.AssociationInfo;
+import android.companion.DevicePresenceEvent;
+import android.os.ParcelUuid;
 
 /** @hide */
 oneway interface ICompanionDeviceService {
     void onDeviceAppeared(in AssociationInfo associationInfo);
     void onDeviceDisappeared(in AssociationInfo associationInfo);
-    void onDeviceEvent(in AssociationInfo associationInfo, int state);
+    void onDevicePresenceEvent(in DevicePresenceEvent event);
 }
diff --git a/core/java/android/companion/ObservingDevicePresenceRequest.aidl b/core/java/android/companion/ObservingDevicePresenceRequest.aidl
new file mode 100644
index 0000000..fed0607
--- /dev/null
+++ b/core/java/android/companion/ObservingDevicePresenceRequest.aidl
@@ -0,0 +1,19 @@
+ /*
+  * Copyright (C) 2024 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 android.companion;
+
+ parcelable ObservingDevicePresenceRequest;
\ No newline at end of file
diff --git a/core/java/android/companion/ObservingDevicePresenceRequest.java b/core/java/android/companion/ObservingDevicePresenceRequest.java
new file mode 100644
index 0000000..f1d594e
--- /dev/null
+++ b/core/java/android/companion/ObservingDevicePresenceRequest.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2024 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 android.companion;
+
+import android.annotation.FlaggedApi;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.os.Parcel;
+import android.os.ParcelUuid;
+import android.os.Parcelable;
+import android.provider.OneTimeUseBuilder;
+
+import java.util.Objects;
+
+/**
+ * A request for setting the types of device for observing device presence.
+ *
+ * <p>Only supports association id or ParcelUuid and calling app must declare uses-permission
+ * {@link android.Manifest.permission#REQUEST_OBSERVE_DEVICE_UUID_PRESENCE} if using
+ * {@link Builder#setUuid(ParcelUuid)}.</p>
+ *
+ * Calling apps must use either ObservingDevicePresenceRequest.Builder#setUuid(ParcelUuid) or
+ * ObservingDevicePresenceRequest.Builder#setAssociationId(int), but not both.
+ *
+ * @see Builder#setUuid(ParcelUuid)
+ * @see Builder#setAssociationId(int)
+ * @see CompanionDeviceManager#startObservingDevicePresence(ObservingDevicePresenceRequest)
+ */
+@FlaggedApi(Flags.FLAG_DEVICE_PRESENCE)
+public final class ObservingDevicePresenceRequest implements Parcelable {
+    private final int mAssociationId;
+    @Nullable private final ParcelUuid mUuid;
+
+    private static final int PARCEL_UUID_NULL = 0;
+
+    private static final int PARCEL_UUID_NOT_NULL = 1;
+
+    private ObservingDevicePresenceRequest(int associationId, ParcelUuid uuid) {
+        mAssociationId = associationId;
+        mUuid = uuid;
+    }
+
+    private ObservingDevicePresenceRequest(@NonNull Parcel in) {
+        mAssociationId = in.readInt();
+        if (in.readInt() == PARCEL_UUID_NULL) {
+            mUuid = null;
+        } else {
+            mUuid = ParcelUuid.CREATOR.createFromParcel(in);
+        }
+    }
+
+    /**
+     * @return the association id for observing device presence. It will return
+     * {@link DevicePresenceEvent#NO_ASSOCIATION} if using
+     * {@link Builder#setUuid(ParcelUuid)}.
+     */
+    public int getAssociationId() {
+        return mAssociationId;
+    }
+
+    /**
+     * @return the ParcelUuid for observing device presence.
+     */
+    @Nullable
+    public ParcelUuid getUuid() {
+        return mUuid;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mAssociationId);
+        if (mUuid == null) {
+            // Write 0 to the parcel to indicate the ParcelUuid is null.
+            dest.writeInt(PARCEL_UUID_NULL);
+        } else {
+            dest.writeInt(PARCEL_UUID_NOT_NULL);
+            mUuid.writeToParcel(dest, flags);
+        }
+
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @NonNull
+    public static final Parcelable.Creator<ObservingDevicePresenceRequest> CREATOR =
+            new Parcelable.Creator<ObservingDevicePresenceRequest>() {
+                @Override
+                public ObservingDevicePresenceRequest[] newArray(int size) {
+                    return new ObservingDevicePresenceRequest[size];
+                }
+
+                @Override
+                public ObservingDevicePresenceRequest createFromParcel(@NonNull Parcel in) {
+                    return new ObservingDevicePresenceRequest(in);
+                }
+            };
+
+    @Override
+    public String toString() {
+        return "ObservingDevicePresenceRequest { "
+                + "Association Id= " + mAssociationId + ","
+                + "ParcelUuid= " + mUuid + "}";
+    }
+
+    @Override
+    public boolean equals(@Nullable Object o) {
+        if (this == o) return true;
+        if (!(o instanceof ObservingDevicePresenceRequest that)) return false;
+
+        return Objects.equals(mUuid, that.mUuid) && mAssociationId == that.mAssociationId;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mAssociationId, mUuid);
+    }
+
+    /**
+     * A builder for {@link ObservingDevicePresenceRequest}
+     */
+    public static final class Builder extends OneTimeUseBuilder<ObservingDevicePresenceRequest> {
+        // Initial the association id to {@link DevicePresenceEvent.NO_ASSOCIATION}
+        // to indicate the value is not set yet.
+        private int mAssociationId = DevicePresenceEvent.NO_ASSOCIATION;
+        private ParcelUuid mUuid;
+
+        public Builder() {}
+
+        /**
+         * Set the association id to be observed for device presence.
+         *
+         * <p>The provided device must be {@link CompanionDeviceManager#associate associated}
+         * with the calling app before calling this method if using this API.
+         *
+         * Caller must implement a single {@link CompanionDeviceService} which will be bound to and
+         * receive callbacks to
+         * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)}.</p>
+         *
+         * <p>Calling apps must use either {@link #setUuid(ParcelUuid)}
+         * or this API, but not both.</p>
+         *
+         * @param associationId The association id for observing device presence.
+         */
+        @NonNull
+        public Builder setAssociationId(int associationId) {
+            checkNotUsed();
+            this.mAssociationId = associationId;
+            return this;
+        }
+
+        /**
+         * Set the ParcelUuid to be observed for device presence.
+         *
+         * <p>It does not require to create the association before calling this API.
+         * This only supports classic Bluetooth scan and caller must implement
+         * a single {@link CompanionDeviceService} which will be bound to and receive callbacks to
+         * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)}.</p>
+         *
+         * <p>The Uuid should be matching one of the ParcelUuid form
+         * {@link android.bluetooth.BluetoothDevice#getUuids()}</p>
+         *
+         * <p>Calling apps must use either this API or {@link #setAssociationId(int)},
+         * but not both.</p>
+         *
+         * @param uuid The ParcelUuid for observing device presence.
+         */
+        @NonNull
+        @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_DEVICE_UUID_PRESENCE)
+        public Builder setUuid(@NonNull ParcelUuid uuid) {
+            checkNotUsed();
+            this.mUuid = uuid;
+            return this;
+        }
+
+        @NonNull
+        @Override
+        public ObservingDevicePresenceRequest build() {
+            markUsed();
+            if (mUuid != null && mAssociationId != DevicePresenceEvent.NO_ASSOCIATION) {
+                throw new IllegalStateException("Cannot observe device presence based on "
+                        + "both ParcelUuid and association ID. Choose one or the other.");
+            } else if (mUuid == null && mAssociationId <= 0) {
+                throw new IllegalStateException("Must provide either a ParcelUuid or "
+                        + "a valid association ID to observe device presence.");
+            }
+
+            return new ObservingDevicePresenceRequest(mAssociationId, mUuid);
+        }
+    }
+}
diff --git a/core/java/android/companion/flags.aconfig b/core/java/android/companion/flags.aconfig
index 9e410b8..d634b64 100644
--- a/core/java/android/companion/flags.aconfig
+++ b/core/java/android/companion/flags.aconfig
@@ -33,4 +33,4 @@
     namespace: "companion"
     description: "Expose perm sync user consent API"
     bug: "309528663"
-}
\ No newline at end of file
+}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 5f3f641..5b4404b 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -5708,6 +5708,14 @@
                 android:description="@string/permdesc_observeCompanionDevicePresence"
                 android:protectionLevel="normal" />
 
+    <!-- Allows an application to subscribe to notifications about the nearby devices' presence
+         status change base on the UUIDs.
+         <p>Not for use by third-party applications.</p>
+         @FlaggedApi("android.companion.flags.device_presence")
+    -->
+    <permission android:name="android.permission.REQUEST_OBSERVE_DEVICE_UUID_PRESENCE"
+                android:protectionLevel="signature|privileged" />
+
     <!-- Allows an application to deliver companion messages to system
          -->
     <permission android:name="android.permission.DELIVER_COMPANION_MESSAGES"
diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml
index a1ea2b8..8fe0b6f 100644
--- a/data/etc/privapp-permissions-platform.xml
+++ b/data/etc/privapp-permissions-platform.xml
@@ -399,6 +399,8 @@
         <permission name="android.permission.REQUEST_COMPANION_PROFILE_NEARBY_DEVICE_STREAMING" />
         <permission name="android.permission.REQUEST_COMPANION_PROFILE_COMPUTER" />
         <permission name="android.permission.REQUEST_COMPANION_SELF_MANAGED" />
+        <permission name="android.permission.REQUEST_OBSERVE_DEVICE_UUID_PRESENCE" />
+
         <!-- Permission required for testing registering pull atom callbacks. -->
         <permission name="android.permission.REGISTER_STATS_PULL_ATOM"/>
         <!-- Permission required for testing system audio effect APIs. -->
diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml
index 507d9c4..fa6346f 100644
--- a/packages/Shell/AndroidManifest.xml
+++ b/packages/Shell/AndroidManifest.xml
@@ -343,6 +343,7 @@
     <uses-permission android:name="android.permission.REQUEST_COMPANION_PROFILE_GLASSES" />
     <uses-permission android:name="android.permission.REQUEST_COMPANION_SELF_MANAGED" />
     <uses-permission android:name="android.permission.USE_COMPANION_TRANSPORTS" />
+    <uses-permission android:name="android.permission.REQUEST_OBSERVE_DEVICE_UUID_PRESENCE" />
 
     <uses-permission android:name="android.permission.MANAGE_APPOPS" />
     <uses-permission android:name="android.permission.WATCH_APPOPS" />
diff --git a/services/companion/java/com/android/server/companion/CompanionApplicationController.java b/services/companion/java/com/android/server/companion/CompanionApplicationController.java
index c2d2468..586aa8a 100644
--- a/services/companion/java/com/android/server/companion/CompanionApplicationController.java
+++ b/services/companion/java/com/android/server/companion/CompanionApplicationController.java
@@ -22,9 +22,11 @@
 import android.annotation.UserIdInt;
 import android.companion.AssociationInfo;
 import android.companion.CompanionDeviceService;
+import android.companion.DevicePresenceEvent;
 import android.content.ComponentName;
 import android.content.Context;
 import android.os.Handler;
+import android.os.ParcelUuid;
 import android.util.Log;
 import android.util.Slog;
 import android.util.SparseArray;
@@ -46,7 +48,8 @@
  * the services, maintaining the connection (the binding), and invoking callback methods such as
  * {@link CompanionDeviceService#onDeviceAppeared(AssociationInfo)},
  * {@link CompanionDeviceService#onDeviceDisappeared(AssociationInfo)} and
- * {@link CompanionDeviceService#onDeviceEvent(AssociationInfo, int)} in the application process.
+ * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)} in the
+ * application process.
  *
  * <p>
  * The following is the list of the APIs provided by {@link CompanionApplicationController} (to be
@@ -54,7 +57,7 @@
  * <ul>
  * <li> {@link #bindCompanionApplication(int, String, boolean)}
  * <li> {@link #unbindCompanionApplication(int, String)}
- * <li> {@link #notifyCompanionApplicationDeviceEvent(AssociationInfo, int)} (AssociationInfo, int)}
+ * <li> {@link #notifyCompanionApplicationDevicePresenceEvent(AssociationInfo, int)}
  * <li> {@link #isCompanionApplicationBound(int, String)}
  * <li> {@link #isRebindingCompanionApplicationScheduled(int, String)}
  * </ul>
@@ -72,6 +75,7 @@
 
     private final @NonNull Context mContext;
     private final @NonNull AssociationStore mAssociationStore;
+    private final @NonNull ObservableUuidStore mObservableUuidStore;
     private final @NonNull CompanionDevicePresenceMonitor mDevicePresenceMonitor;
     private final @NonNull CompanionServicesRegister mCompanionServicesRegister;
 
@@ -82,9 +86,11 @@
     private final @NonNull AndroidPackageMap<Boolean> mScheduledForRebindingCompanionApplications;
 
     CompanionApplicationController(Context context, AssociationStore associationStore,
+            ObservableUuidStore observableUuidStore,
             CompanionDevicePresenceMonitor companionDevicePresenceMonitor) {
         mContext = context;
         mAssociationStore = associationStore;
+        mObservableUuidStore =  observableUuidStore;
         mDevicePresenceMonitor = companionDevicePresenceMonitor;
         mCompanionServicesRegister = new CompanionServicesRegister();
         mBoundCompanionApplications = new AndroidPackageMap<>();
@@ -281,25 +287,50 @@
         primaryServiceConnector.postOnDeviceDisappeared(association);
     }
 
-    void notifyCompanionApplicationDeviceEvent(AssociationInfo association, int event) {
+    void notifyCompanionApplicationDevicePresenceEvent(AssociationInfo association, int event) {
         final int userId = association.getUserId();
         final String packageName = association.getPackageName();
         final CompanionDeviceServiceConnector primaryServiceConnector =
                 getPrimaryServiceConnector(userId, packageName);
+        final DevicePresenceEvent devicePresenceEvent =
+                new DevicePresenceEvent(association.getId(), event, null);
 
         if (primaryServiceConnector == null) {
-            Slog.e(TAG, "notifyCompanionApplicationDeviceEvent(): "
+            Slog.e(TAG, "notifyCompanionApplicationDevicePresenceEvent(): "
                         + "u" + userId + "/" + packageName
                         + " event=[ " + event  + " ] is NOT bound.");
             Slog.e(TAG, "Stacktrace", new Throwable());
             return;
         }
 
-        Slog.i(TAG, "Calling onDeviceEvent() to userId=[" + userId + "] package=["
+        Slog.i(TAG, "Calling onDevicePresenceEvent() to userId=[" + userId + "] package=["
                 + packageName + "] associationId=[" + association.getId()
-                + "] state=[" + event + "]");
+                + "] event=[" + event + "]");
 
-        primaryServiceConnector.postOnDeviceEvent(association, event);
+        primaryServiceConnector.postOnDevicePresenceEvent(devicePresenceEvent);
+    }
+
+    void notifyApplicationDevicePresenceEvent(ObservableUuid uuid, int event) {
+        final int userId = uuid.getUserId();
+        final ParcelUuid parcelUuid = uuid.getUuid();
+        final String packageName = uuid.getPackageName();
+        final CompanionDeviceServiceConnector primaryServiceConnector =
+                getPrimaryServiceConnector(userId, packageName);
+        final DevicePresenceEvent devicePresenceEvent =
+                new DevicePresenceEvent(DevicePresenceEvent.NO_ASSOCIATION, event, parcelUuid);
+
+        if (primaryServiceConnector == null) {
+            Slog.e(TAG, "notifyApplicationDevicePresenceChanged(): "
+                    + "u" + userId + "/" + packageName
+                    + " event=[ " + event  + " ] is NOT bound.");
+            Slog.e(TAG, "Stacktrace", new Throwable());
+            return;
+        }
+
+        Slog.i(TAG, "Calling onDevicePresenceEvent() to userId=[" + userId + "] package=["
+                + packageName + "]" + "event= [" + event + "]");
+
+        primaryServiceConnector.postOnDevicePresenceEvent(devicePresenceEvent);
     }
 
     void dump(@NonNull PrintWriter out) {
@@ -364,6 +395,9 @@
         // Make sure to clean up the state for all the associations
         // that associate with this package.
         boolean shouldScheduleRebind = false;
+        boolean shouldScheduleRebindForUuid = false;
+        final List<ObservableUuid> uuids =
+                mObservableUuidStore.getObservableUuidsForPackage(userId, packageName);
 
         for (AssociationInfo ai :
                 mAssociationStore.getAssociationsForPackage(userId, packageName)) {
@@ -385,7 +419,14 @@
             }
         }
 
-        return stillAssociated && shouldScheduleRebind;
+        for (ObservableUuid uuid : uuids) {
+            if (mDevicePresenceMonitor.isDeviceUuidPresent(uuid.getUuid())) {
+                shouldScheduleRebindForUuid = true;
+                break;
+            }
+        }
+
+        return (stillAssociated && shouldScheduleRebind) || shouldScheduleRebindForUuid;
     }
 
     private class CompanionServicesRegister extends PerUser<Map<String, List<ComponentName>>> {
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
index 50e1862..ede27b3 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java
@@ -20,17 +20,17 @@
 import static android.Manifest.permission.ASSOCIATE_COMPANION_DEVICES;
 import static android.Manifest.permission.DELIVER_COMPANION_MESSAGES;
 import static android.Manifest.permission.MANAGE_COMPANION_DEVICES;
-import static android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE;
 import static android.Manifest.permission.REQUEST_COMPANION_PROFILE_AUTOMOTIVE_PROJECTION;
+import static android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE;
 import static android.Manifest.permission.USE_COMPANION_TRANSPORTS;
 import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE;
 import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION;
-import static android.companion.CompanionDeviceService.DEVICE_EVENT_BLE_APPEARED;
-import static android.companion.CompanionDeviceService.DEVICE_EVENT_BLE_DISAPPEARED;
-import static android.companion.CompanionDeviceService.DEVICE_EVENT_BT_CONNECTED;
-import static android.companion.CompanionDeviceService.DEVICE_EVENT_BT_DISCONNECTED;
-import static android.companion.CompanionDeviceService.DEVICE_EVENT_SELF_MANAGED_APPEARED;
-import static android.companion.CompanionDeviceService.DEVICE_EVENT_SELF_MANAGED_DISAPPEARED;
+import static android.companion.DevicePresenceEvent.EVENT_BLE_APPEARED;
+import static android.companion.DevicePresenceEvent.EVENT_BLE_DISAPPEARED;
+import static android.companion.DevicePresenceEvent.EVENT_BT_CONNECTED;
+import static android.companion.DevicePresenceEvent.EVENT_BT_DISCONNECTED;
+import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_APPEARED;
+import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_DISAPPEARED;
 import static android.content.pm.PackageManager.CERT_INPUT_SHA256;
 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
 import static android.os.Process.SYSTEM_UID;
@@ -45,6 +45,7 @@
 import static com.android.server.companion.PackageUtils.getPackageInfo;
 import static com.android.server.companion.PermissionsUtils.checkCallerCanManageCompanionDevice;
 import static com.android.server.companion.PermissionsUtils.enforceCallerCanManageAssociationsForPackage;
+import static com.android.server.companion.PermissionsUtils.enforceCallerCanObservingDevicePresenceByUuid;
 import static com.android.server.companion.PermissionsUtils.enforceCallerIsSystemOr;
 import static com.android.server.companion.PermissionsUtils.enforceCallerIsSystemOrCanInteractWithUserId;
 import static com.android.server.companion.PermissionsUtils.sanitizeWithCallerChecks;
@@ -75,6 +76,7 @@
 import android.companion.IOnMessageReceivedListener;
 import android.companion.IOnTransportsChangedListener;
 import android.companion.ISystemDataTransferCallback;
+import android.companion.ObservingDevicePresenceRequest;
 import android.companion.datatransfer.PermissionSyncRequest;
 import android.content.ComponentName;
 import android.content.Context;
@@ -92,6 +94,7 @@
 import android.os.Message;
 import android.os.Parcel;
 import android.os.ParcelFileDescriptor;
+import android.os.ParcelUuid;
 import android.os.PowerManagerInternal;
 import android.os.PowerWhitelistManager;
 import android.os.RemoteCallbackList;
@@ -132,6 +135,7 @@
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
@@ -221,6 +225,8 @@
 
     private CrossDeviceSyncController mCrossDeviceSyncController;
 
+    private ObservableUuidStore mObservableUuidStore;
+
     public CompanionDeviceManagerService(Context context) {
         super(context);
 
@@ -240,6 +246,7 @@
         mOnPackageVisibilityChangeListener =
                 new OnPackageVisibilityChangeListener(mActivityManager);
         mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class);
+        mObservableUuidStore = new ObservableUuidStore();
     }
 
     @Override
@@ -249,15 +256,18 @@
         mPersistentStore = new PersistentDataStore();
 
         loadAssociationsFromDisk();
+
+        mObservableUuidStore.getObservableUuidsForUser(getContext().getUserId());
+
         mAssociationStore.registerListener(mAssociationStoreChangeListener);
 
         mDevicePresenceMonitor = new CompanionDevicePresenceMonitor(mUserManager,
-                mAssociationStore, mDevicePresenceCallback);
+                mAssociationStore, mObservableUuidStore, mDevicePresenceCallback);
 
         mAssociationRequestsProcessor = new AssociationRequestsProcessor(
                 /* cdmService */this, mAssociationStore);
         mCompanionAppController = new CompanionApplicationController(
-                context, mAssociationStore, mDevicePresenceMonitor);
+                context, mAssociationStore, mObservableUuidStore, mDevicePresenceMonitor);
         mTransportManager = new CompanionTransportManager(context, mAssociationStore);
         mSystemDataTransferProcessor = new SystemDataTransferProcessor(this,
                 mPackageManagerInternal, mAssociationStore,
@@ -352,13 +362,29 @@
         final int userId = user.getUserIdentifier();
         final Set<BluetoothDevice> blueToothDevices =
                 mDevicePresenceMonitor.getPendingConnectedDevices().get(userId);
+
+        final List<ObservableUuid> observableUuids =
+                mObservableUuidStore.getObservableUuidsForUser(userId);
+
         if (blueToothDevices != null) {
             for (BluetoothDevice bluetoothDevice : blueToothDevices) {
+                final List<ParcelUuid> deviceUuids = bluetoothDevice.getUuids() == null
+                        ? Collections.emptyList() : Arrays.asList(bluetoothDevice.getUuids());
+
                 for (AssociationInfo ai:
                         mAssociationStore.getAssociationsByAddress(bluetoothDevice.getAddress())) {
                     Slog.i(TAG, "onUserUnlocked, device id( " + ai.getId() + " ) is connected");
                     mDevicePresenceMonitor.onBluetoothCompanionDeviceConnected(ai.getId());
                 }
+
+                for (ObservableUuid observableUuid : observableUuids) {
+                    if (deviceUuids.contains(observableUuid.getUuid())) {
+                        Slog.i(TAG, "onUserUnlocked, UUID( "
+                                + observableUuid.getUuid() + " ) is connected");
+                        mDevicePresenceMonitor.onDevicePresenceEventByUuid(
+                                observableUuid, EVENT_BT_CONNECTED);
+                    }
+                }
             }
         }
     }
@@ -423,31 +449,31 @@
         }
     }
 
-    private void onDeviceEventInternal(int associationId, int event) {
-        Slog.i(TAG, "onDeviceEventInternal() id=" + associationId + " event= " + event);
+    private void onDevicePresenceEventInternal(int associationId, int event) {
+        Slog.i(TAG, "onDevicePresenceEventInternal() id=" + associationId + " event= " + event);
         final AssociationInfo association = mAssociationStore.getAssociationById(associationId);
         final String packageName = association.getPackageName();
         final int userId = association.getUserId();
         switch (event) {
-            case DEVICE_EVENT_BLE_APPEARED:
-            case DEVICE_EVENT_BT_CONNECTED:
-            case DEVICE_EVENT_SELF_MANAGED_APPEARED:
+            case EVENT_BLE_APPEARED:
+            case EVENT_BT_CONNECTED:
+            case EVENT_SELF_MANAGED_APPEARED:
                 if (!association.shouldBindWhenPresent()) return;
 
                 bindApplicationIfNeeded(association);
 
-                mCompanionAppController.notifyCompanionApplicationDeviceEvent(
+                mCompanionAppController.notifyCompanionApplicationDevicePresenceEvent(
                         association, event);
                 break;
-            case DEVICE_EVENT_BLE_DISAPPEARED:
-            case DEVICE_EVENT_BT_DISCONNECTED:
-            case DEVICE_EVENT_SELF_MANAGED_DISAPPEARED:
+            case EVENT_BLE_DISAPPEARED:
+            case EVENT_BT_DISCONNECTED:
+            case EVENT_SELF_MANAGED_DISAPPEARED:
                 if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) {
                     if (DEBUG) Log.w(TAG, "u" + userId + "\\" + packageName + " is NOT bound");
                     return;
                 }
                 if (association.shouldBindWhenPresent()) {
-                    mCompanionAppController.notifyCompanionApplicationDeviceEvent(
+                    mCompanionAppController.notifyCompanionApplicationDevicePresenceEvent(
                             association, event);
                 }
                 // Check if there are other devices associated to the app that are present.
@@ -460,6 +486,45 @@
         }
     }
 
+    private void onDevicePresenceEventByUuidInternal(ObservableUuid uuid, int event) {
+        Slog.i(TAG, "onDevicePresenceEventByUuidInternal() id=" + uuid.getUuid()
+                + "for package=" + uuid.getPackageName() + " event=" + event);
+        final String packageName = uuid.getPackageName();
+        final int userId = uuid.getUserId();
+
+        switch(event) {
+            case EVENT_BT_CONNECTED:
+                if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) {
+                    mCompanionAppController.bindCompanionApplication(
+                            userId, packageName, /*bindImportant*/ false);
+
+                } else if (DEBUG) {
+                    Log.i(TAG, "u" + userId + "\\" + packageName + " is already bound");
+                }
+
+                mCompanionAppController.notifyApplicationDevicePresenceEvent(uuid, event);
+
+                break;
+            case EVENT_BT_DISCONNECTED:
+                if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) {
+                    if (DEBUG) Log.w(TAG, "u" + userId + "\\" + packageName + " is NOT bound");
+                    return;
+                }
+
+                mCompanionAppController.notifyApplicationDevicePresenceEvent(uuid, event);
+                // Check if there are other devices associated to the app or the UUID to be
+                // observed are present.
+                if (shouldBindPackage(userId, packageName)) return;
+
+                mCompanionAppController.unbindCompanionApplication(userId, packageName);
+
+                break;
+            default:
+                Slog.e(TAG, "Event: " + event + "is not supported");
+                break;
+        }
+    }
+
     private void bindApplicationIfNeeded(AssociationInfo association) {
         final String packageName = association.getPackageName();
         final int userId = association.getUserId();
@@ -476,15 +541,26 @@
 
     /**
      * @return whether the package should be bound (i.e. at least one of the devices associated with
-     *         the package is currently present).
+     *         the package is currently present OR the UUID to be observed by this package is
+     *         currently present).
      */
     private boolean shouldBindPackage(@UserIdInt int userId, @NonNull String packageName) {
         final List<AssociationInfo> packageAssociations =
                 mAssociationStore.getAssociationsForPackage(userId, packageName);
+        final List<ObservableUuid> observableUuids =
+                mObservableUuidStore.getObservableUuidsForPackage(userId, packageName);
+
         for (AssociationInfo association : packageAssociations) {
             if (!association.shouldBindWhenPresent()) continue;
             if (mDevicePresenceMonitor.isDevicePresent(association.getId())) return true;
         }
+
+        for (ObservableUuid uuid : observableUuids) {
+            if (mDevicePresenceMonitor.isDeviceUuidPresent(uuid.getUuid())) {
+                return true;
+            }
+        }
+
         return false;
     }
 
@@ -568,6 +644,8 @@
         // Clear associations.
         final List<AssociationInfo> associationsForPackage =
                 mAssociationStore.getAssociationsForPackage(userId, packageName);
+        final List<ObservableUuid> uuidsTobeObserved =
+                mObservableUuidStore.getObservableUuidsForPackage(userId, packageName);
         for (AssociationInfo association : associationsForPackage) {
             mAssociationStore.removeAssociation(association.getId());
         }
@@ -575,6 +653,10 @@
         for (AssociationInfo association : associationsForPackage) {
             maybeRemoveRoleHolderForAssociation(association);
         }
+        // Clear the uuids to be observed.
+        for (ObservableUuid uuid : uuidsTobeObserved) {
+            mObservableUuidStore.removeObservableUuid(userId, uuid.getUuid(), packageName);
+        }
 
         mCompanionAppController.onPackagesChanged(userId);
     }
@@ -855,6 +937,95 @@
         }
 
         @Override
+        @EnforcePermission(REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE)
+        public void startObservingDevicePresence(ObservingDevicePresenceRequest request,
+                String packageName, int userId) {
+            startObservingDevicePresence_enforcePermission();
+            registerDevicePresenceListener(request, packageName, userId, /* active */ true);
+        }
+
+        @Override
+        @EnforcePermission(REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE)
+        public void stopObservingDevicePresence(ObservingDevicePresenceRequest request,
+                String packageName, int userId) {
+            stopObservingDevicePresence_enforcePermission();
+            registerDevicePresenceListener(request, packageName, userId, /* active */ false);
+        }
+
+        private void registerDevicePresenceListener(ObservingDevicePresenceRequest request,
+                String packageName, int userId, boolean active) {
+            enforceUsesCompanionDeviceFeature(getContext(), userId, packageName);
+            enforceCallerIsSystemOr(userId, packageName);
+
+            final int associationId = request.getAssociationId();
+            final AssociationInfo associationInfo = mAssociationStore.getAssociationById(
+                    associationId);
+            final ParcelUuid uuid = request.getUuid();
+
+            if (uuid != null) {
+                enforceCallerCanObservingDevicePresenceByUuid(getContext());
+                if (active) {
+                    startObservingDevicePresenceByUuid(uuid, packageName, userId);
+                } else {
+                    stopObservingDevicePresenceByUuid(uuid, packageName, userId);
+                }
+            } else if (associationInfo == null) {
+                throw new IllegalArgumentException("App " + packageName
+                        + " is not associated with device " + request.getAssociationId()
+                        + " for user " + userId);
+            } else {
+                processDevicePresenceListener(
+                        associationInfo, userId, packageName, active);
+            }
+        }
+
+        private void startObservingDevicePresenceByUuid(ParcelUuid uuid, String packageName,
+                int userId) {
+            final List<ObservableUuid> observableUuids =
+                    mObservableUuidStore.getObservableUuidsForPackage(userId, packageName);
+
+            for (ObservableUuid observableUuid : observableUuids) {
+                if (observableUuid.getUuid().equals(uuid)) {
+                    Slog.i(TAG, "The uuid: " + uuid + " for package:" + packageName
+                            + "has been already scheduled for observing");
+                    return;
+                }
+            }
+
+            final ObservableUuid observableUuid = new ObservableUuid(userId, uuid,
+                    packageName, System.currentTimeMillis());
+
+            mObservableUuidStore.writeObservableUuid(userId, observableUuid);
+        }
+
+        private void stopObservingDevicePresenceByUuid(ParcelUuid uuid, String packageName,
+                int userId) {
+            final List<ObservableUuid> uuidsTobeObserved =
+                    mObservableUuidStore.getObservableUuidsForPackage(userId, packageName);
+            boolean isScheduledObserving = false;
+
+            for (ObservableUuid observableUuid : uuidsTobeObserved) {
+                if (observableUuid.getUuid().equals(uuid)) {
+                    isScheduledObserving = true;
+                    break;
+                }
+            }
+
+            if (!isScheduledObserving) {
+                Slog.i(TAG, "The uuid: " + uuid.toString() + " for package:" + packageName
+                        + "has NOT been scheduled for observing yet");
+                return;
+            }
+
+            mObservableUuidStore.removeObservableUuid(userId, uuid, packageName);
+            mDevicePresenceMonitor.removeCurrentConnectedUuidDevice(uuid);
+
+            if (!shouldBindPackage(userId, packageName)) {
+                mCompanionAppController.unbindCompanionApplication(userId, packageName);
+            }
+        }
+
+        @Override
         public PendingIntent buildPermissionTransferUserConsentIntent(String packageName,
                 int userId, int associationId) {
             return mSystemDataTransferProcessor.buildPermissionTransferUserConsentIntent(
@@ -1002,6 +1173,11 @@
                         + " for user " + userId));
             }
 
+            processDevicePresenceListener(association, userId, packageName, active);
+        }
+
+        private void processDevicePresenceListener(AssociationInfo association,
+                int userId, String packageName, boolean active) {
             // If already at specified state, then no-op.
             if (active == association.isNotifyOnDeviceNearby()) {
                 if (DEBUG) Log.d(TAG, "Device presence listener is already at desired state.");
@@ -1025,9 +1201,9 @@
                 if (mDevicePresenceMonitor.isBlePresent(associationId)
                         || mDevicePresenceMonitor.isSimulatePresent(associationId)) {
                     onDeviceAppearedInternal(associationId);
-                    onDeviceEventInternal(associationId, DEVICE_EVENT_BLE_APPEARED);
+                    onDevicePresenceEventInternal(associationId, EVENT_BLE_APPEARED);
                 } else if (mDevicePresenceMonitor.isBtConnected(associationId)) {
-                    onDeviceEventInternal(associationId, DEVICE_EVENT_BT_CONNECTED);
+                    onDevicePresenceEventInternal(associationId, EVENT_BT_CONNECTED);
                 }
             }
 
@@ -1518,20 +1694,25 @@
 
     private final CompanionDevicePresenceMonitor.Callback mDevicePresenceCallback =
             new CompanionDevicePresenceMonitor.Callback() {
-        @Override
-        public void onDeviceAppeared(int associationId) {
-            onDeviceAppearedInternal(associationId);
-        }
+                @Override
+                public void onDeviceAppeared(int associationId) {
+                    onDeviceAppearedInternal(associationId);
+                }
 
-        @Override
-        public void onDeviceDisappeared(int associationId) {
-            onDeviceDisappearedInternal(associationId);
-        }
+                @Override
+                public void onDeviceDisappeared(int associationId) {
+                    onDeviceDisappearedInternal(associationId);
+                }
 
-        @Override
-        public void onDeviceEvent(int associationId, int event) {
-            onDeviceEventInternal(associationId, event);
-        }
+                @Override
+                public void onDevicePresenceEvent(int associationId, int event) {
+                    onDevicePresenceEventInternal(associationId, event);
+                }
+
+                @Override
+                public void onDevicePresenceEventByUuid(ObservableUuid uuid, int event) {
+                    onDevicePresenceEventByUuidInternal(uuid, event);
+                }
     };
 
     private final PackageMonitor mPackageMonitor = new PackageMonitor() {
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java b/services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java
index 928842c..5abdb42 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java
@@ -26,6 +26,7 @@
 import android.annotation.UserIdInt;
 import android.companion.AssociationInfo;
 import android.companion.CompanionDeviceService;
+import android.companion.DevicePresenceEvent;
 import android.companion.ICompanionDeviceService;
 import android.content.ComponentName;
 import android.content.Context;
@@ -106,12 +107,11 @@
     void postOnDeviceDisappeared(@NonNull AssociationInfo associationInfo) {
         post(companionService -> companionService.onDeviceDisappeared(associationInfo));
     }
-    void postOnDeviceEvent(@NonNull AssociationInfo associationInfo, int event) {
-        post(companionService -> companionService.onDeviceEvent(associationInfo, event));
+
+    void postOnDevicePresenceEvent(@NonNull DevicePresenceEvent event) {
+        post(companionService -> companionService.onDevicePresenceEvent(event));
     }
 
-
-
     /**
      * Post "unbind" job, which will run *after* all previously posted jobs complete.
      *
diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
index 53c0184c..27b40c9 100644
--- a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
+++ b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java
@@ -27,6 +27,7 @@
 import android.companion.datatransfer.PermissionSyncRequest;
 import android.net.MacAddress;
 import android.os.Binder;
+import android.os.ParcelUuid;
 import android.os.ShellCommand;
 import android.util.Base64;
 import android.util.proto.ProtoOutputStream;
@@ -80,6 +81,19 @@
                 mDevicePresenceMonitor.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());
+                mDevicePresenceMonitor.simulateDeviceEventByUuid(observableUuid, event);
+                return 0;
+            }
+
             switch (cmd) {
                 case "list": {
                     final int userId = getNextIntArgRequired();
@@ -447,6 +461,16 @@
             pw.println("    Case(3): ");
             pw.println("      Make CDM act as if the given companion device is BT disconnected ");
             pw.println("      USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY.");
+
+            pw.println("  simulate-device-uuid-event UUID PACKAGE USERID EVENT");
+            pw.println("  Simulate the companion device event changes:");
+            pw.println("    Case(2): ");
+            pw.println("      Make CDM act as if the given DEVICE is BT connected base"
+                    + "on the UUID");
+            pw.println("    Case(3): ");
+            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("  remove-inactive-associations");
diff --git a/services/companion/java/com/android/server/companion/ObservableUuid.java b/services/companion/java/com/android/server/companion/ObservableUuid.java
new file mode 100644
index 0000000..6ab3188
--- /dev/null
+++ b/services/companion/java/com/android/server/companion/ObservableUuid.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2023 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.companion;
+
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.os.ParcelUuid;
+
+public class ObservableUuid {
+    private final int mUserId;
+    private final String mPackageName;
+
+    private final ParcelUuid mUuid;
+
+    private final long mTimeApprovedMs;
+
+    public ObservableUuid(@UserIdInt int userId, @NonNull ParcelUuid uuid,
+            @NonNull String packageName, Long timeApprovedMs) {
+        mUserId = userId;
+        mUuid = uuid;
+        mPackageName = packageName;
+        mTimeApprovedMs = timeApprovedMs;
+    }
+
+    public int getUserId() {
+        return mUserId;
+    }
+
+    public ParcelUuid getUuid() {
+        return mUuid;
+    }
+
+    public String getPackageName() {
+        return mPackageName;
+    }
+
+    public long getTimeApprovedMs() {
+        return mTimeApprovedMs;
+    }
+}
diff --git a/services/companion/java/com/android/server/companion/ObservableUuidStore.java b/services/companion/java/com/android/server/companion/ObservableUuidStore.java
new file mode 100644
index 0000000..94be22a
--- /dev/null
+++ b/services/companion/java/com/android/server/companion/ObservableUuidStore.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright (C) 2023 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.companion;
+
+import static com.android.internal.util.XmlUtils.readIntAttribute;
+import static com.android.internal.util.XmlUtils.readLongAttribute;
+import static com.android.internal.util.XmlUtils.readStringAttribute;
+import static com.android.internal.util.XmlUtils.writeIntAttribute;
+import static com.android.internal.util.XmlUtils.writeLongAttribute;
+import static com.android.internal.util.XmlUtils.writeStringAttribute;
+import static com.android.server.companion.DataStoreUtils.createStorageFileForUser;
+import static com.android.server.companion.DataStoreUtils.isEndOfTag;
+import static com.android.server.companion.DataStoreUtils.isStartOfTag;
+import static com.android.server.companion.DataStoreUtils.writeToFileSafely;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.os.ParcelUuid;
+import android.util.AtomicFile;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.Xml;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.XmlUtils;
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+public class ObservableUuidStore {
+    private static final String TAG = "CDM_ObservableUuidStore";
+    private static final String FILE_NAME = "observing_uuids_presence.xml";
+    private static final String XML_TAG_UUIDS = "uuids";
+    private static final String XML_TAG_UUID = "uuid";
+    private static final String XML_ATTR_UUID = "uuid";
+    private static final String XML_ATTR_TIME_APPROVED = "time_approved";
+    private static final String XML_ATTR_USER_ID = "user_id";
+    private static final String XML_ATTR_PACKAGE = "package_name";
+    private static final int READ_FROM_DISK_TIMEOUT = 5; // in seconds
+
+
+    private final ExecutorService mExecutor;
+    private final ConcurrentMap<Integer, AtomicFile> mUserIdToStorageFile =
+            new ConcurrentHashMap<>();
+
+    private final Object mLock = new Object();
+
+    @GuardedBy("mLock")
+    private final SparseArray<List<ObservableUuid>> mCachedPerUser =
+            new SparseArray<>();
+
+    public ObservableUuidStore() {
+        mExecutor = Executors.newSingleThreadExecutor();
+    }
+
+    /**
+     * Remove the observable uuid from the disk.
+     */
+    void removeObservableUuid(@UserIdInt int userId, ParcelUuid uuid, String packageName) {
+        List<ObservableUuid> cachedObservableUuids;
+
+        synchronized (mLock) {
+            // Remove requests from cache
+            cachedObservableUuids = readObservableUuidsFromCache(userId);
+            cachedObservableUuids.removeIf(
+                    uuid1 -> uuid1.getPackageName().equals(packageName)
+                            && uuid1.getUuid().equals(uuid));
+            mCachedPerUser.set(userId, cachedObservableUuids);
+        }
+        // Remove requests from store
+        mExecutor.execute(() -> writeObservableUuidToStore(userId, cachedObservableUuids));
+    }
+
+    void writeObservableUuid(@UserIdInt int userId, ObservableUuid uuid) {
+        Slog.i(TAG, "Writing uuid=" + uuid.getUuid() + " to store.");
+
+        List<ObservableUuid> cachedObservableUuids;
+        synchronized (mLock) {
+            // Write to cache
+            cachedObservableUuids = readObservableUuidsFromCache(userId);
+            cachedObservableUuids.removeIf(uuid1 -> uuid1.getUuid().equals(
+                    uuid.getUuid()) && uuid1.getPackageName().equals(uuid.getPackageName()));
+            cachedObservableUuids.add(uuid);
+            mCachedPerUser.set(userId, cachedObservableUuids);
+        }
+        // Write to store
+        mExecutor.execute(() -> writeObservableUuidToStore(userId, cachedObservableUuids));
+    }
+
+    private void writeObservableUuidToStore(@UserIdInt int userId,
+            @NonNull List<ObservableUuid> cachedObservableUuids) {
+        final AtomicFile file = getStorageFileForUser(userId);
+        Slog.i(TAG, "Writing ObservableUuid for user " + userId + " to file="
+                + file.getBaseFile().getPath());
+
+        // getStorageFileForUser() ALWAYS returns the SAME OBJECT, which allows us to synchronize
+        // accesses to the file on the file system using this AtomicFile object.
+        synchronized (file) {
+            writeToFileSafely(file, out -> {
+                final TypedXmlSerializer serializer = Xml.resolveSerializer(out);
+                serializer.setFeature(
+                        "http://xmlpull.org/v1/doc/features.html#indent-output", true);
+                serializer.startDocument(null, true);
+                writeObservableUuidToXml(serializer, cachedObservableUuids);
+                serializer.endDocument();
+            });
+        }
+    }
+
+    private void writeObservableUuidToXml(@NonNull TypedXmlSerializer serializer,
+            @Nullable Collection<ObservableUuid> uuids) throws IOException {
+        serializer.startTag(null, XML_TAG_UUIDS);
+
+        for (ObservableUuid uuid : uuids) {
+            writeUuidToXml(serializer, uuid);
+        }
+
+        serializer.endTag(null, XML_TAG_UUIDS);
+    }
+
+    private void writeUuidToXml(@NonNull TypedXmlSerializer serializer,
+            @NonNull ObservableUuid uuid) throws IOException {
+        serializer.startTag(null, XML_TAG_UUID);
+
+        writeIntAttribute(serializer, XML_ATTR_USER_ID, uuid.getUserId());
+        writeStringAttribute(serializer, XML_ATTR_UUID, uuid.getUuid().toString());
+        writeStringAttribute(serializer, XML_ATTR_PACKAGE, uuid.getPackageName());
+        writeLongAttribute(serializer, XML_ATTR_TIME_APPROVED, uuid.getTimeApprovedMs());
+
+        serializer.endTag(null, XML_TAG_UUID);
+    }
+
+    /**
+     * Read the observable UUIDs from the cache.
+     */
+    @GuardedBy("mLock")
+    private List<ObservableUuid> readObservableUuidsFromCache(@UserIdInt int userId) {
+        List<ObservableUuid> cachedObservableUuids = mCachedPerUser.get(userId);
+        if (cachedObservableUuids == null) {
+            Future<List<ObservableUuid>> future =
+                    mExecutor.submit(() -> readObservableUuidFromStore(userId));
+            try {
+                cachedObservableUuids = future.get(READ_FROM_DISK_TIMEOUT, TimeUnit.SECONDS);
+            } catch (InterruptedException e) {
+                Slog.e(TAG, "Thread reading ObservableUuid from disk is "
+                        + "interrupted.");
+            } catch (ExecutionException e) {
+                Slog.e(TAG, "Error occurred while reading ObservableUuid "
+                        + "from disk.");
+            } catch (TimeoutException e) {
+                Slog.e(TAG, "Reading ObservableUuid from disk timed out.");
+            }
+            mCachedPerUser.set(userId, cachedObservableUuids);
+        }
+        return cachedObservableUuids;
+    }
+
+    /**
+     * Reads previously persisted data for the given user
+     *
+     * @param userId Android UserID
+     * @return a list of ObservableUuid
+     */
+    @NonNull
+    public List<ObservableUuid> readObservableUuidFromStore(@UserIdInt int userId) {
+        final AtomicFile file = getStorageFileForUser(userId);
+        Slog.i(TAG, "Reading ObservableUuid for user " + userId + " from "
+                + "file=" + file.getBaseFile().getPath());
+
+        // getStorageFileForUser() ALWAYS returns the SAME OBJECT, which allows us to synchronize
+        // accesses to the file on the file system using this AtomicFile object.
+        synchronized (file) {
+            if (!file.getBaseFile().exists()) {
+                Slog.d(TAG, "File does not exist -> Abort");
+                return new ArrayList<>();
+            }
+            try (FileInputStream in = file.openRead()) {
+                final TypedXmlPullParser parser = Xml.resolvePullParser(in);
+                XmlUtils.beginDocument(parser, XML_TAG_UUIDS);
+
+                return readObservableUuidFromXml(parser);
+            } catch (XmlPullParserException | IOException e) {
+                Slog.e(TAG, "Error while reading requests file", e);
+                return new ArrayList<>();
+            }
+        }
+    }
+
+    @NonNull
+    private List<ObservableUuid> readObservableUuidFromXml(
+            @NonNull TypedXmlPullParser parser) throws XmlPullParserException, IOException {
+        if (!isStartOfTag(parser, XML_TAG_UUIDS)) {
+            throw new XmlPullParserException("The XML doesn't have start tag: " + XML_TAG_UUIDS);
+        }
+
+        List<ObservableUuid> observableUuids = new ArrayList<>();
+
+        while (true) {
+            parser.nextTag();
+            if (isEndOfTag(parser, XML_TAG_UUIDS)) {
+                break;
+            }
+            if (isStartOfTag(parser, XML_TAG_UUID)) {
+                observableUuids.add(readUuidFromXml(parser));
+            }
+        }
+
+        return observableUuids;
+    }
+
+    private ObservableUuid readUuidFromXml(@NonNull TypedXmlPullParser parser)
+            throws XmlPullParserException, IOException {
+        if (!isStartOfTag(parser, XML_TAG_UUID)) {
+            throw new XmlPullParserException("XML doesn't have start tag: " + XML_TAG_UUID);
+        }
+
+        final int userId = readIntAttribute(parser, XML_ATTR_USER_ID);
+        final ParcelUuid uuid = ParcelUuid.fromString(readStringAttribute(parser, XML_ATTR_UUID));
+        final String packageName = readStringAttribute(parser, XML_ATTR_PACKAGE);
+        final Long timeApproved = readLongAttribute(parser, XML_ATTR_TIME_APPROVED);
+
+        return new ObservableUuid(userId, uuid, packageName, timeApproved);
+    }
+
+    /**
+     * Creates and caches {@link AtomicFile} object that represents the back-up file for the given
+     * user.
+     * <p>
+     * IMPORTANT: the method will ALWAYS return the same {@link AtomicFile} object, which makes it
+     * possible to synchronize reads and writes to the file using the returned object.
+     */
+    @NonNull
+    private AtomicFile getStorageFileForUser(@UserIdInt int userId) {
+        return mUserIdToStorageFile.computeIfAbsent(userId,
+                u -> createStorageFileForUser(userId, FILE_NAME));
+    }
+
+    /**
+     * @return A list of ObservableUuids per package.
+     */
+    public List<ObservableUuid> getObservableUuidsForPackage(
+            @UserIdInt int userId, @NonNull String packageName) {
+        final List<ObservableUuid> uuidsTobeObservedPerPackage = new ArrayList<>();
+        synchronized (mLock) {
+            final List<ObservableUuid> uuids = readObservableUuidsFromCache(userId);
+
+            for (ObservableUuid uuid : uuids) {
+                if (uuid.getPackageName().equals(packageName)) {
+                    uuidsTobeObservedPerPackage.add(uuid);
+                }
+            }
+        }
+
+        return uuidsTobeObservedPerPackage;
+    }
+
+    /**
+     * @return A list of ObservableUuids per user.
+     */
+    public List<ObservableUuid> getObservableUuidsForUser(@UserIdInt int userId) {
+        synchronized (mLock) {
+            return readObservableUuidsFromCache(userId);
+        }
+    }
+}
diff --git a/services/companion/java/com/android/server/companion/PermissionsUtils.java b/services/companion/java/com/android/server/companion/PermissionsUtils.java
index f4e14df..15bebba 100644
--- a/services/companion/java/com/android/server/companion/PermissionsUtils.java
+++ b/services/companion/java/com/android/server/companion/PermissionsUtils.java
@@ -19,6 +19,7 @@
 import static android.Manifest.permission.INTERACT_ACROSS_USERS;
 import static android.Manifest.permission.MANAGE_COMPANION_DEVICES;
 import static android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED;
+import static android.Manifest.permission.REQUEST_OBSERVE_DEVICE_UUID_PRESENCE;
 import static android.app.AppOpsManager.MODE_ALLOWED;
 import static android.companion.AssociationRequest.DEVICE_PROFILE_APP_STREAMING;
 import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION;
@@ -174,6 +175,14 @@
                 + " for u" + userId + "/" + packageName);
     }
 
+    static void enforceCallerCanObservingDevicePresenceByUuid(@NonNull Context context) {
+        if (context.checkCallingPermission(REQUEST_OBSERVE_DEVICE_UUID_PRESENCE)
+                != PERMISSION_GRANTED) {
+            throw new SecurityException("Caller (uid=" + getCallingUid() + ") does not have "
+                    + "permissions to request observing device presence base on the UUID");
+        }
+    }
+
     /**
      * Check if the caller is either:
      * <ul>
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 6ba85bd..7eca119 100644
--- a/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java
+++ b/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java
@@ -16,6 +16,9 @@
 
 package com.android.server.companion.presence;
 
+import static android.companion.DevicePresenceEvent.EVENT_BT_CONNECTED;
+import static android.companion.DevicePresenceEvent.EVENT_BT_DISCONNECTED;
+
 import static com.android.server.companion.presence.CompanionDevicePresenceMonitor.DEBUG;
 import static com.android.server.companion.presence.Utils.btDeviceToString;
 
@@ -27,6 +30,7 @@
 import android.net.MacAddress;
 import android.os.Handler;
 import android.os.HandlerExecutor;
+import android.os.ParcelUuid;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.util.Log;
@@ -35,8 +39,11 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.server.companion.AssociationStore;
+import com.android.server.companion.ObservableUuid;
+import com.android.server.companion.ObservableUuidStore;
 
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -53,6 +60,8 @@
         void onBluetoothCompanionDeviceConnected(int associationId);
 
         void onBluetoothCompanionDeviceDisconnected(int associationId);
+
+        void onDevicePresenceEventByUuid(ObservableUuid uuid, int event);
     }
 
     private final UserManager mUserManager;
@@ -61,6 +70,8 @@
     /** A set of ALL connected BT device (not only companion.) */
     private final @NonNull Map<MacAddress, BluetoothDevice> mAllConnectedDevices = new HashMap<>();
 
+    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.
@@ -70,8 +81,10 @@
     final SparseArray<Set<BluetoothDevice>> mPendingConnectedDevices = new SparseArray<>();
 
     BluetoothCompanionDeviceConnectionListener(UserManager userManager,
-            @NonNull AssociationStore associationStore, @NonNull Callback callback) {
+            @NonNull AssociationStore associationStore,
+            @NonNull ObservableUuidStore observableUuidStore, @NonNull Callback callback) {
         mAssociationStore = associationStore;
+        mObservableUuidStore = observableUuidStore;
         mCallback = callback;
         mUserManager = userManager;
     }
@@ -109,7 +122,6 @@
                 bluetoothDevices.add(device);
                 mPendingConnectedDevices.put(userId, bluetoothDevices);
             }
-
         } else {
             onDeviceConnectivityChanged(device, true);
         }
@@ -155,8 +167,13 @@
     }
 
     private void onDeviceConnectivityChanged(@NonNull BluetoothDevice device, boolean connected) {
+        int userId = UserHandle.myUserId();
         final List<AssociationInfo> associations =
                 mAssociationStore.getAssociationsByAddress(device.getAddress());
+        final List<ObservableUuid> observableUuids =
+                mObservableUuidStore.getObservableUuidsForUser(userId);
+        final List<ParcelUuid> deviceUuids = device.getUuids() == null
+                ? Collections.emptyList() : Arrays.asList(device.getUuids());
 
         if (DEBUG) {
             Log.d(TAG, "onDevice_ConnectivityChanged() " + btDeviceToString(device)
@@ -177,6 +194,14 @@
                 mCallback.onBluetoothCompanionDeviceDisconnected(id);
             }
         }
+
+        for (ObservableUuid uuid : observableUuids) {
+            if (deviceUuids.contains(uuid.getUuid())) {
+                mCallback.onDevicePresenceEventByUuid(
+                        uuid, connected ? EVENT_BT_CONNECTED
+                                : EVENT_BT_DISCONNECTED);
+            }
+        }
     }
 
     @Override
diff --git a/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java b/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java
index e42b935..54a4692 100644
--- a/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java
+++ b/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java
@@ -16,12 +16,12 @@
 
 package com.android.server.companion.presence;
 
-import static android.companion.CompanionDeviceService.DEVICE_EVENT_BLE_APPEARED;
-import static android.companion.CompanionDeviceService.DEVICE_EVENT_BLE_DISAPPEARED;
-import static android.companion.CompanionDeviceService.DEVICE_EVENT_BT_CONNECTED;
-import static android.companion.CompanionDeviceService.DEVICE_EVENT_BT_DISCONNECTED;
-import static android.companion.CompanionDeviceService.DEVICE_EVENT_SELF_MANAGED_APPEARED;
-import static android.companion.CompanionDeviceService.DEVICE_EVENT_SELF_MANAGED_DISAPPEARED;
+import static android.companion.DevicePresenceEvent.EVENT_BLE_APPEARED;
+import static android.companion.DevicePresenceEvent.EVENT_BLE_DISAPPEARED;
+import static android.companion.DevicePresenceEvent.EVENT_BT_CONNECTED;
+import static android.companion.DevicePresenceEvent.EVENT_BT_DISCONNECTED;
+import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_APPEARED;
+import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_DISAPPEARED;
 import static android.os.Process.ROOT_UID;
 import static android.os.Process.SHELL_UID;
 
@@ -36,12 +36,15 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
+import android.os.ParcelUuid;
 import android.os.UserManager;
 import android.util.Log;
 import android.util.Slog;
 import android.util.SparseArray;
 
 import com.android.server.companion.AssociationStore;
+import com.android.server.companion.ObservableUuid;
+import com.android.server.companion.ObservableUuidStore;
 
 import java.io.PrintWriter;
 import java.util.HashSet;
@@ -61,7 +64,7 @@
  * <li> {@link #isDevicePresent(int)}
  * <li> {@link Callback#onDeviceAppeared(int) Callback.onDeviceAppeared(int)}
  * <li> {@link Callback#onDeviceDisappeared(int) Callback.onDeviceDisappeared(int)}
- * <li> {@link Callback#onDeviceStateChanged(int, int)}}
+ * <li> {@link Callback#onDevicePresenceEvent(int, int)}}
  * </ul>
  */
 @SuppressLint("LongLogTag")
@@ -78,11 +81,15 @@
         /** Invoked when a companion device no longer seen nearby or disconnects. */
         void onDeviceDisappeared(int associationId);
 
-        /**Invoked when device has corresponding event changes. */
-        void onDeviceEvent(int associationId, int event);
+        /** Invoked when device has corresponding event changes. */
+        void onDevicePresenceEvent(int associationId, int event);
+
+        /** Invoked when device has corresponding event changes base on the UUID */
+        void onDevicePresenceEventByUuid(ObservableUuid uuid, int event);
     }
 
     private final @NonNull AssociationStore mAssociationStore;
+    private final @NonNull ObservableUuidStore mObservableUuidStore;
     private final @NonNull Callback mCallback;
     private final @NonNull BluetoothCompanionDeviceConnectionListener mBtConnectionListener;
     private final @NonNull BleCompanionDeviceScanner mBleScanner;
@@ -94,6 +101,7 @@
     private final @NonNull Set<Integer> mConnectedBtDevices = new HashSet<>();
     private final @NonNull Set<Integer> mNearbyBleDevices = new HashSet<>();
     private final @NonNull Set<Integer> mReportedSelfManagedDevices = new HashSet<>();
+    private final @NonNull Set<ParcelUuid> mConnectedUuidDevices = new HashSet<>();
 
     // Tracking "simulated" presence. Used for debugging and testing only.
     private final @NonNull Set<Integer> mSimulated = new HashSet<>();
@@ -101,11 +109,14 @@
             new SimulatedDevicePresenceSchedulerHelper();
 
     public CompanionDevicePresenceMonitor(UserManager userManager,
-            @NonNull AssociationStore associationStore, @NonNull Callback callback) {
+            @NonNull AssociationStore associationStore,
+            @NonNull ObservableUuidStore observableUuidStore, @NonNull Callback callback) {
         mAssociationStore = associationStore;
+        mObservableUuidStore = observableUuidStore;
         mCallback = callback;
         mBtConnectionListener = new BluetoothCompanionDeviceConnectionListener(userManager,
-                associationStore, /* BluetoothCompanionDeviceConnectionListener.Callback */ this);
+                associationStore, mObservableUuidStore,
+                /* BluetoothCompanionDeviceConnectionListener.Callback */ this);
         mBleScanner = new BleCompanionDeviceScanner(associationStore,
                 /* BleCompanionDeviceScanner.Callback */ this);
     }
@@ -126,6 +137,20 @@
     }
 
     /**
+     * @return current connected UUID devices.
+     */
+    public Set<ParcelUuid> getCurrentConnectedUuidDevices() {
+        return mConnectedUuidDevices;
+    }
+
+    /**
+     * Remove current connected UUID device.
+     */
+    public void removeCurrentConnectedUuidDevice(ParcelUuid uuid) {
+        mConnectedUuidDevices.remove(uuid);
+    }
+
+    /**
      * @return whether the associated companion devices is present. I.e. device is nearby (for BLE);
      *         or devices is connected (for Bluetooth); or reported (by the application) to be
      *         nearby (for "self-managed" associations).
@@ -138,6 +163,13 @@
     }
 
     /**
+     * @return whether the current uuid to be observed is present.
+     */
+    public boolean isDeviceUuidPresent(ParcelUuid uuid) {
+        return mConnectedUuidDevices.contains(uuid);
+    }
+
+    /**
      * @return whether the current device is BT connected and had already reported to the app.
      */
 
@@ -169,8 +201,8 @@
      * {@link android.companion.CompanionDeviceManager#notifyDeviceAppeared(int) notifyDeviceAppeared()}
      */
     public void onSelfManagedDeviceConnected(int associationId) {
-        onDeviceEvent(mReportedSelfManagedDevices,
-                associationId, DEVICE_EVENT_SELF_MANAGED_APPEARED);
+        onDevicePresenceEvent(mReportedSelfManagedDevices,
+                associationId, EVENT_SELF_MANAGED_APPEARED);
     }
 
     /**
@@ -183,23 +215,23 @@
      * {@link android.companion.CompanionDeviceManager#notifyDeviceDisappeared(int) notifyDeviceDisappeared()}
      */
     public void onSelfManagedDeviceDisconnected(int associationId) {
-        onDeviceEvent(mReportedSelfManagedDevices,
-                associationId, DEVICE_EVENT_SELF_MANAGED_DISAPPEARED);
+        onDevicePresenceEvent(mReportedSelfManagedDevices,
+                associationId, EVENT_SELF_MANAGED_DISAPPEARED);
     }
 
     /**
      * Marks a "self-managed" device as disconnected when binderDied.
      */
     public void onSelfManagedDeviceReporterBinderDied(int associationId) {
-        onDeviceEvent(mReportedSelfManagedDevices,
-                associationId, DEVICE_EVENT_SELF_MANAGED_DISAPPEARED);
+        onDevicePresenceEvent(mReportedSelfManagedDevices,
+                associationId, EVENT_SELF_MANAGED_DISAPPEARED);
     }
 
     @Override
     public void onBluetoothCompanionDeviceConnected(int associationId) {
         Slog.i(TAG, "onBluetoothCompanionDeviceConnected: "
                 + "associationId( " + associationId + " )");
-        onDeviceEvent(mConnectedBtDevices, associationId, DEVICE_EVENT_BT_CONNECTED);
+        onDevicePresenceEvent(mConnectedBtDevices, associationId, EVENT_BT_CONNECTED);
         // Stop scanning for BLE devices when this device is connected
         // and there are no other devices to connect to.
         if (canStopBleScan()) {
@@ -214,22 +246,53 @@
         // Start BLE scanning when the device is disconnected.
         mBleScanner.startScan();
 
-        onDeviceEvent(mConnectedBtDevices, associationId, DEVICE_EVENT_BT_DISCONNECTED);
+        onDevicePresenceEvent(mConnectedBtDevices, associationId, EVENT_BT_DISCONNECTED);
     }
 
     @Override
+    public void onDevicePresenceEventByUuid(ObservableUuid uuid, int event) {
+        final ParcelUuid parcelUuid = uuid.getUuid();
+
+        switch(event) {
+            case EVENT_BT_CONNECTED:
+                boolean added = mConnectedUuidDevices.add(parcelUuid);
+
+                if (!added) {
+                    Slog.w(TAG, "Uuid= " + parcelUuid + "is ALREADY reported as "
+                            + "present by this event=" + event);
+                }
+
+                break;
+            case EVENT_BT_DISCONNECTED:
+                final boolean removed = mConnectedUuidDevices.remove(parcelUuid);
+
+                if (!removed) {
+                    Slog.w(TAG, "UUID= " + parcelUuid + " was NOT reported "
+                            + "as present by this event= " + event);
+
+                    return;
+                }
+
+                break;
+        }
+
+        mCallback.onDevicePresenceEventByUuid(uuid, event);
+    }
+
+
+    @Override
     public void onBleCompanionDeviceFound(int associationId) {
-        onDeviceEvent(mNearbyBleDevices, associationId, DEVICE_EVENT_BLE_APPEARED);
+        onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_APPEARED);
     }
 
     @Override
     public void onBleCompanionDeviceLost(int associationId) {
-        onDeviceEvent(mNearbyBleDevices, associationId, DEVICE_EVENT_BLE_DISAPPEARED);
+        onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_DISAPPEARED);
     }
 
     /** FOR DEBUGGING AND/OR TESTING PURPOSES ONLY. */
     @TestApi
-    public void simulateDeviceEvent(int associationId, int state) {
+    public void simulateDeviceEvent(int associationId, int event) {
         // IMPORTANT: this API should only be invoked via the
         // 'companiondevice simulate-device-appeared' Shell command, so the only uid-s allowed to
         // make this call are SHELL and ROOT.
@@ -238,32 +301,43 @@
         // Make sure the association exists.
         enforceAssociationExists(associationId);
 
-        switch (state) {
-            case DEVICE_EVENT_BLE_APPEARED:
-                simulateDeviceAppeared(associationId, state);
+        switch (event) {
+            case EVENT_BLE_APPEARED:
+                simulateDeviceAppeared(associationId, event);
                 break;
-            case DEVICE_EVENT_BT_CONNECTED:
+            case EVENT_BT_CONNECTED:
                 onBluetoothCompanionDeviceConnected(associationId);
                 break;
-            case DEVICE_EVENT_BLE_DISAPPEARED:
-                simulateDeviceDisappeared(associationId, state);
+            case EVENT_BLE_DISAPPEARED:
+                simulateDeviceDisappeared(associationId, event);
                 break;
-            case DEVICE_EVENT_BT_DISCONNECTED:
+            case EVENT_BT_DISCONNECTED:
                 onBluetoothCompanionDeviceDisconnected(associationId);
                 break;
             default:
-                throw new IllegalArgumentException("State: " + state + "is not supported");
+                throw new IllegalArgumentException("Event: " + event + "is not supported");
         }
     }
 
+    /** FOR DEBUGGING AND/OR TESTING PURPOSES ONLY. */
+    @TestApi
+    public void simulateDeviceEventByUuid(ObservableUuid uuid, int event) {
+        // IMPORTANT: this API should only be invoked via the
+        // 'companiondevice simulate-device-uuid-events' 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();
+        onDevicePresenceEventByUuid(uuid, event);
+    }
+
     private void simulateDeviceAppeared(int associationId, int state) {
-        onDeviceEvent(mSimulated, associationId, state);
+        onDevicePresenceEvent(mSimulated, associationId, state);
         mSchedulerHelper.scheduleOnDeviceGoneCallForSimulatedDevicePresence(associationId);
     }
 
     private void simulateDeviceDisappeared(int associationId, int state) {
         mSchedulerHelper.unscheduleOnDeviceGoneCallForSimulatedDevicePresence(associationId);
-        onDeviceEvent(mSimulated, associationId, state);
+        onDevicePresenceEvent(mSimulated, associationId, state);
     }
 
     private void enforceAssociationExists(int associationId) {
@@ -273,14 +347,14 @@
         }
     }
 
-    private void onDeviceEvent(@NonNull Set<Integer> presentDevicesForSource,
+    private void onDevicePresenceEvent(@NonNull Set<Integer> presentDevicesForSource,
             int associationId, int event) {
-        Slog.i(TAG, "onDeviceEvent() id=" + associationId + ", state=" + event);
+        Slog.i(TAG, "onDevicePresenceEvent() id=" + associationId + ", event=" + event);
 
         switch (event) {
-            case DEVICE_EVENT_BLE_APPEARED:
-            case DEVICE_EVENT_BT_CONNECTED:
-            case DEVICE_EVENT_SELF_MANAGED_APPEARED:
+            case EVENT_BLE_APPEARED:
+            case EVENT_BT_CONNECTED:
+            case EVENT_SELF_MANAGED_APPEARED:
                 final boolean added = presentDevicesForSource.add(associationId);
 
                 if (!added) {
@@ -292,9 +366,9 @@
                 mCallback.onDeviceAppeared(associationId);
 
                 break;
-            case DEVICE_EVENT_BLE_DISAPPEARED:
-            case DEVICE_EVENT_BT_DISCONNECTED:
-            case DEVICE_EVENT_SELF_MANAGED_DISAPPEARED:
+            case EVENT_BLE_DISAPPEARED:
+            case EVENT_BT_DISCONNECTED:
+            case EVENT_SELF_MANAGED_DISAPPEARED:
                 final boolean removed = presentDevicesForSource.remove(associationId);
 
                 if (!removed) {
@@ -312,7 +386,7 @@
                 return;
         }
 
-        mCallback.onDeviceEvent(associationId, event);
+        mCallback.onDevicePresenceEvent(associationId, event);
     }
 
     /**
@@ -436,7 +510,7 @@
         public void handleMessage(@NonNull Message msg) {
             final int associationId = msg.what;
             if (mSimulated.contains(associationId)) {
-                onDeviceEvent(mSimulated, associationId, DEVICE_EVENT_BLE_DISAPPEARED);
+                onDevicePresenceEvent(mSimulated, associationId, EVENT_BLE_DISAPPEARED);
             }
         }
     }