Nearby Presence Nanoapp host side implementation.
This is used to demo Nearby nanoapp e2e.
To test on device, once the device is booted, screen on will update the
fitler and the filtered result will show in the log:
adb logcat -s NearbyService -s PresenceService -s CHRE
Test: atest NearbyUnitTests and on device test as above.
Bug: 221082271
Change-Id: Idf4a8ce34cd43fcf529a13a9f99be7fe9fa6bf65
Merged-In: Idf4a8ce34cd43fcf529a13a9f99be7fe9fa6bf65
(cherry picked from commit ae768eb44c0f4ffdc393169396616aab2cf46f2a)
diff --git a/nearby/service/Android.bp b/nearby/service/Android.bp
index 6743c21..12fce04 100644
--- a/nearby/service/Android.bp
+++ b/nearby/service/Android.bp
@@ -67,6 +67,7 @@
"modules-utils-handlerexecutor",
"modules-utils-preconditions",
"modules-utils-backgroundthread",
+ "presence-lite-protos",
],
sdk_version: "system_server_current",
// This is included in service-connectivity which is 30+
diff --git a/nearby/service/java/com/android/server/nearby/NearbyService.java b/nearby/service/java/com/android/server/nearby/NearbyService.java
index 5326673..6d149fc 100644
--- a/nearby/service/java/com/android/server/nearby/NearbyService.java
+++ b/nearby/service/java/com/android/server/nearby/NearbyService.java
@@ -26,6 +26,7 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.hardware.location.ContextHubManager;
import android.nearby.INearbyManager;
import android.nearby.IScanListener;
import android.nearby.ScanRequest;
@@ -33,34 +34,42 @@
import com.android.server.nearby.common.locator.LocatorContextWrapper;
import com.android.server.nearby.fastpair.FastPairManager;
+import com.android.server.nearby.injector.ContextHubManagerAdapter;
import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.presence.ChreCommunication;
+import com.android.server.nearby.presence.PresenceManager;
import com.android.server.nearby.provider.DiscoveryProviderManager;
import com.android.server.nearby.provider.FastPairDataProvider;
-/**
- * Service implementing nearby functionality.
- */
+import java.util.concurrent.Executors;
+
+import service.proto.Blefilter;
+
+/** Service implementing nearby functionality. */
public class NearbyService extends INearbyManager.Stub {
public static final String TAG = "NearbyService";
private final Context mContext;
private final SystemInjector mSystemInjector;
private final FastPairManager mFastPairManager;
- private final BroadcastReceiver mBluetoothReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- int state = intent
- .getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
- if (state == BluetoothAdapter.STATE_ON) {
- if (mSystemInjector != null) {
- // Have to do this logic in listener. Even during PHASE_BOOT_COMPLETED
- // phase, BluetoothAdapter is not null, the BleScanner is null.
- Log.v(TAG, "Initiating BluetoothAdapter when Bluetooth is turned on.");
- mSystemInjector.initializeBluetoothAdapter();
+ private final PresenceManager mPresenceManager;
+ private final BroadcastReceiver mBluetoothReceiver =
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ int state =
+ intent.getIntExtra(
+ BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR);
+ if (state == BluetoothAdapter.STATE_ON) {
+ if (mSystemInjector != null) {
+ // Have to do this logic in listener. Even during PHASE_BOOT_COMPLETED
+ // phase, BluetoothAdapter is not null, the BleScanner is null.
+ Log.v(TAG, "Initiating BluetoothAdapter when Bluetooth is turned on.");
+ mSystemInjector.initializeBluetoothAdapter();
+ }
+ }
}
- }
- }
- };
+ };
private DiscoveryProviderManager mProviderManager;
public NearbyService(Context context) {
@@ -69,6 +78,19 @@
mProviderManager = new DiscoveryProviderManager(context, mSystemInjector);
final LocatorContextWrapper lcw = new LocatorContextWrapper(context, null);
mFastPairManager = new FastPairManager(lcw);
+ mPresenceManager =
+ new PresenceManager(
+ mContext,
+ (results) -> {
+ // TODO(b/221082271): hooked with API codes.
+ for (Blefilter.BleFilterResult result : results.getResultList()) {
+ Log.i(
+ TAG,
+ String.format(
+ "received filter result with id: %d",
+ result.getId()));
+ }
+ });
}
@Override
@@ -84,7 +106,7 @@
/**
* Called by the service initializer.
*
- * {@see com.android.server.SystemService#onBootPhase}.
+ * <p>{@see com.android.server.SystemService#onBootPhase}.
*/
public void onBootPhase(int phase) {
switch (phase) {
@@ -95,17 +117,22 @@
case PHASE_BOOT_COMPLETED:
// The nearby service must be functioning after this boot phase.
mSystemInjector.initializeBluetoothAdapter();
- mContext.registerReceiver(mBluetoothReceiver,
+ mContext.registerReceiver(
+ mBluetoothReceiver,
new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
mFastPairManager.initiate();
+ mSystemInjector.initializeContextHubManagerAdapter();
+ mPresenceManager.initiate(
+ new ChreCommunication(
+ mSystemInjector, Executors.newSingleThreadExecutor()));
break;
}
}
private static final class SystemInjector implements Injector {
private final Context mContext;
- @Nullable
- private BluetoothAdapter mBluetoothAdapter;
+ @Nullable private BluetoothAdapter mBluetoothAdapter;
+ @Nullable private ContextHubManagerAdapter mContextHubManagerAdapter;
SystemInjector(Context context) {
mContext = context;
@@ -117,6 +144,11 @@
return mBluetoothAdapter;
}
+ @Override
+ public ContextHubManagerAdapter getContextHubManagerAdapter() {
+ return mContextHubManagerAdapter;
+ }
+
synchronized void initializeBluetoothAdapter() {
if (mBluetoothAdapter != null) {
return;
@@ -127,6 +159,17 @@
}
mBluetoothAdapter = manager.getAdapter();
}
- }
+ synchronized void initializeContextHubManagerAdapter() {
+ if (mContextHubManagerAdapter != null) {
+ return;
+ }
+ ContextHubManager manager = mContext.getSystemService(ContextHubManager.class);
+ if (manager == null) {
+ return;
+ }
+ mContextHubManagerAdapter = new ContextHubManagerAdapter(manager);
+ }
+
+ }
}
diff --git a/nearby/service/java/com/android/server/nearby/injector/ContextHubManagerAdapter.java b/nearby/service/java/com/android/server/nearby/injector/ContextHubManagerAdapter.java
new file mode 100644
index 0000000..9af0227
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/injector/ContextHubManagerAdapter.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2022 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.injector;
+
+import android.hardware.location.ContextHubClient;
+import android.hardware.location.ContextHubClientCallback;
+import android.hardware.location.ContextHubInfo;
+import android.hardware.location.ContextHubManager;
+import android.hardware.location.ContextHubTransaction;
+import android.hardware.location.NanoAppState;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/** Wrap {@link ContextHubManager} for dependence injection. */
+public class ContextHubManagerAdapter {
+ private final ContextHubManager mManager;
+
+ public ContextHubManagerAdapter(ContextHubManager manager) {
+ mManager = manager;
+ }
+
+ /**
+ * Returns the list of ContextHubInfo objects describing the available Context Hubs.
+ *
+ * @return the list of ContextHubInfo objects
+ * @see ContextHubInfo
+ */
+ public List<ContextHubInfo> getContextHubs() {
+ return mManager.getContextHubs();
+ }
+
+ /**
+ * Requests a query for nanoapps loaded at the specified Context Hub.
+ *
+ * @param hubInfo the hub to query a list of nanoapps from
+ * @return the ContextHubTransaction of the request
+ * @throws NullPointerException if hubInfo is null
+ */
+ public ContextHubTransaction<List<NanoAppState>> queryNanoApps(ContextHubInfo hubInfo) {
+ return mManager.queryNanoApps(hubInfo);
+ }
+
+ /**
+ * Creates and registers a client and its callback with the Context Hub Service.
+ *
+ * <p>A client is registered with the Context Hub Service for a specified Context Hub. When the
+ * registration succeeds, the client can send messages to nanoapps through the returned {@link
+ * ContextHubClient} object, and receive notifications through the provided callback.
+ *
+ * @param hubInfo the hub to attach this client to
+ * @param executor the executor to invoke the callback
+ * @param callback the notification callback to register
+ * @return the registered client object
+ * @throws IllegalArgumentException if hubInfo does not represent a valid hub
+ * @throws IllegalStateException if there were too many registered clients at the service
+ * @throws NullPointerException if callback, hubInfo, or executor is null
+ */
+ public ContextHubClient createClient(
+ ContextHubInfo hubInfo, ContextHubClientCallback callback, Executor executor) {
+ return mManager.createClient(hubInfo, callback, executor);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/injector/Injector.java b/nearby/service/java/com/android/server/nearby/injector/Injector.java
index d221722..f990dc9 100644
--- a/nearby/service/java/com/android/server/nearby/injector/Injector.java
+++ b/nearby/service/java/com/android/server/nearby/injector/Injector.java
@@ -19,14 +19,14 @@
import android.bluetooth.BluetoothAdapter;
/**
- * Nearby dependency injector. To be used for accessing various Nearby class instances and as a
- * handle for mock injection.
+ * Nearby dependency injector. To be used for accessing various Nearby class instances and as a
+ * handle for mock injection.
*/
public interface Injector {
- /**
- * Get the BluetoothAdapter for BleDiscoveryProvider to scan.
- */
+ /** Get the BluetoothAdapter for BleDiscoveryProvider to scan. */
BluetoothAdapter getBluetoothAdapter();
+ /** Get the ContextHubManagerAdapter for ChreDiscoveryProvider to scan. */
+ ContextHubManagerAdapter getContextHubManagerAdapter();
}
diff --git a/nearby/service/java/com/android/server/nearby/presence/ChreCommunication.java b/nearby/service/java/com/android/server/nearby/presence/ChreCommunication.java
new file mode 100644
index 0000000..fc9863e
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/presence/ChreCommunication.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2022 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.presence;
+
+import android.annotation.Nullable;
+import android.hardware.location.ContextHubClient;
+import android.hardware.location.ContextHubClientCallback;
+import android.hardware.location.ContextHubInfo;
+import android.hardware.location.ContextHubTransaction;
+import android.hardware.location.NanoAppMessage;
+import android.hardware.location.NanoAppState;
+import android.util.Log;
+
+import com.android.server.nearby.injector.ContextHubManagerAdapter;
+import com.android.server.nearby.injector.Injector;
+
+import com.google.common.base.Preconditions;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.concurrent.Executor;
+
+/**
+ * Responsible for setting up communication with the appropriate contexthub on the device and
+ * handling nanoapp messages to / from it.
+ */
+public class ChreCommunication extends ContextHubClientCallback {
+
+ /** Callback that receives messages forwarded from the context hub. */
+ public interface ContextHubCommsCallback {
+ /** Indicates whether {@link ChreCommunication} was started successfully. */
+ void started(boolean success);
+
+ /** Indicates the ContextHub has been restarted. */
+ void onHubReset();
+
+ /**
+ * Indicates the given {@code nanoAppId} has been restarted. Either via code download or by
+ * being enabled by CHRE.
+ */
+ void onNanoAppRestart(long nanoAppId);
+
+ /** Indicates a new {@link NanoAppMessage} has been received. */
+ void onMessageFromNanoApp(NanoAppMessage message);
+ }
+
+ public static final String TAG = "PresenceService";
+ @Nullable private final ContextHubManagerAdapter mManager;
+ private final Executor mExecutor;
+
+ private boolean mStarted = false;
+ @Nullable private ContextHubCommsCallback mCallback;
+ @Nullable private ContextHubClient mContextHubClient;
+
+ public ChreCommunication(Injector injector, Executor executor) {
+ this.mManager = injector.getContextHubManagerAdapter();
+ mExecutor = executor;
+ }
+
+ /**
+ * Starts communication with the contexthub. This will invoke {@link
+ * ContextHubCommsCallback#start(boolean)} on completion.
+ *
+ * @param nanoAppIds - List of IDs that must have at least one match inside the chosen
+ * contexthub.
+ */
+ public synchronized void start(ContextHubCommsCallback callback, Set<Long> nanoAppIds) {
+ if (this.mManager == null) {
+ Log.e(TAG, "ContexHub not available in this device");
+ return;
+ } else {
+ Log.i(TAG, "Start ChreCommunication");
+ }
+ Preconditions.checkNotNull(callback);
+ if (nanoAppIds.isEmpty() || mManager == null) {
+ callback.started(false);
+ return;
+ }
+ if (mStarted) {
+ this.mCallback.started(true);
+ return;
+ }
+
+ // Use this to indicate whether stop was called before the transaction below
+ // completes.
+ mStarted = true;
+ this.mCallback = callback;
+
+ List<ContextHubInfo> contextHubs = mManager.getContextHubs();
+
+ // Make a copy of the list so we can modify it during our async callbacks (in case the code
+ // is still iterating)
+ List<ContextHubInfo> validContextHubs = new ArrayList<>(contextHubs);
+
+ for (ContextHubInfo info : contextHubs) {
+ ContextHubTransaction<List<NanoAppState>> transaction = mManager.queryNanoApps(info);
+ Log.i(TAG, "After query Nano Apps ");
+ transaction.setOnCompleteListener(
+ new OnQueryCompleteListener(info, validContextHubs, nanoAppIds), mExecutor);
+ }
+ }
+
+ /**
+ * Closes the connection to the {@link ContextHub} chosen during start.
+ *
+ * <p>NOTE: Do not invoke any other methods on this class after this returns.
+ */
+ public synchronized void stop() {
+ if (!mStarted) {
+ return;
+ }
+ mStarted = false;
+ if (mContextHubClient != null) {
+ mContextHubClient.close();
+ mContextHubClient = null;
+ }
+ }
+
+ /** Sends a {@link NanoAppMessage} to Context Hub Nearby nanoapp. */
+ public synchronized boolean sendMessageToNanoApp(NanoAppMessage message) {
+ if (mContextHubClient == null) {
+ Log.i(TAG, "Error sending message to nanoapp, contextHubClient is null");
+ return false;
+ }
+ int result = mContextHubClient.sendMessageToNanoApp(message);
+ if (result != ContextHubTransaction.RESULT_SUCCESS) {
+ Log.i(
+ TAG,
+ String.format(
+ Locale.getDefault(),
+ "Error sending message to nanoapp: %s",
+ contextHubTransactionResultToString(result)));
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public synchronized void onMessageFromNanoApp(ContextHubClient client, NanoAppMessage message) {
+ mCallback.onMessageFromNanoApp(message);
+ }
+
+ @Override
+ public synchronized void onHubReset(ContextHubClient client) {
+ mCallback.onHubReset();
+ }
+
+ @Override
+ public synchronized void onNanoAppLoaded(ContextHubClient client, long nanoAppId) {
+ Log.i(TAG, String.format("Nanoapp ID loaded: %s", nanoAppId));
+ mCallback.onNanoAppRestart(nanoAppId);
+ }
+
+ private static String contextHubTransactionResultToString(int result) {
+ switch (result) {
+ case ContextHubTransaction.RESULT_SUCCESS:
+ return "RESULT_SUCCESS";
+ case ContextHubTransaction.RESULT_FAILED_UNKNOWN:
+ return "RESULT_FAILED_UNKNOWN";
+ case ContextHubTransaction.RESULT_FAILED_BAD_PARAMS:
+ return "RESULT_FAILED_BAD_PARAMS";
+ case ContextHubTransaction.RESULT_FAILED_UNINITIALIZED:
+ return "RESULT_FAILED_UNINITIALIZED";
+ case ContextHubTransaction.RESULT_FAILED_BUSY:
+ return "RESULT_FAILED_BUSY";
+ case ContextHubTransaction.RESULT_FAILED_AT_HUB:
+ return "RESULT_FAILED_AT_HUB";
+ case ContextHubTransaction.RESULT_FAILED_TIMEOUT:
+ return "RESULT_FAILED_TIMEOUT";
+ case ContextHubTransaction.RESULT_FAILED_SERVICE_INTERNAL_FAILURE:
+ return "RESULT_FAILED_SERVICE_INTERNAL_FAILURE";
+ case ContextHubTransaction.RESULT_FAILED_HAL_UNAVAILABLE:
+ return "RESULT_FAILED_HAL_UNAVAILABLE";
+ default:
+ return String.format(Locale.getDefault(), "UNKNOWN_RESULT value=%d", result);
+ }
+ }
+
+ /**
+ * Used when initializing the class to identify the appropriate {@link ContextHubInfo} to listen
+ * to.
+ */
+ class OnQueryCompleteListener
+ implements ContextHubTransaction.OnCompleteListener<List<NanoAppState>> {
+
+ private final ContextHubInfo mQueriedContextHub;
+ private final List<ContextHubInfo> mContextHubs;
+ private final Set<Long> mNanoAppIds;
+
+ OnQueryCompleteListener(
+ ContextHubInfo queriedContextHub,
+ List<ContextHubInfo> contextHubs,
+ Set<Long> nanoAppIds) {
+ this.mQueriedContextHub = queriedContextHub;
+ this.mContextHubs = contextHubs;
+ this.mNanoAppIds = nanoAppIds;
+ }
+
+ @Override
+ public void onComplete(
+ ContextHubTransaction<List<NanoAppState>> transaction,
+ ContextHubTransaction.Response<List<NanoAppState>> response) {
+ Log.i(TAG, "query nano app onComplete");
+ // Ensure the class hasn't found a client already or stop hasn't been called before
+ // the transaction completed to avoid messing with state.
+ if (mContextHubClient != null || !mStarted) {
+ return;
+ }
+
+ if (response.getResult() == ContextHubTransaction.RESULT_SUCCESS) {
+ for (NanoAppState state : response.getContents()) {
+ if (mNanoAppIds.contains(state.getNanoAppId())) {
+ Log.i(
+ TAG,
+ String.format(
+ "Found valid contexthub: %s", mQueriedContextHub.getId()));
+ mContextHubClient =
+ mManager.createClient(
+ mQueriedContextHub, ChreCommunication.this, mExecutor);
+ mCallback.started(true);
+ return;
+ }
+ }
+ Log.e(
+ TAG,
+ String.format(
+ "Didn't find the nanoapp on contexthub: %s",
+ mQueriedContextHub.getId()));
+ } else {
+ Log.e(
+ TAG,
+ String.format(
+ "Failed to communicate with contexthub: %s",
+ mQueriedContextHub.getId()));
+ }
+
+ mContextHubs.remove(mQueriedContextHub);
+ // If this is the last context hub response left to receive, indicate that
+ // there isn't a valid context available on this device.
+ if (mContextHubs.isEmpty()) {
+ mCallback.started(false);
+ }
+ }
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/presence/PresenceManager.java b/nearby/service/java/com/android/server/nearby/presence/PresenceManager.java
new file mode 100644
index 0000000..66d4864
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/presence/PresenceManager.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2022 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.presence;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.hardware.location.NanoAppMessage;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import com.google.protobuf.ByteString;
+import com.google.protobuf.InvalidProtocolBufferException;
+
+import java.util.Collections;
+
+import service.proto.Blefilter;
+
+/** PresenceManager is the class initiated in nearby service to handle presence related work. */
+public class PresenceManager {
+ /** Callback that receives filter results from CHRE Nanoapp. */
+ public interface PresenceCallback {
+ /** Called when {@link BleFilterResults} has been received. */
+ void onFilterResults(Blefilter.BleFilterResults filterResults);
+ }
+
+ private static final String TAG = "PresenceService";
+ // Nanoapp ID reserved for Nearby Presence.
+ /** @hide */
+ @VisibleForTesting
+ public static final long NANOAPP_ID = 0x476f6f676c001031L;
+ /** @hide */
+ @VisibleForTesting
+ public static final int NANOAPP_MESSAGE_TYPE_FILTER = 3;
+ /** @hide */
+ @VisibleForTesting
+ public static final int NANOAPP_MESSAGE_TYPE_FILTER_RESULT = 4;
+ private final Context mContext;
+ private final PresenceCallback mPresenceCallback;
+ private final ChreCallback mChreCallback;
+ private ChreCommunication mChreCommunication;
+
+ private Blefilter.BleFilters mFilters = null;
+ private boolean mChreStarted = false;
+
+ private final IntentFilter mIntentFilter;
+
+ private final BroadcastReceiver mScreenBroadcastReceiver =
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(Intent.ACTION_SCREEN_ON)) {
+ // TODO(b/221082271): removed this faked data once hooked with API codes.
+ Log.d(TAG, "Update Presence CHRE filter");
+ ByteString mac_addr = ByteString.copyFrom(new byte[] {1, 2, 3, 4, 5, 6});
+ Blefilter.BleFilter filter =
+ Blefilter.BleFilter.newBuilder()
+ .setId(0)
+ .setUuid(0xFCF1)
+ .setIntent(1)
+ .setMacAddress(mac_addr)
+ .build();
+ Blefilter.BleFilters filters =
+ Blefilter.BleFilters.newBuilder().addFilter(filter).build();
+ updateFilters(filters);
+ }
+ }
+ };
+
+ public PresenceManager(Context context, PresenceCallback presenceCallback) {
+ mContext = context;
+ mPresenceCallback = presenceCallback;
+ mChreCallback = new ChreCallback();
+ mIntentFilter = new IntentFilter();
+ }
+
+ /** Function called when nearby service start. */
+ public void initiate(ChreCommunication chreCommunication) {
+ mChreCommunication = chreCommunication;
+ mChreCommunication.start(mChreCallback, Collections.singleton(NANOAPP_ID));
+
+ mIntentFilter.addAction(Intent.ACTION_SCREEN_ON);
+ mContext.registerReceiver(mScreenBroadcastReceiver, mIntentFilter);
+ }
+
+ /** Updates the fitlers in Context Hub. */
+ public synchronized void updateFilters(Blefilter.BleFilters filters) {
+ mFilters = filters;
+ if (mChreStarted) {
+ sendFilters(mFilters);
+ mFilters = null;
+ }
+ }
+
+ private void sendFilters(Blefilter.BleFilters filters) {
+ NanoAppMessage message =
+ NanoAppMessage.createMessageToNanoApp(
+ NANOAPP_ID, NANOAPP_MESSAGE_TYPE_FILTER, filters.toByteArray());
+ if (!mChreCommunication.sendMessageToNanoApp(message)) {
+ Log.e(TAG, "Failed to send filters to CHRE.");
+ }
+ }
+
+ private class ChreCallback implements ChreCommunication.ContextHubCommsCallback {
+
+ @Override
+ public void started(boolean success) {
+ if (success) {
+ synchronized (PresenceManager.this) {
+ Log.i(TAG, "CHRE communication started");
+ mChreStarted = true;
+ if (mFilters != null) {
+ sendFilters(mFilters);
+ mFilters = null;
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onHubReset() {
+ // TODO(b/221082271): hooked with upper level codes.
+ Log.i(TAG, "CHRE reset.");
+ }
+
+ @Override
+ public void onNanoAppRestart(long nanoAppId) {
+ // TODO(b/221082271): hooked with upper level codes.
+ Log.i(TAG, String.format("CHRE NanoApp %d restart.", nanoAppId));
+ }
+
+ @Override
+ public void onMessageFromNanoApp(NanoAppMessage message) {
+ if (message.getNanoAppId() != NANOAPP_ID) {
+ Log.e(TAG, "Received message from unknown nano app.");
+ return;
+ }
+ if (message.getMessageType() == NANOAPP_MESSAGE_TYPE_FILTER_RESULT) {
+ try {
+ Blefilter.BleFilterResults results =
+ Blefilter.BleFilterResults.parseFrom(message.getMessageBody());
+ mPresenceCallback.onFilterResults(results);
+ } catch (InvalidProtocolBufferException e) {
+ Log.e(
+ TAG,
+ String.format("Failed to decode the filter result %s", e.toString()));
+ }
+ }
+ }
+ }
+}
diff --git a/nearby/service/proto/Android.bp b/nearby/service/proto/Android.bp
index d8c059e..1b00cf6 100644
--- a/nearby/service/proto/Android.bp
+++ b/nearby/service/proto/Android.bp
@@ -23,10 +23,22 @@
},
sdk_version: "system_current",
min_sdk_version: "30",
- srcs: ["src/*/*.proto"],
+ srcs: ["src/fastpair/*.proto"],
apex_available: [
"com.android.tethering",
],
}
-
+java_library {
+ name: "presence-lite-protos",
+ proto: {
+ type: "lite",
+ canonical_path_from_root: false,
+ },
+ sdk_version: "system_current",
+ min_sdk_version: "30",
+ srcs: ["src/presence/*.proto"],
+ apex_available: [
+ "com.android.tethering",
+ ],
+}
\ No newline at end of file
diff --git a/nearby/service/proto/src/presence/blefilter.proto b/nearby/service/proto/src/presence/blefilter.proto
new file mode 100644
index 0000000..da56522
--- /dev/null
+++ b/nearby/service/proto/src/presence/blefilter.proto
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+// Proto Messages define the interface between Nearby nanoapp and its host.
+//
+// Host registers its interest in BLE event by configuring nanoapp with Filters.
+// The nanoapp keeps watching BLE events and notifies host once an event matches
+// a Filter.
+//
+// Each Filter is defined by its id (required) with optional fields of rssi,
+// uuid, MAC etc. The host should guarantee the uniqueness of ids. It is
+// convenient to assign id incrementally when adding a Filter such that its id
+// is the same as the index of the repeated field in Filters.
+//
+// The nanoapp compares each BLE event against the list of Filters, and notifies
+// host when the event matches a Filter. The Field's id will be sent back to
+// host in the FilterResult.
+//
+// It is possible for the nanoapp to return multiple ids when an event matches
+// multiple Filters.
+
+syntax = "proto2";
+
+package service.proto;
+
+// Certificate to verify BLE events from trusted devices.
+// When receiving an advertisement from a remote device, it will
+// be decrypted by authenticity_key and SHA hashed. The device
+// is verified as trusted if the hash result is equal to
+// metadata_encryption_key_tag.
+// See details in go/ns-certificates.
+message PublicateCertificate {
+ optional bytes authenticity_key = 1;
+ optional bytes metadata_encryption_key_tag = 2;
+}
+
+message BleFilter {
+ optional uint32 id = 1; // Required, unique id of this filter.
+ // Maximum delay to notify the client after an event occurs.
+ optional uint32 latency_ms = 2;
+ optional uint32 uuid = 3;
+ // MAC address of the advertising device.
+ optional bytes mac_address = 4;
+ optional bytes mac_mask = 5;
+ // Represents an action that scanners should take when they receive this
+ // packet. See go/nearby-presence-spec for details.
+ optional uint32 intent = 6;
+ // Notify the client if the advertising device is within the distance.
+ // For moving object, the distance is averaged over data sampled within
+ // the period of latency defined above.
+ optional float distance_m = 7;
+ // Used to verify the list of trusted devices.
+ repeated PublicateCertificate certficate = 8;
+}
+
+message BleFilters {
+ repeated BleFilter filter = 1;
+}
+
+// FilterResult is returned to host when a BLE event matches a Filter.
+message BleFilterResult {
+ optional uint32 id = 1; // id of the matched Filter.
+ // TODO(b/193756395): replace with BLE event proto.
+ optional bytes raw_data = 2;
+}
+
+message BleFilterResults {
+ repeated BleFilterResult result = 1;
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/ChreCommunicationTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/ChreCommunicationTest.java
new file mode 100644
index 0000000..b221f46
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/ChreCommunicationTest.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2022 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.presence;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.hardware.location.ContextHubClient;
+import android.hardware.location.ContextHubInfo;
+import android.hardware.location.ContextHubTransaction;
+import android.hardware.location.NanoAppMessage;
+import android.hardware.location.NanoAppState;
+
+import com.android.server.nearby.injector.ContextHubManagerAdapter;
+import com.android.server.nearby.injector.Injector;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+public class ChreCommunicationTest {
+ @Mock Injector mInjector;
+ @Mock ContextHubManagerAdapter mManager;
+ @Mock ContextHubTransaction<List<NanoAppState>> mTransaction;
+ @Mock ContextHubTransaction.Response<List<NanoAppState>> mTransactionResponse;
+ @Mock ContextHubClient mClient;
+ @Mock ChreCommunication.ContextHubCommsCallback mChreCallback;
+
+ @Captor
+ ArgumentCaptor<ChreCommunication.OnQueryCompleteListener> mOnQueryCompleteListenerCaptor;
+
+ private ChreCommunication mChreCommunication;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ when(mInjector.getContextHubManagerAdapter()).thenReturn(mManager);
+ when(mManager.getContextHubs()).thenReturn(Collections.singletonList(new ContextHubInfo()));
+ when(mManager.queryNanoApps(any())).thenReturn(mTransaction);
+ when(mManager.createClient(any(), any(), any())).thenReturn(mClient);
+ when(mTransactionResponse.getResult()).thenReturn(ContextHubTransaction.RESULT_SUCCESS);
+ when(mTransactionResponse.getContents())
+ .thenReturn(
+ Collections.singletonList(
+ new NanoAppState(PresenceManager.NANOAPP_ID, 1, true)));
+
+ mChreCommunication = new ChreCommunication(mInjector, new InlineExecutor());
+ mChreCommunication.start(mChreCallback, Collections.singleton(PresenceManager.NANOAPP_ID));
+
+ verify(mTransaction).setOnCompleteListener(mOnQueryCompleteListenerCaptor.capture(), any());
+ mOnQueryCompleteListenerCaptor.getValue().onComplete(mTransaction, mTransactionResponse);
+ }
+
+ @Test
+ public void testStart() {
+ verify(mChreCallback).started(true);
+ }
+
+ @Test
+ public void testStop() {
+ mChreCommunication.stop();
+ verify(mClient).close();
+ }
+
+ @Test
+ public void testSendMessageToNanApp() {
+ NanoAppMessage message =
+ NanoAppMessage.createMessageToNanoApp(
+ PresenceManager.NANOAPP_ID,
+ PresenceManager.NANOAPP_MESSAGE_TYPE_FILTER,
+ new byte[] {1, 2, 3});
+ mChreCommunication.sendMessageToNanoApp(message);
+ verify(mClient).sendMessageToNanoApp(eq(message));
+ }
+
+ @Test
+ public void testOnMessageFromNanoApp() {
+ NanoAppMessage message =
+ NanoAppMessage.createMessageToNanoApp(
+ PresenceManager.NANOAPP_ID,
+ PresenceManager.NANOAPP_MESSAGE_TYPE_FILTER_RESULT,
+ new byte[] {1, 2, 3});
+ mChreCommunication.onMessageFromNanoApp(mClient, message);
+ verify(mChreCallback).onMessageFromNanoApp(eq(message));
+ }
+
+ @Test
+ public void testOnHubReset() {
+ mChreCommunication.onHubReset(mClient);
+ verify(mChreCallback).onHubReset();
+ }
+
+ @Test
+ public void testOnNanoAppLoaded() {
+ mChreCommunication.onNanoAppLoaded(mClient, PresenceManager.NANOAPP_ID);
+ verify(mChreCallback).onNanoAppRestart(eq(PresenceManager.NANOAPP_ID));
+ }
+
+ private static class InlineExecutor implements Executor {
+ @Override
+ public void execute(Runnable command) {
+ command.run();
+ }
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceManagerTest.java
new file mode 100644
index 0000000..f9676e3
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceManagerTest.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2022 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.presence;
+
+import static com.android.server.nearby.presence.PresenceManager.NANOAPP_ID;
+import static com.android.server.nearby.presence.PresenceManager.NANOAPP_MESSAGE_TYPE_FILTER_RESULT;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.hardware.location.NanoAppMessage;
+
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Collections;
+
+import service.proto.Blefilter;
+
+public class PresenceManagerTest {
+ @Mock private Context mContext;
+ @Mock private PresenceManager.PresenceCallback mPresenceCallback;
+ @Mock private ChreCommunication mChreCommunication;
+
+ @Captor ArgumentCaptor<ChreCommunication.ContextHubCommsCallback> mChreCallbackCaptor;
+ @Captor ArgumentCaptor<NanoAppMessage> mNanoAppMessageCaptor;
+
+ private PresenceManager mPresenceManager;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mPresenceManager = new PresenceManager(mContext, mPresenceCallback);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testInit() {
+ mPresenceManager.initiate(mChreCommunication);
+ verify(mChreCommunication).start(any(), eq(Collections.singleton(NANOAPP_ID)));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testOnFilterResults() {
+ Blefilter.BleFilterResults results = Blefilter.BleFilterResults.newBuilder().build();
+ NanoAppMessage chre_message =
+ NanoAppMessage.createMessageToNanoApp(
+ NANOAPP_ID, NANOAPP_MESSAGE_TYPE_FILTER_RESULT, results.toByteArray());
+ mPresenceManager.initiate(mChreCommunication);
+ verify(mChreCommunication).start(mChreCallbackCaptor.capture(), any());
+ mChreCallbackCaptor.getValue().onMessageFromNanoApp(chre_message);
+ verify(mPresenceCallback).onFilterResults(eq(results));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testUpdateFiltersBeforeChreStarted() {
+ Blefilter.BleFilters filters = Blefilter.BleFilters.newBuilder().build();
+ mPresenceManager.updateFilters(filters);
+ verify(mChreCommunication, never()).sendMessageToNanoApp(any());
+ mPresenceManager.initiate(mChreCommunication);
+ verify(mChreCommunication).start(mChreCallbackCaptor.capture(), any());
+ mChreCallbackCaptor.getValue().started(true);
+ verify(mChreCommunication, times(1)).sendMessageToNanoApp(mNanoAppMessageCaptor.capture());
+ assertThat(mNanoAppMessageCaptor.getValue().getMessageBody())
+ .isEqualTo(filters.toByteArray());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testUpdateFiltersAfterChreStarted() {
+ mPresenceManager.initiate(mChreCommunication);
+ verify(mChreCommunication).start(mChreCallbackCaptor.capture(), any());
+ mChreCallbackCaptor.getValue().started(true);
+ Blefilter.BleFilters filters = Blefilter.BleFilters.newBuilder().build();
+ mPresenceManager.updateFilters(filters);
+ verify(mChreCommunication, times(1)).sendMessageToNanoApp(mNanoAppMessageCaptor.capture());
+ assertThat(mNanoAppMessageCaptor.getValue().getMessageBody())
+ .isEqualTo(filters.toByteArray());
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/BleDiscoveryProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/BleDiscoveryProviderTest.java
index 4c78885..8e97443 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/BleDiscoveryProviderTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/BleDiscoveryProviderTest.java
@@ -32,6 +32,7 @@
import androidx.test.platform.app.InstrumentationRegistry;
+import com.android.server.nearby.injector.ContextHubManagerAdapter;
import com.android.server.nearby.injector.Injector;
import org.junit.Before;
@@ -81,6 +82,11 @@
public BluetoothAdapter getBluetoothAdapter() {
return mBluetoothAdapter;
}
+
+ @Override
+ public ContextHubManagerAdapter getContextHubManagerAdapter() {
+ return null;
+ }
}
private ScanResult createScanResult() {