Scanner client side code

Add NearbyManager, IScanListener.aidl, INearbyManager.aidl,
NearbyDevice, FastPairDevice, ScanCallback which returns NearbyDevice to callers
and  NearbyDeviceParcelable for service and client communication.

Test: Successfully built. Unit test will be added.
Bug: 189954300
Change-Id: I71366f888e245f1175a67b6d4040898e35f49c35
diff --git a/nearby/framework/java/android/nearby/FastPairDevice.java b/nearby/framework/java/android/nearby/FastPairDevice.java
new file mode 100644
index 0000000..3f1fd42
--- /dev/null
+++ b/nearby/framework/java/android/nearby/FastPairDevice.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (C) 2007 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.nearby;
+
+import android.annotation.Hide;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * A class represents a Fast Pair device that can be discovered by multiple mediums.
+ *
+ * @hide
+ */
+public class FastPairDevice extends NearbyDevice implements Parcelable {
+    public static final Creator<FastPairDevice> CREATOR = new Creator<FastPairDevice>() {
+        @Override
+        public FastPairDevice createFromParcel(Parcel in) {
+            FastPairDevice.Builder builder = new FastPairDevice.Builder();
+            if (in.readInt() == 1) {
+                builder.setName(in.readString());
+            }
+            builder.setMedium(in.readInt());
+            builder.setRssi(in.readInt());
+            if (in.readInt() == 1) {
+                builder.setModelId(in.readString());
+            }
+            builder.setBluetoothAddress(in.readString());
+            if (in.readInt() == 1) {
+                int dataLength = in.readInt();
+                byte[] data = new byte[dataLength];
+                in.readByteArray(data);
+                builder.setData(data);
+            }
+            return builder.build();
+        }
+
+        @Override
+        public FastPairDevice[] newArray(int size) {
+            return new FastPairDevice[size];
+        }
+    };
+
+    // Some OEM devices devices don't have model Id.
+    @Nullable private final String mModelId;
+
+    // Bluetooth hardware address as string. Can be read from BLE ScanResult.
+    private final String mBluetoothAddress;
+
+    @Nullable
+    private final byte[] mData;
+
+    public FastPairDevice(@Nullable String name,
+            @Medium int medium,
+            int rssi,
+            @Nullable String modelId,
+            @NonNull String bluetoothAddress,
+            @Nullable byte[] data) {
+        super(name, medium, rssi);
+        this.mModelId = modelId;
+        this.mBluetoothAddress = bluetoothAddress;
+        this.mData = data;
+    }
+
+    @Nullable
+    @Override
+    public String getName() {
+        return mName;
+    }
+
+    @Override
+    public int getMedium() {
+        return mMedium;
+    }
+
+    @Override
+    public float getRssi() {
+        return mRssi;
+    }
+
+    @Nullable
+    public String getModelId() {
+        return this.mModelId;
+    }
+
+    @NonNull
+    public String getBluetoothAddress() {
+        return mBluetoothAddress;
+    }
+
+    // Only visible to system clients.
+    @Nullable
+    @Hide
+    public byte[] getData() {
+        return mData;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder stringBuilder = new StringBuilder();
+        stringBuilder.append("FastPairDevice [");
+        if (mName != null && !mName.isEmpty()) {
+            stringBuilder.append("name=").append(mName).append(", ");
+        }
+        stringBuilder.append("medium=").append(mediumToString(mMedium));
+        stringBuilder.append(" rssi=").append(mRssi);
+        stringBuilder.append(" modelId=").append(mModelId);
+        stringBuilder.append(" bluetoothAddress=").append(mBluetoothAddress);
+        stringBuilder.append("]");
+        return stringBuilder.toString();
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (other instanceof FastPairDevice) {
+            FastPairDevice otherDevice = (FastPairDevice) other;
+            if (!super.equals(other)) {
+                return false;
+            }
+            return Objects.equals(mModelId, otherDevice.mModelId)
+                    && Objects.equals(mBluetoothAddress, otherDevice.mBluetoothAddress)
+                    && Arrays.equals(mData, otherDevice.mData);
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(
+                mName, mMedium, mRssi, mModelId, mBluetoothAddress, Arrays.hashCode(mData));
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mName == null ? 0 : 1);
+        if (mName != null) {
+            dest.writeString(mName);
+        }
+        dest.writeInt(mMedium);
+        dest.writeInt(mRssi);
+        dest.writeInt(mModelId == null ? 0 : 1);
+        if (mModelId != null) {
+            dest.writeString(mModelId);
+        }
+        dest.writeString(mBluetoothAddress);
+        dest.writeInt(mData == null ? 0 : 1);
+        if (mData != null) {
+            dest.writeInt(mData.length);
+            dest.writeByteArray(mData);
+        }
+    }
+
+    /**
+     * A builder class for {@link FastPairDevice}
+     *
+     * @hide
+     */
+    public static final class Builder extends NearbyDevice.Builder {
+
+        @Nullable private String mName;
+        @Medium private int mMedium;
+        private int mRssi;
+        @Nullable private String mModelId;
+        private String mBluetoothAddress;
+        @Nullable private byte[] mData;
+        /**
+         * Sets the name of the Fast Pair device.
+         */
+        @NonNull
+        public Builder setName(@Nullable String name) {
+            mName = name;
+            return this;
+        }
+
+        /**
+         * Sets the medium over which the Fast Pair device is discovered.
+         */
+        @NonNull
+        public Builder setMedium(@Medium int medium) {
+            mMedium = medium;
+            return this;
+        }
+
+        /**
+         * Sets the RSSI between the scan device and the discovered Fast Pair device.
+         */
+        @NonNull
+        public Builder setRssi(int rssi) {
+            mRssi = rssi;
+            return this;
+        }
+
+        /**
+         * Sets the model Id of this Fast Pair device.
+         */
+        @NonNull
+        public Builder setModelId(String modelId) {
+            mModelId = modelId;
+            return this;
+        }
+
+        /**
+         * Sets the hardware address of this BluetoothDevice.
+         */
+        @NonNull
+        public Builder setBluetoothAddress(@NonNull String maskedBluetoothAddress) {
+            mBluetoothAddress = maskedBluetoothAddress;
+            return this;
+        }
+
+        /**
+         * Sets the raw data. Only visible to system API.
+         * @hide
+         */
+        @Hide
+        @NonNull
+        public Builder setData(byte[] data) {
+            mData = data;
+            return this;
+        }
+
+        /**
+         * Builds a FastPairDevice and return it.
+         */
+        @NonNull
+        public FastPairDevice build() {
+            return new FastPairDevice(mName, mMedium, mRssi, mModelId,
+                    mBluetoothAddress, mData);
+        }
+    }
+}
diff --git a/nearby/framework/java/android/nearby/INearbyManager.aidl b/nearby/framework/java/android/nearby/INearbyManager.aidl
index 031c31d..cd05106 100644
--- a/nearby/framework/java/android/nearby/INearbyManager.aidl
+++ b/nearby/framework/java/android/nearby/INearbyManager.aidl
@@ -1,23 +1,32 @@
-// Copyright (C) 2021 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
+/*
+ * Copyright (C) 2007, 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.nearby;
 
+import android.nearby.IScanListener;
+import android.nearby.ScanRequest;
+
 /**
  * Interface for communicating with the nearby services.
  *
  * @hide
  */
 interface INearbyManager {
+
+    void registerScanListener(in ScanRequest scanRequest, in IScanListener listener);
+
+    void unregisterScanListener(in IScanListener listener);
 }
\ No newline at end of file
diff --git a/nearby/framework/java/android/nearby/IScanListener.aidl b/nearby/framework/java/android/nearby/IScanListener.aidl
new file mode 100644
index 0000000..8747b07
--- /dev/null
+++ b/nearby/framework/java/android/nearby/IScanListener.aidl
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2007, 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.nearby;
+
+import android.nearby.NearbyDeviceParcelable;
+
+/**
+ * Binder callback for ScanCallback.
+ *
+ * {@hide}
+ */
+oneway interface IScanListener {
+        /** Reports a {@link NearbyDevice} being discovered. */
+        void onDiscovered(in NearbyDeviceParcelable nearbyDeviceParcelable);
+
+        /** Reports a {@link NearbyDevice} information(distance, packet, and etc) changed. */
+        void onUpdated(in NearbyDeviceParcelable nearbyDeviceParcelable);
+
+        /** Reports a {@link NearbyDevice} is no longer within range. */
+        void onLost(in NearbyDeviceParcelable nearbyDeviceParcelable);
+}
diff --git a/nearby/framework/java/android/nearby/NearbyDevice.java b/nearby/framework/java/android/nearby/NearbyDevice.java
new file mode 100644
index 0000000..0d82530
--- /dev/null
+++ b/nearby/framework/java/android/nearby/NearbyDevice.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Objects;
+
+/**
+ * A class represents a device that can be discovered by multiple mediums.
+ *
+ * @hide
+ */
+public abstract class NearbyDevice {
+
+    @Nullable
+    final String mName;
+
+    @Medium
+    final int mMedium;
+
+    final int mRssi;
+
+    /**
+     * @hide
+     */
+    public NearbyDevice(@Nullable String name, @Medium int medium, int rssi) {
+        Preconditions.checkState(isValidMedium(medium),
+                "Not supported medium: " + medium
+                        + ", scan medium must be one of NearbyDevice#Medium.");
+        mName = name;
+        mMedium = medium;
+        mRssi = rssi;
+    }
+
+    static String mediumToString(@Medium int medium) {
+        switch (medium) {
+            case Medium.BLE:
+                return "BLE";
+            case Medium.BLUETOOTH:
+                return "Bluetooth Classic";
+            default:
+                return "Unknown";
+        }
+    }
+
+    /**
+     * True if the medium is defined in {@link Medium}.
+     */
+    public static boolean isValidMedium(@Medium int medium) {
+        return medium == Medium.BLE
+                || medium == Medium.BLUETOOTH;
+    }
+
+    /**
+     * The name of the device, or null if not available.
+     *
+     * @hide
+     */
+    @Nullable
+    public String getName() {
+        return mName;
+    }
+
+    /** The medium over which this device was discovered. */
+    @Medium
+    public int getMedium() {
+        return mMedium;
+    }
+
+    /**
+     * The RSSI this device is away.
+     */
+    public float getRssi() {
+        return mRssi;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder stringBuilder = new StringBuilder();
+        stringBuilder.append("NearbyDevice [");
+        if (mName != null && !mName.isEmpty()) {
+            stringBuilder.append("name=").append(mName).append(", ");
+        }
+        stringBuilder.append("medium=").append(mediumToString(mMedium));
+        stringBuilder.append(" rssi=").append(mRssi);
+        stringBuilder.append("]");
+        return stringBuilder.toString();
+    }
+
+    @Override
+    public boolean equals(Object other) {
+        if (other instanceof NearbyDevice) {
+            NearbyDevice otherDevice = (NearbyDevice) other;
+            return Objects.equals(mName, otherDevice.mName)
+                    && mMedium == otherDevice.mMedium
+                    && mRssi == otherDevice.mRssi;
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mName, mMedium, mRssi);
+    }
+
+    /**
+     * The medium where a NearbyDevice was discovered on.
+     *
+     * @hide
+     */
+    @IntDef({Medium.BLE, Medium.BLUETOOTH})
+    public @interface Medium {
+        int BLE = 1;
+        int BLUETOOTH = 2;
+    }
+
+    /**
+     * Builder for a NearbyDevice.
+     */
+    public abstract static class Builder {
+
+        /**
+         * Sets the name of Nearby Device.
+         */
+        public abstract Builder setName(String name);
+
+        /**
+         * Sets the medium over which the Nearby Device is discovered.
+         */
+        public abstract Builder setMedium(int medium);
+
+        /**
+         * Sets the RSSI between scanned device and the discovered device.
+         */
+        public abstract Builder setRssi(int rssi);
+
+        /**
+         * Builds the Nearby Device.
+         */
+        public abstract NearbyDevice build();
+    }
+}
+
diff --git a/nearby/framework/java/android/nearby/NearbyDeviceParcelable.aidl b/nearby/framework/java/android/nearby/NearbyDeviceParcelable.aidl
new file mode 100644
index 0000000..e211187
--- /dev/null
+++ b/nearby/framework/java/android/nearby/NearbyDeviceParcelable.aidl
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2012, 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.nearby;
+
+parcelable NearbyDeviceParcelable;
+
diff --git a/nearby/framework/java/android/nearby/NearbyDeviceParcelable.java b/nearby/framework/java/android/nearby/NearbyDeviceParcelable.java
new file mode 100644
index 0000000..6910d13
--- /dev/null
+++ b/nearby/framework/java/android/nearby/NearbyDeviceParcelable.java
@@ -0,0 +1,214 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A data class representing scan result from Nearby Service. Scan result can come from multiple
+ * mediums like BLE, Wi-Fi Aware, and etc.
+ * A scan result  consists of
+ * An encapsulation of various parameters for requesting nearby scans.
+ *
+ * <p>All scan results generated through {@link NearbyManager} are guaranteed to have a valid
+ * medium, identifier, timestamp (both UTC time and elapsed real-time since boot), and accuracy.
+ * All other parameters are optional.
+ *
+ * @hide
+ */
+public final class NearbyDeviceParcelable implements Parcelable {
+    public static final Creator<NearbyDeviceParcelable> CREATOR =
+            new Creator<NearbyDeviceParcelable>() {
+                @Override
+                public NearbyDeviceParcelable createFromParcel(Parcel in) {
+                    Builder builder = new Builder();
+                    if (in.readInt() == 1) {
+                        builder.setName(in.readString());
+                    }
+                    builder.setMedium(in.readInt());
+                    builder.setRssi(in.readInt());
+                    if (in.readInt() == 1) {
+                        builder.setFastPairModelId(in.readString());
+                    }
+                    if (in.readInt() == 1) {
+                        int dataLength = in.readInt();
+                        byte[] data = new byte[dataLength];
+                        in.readByteArray(data);
+                        builder.setData(data);
+                    }
+                    return builder.build();
+                }
+
+                @Override
+                public NearbyDeviceParcelable[] newArray(int size) {
+                    return new NearbyDeviceParcelable[size];
+                }
+            };
+    @Nullable
+    private final String mName;
+    @NearbyDevice.Medium
+    private final int mMedium;
+    private final int mRssi;
+
+    @Nullable
+    private final String mBluetoothAddress;
+    @Nullable
+    private final String mFastPairModelId;
+    @Nullable
+    private final byte[] mData;
+
+    private NearbyDeviceParcelable(@Nullable String name, int medium, int rssi,
+            @Nullable String fastPairModelId, @Nullable String bluetoothAddress, byte[] data) {
+        mName = name;
+        mMedium = medium;
+        mRssi = rssi;
+        mFastPairModelId = fastPairModelId;
+        mBluetoothAddress = bluetoothAddress;
+        mData = data;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeInt(mName == null ? 0 : 1);
+        if (mName != null) {
+            dest.writeString(mName);
+        }
+        dest.writeInt(mMedium);
+        dest.writeInt(mRssi);
+        dest.writeInt(mFastPairModelId == null ? 0 : 1);
+        if (mFastPairModelId != null) {
+            dest.writeString(mFastPairModelId);
+        }
+        dest.writeInt(mBluetoothAddress == null ? 0 : 1);
+        if (mFastPairModelId != null) {
+            dest.writeString(mBluetoothAddress);
+        }
+        dest.writeInt(mData == null ? 0 : 1);
+        if (mData != null) {
+            dest.writeInt(mData.length);
+            dest.writeByteArray(mData);
+        }
+    }
+
+    public String getName() {
+        return mName;
+    }
+
+    public int getMedium() {
+        return mMedium;
+    }
+
+    public int getRssi() {
+        return mRssi;
+    }
+
+    @Nullable
+    public String getFastPairModelId() {
+        return mFastPairModelId;
+    }
+
+    @Nullable
+    public String getBluetoothAddress() {
+        return mBluetoothAddress;
+    }
+
+    public byte[] getData() {
+        return mData;
+    }
+
+    /**
+     * Builder class for {@link NearbyDeviceParcelable}.
+     */
+    public static final class Builder {
+        @Nullable
+        private String mName;
+        @NearbyDevice.Medium
+        private int mMedium;
+        private int mRssi;
+        @Nullable
+        private String mFastPairModelId;
+        @Nullable
+        private String mBluetoothAddress;
+        @Nullable
+        private byte[] mData;
+
+        /**
+         * Sets the name of the scanned device.
+         */
+        public Builder setName(String name) {
+            mName = name;
+            return this;
+        }
+
+        /**
+         * Sets the medium over which the device is discovered.
+         */
+        public Builder setMedium(int medium) {
+            mMedium = medium;
+            return this;
+        }
+
+        /**
+         * Sets the RSSI between scanned device and the discovered device.
+         */
+        public Builder setRssi(int rssi) {
+            mRssi = rssi;
+            return this;
+        }
+
+        /**
+         * Sets the identifier of the device.
+         */
+        public Builder setFastPairModelId(String fastPairModelId) {
+            mFastPairModelId = fastPairModelId;
+            return this;
+        }
+
+        /**
+         * Sets the scanned Fast Pair data.
+         */
+        public Builder setBluetoothAddress(String bluetoothAddress) {
+            mBluetoothAddress = bluetoothAddress;
+            return this;
+        }
+
+        /**
+         * Sets the raw data.
+         */
+        public Builder setData(byte[] data) {
+            mData = data;
+            return this;
+        }
+
+        /**
+         * Builds a ScanResult.
+         */
+        public NearbyDeviceParcelable build() {
+            return new NearbyDeviceParcelable(mName, mMedium, mRssi, mFastPairModelId,
+                    mBluetoothAddress, mData);
+        }
+    }
+}
diff --git a/nearby/framework/java/android/nearby/NearbyManager.java b/nearby/framework/java/android/nearby/NearbyManager.java
index b75e640..d8f6917 100644
--- a/nearby/framework/java/android/nearby/NearbyManager.java
+++ b/nearby/framework/java/android/nearby/NearbyManager.java
@@ -16,6 +16,19 @@
 
 package android.nearby;
 
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.RemoteException;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+
+import java.lang.ref.WeakReference;
+import java.util.Objects;
+import java.util.WeakHashMap;
+import java.util.concurrent.Executor;
+
 /**
  * This class provides a way to perform Nearby related operations such as scanning and connecting
  * to nearby devices.
@@ -25,6 +38,139 @@
  *
  * @hide
  */
-// TODO(b/189954300): implement NearyManager API and unhide it.
 public class NearbyManager {
+
+    private static final String TAG = "NearbyManager";
+    @GuardedBy("sScanListeners")
+    private static final WeakHashMap<ScanCallback, WeakReference<ScanListenerTransport>>
+            sScanListeners = new WeakHashMap<>();
+    private final INearbyManager mService;
+
+    public NearbyManager(@NonNull INearbyManager service) {
+        mService = service;
+    }
+
+    private static NearbyDevice toClientNearbyDevice(
+            NearbyDeviceParcelable nearbyDeviceParcelable,
+            @ScanRequest.ScanType int scanType) {
+        if (scanType == ScanRequest.SCAN_TYPE_FAST_PAIR) {
+            return new FastPairDevice.Builder()
+                    .setName(nearbyDeviceParcelable.getName())
+                    .setMedium(nearbyDeviceParcelable.getMedium())
+                    .setRssi(nearbyDeviceParcelable.getRssi())
+                    .setModelId(nearbyDeviceParcelable.getFastPairModelId())
+                    .setBluetoothAddress(nearbyDeviceParcelable.getBluetoothAddress())
+                    .setData(nearbyDeviceParcelable.getData()).build();
+        }
+        return null;
+    }
+
+    /**
+     * Start scan for nearby devices with given parameters. Devices matching {@link ScanRequest}
+     * will be delivered through the given callback.
+     *
+     * @param executor Executor where the listener method is called.
+     */
+    public void startScan(ScanRequest scanRequest, ScanCallback scanCallback, @CallbackExecutor
+            Executor executor) {
+        Objects.requireNonNull(scanRequest, "scanRequest must not be null");
+        Objects.requireNonNull(scanCallback, "scanCallback must not be null");
+        Objects.requireNonNull(executor, "executor must not be null");
+
+        try {
+            synchronized (sScanListeners) {
+                WeakReference<ScanListenerTransport> reference = sScanListeners.get(scanCallback);
+                ScanListenerTransport transport = reference != null ? reference.get() : null;
+                if (transport == null) {
+                    transport = new ScanListenerTransport(scanRequest.getScanType(), scanCallback,
+                            executor);
+                } else {
+                    Preconditions.checkState(transport.isRegistered());
+                    transport.setExecutor(executor);
+                }
+                mService.registerScanListener(scanRequest, transport);
+                sScanListeners.put(scanCallback, new WeakReference<>(transport));
+            }
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Stops the nearby device scan for the specified callback. The given callback
+     * is guaranteed not to receive any invocations that happen after this method
+     * is invoked.
+     */
+    public void stopScan(@NonNull ScanCallback scanCallback) {
+        Preconditions.checkArgument(scanCallback != null,
+                "invalid null scanCallback");
+        try {
+            synchronized (sScanListeners) {
+                WeakReference<ScanListenerTransport> reference = sScanListeners.remove(
+                        scanCallback);
+                ScanListenerTransport transport = reference != null ? reference.get() : null;
+                if (transport != null) {
+                    transport.unregister();
+                    mService.unregisterScanListener(transport);
+                }
+            }
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    private static class ScanListenerTransport extends IScanListener.Stub {
+
+        private @ScanRequest.ScanType int mScanType;
+        private volatile @Nullable ScanCallback mScanCallback;
+        private Executor mExecutor;
+
+        ScanListenerTransport(@ScanRequest.ScanType int scanType, ScanCallback scanCallback,
+                @CallbackExecutor Executor executor) {
+            Preconditions.checkArgument(scanCallback != null,
+                    "invalid null callback");
+            Preconditions.checkState(ScanRequest.isValidScanType(mScanType),
+                    "invalid scan type : " + mScanType
+                            + ", scan type must be one of ScanRequest#SCAN_TYPE_");
+            mScanType = scanType;
+            mScanCallback = scanCallback;
+            mExecutor = executor;
+        }
+
+        void setExecutor(Executor executor) {
+            Preconditions.checkArgument(
+                    executor != null, "invalid null executor");
+            mExecutor = executor;
+        }
+
+        boolean isRegistered() {
+            return mScanCallback != null;
+        }
+
+        void unregister() {
+            mScanCallback = null;
+        }
+
+        @Override
+        public void onDiscovered(NearbyDeviceParcelable nearbyDeviceParcelable)
+                throws RemoteException {
+            mExecutor.execute(() -> mScanCallback.onDiscovered(
+                    toClientNearbyDevice(nearbyDeviceParcelable, mScanType)));
+        }
+
+        @Override
+        public void onUpdated(NearbyDeviceParcelable nearbyDeviceParcelable)
+                throws RemoteException {
+            mExecutor.execute(
+                    () -> mScanCallback.onUpdated(
+                            toClientNearbyDevice(nearbyDeviceParcelable, mScanType)));
+        }
+
+        @Override
+        public void onLost(NearbyDeviceParcelable nearbyDeviceParcelable) throws RemoteException {
+            mExecutor.execute(
+                    () -> mScanCallback.onLost(
+                            toClientNearbyDevice(nearbyDeviceParcelable, mScanType)));
+        }
+    }
 }
diff --git a/nearby/framework/java/android/nearby/ScanCallback.java b/nearby/framework/java/android/nearby/ScanCallback.java
new file mode 100644
index 0000000..498e672
--- /dev/null
+++ b/nearby/framework/java/android/nearby/ScanCallback.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.nearby;
+
+import android.annotation.NonNull;
+
+/**
+ * Reports newly discovered devices.
+ * Note: The frequency of the callback is dependent on whether the caller
+ * is in the foreground or background. Foreground callbacks will occur
+ * as fast as the underlying medium supports, whereas background
+ * use cases will be rate limited to improve performance (ie, only on
+ * found/lost/significant changes).
+ *
+ * @hide
+ */
+public interface ScanCallback {
+    /** Reports a {@link NearbyDevice} being discovered. */
+    void onDiscovered(@NonNull NearbyDevice device);
+
+    /** Reports a {@link NearbyDevice} information(distance, packet, and etc) changed. */
+    void onUpdated(@NonNull NearbyDevice device);
+
+    /** Reports a {@link NearbyDevice} is no longer within range. */
+    void onLost(@NonNull NearbyDevice device);
+}
diff --git a/nearby/framework/java/android/nearby/ScanRequest.aidl b/nearby/framework/java/android/nearby/ScanRequest.aidl
new file mode 100644
index 0000000..0aaf3af
--- /dev/null
+++ b/nearby/framework/java/android/nearby/ScanRequest.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2012, 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.nearby;
+
+parcelable ScanRequest;
diff --git a/nearby/framework/java/android/nearby/ScanRequest.java b/nearby/framework/java/android/nearby/ScanRequest.java
index 56da371..c9a4072 100644
--- a/nearby/framework/java/android/nearby/ScanRequest.java
+++ b/nearby/framework/java/android/nearby/ScanRequest.java
@@ -38,12 +38,6 @@
  */
 public final class ScanRequest implements Parcelable {
 
-    /** @hide **/
-    @Retention(RetentionPolicy.SOURCE)
-    @IntDef({SCAN_TYPE_FAST_PAIR, SCAN_TYPE_NEARBY_SHARE, SCAN_TYPE_NEARBY_PRESENCE,
-            SCAN_TYPE_EXPOSURE_NOTIFICATION})
-    public @interface ScanType{}
-
     /** Scan type for scanning devices using fast pair protocol. */
     public static final int SCAN_TYPE_FAST_PAIR = 1;
     /** Scan type for scanning devices using nearby share protocol. */
@@ -52,16 +46,87 @@
     public static final int SCAN_TYPE_NEARBY_PRESENCE = 3;
     /** Scan type for scanning devices using exposure notification protocol. */
     public static final int SCAN_TYPE_EXPOSURE_NOTIFICATION = 4;
+    /** Scan mode uses highest duty cycle. */
+    public static final int SCAN_MODE_LOW_LATENCY = 2;
+    /** Scan in balanced power mode.
+     *  Scan results are returned at a rate that provides a good trade-off between scan
+     *  frequency and power consumption.
+     */
+    public static final int SCAN_MODE_BALANCED = 1;
+    /** Perform scan in low power mode. This is the default scan mode. */
+    public static final int SCAN_MODE_LOW_POWER = 0;
+    /**
+     * A special scan mode. Applications using this scan mode will passively listen for other scan
+     * results without starting BLE scans themselves.
+     */
+    public static final int SCAN_MODE_NO_POWER = -1;
+    public static final Creator<ScanRequest> CREATOR = new Creator<ScanRequest>() {
+        @Override
+        public ScanRequest createFromParcel(Parcel in) {
+            return new ScanRequest(
+                    /* scanType= */ in.readInt(),
+                    /* scanMode= */ in.readInt(),
+                    /* enableBle= */ in.readBoolean(),
+                    /* workSource= */ in.readTypedObject(WorkSource.CREATOR));
+        }
 
+        @Override
+        public ScanRequest[] newArray(int size) {
+            return new ScanRequest[size];
+        }
+    };
     private final @ScanType int mScanType;
-    private final @Nullable WorkSource mWorkSource;
+    private final @ScanMode int mScanMode;
+    private final boolean mEnableBle;
+    private final WorkSource mWorkSource;
 
-    private ScanRequest(@ScanType int scanType, @Nullable WorkSource workSource) {
+    private ScanRequest(@ScanType int scanType, @ScanMode int scanMode, boolean enableBle,
+            WorkSource workSource) {
         mScanType = scanType;
+        mScanMode = scanMode;
+        mEnableBle = enableBle;
         mWorkSource = workSource;
     }
 
     /**
+     * Convert scan mode to readable string.
+     */
+    public static String scanModeToString(@ScanMode int scanMode) {
+        switch (scanMode) {
+            case SCAN_MODE_LOW_LATENCY:
+                return "SCAN_MODE_LOW_LATENCY";
+            case SCAN_MODE_BALANCED:
+                return "SCAN_MODE_BALANCED";
+            case SCAN_MODE_LOW_POWER:
+                return "SCAN_MODE_LOW_POWER";
+            case SCAN_MODE_NO_POWER:
+                return "SCAN_MODE_NO_POWER";
+            default:
+                return "SCAN_MODE_INVALID";
+        }
+    }
+
+    /**
+     * Returns true if an integer is a defined scan type.
+     */
+    public static boolean isValidScanType(int scanType) {
+        return scanType == SCAN_TYPE_FAST_PAIR
+                || scanType == SCAN_TYPE_NEARBY_SHARE
+                || scanType == SCAN_TYPE_NEARBY_PRESENCE
+                || scanType == SCAN_TYPE_EXPOSURE_NOTIFICATION;
+    }
+
+    /**
+     * Returns true if an integer is a defined scan mode.
+     */
+    public static boolean isValidScanMode(int scanMode) {
+        return scanMode == SCAN_MODE_LOW_LATENCY
+                || scanMode == SCAN_MODE_BALANCED
+                || scanMode == SCAN_MODE_LOW_POWER
+                || scanMode == SCAN_MODE_NO_POWER;
+    }
+
+    /**
      * Returns the scan type for this request.
      */
     public @ScanType int getScanType() {
@@ -69,28 +134,28 @@
     }
 
     /**
+     * Returns the scan mode for this request.
+     */
+    public @ScanMode int getScanMode() {
+        return mScanMode;
+    }
+
+    /**
+     * Returns if Bluetooth Low Energy enabled for scanning.
+     */
+    public boolean isEnableBle() {
+        return mEnableBle;
+    }
+
+    /**
      * Returns the work source used for power attribution of this request.
      *
      * @hide
      */
-    public @Nullable WorkSource getWorkSource() {
+    public WorkSource getWorkSource() {
         return mWorkSource;
     }
 
-    public static final Creator<ScanRequest> CREATOR = new Creator<ScanRequest>() {
-        @Override
-        public ScanRequest createFromParcel(Parcel in) {
-            return new ScanRequest(
-                    /* scanType= */ in.readInt(),
-                    /* workSource= */ in.readTypedObject(WorkSource.CREATOR));
-        }
-
-        @Override
-        public ScanRequest[] newArray(int size) {
-            return new ScanRequest[size];
-        }
-    };
-
     @Override
     public int describeContents() {
         return 0;
@@ -101,9 +166,9 @@
         StringBuilder stringBuilder = new StringBuilder();
         stringBuilder.append("Request[")
                 .append("scanType=").append(mScanType);
-        if (mWorkSource != null && !mWorkSource.isEmpty()) {
-            stringBuilder.append(", workSource=").append(mWorkSource);
-        }
+        stringBuilder.append(", scanMode=").append(scanModeToString(mScanMode));
+        stringBuilder.append(", enableBle=").append(mEnableBle);
+        stringBuilder.append(", workSource=").append(mWorkSource);
         stringBuilder.append("]");
         return stringBuilder.toString();
     }
@@ -111,6 +176,8 @@
     @Override
     public void writeToParcel(@NonNull Parcel dest, int flags) {
         dest.writeInt(mScanType);
+        dest.writeInt(mScanMode);
+        dest.writeBoolean(mEnableBle);
         dest.writeTypedObject(mWorkSource, /* parcelableFlags= */0);
     }
 
@@ -119,26 +186,46 @@
         if (other instanceof ScanRequest) {
             ScanRequest otherRequest = (ScanRequest) other;
             return mScanType == otherRequest.mScanType
-                    && Objects.equals(mWorkSource, otherRequest.mWorkSource);
+                    && (mScanMode == otherRequest.mScanMode)
+                    && (mEnableBle == otherRequest.mEnableBle)
+                    && (Objects.equals(mWorkSource, otherRequest.mWorkSource));
         }
         return false;
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(mScanType, mWorkSource);
+        return Objects.hash(mScanType, mScanMode, mEnableBle, mWorkSource);
     }
 
+    /** @hide **/
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({SCAN_TYPE_FAST_PAIR, SCAN_TYPE_NEARBY_SHARE, SCAN_TYPE_NEARBY_PRESENCE,
+            SCAN_TYPE_EXPOSURE_NOTIFICATION})
+    public @interface ScanType {
+    }
+
+    /** @hide **/
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({SCAN_MODE_LOW_LATENCY, SCAN_MODE_BALANCED,
+            SCAN_MODE_LOW_POWER,
+            SCAN_MODE_NO_POWER})
+    public @interface ScanMode {}
+
     /** A builder class for {@link ScanRequest}. */
     public static final class Builder {
         private static final int INVALID_SCAN_TYPE = -1;
         private @ScanType int mScanType;
-        private @Nullable WorkSource mWorkSource;
+        private @ScanMode int mScanMode;
+
+        private boolean mEnableBle;
+        private WorkSource mWorkSource;
 
         /** Creates a new Builder with the given scan type. */
         public Builder() {
             mScanType = INVALID_SCAN_TYPE;
-            mWorkSource = null;
+            mEnableBle = true;
+            mWorkSource = new WorkSource();
         }
 
         /**
@@ -149,6 +236,25 @@
             mScanType = scanType;
             return this;
         }
+
+        /**
+         * Sets the scan mode for the request. The scan type must be one of the SCAN_MODE_ constants
+         * in {@link ScanRequest}.
+         */
+        public Builder setScanMode(@ScanMode int scanMode) {
+            mScanMode = scanMode;
+            return this;
+        }
+
+        /**
+         * Sets if the ble is enabled for scanning.
+         * in {@link ScanRequest}.
+         */
+        public Builder setEnableBle(boolean enableBle) {
+            mEnableBle = enableBle;
+            return this;
+        }
+
         /**
          * Sets the work source to use for power attribution for this scan request. Defaults to
          * empty work source, which implies the caller that sends the scan request will be used
@@ -160,30 +266,32 @@
          * @hide
          */
         @RequiresPermission(Manifest.permission.UPDATE_DEVICE_STATS)
-        public @NonNull Builder setWorkSource(@Nullable WorkSource workSource) {
-            this.mWorkSource = workSource;
+        @NonNull
+        public Builder setWorkSource(@Nullable WorkSource workSource) {
+            if (workSource == null) {
+                mWorkSource = new WorkSource();
+            } else {
+                mWorkSource = workSource;
+            }
             return this;
         }
 
         /**
          * Builds a scan request from this builder.
          *
-         * @throws IllegalStateException if the scanType is not one of the SCAN_TYPE_ constants in
-         *         {@link ScanRequest}.
          * @return a new nearby scan request.
+         * @throws IllegalStateException if the scanType is not one of the SCAN_TYPE_ constants in
+         *                               {@link ScanRequest}.
          */
-        public @NonNull ScanRequest build() {
+        @NonNull
+        public ScanRequest build() {
             Preconditions.checkState(isValidScanType(mScanType),
                     "invalid scan type : " + mScanType
                             + ", scan type must be one of ScanRequest#SCAN_TYPE_");
-            return new ScanRequest(mScanType, mWorkSource);
-        }
-
-        private static boolean isValidScanType(int scanType) {
-            return scanType == SCAN_TYPE_FAST_PAIR
-                    || scanType == SCAN_TYPE_NEARBY_SHARE
-                    || scanType == SCAN_TYPE_NEARBY_PRESENCE
-                    || scanType == SCAN_TYPE_EXPOSURE_NOTIFICATION;
+            Preconditions.checkState(isValidScanMode(mScanMode),
+                    "invalid scan mode : " + mScanMode
+                            + ", scan mode must be one of ScanMode#SCAN_MODE_");
+            return new ScanRequest(mScanType, mScanMode, mEnableBle, mWorkSource);
         }
     }
 }
diff --git a/nearby/service/java/com/android/server/nearby/NearbyService.java b/nearby/service/java/com/android/server/nearby/NearbyService.java
index 0691d21..764978f 100644
--- a/nearby/service/java/com/android/server/nearby/NearbyService.java
+++ b/nearby/service/java/com/android/server/nearby/NearbyService.java
@@ -37,8 +37,6 @@
 
     private LocatorContextWrapper mLocatorContextWrapper;
 
-
-
     public NearbyService(Context contextBase) {
         super(contextBase);
         mImpl = new NearbyServiceImpl(contextBase);
diff --git a/nearby/service/java/com/android/server/nearby/NearbyServiceImpl.java b/nearby/service/java/com/android/server/nearby/NearbyServiceImpl.java
index 959d12d..c84169e 100644
--- a/nearby/service/java/com/android/server/nearby/NearbyServiceImpl.java
+++ b/nearby/service/java/com/android/server/nearby/NearbyServiceImpl.java
@@ -18,6 +18,8 @@
 
 import android.content.Context;
 import android.nearby.INearbyManager;
+import android.nearby.IScanListener;
+import android.nearby.ScanRequest;
 
 /**
  * Implementation of {@link NearbyService}.
@@ -26,4 +28,12 @@
 
     public NearbyServiceImpl(Context context) {
     }
+
+    @Override
+    public void registerScanListener(ScanRequest scanRequest, IScanListener listener) {
+    }
+
+    @Override
+    public void unregisterScanListener(IScanListener listener) {
+    }
 }
diff --git a/nearby/tests/src/android/nearby/ScanRequestTest.java b/nearby/tests/src/android/nearby/ScanRequestTest.java
index ab9f161..fe80a15 100644
--- a/nearby/tests/src/android/nearby/ScanRequestTest.java
+++ b/nearby/tests/src/android/nearby/ScanRequestTest.java
@@ -16,6 +16,8 @@
 
 package android.nearby;
 
+import static android.nearby.ScanRequest.SCAN_MODE_BALANCED;
+import static android.nearby.ScanRequest.SCAN_MODE_LOW_POWER;
 import static android.nearby.ScanRequest.SCAN_TYPE_EXPOSURE_NOTIFICATION;
 import static android.nearby.ScanRequest.SCAN_TYPE_FAST_PAIR;
 import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
@@ -39,6 +41,12 @@
 @RunWith(AndroidJUnit4.class)
 public class ScanRequestTest {
 
+    private static WorkSource getWorkSource() {
+        final int uid = 1001;
+        final String appName = "android.nearby.tests";
+        return new WorkSource(uid, appName);
+    }
+
     /** Test creating a scan request. */
     @Test
     public void testScanRequestBuilder() {
@@ -46,20 +54,29 @@
         ScanRequest request = new ScanRequest.Builder().setScanType(scanType).build();
 
         assertThat(request.getScanType()).isEqualTo(scanType);
+        assertThat(request.getScanMode()).isEqualTo(SCAN_MODE_LOW_POWER);
         // Work source is null if not set.
-        assertThat(request.getWorkSource()).isNull();
+        assertThat(request.getWorkSource().isEmpty()).isTrue();
     }
 
     /** Verify RuntimeException is thrown when creating scan request with invalid scan type. */
     @Test(expected = RuntimeException.class)
     public void testScanRequestBuilder_invalidScanType() {
         final int invalidScanType = -1;
-        ScanRequest.Builder builder = new ScanRequest.Builder().setScanType(
-                invalidScanType);
+        ScanRequest.Builder builder = new ScanRequest.Builder().setScanType(invalidScanType);
 
         builder.build();
     }
 
+    /** Verify RuntimeException is thrown when creating scan mode with invalid scan mode. */
+    @Test(expected = RuntimeException.class)
+    public void testScanModeBuilder_invalidScanType() {
+        final int invalidScanMode = -5;
+        ScanRequest.Builder builder = new ScanRequest.Builder().setScanType(
+                SCAN_TYPE_FAST_PAIR).setScanMode(invalidScanMode);
+        builder.build();
+    }
+
     /** Verify setting work source in the scan request. */
     @Test
     public void testSetWorkSource() {
@@ -81,7 +98,7 @@
                 .build();
 
         // Null work source is allowed.
-        assertThat(request.getWorkSource()).isNull();
+        assertThat(request.getWorkSource().isEmpty()).isTrue();
     }
 
     /** Verify toString returns expected string. */
@@ -90,20 +107,24 @@
         WorkSource workSource = getWorkSource();
         ScanRequest request = new ScanRequest.Builder()
                 .setScanType(SCAN_TYPE_NEARBY_SHARE)
+                .setScanMode(SCAN_MODE_BALANCED)
+                .setEnableBle(true)
                 .setWorkSource(workSource)
                 .build();
 
         assertThat(request.toString()).isEqualTo(
-                "Request[scanType=2, workSource=WorkSource{1001 android.nearby.tests}]");
+                "Request[scanType=2, scanMode=SCAN_MODE_BALANCED, "
+                        + "enableBle=true, workSource=WorkSource{1001 android.nearby.tests}]");
     }
 
     /** Verify toString works correctly with null WorkSource. */
     @Test
     public void testToString_nullWorkSource() {
         ScanRequest request = new ScanRequest.Builder().setScanType(
-                SCAN_TYPE_FAST_PAIR).build();
+                SCAN_TYPE_FAST_PAIR).setWorkSource(null).build();
 
-        assertThat(request.toString()).isEqualTo("Request[scanType=1]");
+        assertThat(request.toString()).isEqualTo("Request[scanType=1, "
+                + "scanMode=SCAN_MODE_LOW_POWER, enableBle=true, workSource=WorkSource{}]");
     }
 
     /** Verify writing and reading from parcel for scan request. */
@@ -113,6 +134,8 @@
         WorkSource workSource = getWorkSource();
         ScanRequest originalRequest = new ScanRequest.Builder()
                 .setScanType(scanType)
+                .setScanMode(SCAN_MODE_BALANCED)
+                .setEnableBle(true)
                 .setWorkSource(workSource)
                 .build();
 
@@ -141,10 +164,4 @@
         parcel.setDataPosition(0);
         return ScanRequest.CREATOR.createFromParcel(parcel);
     }
-
-    private static WorkSource getWorkSource() {
-        final int uid = 1001;
-        final String appName = "android.nearby.tests";
-        return new WorkSource(uid, appName);
-    }
 }