Add Nearby servicemonitor and ProxyFastPairDataProvider.

Bug: 204780849
Test: unit test, to be done
Change-Id: If80cd68748d0f35d4eb76f0cc199171927fcaa12
diff --git a/nearby/service/Android.bp b/nearby/service/Android.bp
index 4a8ab0b..bc3f91d 100644
--- a/nearby/service/Android.bp
+++ b/nearby/service/Android.bp
@@ -44,6 +44,10 @@
         "guava",
         "libprotobuf-java-lite",
         "fast-pair-lite-protos",
+        "modules-utils-build",
+        "modules-utils-handlerexecutor",
+        "modules-utils-preconditions",
+        "modules-utils-backgroundthread",
     ],
     sdk_version: "system_server_current",
 
diff --git a/nearby/service/java/com/android/server/nearby/NearbyService.java b/nearby/service/java/com/android/server/nearby/NearbyService.java
index 764978f..464d469 100644
--- a/nearby/service/java/com/android/server/nearby/NearbyService.java
+++ b/nearby/service/java/com/android/server/nearby/NearbyService.java
@@ -22,7 +22,7 @@
 import com.android.server.SystemService;
 import com.android.server.nearby.common.locator.LocatorContextWrapper;
 import com.android.server.nearby.fastpair.FastPairManager;
-
+import com.android.server.nearby.provider.FastPairDataProvider;
 
 /**
  * Service implementing nearby functionality. The actual implementation is delegated to
@@ -30,9 +30,11 @@
  */
 // TODO(189954300): Implement nearby service.
 public class NearbyService extends SystemService {
+
     private static final String TAG = "NearbyService";
     private static final boolean DBG = true;
     private final NearbyServiceImpl mImpl;
+    private final Context mContext;
     private final FastPairManager mFastPairManager;
 
     private LocatorContextWrapper mLocatorContextWrapper;
@@ -40,6 +42,7 @@
     public NearbyService(Context contextBase) {
         super(contextBase);
         mImpl = new NearbyServiceImpl(contextBase);
+        mContext = contextBase;
         mLocatorContextWrapper = new LocatorContextWrapper(contextBase, null);
         mFastPairManager = new FastPairManager(mLocatorContextWrapper);
     }
@@ -54,7 +57,13 @@
 
     @Override
     public void onBootPhase(int phase) {
+        if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) {
+            onSystemThirdPartyAppsCanStart();
+        }
     }
 
-
-}
\ No newline at end of file
+    private void onSystemThirdPartyAppsCanStart() {
+        // Ensures that a fast pair data provider exists which will work in direct boot.
+        FastPairDataProvider.init(mContext);
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/servicemonitor/CurrentUserServiceProvider.java b/nearby/service/java/com/android/server/nearby/common/servicemonitor/CurrentUserServiceProvider.java
new file mode 100644
index 0000000..80248e8
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/servicemonitor/CurrentUserServiceProvider.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.servicemonitor;
+
+import static android.content.pm.PackageManager.GET_META_DATA;
+import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AUTO;
+import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
+import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
+import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
+
+import android.app.ActivityManager;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ResolveInfo;
+import android.os.UserHandle;
+
+import com.android.internal.util.Preconditions;
+import com.android.server.nearby.common.servicemonitor.ServiceMonitor.ServiceChangedListener;
+import com.android.server.nearby.common.servicemonitor.ServiceMonitor.ServiceProvider;
+
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * This is mostly borrowed from frameworks CurrentUserServiceSupplier.
+ * Provides services based on the current active user and version as defined in the service
+ * manifest. This implementation uses {@link android.content.pm.PackageManager#MATCH_SYSTEM_ONLY} to
+ * ensure only system (ie, privileged) services are matched. It also handles services that are not
+ * direct boot aware, and will automatically pick the best service as the user's direct boot state
+ * changes.
+ */
+public final class CurrentUserServiceProvider extends BroadcastReceiver implements
+        ServiceProvider<CurrentUserServiceProvider.BoundServiceInfo> {
+
+    private static final String TAG = "CurrentUserServiceProvider";
+
+    private static final String EXTRA_SERVICE_VERSION = "serviceVersion";
+
+    // This is equal to the hidden Intent.ACTION_USER_SWITCHED.
+    private static final String ACTION_USER_SWITCHED = "android.intent.action.USER_SWITCHED";
+    // This is equal to the hidden Intent.EXTRA_USER_HANDLE.
+    private static final String EXTRA_USER_HANDLE = "android.intent.extra.user_handle";
+    // This is equal to the hidden UserHandle.USER_NULL.
+    private static final int USER_NULL = -10000;
+
+    private static final Comparator<BoundServiceInfo> sBoundServiceInfoComparator = (o1, o2) -> {
+        if (o1 == o2) {
+            return 0;
+        } else if (o1 == null) {
+            return -1;
+        } else if (o2 == null) {
+            return 1;
+        }
+
+        // ServiceInfos with higher version numbers always win.
+        return Integer.compare(o1.getVersion(), o2.getVersion());
+    };
+
+    /** Bound service information with version information. */
+    public static class BoundServiceInfo extends ServiceMonitor.BoundServiceInfo {
+
+        private static int parseUid(ResolveInfo resolveInfo) {
+            return resolveInfo.serviceInfo.applicationInfo.uid;
+        }
+
+        private static int parseVersion(ResolveInfo resolveInfo) {
+            int version = Integer.MIN_VALUE;
+            if (resolveInfo.serviceInfo.metaData != null) {
+                version = resolveInfo.serviceInfo.metaData.getInt(EXTRA_SERVICE_VERSION, version);
+            }
+            return version;
+        }
+
+        private final int mVersion;
+
+        protected BoundServiceInfo(String action, ResolveInfo resolveInfo) {
+            this(
+                    action,
+                    parseUid(resolveInfo),
+                    new ComponentName(
+                            resolveInfo.serviceInfo.packageName,
+                            resolveInfo.serviceInfo.name),
+                    parseVersion(resolveInfo));
+        }
+
+        protected BoundServiceInfo(String action, int uid, ComponentName componentName,
+                int version) {
+            super(action, uid, componentName);
+            mVersion = version;
+        }
+
+        public int getVersion() {
+            return mVersion;
+        }
+
+        @Override
+        public String toString() {
+            return super.toString() + "@" + mVersion;
+        }
+    }
+
+    /**
+     * Creates an instance with the specific service details.
+     *
+     * @param context the context the provider is to use
+     * @param action the action the service must declare in its intent-filter
+     */
+    public static CurrentUserServiceProvider create(Context context, String action) {
+        return new CurrentUserServiceProvider(context, action);
+    }
+
+    private final Context mContext;
+    private final Intent mIntent;
+    private volatile ServiceChangedListener mListener;
+
+    private CurrentUserServiceProvider(Context context, String action) {
+        mContext = context;
+        mIntent = new Intent(action);
+    }
+
+    @Override
+    public boolean hasMatchingService() {
+        int intentQueryFlags =
+                MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE | MATCH_SYSTEM_ONLY;
+        List<ResolveInfo> resolveInfos = mContext.getPackageManager().queryIntentServicesAsUser(
+                mIntent, intentQueryFlags, UserHandle.SYSTEM);
+        return !resolveInfos.isEmpty();
+    }
+
+    @Override
+    public void register(ServiceChangedListener listener) {
+        Preconditions.checkState(mListener == null);
+
+        mListener = listener;
+
+        IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(ACTION_USER_SWITCHED);
+        intentFilter.addAction(Intent.ACTION_USER_UNLOCKED);
+        mContext.registerReceiverForAllUsers(this, intentFilter, null,
+                ForegroundThread.getHandler());
+    }
+
+    @Override
+    public void unregister() {
+        Preconditions.checkArgument(mListener != null);
+
+        mListener = null;
+        mContext.unregisterReceiver(this);
+    }
+
+    @Override
+    public BoundServiceInfo getServiceInfo() {
+        BoundServiceInfo bestServiceInfo = null;
+
+        // only allow services in the correct direct boot state to match
+        int intentQueryFlags = MATCH_DIRECT_BOOT_AUTO | GET_META_DATA | MATCH_SYSTEM_ONLY;
+        List<ResolveInfo> resolveInfos = mContext.getPackageManager().queryIntentServicesAsUser(
+                mIntent, intentQueryFlags, UserHandle.of(ActivityManager.getCurrentUser()));
+        for (ResolveInfo resolveInfo : resolveInfos) {
+            BoundServiceInfo serviceInfo =
+                    new BoundServiceInfo(mIntent.getAction(), resolveInfo);
+
+            if (sBoundServiceInfoComparator.compare(serviceInfo, bestServiceInfo) > 0) {
+                bestServiceInfo = serviceInfo;
+            }
+        }
+
+        return bestServiceInfo;
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        String action = intent.getAction();
+        if (action == null) {
+            return;
+        }
+        int userId = intent.getIntExtra(EXTRA_USER_HANDLE, USER_NULL);
+        if (userId == USER_NULL) {
+            return;
+        }
+        ServiceChangedListener listener = mListener;
+        if (listener == null) {
+            return;
+        }
+
+        switch (action) {
+            case ACTION_USER_SWITCHED:
+                listener.onServiceChanged();
+                break;
+            case Intent.ACTION_USER_UNLOCKED:
+                // user unlocked implies direct boot mode may have changed
+                if (userId == ActivityManager.getCurrentUser()) {
+                    listener.onServiceChanged();
+                }
+                break;
+            default:
+                break;
+        }
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/servicemonitor/ForegroundThread.java b/nearby/service/java/com/android/server/nearby/common/servicemonitor/ForegroundThread.java
new file mode 100644
index 0000000..2c363f8
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/servicemonitor/ForegroundThread.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.servicemonitor;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+
+import com.android.modules.utils.HandlerExecutor;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Thread for asynchronous event processing. This thread is configured as
+ * {@link android.os.Process#THREAD_PRIORITY_FOREGROUND}, which means more CPU
+ * resources will be dedicated to it, and it will be treated like "a user
+ * interface that the user is interacting with."
+ * <p>
+ * This thread is best suited for tasks that the user is actively waiting for,
+ * or for tasks that the user expects to be executed immediately.
+ *
+ */
+public final class ForegroundThread extends HandlerThread {
+    private static ForegroundThread sInstance;
+    private static Handler sHandler;
+    private static HandlerExecutor sHandlerExecutor;
+
+    private ForegroundThread() {
+        super("nearbyfg", android.os.Process.THREAD_PRIORITY_FOREGROUND);
+    }
+
+    private static void ensureThreadLocked() {
+        if (sInstance == null) {
+            sInstance = new ForegroundThread();
+            sInstance.start();
+            sHandler = new Handler(sInstance.getLooper());
+            sHandlerExecutor = new HandlerExecutor(sHandler);
+        }
+    }
+
+    /** Get ForegroundThread singleton instance. */
+    public static ForegroundThread get() {
+        synchronized (ForegroundThread.class) {
+            ensureThreadLocked();
+            return sInstance;
+        }
+    }
+
+    /** Get ForegroundThread singleton handler. */
+    public static Handler getHandler() {
+        synchronized (ForegroundThread.class) {
+            ensureThreadLocked();
+            return sHandler;
+        }
+    }
+
+    /** Get ForegroundThread singleton executor. */
+    public static Executor getExecutor() {
+        synchronized (ForegroundThread.class) {
+            ensureThreadLocked();
+            return sHandlerExecutor;
+        }
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/servicemonitor/PackageWatcher.java b/nearby/service/java/com/android/server/nearby/common/servicemonitor/PackageWatcher.java
new file mode 100644
index 0000000..7d1db57
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/servicemonitor/PackageWatcher.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.servicemonitor;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+
+import com.android.modules.utils.BackgroundThread;
+
+import java.util.Objects;
+
+/**
+ * This is mostly from frameworks PackageMonitor.
+ * Helper class for watching somePackagesChanged.
+ */
+public abstract class PackageWatcher extends BroadcastReceiver {
+    static final String TAG = "PackageWatcher";
+    static final IntentFilter sPackageFilt = new IntentFilter();
+    static final IntentFilter sNonDataFilt = new IntentFilter();
+    static final IntentFilter sExternalFilt = new IntentFilter();
+
+    static {
+        sPackageFilt.addAction(Intent.ACTION_PACKAGE_ADDED);
+        sPackageFilt.addAction(Intent.ACTION_PACKAGE_REMOVED);
+        sPackageFilt.addAction(Intent.ACTION_PACKAGE_CHANGED);
+        sPackageFilt.addDataScheme("package");
+        sNonDataFilt.addAction(Intent.ACTION_PACKAGES_SUSPENDED);
+        sNonDataFilt.addAction(Intent.ACTION_PACKAGES_UNSUSPENDED);
+        sExternalFilt.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
+        sExternalFilt.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
+    }
+
+    Context mRegisteredContext;
+    Handler mRegisteredHandler;
+    boolean mSomePackagesChanged;
+
+    public PackageWatcher() {
+    }
+
+    void register(Context context, Looper thread, boolean externalStorage) {
+        register(context, externalStorage,
+                (thread == null) ? BackgroundThread.getHandler() : new Handler(thread));
+    }
+
+    void register(Context context, boolean externalStorage, Handler handler) {
+        if (mRegisteredContext != null) {
+            throw new IllegalStateException("Already registered");
+        }
+        mRegisteredContext = context;
+        mRegisteredHandler = Objects.requireNonNull(handler);
+        context.registerReceiverForAllUsers(this, sPackageFilt, null, mRegisteredHandler);
+        context.registerReceiverForAllUsers(this, sNonDataFilt, null, mRegisteredHandler);
+        if (externalStorage) {
+            context.registerReceiverForAllUsers(this, sExternalFilt, null, mRegisteredHandler);
+        }
+    }
+
+    void unregister() {
+        if (mRegisteredContext == null) {
+            throw new IllegalStateException("Not registered");
+        }
+        mRegisteredContext.unregisterReceiver(this);
+        mRegisteredContext = null;
+    }
+
+    // Called when some package has been changed.
+    abstract void onSomePackagesChanged();
+
+    String getPackageName(Intent intent) {
+        Uri uri = intent.getData();
+        String pkg = uri != null ? uri.getSchemeSpecificPart() : null;
+        return pkg;
+    }
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        mSomePackagesChanged = false;
+
+        String action = intent.getAction();
+        if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
+            // We consider something to have changed regardless of whether
+            // this is just an update, because the update is now finished
+            // and the contents of the package may have changed.
+            mSomePackagesChanged = true;
+        } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
+            String pkg = getPackageName(intent);
+            if (pkg != null) {
+                if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
+                    mSomePackagesChanged = true;
+                }
+            }
+        } else if (Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
+            String pkg = getPackageName(intent);
+            if (pkg != null) {
+                mSomePackagesChanged = true;
+            }
+        } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action)) {
+            mSomePackagesChanged = true;
+        } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) {
+            mSomePackagesChanged = true;
+        } else if (Intent.ACTION_PACKAGES_SUSPENDED.equals(action)) {
+            mSomePackagesChanged = true;
+        } else if (Intent.ACTION_PACKAGES_UNSUSPENDED.equals(action)) {
+            mSomePackagesChanged = true;
+        }
+
+        if (mSomePackagesChanged) {
+            onSomePackagesChanged();
+        }
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/servicemonitor/ServiceMonitor.java b/nearby/service/java/com/android/server/nearby/common/servicemonitor/ServiceMonitor.java
new file mode 100644
index 0000000..a86af85
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/servicemonitor/ServiceMonitor.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.servicemonitor;
+
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import java.io.PrintWriter;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * This is exported from frameworks ServiceWatcher.
+ * A ServiceMonitor is responsible for continuously maintaining an active binding to a service
+ * selected by it's {@link ServiceProvider}. The {@link ServiceProvider} may change the service it
+ * selects over time, and the currently bound service may crash, restart, have a user change, have
+ * changes made to its package, and so on and so forth. The ServiceMonitor is responsible for
+ * maintaining the binding across all these changes.
+ *
+ * <p>Clients may invoke {@link BinderOperation}s on the ServiceMonitor, and it will make a best
+ * effort to run these on the currently bound service, but individual operations may fail (if there
+ * is no service currently bound for instance). In order to help clients maintain the correct state,
+ * clients may supply a {@link ServiceListener}, which is informed when the ServiceMonitor connects
+ * and disconnects from a service. This allows clients to bring a bound service back into a known
+ * state on connection, and then run binder operations from there. In order to help clients
+ * accomplish this, ServiceMonitor guarantees that {@link BinderOperation}s and the
+ * {@link ServiceListener} will always be run on the same thread, so that strong ordering guarantees
+ * can be established between them.
+ *
+ * There is never any guarantee of whether a ServiceMonitor is currently connected to a service, and
+ * whether any particular {@link BinderOperation} will succeed. Clients must ensure they do not rely
+ * on this, and instead use {@link ServiceListener} notifications as necessary to recover from
+ * failures.
+ */
+public interface ServiceMonitor {
+
+    /**
+     * Operation to run on a binder interface. All operations will be run on the thread used by the
+     * ServiceMonitor this is run with.
+     */
+    interface BinderOperation {
+        /** Invoked to run the operation. Run on the ServiceMonitor thread. */
+        void run(IBinder binder) throws RemoteException;
+
+        /**
+         * Invoked if {@link #run(IBinder)} could not be invoked because there was no current
+         * binding, or if {@link #run(IBinder)} threw an exception ({@link RemoteException} or
+         * {@link RuntimeException}). This callback is only intended for resource deallocation and
+         * cleanup in response to a single binder operation, it should not be used to propagate
+         * errors further. Run on the ServiceMonitor thread.
+         */
+        default void onError() {}
+    }
+
+    /**
+     * Listener for bind and unbind events. All operations will be run on the thread used by the
+     * ServiceMonitor this is run with.
+     *
+     * @param <TBoundServiceInfo> type of bound service
+     */
+    interface ServiceListener<TBoundServiceInfo extends BoundServiceInfo> {
+        /** Invoked when a service is bound. Run on the ServiceMonitor thread. */
+        void onBind(IBinder binder, TBoundServiceInfo service) throws RemoteException;
+
+        /** Invoked when a service is unbound. Run on the ServiceMonitor thread. */
+        void onUnbind();
+    }
+
+    /**
+     * A listener for when a {@link ServiceProvider} decides that the current service has changed.
+     */
+    interface ServiceChangedListener {
+        /**
+         * Should be invoked when the current service may have changed.
+         */
+        void onServiceChanged();
+    }
+
+    /**
+     * This provider encapsulates the logic of deciding what service a {@link ServiceMonitor} should
+     * be bound to at any given moment.
+     *
+     * @param <TBoundServiceInfo> type of bound service
+     */
+    interface ServiceProvider<TBoundServiceInfo extends BoundServiceInfo> {
+        /**
+         * Should return true if there exists at least one service capable of meeting the criteria
+         * of this provider. This does not imply that {@link #getServiceInfo()} will always return a
+         * non-null result, as any service may be disqualified for various reasons at any point in
+         * time. May be invoked at any time from any thread and thus should generally not have any
+         * dependency on the other methods in this interface.
+         */
+        boolean hasMatchingService();
+
+        /**
+         * Invoked when the provider should start monitoring for any changes that could result in a
+         * different service selection, and should invoke
+         * {@link ServiceChangedListener#onServiceChanged()} in that case. {@link #getServiceInfo()}
+         * may be invoked after this method is called.
+         */
+        void register(ServiceChangedListener listener);
+
+        /**
+         * Invoked when the provider should stop monitoring for any changes that could result in a
+         * different service selection, should no longer invoke
+         * {@link ServiceChangedListener#onServiceChanged()}. {@link #getServiceInfo()} will not be
+         * invoked after this method is called.
+         */
+        void unregister();
+
+        /**
+         * Must be implemented to return the current service selected by this provider. May return
+         * null if no service currently meets the criteria. Only invoked while registered.
+         */
+        @Nullable TBoundServiceInfo getServiceInfo();
+    }
+
+    /**
+     * Information on the service selected as the best option for binding.
+     */
+    class BoundServiceInfo {
+
+        protected final @Nullable String mAction;
+        protected final int mUid;
+        protected final ComponentName mComponentName;
+
+        protected BoundServiceInfo(String action, int uid, ComponentName componentName) {
+            mAction = action;
+            mUid = uid;
+            mComponentName = Objects.requireNonNull(componentName);
+        }
+
+        /** Returns the action associated with this bound service. */
+        public @Nullable String getAction() {
+            return mAction;
+        }
+
+        /** Returns the component of this bound service. */
+        public ComponentName getComponentName() {
+            return mComponentName;
+        }
+
+        @Override
+        public final boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (!(o instanceof BoundServiceInfo)) {
+                return false;
+            }
+
+            BoundServiceInfo that = (BoundServiceInfo) o;
+            return mUid == that.mUid
+                    && Objects.equals(mAction, that.mAction)
+                    && mComponentName.equals(that.mComponentName);
+        }
+
+        @Override
+        public final int hashCode() {
+            return Objects.hash(mAction, mUid, mComponentName);
+        }
+
+        @Override
+        public String toString() {
+            if (mComponentName == null) {
+                return "none";
+            } else {
+                return mUid + "/" + mComponentName.flattenToShortString();
+            }
+        }
+    }
+
+    /**
+     * Creates a new ServiceMonitor instance.
+     */
+    static <TBoundServiceInfo extends BoundServiceInfo> ServiceMonitor create(
+            Context context,
+            String tag,
+            ServiceProvider<TBoundServiceInfo> serviceProvider,
+            @Nullable ServiceListener<? super TBoundServiceInfo> serviceListener) {
+        return create(context, ForegroundThread.getHandler(), ForegroundThread.getExecutor(), tag,
+                serviceProvider, serviceListener);
+    }
+
+    /**
+     * Creates a new ServiceMonitor instance that runs on the given handler.
+     */
+    static <TBoundServiceInfo extends BoundServiceInfo> ServiceMonitor create(
+            Context context,
+            Handler handler,
+            Executor executor,
+            String tag,
+            ServiceProvider<TBoundServiceInfo> serviceProvider,
+            @Nullable ServiceListener<? super TBoundServiceInfo> serviceListener) {
+        return new ServiceMonitorImpl<>(context, handler, executor, tag, serviceProvider,
+                serviceListener);
+    }
+
+    /**
+     * Returns true if there is at least one service that the ServiceMonitor could hypothetically
+     * bind to, as selected by the {@link ServiceProvider}.
+     */
+    boolean checkServiceResolves();
+
+    /**
+     * Registers the ServiceMonitor, so that it will begin maintaining an active binding to the
+     * service selected by {@link ServiceProvider}, until {@link #unregister()} is called.
+     */
+    void register();
+
+    /**
+     * Unregisters the ServiceMonitor, so that it will release any active bindings. If the
+     * ServiceMonitor is currently bound, this will result in one final
+     * {@link ServiceListener#onUnbind()} invocation, which may happen after this method completes
+     * (but which is guaranteed to occur before any further
+     * {@link ServiceListener#onBind(IBinder, BoundServiceInfo)} invocation in response to a later
+     * call to {@link #register()}).
+     */
+    void unregister();
+
+    /**
+     * Runs the given binder operation on the currently bound service (if available). The operation
+     * will always fail if the ServiceMonitor is not currently registered.
+     */
+    void runOnBinder(BinderOperation operation);
+
+    /**
+     * Dumps ServiceMonitor information.
+     */
+    void dump(PrintWriter pw);
+}
diff --git a/nearby/service/java/com/android/server/nearby/common/servicemonitor/ServiceMonitorImpl.java b/nearby/service/java/com/android/server/nearby/common/servicemonitor/ServiceMonitorImpl.java
new file mode 100644
index 0000000..d0d6c3b
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/common/servicemonitor/ServiceMonitorImpl.java
@@ -0,0 +1,305 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.common.servicemonitor;
+
+import static android.content.Context.BIND_AUTO_CREATE;
+import static android.content.Context.BIND_NOT_FOREGROUND;
+
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+import com.android.server.nearby.common.servicemonitor.ServiceMonitor.BoundServiceInfo;
+import com.android.server.nearby.common.servicemonitor.ServiceMonitor.ServiceChangedListener;
+
+import java.io.PrintWriter;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+/**
+ * Implementation of ServiceMonitor. Keeping the implementation separate from the interface allows
+ * us to store the generic relationship between the service provider and the service listener, while
+ * hiding the generics from clients, simplifying the API.
+ */
+class ServiceMonitorImpl<TBoundServiceInfo extends BoundServiceInfo> implements ServiceMonitor,
+        ServiceChangedListener {
+
+    private static final String TAG = "ServiceMonitor";
+    private static final boolean D = Log.isLoggable(TAG, Log.DEBUG);
+    private static final long RETRY_DELAY_MS = 15 * 1000;
+
+    // This is the same as Context.BIND_NOT_VISIBLE.
+    private static final int BIND_NOT_VISIBLE = 0x40000000;
+
+    final Context mContext;
+    final Handler mHandler;
+    final Executor mExecutor;
+    final String mTag;
+    final ServiceProvider<TBoundServiceInfo> mServiceProvider;
+    final @Nullable ServiceListener<? super TBoundServiceInfo> mServiceListener;
+
+    private final PackageWatcher mPackageWatcher = new PackageWatcher() {
+        @Override
+        public void onSomePackagesChanged() {
+            onServiceChanged(false);
+        }
+    };
+
+    @GuardedBy("this")
+    private boolean mRegistered = false;
+    @GuardedBy("this")
+    private MyServiceConnection mServiceConnection = new MyServiceConnection(null);
+
+    ServiceMonitorImpl(Context context, Handler handler, Executor executor, String tag,
+            ServiceProvider<TBoundServiceInfo> serviceProvider,
+            ServiceListener<? super TBoundServiceInfo> serviceListener) {
+        mContext = context;
+        mExecutor = executor;
+        mHandler = handler;
+        mTag = tag;
+        mServiceProvider = serviceProvider;
+        mServiceListener = serviceListener;
+    }
+
+    @Override
+    public boolean checkServiceResolves() {
+        return mServiceProvider.hasMatchingService();
+    }
+
+    @Override
+    public synchronized void register() {
+        Preconditions.checkState(!mRegistered);
+
+        mRegistered = true;
+        mPackageWatcher.register(mContext, /*externalStorage=*/ true, mHandler);
+        mServiceProvider.register(this);
+
+        onServiceChanged(false);
+    }
+
+    @Override
+    public synchronized void unregister() {
+        Preconditions.checkState(mRegistered);
+
+        mServiceProvider.unregister();
+        mPackageWatcher.unregister();
+        mRegistered = false;
+
+        onServiceChanged(false);
+    }
+
+    @Override
+    public synchronized void onServiceChanged() {
+        onServiceChanged(false);
+    }
+
+    @Override
+    public synchronized void runOnBinder(BinderOperation operation) {
+        MyServiceConnection serviceConnection = mServiceConnection;
+        mHandler.post(() -> serviceConnection.runOnBinder(operation));
+    }
+
+    synchronized void onServiceChanged(boolean forceRebind) {
+        TBoundServiceInfo newBoundServiceInfo;
+        if (mRegistered) {
+            newBoundServiceInfo = mServiceProvider.getServiceInfo();
+        } else {
+            newBoundServiceInfo = null;
+        }
+
+        if (forceRebind || !Objects.equals(mServiceConnection.getBoundServiceInfo(),
+                newBoundServiceInfo)) {
+            Log.i(TAG, "[" + mTag + "] chose new implementation " + newBoundServiceInfo);
+            MyServiceConnection oldServiceConnection = mServiceConnection;
+            MyServiceConnection newServiceConnection = new MyServiceConnection(newBoundServiceInfo);
+            mServiceConnection = newServiceConnection;
+            mHandler.post(() -> {
+                oldServiceConnection.unbind();
+                newServiceConnection.bind();
+            });
+        }
+    }
+
+    @Override
+    public String toString() {
+        MyServiceConnection serviceConnection;
+        synchronized (this) {
+            serviceConnection = mServiceConnection;
+        }
+
+        return serviceConnection.getBoundServiceInfo().toString();
+    }
+
+    @Override
+    public void dump(PrintWriter pw) {
+        MyServiceConnection serviceConnection;
+        synchronized (this) {
+            serviceConnection = mServiceConnection;
+        }
+
+        pw.println("target service=" + serviceConnection.getBoundServiceInfo());
+        pw.println("connected=" + serviceConnection.isConnected());
+    }
+
+    // runs on the handler thread, and expects most of its methods to be called from that thread
+    private class MyServiceConnection implements ServiceConnection {
+
+        private final @Nullable TBoundServiceInfo mBoundServiceInfo;
+
+        // volatile so that isConnected can be called from any thread easily
+        private volatile @Nullable IBinder mBinder;
+        private @Nullable Runnable mRebinder;
+
+        MyServiceConnection(@Nullable TBoundServiceInfo boundServiceInfo) {
+            mBoundServiceInfo = boundServiceInfo;
+        }
+
+        // may be called from any thread
+        @Nullable TBoundServiceInfo getBoundServiceInfo() {
+            return mBoundServiceInfo;
+        }
+
+        // may be called from any thread
+        boolean isConnected() {
+            return mBinder != null;
+        }
+
+        void bind() {
+            Preconditions.checkState(Looper.myLooper() == mHandler.getLooper());
+
+            if (mBoundServiceInfo == null) {
+                return;
+            }
+
+            if (D) {
+                Log.d(TAG, "[" + mTag + "] binding to " + mBoundServiceInfo);
+            }
+
+            Intent bindIntent = new Intent(mBoundServiceInfo.getAction())
+                    .setComponent(mBoundServiceInfo.getComponentName());
+            if (!mContext.bindService(bindIntent,
+                    BIND_AUTO_CREATE | BIND_NOT_FOREGROUND | BIND_NOT_VISIBLE,
+                    mExecutor, this)) {
+                Log.e(TAG, "[" + mTag + "] unexpected bind failure - retrying later");
+                mRebinder = this::bind;
+                mHandler.postDelayed(mRebinder, RETRY_DELAY_MS);
+            } else {
+                mRebinder = null;
+            }
+        }
+
+        void unbind() {
+            Preconditions.checkState(Looper.myLooper() == mHandler.getLooper());
+
+            if (mBoundServiceInfo == null) {
+                return;
+            }
+
+            if (D) {
+                Log.d(TAG, "[" + mTag + "] unbinding from " + mBoundServiceInfo);
+            }
+
+            if (mRebinder != null) {
+                mHandler.removeCallbacks(mRebinder);
+                mRebinder = null;
+            } else {
+                mContext.unbindService(this);
+            }
+
+            onServiceDisconnected(mBoundServiceInfo.getComponentName());
+        }
+
+        void runOnBinder(BinderOperation operation) {
+            Preconditions.checkState(Looper.myLooper() == mHandler.getLooper());
+
+            if (mBinder == null) {
+                operation.onError();
+                return;
+            }
+
+            try {
+                operation.run(mBinder);
+            } catch (RuntimeException | RemoteException e) {
+                // binders may propagate some specific non-RemoteExceptions from the other side
+                // through the binder as well - we cannot allow those to crash the system server
+                Log.e(TAG, "[" + mTag + "] error running operation on " + mBoundServiceInfo, e);
+                operation.onError();
+            }
+        }
+
+        @Override
+        public final void onServiceConnected(ComponentName component, IBinder binder) {
+            Preconditions.checkState(Looper.myLooper() == mHandler.getLooper());
+            Preconditions.checkState(mBinder == null);
+
+            Log.i(TAG, "[" + mTag + "] connected to " + component.toShortString());
+
+            mBinder = binder;
+
+            if (mServiceListener != null) {
+                try {
+                    mServiceListener.onBind(binder, mBoundServiceInfo);
+                } catch (RuntimeException | RemoteException e) {
+                    // binders may propagate some specific non-RemoteExceptions from the other side
+                    // through the binder as well - we cannot allow those to crash the system server
+                    Log.e(TAG, "[" + mTag + "] error running operation on " + mBoundServiceInfo, e);
+                }
+            }
+        }
+
+        @Override
+        public final void onServiceDisconnected(ComponentName component) {
+            Preconditions.checkState(Looper.myLooper() == mHandler.getLooper());
+
+            if (mBinder == null) {
+                return;
+            }
+
+            Log.i(TAG, "[" + mTag + "] disconnected from " + mBoundServiceInfo);
+
+            mBinder = null;
+            if (mServiceListener != null) {
+                mServiceListener.onUnbind();
+            }
+        }
+
+        @Override
+        public final void onBindingDied(ComponentName component) {
+            Preconditions.checkState(Looper.myLooper() == mHandler.getLooper());
+
+            Log.w(TAG, "[" + mTag + "] " + mBoundServiceInfo + " died");
+
+            // introduce a small delay to prevent spamming binding over and over, since the likely
+            // cause of a binding dying is some package event that may take time to recover from
+            mHandler.postDelayed(() -> onServiceChanged(true), 500);
+        }
+
+        @Override
+        public final void onNullBinding(ComponentName component) {
+            Log.e(TAG, "[" + mTag + "] " + mBoundServiceInfo + " has null binding");
+        }
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/provider/FastPairDataProvider.java b/nearby/service/java/com/android/server/nearby/provider/FastPairDataProvider.java
new file mode 100644
index 0000000..cf9629b
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/provider/FastPairDataProvider.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.provider;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.util.Log;
+
+import androidx.annotation.WorkerThread;
+
+import service.proto.Rpcs;
+
+/** FastPairDataProvider is a singleton that implements APIs to get FastPair data. */
+public class FastPairDataProvider {
+
+    private static final String TAG = "FastPairDataProvider";
+    // TODO(204780849): move this to system api.
+    private static final String ACTION_FAST_PAIR_DATA_PROVIDER =
+              "com.android.nearby.service.FastPairDataProvider";
+    private static FastPairDataProvider sInstance;
+
+    private ProxyFastPairDataProvider mProxyProvider;
+
+    /** Initializes FastPairDataProvider singleton. */
+    public static synchronized FastPairDataProvider init(Context context) {
+        if (sInstance == null) {
+            sInstance = new FastPairDataProvider(context);
+        }
+        if (sInstance.mProxyProvider == null) {
+            Log.wtf(TAG, "no proxy fast pair data provider found");
+        } else {
+            sInstance.mProxyProvider.register();
+        }
+        return sInstance;
+    }
+
+    @Nullable
+    public static synchronized FastPairDataProvider getInstance() {
+        return sInstance;
+    }
+
+    private FastPairDataProvider(Context context) {
+        mProxyProvider = ProxyFastPairDataProvider.create(context, ACTION_FAST_PAIR_DATA_PROVIDER);
+    }
+
+    /** loadFastPairDeviceMetadata. */
+    @WorkerThread
+    @Nullable
+    public Rpcs.GetObservedDeviceResponse loadFastPairDeviceMetadata(byte[] modelId) {
+        if (mProxyProvider != null) {
+            return mProxyProvider.loadFastPairDeviceMetadata(modelId);
+        }
+        throw new IllegalStateException("No ProxyFastPairDataProvider yet constructed");
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/provider/ProxyFastPairDataProvider.java b/nearby/service/java/com/android/server/nearby/provider/ProxyFastPairDataProvider.java
new file mode 100644
index 0000000..073518b
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/provider/ProxyFastPairDataProvider.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.provider;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.nearby.FastPairDeviceMetadataParcel;
+import android.nearby.FastPairDeviceMetadataRequestParcel;
+import android.nearby.IFastPairDataCallback;
+import android.nearby.IFastPairDataProvider;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import androidx.annotation.WorkerThread;
+
+import com.android.server.nearby.common.servicemonitor.CurrentUserServiceProvider;
+import com.android.server.nearby.common.servicemonitor.CurrentUserServiceProvider.BoundServiceInfo;
+import com.android.server.nearby.common.servicemonitor.ServiceMonitor;
+import com.android.server.nearby.common.servicemonitor.ServiceMonitor.ServiceListener;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import service.proto.Rpcs;
+
+/**
+ * Proxy for IFastPairDataProvider implementations.
+ */
+public class ProxyFastPairDataProvider implements ServiceListener<BoundServiceInfo> {
+
+    /**
+     * Creates and registers this proxy. If no suitable service is available for the proxy, returns
+     * null.
+     */
+    @Nullable
+    public static ProxyFastPairDataProvider create(Context context, String action) {
+        ProxyFastPairDataProvider proxy = new ProxyFastPairDataProvider(context, action);
+        if (proxy.checkServiceResolves()) {
+            return proxy;
+        } else {
+            return null;
+        }
+    }
+
+    private final ServiceMonitor mServiceMonitor;
+
+    private ProxyFastPairDataProvider(Context context, String action) {
+        // safe to use direct executor since our locks are not acquired in a code path invoked by
+        // our owning provider
+
+        mServiceMonitor = ServiceMonitor.create(context, "FAST_PAIR_DATA_PROVIDER",
+                CurrentUserServiceProvider.create(context, action), this);
+    }
+
+    private boolean checkServiceResolves() {
+        return mServiceMonitor.checkServiceResolves();
+    }
+
+    /** User service watch to connect to actually services implemented by OEMs. */
+    public void register() {
+        mServiceMonitor.register();
+    }
+
+    // Fast Pair Data Provider doesn't maintain a long running state.
+    // Therefore, it doesn't need setup at bind time.
+    @Override
+    public void onBind(IBinder binder, BoundServiceInfo boundServiceInfo) throws RemoteException {
+    }
+
+    // Fast Pair Data Provider doesn't maintain a long running state.
+    // Therefore, it doesn't need tear down at unbind time.
+    @Override
+    public void onUnbind() {
+    }
+
+    /** Invoke loadFastPairDeviceMetadata. */
+    @WorkerThread
+    @Nullable
+    Rpcs.GetObservedDeviceResponse loadFastPairDeviceMetadata(byte[] modelId) {
+        final CountDownLatch waitForCompletionLatch = new CountDownLatch(1);
+        final AtomicReference<Rpcs.GetObservedDeviceResponse> response = new AtomicReference<>();
+        mServiceMonitor.runOnBinder(new ServiceMonitor.BinderOperation() {
+            @Override
+            public void run(IBinder binder) throws RemoteException {
+                IFastPairDataProvider provider = IFastPairDataProvider.Stub.asInterface(binder);
+                FastPairDeviceMetadataRequestParcel requestParcel =
+                        new FastPairDeviceMetadataRequestParcel();
+                requestParcel.modelId = modelId;
+                IFastPairDataCallback callback = new IFastPairDataCallback.Stub() {
+                    public void onFastPairDeviceMetadataReceived(
+                            FastPairDeviceMetadataParcel metadata) {
+                        response.set(Utils.convert(metadata));
+                        waitForCompletionLatch.countDown();
+                    }
+                };
+                provider.loadFastPairDeviceMetadata(requestParcel, callback);
+            }
+
+            @Override
+            public void onError() {
+                waitForCompletionLatch.countDown();
+            }
+        });
+        try {
+            waitForCompletionLatch.await(10000, TimeUnit.MILLISECONDS);
+        } catch (InterruptedException e) {
+            // skip.
+        }
+        return response.get();
+    }
+}
diff --git a/nearby/service/java/com/android/server/nearby/provider/Utils.java b/nearby/service/java/com/android/server/nearby/provider/Utils.java
new file mode 100644
index 0000000..115dfee
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/provider/Utils.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.nearby.provider;
+
+import android.nearby.FastPairDeviceMetadataParcel;
+
+import com.google.protobuf.ByteString;
+
+import service.proto.Rpcs;
+
+class Utils {
+
+    static Rpcs.GetObservedDeviceResponse convert(FastPairDeviceMetadataParcel metadata) {
+        return Rpcs.GetObservedDeviceResponse.newBuilder()
+          .setDevice(
+              Rpcs.Device.newBuilder()
+              .setImageUrl(metadata.imageUrl)
+              .setIntentUri(metadata.intentUri)
+              .setAntiSpoofingKeyPair(
+                  Rpcs.AntiSpoofingKeyPair.newBuilder()
+                  .setPublicKey(ByteString.copyFrom(metadata.antiSpoofPublicKey))
+                  .build())
+              .setBleTxPower(metadata.bleTxPower)
+              .setTriggerDistance(metadata.triggerDistance)
+              .setDeviceType(Rpcs.DeviceType.forNumber(metadata.deviceType))
+              .build())
+          .setImage(ByteString.copyFrom(metadata.image))
+          .build();
+    }
+}