[automerger skipped] cronet: delete cronet prebuilts am: 1310346c5b -s ours
am skip reason: Merged-In I2a5ba4d5805fbb56945de74eb14ae9ef510e6f45 with SHA-1 00e8b19a11 is already in history
Original change: https://android-review.googlesource.com/c/platform/packages/modules/Connectivity/+/2388908
Change-Id: Ibbe0415ff069dffb88383f280fa0ce612f376db3
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/Cronet/TEST_MAPPING b/Cronet/TEST_MAPPING
deleted file mode 100644
index b1f3088..0000000
--- a/Cronet/TEST_MAPPING
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "presubmit": [
- {
- "name": "CtsCronetTestCases"
- }
- ]
-}
diff --git a/Cronet/tests/cts/Android.bp b/Cronet/tests/cts/Android.bp
index 95fc9ab..f9ff7e0 100644
--- a/Cronet/tests/cts/Android.bp
+++ b/Cronet/tests/cts/Android.bp
@@ -21,7 +21,7 @@
// cronet_test_java_defaults can be used to specify a java_defaults target that
// either enables or disables Cronet tests. This is used to disable Cronet
// tests on tm-mainline-prod where the required APIs are not present.
-cronet_test_java_defaults = "CronetTestJavaDefaultsEnabled"
+cronet_test_java_defaults = "CronetTestJavaDefaultsDisabled"
// This is a placeholder comment to avoid merge conflicts
// as cronet_test_java_defaults may have different values
// depending on the branch
@@ -42,7 +42,7 @@
}
android_test {
- name: "CtsCronetTestCases",
+ name: "CtsNetHttpTestCases",
compile_multilib: "both", // Include both the 32 and 64 bit versions
defaults: [
"CronetTestJavaDefaults",
@@ -59,6 +59,7 @@
"ctstestrunner-axt",
"ctstestserver",
"junit",
+ "hamcrest-library",
],
libs: [
"android.test.runner",
diff --git a/Cronet/tests/cts/AndroidTest.xml b/Cronet/tests/cts/AndroidTest.xml
index d2422f1..e0421fd 100644
--- a/Cronet/tests/cts/AndroidTest.xml
+++ b/Cronet/tests/cts/AndroidTest.xml
@@ -23,14 +23,14 @@
<option name="config-descriptor:metadata" key="parameter" value="secondary_user" />
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="cleanup-apks" value="true" />
- <option name="test-file-name" value="CtsCronetTestCases.apk" />
+ <option name="test-file-name" value="CtsNetHttpTestCases.apk" />
</target_preparer>
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
<option name="package" value="android.net.http.cts" />
<option name="runtime-hint" value="10s" />
</test>
- <!-- Only run CtsCronetTestcasess in MTS if the Tethering Mainline module is installed. -->
+ <!-- Only run CtsNetHttpTestCases in MTS if the Tethering Mainline module is installed. -->
<object type="module_controller"
class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
<option name="mainline-module-package-name" value="com.google.android.tethering" />
diff --git a/Cronet/tests/cts/src/android/net/http/cts/HttpEngineTest.java b/Cronet/tests/cts/src/android/net/http/cts/HttpEngineTest.java
new file mode 100644
index 0000000..b07367a
--- /dev/null
+++ b/Cronet/tests/cts/src/android/net/http/cts/HttpEngineTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.http.cts;
+
+import static android.net.http.cts.util.TestUtilsKt.assertOKStatusCode;
+import static android.net.http.cts.util.TestUtilsKt.skipIfNoInternetConnection;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.Assert.assertEquals;
+
+import android.content.Context;
+import android.net.http.HttpEngine;
+import android.net.http.UrlRequest;
+import android.net.http.UrlResponseInfo;
+import android.net.http.cts.util.TestUrlRequestCallback;
+import android.net.http.cts.util.TestUrlRequestCallback.ResponseStep;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class HttpEngineTest {
+ private static final String HOST = "source.android.com";
+ private static final String URL = "https://" + HOST;
+
+ private HttpEngine.Builder mEngineBuilder;
+ private TestUrlRequestCallback mCallback;
+ private HttpEngine mEngine;
+
+ @Before
+ public void setUp() throws Exception {
+ Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ skipIfNoInternetConnection(context);
+ mEngineBuilder = new HttpEngine.Builder(context);
+ mCallback = new TestUrlRequestCallback();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (mEngine != null) {
+ mEngine.shutdown();
+ }
+ }
+
+ @Test
+ public void testHttpEngine_Default() throws Exception {
+ mEngine = mEngineBuilder.build();
+ UrlRequest.Builder builder =
+ mEngine.newUrlRequestBuilder(URL, mCallback, mCallback.getExecutor());
+ builder.build().start();
+
+ mCallback.expectCallback(ResponseStep.ON_SUCCEEDED);
+ UrlResponseInfo info = mCallback.mResponseInfo;
+ assertOKStatusCode(info);
+ assertEquals("h2", info.getNegotiatedProtocol());
+ }
+
+ @Test
+ public void testHttpEngine_DisableHttp2() throws Exception {
+ mEngine = mEngineBuilder.setEnableHttp2(false).build();
+ UrlRequest.Builder builder =
+ mEngine.newUrlRequestBuilder(URL, mCallback, mCallback.getExecutor());
+ builder.build().start();
+
+ mCallback.expectCallback(ResponseStep.ON_SUCCEEDED);
+ UrlResponseInfo info = mCallback.mResponseInfo;
+ assertOKStatusCode(info);
+ assertEquals("http/1.1", info.getNegotiatedProtocol());
+ }
+
+ @Test
+ public void testHttpEngine_EnableQuic() throws Exception {
+ // The hint doesn't guarantee that QUIC will win the race, just that it will race TCP.
+ // If this ends up being flaky, consider sending multiple requests.
+ mEngine = mEngineBuilder.setEnableQuic(true).addQuicHint(HOST, 443, 443).build();
+ UrlRequest.Builder builder =
+ mEngine.newUrlRequestBuilder(URL, mCallback, mCallback.getExecutor());
+ builder.build().start();
+
+ mCallback.expectCallback(ResponseStep.ON_SUCCEEDED);
+ UrlResponseInfo info = mCallback.mResponseInfo;
+ assertOKStatusCode(info);
+ assertEquals("h3", info.getNegotiatedProtocol());
+ }
+
+ @Test
+ public void testHttpEngine_GetDefaultUserAgent() throws Exception {
+ assertThat(mEngineBuilder.getDefaultUserAgent(), containsString("AndroidHttpClient"));
+ }
+}
diff --git a/Cronet/tests/cts/src/android/net/http/cts/CronetUrlRequestTest.java b/Cronet/tests/cts/src/android/net/http/cts/UrlRequestTest.java
similarity index 61%
rename from Cronet/tests/cts/src/android/net/http/cts/CronetUrlRequestTest.java
rename to Cronet/tests/cts/src/android/net/http/cts/UrlRequestTest.java
index 598be0e..d7d3679 100644
--- a/Cronet/tests/cts/src/android/net/http/cts/CronetUrlRequestTest.java
+++ b/Cronet/tests/cts/src/android/net/http/cts/UrlRequestTest.java
@@ -16,22 +16,22 @@
package android.net.http.cts;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
+import static android.net.http.cts.util.TestUtilsKt.assertOKStatusCode;
+import static android.net.http.cts.util.TestUtilsKt.skipIfNoInternetConnection;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.greaterThan;
import android.content.Context;
-import android.net.ConnectivityManager;
import android.net.http.HttpEngine;
import android.net.http.UrlRequest;
import android.net.http.UrlRequest.Status;
import android.net.http.UrlResponseInfo;
-import android.net.http.cts.util.CronetCtsTestServer;
+import android.net.http.cts.util.HttpCtsTestServer;
import android.net.http.cts.util.TestStatusListener;
import android.net.http.cts.util.TestUrlRequestCallback;
import android.net.http.cts.util.TestUrlRequestCallback.ResponseStep;
-import androidx.annotation.NonNull;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
@@ -41,40 +41,29 @@
import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class)
-public class CronetUrlRequestTest {
- private static final String TAG = CronetUrlRequestTest.class.getSimpleName();
-
- @NonNull private HttpEngine mHttpEngine;
- @NonNull private TestUrlRequestCallback mCallback;
- @NonNull private ConnectivityManager mCm;
- @NonNull private CronetCtsTestServer mTestServer;
+public class UrlRequestTest {
+ private TestUrlRequestCallback mCallback;
+ private HttpCtsTestServer mTestServer;
+ private HttpEngine mHttpEngine;
@Before
public void setUp() throws Exception {
Context context = InstrumentationRegistry.getInstrumentation().getContext();
- mCm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ skipIfNoInternetConnection(context);
HttpEngine.Builder builder = new HttpEngine.Builder(context);
- builder.setEnableHttpCache(HttpEngine.Builder.HTTP_CACHE_IN_MEMORY, 100 * 1024)
- .setEnableHttp2(true)
- // .setEnableBrotli(true)
- .setEnableQuic(true);
mHttpEngine = builder.build();
mCallback = new TestUrlRequestCallback();
- mTestServer = new CronetCtsTestServer(context);
+ mTestServer = new HttpCtsTestServer(context);
}
@After
public void tearDown() throws Exception {
- mHttpEngine.shutdown();
- mTestServer.shutdown();
- }
-
- private static void assertGreaterThan(String msg, int first, int second) {
- assertTrue(msg + " Excepted " + first + " to be greater than " + second, first > second);
- }
-
- private void assertHasTestableNetworks() {
- assertNotNull("This test requires a working Internet connection", mCm.getActiveNetwork());
+ if (mHttpEngine != null) {
+ mHttpEngine.shutdown();
+ }
+ if (mTestServer != null) {
+ mTestServer.shutdown();
+ }
}
private UrlRequest buildUrlRequest(String url) {
@@ -83,18 +72,14 @@
@Test
public void testUrlRequestGet_CompletesSuccessfully() throws Exception {
- assertHasTestableNetworks();
String url = mTestServer.getSuccessUrl();
UrlRequest request = buildUrlRequest(url);
request.start();
mCallback.expectCallback(ResponseStep.ON_SUCCEEDED);
-
UrlResponseInfo info = mCallback.mResponseInfo;
- assertEquals(
- "Unexpected http status code from " + url + ".", 200, info.getHttpStatusCode());
- assertGreaterThan(
- "Received byte from " + url + " is 0.", (int) info.getReceivedByteCount(), 0);
+ assertOKStatusCode(info);
+ assertThat("Received byte count must be > 0", info.getReceivedByteCount(), greaterThan(0L));
}
@Test
diff --git a/Cronet/tests/cts/src/android/net/http/cts/util/CronetCtsTestServer.kt b/Cronet/tests/cts/src/android/net/http/cts/util/HttpCtsTestServer.kt
similarity index 82%
rename from Cronet/tests/cts/src/android/net/http/cts/util/CronetCtsTestServer.kt
rename to Cronet/tests/cts/src/android/net/http/cts/util/HttpCtsTestServer.kt
index 3ccb571..87d5108 100644
--- a/Cronet/tests/cts/src/android/net/http/cts/util/CronetCtsTestServer.kt
+++ b/Cronet/tests/cts/src/android/net/http/cts/util/HttpCtsTestServer.kt
@@ -19,8 +19,8 @@
import android.content.Context
import android.webkit.cts.CtsTestServer
-/** Extends CtsTestServer to handle POST requests and other cronet specific test requests */
-class CronetCtsTestServer(context: Context) : CtsTestServer(context) {
+/** Extends CtsTestServer to handle POST requests and other test specific requests */
+class HttpCtsTestServer(context: Context) : CtsTestServer(context) {
val successUrl: String = getAssetUrl("html/hello_world.html")
}
diff --git a/Cronet/tests/cts/src/android/net/http/cts/util/TestUploadDataProvider.java b/Cronet/tests/cts/src/android/net/http/cts/util/TestUploadDataProvider.java
new file mode 100644
index 0000000..d047828
--- /dev/null
+++ b/Cronet/tests/cts/src/android/net/http/cts/util/TestUploadDataProvider.java
@@ -0,0 +1,294 @@
+/*
+ * 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 android.net.http.cts.util;
+
+import android.net.http.UploadDataProvider;
+import android.net.http.UploadDataSink;
+import android.os.ConditionVariable;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.ClosedChannelException;
+import java.util.ArrayList;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/** An UploadDataProvider implementation used in tests. */
+public class TestUploadDataProvider extends UploadDataProvider {
+ // Indicates whether all success callbacks are synchronous or asynchronous.
+ // Doesn't apply to errors.
+ public enum SuccessCallbackMode {
+ SYNC,
+ ASYNC
+ }
+
+ // Indicates whether failures should throw exceptions, invoke callbacks
+ // synchronously, or invoke callback asynchronously.
+ public enum FailMode {
+ NONE,
+ THROWN,
+ CALLBACK_SYNC,
+ CALLBACK_ASYNC
+ }
+
+ private final ArrayList<byte[]> mReads = new ArrayList<byte[]>();
+ private final SuccessCallbackMode mSuccessCallbackMode;
+ private final Executor mExecutor;
+
+ private boolean mChunked;
+
+ // Index of read to fail on.
+ private int mReadFailIndex = -1;
+ // Indicates how to fail on a read.
+ private FailMode mReadFailMode = FailMode.NONE;
+
+ private FailMode mRewindFailMode = FailMode.NONE;
+
+ private FailMode mLengthFailMode = FailMode.NONE;
+
+ private int mNumReadCalls;
+ private int mNumRewindCalls;
+
+ private int mNextRead;
+ private boolean mStarted;
+ private boolean mReadPending;
+ private boolean mRewindPending;
+ // Used to ensure there are no read/rewind requests after a failure.
+ private boolean mFailed;
+
+ private final AtomicBoolean mClosed = new AtomicBoolean(false);
+ private final ConditionVariable mAwaitingClose = new ConditionVariable(false);
+
+ public TestUploadDataProvider(
+ SuccessCallbackMode successCallbackMode, final Executor executor) {
+ mSuccessCallbackMode = successCallbackMode;
+ mExecutor = executor;
+ }
+
+ // Adds the result to be returned by a successful read request. The
+ // returned bytes must all fit within the read buffer provided by Cronet.
+ // After a rewind, if there is one, all reads will be repeated.
+ public void addRead(byte[] read) {
+ if (mStarted) {
+ throw new IllegalStateException("Adding bytes after read");
+ }
+ mReads.add(read);
+ }
+
+ public void setReadFailure(int readFailIndex, FailMode readFailMode) {
+ mReadFailIndex = readFailIndex;
+ mReadFailMode = readFailMode;
+ }
+
+ public void setLengthFailure() {
+ mLengthFailMode = FailMode.THROWN;
+ }
+
+ public void setRewindFailure(FailMode rewindFailMode) {
+ mRewindFailMode = rewindFailMode;
+ }
+
+ public void setChunked(boolean chunked) {
+ mChunked = chunked;
+ }
+
+ public int getNumReadCalls() {
+ return mNumReadCalls;
+ }
+
+ public int getNumRewindCalls() {
+ return mNumRewindCalls;
+ }
+
+ /** Returns the cumulative length of all data added by calls to addRead. */
+ @Override
+ public long getLength() throws IOException {
+ if (mClosed.get()) {
+ throw new ClosedChannelException();
+ }
+ if (mLengthFailMode == FailMode.THROWN) {
+ throw new IllegalStateException("Sync length failure");
+ }
+ return getUploadedLength();
+ }
+
+ public long getUploadedLength() {
+ if (mChunked) {
+ return -1;
+ }
+ long length = 0;
+ for (byte[] read : mReads) {
+ length += read.length;
+ }
+ return length;
+ }
+
+ @Override
+ public void read(final UploadDataSink uploadDataSink, final ByteBuffer byteBuffer)
+ throws IOException {
+ int currentReadCall = mNumReadCalls;
+ ++mNumReadCalls;
+ if (mClosed.get()) {
+ throw new ClosedChannelException();
+ }
+ assertIdle();
+
+ if (maybeFailRead(currentReadCall, uploadDataSink)) {
+ mFailed = true;
+ return;
+ }
+
+ mReadPending = true;
+ mStarted = true;
+
+ final boolean finalChunk = (mChunked && mNextRead == mReads.size() - 1);
+ if (mNextRead < mReads.size()) {
+ if ((byteBuffer.limit() - byteBuffer.position()) < mReads.get(mNextRead).length) {
+ throw new IllegalStateException("Read buffer smaller than expected.");
+ }
+ byteBuffer.put(mReads.get(mNextRead));
+ ++mNextRead;
+ } else {
+ throw new IllegalStateException("Too many reads: " + mNextRead);
+ }
+
+ Runnable completeRunnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ mReadPending = false;
+ uploadDataSink.onReadSucceeded(finalChunk);
+ }
+ };
+ if (mSuccessCallbackMode == SuccessCallbackMode.SYNC) {
+ completeRunnable.run();
+ } else {
+ mExecutor.execute(completeRunnable);
+ }
+ }
+
+ @Override
+ public void rewind(final UploadDataSink uploadDataSink) throws IOException {
+ ++mNumRewindCalls;
+ if (mClosed.get()) {
+ throw new ClosedChannelException();
+ }
+ assertIdle();
+
+ if (maybeFailRewind(uploadDataSink)) {
+ mFailed = true;
+ return;
+ }
+
+ if (mNextRead == 0) {
+ // Should never try and rewind when rewinding does nothing.
+ throw new IllegalStateException("Unexpected rewind when already at beginning");
+ }
+
+ mRewindPending = true;
+ mNextRead = 0;
+
+ Runnable completeRunnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ mRewindPending = false;
+ uploadDataSink.onRewindSucceeded();
+ }
+ };
+ if (mSuccessCallbackMode == SuccessCallbackMode.SYNC) {
+ completeRunnable.run();
+ } else {
+ mExecutor.execute(completeRunnable);
+ }
+ }
+
+ private void assertIdle() {
+ if (mReadPending) {
+ throw new IllegalStateException("Unexpected operation during read");
+ }
+ if (mRewindPending) {
+ throw new IllegalStateException("Unexpected operation during rewind");
+ }
+ if (mFailed) {
+ throw new IllegalStateException("Unexpected operation after failure");
+ }
+ }
+
+ private boolean maybeFailRead(int readIndex, final UploadDataSink uploadDataSink) {
+ if (readIndex != mReadFailIndex) return false;
+
+ switch (mReadFailMode) {
+ case THROWN:
+ throw new IllegalStateException("Thrown read failure");
+ case CALLBACK_SYNC:
+ uploadDataSink.onReadError(new IllegalStateException("Sync read failure"));
+ return true;
+ case CALLBACK_ASYNC:
+ Runnable errorRunnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ uploadDataSink.onReadError(
+ new IllegalStateException("Async read failure"));
+ }
+ };
+ mExecutor.execute(errorRunnable);
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private boolean maybeFailRewind(final UploadDataSink uploadDataSink) {
+ switch (mRewindFailMode) {
+ case THROWN:
+ throw new IllegalStateException("Thrown rewind failure");
+ case CALLBACK_SYNC:
+ uploadDataSink.onRewindError(new IllegalStateException("Sync rewind failure"));
+ return true;
+ case CALLBACK_ASYNC:
+ Runnable errorRunnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ uploadDataSink.onRewindError(
+ new IllegalStateException("Async rewind failure"));
+ }
+ };
+ mExecutor.execute(errorRunnable);
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (!mClosed.compareAndSet(false, true)) {
+ throw new AssertionError("Closed twice");
+ }
+ mAwaitingClose.open();
+ }
+
+ public void assertClosed() {
+ mAwaitingClose.block(5000);
+ if (!mClosed.get()) {
+ throw new AssertionError("Was not closed");
+ }
+ }
+}
diff --git a/Cronet/tests/cts/src/android/net/http/cts/util/TestUtils.kt b/Cronet/tests/cts/src/android/net/http/cts/util/TestUtils.kt
new file mode 100644
index 0000000..d30c059
--- /dev/null
+++ b/Cronet/tests/cts/src/android/net/http/cts/util/TestUtils.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.http.cts.util
+
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.http.UrlResponseInfo
+import org.junit.Assert.assertEquals
+import org.junit.Assume.assumeNotNull
+
+fun skipIfNoInternetConnection(context: Context) {
+ val connectivityManager = context.getSystemService(ConnectivityManager::class.java)
+ assumeNotNull(
+ "This test requires a working Internet connection", connectivityManager.getActiveNetwork())
+}
+
+fun assertOKStatusCode(info: UrlResponseInfo) {
+ assertEquals("Status code must be 200 OK", 200, info.getHttpStatusCode())
+}
diff --git a/OWNERS_core_networking b/OWNERS_core_networking
index 172670e..6d17476 100644
--- a/OWNERS_core_networking
+++ b/OWNERS_core_networking
@@ -1,4 +1,3 @@
-chenbruce@google.com
chiachangwang@google.com
cken@google.com
huangaaron@google.com
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 700a085..a1e81c8 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -64,6 +64,9 @@
"name": "connectivity_native_test"
},
{
+ "name": "CtsNetHttpTestCases"
+ },
+ {
"name": "libclat_test"
},
{
diff --git a/Tethering/AndroidManifest.xml b/Tethering/AndroidManifest.xml
index b832e16..23467e7 100644
--- a/Tethering/AndroidManifest.xml
+++ b/Tethering/AndroidManifest.xml
@@ -43,7 +43,9 @@
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
+ <!-- Sending non-protected broadcast from system uid is not allowed. -->
<protected-broadcast android:name="com.android.server.connectivity.tethering.DISABLE_TETHERING" />
+ <protected-broadcast android:name="com.android.server.connectivity.KeepaliveTracker.TCP_POLLING_ALARM" />
<application
android:process="com.android.networkstack.process"
diff --git a/Tethering/apex/Android.bp b/Tethering/apex/Android.bp
index b26911c..31fd645 100644
--- a/Tethering/apex/Android.bp
+++ b/Tethering/apex/Android.bp
@@ -36,16 +36,16 @@
// different value depending on the branch.
java_defaults {
name: "ConnectivityNextEnableDefaults",
- enabled: true,
+ enabled: false,
}
apex_defaults {
name: "ConnectivityApexDefaults",
// Tethering app to include in the AOSP apex. Branches that disable the "next" targets may use
// a stable tethering app instead, but will generally override the AOSP apex to use updatable
// package names and keys, so that apex will be unused anyway.
- apps: ["TetheringNext"], // Replace to "Tethering" if ConnectivityNextEnableDefaults is false.
+ apps: ["Tethering"], // Replace to "Tethering" if ConnectivityNextEnableDefaults is false.
}
-enable_tethering_next_apex = true
+enable_tethering_next_apex = false
// This is a placeholder comment to avoid merge conflicts
// as the above target may have different "enabled" values
// depending on the branch
@@ -54,7 +54,7 @@
// enables or disables inclusion of Cronet in the Tethering apex. This is used to disable Cronet
// on tm-mainline-prod. Note: in order for Cronet APIs to work Cronet must also be enabled
// by the cronet_java_*_defaults in common/TetheringLib/Android.bp.
-cronet_in_tethering_apex_defaults = "CronetInTetheringApexDefaultsEnabled"
+cronet_in_tethering_apex_defaults = "CronetInTetheringApexDefaultsDisabled"
// This is a placeholder comment to avoid merge conflicts
// as cronet_apex_defaults may have different values
// depending on the branch
diff --git a/Tethering/common/TetheringLib/Android.bp b/Tethering/common/TetheringLib/Android.bp
index cbdf0c0..7e9e868 100644
--- a/Tethering/common/TetheringLib/Android.bp
+++ b/Tethering/common/TetheringLib/Android.bp
@@ -21,16 +21,8 @@
// specify a java_defaults target that either enables or disables Cronet. This
// is used to disable Cronet on tm-mainline-prod.
// Note: they must either both be enabled or disabled.
-cronet_java_defaults = "CronetJavaDefaultsEnabled"
-cronet_java_prejarjar_defaults = "CronetJavaPrejarjarDefaultsEnabled"
-// This is a placeholder comment to avoid merge conflicts
-// as cronet_defaults may have different values
-// depending on the branch
-
-// cronet_java_defaults_enabled_srcs is used to specify the srcs of CronetJavaDefaultsEnabled
-// This is required until the external/cronet is auto-merged to tm-mainline-prod and
-// :cronet_aml_api_sources is available
-cronet_java_defaults_enabled_srcs = [":cronet_aml_api_sources"]
+cronet_java_defaults = "CronetJavaDefaultsDisabled"
+cronet_java_prejarjar_defaults = "CronetJavaPrejarjarDefaultsDisabled"
// This is a placeholder comment to avoid merge conflicts
// as cronet_defaults may have different values
// depending on the branch
@@ -82,7 +74,7 @@
java_defaults {
name: "CronetJavaDefaultsEnabled",
- srcs: cronet_java_defaults_enabled_srcs,
+ srcs: [":cronet_aml_api_sources"],
libs: [
"androidx.annotation_annotation",
],
diff --git a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
index 63702f2..f90b3a4 100644
--- a/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
+++ b/Tethering/tests/unit/src/com/android/networkstack/tethering/TetheringTest.java
@@ -1802,7 +1802,7 @@
TestNetworkAgent mobile, TestNetworkAgent wifi, TestNetworkAgent dun) throws Exception {
final NetworkCallback dunNetworkCallback = setupDunUpstreamTest(automatic, inOrder);
- // Pretend cellular connected and expect the upstream to be set.
+ // Pretend cellular connected and expect the upstream to be not set.
mobile.fakeConnect();
mCm.makeDefaultNetwork(mobile, BROADCAST_FIRST);
mLooper.dispatchAll();
diff --git a/bpf_progs/bpf_net_helpers.h b/bpf_progs/bpf_net_helpers.h
index c39269e..b7ca3af 100644
--- a/bpf_progs/bpf_net_helpers.h
+++ b/bpf_progs/bpf_net_helpers.h
@@ -21,6 +21,18 @@
#include <stdbool.h>
#include <stdint.h>
+// bionic kernel uapi linux/udp.h header is munged...
+#define __kernel_udphdr udphdr
+#include <linux/udp.h>
+
+// Offsets from beginning of L4 (TCP/UDP) header
+#define TCP_OFFSET(field) offsetof(struct tcphdr, field)
+#define UDP_OFFSET(field) offsetof(struct udphdr, field)
+
+// Offsets from beginning of L3 (IPv4/IPv6) header
+#define IP4_OFFSET(field) offsetof(struct iphdr, field)
+#define IP6_OFFSET(field) offsetof(struct ipv6hdr, field)
+
// this returns 0 iff skb->sk is NULL
static uint64_t (*bpf_get_socket_cookie)(struct __sk_buff* skb) = (void*)BPF_FUNC_get_socket_cookie;
diff --git a/bpf_progs/netd.c b/bpf_progs/netd.c
index 43920d0..84da79d 100644
--- a/bpf_progs/netd.c
+++ b/bpf_progs/netd.c
@@ -46,8 +46,9 @@
static const bool INGRESS = false;
static const bool EGRESS = true;
-#define IP_PROTO_OFF offsetof(struct iphdr, protocol)
-#define IPV6_PROTO_OFF offsetof(struct ipv6hdr, nexthdr)
+// Used for 'bool enable_tracing'
+static const bool TRACE_ON = true;
+static const bool TRACE_OFF = false;
// offsetof(struct iphdr, ihl) -- but that's a bitfield
#define IPPROTO_IHL_OFF 0
@@ -60,14 +61,18 @@
#define TCP_FLAG32_OFF 12
// For maps netd does not need to access
-#define DEFINE_BPF_MAP_NO_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
- DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, \
- AID_ROOT, AID_NET_BW_ACCT, 0060, "fs_bpf_net_shared", "", false)
+#define DEFINE_BPF_MAP_NO_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
+ DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, \
+ AID_ROOT, AID_NET_BW_ACCT, 0060, "fs_bpf_net_shared", "", false, \
+ BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, /*ignore_on_eng*/false, \
+ /*ignore_on_user*/false, /*ignore_on_userdebug*/false)
// For maps netd only needs read only access to
-#define DEFINE_BPF_MAP_RO_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
- DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, \
- AID_ROOT, AID_NET_BW_ACCT, 0460, "fs_bpf_netd_readonly", "", false)
+#define DEFINE_BPF_MAP_RO_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
+ DEFINE_BPF_MAP_EXT(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries, \
+ AID_ROOT, AID_NET_BW_ACCT, 0460, "fs_bpf_netd_readonly", "", false, \
+ BPFLOADER_MIN_VER, BPFLOADER_MAX_VER, /*ignore_on_eng*/false, \
+ /*ignore_on_user*/false, /*ignore_on_userdebug*/false)
// For maps netd needs to be able to read and write
#define DEFINE_BPF_MAP_RW_NETD(the_map, TYPE, TypeOfKey, TypeOfValue, num_entries) \
@@ -95,6 +100,19 @@
/* never actually used from ebpf */
DEFINE_BPF_MAP_NO_NETD(iface_index_name_map, HASH, uint32_t, IfaceValue, IFACE_INDEX_NAME_MAP_SIZE)
+// A single-element configuration array, packet tracing is enabled when 'true'.
+DEFINE_BPF_MAP_EXT(packet_trace_enabled_map, ARRAY, uint32_t, bool, 1,
+ AID_ROOT, AID_SYSTEM, 0060, "fs_bpf_net_shared", "", false,
+ BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, /*ignore_on_eng*/false,
+ /*ignore_on_user*/true, /*ignore_on_userdebug*/false)
+
+// A ring buffer on which packet information is pushed. This map will only be loaded
+// on eng and userdebug devices. User devices won't load this to save memory.
+DEFINE_BPF_RINGBUF_EXT(packet_trace_ringbuf, PacketTrace, PACKET_TRACE_BUF_SIZE,
+ AID_ROOT, AID_SYSTEM, 0060, "fs_bpf_net_shared", "", false,
+ BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, /*ignore_on_eng*/false,
+ /*ignore_on_user*/true, /*ignore_on_userdebug*/false);
+
// iptables xt_bpf programs need to be usable by both netd and netutils_wrappers
// selinux contexts, because even non-xt_bpf iptables mutations are implemented as
// a full table dump, followed by an update in userspace, and then a reload into the kernel,
@@ -222,12 +240,72 @@
: bpf_skb_load_bytes(skb, L3_off, to, len);
}
+static __always_inline inline void do_packet_tracing(
+ const struct __sk_buff* const skb, const bool egress, const uint32_t uid,
+ const uint32_t tag, const bool enable_tracing, const unsigned kver) {
+ if (!enable_tracing) return;
+ if (kver < KVER(5, 8, 0)) return;
+
+ uint32_t mapKey = 0;
+ bool* traceConfig = bpf_packet_trace_enabled_map_lookup_elem(&mapKey);
+ if (traceConfig == NULL) return;
+ if (*traceConfig == false) return;
+
+ PacketTrace* pkt = bpf_packet_trace_ringbuf_reserve();
+ if (pkt == NULL) return;
+
+ // Errors from bpf_skb_load_bytes_net are ignored to favor returning something
+ // over returning nothing. In the event of an error, the kernel will fill in
+ // zero for the destination memory. Do not change the default '= 0' below.
+
+ uint8_t proto = 0;
+ uint8_t L4_off = 0;
+ uint8_t ipVersion = 0;
+ if (skb->protocol == htons(ETH_P_IP)) {
+ (void)bpf_skb_load_bytes_net(skb, IP4_OFFSET(protocol), &proto, sizeof(proto), kver);
+ (void)bpf_skb_load_bytes_net(skb, IPPROTO_IHL_OFF, &L4_off, sizeof(L4_off), kver);
+ L4_off = (L4_off & 0x0F) * 4; // IHL calculation.
+ ipVersion = 4;
+ } else if (skb->protocol == htons(ETH_P_IPV6)) {
+ (void)bpf_skb_load_bytes_net(skb, IP6_OFFSET(nexthdr), &proto, sizeof(proto), kver);
+ L4_off = sizeof(struct ipv6hdr);
+ ipVersion = 6;
+ }
+
+ uint8_t flags = 0;
+ __be16 sport = 0, dport = 0;
+ if (proto == IPPROTO_TCP && L4_off >= 20) {
+ (void)bpf_skb_load_bytes_net(skb, L4_off + TCP_FLAG32_OFF + 1, &flags, sizeof(flags), kver);
+ (void)bpf_skb_load_bytes_net(skb, L4_off + TCP_OFFSET(source), &sport, sizeof(sport), kver);
+ (void)bpf_skb_load_bytes_net(skb, L4_off + TCP_OFFSET(dest), &dport, sizeof(dport), kver);
+ } else if (proto == IPPROTO_UDP && L4_off >= 20) {
+ (void)bpf_skb_load_bytes_net(skb, L4_off + UDP_OFFSET(source), &sport, sizeof(sport), kver);
+ (void)bpf_skb_load_bytes_net(skb, L4_off + UDP_OFFSET(dest), &dport, sizeof(dport), kver);
+ }
+
+ pkt->timestampNs = bpf_ktime_get_boot_ns();
+ pkt->ifindex = skb->ifindex;
+ pkt->length = skb->len;
+
+ pkt->uid = uid;
+ pkt->tag = tag;
+ pkt->sport = sport;
+ pkt->dport = dport;
+
+ pkt->egress = egress;
+ pkt->ipProto = proto;
+ pkt->tcpFlags = flags;
+ pkt->ipVersion = ipVersion;
+
+ bpf_packet_trace_ringbuf_submit(pkt);
+}
+
static __always_inline inline bool skip_owner_match(struct __sk_buff* skb, const unsigned kver) {
uint32_t flag = 0;
if (skb->protocol == htons(ETH_P_IP)) {
uint8_t proto;
// no need to check for success, proto will be zeroed if bpf_skb_load_bytes_net() fails
- (void)bpf_skb_load_bytes_net(skb, IP_PROTO_OFF, &proto, sizeof(proto), kver);
+ (void)bpf_skb_load_bytes_net(skb, IP4_OFFSET(protocol), &proto, sizeof(proto), kver);
if (proto == IPPROTO_ESP) return true;
if (proto != IPPROTO_TCP) return false; // handles read failure above
uint8_t ihl;
@@ -243,7 +321,7 @@
} else if (skb->protocol == htons(ETH_P_IPV6)) {
uint8_t proto;
// no need to check for success, proto will be zeroed if bpf_skb_load_bytes_net() fails
- (void)bpf_skb_load_bytes_net(skb, IPV6_PROTO_OFF, &proto, sizeof(proto), kver);
+ (void)bpf_skb_load_bytes_net(skb, IP6_OFFSET(nexthdr), &proto, sizeof(proto), kver);
if (proto == IPPROTO_ESP) return true;
if (proto != IPPROTO_TCP) return false; // handles read failure above
// if the read below fails, we'll just assume no TCP flags are set, which is fine.
@@ -315,6 +393,7 @@
}
static __always_inline inline int bpf_traffic_account(struct __sk_buff* skb, bool egress,
+ const bool enable_tracing,
const unsigned kver) {
uint32_t sock_uid = bpf_get_socket_uid(skb);
uint64_t cookie = bpf_get_socket_cookie(skb);
@@ -374,34 +453,51 @@
key.tag = 0;
}
+ do_packet_tracing(skb, egress, uid, tag, enable_tracing, kver);
update_stats_with_config(skb, egress, &key, *selectedMap);
update_app_uid_stats_map(skb, egress, &uid);
asm("%0 &= 1" : "+r"(match));
return match;
}
+DEFINE_BPF_PROG_EXT("cgroupskb/ingress/stats$trace", AID_ROOT, AID_SYSTEM,
+ bpf_cgroup_ingress_trace, KVER(5, 8, 0), KVER_INF,
+ BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, false,
+ "fs_bpf_netd_readonly", "", false, true, false)
+(struct __sk_buff* skb) {
+ return bpf_traffic_account(skb, INGRESS, TRACE_ON, KVER(5, 8, 0));
+}
+
DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/ingress/stats$4_19", AID_ROOT, AID_SYSTEM,
bpf_cgroup_ingress_4_19, KVER(4, 19, 0), KVER_INF)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, INGRESS, KVER(4, 19, 0));
+ return bpf_traffic_account(skb, INGRESS, TRACE_OFF, KVER(4, 19, 0));
}
DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/ingress/stats$4_14", AID_ROOT, AID_SYSTEM,
bpf_cgroup_ingress_4_14, KVER_NONE, KVER(4, 19, 0))
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, INGRESS, KVER_NONE);
+ return bpf_traffic_account(skb, INGRESS, TRACE_OFF, KVER_NONE);
+}
+
+DEFINE_BPF_PROG_EXT("cgroupskb/egress/stats$trace", AID_ROOT, AID_SYSTEM,
+ bpf_cgroup_egress_trace, KVER(5, 8, 0), KVER_INF,
+ BPFLOADER_IGNORED_ON_VERSION, BPFLOADER_MAX_VER, false,
+ "fs_bpf_netd_readonly", "", false, true, false)
+(struct __sk_buff* skb) {
+ return bpf_traffic_account(skb, EGRESS, TRACE_ON, KVER(5, 8, 0));
}
DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/egress/stats$4_19", AID_ROOT, AID_SYSTEM,
bpf_cgroup_egress_4_19, KVER(4, 19, 0), KVER_INF)
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, EGRESS, KVER(4, 19, 0));
+ return bpf_traffic_account(skb, EGRESS, TRACE_OFF, KVER(4, 19, 0));
}
DEFINE_NETD_BPF_PROG_KVER_RANGE("cgroupskb/egress/stats$4_14", AID_ROOT, AID_SYSTEM,
bpf_cgroup_egress_4_14, KVER_NONE, KVER(4, 19, 0))
(struct __sk_buff* skb) {
- return bpf_traffic_account(skb, EGRESS, KVER_NONE);
+ return bpf_traffic_account(skb, EGRESS, TRACE_OFF, KVER_NONE);
}
// WARNING: Android T's non-updatable netd depends on the name of this program.
diff --git a/bpf_progs/netd.h b/bpf_progs/netd.h
index cc88680..be604f9 100644
--- a/bpf_progs/netd.h
+++ b/bpf_progs/netd.h
@@ -69,6 +69,24 @@
uint64_t tcpTxPackets;
} Stats;
+typedef struct {
+ uint64_t timestampNs;
+ uint32_t ifindex;
+ uint32_t length;
+
+ uint32_t uid;
+ uint32_t tag;
+
+ __be16 sport;
+ __be16 dport;
+
+ bool egress;
+ uint8_t ipProto;
+ uint8_t tcpFlags;
+ uint8_t ipVersion; // 4=IPv4, 6=IPv6, 0=unknown
+} PacketTrace;
+STRUCT_SIZE(PacketTrace, 8+4+4 + 4+4 + 2+2 + 1+1+1+1);
+
// Since we cannot garbage collect the stats map since device boot, we need to make these maps as
// large as possible. The maximum size of number of map entries we can have is depend on the rlimit
// of MEM_LOCK granted to netd. The memory space needed by each map can be calculated by the
@@ -87,7 +105,8 @@
// dozable_uid_map: key: 4 bytes, value: 1 bytes, cost: 145216 bytes = 145Kbytes
// standby_uid_map: key: 4 bytes, value: 1 bytes, cost: 145216 bytes = 145Kbytes
// powersave_uid_map: key: 4 bytes, value: 1 bytes, cost: 145216 bytes = 145Kbytes
-// total: 4930Kbytes
+// packet_trace_ringbuf:key: 0 bytes, value: 24 bytes, cost: 32768 bytes = 32Kbytes
+// total: 4962Kbytes
// It takes maximum 4.9MB kernel memory space if all maps are full, which requires any devices
// running this module to have a memlock rlimit to be larger then 5MB. In the old qtaguid module,
// we don't have a total limit for data entries but only have limitation of tags each uid can have.
@@ -102,6 +121,7 @@
static const int IFACE_STATS_MAP_SIZE = 1000;
static const int CONFIGURATION_MAP_SIZE = 2;
static const int UID_OWNER_MAP_SIZE = 4000;
+static const int PACKET_TRACE_BUF_SIZE = 32 * 1024;
#ifdef __cplusplus
@@ -145,6 +165,8 @@
#define CONFIGURATION_MAP_PATH BPF_NETD_PATH "map_netd_configuration_map"
#define UID_OWNER_MAP_PATH BPF_NETD_PATH "map_netd_uid_owner_map"
#define UID_PERMISSION_MAP_PATH BPF_NETD_PATH "map_netd_uid_permission_map"
+#define PACKET_TRACE_RINGBUF_PATH BPF_NETD_PATH "map_netd_packet_trace_ringbuf"
+#define PACKET_TRACE_ENABLED_MAP_PATH BPF_NETD_PATH "map_netd_packet_trace_enabled_map"
#endif // __cplusplus
diff --git a/framework-t/api/current.txt b/framework-t/api/current.txt
index eb77288..5532853 100644
--- a/framework-t/api/current.txt
+++ b/framework-t/api/current.txt
@@ -192,15 +192,20 @@
method @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE) public void discoverServices(@NonNull String, int, @NonNull android.net.NetworkRequest, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.DiscoveryListener);
method public void registerService(android.net.nsd.NsdServiceInfo, int, android.net.nsd.NsdManager.RegistrationListener);
method public void registerService(@NonNull android.net.nsd.NsdServiceInfo, int, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.RegistrationListener);
- method public void resolveService(android.net.nsd.NsdServiceInfo, android.net.nsd.NsdManager.ResolveListener);
- method public void resolveService(@NonNull android.net.nsd.NsdServiceInfo, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.ResolveListener);
+ method public void registerServiceInfoCallback(@NonNull android.net.nsd.NsdServiceInfo, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.ServiceInfoCallback);
+ method @Deprecated public void resolveService(android.net.nsd.NsdServiceInfo, android.net.nsd.NsdManager.ResolveListener);
+ method @Deprecated public void resolveService(@NonNull android.net.nsd.NsdServiceInfo, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.ResolveListener);
method public void stopServiceDiscovery(android.net.nsd.NsdManager.DiscoveryListener);
+ method public void stopServiceResolution(@NonNull android.net.nsd.NsdManager.ResolveListener);
method public void unregisterService(android.net.nsd.NsdManager.RegistrationListener);
+ method public void unregisterServiceInfoCallback(@NonNull android.net.nsd.NsdManager.ServiceInfoCallback);
field public static final String ACTION_NSD_STATE_CHANGED = "android.net.nsd.STATE_CHANGED";
field public static final String EXTRA_NSD_STATE = "nsd_state";
field public static final int FAILURE_ALREADY_ACTIVE = 3; // 0x3
+ field public static final int FAILURE_BAD_PARAMETERS = 6; // 0x6
field public static final int FAILURE_INTERNAL_ERROR = 0; // 0x0
field public static final int FAILURE_MAX_LIMIT = 4; // 0x4
+ field public static final int FAILURE_OPERATION_NOT_RUNNING = 5; // 0x5
field public static final int NSD_STATE_DISABLED = 1; // 0x1
field public static final int NSD_STATE_ENABLED = 2; // 0x2
field public static final int PROTOCOL_DNS_SD = 1; // 0x1
@@ -224,21 +229,32 @@
public static interface NsdManager.ResolveListener {
method public void onResolveFailed(android.net.nsd.NsdServiceInfo, int);
+ method public default void onResolveStopped(@NonNull android.net.nsd.NsdServiceInfo);
method public void onServiceResolved(android.net.nsd.NsdServiceInfo);
+ method public default void onStopResolutionFailed(@NonNull android.net.nsd.NsdServiceInfo, int);
+ }
+
+ public static interface NsdManager.ServiceInfoCallback {
+ method public void onServiceInfoCallbackRegistrationFailed(int);
+ method public void onServiceInfoCallbackUnregistered();
+ method public void onServiceLost();
+ method public void onServiceUpdated(@NonNull android.net.nsd.NsdServiceInfo);
}
public final class NsdServiceInfo implements android.os.Parcelable {
ctor public NsdServiceInfo();
method public int describeContents();
method public java.util.Map<java.lang.String,byte[]> getAttributes();
- method public java.net.InetAddress getHost();
+ method @Deprecated public java.net.InetAddress getHost();
+ method @NonNull public java.util.List<java.net.InetAddress> getHostAddresses();
method @Nullable public android.net.Network getNetwork();
method public int getPort();
method public String getServiceName();
method public String getServiceType();
method public void removeAttribute(String);
method public void setAttribute(String, String);
- method public void setHost(java.net.InetAddress);
+ method @Deprecated public void setHost(java.net.InetAddress);
+ method public void setHostAddresses(@NonNull java.util.List<java.net.InetAddress>);
method public void setNetwork(@Nullable android.net.Network);
method public void setPort(int);
method public void setServiceName(String);
diff --git a/framework-t/src/android/net/nsd/INsdManagerCallback.aidl b/framework-t/src/android/net/nsd/INsdManagerCallback.aidl
index 1a262ec..d89bfa9 100644
--- a/framework-t/src/android/net/nsd/INsdManagerCallback.aidl
+++ b/framework-t/src/android/net/nsd/INsdManagerCallback.aidl
@@ -36,4 +36,10 @@
void onUnregisterServiceSucceeded(int listenerKey);
void onResolveServiceFailed(int listenerKey, int error);
void onResolveServiceSucceeded(int listenerKey, in NsdServiceInfo info);
+ void onStopResolutionFailed(int listenerKey, int error);
+ void onStopResolutionSucceeded(int listenerKey);
+ void onServiceInfoCallbackRegistrationFailed(int listenerKey, int error);
+ void onServiceUpdated(int listenerKey, in NsdServiceInfo info);
+ void onServiceUpdatedLost(int listenerKey);
+ void onServiceInfoCallbackUnregistered(int listenerKey);
}
diff --git a/framework-t/src/android/net/nsd/INsdServiceConnector.aidl b/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
index b06ae55..5533154 100644
--- a/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
+++ b/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
@@ -32,4 +32,7 @@
void stopDiscovery(int listenerKey);
void resolveService(int listenerKey, in NsdServiceInfo serviceInfo);
void startDaemon();
+ void stopResolution(int listenerKey);
+ void registerServiceInfoCallback(int listenerKey, in NsdServiceInfo serviceInfo);
+ void unregisterServiceInfoCallback(int listenerKey);
}
\ No newline at end of file
diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java
index 45def36..122e3a0 100644
--- a/framework-t/src/android/net/nsd/NsdManager.java
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -16,6 +16,7 @@
package android.net.nsd;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
@@ -44,6 +45,8 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.Objects;
import java.util.concurrent.Executor;
@@ -230,7 +233,6 @@
/** @hide */
public static final int DAEMON_CLEANUP = 18;
-
/** @hide */
public static final int DAEMON_STARTUP = 19;
@@ -245,6 +247,27 @@
/** @hide */
public static final int MDNS_DISCOVERY_MANAGER_EVENT = 23;
+ /** @hide */
+ public static final int STOP_RESOLUTION = 24;
+ /** @hide */
+ public static final int STOP_RESOLUTION_FAILED = 25;
+ /** @hide */
+ public static final int STOP_RESOLUTION_SUCCEEDED = 26;
+
+ /** @hide */
+ public static final int REGISTER_SERVICE_CALLBACK = 27;
+ /** @hide */
+ public static final int REGISTER_SERVICE_CALLBACK_FAILED = 28;
+ /** @hide */
+ public static final int SERVICE_UPDATED = 29;
+ /** @hide */
+ public static final int SERVICE_UPDATED_LOST = 30;
+
+ /** @hide */
+ public static final int UNREGISTER_SERVICE_CALLBACK = 31;
+ /** @hide */
+ public static final int UNREGISTER_SERVICE_CALLBACK_SUCCEEDED = 32;
+
/** Dns based service discovery protocol */
public static final int PROTOCOL_DNS_SD = 0x0001;
@@ -270,6 +293,15 @@
EVENT_NAMES.put(DAEMON_CLEANUP, "DAEMON_CLEANUP");
EVENT_NAMES.put(DAEMON_STARTUP, "DAEMON_STARTUP");
EVENT_NAMES.put(MDNS_SERVICE_EVENT, "MDNS_SERVICE_EVENT");
+ EVENT_NAMES.put(STOP_RESOLUTION, "STOP_RESOLUTION");
+ EVENT_NAMES.put(STOP_RESOLUTION_FAILED, "STOP_RESOLUTION_FAILED");
+ EVENT_NAMES.put(STOP_RESOLUTION_SUCCEEDED, "STOP_RESOLUTION_SUCCEEDED");
+ EVENT_NAMES.put(REGISTER_SERVICE_CALLBACK, "REGISTER_SERVICE_CALLBACK");
+ EVENT_NAMES.put(REGISTER_SERVICE_CALLBACK_FAILED, "REGISTER_SERVICE_CALLBACK_FAILED");
+ EVENT_NAMES.put(SERVICE_UPDATED, "SERVICE_UPDATED");
+ EVENT_NAMES.put(UNREGISTER_SERVICE_CALLBACK, "UNREGISTER_SERVICE_CALLBACK");
+ EVENT_NAMES.put(UNREGISTER_SERVICE_CALLBACK_SUCCEEDED,
+ "UNREGISTER_SERVICE_CALLBACK_SUCCEEDED");
}
/** @hide */
@@ -595,6 +627,36 @@
public void onResolveServiceSucceeded(int listenerKey, NsdServiceInfo info) {
sendInfo(RESOLVE_SERVICE_SUCCEEDED, listenerKey, info);
}
+
+ @Override
+ public void onStopResolutionFailed(int listenerKey, int error) {
+ sendError(STOP_RESOLUTION_FAILED, listenerKey, error);
+ }
+
+ @Override
+ public void onStopResolutionSucceeded(int listenerKey) {
+ sendNoArg(STOP_RESOLUTION_SUCCEEDED, listenerKey);
+ }
+
+ @Override
+ public void onServiceInfoCallbackRegistrationFailed(int listenerKey, int error) {
+ sendError(REGISTER_SERVICE_CALLBACK_FAILED, listenerKey, error);
+ }
+
+ @Override
+ public void onServiceUpdated(int listenerKey, NsdServiceInfo info) {
+ sendInfo(SERVICE_UPDATED, listenerKey, info);
+ }
+
+ @Override
+ public void onServiceUpdatedLost(int listenerKey) {
+ sendNoArg(SERVICE_UPDATED_LOST, listenerKey);
+ }
+
+ @Override
+ public void onServiceInfoCallbackUnregistered(int listenerKey) {
+ sendNoArg(UNREGISTER_SERVICE_CALLBACK_SUCCEEDED, listenerKey);
+ }
}
/**
@@ -618,6 +680,37 @@
*/
public static final int FAILURE_MAX_LIMIT = 4;
+ /**
+ * Indicates that the stop operation failed because it is not running.
+ * This failure is passed with {@link ResolveListener#onStopResolutionFailed}.
+ */
+ public static final int FAILURE_OPERATION_NOT_RUNNING = 5;
+
+ /**
+ * Indicates that the service has failed to resolve because of bad parameters.
+ *
+ * This failure is passed with
+ * {@link ServiceInfoCallback#onServiceInfoCallbackRegistrationFailed}.
+ */
+ public static final int FAILURE_BAD_PARAMETERS = 6;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ FAILURE_OPERATION_NOT_RUNNING,
+ })
+ public @interface StopOperationFailureCode {
+ }
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ FAILURE_ALREADY_ACTIVE,
+ FAILURE_BAD_PARAMETERS,
+ })
+ public @interface ResolutionFailureCode {
+ }
+
/** Interface for callback invocation for service discovery */
public interface DiscoveryListener {
@@ -646,12 +739,97 @@
public void onServiceUnregistered(NsdServiceInfo serviceInfo);
}
- /** Interface for callback invocation for service resolution */
+ /**
+ * Callback for use with {@link NsdManager#resolveService} to resolve the service info and use
+ * with {@link NsdManager#stopServiceResolution} to stop resolution.
+ */
public interface ResolveListener {
- public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode);
+ /**
+ * Called on the internal thread or with an executor passed to
+ * {@link NsdManager#resolveService} to report the resolution was failed with an error.
+ *
+ * A resolution operation would call either onServiceResolved or onResolveFailed once based
+ * on the result.
+ */
+ void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode);
- public void onServiceResolved(NsdServiceInfo serviceInfo);
+ /**
+ * Called on the internal thread or with an executor passed to
+ * {@link NsdManager#resolveService} to report the resolved service info.
+ *
+ * A resolution operation would call either onServiceResolved or onResolveFailed once based
+ * on the result.
+ */
+ void onServiceResolved(NsdServiceInfo serviceInfo);
+
+ /**
+ * Called on the internal thread or with an executor passed to
+ * {@link NsdManager#resolveService} to report the resolution was stopped.
+ *
+ * A stop resolution operation would call either onResolveStopped or onStopResolutionFailed
+ * once based on the result.
+ */
+ default void onResolveStopped(@NonNull NsdServiceInfo serviceInfo) { }
+
+ /**
+ * Called once on the internal thread or with an executor passed to
+ * {@link NsdManager#resolveService} to report that stopping resolution failed with an
+ * error.
+ *
+ * A stop resolution operation would call either onResolveStopped or onStopResolutionFailed
+ * once based on the result.
+ */
+ default void onStopResolutionFailed(@NonNull NsdServiceInfo serviceInfo,
+ @StopOperationFailureCode int errorCode) { }
+ }
+
+ /**
+ * Callback to listen to service info updates.
+ *
+ * For use with {@link NsdManager#registerServiceInfoCallback} to register, and with
+ * {@link NsdManager#unregisterServiceInfoCallback} to stop listening.
+ */
+ public interface ServiceInfoCallback {
+
+ /**
+ * Reports that registering the callback failed with an error.
+ *
+ * Called on the executor passed to {@link NsdManager#registerServiceInfoCallback}.
+ *
+ * onServiceInfoCallbackRegistrationFailed will be called exactly once when the callback
+ * could not be registered. No other callback will be sent in that case.
+ */
+ void onServiceInfoCallbackRegistrationFailed(@ResolutionFailureCode int errorCode);
+
+ /**
+ * Reports updated service info.
+ *
+ * Called on the executor passed to {@link NsdManager#registerServiceInfoCallback}. Any
+ * service updates will be notified via this callback until
+ * {@link NsdManager#unregisterServiceInfoCallback} is called. This will only be called once
+ * the service is found, so may never be called if the service is never present.
+ */
+ void onServiceUpdated(@NonNull NsdServiceInfo serviceInfo);
+
+ /**
+ * Reports when the service that this callback listens to becomes unavailable.
+ *
+ * Called on the executor passed to {@link NsdManager#registerServiceInfoCallback}. The
+ * service may become available again, in which case {@link #onServiceUpdated} will be
+ * called.
+ */
+ void onServiceLost();
+
+ /**
+ * Reports that service info updates have stopped.
+ *
+ * Called on the executor passed to {@link NsdManager#registerServiceInfoCallback}.
+ *
+ * A callback unregistration operation will call onServiceInfoCallbackUnregistered
+ * once. After this, the callback may be reused.
+ */
+ void onServiceInfoCallbackUnregistered();
}
@VisibleForTesting
@@ -744,6 +922,33 @@
executor.execute(() -> ((ResolveListener) listener).onServiceResolved(
(NsdServiceInfo) obj));
break;
+ case STOP_RESOLUTION_FAILED:
+ removeListener(key);
+ executor.execute(() -> ((ResolveListener) listener).onStopResolutionFailed(
+ ns, errorCode));
+ break;
+ case STOP_RESOLUTION_SUCCEEDED:
+ removeListener(key);
+ executor.execute(() -> ((ResolveListener) listener).onResolveStopped(
+ ns));
+ break;
+ case REGISTER_SERVICE_CALLBACK_FAILED:
+ removeListener(key);
+ executor.execute(() -> ((ServiceInfoCallback) listener)
+ .onServiceInfoCallbackRegistrationFailed(errorCode));
+ break;
+ case SERVICE_UPDATED:
+ executor.execute(() -> ((ServiceInfoCallback) listener)
+ .onServiceUpdated((NsdServiceInfo) obj));
+ break;
+ case SERVICE_UPDATED_LOST:
+ executor.execute(() -> ((ServiceInfoCallback) listener).onServiceLost());
+ break;
+ case UNREGISTER_SERVICE_CALLBACK_SUCCEEDED:
+ removeListener(key);
+ executor.execute(() -> ((ServiceInfoCallback) listener)
+ .onServiceInfoCallbackUnregistered());
+ break;
default:
Log.d(TAG, "Ignored " + message);
break;
@@ -1055,7 +1260,14 @@
* @param serviceInfo service to be resolved
* @param listener to receive callback upon success or failure. Cannot be null.
* Cannot be in use for an active service resolution.
+ *
+ * @deprecated the returned ServiceInfo may get stale at any time after resolution, including
+ * immediately after the callback is called, and may not contain some service information that
+ * could be delivered later, like additional host addresses. Prefer using
+ * {@link #registerServiceInfoCallback}, which will keep the application up-to-date with the
+ * state of the service.
*/
+ @Deprecated
public void resolveService(NsdServiceInfo serviceInfo, ResolveListener listener) {
resolveService(serviceInfo, Runnable::run, listener);
}
@@ -1067,7 +1279,14 @@
* @param serviceInfo service to be resolved
* @param executor Executor to run listener callbacks with
* @param listener to receive callback upon success or failure.
+ *
+ * @deprecated the returned ServiceInfo may get stale at any time after resolution, including
+ * immediately after the callback is called, and may not contain some service information that
+ * could be delivered later, like additional host addresses. Prefer using
+ * {@link #registerServiceInfoCallback}, which will keep the application up-to-date with the
+ * state of the service.
*/
+ @Deprecated
public void resolveService(@NonNull NsdServiceInfo serviceInfo,
@NonNull Executor executor, @NonNull ResolveListener listener) {
checkServiceInfo(serviceInfo);
@@ -1079,6 +1298,85 @@
}
}
+ /**
+ * Stop service resolution initiated with {@link #resolveService}.
+ *
+ * A successful stop is notified with a call to {@link ResolveListener#onResolveStopped}.
+ *
+ * <p> Upon failure to stop service resolution for example if resolution is done or the
+ * requester stops resolution repeatedly, the application is notified
+ * {@link ResolveListener#onStopResolutionFailed} with {@link #FAILURE_OPERATION_NOT_RUNNING}
+ *
+ * @param listener This should be a listener object that was passed to {@link #resolveService}.
+ * It identifies the resolution that should be stopped and notifies of a
+ * successful or unsuccessful stop. Throws {@code IllegalArgumentException} if
+ * the listener was not passed to resolveService before.
+ */
+ public void stopServiceResolution(@NonNull ResolveListener listener) {
+ int id = getListenerKey(listener);
+ try {
+ mService.stopResolution(id);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Register a callback to listen for updates to a service.
+ *
+ * An application can listen to a service to continuously monitor availability of given service.
+ * The callback methods will be called on the passed executor. And service updates are sent with
+ * continuous calls to {@link ServiceInfoCallback#onServiceUpdated}.
+ *
+ * This is different from {@link #resolveService} which provides one shot service information.
+ *
+ * <p> An application can listen to a service once a time. It needs to cancel the registration
+ * before registering other callbacks. Upon failure to register a callback for example if
+ * it's a duplicated registration, the application is notified through
+ * {@link ServiceInfoCallback#onServiceInfoCallbackRegistrationFailed} with
+ * {@link #FAILURE_BAD_PARAMETERS} or {@link #FAILURE_ALREADY_ACTIVE}.
+ *
+ * @param serviceInfo the service to receive updates for
+ * @param executor Executor to run callbacks with
+ * @param listener to receive callback upon service update
+ */
+ public void registerServiceInfoCallback(@NonNull NsdServiceInfo serviceInfo,
+ @NonNull Executor executor, @NonNull ServiceInfoCallback listener) {
+ checkServiceInfo(serviceInfo);
+ int key = putListener(listener, executor, serviceInfo);
+ try {
+ mService.registerServiceInfoCallback(key, serviceInfo);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Unregister a callback registered with {@link #registerServiceInfoCallback}.
+ *
+ * A successful unregistration is notified with a call to
+ * {@link ServiceInfoCallback#onServiceInfoCallbackUnregistered}. The same callback can only be
+ * reused after this is called.
+ *
+ * <p>If the callback is not already registered, this will throw with
+ * {@link IllegalArgumentException}.
+ *
+ * @param listener This should be a listener object that was passed to
+ * {@link #registerServiceInfoCallback}. It identifies the registration that
+ * should be unregistered and notifies of a successful or unsuccessful stop.
+ * Throws {@code IllegalArgumentException} if the listener was not passed to
+ * {@link #registerServiceInfoCallback} before.
+ */
+ public void unregisterServiceInfoCallback(@NonNull ServiceInfoCallback listener) {
+ // Will throw IllegalArgumentException if the listener is not known
+ int id = getListenerKey(listener);
+ try {
+ mService.unregisterServiceInfoCallback(id);
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
private static void checkListener(Object listener) {
Objects.requireNonNull(listener, "listener cannot be null");
}
diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java
index 6438a60..caeecdd 100644
--- a/framework-t/src/android/net/nsd/NsdServiceInfo.java
+++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java
@@ -26,10 +26,14 @@
import android.util.ArrayMap;
import android.util.Log;
+import com.android.net.module.util.InetAddressUtils;
+
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
import java.util.Collections;
+import java.util.List;
import java.util.Map;
/**
@@ -46,7 +50,7 @@
private final ArrayMap<String, byte[]> mTxtRecord = new ArrayMap<>();
- private InetAddress mHost;
+ private final List<InetAddress> mHostAddresses = new ArrayList<>();
private int mPort;
@@ -84,17 +88,32 @@
mServiceType = s;
}
- /** Get the host address. The host address is valid for a resolved service. */
+ /**
+ * Get the host address. The host address is valid for a resolved service.
+ *
+ * @deprecated Use {@link #getHostAddresses()} to get the entire list of addresses for the host.
+ */
+ @Deprecated
public InetAddress getHost() {
- return mHost;
+ return mHostAddresses.size() == 0 ? null : mHostAddresses.get(0);
}
- /** Set the host address */
+ /**
+ * Set the host address
+ *
+ * @deprecated Use {@link #setHostAddresses(List)} to set multiple addresses for the host.
+ */
+ @Deprecated
public void setHost(InetAddress s) {
- mHost = s;
+ setHostAddresses(Collections.singletonList(s));
}
- /** Get port number. The port number is valid for a resolved service. */
+ /**
+ * Get port number. The port number is valid for a resolved service.
+ *
+ * The port is valid for all addresses.
+ * @see #getHostAddresses()
+ */
public int getPort() {
return mPort;
}
@@ -105,6 +124,24 @@
}
/**
+ * Get the host addresses.
+ *
+ * All host addresses are valid for the resolved service.
+ * All addresses share the same port
+ * @see #getPort()
+ */
+ @NonNull
+ public List<InetAddress> getHostAddresses() {
+ return new ArrayList<>(mHostAddresses);
+ }
+
+ /** Set the host addresses */
+ public void setHostAddresses(@NonNull List<InetAddress> addresses) {
+ mHostAddresses.clear();
+ mHostAddresses.addAll(addresses);
+ }
+
+ /**
* Unpack txt information from a base-64 encoded byte array.
*
* @param txtRecordsRawBytes The raw base64 encoded byte array.
@@ -359,7 +396,7 @@
StringBuilder sb = new StringBuilder();
sb.append("name: ").append(mServiceName)
.append(", type: ").append(mServiceType)
- .append(", host: ").append(mHost)
+ .append(", hostAddresses: ").append(TextUtils.join(", ", mHostAddresses))
.append(", port: ").append(mPort)
.append(", network: ").append(mNetwork);
@@ -377,12 +414,6 @@
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(mServiceName);
dest.writeString(mServiceType);
- if (mHost != null) {
- dest.writeInt(1);
- dest.writeByteArray(mHost.getAddress());
- } else {
- dest.writeInt(0);
- }
dest.writeInt(mPort);
// TXT record key/value pairs.
@@ -401,6 +432,10 @@
dest.writeParcelable(mNetwork, 0);
dest.writeInt(mInterfaceIndex);
+ dest.writeInt(mHostAddresses.size());
+ for (InetAddress address : mHostAddresses) {
+ InetAddressUtils.parcelInetAddress(dest, address, flags);
+ }
}
/** Implement the Parcelable interface */
@@ -410,13 +445,6 @@
NsdServiceInfo info = new NsdServiceInfo();
info.mServiceName = in.readString();
info.mServiceType = in.readString();
-
- if (in.readInt() == 1) {
- try {
- info.mHost = InetAddress.getByAddress(in.createByteArray());
- } catch (java.net.UnknownHostException e) {}
- }
-
info.mPort = in.readInt();
// TXT record key/value pairs.
@@ -432,6 +460,10 @@
}
info.mNetwork = in.readParcelable(null, Network.class);
info.mInterfaceIndex = in.readInt();
+ int size = in.readInt();
+ for (int i = 0; i < size; i++) {
+ info.mHostAddresses.add(InetAddressUtils.unparcelInetAddress(in));
+ }
return info;
}
diff --git a/framework/api/system-current.txt b/framework/api/system-current.txt
index dd3404c..0b03983 100644
--- a/framework/api/system-current.txt
+++ b/framework/api/system-current.txt
@@ -470,7 +470,9 @@
}
public abstract class SocketKeepalive implements java.lang.AutoCloseable {
+ method public final void start(@IntRange(from=0xa, to=0xe10) int, int);
field public static final int ERROR_NO_SUCH_SLOT = -33; // 0xffffffdf
+ field public static final int FLAG_AUTOMATIC_ON_OFF = 1; // 0x1
field public static final int SUCCESS = 0; // 0x0
}
diff --git a/framework/src/android/net/IConnectivityManager.aidl b/framework/src/android/net/IConnectivityManager.aidl
index 7b6e769..7db231e 100644
--- a/framework/src/android/net/IConnectivityManager.aidl
+++ b/framework/src/android/net/IConnectivityManager.aidl
@@ -188,7 +188,7 @@
void startNattKeepaliveWithFd(in Network network, in ParcelFileDescriptor pfd, int resourceId,
int intervalSeconds, in ISocketKeepaliveCallback cb, String srcAddr,
- String dstAddr);
+ String dstAddr, boolean automaticOnOffKeepalives);
void startTcpKeepalive(in Network network, in ParcelFileDescriptor pfd, int intervalSeconds,
in ISocketKeepaliveCallback cb);
diff --git a/framework/src/android/net/NattSocketKeepalive.java b/framework/src/android/net/NattSocketKeepalive.java
index 56cc923..4d45e70 100644
--- a/framework/src/android/net/NattSocketKeepalive.java
+++ b/framework/src/android/net/NattSocketKeepalive.java
@@ -47,13 +47,39 @@
mResourceId = resourceId;
}
+ /**
+ * Request that keepalive be started with the given {@code intervalSec}.
+ *
+ * When a VPN is running with the network for this keepalive as its underlying network, the
+ * system can monitor the TCP connections on that VPN to determine whether this keepalive is
+ * necessary. To enable this behavior, pass {@link SocketKeepalive#FLAG_AUTOMATIC_ON_OFF} into
+ * the flags. When this is enabled, the system will disable sending keepalive packets when
+ * there are no TCP connections over the VPN(s) running over this network to save battery, and
+ * restart sending them as soon as any TCP connection is opened over one of the VPN networks.
+ * When no VPN is running on top of this network, this flag has no effect, i.e. the keepalives
+ * are always sent with the specified interval.
+ *
+ * Also {@see SocketKeepalive}.
+ *
+ * @param intervalSec The target interval in seconds between keepalive packet transmissions.
+ * The interval should be between 10 seconds and 3600 seconds. Otherwise,
+ * the supplied {@link Callback} will see a call to
+ * {@link Callback#onError(int)} with {@link #ERROR_INVALID_INTERVAL}.
+ * @param flags Flags to enable/disable available options on this keepalive.
+ * @hide
+ */
@Override
- protected void startImpl(int intervalSec) {
+ protected void startImpl(int intervalSec, int flags) {
+ if (0 != (flags & ~FLAG_AUTOMATIC_ON_OFF)) {
+ throw new IllegalArgumentException("Illegal flag value for "
+ + this.getClass().getSimpleName() + " : " + flags);
+ }
+ final boolean automaticOnOffKeepalives = 0 != (flags & FLAG_AUTOMATIC_ON_OFF);
mExecutor.execute(() -> {
try {
mService.startNattKeepaliveWithFd(mNetwork, mPfd, mResourceId,
- intervalSec, mCallback,
- mSource.getHostAddress(), mDestination.getHostAddress());
+ intervalSec, mCallback, mSource.getHostAddress(),
+ mDestination.getHostAddress(), automaticOnOffKeepalives);
} catch (RemoteException e) {
Log.e(TAG, "Error starting socket keepalive: ", e);
throw e.rethrowFromSystemServer();
diff --git a/framework/src/android/net/NetworkAgent.java b/framework/src/android/net/NetworkAgent.java
index 1486619..732bd87 100644
--- a/framework/src/android/net/NetworkAgent.java
+++ b/framework/src/android/net/NetworkAgent.java
@@ -483,6 +483,20 @@
*/
public static final int EVENT_UNREGISTER_AFTER_REPLACEMENT = BASE + 29;
+ /**
+ * Sent by AutomaticOnOffKeepaliveTracker periodically (when relevant) to trigger monitor
+ * automatic keepalive request.
+ *
+ * NATT keepalives have an automatic mode where the system only sends keepalive packets when
+ * TCP sockets are open over a VPN. The system will check periodically for presence of
+ * such open sockets, and this message is what triggers the re-evaluation.
+ *
+ * arg1 = hardware slot number of the keepalive
+ * obj = {@link Network} that the keepalive is started on.
+ * @hide
+ */
+ public static final int CMD_MONITOR_AUTOMATIC_KEEPALIVE = BASE + 30;
+
private static NetworkInfo getLegacyNetworkInfo(final NetworkAgentConfig config) {
final NetworkInfo ni = new NetworkInfo(config.legacyType, config.legacySubType,
config.legacyTypeName, config.legacySubTypeName);
diff --git a/framework/src/android/net/SocketKeepalive.java b/framework/src/android/net/SocketKeepalive.java
index 57cf5e3..90e5e9b 100644
--- a/framework/src/android/net/SocketKeepalive.java
+++ b/framework/src/android/net/SocketKeepalive.java
@@ -16,6 +16,8 @@
package android.net;
+import static android.annotation.SystemApi.Client.PRIVILEGED_APPS;
+
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
@@ -174,6 +176,27 @@
public @interface KeepaliveEvent {}
/**
+ * Whether the system automatically toggles keepalive when no TCP connection is open on the VPN.
+ *
+ * If this flag is present, the system will monitor the VPN(s) running on top of the specified
+ * network for open TCP connections. When no such connections are open, it will turn off the
+ * keepalives to conserve battery power. When there is at least one such connection it will
+ * turn on the keepalives to make sure functionality is preserved.
+ *
+ * This only works with {@link NattSocketKeepalive}.
+ * @hide
+ */
+ @SystemApi
+ public static final int FLAG_AUTOMATIC_ON_OFF = 1 << 0;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "FLAG_"}, flag = true, value = {
+ FLAG_AUTOMATIC_ON_OFF
+ })
+ public @interface StartFlags {}
+
+ /**
* The minimum interval in seconds between keepalive packet transmissions.
*
* @hide
@@ -294,13 +317,15 @@
}
/**
- * Request that keepalive be started with the given {@code intervalSec}. See
- * {@link SocketKeepalive}. If the remote binder dies, or the binder call throws an exception
- * when invoking start or stop of the {@link SocketKeepalive}, a {@link RemoteException} will be
- * thrown into the {@code executor}. This is typically not important to catch because the remote
- * party is the system, so if it is not in shape to communicate through binder the system is
- * probably going down anyway. If the caller cares regardless, it can use a custom
- * {@link Executor} to catch the {@link RemoteException}.
+ * Request that keepalive be started with the given {@code intervalSec}.
+ *
+ * See {@link SocketKeepalive}. If the remote binder dies, or the binder call throws an
+ * exception when invoking start or stop of the {@link SocketKeepalive}, a
+ * {@link RuntimeException} caused by a {@link RemoteException} will be thrown into the
+ * {@link Executor}. This is typically not important to catch because the remote party is
+ * the system, so if it is not in shape to communicate through binder the system is going
+ * down anyway. If the caller still cares, it can use a custom {@link Executor} to catch the
+ * {@link RuntimeException}.
*
* @param intervalSec The target interval in seconds between keepalive packet transmissions.
* The interval should be between 10 seconds and 3600 seconds, otherwise
@@ -308,11 +333,35 @@
*/
public final void start(@IntRange(from = MIN_INTERVAL_SEC, to = MAX_INTERVAL_SEC)
int intervalSec) {
- startImpl(intervalSec);
+ startImpl(intervalSec, 0 /* flags */);
+ }
+
+ /**
+ * Request that keepalive be started with the given {@code intervalSec}.
+ *
+ * See {@link SocketKeepalive}. If the remote binder dies, or the binder call throws an
+ * exception when invoking start or stop of the {@link SocketKeepalive}, a
+ * {@link RuntimeException} caused by a {@link RemoteException} will be thrown into the
+ * {@link Executor}. This is typically not important to catch because the remote party is
+ * the system, so if it is not in shape to communicate through binder the system is going
+ * down anyway. If the caller still cares, it can use a custom {@link Executor} to catch the
+ * {@link RuntimeException}.
+ *
+ * @param intervalSec The target interval in seconds between keepalive packet transmissions.
+ * The interval should be between 10 seconds and 3600 seconds. Otherwise,
+ * the supplied {@link Callback} will see a call to
+ * {@link Callback#onError(int)} with {@link #ERROR_INVALID_INTERVAL}.
+ * @param flags Flags to enable/disable available options on this keepalive.
+ * @hide
+ */
+ @SystemApi(client = PRIVILEGED_APPS)
+ public final void start(@IntRange(from = MIN_INTERVAL_SEC, to = MAX_INTERVAL_SEC)
+ int intervalSec, @StartFlags int flags) {
+ startImpl(intervalSec, flags);
}
/** @hide */
- protected abstract void startImpl(int intervalSec);
+ protected abstract void startImpl(int intervalSec, @StartFlags int flags);
/**
* Requests that keepalive be stopped. The application must wait for {@link Callback#onStopped}
diff --git a/framework/src/android/net/TcpSocketKeepalive.java b/framework/src/android/net/TcpSocketKeepalive.java
index 7131784..51d805e 100644
--- a/framework/src/android/net/TcpSocketKeepalive.java
+++ b/framework/src/android/net/TcpSocketKeepalive.java
@@ -50,7 +50,11 @@
* acknowledgement.
*/
@Override
- protected void startImpl(int intervalSec) {
+ protected void startImpl(int intervalSec, int flags) {
+ if (0 != flags) {
+ throw new IllegalArgumentException("Illegal flag value for "
+ + this.getClass().getSimpleName() + " : " + flags);
+ }
mExecutor.execute(() -> {
try {
mService.startTcpKeepalive(mNetwork, mPfd, intervalSec, mCallback);
diff --git a/nearby/README.md b/nearby/README.md
index 6925dc4..8451882 100644
--- a/nearby/README.md
+++ b/nearby/README.md
@@ -29,6 +29,20 @@
$ aidegen .
# This will launch Intellij project for Nearby module.
```
+Note, the setup above may fail to index classes defined in proto, such
+that all classes defined in proto shows red in IDE and cannot be auto-completed.
+To fix, you can mannually add jar files generated from proto to the class path
+as below. First, find the jar file of presence proto with
+```sh
+ls $ANDROID_BUILD_TOP/out/soong/.intermediates/packages/modules/Connectivity/nearby/service/proto/presence-lite-protos/android_common/combined/presence-lite-protos.jar
+```
+Then, add the jar in IDE as below.
+1. Menu: File > Project Structure
+2. Select Modules at the left panel and select the Dependencies tab.
+3. Select the + icon and select 1 JARs or Directories option.
+4. Select the JAR file found above, make sure it is checked in the beginning square.
+5. Click the OK button.
+6. Restart the IDE to re-index.
## Build and Install
@@ -40,3 +54,23 @@
--output /tmp/tethering.apex
$ adb install -r /tmp/tethering.apex
```
+
+## Build and Install from tm-mainline-prod branch
+When build and flash the APEX from tm-mainline-prod, you may see the error below.
+```
+[INSTALL_FAILED_VERSION_DOWNGRADE: Downgrade of APEX package com.google.android.tethering is not allowed. Active version: 990090000 attempted: 339990000])
+```
+This is because the device is flashed with AOSP built from master or other branches, which has
+prebuilt APEX with higher version. We can use root access to replace the prebuilt APEX with the APEX
+built from tm-mainline-prod as below.
+1. adb root && adb remount; adb shell mount -orw,remount /system/apex
+2. cp tethering.next.apex com.google.android.tethering.apex
+3. adb push com.google.android.tethering.apex /system/apex/
+4. adb reboot
+After the steps above, the APEX can be reinstalled with adb install -r.
+(More APEX background can be found in https://source.android.com/docs/core/ota/apex#using-an-apex.)
+
+## Build APEX to support multiple platforms
+If you need to flash the APEX to different devices, Pixel 6, Pixel 7, or even devices from OEM, you
+can share the APEX by ```source build/envsetup.sh && lunch aosp_arm64-userdebug```. This can avoid
+ re-compiling for different targets.
diff --git a/nearby/framework/java/android/nearby/DataElement.java b/nearby/framework/java/android/nearby/DataElement.java
index 6fa5fb5..4592c33 100644
--- a/nearby/framework/java/android/nearby/DataElement.java
+++ b/nearby/framework/java/android/nearby/DataElement.java
@@ -16,13 +16,17 @@
package android.nearby;
+import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
import com.android.internal.util.Preconditions;
+import java.util.Arrays;
+import java.util.Objects;
/**
* Represents a data element in Nearby Presence.
@@ -35,11 +39,80 @@
private final int mKey;
private final byte[] mValue;
+ /** @hide */
+ @IntDef({
+ DataType.BLE_SERVICE_DATA,
+ DataType.BLE_ADDRESS,
+ DataType.SALT,
+ DataType.PRIVATE_IDENTITY,
+ DataType.TRUSTED_IDENTITY,
+ DataType.PUBLIC_IDENTITY,
+ DataType.PROVISIONED_IDENTITY,
+ DataType.TX_POWER,
+ DataType.ACTION,
+ DataType.MODEL_ID,
+ DataType.EDDYSTONE_EPHEMERAL_IDENTIFIER,
+ DataType.ACCOUNT_KEY_DATA,
+ DataType.CONNECTION_STATUS,
+ DataType.BATTERY,
+ DataType.SCAN_MODE
+ })
+ public @interface DataType {
+ int BLE_SERVICE_DATA = 100;
+ int BLE_ADDRESS = 101;
+ // This is to indicate if the scan is offload only
+ int SCAN_MODE = 102;
+ int SALT = 0;
+ int PRIVATE_IDENTITY = 1;
+ int TRUSTED_IDENTITY = 2;
+ int PUBLIC_IDENTITY = 3;
+ int PROVISIONED_IDENTITY = 4;
+ int TX_POWER = 5;
+ int ACTION = 6;
+ int MODEL_ID = 7;
+ int EDDYSTONE_EPHEMERAL_IDENTIFIER = 8;
+ int ACCOUNT_KEY_DATA = 9;
+ int CONNECTION_STATUS = 10;
+ int BATTERY = 11;
+ }
+
+ /**
+ * @hide
+ */
+ public static boolean isValidType(int type) {
+ return type == DataType.BLE_SERVICE_DATA
+ || type == DataType.ACCOUNT_KEY_DATA
+ || type == DataType.BLE_ADDRESS
+ || type == DataType.SCAN_MODE
+ || type == DataType.SALT
+ || type == DataType.PRIVATE_IDENTITY
+ || type == DataType.TRUSTED_IDENTITY
+ || type == DataType.PUBLIC_IDENTITY
+ || type == DataType.PROVISIONED_IDENTITY
+ || type == DataType.TX_POWER
+ || type == DataType.ACTION
+ || type == DataType.MODEL_ID
+ || type == DataType.EDDYSTONE_EPHEMERAL_IDENTIFIER
+ || type == DataType.CONNECTION_STATUS
+ || type == DataType.BATTERY;
+ }
+
+ /**
+ * @return {@code true} if this is identity type.
+ * @hide
+ */
+ public boolean isIdentityDataType() {
+ return mKey == DataType.PRIVATE_IDENTITY
+ || mKey == DataType.TRUSTED_IDENTITY
+ || mKey == DataType.PUBLIC_IDENTITY
+ || mKey == DataType.PROVISIONED_IDENTITY;
+ }
+
/**
* Constructs a {@link DataElement}.
*/
public DataElement(int key, @NonNull byte[] value) {
- Preconditions.checkState(value != null, "value cannot be null");
+ Preconditions.checkArgument(value != null, "value cannot be null");
mKey = key;
mValue = value;
}
@@ -61,6 +134,20 @@
};
@Override
+ public boolean equals(@Nullable Object obj) {
+ if (obj instanceof DataElement) {
+ return mKey == ((DataElement) obj).mKey
+ && Arrays.equals(mValue, ((DataElement) obj).mValue);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mKey, Arrays.hashCode(mValue));
+ }
+
+ @Override
public int describeContents() {
return 0;
}
diff --git a/nearby/framework/java/android/nearby/NearbyDevice.java b/nearby/framework/java/android/nearby/NearbyDevice.java
index 538940c..e8fcc28 100644
--- a/nearby/framework/java/android/nearby/NearbyDevice.java
+++ b/nearby/framework/java/android/nearby/NearbyDevice.java
@@ -21,11 +21,13 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
+import android.util.ArraySet;
import com.android.internal.util.Preconditions;
import java.util.List;
import java.util.Objects;
+import java.util.Set;
/**
* A class represents a device that can be discovered by multiple mediums.
@@ -123,13 +125,17 @@
@Override
public boolean equals(Object other) {
- if (other instanceof NearbyDevice) {
- NearbyDevice otherDevice = (NearbyDevice) other;
- return Objects.equals(mName, otherDevice.mName)
- && mMediums == otherDevice.mMediums
- && mRssi == otherDevice.mRssi;
+ if (!(other instanceof NearbyDevice)) {
+ return false;
}
- return false;
+ NearbyDevice otherDevice = (NearbyDevice) other;
+ Set<Integer> mediumSet = new ArraySet<>(mMediums);
+ Set<Integer> otherMediumSet = new ArraySet<>(otherDevice.mMediums);
+ if (!mediumSet.equals(otherMediumSet)) {
+ return false;
+ }
+
+ return Objects.equals(mName, otherDevice.mName) && mRssi == otherDevice.mRssi;
}
@Override
diff --git a/nearby/framework/java/android/nearby/NearbyDeviceParcelable.java b/nearby/framework/java/android/nearby/NearbyDeviceParcelable.java
index 8f44091..180b662 100644
--- a/nearby/framework/java/android/nearby/NearbyDeviceParcelable.java
+++ b/nearby/framework/java/android/nearby/NearbyDeviceParcelable.java
@@ -76,6 +76,17 @@
in.readByteArray(salt);
builder.setData(salt);
}
+ if (in.readInt() == 1) {
+ builder.setPresenceDevice(in.readParcelable(
+ PresenceDevice.class.getClassLoader(),
+ PresenceDevice.class));
+ }
+ if (in.readInt() == 1) {
+ int encryptionKeyTagLength = in.readInt();
+ byte[] keyTag = new byte[encryptionKeyTagLength];
+ in.readByteArray(keyTag);
+ builder.setData(keyTag);
+ }
return builder.build();
}
@@ -96,6 +107,8 @@
@Nullable private final String mFastPairModelId;
@Nullable private final byte[] mData;
@Nullable private final byte[] mSalt;
+ @Nullable private final PresenceDevice mPresenceDevice;
+ @Nullable private final byte[] mEncryptionKeyTag;
private NearbyDeviceParcelable(
@ScanRequest.ScanType int scanType,
@@ -108,7 +121,9 @@
@Nullable String fastPairModelId,
@Nullable String bluetoothAddress,
@Nullable byte[] data,
- @Nullable byte[] salt) {
+ @Nullable byte[] salt,
+ @Nullable PresenceDevice presenceDevice,
+ @Nullable byte[] encryptionKeyTag) {
mScanType = scanType;
mName = name;
mMedium = medium;
@@ -120,6 +135,8 @@
mBluetoothAddress = bluetoothAddress;
mData = data;
mSalt = salt;
+ mPresenceDevice = presenceDevice;
+ mEncryptionKeyTag = encryptionKeyTag;
}
/** No special parcel contents. */
@@ -164,6 +181,15 @@
dest.writeInt(mSalt.length);
dest.writeByteArray(mSalt);
}
+ dest.writeInt(mPresenceDevice == null ? 0 : 1);
+ if (mPresenceDevice != null) {
+ dest.writeParcelable(mPresenceDevice, /* parcelableFlags= */ 0);
+ }
+ dest.writeInt(mEncryptionKeyTag == null ? 0 : 1);
+ if (mEncryptionKeyTag != null) {
+ dest.writeInt(mEncryptionKeyTag.length);
+ dest.writeByteArray(mEncryptionKeyTag);
+ }
}
/** Returns a string representation of this ScanRequest. */
@@ -210,7 +236,11 @@
&& (Objects.equals(
mFastPairModelId, otherNearbyDeviceParcelable.mFastPairModelId))
&& (Arrays.equals(mData, otherNearbyDeviceParcelable.mData))
- && (Arrays.equals(mSalt, otherNearbyDeviceParcelable.mSalt));
+ && (Arrays.equals(mSalt, otherNearbyDeviceParcelable.mSalt))
+ && (Objects.equals(
+ mPresenceDevice, otherNearbyDeviceParcelable.mPresenceDevice))
+ && (Arrays.equals(
+ mEncryptionKeyTag, otherNearbyDeviceParcelable.mEncryptionKeyTag));
}
return false;
}
@@ -227,7 +257,9 @@
mBluetoothAddress,
mFastPairModelId,
Arrays.hashCode(mData),
- Arrays.hashCode(mSalt));
+ Arrays.hashCode(mSalt),
+ mPresenceDevice,
+ Arrays.hashCode(mEncryptionKeyTag));
}
/**
@@ -351,6 +383,26 @@
return mSalt;
}
+ /**
+ * Gets the {@link PresenceDevice} Nearby Presence device. This field is
+ * for Fast Pair client only.
+ */
+ @Nullable
+ public PresenceDevice getPresenceDevice() {
+ return mPresenceDevice;
+ }
+
+ /**
+ * Gets the encryption key tag calculated from advertisement
+ * Returns {@code null} if the data is not encrypted or this is not a Presence device.
+ *
+ * Used in Presence.
+ */
+ @Nullable
+ public byte[] getEncryptionKeyTag() {
+ return mEncryptionKeyTag;
+ }
+
/** Builder class for {@link NearbyDeviceParcelable}. */
public static final class Builder {
@Nullable private String mName;
@@ -364,6 +416,8 @@
@Nullable private String mBluetoothAddress;
@Nullable private byte[] mData;
@Nullable private byte[] mSalt;
+ @Nullable private PresenceDevice mPresenceDevice;
+ @Nullable private byte[] mEncryptionKeyTag;
/**
* Sets the scan type of the NearbyDeviceParcelable.
@@ -469,7 +523,7 @@
/**
* Sets the scanned raw data.
*
- * @param data Data the scan. For example, {@link ScanRecord#getServiceData()} if scanned by
+ * @param data raw data scanned, like {@link ScanRecord#getServiceData()} if scanned by
* Bluetooth.
*/
@NonNull
@@ -479,6 +533,17 @@
}
/**
+ * Sets the encryption key tag calculated from the advertisement.
+ *
+ * @param encryptionKeyTag calculated from identity scanned from the advertisement
+ */
+ @NonNull
+ public Builder setEncryptionKeyTag(@Nullable byte[] encryptionKeyTag) {
+ mEncryptionKeyTag = encryptionKeyTag;
+ return this;
+ }
+
+ /**
* Sets the slat in the advertisement from the Nearby Presence device.
*
* @param salt in the advertisement from the Nearby Presence device.
@@ -489,6 +554,17 @@
return this;
}
+ /**
+ * Sets the {@link PresenceDevice} if there is any.
+ * The current {@link NearbyDeviceParcelable} can be seen as the wrapper of the
+ * {@link PresenceDevice}.
+ */
+ @Nullable
+ public Builder setPresenceDevice(@Nullable PresenceDevice presenceDevice) {
+ mPresenceDevice = presenceDevice;
+ return this;
+ }
+
/** Builds a ScanResult. */
@NonNull
public NearbyDeviceParcelable build() {
@@ -503,7 +579,9 @@
mFastPairModelId,
mBluetoothAddress,
mData,
- mSalt);
+ mSalt,
+ mPresenceDevice,
+ mEncryptionKeyTag);
}
}
}
diff --git a/nearby/framework/java/android/nearby/NearbyManager.java b/nearby/framework/java/android/nearby/NearbyManager.java
index 106c290..6d6b69a 100644
--- a/nearby/framework/java/android/nearby/NearbyManager.java
+++ b/nearby/framework/java/android/nearby/NearbyManager.java
@@ -62,7 +62,7 @@
ScanStatus.ERROR,
})
public @interface ScanStatus {
- // Default, invalid state.
+ // The undetermined status, some modules may be initializing. Retry is suggested.
int UNKNOWN = 0;
// The successful state.
int SUCCESS = 1;
@@ -103,6 +103,7 @@
mService = service;
}
+ @Nullable
private static NearbyDevice toClientNearbyDevice(
NearbyDeviceParcelable nearbyDeviceParcelable,
@ScanRequest.ScanType int scanType) {
@@ -118,23 +119,12 @@
}
if (scanType == ScanRequest.SCAN_TYPE_NEARBY_PRESENCE) {
- PublicCredential publicCredential = nearbyDeviceParcelable.getPublicCredential();
- if (publicCredential == null) {
- return null;
+ PresenceDevice presenceDevice = nearbyDeviceParcelable.getPresenceDevice();
+ if (presenceDevice == null) {
+ Log.e(TAG,
+ "Cannot find any Presence device in discovered NearbyDeviceParcelable");
}
- byte[] salt = nearbyDeviceParcelable.getSalt();
- if (salt == null) {
- salt = new byte[0];
- }
- return new PresenceDevice.Builder(
- // Use the public credential hash as the device Id.
- String.valueOf(publicCredential.hashCode()),
- salt,
- publicCredential.getSecretId(),
- publicCredential.getEncryptedMetadata())
- .setRssi(nearbyDeviceParcelable.getRssi())
- .addMedium(nearbyDeviceParcelable.getMedium())
- .build();
+ return presenceDevice;
}
return null;
}
@@ -339,9 +329,9 @@
public void onDiscovered(NearbyDeviceParcelable nearbyDeviceParcelable)
throws RemoteException {
mExecutor.execute(() -> {
- if (mScanCallback != null) {
- mScanCallback.onDiscovered(
- toClientNearbyDevice(nearbyDeviceParcelable, mScanType));
+ NearbyDevice nearbyDevice = toClientNearbyDevice(nearbyDeviceParcelable, mScanType);
+ if (mScanCallback != null && nearbyDevice != null) {
+ mScanCallback.onDiscovered(nearbyDevice);
}
});
}
@@ -350,7 +340,8 @@
public void onUpdated(NearbyDeviceParcelable nearbyDeviceParcelable)
throws RemoteException {
mExecutor.execute(() -> {
- if (mScanCallback != null) {
+ NearbyDevice nearbyDevice = toClientNearbyDevice(nearbyDeviceParcelable, mScanType);
+ if (mScanCallback != null && nearbyDevice != null) {
mScanCallback.onUpdated(
toClientNearbyDevice(nearbyDeviceParcelable, mScanType));
}
@@ -360,7 +351,8 @@
@Override
public void onLost(NearbyDeviceParcelable nearbyDeviceParcelable) throws RemoteException {
mExecutor.execute(() -> {
- if (mScanCallback != null) {
+ NearbyDevice nearbyDevice = toClientNearbyDevice(nearbyDeviceParcelable, mScanType);
+ if (mScanCallback != null && nearbyDevice != null) {
mScanCallback.onLost(
toClientNearbyDevice(nearbyDeviceParcelable, mScanType));
}
diff --git a/nearby/framework/java/android/nearby/PresenceDevice.java b/nearby/framework/java/android/nearby/PresenceDevice.java
index cb406e4..7efa5e6 100644
--- a/nearby/framework/java/android/nearby/PresenceDevice.java
+++ b/nearby/framework/java/android/nearby/PresenceDevice.java
@@ -26,6 +26,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.Objects;
@@ -134,6 +135,54 @@
return mExtendedProperties;
}
+ /**
+ * This can only be hidden because this is the System API,
+ * which cannot be changed in T timeline.
+ *
+ * @hide
+ */
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof PresenceDevice) {
+ PresenceDevice otherDevice = (PresenceDevice) other;
+ if (super.equals(otherDevice)) {
+ return Arrays.equals(mSalt, otherDevice.mSalt)
+ && Arrays.equals(mSecretId, otherDevice.mSecretId)
+ && Arrays.equals(mEncryptedIdentity, otherDevice.mEncryptedIdentity)
+ && Objects.equals(mDeviceId, otherDevice.mDeviceId)
+ && mDeviceType == otherDevice.mDeviceType
+ && Objects.equals(mDeviceImageUrl, otherDevice.mDeviceImageUrl)
+ && mDiscoveryTimestampMillis == otherDevice.mDiscoveryTimestampMillis
+ && Objects.equals(mExtendedProperties, otherDevice.mExtendedProperties);
+ }
+ }
+ return false;
+ }
+
+ /**
+ * This can only be hidden because this is the System API,
+ * which cannot be changed in T timeline.
+ *
+ * @hide
+ *
+ * @return The unique hash value of the {@link PresenceDevice}
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ getName(),
+ getMediums(),
+ getRssi(),
+ Arrays.hashCode(mSalt),
+ Arrays.hashCode(mSecretId),
+ Arrays.hashCode(mEncryptedIdentity),
+ mDeviceId,
+ mDeviceType,
+ mDeviceImageUrl,
+ mDiscoveryTimestampMillis,
+ mExtendedProperties);
+ }
+
private PresenceDevice(String deviceName, List<Integer> mMediums, int rssi, String deviceId,
byte[] salt, byte[] secretId, byte[] encryptedIdentity, int deviceType,
String deviceImageUrl, long discoveryTimestampMillis,
diff --git a/nearby/framework/java/android/nearby/PresenceScanFilter.java b/nearby/framework/java/android/nearby/PresenceScanFilter.java
index f0c3c06..50e97b4 100644
--- a/nearby/framework/java/android/nearby/PresenceScanFilter.java
+++ b/nearby/framework/java/android/nearby/PresenceScanFilter.java
@@ -71,7 +71,7 @@
super(ScanRequest.SCAN_TYPE_NEARBY_PRESENCE, rssiThreshold);
mCredentials = new ArrayList<>(credentials);
mPresenceActions = new ArrayList<>(presenceActions);
- mExtendedProperties = extendedProperties;
+ mExtendedProperties = new ArrayList<>(extendedProperties);
}
private PresenceScanFilter(Parcel in) {
@@ -132,7 +132,7 @@
}
dest.writeInt(mExtendedProperties.size());
if (!mExtendedProperties.isEmpty()) {
- dest.writeList(mExtendedProperties);
+ dest.writeParcelableList(mExtendedProperties, 0);
}
}
diff --git a/nearby/framework/java/android/nearby/ScanRequest.java b/nearby/framework/java/android/nearby/ScanRequest.java
index c717ac7..9421820 100644
--- a/nearby/framework/java/android/nearby/ScanRequest.java
+++ b/nearby/framework/java/android/nearby/ScanRequest.java
@@ -62,6 +62,12 @@
*/
public static final int SCAN_MODE_NO_POWER = -1;
/**
+ * A special scan mode to indicate that client only wants to use CHRE to scan.
+ *
+ * @hide
+ */
+ public static final int SCAN_MODE_CHRE_ONLY = 3;
+ /**
* Used to read a ScanRequest from a Parcel.
*/
@NonNull
diff --git a/nearby/halfsheet/res/values-ky/strings.xml b/nearby/halfsheet/res/values-ky/strings.xml
index 812e0e8..b0dfe20 100644
--- a/nearby/halfsheet/res/values-ky/strings.xml
+++ b/nearby/halfsheet/res/values-ky/strings.xml
@@ -25,5 +25,5 @@
<string name="paring_action_save" msgid="6259357442067880136">"Сактоо"</string>
<string name="paring_action_connect" msgid="4801102939608129181">"Туташуу"</string>
<string name="paring_action_launch" msgid="8940808384126591230">"Жөндөө"</string>
- <string name="paring_action_settings" msgid="424875657242864302">"Жөндөөлөр"</string>
+ <string name="paring_action_settings" msgid="424875657242864302">"Параметрлер"</string>
</resources>
diff --git a/nearby/halfsheet/src/com/android/nearby/halfsheet/HalfSheetActivity.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/HalfSheetActivity.java
index 2a38b8a..07e5776 100644
--- a/nearby/halfsheet/src/com/android/nearby/halfsheet/HalfSheetActivity.java
+++ b/nearby/halfsheet/src/com/android/nearby/halfsheet/HalfSheetActivity.java
@@ -16,6 +16,8 @@
package com.android.nearby.halfsheet;
+import static android.Manifest.permission.ACCESS_FINE_LOCATION;
+
import static com.android.nearby.halfsheet.fragment.DevicePairingFragment.APP_LAUNCH_FRAGMENT_TYPE;
import static com.android.server.nearby.common.bluetooth.fastpair.FastPairConstants.EXTRA_MODEL_ID;
import static com.android.server.nearby.common.fastpair.service.UserActionHandlerBase.EXTRA_MAC_ADDRESS;
@@ -226,7 +228,8 @@
EXTRA_HALF_SHEET_IS_RETROACTIVE,
getIntent().getBooleanExtra(EXTRA_HALF_SHEET_IS_RETROACTIVE,
false))
- .putExtra(EXTRA_MAC_ADDRESS, mScanFastPairStoreItem.getAddress()));
+ .putExtra(EXTRA_MAC_ADDRESS, mScanFastPairStoreItem.getAddress()),
+ ACCESS_FINE_LOCATION);
}
}
diff --git a/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/BroadcastUtils.java b/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/BroadcastUtils.java
index 467997c..2f1e90a 100644
--- a/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/BroadcastUtils.java
+++ b/nearby/halfsheet/src/com/android/nearby/halfsheet/utils/BroadcastUtils.java
@@ -31,6 +31,13 @@
context.sendBroadcast(intent);
}
+ /**
+ * Helps send a broadcast with specified receiver permission.
+ */
+ public static void sendBroadcast(Context context, Intent intent, String receiverPermission) {
+ context.sendBroadcast(intent, receiverPermission);
+ }
+
private BroadcastUtils() {
}
}
diff --git a/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java b/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
index 8fdac87..0ed8fff 100644
--- a/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
+++ b/nearby/service/java/com/android/server/nearby/NearbyConfiguration.java
@@ -18,25 +18,40 @@
import android.provider.DeviceConfig;
-import androidx.annotation.VisibleForTesting;
-
/**
* A utility class for encapsulating Nearby feature flag configurations.
*/
public class NearbyConfiguration {
/**
- * Flag use to enable presence legacy broadcast.
+ * Flag used to enable presence legacy broadcast.
*/
public static final String NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY =
"nearby_enable_presence_broadcast_legacy";
+ /**
+ * Flag used to for minimum nano app version to make Nearby CHRE scan work.
+ */
+ public static final String NEARBY_MAINLINE_NANO_APP_MIN_VERSION =
+ "nearby_mainline_nano_app_min_version";
+
+ /**
+ * Flag used to allow test mode and customization.
+ */
+ public static final String NEARBY_SUPPORT_TEST_APP = "nearby_support_test_app";
private boolean mEnablePresenceBroadcastLegacy;
+ private int mNanoAppMinVersion;
+
+ private boolean mSupportTestApp;
+
public NearbyConfiguration() {
mEnablePresenceBroadcastLegacy = getDeviceConfigBoolean(
NEARBY_ENABLE_PRESENCE_BROADCAST_LEGACY, false /* defaultValue */);
-
+ mNanoAppMinVersion = getDeviceConfigInt(
+ NEARBY_MAINLINE_NANO_APP_MIN_VERSION, 0 /* defaultValue */);
+ mSupportTestApp = getDeviceConfigBoolean(
+ NEARBY_SUPPORT_TEST_APP, false /* defaultValue */);
}
/**
@@ -46,13 +61,28 @@
return mEnablePresenceBroadcastLegacy;
}
+ public int getNanoAppMinVersion() {
+ return mNanoAppMinVersion;
+ }
+
+ /**
+ * @return {@code true} when in test mode and allows customization.
+ */
+ public boolean isTestAppSupported() {
+ return mSupportTestApp;
+ }
+
private boolean getDeviceConfigBoolean(final String name, final boolean defaultValue) {
final String value = getDeviceConfigProperty(name);
return value != null ? Boolean.parseBoolean(value) : defaultValue;
}
- @VisibleForTesting
- protected String getDeviceConfigProperty(String name) {
+ private int getDeviceConfigInt(final String name, final int defaultValue) {
+ final String value = getDeviceConfigProperty(name);
+ return value != null ? Integer.parseInt(value) : defaultValue;
+ }
+
+ private String getDeviceConfigProperty(String name) {
return DeviceConfig.getProperty(DeviceConfig.NAMESPACE_TETHERING, name);
}
}
diff --git a/nearby/service/java/com/android/server/nearby/NearbyService.java b/nearby/service/java/com/android/server/nearby/NearbyService.java
index 5ebf1e5..a1bca19 100644
--- a/nearby/service/java/com/android/server/nearby/NearbyService.java
+++ b/nearby/service/java/com/android/server/nearby/NearbyService.java
@@ -29,6 +29,7 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.pm.PackageManager;
import android.hardware.location.ContextHubManager;
import android.nearby.BroadcastRequestParcelable;
import android.nearby.IBroadcastListener;
@@ -41,8 +42,8 @@
import com.android.internal.annotations.VisibleForTesting;
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.PresenceManager;
import com.android.server.nearby.provider.BroadcastProviderManager;
import com.android.server.nearby.provider.DiscoveryProviderManager;
import com.android.server.nearby.provider.FastPairDataProvider;
@@ -53,10 +54,16 @@
/** Service implementing nearby functionality. */
public class NearbyService extends INearbyManager.Stub {
public static final String TAG = "NearbyService";
+ // Sets to true to start BLE scan from PresenceManager for manual testing.
+ public static final Boolean MANUAL_TEST = false;
+ // Sets to true to support Mainline Test App.
+ // This will disable BLE privilege check and legacy broadcast support check.
+ public static final Boolean SUPPORT_TEST_APP = false;
private final Context mContext;
private Injector mInjector;
private final FastPairManager mFastPairManager;
+ private final PresenceManager mPresenceManager;
private final BroadcastReceiver mBluetoothReceiver =
new BroadcastReceiver() {
@Override
@@ -84,6 +91,7 @@
mBroadcastProviderManager = new BroadcastProviderManager(context, mInjector);
final LocatorContextWrapper lcw = new LocatorContextWrapper(context, null);
mFastPairManager = new FastPairManager(lcw);
+ mPresenceManager = new PresenceManager(lcw);
}
@VisibleForTesting
@@ -100,10 +108,7 @@
CallerIdentity identity = CallerIdentity.fromBinder(mContext, packageName, attributionTag);
DiscoveryPermissions.enforceDiscoveryPermission(mContext, identity);
- if (mProviderManager.registerScanListener(scanRequest, listener, identity)) {
- return NearbyManager.ScanStatus.SUCCESS;
- }
- return NearbyManager.ScanStatus.ERROR;
+ return mProviderManager.registerScanListener(scanRequest, listener, identity);
}
@Override
@@ -157,16 +162,22 @@
FastPairDataProvider.init(mContext);
break;
case PHASE_BOOT_COMPLETED:
+ // mInjector needs to be initialized before mProviderManager.
if (mInjector instanceof SystemInjector) {
// The nearby service must be functioning after this boot phase.
((SystemInjector) mInjector).initializeBluetoothAdapter();
// Initialize ContextManager for CHRE scan.
- ((SystemInjector) mInjector).initializeContextHubManagerAdapter();
+ ((SystemInjector) mInjector).initializeContextHubManager();
}
+ mProviderManager.init();
mContext.registerReceiver(
mBluetoothReceiver,
new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
mFastPairManager.initiate();
+ // Only enable for manual Presence test on device.
+ if (MANUAL_TEST) {
+ mPresenceManager.initiate();
+ }
break;
}
}
@@ -178,15 +189,17 @@
*/
@RequiresPermission(android.Manifest.permission.BLUETOOTH_PRIVILEGED)
private static void enforceBluetoothPrivilegedPermission(Context context) {
- context.enforceCallingOrSelfPermission(
- android.Manifest.permission.BLUETOOTH_PRIVILEGED,
- "Need BLUETOOTH PRIVILEGED permission");
+ if (!SUPPORT_TEST_APP) {
+ context.enforceCallingOrSelfPermission(
+ android.Manifest.permission.BLUETOOTH_PRIVILEGED,
+ "Need BLUETOOTH PRIVILEGED permission");
+ }
}
private static final class SystemInjector implements Injector {
private final Context mContext;
@Nullable private BluetoothAdapter mBluetoothAdapter;
- @Nullable private ContextHubManagerAdapter mContextHubManagerAdapter;
+ @Nullable private ContextHubManager mContextHubManager;
@Nullable private AppOpsManager mAppOpsManager;
SystemInjector(Context context) {
@@ -201,8 +214,8 @@
@Override
@Nullable
- public ContextHubManagerAdapter getContextHubManagerAdapter() {
- return mContextHubManagerAdapter;
+ public ContextHubManager getContextHubManager() {
+ return mContextHubManager;
}
@Override
@@ -222,15 +235,13 @@
mBluetoothAdapter = manager.getAdapter();
}
- synchronized void initializeContextHubManagerAdapter() {
- if (mContextHubManagerAdapter != null) {
+ synchronized void initializeContextHubManager() {
+ if (mContextHubManager != null) {
return;
}
- ContextHubManager manager = mContext.getSystemService(ContextHubManager.class);
- if (manager == null) {
- return;
+ if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CONTEXT_HUB)) {
+ mContextHubManager = mContext.getSystemService(ContextHubManager.class);
}
- mContextHubManagerAdapter = new ContextHubManagerAdapter(manager);
}
synchronized void initializeAppOpsManager() {
diff --git a/nearby/service/java/com/android/server/nearby/common/ble/BleFilter.java b/nearby/service/java/com/android/server/nearby/common/ble/BleFilter.java
index 23d5170..dc4e11e 100644
--- a/nearby/service/java/com/android/server/nearby/common/ble/BleFilter.java
+++ b/nearby/service/java/com/android/server/nearby/common/ble/BleFilter.java
@@ -500,16 +500,16 @@
return false;
}
BleFilter other = (BleFilter) obj;
- return mDeviceName.equals(other.mDeviceName)
- && mDeviceAddress.equals(other.mDeviceAddress)
+ return equal(mDeviceName, other.mDeviceName)
+ && equal(mDeviceAddress, other.mDeviceAddress)
&& mManufacturerId == other.mManufacturerId
&& Arrays.equals(mManufacturerData, other.mManufacturerData)
&& Arrays.equals(mManufacturerDataMask, other.mManufacturerDataMask)
- && mServiceDataUuid.equals(other.mServiceDataUuid)
+ && equal(mServiceDataUuid, other.mServiceDataUuid)
&& Arrays.equals(mServiceData, other.mServiceData)
&& Arrays.equals(mServiceDataMask, other.mServiceDataMask)
- && mServiceUuid.equals(other.mServiceUuid)
- && mServiceUuidMask.equals(other.mServiceUuidMask);
+ && equal(mServiceUuid, other.mServiceUuid)
+ && equal(mServiceUuidMask, other.mServiceUuidMask);
}
/** Builder class for {@link BleFilter}. */
@@ -743,4 +743,11 @@
}
return osFilterBuilder.build();
}
+
+ /**
+ * equal() method for two possibly-null objects
+ */
+ private static boolean equal(@Nullable Object obj1, @Nullable Object obj2) {
+ return obj1 == obj2 || (obj1 != null && obj1.equals(obj2));
+ }
}
diff --git a/nearby/service/java/com/android/server/nearby/common/ble/testing/FastPairTestData.java b/nearby/service/java/com/android/server/nearby/common/ble/testing/FastPairTestData.java
index f27899f..b4f46f8 100644
--- a/nearby/service/java/com/android/server/nearby/common/ble/testing/FastPairTestData.java
+++ b/nearby/service/java/com/android/server/nearby/common/ble/testing/FastPairTestData.java
@@ -46,6 +46,12 @@
/** Model ID in {@link #getFastPairRecord()}. */
public static final byte[] FAST_PAIR_MODEL_ID = Hex.stringToBytes("AABBCC");
+ /** An arbitrary BLE device address. */
+ public static final String DEVICE_ADDRESS = "00:00:00:00:00:01";
+
+ /** Arbitrary RSSI (Received Signal Strength Indicator). */
+ public static final int RSSI = -72;
+
/** @see #getFastPairRecord() */
public static byte[] newFastPairRecord(byte header, byte[] modelId) {
return newFastPairRecord(
@@ -61,6 +67,45 @@
Hex.bytesToStringUppercase(serviceData)));
}
+ // This is an example extended inquiry response for a phone with PANU
+ // and Hands-free Audio Gateway
+ public static byte[] eir_1 = {
+ 0x06, // Length of this Data
+ 0x09, // <<Complete Local Name>>
+ 'P',
+ 'h',
+ 'o',
+ 'n',
+ 'e',
+ 0x05, // Length of this Data
+ 0x03, // <<Complete list of 16-bit Service UUIDs>>
+ 0x15,
+ 0x11, // PANU service class UUID
+ 0x1F,
+ 0x11, // Hands-free Audio Gateway service class UUID
+ 0x01, // Length of this data
+ 0x05, // <<Complete list of 32-bit Service UUIDs>>
+ 0x11, // Length of this data
+ 0x07, // <<Complete list of 128-bit Service UUIDs>>
+ 0x01,
+ 0x02,
+ 0x03,
+ 0x04,
+ 0x05,
+ 0x06,
+ 0x07,
+ 0x08, // Made up UUID
+ 0x11,
+ 0x12,
+ 0x13,
+ 0x14,
+ 0x15,
+ 0x16,
+ 0x17,
+ 0x18, //
+ 0x00 // End of Data (Not transmitted over the air
+ };
+
// This is an example of advertising data with AD types
public static byte[] adv_1 = {
0x02, // Length of this Data
@@ -138,4 +183,46 @@
0x00
};
+ // An Eddystone UID frame. go/eddystone for more info
+ public static byte[] eddystone_header_and_uuid = {
+ // BLE Flags
+ (byte) 0x02,
+ (byte) 0x01,
+ (byte) 0x06,
+ // Service UUID
+ (byte) 0x03,
+ (byte) 0x03,
+ (byte) 0xaa,
+ (byte) 0xfe,
+ // Service data header
+ (byte) 0x17,
+ (byte) 0x16,
+ (byte) 0xaa,
+ (byte) 0xfe,
+ // Eddystone frame type
+ (byte) 0x00,
+ // Ranging data
+ (byte) 0xb3,
+ // Eddystone ID namespace
+ (byte) 0x0a,
+ (byte) 0x09,
+ (byte) 0x08,
+ (byte) 0x07,
+ (byte) 0x06,
+ (byte) 0x05,
+ (byte) 0x04,
+ (byte) 0x03,
+ (byte) 0x02,
+ (byte) 0x01,
+ // Eddystone ID instance
+ (byte) 0x16,
+ (byte) 0x15,
+ (byte) 0x14,
+ (byte) 0x13,
+ (byte) 0x12,
+ (byte) 0x11,
+ // RFU
+ (byte) 0x00,
+ (byte) 0x00
+ };
}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Bytes.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Bytes.java
index 637cd03..f6e77e6 100644
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Bytes.java
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Bytes.java
@@ -17,6 +17,7 @@
package com.android.server.nearby.common.bluetooth.fastpair;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@@ -51,7 +52,8 @@
return this.mByteOrder.equals(byteOrder) ? getBytes() : reverse(getBytes());
}
- private static byte[] reverse(byte[] bytes) {
+ @VisibleForTesting
+ static byte[] reverse(byte[] bytes) {
byte[] reversedBytes = new byte[bytes.length];
for (int i = 0; i < bytes.length; i++) {
reversedBytes[i] = bytes[bytes.length - i - 1];
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Event.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Event.java
index 0b50dfd..7a0548b 100644
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Event.java
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Event.java
@@ -133,14 +133,11 @@
Event that = (Event) o;
return this.mEventCode == that.getEventCode()
&& this.mTimestamp == that.getTimestamp()
- && (this.mProfile == null
- ? that.getProfile() == null : this.mProfile.equals(that.getProfile()))
&& (this.mBluetoothDevice == null
- ? that.getBluetoothDevice() == null :
- this.mBluetoothDevice.equals(that.getBluetoothDevice()))
- && (this.mException == null
- ? that.getException() == null :
- this.mException.equals(that.getException()));
+ ? that.getBluetoothDevice() == null :
+ this.mBluetoothDevice.equals(that.getBluetoothDevice()))
+ && (this.mProfile == null
+ ? that.getProfile() == null : this.mProfile.equals(that.getProfile()));
}
return false;
}
@@ -150,7 +147,6 @@
return Objects.hash(mEventCode, mTimestamp, mProfile, mBluetoothDevice, mException);
}
-
/**
* Builder
*/
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairDualConnection.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairDualConnection.java
index 789ef59..60afa31 100644
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairDualConnection.java
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/FastPairDualConnection.java
@@ -1993,7 +1993,8 @@
return getLeState(bluetoothAdapter);
}
- private static int getLeState(android.bluetooth.BluetoothAdapter adapter) {
+ @VisibleForTesting
+ static int getLeState(android.bluetooth.BluetoothAdapter adapter) {
try {
return (Integer) Reflect.on(adapter).withMethod("getLeState").get();
} catch (ReflectionException e) {
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Preferences.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Preferences.java
index bb7b71b..eb5bad5 100644
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Preferences.java
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/fastpair/Preferences.java
@@ -1042,104 +1042,6 @@
*/
public static Builder builder() {
return new Preferences.Builder()
- .setGattOperationTimeoutSeconds(3)
- .setGattConnectionTimeoutSeconds(15)
- .setBluetoothToggleTimeoutSeconds(10)
- .setBluetoothToggleSleepSeconds(2)
- .setClassicDiscoveryTimeoutSeconds(10)
- .setNumDiscoverAttempts(3)
- .setDiscoveryRetrySleepSeconds(1)
- .setIgnoreDiscoveryError(false)
- .setSdpTimeoutSeconds(10)
- .setNumSdpAttempts(3)
- .setNumCreateBondAttempts(3)
- .setNumConnectAttempts(1)
- .setNumWriteAccountKeyAttempts(3)
- .setToggleBluetoothOnFailure(false)
- .setBluetoothStateUsesPolling(true)
- .setBluetoothStatePollingMillis(1000)
- .setNumAttempts(2)
- .setEnableBrEdrHandover(false)
- .setBrHandoverDataCharacteristicId(get16BitUuid(
- Constants.TransportDiscoveryService.BrHandoverDataCharacteristic.ID))
- .setBluetoothSigDataCharacteristicId(get16BitUuid(
- Constants.TransportDiscoveryService.BluetoothSigDataCharacteristic.ID))
- .setFirmwareVersionCharacteristicId(get16BitUuid(FirmwareVersionCharacteristic.ID))
- .setBrTransportBlockDataDescriptorId(
- get16BitUuid(
- Constants.TransportDiscoveryService.BluetoothSigDataCharacteristic
- .BrTransportBlockDataDescriptor.ID))
- .setWaitForUuidsAfterBonding(true)
- .setReceiveUuidsAndBondedEventBeforeClose(true)
- .setRemoveBondTimeoutSeconds(5)
- .setRemoveBondSleepMillis(1000)
- .setCreateBondTimeoutSeconds(15)
- .setHidCreateBondTimeoutSeconds(40)
- .setProxyTimeoutSeconds(2)
- .setRejectPhonebookAccess(false)
- .setRejectMessageAccess(false)
- .setRejectSimAccess(false)
- .setAcceptPasskey(true)
- .setSupportedProfileUuids(Constants.getSupportedProfiles())
- .setWriteAccountKeySleepMillis(2000)
- .setProviderInitiatesBondingIfSupported(false)
- .setAttemptDirectConnectionWhenPreviouslyBonded(false)
- .setAutomaticallyReconnectGattWhenNeeded(false)
- .setSkipDisconnectingGattBeforeWritingAccountKey(false)
- .setSkipConnectingProfiles(false)
- .setIgnoreUuidTimeoutAfterBonded(false)
- .setSpecifyCreateBondTransportType(false)
- .setCreateBondTransportType(0 /*BluetoothDevice.TRANSPORT_AUTO*/)
- .setIncreaseIntentFilterPriority(true)
- .setEvaluatePerformance(false)
- .setKeepSameAccountKeyWrite(true)
- .setEnableNamingCharacteristic(false)
- .setEnableFirmwareVersionCharacteristic(false)
- .setIsRetroactivePairing(false)
- .setNumSdpAttemptsAfterBonded(1)
- .setSupportHidDevice(false)
- .setEnablePairingWhileDirectlyConnecting(true)
- .setAcceptConsentForFastPairOne(true)
- .setGattConnectRetryTimeoutMillis(0)
- .setEnable128BitCustomGattCharacteristicsId(true)
- .setEnableSendExceptionStepToValidator(true)
- .setEnableAdditionalDataTypeWhenActionOverBle(true)
- .setCheckBondStateWhenSkipConnectingProfiles(true)
- .setHandlePasskeyConfirmationByUi(false)
- .setMoreEventLogForQuality(true)
- .setRetryGattConnectionAndSecretHandshake(true)
- .setGattConnectShortTimeoutMs(7000)
- .setGattConnectLongTimeoutMs(15000)
- .setGattConnectShortTimeoutRetryMaxSpentTimeMs(10000)
- .setAddressRotateRetryMaxSpentTimeMs(15000)
- .setPairingRetryDelayMs(100)
- .setSecretHandshakeShortTimeoutMs(3000)
- .setSecretHandshakeLongTimeoutMs(10000)
- .setSecretHandshakeShortTimeoutRetryMaxSpentTimeMs(5000)
- .setSecretHandshakeLongTimeoutRetryMaxSpentTimeMs(7000)
- .setSecretHandshakeRetryAttempts(3)
- .setSecretHandshakeRetryGattConnectionMaxSpentTimeMs(15000)
- .setSignalLostRetryMaxSpentTimeMs(15000)
- .setGattConnectionAndSecretHandshakeNoRetryGattError(ImmutableSet.of())
- .setRetrySecretHandshakeTimeout(false)
- .setLogUserManualRetry(true)
- .setPairFailureCounts(0)
- .setEnablePairFlowShowUiWithoutProfileConnection(true)
- .setPairFailureCounts(0)
- .setLogPairWithCachedModelId(true)
- .setDirectConnectProfileIfModelIdInCache(false)
- .setCachedDeviceAddress("")
- .setPossibleCachedDeviceAddress("")
- .setSameModelIdPairedDeviceCount(0)
- .setIsDeviceFinishCheckAddressFromCache(true);
- }
-
- /**
- * Constructs a builder from GmsLog.
- */
- // TODO(b/206668142): remove this builder once api is ready.
- public static Builder builderFromGmsLog() {
- return new Preferences.Builder()
.setGattOperationTimeoutSeconds(10)
.setGattConnectionTimeoutSeconds(15)
.setBluetoothToggleTimeoutSeconds(10)
@@ -1158,10 +1060,15 @@
.setBluetoothStatePollingMillis(1000)
.setNumAttempts(2)
.setEnableBrEdrHandover(false)
- .setBrHandoverDataCharacteristicId((short) 11265)
- .setBluetoothSigDataCharacteristicId((short) 11266)
- .setFirmwareVersionCharacteristicId((short) 10790)
- .setBrTransportBlockDataDescriptorId((short) 11267)
+ .setBrHandoverDataCharacteristicId(get16BitUuid(
+ Constants.TransportDiscoveryService.BrHandoverDataCharacteristic.ID))
+ .setBluetoothSigDataCharacteristicId(get16BitUuid(
+ Constants.TransportDiscoveryService.BluetoothSigDataCharacteristic.ID))
+ .setFirmwareVersionCharacteristicId(get16BitUuid(FirmwareVersionCharacteristic.ID))
+ .setBrTransportBlockDataDescriptorId(
+ get16BitUuid(
+ Constants.TransportDiscoveryService.BluetoothSigDataCharacteristic
+ .BrTransportBlockDataDescriptor.ID))
.setWaitForUuidsAfterBonding(true)
.setReceiveUuidsAndBondedEventBeforeClose(true)
.setRemoveBondTimeoutSeconds(5)
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothDevice.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothDevice.java
index 5b45f61..b2002c5 100644
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothDevice.java
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothDevice.java
@@ -187,12 +187,6 @@
return mWrappedBluetoothDevice.createInsecureRfcommSocketToServiceRecord(uuid);
}
- /** See {@link android.bluetooth.BluetoothDevice#setPin(byte[])}. */
- @TargetApi(19)
- public boolean setPairingConfirmation(byte[] pin) {
- return mWrappedBluetoothDevice.setPin(pin);
- }
-
/** See {@link android.bluetooth.BluetoothDevice#setPairingConfirmation(boolean)}. */
public boolean setPairingConfirmation(boolean confirm) {
return mWrappedBluetoothDevice.setPairingConfirmation(confirm);
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServer.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServer.java
index 3f6f361..d4873fd 100644
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServer.java
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServer.java
@@ -51,6 +51,11 @@
return new BluetoothGattServer(instance);
}
+ /** Unwraps a Bluetooth Gatt server. */
+ public android.bluetooth.BluetoothGattServer unwrap() {
+ return mWrappedInstance;
+ }
+
/**
* See {@link android.bluetooth.BluetoothGattServer#connect(
* android.bluetooth.BluetoothDevice, boolean)}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeAdvertiser.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeAdvertiser.java
index 6fe4432..b2c61ab 100644
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeAdvertiser.java
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeAdvertiser.java
@@ -71,4 +71,10 @@
}
return new BluetoothLeAdvertiser(bluetoothLeAdvertiser);
}
+
+ /** Unwraps a Bluetooth LE advertiser. */
+ @Nullable
+ public android.bluetooth.le.BluetoothLeAdvertiser unwrap() {
+ return mWrappedInstance;
+ }
}
diff --git a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeScanner.java b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeScanner.java
index 8a13abe..9b3447e 100644
--- a/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeScanner.java
+++ b/nearby/service/java/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeScanner.java
@@ -77,6 +77,12 @@
mWrappedBluetoothLeScanner.stopScan(callbackIntent);
}
+ /** Unwraps a Bluetooth LE scanner. */
+ @Nullable
+ public android.bluetooth.le.BluetoothLeScanner unwrap() {
+ return mWrappedBluetoothLeScanner;
+ }
+
/** Wraps a Bluetooth LE scanner. */
@Nullable
public static BluetoothLeScanner wrap(
diff --git a/nearby/service/java/com/android/server/nearby/common/locator/Locator.java b/nearby/service/java/com/android/server/nearby/common/locator/Locator.java
index f8b43a6..2003335 100644
--- a/nearby/service/java/com/android/server/nearby/common/locator/Locator.java
+++ b/nearby/service/java/com/android/server/nearby/common/locator/Locator.java
@@ -110,7 +110,8 @@
throw new IllegalStateException(errorMessage);
}
- private String getUnboundErrorMessage(Class<?> type) {
+ @VisibleForTesting
+ String getUnboundErrorMessage(Class<?> type) {
StringBuilder sb = new StringBuilder();
sb.append("Unbound type: ").append(type.getName()).append("\n").append(
"Searched locators:\n");
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/FastPairAdvHandler.java b/nearby/service/java/com/android/server/nearby/fastpair/FastPairAdvHandler.java
index 2ecce47..d459329 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/FastPairAdvHandler.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/FastPairAdvHandler.java
@@ -31,10 +31,7 @@
import com.android.server.nearby.common.ble.decode.FastPairDecoder;
import com.android.server.nearby.common.ble.util.RangingUtils;
import com.android.server.nearby.common.bloomfilter.BloomFilter;
-import com.android.server.nearby.common.bloomfilter.FastPairBloomFilterHasher;
import com.android.server.nearby.common.locator.Locator;
-import com.android.server.nearby.fastpair.cache.DiscoveryItem;
-import com.android.server.nearby.fastpair.cache.FastPairCacheManager;
import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
import com.android.server.nearby.provider.FastPairDataProvider;
import com.android.server.nearby.util.DataUtils;
@@ -120,7 +117,7 @@
Log.e(TAG, "OEM does not construct fast pair data proxy correctly");
}
} else {
- // Start to process bloom filter
+ // Start to process bloom filter. Yet to finish.
try {
List<Account> accountList = mPairDataProvider.loadFastPairEligibleAccounts();
byte[] bloomFilterByteArray = FastPairDecoder
@@ -130,73 +127,9 @@
if (bloomFilterByteArray == null || bloomFilterByteArray.length == 0) {
return;
}
- for (Account account : accountList) {
- List<Data.FastPairDeviceWithAccountKey> listDevices =
- mPairDataProvider.loadFastPairDeviceWithAccountKey(account);
- Data.FastPairDeviceWithAccountKey recognizedDevice =
- findRecognizedDevice(listDevices,
- new BloomFilter(bloomFilterByteArray,
- new FastPairBloomFilterHasher()), bloomFilterSalt);
-
- if (recognizedDevice != null) {
- Log.d(TAG, "find matched device show notification to remind"
- + " user to pair");
- // Check the distance of the device if the distance is larger than the
- // threshold
- // do not show half sheet.
- if (!isNearby(fastPairDevice.getRssi(),
- recognizedDevice.getDiscoveryItem().getTxPower() == 0
- ? fastPairDevice.getTxPower()
- : recognizedDevice.getDiscoveryItem().getTxPower())) {
- return;
- }
- // Check if the device is already paired
- List<Cache.StoredFastPairItem> storedFastPairItemList =
- Locator.get(mContext, FastPairCacheManager.class)
- .getAllSavedStoredFastPairItem();
- Cache.StoredFastPairItem recognizedStoredFastPairItem =
- findRecognizedDeviceFromCachedItem(storedFastPairItemList,
- new BloomFilter(bloomFilterByteArray,
- new FastPairBloomFilterHasher()), bloomFilterSalt);
- if (recognizedStoredFastPairItem != null) {
- // The bloomfilter is recognized in the cache so the device is paired
- // before
- Log.d(TAG, "bloom filter is recognized in the cache");
- continue;
- } else {
- Log.d(TAG, "bloom filter is recognized not paired before should"
- + "show subsequent pairing notification");
- if (mIsFirst) {
- mIsFirst = false;
- // Get full info from api the initial request will only return
- // part of the info due to size limit.
- List<Data.FastPairDeviceWithAccountKey> resList =
- mPairDataProvider.loadFastPairDeviceWithAccountKey(account,
- List.of(recognizedDevice.getAccountKey()
- .toByteArray()));
- if (resList != null && resList.size() > 0) {
- //Saved device from footprint does not have ble address so
- // fill ble address with current scan result.
- Cache.StoredDiscoveryItem storedDiscoveryItem =
- resList.get(0).getDiscoveryItem().toBuilder()
- .setMacAddress(
- fastPairDevice.getBluetoothAddress())
- .build();
- Locator.get(mContext, FastPairController.class).pair(
- new DiscoveryItem(mContext, storedDiscoveryItem),
- resList.get(0).getAccountKey().toByteArray(),
- /** companionApp=*/null);
- }
- }
- }
-
- return;
- }
- }
} catch (IllegalStateException e) {
Log.e(TAG, "OEM does not construct fast pair data proxy correctly");
}
-
}
}
@@ -249,5 +182,4 @@
boolean isNearby(int rssi, int txPower) {
return RangingUtils.distanceFromRssiAndTxPower(rssi, txPower) < NEARBY_DISTANCE_THRESHOLD;
}
-
}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/FastPairManager.java b/nearby/service/java/com/android/server/nearby/fastpair/FastPairManager.java
index f368080..e3de4e2 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/FastPairManager.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/FastPairManager.java
@@ -242,7 +242,7 @@
String modelId = item.getTriggerId();
Preferences.Builder prefsBuilder =
- Preferences.builderFromGmsLog()
+ Preferences.builder()
.setEnableBrEdrHandover(false)
.setIgnoreDiscoveryError(true);
pairingProgressHandlerBase.onSetupPreferencesBuilder(prefsBuilder);
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/cache/DiscoveryItem.java b/nearby/service/java/com/android/server/nearby/fastpair/cache/DiscoveryItem.java
index 6065f99..5ce4488 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/cache/DiscoveryItem.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/cache/DiscoveryItem.java
@@ -28,6 +28,7 @@
import android.text.TextUtils;
import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.nearby.common.ble.util.RangingUtils;
import com.android.server.nearby.common.fastpair.IconUtils;
import com.android.server.nearby.common.locator.Locator;
@@ -106,15 +107,6 @@
}
/**
- * Sets the store discovery item mac address.
- */
- public void setMacAddress(String address) {
- mStoredDiscoveryItem = mStoredDiscoveryItem.toBuilder().setMacAddress(address).build();
-
- mFastPairCacheManager.saveDiscoveryItem(this);
- }
-
- /**
* Checks if the item is expired. Expired items are those over getItemExpirationMillis() eg. 2
* minutes
*/
@@ -295,7 +287,8 @@
* Returns the app name of discovery item.
*/
@Nullable
- private String getAppName() {
+ @VisibleForTesting
+ protected String getAppName() {
return mStoredDiscoveryItem.getAppName();
}
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/cache/FastPairCacheManager.java b/nearby/service/java/com/android/server/nearby/fastpair/cache/FastPairCacheManager.java
index b840091..c6134f5 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/cache/FastPairCacheManager.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/cache/FastPairCacheManager.java
@@ -64,16 +64,6 @@
}
/**
- * Checks if the entry can be auto deleted from the cache
- */
- public boolean isDeletable(Cache.ServerResponseDbItem entry) {
- if (!entry.getExpirable()) {
- return false;
- }
- return true;
- }
-
- /**
* Save discovery item into database. Discovery item is item that discovered through Ble before
* pairing success.
*/
diff --git a/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBase.java b/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBase.java
index ccd7e5e..5fb05d5 100644
--- a/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBase.java
+++ b/nearby/service/java/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBase.java
@@ -24,6 +24,7 @@
import android.content.Context;
import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.nearby.common.bluetooth.fastpair.FastPairConnection;
import com.android.server.nearby.common.bluetooth.fastpair.Preferences;
import com.android.server.nearby.fastpair.cache.DiscoveryItem;
@@ -184,7 +185,8 @@
+ maskBluetoothAddress(address));
}
- private static void optInFootprintsForInitialPairing(
+ @VisibleForTesting
+ static void optInFootprintsForInitialPairing(
FootprintsDeviceManager footprints,
DiscoveryItem item,
byte[] accountKey,
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 57784a9..3152ee6 100644
--- a/nearby/service/java/com/android/server/nearby/injector/Injector.java
+++ b/nearby/service/java/com/android/server/nearby/injector/Injector.java
@@ -18,6 +18,7 @@
import android.app.AppOpsManager;
import android.bluetooth.BluetoothAdapter;
+import android.hardware.location.ContextHubManager;
/**
* Nearby dependency injector. To be used for accessing various Nearby class instances and as a
@@ -29,7 +30,7 @@
BluetoothAdapter getBluetoothAdapter();
/** Get the ContextHubManagerAdapter for ChreDiscoveryProvider to scan. */
- ContextHubManagerAdapter getContextHubManagerAdapter();
+ ContextHubManager getContextHubManager();
/** Get the AppOpsManager to control access. */
AppOpsManager getAppOpsManager();
diff --git a/nearby/service/java/com/android/server/nearby/presence/Advertisement.java b/nearby/service/java/com/android/server/nearby/presence/Advertisement.java
new file mode 100644
index 0000000..d42f6c7
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/presence/Advertisement.java
@@ -0,0 +1,74 @@
+/*
+ * 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.nearby.BroadcastRequest;
+import android.nearby.PresenceCredential;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** A Nearby Presence advertisement to be advertised. */
+public abstract class Advertisement {
+
+ @BroadcastRequest.BroadcastVersion
+ int mVersion = BroadcastRequest.PRESENCE_VERSION_UNKNOWN;
+ int mLength;
+ @PresenceCredential.IdentityType int mIdentityType;
+ byte[] mIdentity;
+ byte[] mSalt;
+ List<Integer> mActions;
+
+ /** Serialize an {@link Advertisement} object into bytes. */
+ @Nullable
+ public byte[] toBytes() {
+ return new byte[0];
+ }
+
+ /** Returns the length of the advertisement. */
+ public int getLength() {
+ return mLength;
+ }
+
+ /** Returns the version in the advertisement. */
+ @BroadcastRequest.BroadcastVersion
+ public int getVersion() {
+ return mVersion;
+ }
+
+ /** Returns the identity type in the advertisement. */
+ @PresenceCredential.IdentityType
+ public int getIdentityType() {
+ return mIdentityType;
+ }
+
+ /** Returns the identity bytes in the advertisement. */
+ public byte[] getIdentity() {
+ return mIdentity.clone();
+ }
+
+ /** Returns the salt of the advertisement. */
+ public byte[] getSalt() {
+ return mSalt.clone();
+ }
+
+ /** Returns the actions in the advertisement. */
+ public List<Integer> getActions() {
+ return new ArrayList<>(mActions);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/presence/DataElementHeader.java b/nearby/service/java/com/android/server/nearby/presence/DataElementHeader.java
new file mode 100644
index 0000000..ae4a728
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/presence/DataElementHeader.java
@@ -0,0 +1,266 @@
+/*
+ * 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.nearby.BroadcastRequest;
+import android.nearby.DataElement;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import javax.annotation.Nonnull;
+
+/**
+ * Represents a data element header in Nearby Presence.
+ * Each header has 3 parts: tag, length and style.
+ * Tag: 1 bit (MSB at each byte). 1 for extending, which means there will be more bytes after
+ * the current one for the header.
+ * Length: The total length of a Data Element field. Length is up to 127 and is limited within
+ * the entire first byte in the header. (7 bits, MSB is the tag).
+ * Type: Represents {@link DataElement.DataType}. There is no limit for the type number.
+ *
+ * @hide
+ */
+public class DataElementHeader {
+ // Each Data reserved MSB for tag.
+ static final int TAG_BITMASK = 0b10000000;
+ static final int TAG_OFFSET = 7;
+
+ // If the header is only 1 byte, it has the format: 0b0LLLTTTT. (L for length, T for type.)
+ static final int SINGLE_AVAILABLE_LENGTH_BIT = 3;
+ static final int SINGLE_AVAILABLE_TYPE_BIT = 4;
+ static final int SINGLE_LENGTH_BITMASK = 0b01110000;
+ static final int SINGLE_LENGTH_OFFSET = SINGLE_AVAILABLE_TYPE_BIT;
+ static final int SINGLE_TYPE_BITMASK = 0b00001111;
+
+ // If there are multiple data element headers.
+ // First byte is always the length.
+ static final int MULTIPLE_LENGTH_BYTE = 1;
+ // Each byte reserves MSB for tag.
+ static final int MULTIPLE_BITMASK = 0b01111111;
+
+ @BroadcastRequest.BroadcastVersion
+ private final int mVersion;
+ @DataElement.DataType
+ private final int mDataType;
+ private final int mDataLength;
+
+ DataElementHeader(@BroadcastRequest.BroadcastVersion int version,
+ @DataElement.DataType int dataType, int dataLength) {
+ Preconditions.checkArgument(version == BroadcastRequest.PRESENCE_VERSION_V1,
+ "DataElementHeader is only supported in V1.");
+ Preconditions.checkArgument(dataLength >= 0, "Length should not be negative.");
+ Preconditions.checkArgument(dataLength < (1 << TAG_OFFSET),
+ "Data element should be equal or shorter than 128.");
+
+ this.mVersion = version;
+ this.mDataType = dataType;
+ this.mDataLength = dataLength;
+ }
+
+ /**
+ * The total type of the data element.
+ */
+ @DataElement.DataType
+ public int getDataType() {
+ return mDataType;
+ }
+
+ /**
+ * The total length of a Data Element field.
+ */
+ public int getDataLength() {
+ return mDataLength;
+ }
+
+ /** Serialize a {@link DataElementHeader} object into bytes. */
+ public byte[] toBytes() {
+ Preconditions.checkState(mVersion == BroadcastRequest.PRESENCE_VERSION_V1,
+ "DataElementHeader is only supported in V1.");
+ // Only 1 byte needed for the header
+ if (mDataType < (1 << SINGLE_AVAILABLE_TYPE_BIT)
+ && mDataLength < (1 << SINGLE_AVAILABLE_LENGTH_BIT)) {
+ return new byte[]{createSingleByteHeader(mDataType, mDataLength)};
+ }
+
+ return createMultipleBytesHeader(mDataType, mDataLength);
+ }
+
+ /** Creates a {@link DataElementHeader} object from bytes. */
+ @Nullable
+ public static DataElementHeader fromBytes(@BroadcastRequest.BroadcastVersion int version,
+ @Nonnull byte[] bytes) {
+ Objects.requireNonNull(bytes, "Data parsed in for DataElement should not be null.");
+
+ if (bytes.length == 0) {
+ return null;
+ }
+
+ if (bytes.length == 1) {
+ if (isExtending(bytes[0])) {
+ throw new IllegalArgumentException("The header is not complete.");
+ }
+ return new DataElementHeader(BroadcastRequest.PRESENCE_VERSION_V1,
+ getTypeSingleByte(bytes[0]), getLengthSingleByte(bytes[0]));
+ }
+
+ // The first byte should be length and there should be at least 1 more byte following to
+ // represent type.
+ // The last header byte's MSB should be 0.
+ if (!isExtending(bytes[0]) || isExtending(bytes[bytes.length - 1])) {
+ throw new IllegalArgumentException("The header format is wrong.");
+ }
+
+ return new DataElementHeader(version,
+ getTypeMultipleBytes(Arrays.copyOfRange(bytes, 1, bytes.length)),
+ getHeaderValue(bytes[0]));
+ }
+
+ /** Creates a header based on type and length.
+ * This is used when the type is <= 16 and length is <= 7. */
+ static byte createSingleByteHeader(int type, int length) {
+ return (byte) (convertTag(/* extend= */ false)
+ | convertLengthSingleByte(length)
+ | convertTypeSingleByte(type));
+ }
+
+ /** Creates a header based on type and length.
+ * This is used when the type is > 16 or length is > 7. */
+ static byte[] createMultipleBytesHeader(int type, int length) {
+ List<Byte> typeIntList = convertTypeMultipleBytes(type);
+ byte[] res = new byte[typeIntList.size() + MULTIPLE_LENGTH_BYTE];
+ int index = 0;
+ res[index++] = convertLengthMultipleBytes(length);
+
+ for (int typeInt : typeIntList) {
+ res[index++] = (byte) typeInt;
+ }
+ return res;
+ }
+
+ /** Constructs a Data Element header with length indicated in byte format.
+ * The most significant bit is the tag, 2- 4 bits are the length, 5 - 8 bits are the type.
+ */
+ @VisibleForTesting
+ static int convertLengthSingleByte(int length) {
+ Preconditions.checkArgument(length >= 0, "Length should not be negative.");
+ Preconditions.checkArgument(length < (1 << SINGLE_AVAILABLE_LENGTH_BIT),
+ "In single Data Element header, length should be shorter than 8.");
+ return (length << SINGLE_LENGTH_OFFSET) & SINGLE_LENGTH_BITMASK;
+ }
+
+ /** Constructs a Data Element header with type indicated in byte format.
+ * The most significant bit is the tag, 2- 4 bits are the length, 5 - 8 bits are the type.
+ */
+ @VisibleForTesting
+ static int convertTypeSingleByte(int type) {
+ Preconditions.checkArgument(type >= 0, "Type should not be negative.");
+ Preconditions.checkArgument(type < (1 << SINGLE_AVAILABLE_TYPE_BIT),
+ "In single Data Element header, type should be smaller than 16.");
+
+ return type & SINGLE_TYPE_BITMASK;
+ }
+
+ /**
+ * Gets the length of Data Element from the header. (When there is only 1 byte of header)
+ */
+ static int getLengthSingleByte(byte header) {
+ Preconditions.checkArgument(!isExtending(header),
+ "Cannot apply this method for the extending header.");
+ return (header & SINGLE_LENGTH_BITMASK) >> SINGLE_LENGTH_OFFSET;
+ }
+
+ /**
+ * Gets the type of Data Element from the header. (When there is only 1 byte of header)
+ */
+ static int getTypeSingleByte(byte header) {
+ Preconditions.checkArgument(!isExtending(header),
+ "Cannot apply this method for the extending header.");
+ return header & SINGLE_TYPE_BITMASK;
+ }
+
+ /** Creates a DE(data element) header based on length.
+ * This is used when header is more than 1 byte. The first byte is always the length.
+ */
+ static byte convertLengthMultipleBytes(int length) {
+ Preconditions.checkArgument(length < (1 << TAG_OFFSET),
+ "Data element should be equal or shorter than 128.");
+ return (byte) (convertTag(/* extend= */ true) | (length & MULTIPLE_BITMASK));
+ }
+
+ /** Creates a DE(data element) header based on type.
+ * This is used when header is more than 1 byte. The first byte is always the length.
+ */
+ @VisibleForTesting
+ static List<Byte> convertTypeMultipleBytes(int type) {
+ List<Byte> typeBytes = new ArrayList<>();
+ while (type > 0) {
+ byte current = (byte) (type & MULTIPLE_BITMASK);
+ type = type >> TAG_OFFSET;
+ typeBytes.add(current);
+ }
+
+ Collections.reverse(typeBytes);
+ int size = typeBytes.size();
+ // The last byte's MSB should be 0.
+ for (int i = 0; i < size - 1; i++) {
+ typeBytes.set(i, (byte) (convertTag(/* extend= */ true) | typeBytes.get(i)));
+ }
+ return typeBytes;
+ }
+
+ /** Creates a DE(data element) header based on type.
+ * This is used when header is more than 1 byte. The first byte is always the length.
+ * Uses Integer when doing bit operation to avoid error.
+ */
+ @VisibleForTesting
+ static int getTypeMultipleBytes(byte[] typeByteArray) {
+ int type = 0;
+ int size = typeByteArray.length;
+ for (int i = 0; i < size; i++) {
+ type = (type << TAG_OFFSET) | getHeaderValue(typeByteArray[i]);
+ }
+ return type;
+ }
+
+ /** Gets the integer value of the 7 bits in the header. (The MSB is tag) */
+ @VisibleForTesting
+ static int getHeaderValue(byte header) {
+ return (header & MULTIPLE_BITMASK);
+ }
+
+ /** Sets the MSB of the header byte. If this is the last byte of headers, MSB is 0.
+ * If there are at least header following, the MSB is 1.
+ */
+ @VisibleForTesting
+ static byte convertTag(boolean extend) {
+ return (byte) (extend ? 0b10000000 : 0b00000000);
+ }
+
+ /** Returns {@code true} if there are at least 1 byte of header after the current one. */
+ @VisibleForTesting
+ static boolean isExtending(byte header) {
+ return (header & TAG_BITMASK) != 0;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisement.java b/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisement.java
new file mode 100644
index 0000000..34a7514
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisement.java
@@ -0,0 +1,409 @@
+/*
+ * 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.NearbyService.TAG;
+
+import android.annotation.Nullable;
+import android.nearby.BroadcastRequest;
+import android.nearby.DataElement;
+import android.nearby.PresenceBroadcastRequest;
+import android.nearby.PresenceCredential;
+import android.nearby.PublicCredential;
+import android.util.Log;
+
+import com.android.server.nearby.util.encryption.Cryptor;
+import com.android.server.nearby.util.encryption.CryptorImpFake;
+import com.android.server.nearby.util.encryption.CryptorImpIdentityV1;
+import com.android.server.nearby.util.encryption.CryptorImpV1;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A Nearby Presence advertisement to be advertised on BT5.0 devices.
+ *
+ * <p>Serializable between Java object and bytes formats. Java object is used at the upper scanning
+ * and advertising interface as an abstraction of the actual bytes. Bytes format is used at the
+ * underlying BLE and mDNS stacks, which do necessary slicing and merging based on advertising
+ * capacities.
+ *
+ * The extended advertisement is defined in the format below:
+ * Header (1 byte) | salt (1+2 bytes) | Identity + filter (2+16 bytes)
+ * | repeated DE fields (various bytes)
+ * The header contains:
+ * version (3 bits) | 5 bit reserved for future use (RFU)
+ */
+public class ExtendedAdvertisement extends Advertisement{
+
+ public static final int SALT_DATA_LENGTH = 2;
+
+ static final int HEADER_LENGTH = 1;
+
+ static final int IDENTITY_DATA_LENGTH = 16;
+
+ private final List<DataElement> mDataElements;
+
+ private final byte[] mAuthenticityKey;
+
+ // All Data Elements including salt and identity.
+ // Each list item (byte array) is a Data Element (with its header).
+ private final List<byte[]> mCompleteDataElementsBytes;
+ // Signature generated from data elements.
+ private final byte[] mHmacTag;
+
+ /**
+ * Creates an {@link ExtendedAdvertisement} from a Presence Broadcast Request.
+ * @return {@link ExtendedAdvertisement} object. {@code null} when the request is illegal.
+ */
+ @Nullable
+ public static ExtendedAdvertisement createFromRequest(PresenceBroadcastRequest request) {
+ if (request.getVersion() != BroadcastRequest.PRESENCE_VERSION_V1) {
+ Log.v(TAG, "ExtendedAdvertisement only supports V1 now.");
+ return null;
+ }
+
+ byte[] salt = request.getSalt();
+ if (salt.length != SALT_DATA_LENGTH) {
+ Log.v(TAG, "Salt does not match correct length");
+ return null;
+ }
+
+ byte[] identity = request.getCredential().getMetadataEncryptionKey();
+ byte[] authenticityKey = request.getCredential().getAuthenticityKey();
+ if (identity.length != IDENTITY_DATA_LENGTH) {
+ Log.v(TAG, "Identity does not match correct length");
+ return null;
+ }
+
+ List<Integer> actions = request.getActions();
+ if (actions.isEmpty()) {
+ Log.v(TAG, "ExtendedAdvertisement must contain at least one action");
+ return null;
+ }
+
+ List<DataElement> dataElements = request.getExtendedProperties();
+ return new ExtendedAdvertisement(
+ request.getCredential().getIdentityType(),
+ identity,
+ salt,
+ authenticityKey,
+ actions,
+ dataElements);
+ }
+
+ /** Serialize an {@link ExtendedAdvertisement} object into bytes with {@link DataElement}s */
+ @Nullable
+ public byte[] toBytes() {
+ ByteBuffer buffer = ByteBuffer.allocate(getLength());
+
+ // Header
+ buffer.put(ExtendedAdvertisementUtils.constructHeader(getVersion()));
+
+ // Salt
+ buffer.put(mCompleteDataElementsBytes.get(0));
+
+ // Identity
+ buffer.put(mCompleteDataElementsBytes.get(1));
+
+ List<Byte> rawDataBytes = new ArrayList<>();
+ // Data Elements (Already includes salt and identity)
+ for (int i = 2; i < mCompleteDataElementsBytes.size(); i++) {
+ byte[] dataElementBytes = mCompleteDataElementsBytes.get(i);
+ for (Byte b : dataElementBytes) {
+ rawDataBytes.add(b);
+ }
+ }
+
+ byte[] dataElements = new byte[rawDataBytes.size()];
+ for (int i = 0; i < rawDataBytes.size(); i++) {
+ dataElements[i] = rawDataBytes.get(i);
+ }
+
+ buffer.put(
+ getCryptor(/* encrypt= */ true).encrypt(dataElements, getSalt(), mAuthenticityKey));
+
+ buffer.put(mHmacTag);
+
+ return buffer.array();
+ }
+
+ /** Deserialize from bytes into an {@link ExtendedAdvertisement} object.
+ * {@code null} when there is something when parsing.
+ */
+ @Nullable
+ public static ExtendedAdvertisement fromBytes(byte[] bytes, PublicCredential publicCredential) {
+ @BroadcastRequest.BroadcastVersion
+ int version = ExtendedAdvertisementUtils.getVersion(bytes);
+ if (version != PresenceBroadcastRequest.PRESENCE_VERSION_V1) {
+ Log.v(TAG, "ExtendedAdvertisement is used in V1 only and version is " + version);
+ return null;
+ }
+
+ byte[] authenticityKey = publicCredential.getAuthenticityKey();
+
+ int index = HEADER_LENGTH;
+ // Salt
+ byte[] saltHeaderArray = ExtendedAdvertisementUtils.getDataElementHeader(bytes, index);
+ DataElementHeader saltHeader = DataElementHeader.fromBytes(version, saltHeaderArray);
+ if (saltHeader == null || saltHeader.getDataType() != DataElement.DataType.SALT) {
+ Log.v(TAG, "First data element has to be salt.");
+ return null;
+ }
+ index += saltHeaderArray.length;
+ byte[] salt = new byte[saltHeader.getDataLength()];
+ for (int i = 0; i < saltHeader.getDataLength(); i++) {
+ salt[i] = bytes[index++];
+ }
+
+ // Identity
+ byte[] identityHeaderArray = ExtendedAdvertisementUtils.getDataElementHeader(bytes, index);
+ DataElementHeader identityHeader =
+ DataElementHeader.fromBytes(version, identityHeaderArray);
+ if (identityHeader == null) {
+ Log.v(TAG, "The second element has to be identity.");
+ return null;
+ }
+ index += identityHeaderArray.length;
+ @PresenceCredential.IdentityType int identityType =
+ toPresenceCredentialIdentityType(identityHeader.getDataType());
+ if (identityType == PresenceCredential.IDENTITY_TYPE_UNKNOWN) {
+ Log.v(TAG, "The identity type is unknown.");
+ return null;
+ }
+ byte[] encryptedIdentity = new byte[identityHeader.getDataLength()];
+ for (int i = 0; i < identityHeader.getDataLength(); i++) {
+ encryptedIdentity[i] = bytes[index++];
+ }
+ byte[] identity =
+ CryptorImpIdentityV1
+ .getInstance().decrypt(encryptedIdentity, salt, authenticityKey);
+
+ Cryptor cryptor = getCryptor(/* encrypt= */ true);
+ byte[] encryptedDataElements =
+ new byte[bytes.length - index - cryptor.getSignatureLength()];
+ // Decrypt other data elements
+ System.arraycopy(bytes, index, encryptedDataElements, 0, encryptedDataElements.length);
+ byte[] decryptedDataElements =
+ cryptor.decrypt(encryptedDataElements, salt, authenticityKey);
+ if (decryptedDataElements == null) {
+ return null;
+ }
+
+ // Verify the computed HMAC tag is equal to HMAC tag in advertisement
+ if (cryptor.getSignatureLength() > 0) {
+ byte[] expectedHmacTag = new byte[cryptor.getSignatureLength()];
+ System.arraycopy(
+ bytes, bytes.length - cryptor.getSignatureLength(),
+ expectedHmacTag, 0, cryptor.getSignatureLength());
+ if (!cryptor.verify(decryptedDataElements, authenticityKey, expectedHmacTag)) {
+ Log.e(TAG, "HMAC tags not match.");
+ return null;
+ }
+ }
+
+ int dataElementArrayIndex = 0;
+ // Other Data Elements
+ List<Integer> actions = new ArrayList<>();
+ List<DataElement> dataElements = new ArrayList<>();
+ while (dataElementArrayIndex < decryptedDataElements.length) {
+ byte[] deHeaderArray = ExtendedAdvertisementUtils
+ .getDataElementHeader(decryptedDataElements, dataElementArrayIndex);
+ DataElementHeader deHeader = DataElementHeader.fromBytes(version, deHeaderArray);
+ dataElementArrayIndex += deHeaderArray.length;
+
+ @DataElement.DataType int type = Objects.requireNonNull(deHeader).getDataType();
+ if (type == DataElement.DataType.ACTION) {
+ if (deHeader.getDataLength() != 1) {
+ Log.v(TAG, "Action id should only 1 byte.");
+ return null;
+ }
+ actions.add((int) decryptedDataElements[dataElementArrayIndex++]);
+ } else {
+ if (isSaltOrIdentity(type)) {
+ Log.v(TAG, "Type " + type + " is duplicated. There should be only one salt"
+ + " and one identity in the advertisement.");
+ return null;
+ }
+ byte[] deData = new byte[deHeader.getDataLength()];
+ for (int i = 0; i < deHeader.getDataLength(); i++) {
+ deData[i] = decryptedDataElements[dataElementArrayIndex++];
+ }
+ dataElements.add(new DataElement(type, deData));
+ }
+ }
+
+ return new ExtendedAdvertisement(identityType, identity, salt, authenticityKey, actions,
+ dataElements);
+ }
+
+ /** Returns the {@link DataElement}s in the advertisement. */
+ public List<DataElement> getDataElements() {
+ return new ArrayList<>(mDataElements);
+ }
+
+ /** Returns the {@link DataElement}s in the advertisement according to the key. */
+ public List<DataElement> getDataElements(@DataElement.DataType int key) {
+ List<DataElement> res = new ArrayList<>();
+ for (DataElement dataElement : mDataElements) {
+ if (key == dataElement.getKey()) {
+ res.add(dataElement);
+ }
+ }
+ return res;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "ExtendedAdvertisement:"
+ + "<VERSION: %s, length: %s, dataElementCount: %s, identityType: %s,"
+ + " identity: %s, salt: %s, actions: %s>",
+ getVersion(),
+ getLength(),
+ getDataElements().size(),
+ getIdentityType(),
+ Arrays.toString(getIdentity()),
+ Arrays.toString(getSalt()),
+ getActions());
+ }
+
+ ExtendedAdvertisement(
+ @PresenceCredential.IdentityType int identityType,
+ byte[] identity,
+ byte[] salt,
+ byte[] authenticityKey,
+ List<Integer> actions,
+ List<DataElement> dataElements) {
+ this.mVersion = BroadcastRequest.PRESENCE_VERSION_V1;
+ this.mIdentityType = identityType;
+ this.mIdentity = identity;
+ this.mSalt = salt;
+ this.mAuthenticityKey = authenticityKey;
+ this.mActions = actions;
+ this.mDataElements = dataElements;
+ this.mCompleteDataElementsBytes = new ArrayList<>();
+
+ int length = HEADER_LENGTH; // header
+
+ // Salt
+ DataElement saltElement = new DataElement(DataElement.DataType.SALT, salt);
+ byte[] saltByteArray = ExtendedAdvertisementUtils.convertDataElementToBytes(saltElement);
+ mCompleteDataElementsBytes.add(saltByteArray);
+ length += saltByteArray.length;
+
+ // Identity
+ byte[] encryptedIdentity =
+ CryptorImpIdentityV1.getInstance().encrypt(identity, salt, authenticityKey);
+ DataElement identityElement = new DataElement(toDataType(identityType), encryptedIdentity);
+ byte[] identityByteArray =
+ ExtendedAdvertisementUtils.convertDataElementToBytes(identityElement);
+ mCompleteDataElementsBytes.add(identityByteArray);
+ length += identityByteArray.length;
+
+ List<Byte> dataElementBytes = new ArrayList<>();
+ // Intents
+ for (int action : mActions) {
+ DataElement actionElement = new DataElement(DataElement.DataType.ACTION,
+ new byte[] {(byte) action});
+ byte[] intentByteArray =
+ ExtendedAdvertisementUtils.convertDataElementToBytes(actionElement);
+ mCompleteDataElementsBytes.add(intentByteArray);
+ for (Byte b : intentByteArray) {
+ dataElementBytes.add(b);
+ }
+ }
+
+ // Data Elements (Extended properties)
+ for (DataElement dataElement : mDataElements) {
+ byte[] deByteArray = ExtendedAdvertisementUtils.convertDataElementToBytes(dataElement);
+ mCompleteDataElementsBytes.add(deByteArray);
+ for (Byte b : deByteArray) {
+ dataElementBytes.add(b);
+ }
+ }
+
+ byte[] data = new byte[dataElementBytes.size()];
+ for (int i = 0; i < dataElementBytes.size(); i++) {
+ data[i] = dataElementBytes.get(i);
+ }
+ Cryptor cryptor = getCryptor(/* encrypt= */ true);
+ byte[] encryptedDeBytes = cryptor.encrypt(data, salt, authenticityKey);
+
+ length += encryptedDeBytes.length;
+
+ // Signature
+ byte[] hmacTag = Objects.requireNonNull(cryptor.sign(data, authenticityKey));
+ mHmacTag = hmacTag;
+ length += hmacTag.length;
+
+ this.mLength = length;
+ }
+
+ @PresenceCredential.IdentityType
+ private static int toPresenceCredentialIdentityType(@DataElement.DataType int type) {
+ switch (type) {
+ case DataElement.DataType.PRIVATE_IDENTITY:
+ return PresenceCredential.IDENTITY_TYPE_PRIVATE;
+ case DataElement.DataType.PROVISIONED_IDENTITY:
+ return PresenceCredential.IDENTITY_TYPE_PROVISIONED;
+ case DataElement.DataType.TRUSTED_IDENTITY:
+ return PresenceCredential.IDENTITY_TYPE_TRUSTED;
+ case DataElement.DataType.PUBLIC_IDENTITY:
+ default:
+ return PresenceCredential.IDENTITY_TYPE_UNKNOWN;
+ }
+ }
+
+ @DataElement.DataType
+ private static int toDataType(@PresenceCredential.IdentityType int identityType) {
+ switch (identityType) {
+ case PresenceCredential.IDENTITY_TYPE_PRIVATE:
+ return DataElement.DataType.PRIVATE_IDENTITY;
+ case PresenceCredential.IDENTITY_TYPE_PROVISIONED:
+ return DataElement.DataType.PROVISIONED_IDENTITY;
+ case PresenceCredential.IDENTITY_TYPE_TRUSTED:
+ return DataElement.DataType.TRUSTED_IDENTITY;
+ case PresenceCredential.IDENTITY_TYPE_UNKNOWN:
+ default:
+ return DataElement.DataType.PUBLIC_IDENTITY;
+ }
+ }
+
+ /**
+ * Returns {@code true} if the given {@link DataElement.DataType} is salt, or one of the
+ * identities. Identities should be able to convert to {@link PresenceCredential.IdentityType}s.
+ */
+ private static boolean isSaltOrIdentity(@DataElement.DataType int type) {
+ return type == DataElement.DataType.SALT || type == DataElement.DataType.PRIVATE_IDENTITY
+ || type == DataElement.DataType.TRUSTED_IDENTITY
+ || type == DataElement.DataType.PROVISIONED_IDENTITY
+ || type == DataElement.DataType.PUBLIC_IDENTITY;
+ }
+
+ private static Cryptor getCryptor(boolean encrypt) {
+ if (encrypt) {
+ Log.d(TAG, "get V1 Cryptor");
+ return CryptorImpV1.getInstance();
+ }
+ Log.d(TAG, "get fake Cryptor");
+ return CryptorImpFake.getInstance();
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisementUtils.java b/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisementUtils.java
new file mode 100644
index 0000000..06d0f2b
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/presence/ExtendedAdvertisementUtils.java
@@ -0,0 +1,100 @@
+/*
+ * 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.ExtendedAdvertisement.HEADER_LENGTH;
+
+import android.annotation.SuppressLint;
+import android.nearby.BroadcastRequest;
+import android.nearby.DataElement;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Provides serialization and deserialization util methods for {@link ExtendedAdvertisement}.
+ */
+public final class ExtendedAdvertisementUtils {
+
+ // Advertisement header related static fields.
+ private static final int VERSION_MASK = 0b11100000;
+ private static final int VERSION_MASK_AFTER_SHIT = 0b00000111;
+ private static final int HEADER_INDEX = 0;
+ private static final int HEADER_VERSION_OFFSET = 5;
+
+ /**
+ * Constructs the header of a {@link ExtendedAdvertisement}.
+ * 3 bit version, and 5 bit reserved for future use (RFU).
+ */
+ public static byte constructHeader(@BroadcastRequest.BroadcastVersion int version) {
+ return (byte) ((version << 5) & VERSION_MASK);
+ }
+
+ /** Returns the {@link BroadcastRequest.BroadcastVersion} from the advertisement
+ * in bytes format. */
+ public static int getVersion(byte[] advertisement) {
+ if (advertisement.length < HEADER_LENGTH) {
+ throw new IllegalArgumentException("Advertisement must contain header");
+ }
+ return ((advertisement[HEADER_INDEX] & VERSION_MASK) >> HEADER_VERSION_OFFSET)
+ & VERSION_MASK_AFTER_SHIT;
+ }
+
+ /** Returns the {@link DataElementHeader} from the advertisement in bytes format. */
+ public static byte[] getDataElementHeader(byte[] advertisement, int startIndex) {
+ Preconditions.checkArgument(startIndex < advertisement.length,
+ "Advertisement has no longer data left.");
+ List<Byte> headerBytes = new ArrayList<>();
+ while (startIndex < advertisement.length) {
+ byte current = advertisement[startIndex];
+ headerBytes.add(current);
+ if (!DataElementHeader.isExtending(current)) {
+ int size = headerBytes.size();
+ byte[] res = new byte[size];
+ for (int i = 0; i < size; i++) {
+ res[i] = headerBytes.get(i);
+ }
+ return res;
+ }
+ startIndex++;
+ }
+ throw new IllegalArgumentException("There is no end of the DataElement header.");
+ }
+
+ /**
+ * Constructs {@link DataElement}, including header(s) and actual data element data.
+ *
+ * Suppresses warning because {@link DataElement} checks isValidType in constructor.
+ */
+ @SuppressLint("WrongConstant")
+ public static byte[] convertDataElementToBytes(DataElement dataElement) {
+ @DataElement.DataType int type = dataElement.getKey();
+ byte[] data = dataElement.getValue();
+ DataElementHeader header = new DataElementHeader(BroadcastRequest.PRESENCE_VERSION_V1,
+ type, data.length);
+ byte[] headerByteArray = header.toBytes();
+
+ byte[] res = new byte[headerByteArray.length + data.length];
+ System.arraycopy(headerByteArray, 0, res, 0, headerByteArray.length);
+ System.arraycopy(data, 0, res, headerByteArray.length, data.length);
+ return res;
+ }
+
+ private ExtendedAdvertisementUtils() {}
+}
diff --git a/nearby/service/java/com/android/server/nearby/presence/FastAdvertisement.java b/nearby/service/java/com/android/server/nearby/presence/FastAdvertisement.java
index e4df673..ae53ada 100644
--- a/nearby/service/java/com/android/server/nearby/presence/FastAdvertisement.java
+++ b/nearby/service/java/com/android/server/nearby/presence/FastAdvertisement.java
@@ -24,7 +24,6 @@
import com.android.internal.util.Preconditions;
import java.nio.ByteBuffer;
-import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -42,7 +41,7 @@
// The header contains:
// version (3 bits) | provision_mode_flag (1 bit) | identity_type (3 bits) |
// extended_advertisement_mode (1 bit)
-public class FastAdvertisement {
+public class FastAdvertisement extends Advertisement {
private static final int FAST_ADVERTISEMENT_MAX_LENGTH = 24;
@@ -85,7 +84,8 @@
(byte) request.getTxPower());
}
- /** Serialize an {@link FastAdvertisement} object into bytes. */
+ /** Serialize a {@link FastAdvertisement} object into bytes. */
+ @Override
public byte[] toBytes() {
ByteBuffer buffer = ByteBuffer.allocate(getLength());
@@ -100,18 +100,8 @@
return buffer.array();
}
- private final int mLength;
-
private final int mLtvFieldCount;
- @PresenceCredential.IdentityType private final int mIdentityType;
-
- private final byte[] mIdentity;
-
- private final byte[] mSalt;
-
- private final List<Integer> mActions;
-
@Nullable
private final Byte mTxPower;
@@ -121,6 +111,7 @@
byte[] salt,
List<Integer> actions,
@Nullable Byte txPower) {
+ this.mVersion = BroadcastRequest.PRESENCE_VERSION_V0;
this.mIdentityType = identityType;
this.mIdentity = identity;
this.mSalt = salt;
@@ -143,44 +134,12 @@
"FastAdvertisement exceeds maximum length");
}
- /** Returns the version in the advertisement. */
- @BroadcastRequest.BroadcastVersion
- public int getVersion() {
- return BroadcastRequest.PRESENCE_VERSION_V0;
- }
-
- /** Returns the identity type in the advertisement. */
- @PresenceCredential.IdentityType
- public int getIdentityType() {
- return mIdentityType;
- }
-
- /** Returns the identity bytes in the advertisement. */
- public byte[] getIdentity() {
- return mIdentity.clone();
- }
-
- /** Returns the salt of the advertisement. */
- public byte[] getSalt() {
- return mSalt.clone();
- }
-
- /** Returns the actions in the advertisement. */
- public List<Integer> getActions() {
- return new ArrayList<>(mActions);
- }
-
/** Returns the adjusted TX Power in the advertisement. Null if not available. */
@Nullable
public Byte getTxPower() {
return mTxPower;
}
- /** Returns the length of the advertisement. */
- public int getLength() {
- return mLength;
- }
-
/** Returns the count of LTV fields in the advertisement. */
public int getLtvFieldCount() {
return mLtvFieldCount;
diff --git a/nearby/service/java/com/android/server/nearby/presence/PresenceDiscoveryResult.java b/nearby/service/java/com/android/server/nearby/presence/PresenceDiscoveryResult.java
index d1c72ae..5a76d96 100644
--- a/nearby/service/java/com/android/server/nearby/presence/PresenceDiscoveryResult.java
+++ b/nearby/service/java/com/android/server/nearby/presence/PresenceDiscoveryResult.java
@@ -16,31 +16,55 @@
package com.android.server.nearby.presence;
-import android.nearby.NearbyDevice;
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.annotation.NonNull;
+import android.nearby.DataElement;
import android.nearby.NearbyDeviceParcelable;
import android.nearby.PresenceDevice;
import android.nearby.PresenceScanFilter;
import android.nearby.PublicCredential;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
+import java.util.Set;
/** Represents a Presence discovery result. */
public class PresenceDiscoveryResult {
/** Creates a {@link PresenceDiscoveryResult} from the scan data. */
public static PresenceDiscoveryResult fromDevice(NearbyDeviceParcelable device) {
+ PresenceDevice presenceDevice = device.getPresenceDevice();
+ if (presenceDevice != null) {
+ return new PresenceDiscoveryResult.Builder()
+ .setTxPower(device.getTxPower())
+ .setRssi(device.getRssi())
+ .setSalt(presenceDevice.getSalt())
+ .setPublicCredential(device.getPublicCredential())
+ .addExtendedProperties(presenceDevice.getExtendedProperties())
+ .setEncryptedIdentityTag(device.getEncryptionKeyTag())
+ .build();
+ }
byte[] salt = device.getSalt();
if (salt == null) {
salt = new byte[0];
}
- return new PresenceDiscoveryResult.Builder()
- .setTxPower(device.getTxPower())
+
+ PresenceDiscoveryResult.Builder builder = new PresenceDiscoveryResult.Builder();
+ builder.setTxPower(device.getTxPower())
.setRssi(device.getRssi())
.setSalt(salt)
.addPresenceAction(device.getAction())
- .setPublicCredential(device.getPublicCredential())
- .build();
+ .setPublicCredential(device.getPublicCredential());
+ if (device.getPresenceDevice() != null) {
+ builder.addExtendedProperties(device.getPresenceDevice().getExtendedProperties());
+ }
+ return builder.build();
}
private final int mTxPower;
@@ -48,25 +72,35 @@
private final byte[] mSalt;
private final List<Integer> mPresenceActions;
private final PublicCredential mPublicCredential;
+ private final List<DataElement> mExtendedProperties;
+ private final byte[] mEncryptedIdentityTag;
private PresenceDiscoveryResult(
int txPower,
int rssi,
byte[] salt,
List<Integer> presenceActions,
- PublicCredential publicCredential) {
+ PublicCredential publicCredential,
+ List<DataElement> extendedProperties,
+ byte[] encryptedIdentityTag) {
mTxPower = txPower;
mRssi = rssi;
mSalt = salt;
mPresenceActions = presenceActions;
mPublicCredential = publicCredential;
+ mExtendedProperties = extendedProperties;
+ mEncryptedIdentityTag = encryptedIdentityTag;
}
/** Returns whether the discovery result matches the scan filter. */
public boolean matches(PresenceScanFilter scanFilter) {
+ if (accountKeyMatches(scanFilter.getExtendedProperties())) {
+ return true;
+ }
+
return pathLossMatches(scanFilter.getMaxPathLoss())
&& actionMatches(scanFilter.getPresenceActions())
- && credentialMatches(scanFilter.getCredentials());
+ && identityMatches(scanFilter.getCredentials());
}
private boolean pathLossMatches(int maxPathLoss) {
@@ -80,21 +114,47 @@
return filterActions.stream().anyMatch(mPresenceActions::contains);
}
- private boolean credentialMatches(List<PublicCredential> credentials) {
- return credentials.contains(mPublicCredential);
+ @VisibleForTesting
+ boolean accountKeyMatches(List<DataElement> extendedProperties) {
+ Set<byte[]> accountKeys = new ArraySet<>();
+ for (DataElement requestedDe : mExtendedProperties) {
+ if (requestedDe.getKey() != DataElement.DataType.ACCOUNT_KEY_DATA) {
+ continue;
+ }
+ accountKeys.add(requestedDe.getValue());
+ }
+ for (DataElement scannedDe : extendedProperties) {
+ if (scannedDe.getKey() != DataElement.DataType.ACCOUNT_KEY_DATA) {
+ continue;
+ }
+ // If one account key matches, then returns true.
+ for (byte[] key : accountKeys) {
+ if (Arrays.equals(key, scannedDe.getValue())) {
+ return true;
+ }
+ }
+ }
+
+ return false;
}
- /** Converts a presence device from the discovery result. */
- public PresenceDevice toPresenceDevice() {
- return new PresenceDevice.Builder(
- // Use the public credential hash as the device Id.
- String.valueOf(mPublicCredential.hashCode()),
- mSalt,
- mPublicCredential.getSecretId(),
- mPublicCredential.getEncryptedMetadata())
- .setRssi(mRssi)
- .addMedium(NearbyDevice.Medium.BLE)
- .build();
+ @VisibleForTesting
+ /** Gets presence {@link DataElement}s of the discovery result. */
+ public List<DataElement> getExtendedProperties() {
+ return mExtendedProperties;
+ }
+
+ private boolean identityMatches(List<PublicCredential> publicCredentials) {
+ if (mEncryptedIdentityTag.length == 0) {
+ return true;
+ }
+ for (PublicCredential publicCredential : publicCredentials) {
+ if (Arrays.equals(
+ mEncryptedIdentityTag, publicCredential.getEncryptedMetadataKeyTag())) {
+ return true;
+ }
+ }
+ return false;
}
/** Builder for {@link PresenceDiscoveryResult}. */
@@ -105,9 +165,12 @@
private PublicCredential mPublicCredential;
private final List<Integer> mPresenceActions;
+ private final List<DataElement> mExtendedProperties;
+ private byte[] mEncryptedIdentityTag = new byte[0];
public Builder() {
mPresenceActions = new ArrayList<>();
+ mExtendedProperties = new ArrayList<>();
}
/** Sets the calibrated tx power for the discovery result. */
@@ -130,7 +193,18 @@
/** Sets the public credential for the discovery result. */
public Builder setPublicCredential(PublicCredential publicCredential) {
- mPublicCredential = publicCredential;
+ if (publicCredential != null) {
+ mPublicCredential = publicCredential;
+ }
+ return this;
+ }
+
+ /** Sets the encrypted identity tag for the discovery result. Usually it is passed from
+ * {@link NearbyDeviceParcelable} and the tag is calculated with authenticity key when
+ * receiving an advertisement.
+ */
+ public Builder setEncryptedIdentityTag(byte[] encryptedIdentityTag) {
+ mEncryptedIdentityTag = encryptedIdentityTag;
return this;
}
@@ -140,10 +214,34 @@
return this;
}
+ /** Adds presence {@link DataElement}s of the discovery result. */
+ public Builder addExtendedProperties(DataElement dataElement) {
+ if (dataElement.getKey() == DataElement.DataType.ACTION) {
+ byte[] value = dataElement.getValue();
+ if (value.length == 1) {
+ addPresenceAction(Byte.toUnsignedInt(value[0]));
+ } else {
+ Log.e(TAG, "invalid action data element");
+ }
+ } else {
+ mExtendedProperties.add(dataElement);
+ }
+ return this;
+ }
+
+ /** Adds presence {@link DataElement}s of the discovery result. */
+ public Builder addExtendedProperties(@NonNull List<DataElement> dataElements) {
+ for (DataElement dataElement : dataElements) {
+ addExtendedProperties(dataElement);
+ }
+ return this;
+ }
+
/** Builds a {@link PresenceDiscoveryResult}. */
public PresenceDiscoveryResult build() {
return new PresenceDiscoveryResult(
- mTxPower, mRssi, mSalt, mPresenceActions, mPublicCredential);
+ mTxPower, mRssi, mSalt, mPresenceActions,
+ mPublicCredential, mExtendedProperties, mEncryptedIdentityTag);
}
}
}
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..8b2db50
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/presence/PresenceManager.java
@@ -0,0 +1,143 @@
+/*
+ * 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.NearbyService.TAG;
+
+import android.annotation.Nullable;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.nearby.DataElement;
+import android.nearby.NearbyDevice;
+import android.nearby.NearbyManager;
+import android.nearby.PresenceDevice;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
+import android.nearby.ScanCallback;
+import android.nearby.ScanRequest;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.common.locator.Locator;
+import com.android.server.nearby.common.locator.LocatorContextWrapper;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.Executors;
+
+/** PresenceManager is the class initiated in nearby service to handle presence related work. */
+public class PresenceManager {
+
+ final LocatorContextWrapper mLocatorContextWrapper;
+ final Locator mLocator;
+ private final IntentFilter mIntentFilter;
+
+ @VisibleForTesting
+ final ScanCallback mScanCallback =
+ new ScanCallback() {
+ @Override
+ public void onDiscovered(@NonNull NearbyDevice device) {
+ Log.i(TAG, "[PresenceManager] discovered Device.");
+ PresenceDevice presenceDevice = (PresenceDevice) device;
+ List<DataElement> dataElements = presenceDevice.getExtendedProperties();
+ for (DataElement dataElement : dataElements) {
+ Log.i(TAG, "[PresenceManager] Data Element key "
+ + dataElement.getKey());
+ Log.i(TAG, "[PresenceManager] Data Element value "
+ + Arrays.toString(dataElement.getValue()));
+ }
+ }
+
+ @Override
+ public void onUpdated(@NonNull NearbyDevice device) {}
+
+ @Override
+ public void onLost(@NonNull NearbyDevice device) {}
+ };
+
+ private final BroadcastReceiver mScreenBroadcastReceiver =
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ NearbyManager manager = getNearbyManager();
+ if (manager == null) {
+ Log.e(TAG, "Nearby Manager is null");
+ return;
+ }
+ if (intent.getAction().equals(Intent.ACTION_SCREEN_ON)) {
+ Log.d(TAG, "PresenceManager Start scan.");
+ PublicCredential publicCredential =
+ new PublicCredential.Builder(new byte[]{1}, new byte[]{1},
+ new byte[]{1}, new byte[]{1}, new byte[]{1}).build();
+ PresenceScanFilter presenceScanFilter =
+ new PresenceScanFilter.Builder()
+ .setMaxPathLoss(3)
+ .addCredential(publicCredential)
+ .addPresenceAction(1)
+ .addExtendedProperty(new DataElement(
+ DataElement.DataType.ACCOUNT_KEY_DATA,
+ new byte[16]))
+ .build();
+ ScanRequest scanRequest =
+ new ScanRequest.Builder()
+ .setScanType(ScanRequest.SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(presenceScanFilter)
+ .build();
+ Log.d(
+ TAG,
+ String.format(
+ Locale.getDefault(),
+ "[PresenceManager] Start Presence scan with request: %s",
+ scanRequest.toString()));
+ manager.startScan(
+ scanRequest, Executors.newSingleThreadExecutor(), mScanCallback);
+ } else if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF)) {
+ Log.d(TAG, "PresenceManager Stop scan.");
+ manager.stopScan(mScanCallback);
+ }
+ }
+ };
+
+ public PresenceManager(LocatorContextWrapper contextWrapper) {
+ mLocatorContextWrapper = contextWrapper;
+ mLocator = mLocatorContextWrapper.getLocator();
+ mIntentFilter = new IntentFilter();
+ }
+
+ /** Null when the Nearby Service is not available. */
+ @Nullable
+ private NearbyManager getNearbyManager() {
+ return (NearbyManager)
+ mLocatorContextWrapper
+ .getApplicationContext()
+ .getSystemService(Context.NEARBY_SERVICE);
+ }
+
+ /** Function called when nearby service start. */
+ public void initiate() {
+ mIntentFilter.addAction(Intent.ACTION_SCREEN_ON);
+ mIntentFilter.addAction(Intent.ACTION_SCREEN_OFF);
+ mLocatorContextWrapper
+ .getContext()
+ .registerReceiver(mScreenBroadcastReceiver, mIntentFilter);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/provider/AbstractDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/AbstractDiscoveryProvider.java
index f136695..21b4d7c 100644
--- a/nearby/service/java/com/android/server/nearby/provider/AbstractDiscoveryProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/AbstractDiscoveryProvider.java
@@ -40,7 +40,6 @@
protected final DiscoveryProviderController mController;
protected final Executor mExecutor;
protected Listener mListener;
- protected List<ScanFilter> mScanFilters;
/** Interface for listening to discovery providers. */
public interface Listener {
@@ -77,6 +76,12 @@
protected void invalidateScanMode() {}
/**
+ * Callback invoked to inform the provider of new provider scan filters which replaces any prior
+ * provider filters. Always invoked on the provider executor.
+ */
+ protected void onSetScanFilters(List<ScanFilter> filters) {}
+
+ /**
* Retrieves the controller for this discovery provider. Should never be invoked by subclasses,
* as a discovery provider should not be controlling itself. Using this method from subclasses
* could also result in deadlock.
@@ -138,7 +143,7 @@
@Override
public void setProviderScanFilters(List<ScanFilter> filters) {
- mScanFilters = filters;
+ mExecutor.execute(() -> onSetScanFilters(filters));
}
}
}
diff --git a/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java b/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
index 67392ad..2b9fdb9 100644
--- a/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/BleBroadcastProvider.java
@@ -16,17 +16,25 @@
package com.android.server.nearby.provider;
+import static com.android.server.nearby.NearbyService.TAG;
+import static com.android.server.nearby.presence.PresenceConstants.PRESENCE_UUID;
+
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.le.AdvertiseCallback;
import android.bluetooth.le.AdvertiseData;
import android.bluetooth.le.AdvertiseSettings;
+import android.bluetooth.le.AdvertisingSet;
+import android.bluetooth.le.AdvertisingSetCallback;
+import android.bluetooth.le.AdvertisingSetParameters;
import android.bluetooth.le.BluetoothLeAdvertiser;
import android.nearby.BroadcastCallback;
+import android.nearby.BroadcastRequest;
import android.os.ParcelUuid;
+import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.nearby.injector.Injector;
-import java.util.UUID;
import java.util.concurrent.Executor;
/**
@@ -46,13 +54,16 @@
private BroadcastListener mBroadcastListener;
private boolean mIsAdvertising;
-
+ @VisibleForTesting
+ AdvertisingSetCallback mAdvertisingSetCallback;
BleBroadcastProvider(Injector injector, Executor executor) {
mInjector = injector;
mExecutor = executor;
+ mAdvertisingSetCallback = getAdvertisingSetCallback();
}
- void start(byte[] advertisementPackets, BroadcastListener listener) {
+ void start(@BroadcastRequest.BroadcastVersion int version, byte[] advertisementPackets,
+ BroadcastListener listener) {
if (mIsAdvertising) {
stop();
}
@@ -63,23 +74,36 @@
mInjector.getBluetoothAdapter().getBluetoothLeAdvertiser();
if (bluetoothLeAdvertiser != null) {
advertiseStarted = true;
- AdvertiseSettings settings =
- new AdvertiseSettings.Builder()
- .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED)
- .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM)
- .setConnectable(true)
- .build();
-
- // TODO(b/230538655) Use empty data until Presence V1 protocol is implemented.
- ParcelUuid emptyParcelUuid = new ParcelUuid(new UUID(0L, 0L));
- byte[] emptyAdvertisementPackets = new byte[0];
AdvertiseData advertiseData =
new AdvertiseData.Builder()
- .addServiceData(emptyParcelUuid, emptyAdvertisementPackets).build();
+ .addServiceData(new ParcelUuid(PRESENCE_UUID),
+ advertisementPackets).build();
try {
mBroadcastListener = listener;
- bluetoothLeAdvertiser.startAdvertising(settings, advertiseData, this);
+ switch (version) {
+ case BroadcastRequest.PRESENCE_VERSION_V0:
+ bluetoothLeAdvertiser.startAdvertising(getAdvertiseSettings(),
+ advertiseData, this);
+ break;
+ case BroadcastRequest.PRESENCE_VERSION_V1:
+ if (adapter.isLeExtendedAdvertisingSupported()) {
+ bluetoothLeAdvertiser.startAdvertisingSet(
+ getAdvertisingSetParameters(),
+ advertiseData,
+ null, null, null, mAdvertisingSetCallback);
+ } else {
+ Log.w(TAG, "Failed to start advertising set because the chipset"
+ + " does not supports LE Extended Advertising feature.");
+ advertiseStarted = false;
+ }
+ break;
+ default:
+ Log.w(TAG, "Failed to start advertising set because the advertisement"
+ + " is wrong.");
+ advertiseStarted = false;
+ }
} catch (NullPointerException | IllegalStateException | SecurityException e) {
+ Log.w(TAG, "Failed to start advertising.", e);
advertiseStarted = false;
}
}
@@ -97,6 +121,7 @@
mInjector.getBluetoothAdapter().getBluetoothLeAdvertiser();
if (bluetoothLeAdvertiser != null) {
bluetoothLeAdvertiser.stopAdvertising(this);
+ bluetoothLeAdvertiser.stopAdvertisingSet(mAdvertisingSetCallback);
}
}
mBroadcastListener = null;
@@ -120,4 +145,41 @@
mBroadcastListener.onStatusChanged(BroadcastCallback.STATUS_FAILURE);
}
}
+
+ private static AdvertiseSettings getAdvertiseSettings() {
+ return new AdvertiseSettings.Builder()
+ .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED)
+ .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM)
+ .setConnectable(true)
+ .build();
+ }
+
+ private static AdvertisingSetParameters getAdvertisingSetParameters() {
+ return new AdvertisingSetParameters.Builder()
+ .setInterval(AdvertisingSetParameters.INTERVAL_MEDIUM)
+ .setTxPowerLevel(AdvertisingSetParameters.TX_POWER_MEDIUM)
+ .setIncludeTxPower(true)
+ .setConnectable(true)
+ .build();
+ }
+
+ private AdvertisingSetCallback getAdvertisingSetCallback() {
+ return new AdvertisingSetCallback() {
+ @Override
+ public void onAdvertisingSetStarted(AdvertisingSet advertisingSet,
+ int txPower, int status) {
+ if (status == AdvertisingSetCallback.ADVERTISE_SUCCESS) {
+ if (mBroadcastListener != null) {
+ mBroadcastListener.onStatusChanged(BroadcastCallback.STATUS_OK);
+ }
+ mIsAdvertising = true;
+ } else {
+ Log.e(TAG, "Starts advertising failed in status " + status);
+ if (mBroadcastListener != null) {
+ mBroadcastListener.onStatusChanged(BroadcastCallback.STATUS_FAILURE);
+ }
+ }
+ }
+ };
+ }
}
diff --git a/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
index e8aea79..55176ba 100644
--- a/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/BleDiscoveryProvider.java
@@ -27,16 +27,23 @@
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.Context;
+import android.nearby.DataElement;
import android.nearby.NearbyDevice;
import android.nearby.NearbyDeviceParcelable;
+import android.nearby.PresenceDevice;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
import android.nearby.ScanRequest;
import android.os.ParcelUuid;
import android.util.Log;
+import com.android.internal.annotations.GuardedBy;
import com.android.server.nearby.common.bluetooth.fastpair.Constants;
import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.presence.ExtendedAdvertisement;
import com.android.server.nearby.presence.PresenceConstants;
import com.android.server.nearby.util.ForegroundThread;
+import com.android.server.nearby.util.encryption.CryptorImpIdentityV1;
import com.google.common.annotations.VisibleForTesting;
@@ -57,6 +64,21 @@
// Don't block the thread as it may be used by other services.
private static final Executor NEARBY_EXECUTOR = ForegroundThread.getExecutor();
private final Injector mInjector;
+ private final Object mLock = new Object();
+ // Null when the filters are never set
+ @VisibleForTesting
+ @GuardedBy("mLock")
+ @Nullable
+ private List<android.nearby.ScanFilter> mScanFilters;
+ private android.bluetooth.le.ScanCallback mScanCallbackLegacy =
+ new android.bluetooth.le.ScanCallback() {
+ @Override
+ public void onScanResult(int callbackType, ScanResult scanResult) {
+ }
+ @Override
+ public void onScanFailed(int errorCode) {
+ }
+ };
private android.bluetooth.le.ScanCallback mScanCallback =
new android.bluetooth.le.ScanCallback() {
@Override
@@ -81,7 +103,8 @@
} else {
byte[] presenceData = serviceDataMap.get(PRESENCE_UUID);
if (presenceData != null) {
- builder.setData(serviceDataMap.get(PRESENCE_UUID));
+ setPresenceDevice(presenceData, builder, deviceName,
+ scanResult.getRssi());
}
}
}
@@ -91,7 +114,7 @@
@Override
public void onScanFailed(int errorCode) {
- Log.w(TAG, "BLE Scan failed with error code " + errorCode);
+ Log.w(TAG, "BLE 5.0 Scan failed with error code " + errorCode);
}
};
@@ -100,12 +123,39 @@
mInjector = injector;
}
+ private static PresenceDevice getPresenceDevice(ExtendedAdvertisement advertisement,
+ String deviceName, int rssi) {
+ // TODO(238458326): After implementing encryption, use real data.
+ byte[] secretIdBytes = new byte[0];
+ PresenceDevice.Builder builder =
+ new PresenceDevice.Builder(
+ String.valueOf(advertisement.hashCode()),
+ advertisement.getSalt(),
+ secretIdBytes,
+ advertisement.getIdentity())
+ .addMedium(NearbyDevice.Medium.BLE)
+ .setName(deviceName)
+ .setRssi(rssi);
+ for (int i : advertisement.getActions()) {
+ builder.addExtendedProperty(new DataElement(DataElement.DataType.ACTION,
+ new byte[]{(byte) i}));
+ }
+ for (DataElement dataElement : advertisement.getDataElements()) {
+ builder.addExtendedProperty(dataElement);
+ }
+ return builder.build();
+ }
+
private static List<ScanFilter> getScanFilters() {
List<ScanFilter> scanFilterList = new ArrayList<>();
scanFilterList.add(
new ScanFilter.Builder()
.setServiceData(FAST_PAIR_UUID, new byte[]{0}, new byte[]{0})
.build());
+ scanFilterList.add(
+ new ScanFilter.Builder()
+ .setServiceData(PRESENCE_UUID, new byte[]{0}, new byte[]{0})
+ .build());
return scanFilterList;
}
@@ -130,8 +180,9 @@
@Override
protected void onStart() {
if (isBleAvailable()) {
- Log.d(TAG, "BleDiscoveryProvider started.");
- startScan(getScanFilters(), getScanSettings(), mScanCallback);
+ Log.d(TAG, "BleDiscoveryProvider started");
+ startScan(getScanFilters(), getScanSettings(/* legacy= */ false), mScanCallback);
+ startScan(getScanFilters(), getScanSettings(/* legacy= */ true), mScanCallbackLegacy);
return;
}
Log.w(TAG, "Cannot start BleDiscoveryProvider because Ble is not available.");
@@ -148,6 +199,12 @@
}
Log.v(TAG, "Ble scan stopped.");
bluetoothLeScanner.stopScan(mScanCallback);
+ bluetoothLeScanner.stopScan(mScanCallbackLegacy);
+ synchronized (mLock) {
+ if (mScanFilters != null) {
+ mScanFilters = null;
+ }
+ }
}
@Override
@@ -156,6 +213,20 @@
onStart();
}
+ @Override
+ protected void onSetScanFilters(List<android.nearby.ScanFilter> filters) {
+ synchronized (mLock) {
+ mScanFilters = filters == null ? null : List.copyOf(filters);
+ }
+ }
+
+ @VisibleForTesting
+ protected List<android.nearby.ScanFilter> getFiltersLocked() {
+ synchronized (mLock) {
+ return mScanFilters == null ? null : List.copyOf(mScanFilters);
+ }
+ }
+
private void startScan(
List<ScanFilter> scanFilters, ScanSettings scanSettings,
android.bluetooth.le.ScanCallback scanCallback) {
@@ -179,7 +250,7 @@
}
}
- private ScanSettings getScanSettings() {
+ private ScanSettings getScanSettings(boolean legacy) {
int bleScanMode = ScanSettings.SCAN_MODE_LOW_POWER;
switch (mController.getProviderScanMode()) {
case ScanRequest.SCAN_MODE_LOW_LATENCY:
@@ -195,11 +266,42 @@
bleScanMode = ScanSettings.SCAN_MODE_OPPORTUNISTIC;
break;
}
- return new ScanSettings.Builder().setScanMode(bleScanMode).build();
+ return new ScanSettings.Builder().setScanMode(bleScanMode).setLegacy(legacy).build();
}
@VisibleForTesting
ScanCallback getScanCallback() {
return mScanCallback;
}
+
+ private void setPresenceDevice(byte[] data, NearbyDeviceParcelable.Builder builder,
+ String deviceName, int rssi) {
+ synchronized (mLock) {
+ if (mScanFilters == null) {
+ return;
+ }
+ for (android.nearby.ScanFilter scanFilter : mScanFilters) {
+ if (scanFilter instanceof PresenceScanFilter) {
+ // Iterate all possible authenticity key and identity combinations to decrypt
+ // advertisement
+ PresenceScanFilter presenceFilter = (PresenceScanFilter) scanFilter;
+ for (PublicCredential credential : presenceFilter.getCredentials()) {
+ ExtendedAdvertisement advertisement =
+ ExtendedAdvertisement.fromBytes(data, credential);
+ if (advertisement == null) {
+ continue;
+ }
+ if (CryptorImpIdentityV1.getInstance().verify(
+ advertisement.getIdentity(),
+ credential.getEncryptedMetadataKeyTag())) {
+ builder.setPresenceDevice(getPresenceDevice(advertisement, deviceName,
+ rssi));
+ builder.setEncryptionKeyTag(credential.getEncryptedMetadataKeyTag());
+ return;
+ }
+ }
+ }
+ }
+ }
+ }
}
diff --git a/nearby/service/java/com/android/server/nearby/provider/BroadcastProviderManager.java b/nearby/service/java/com/android/server/nearby/provider/BroadcastProviderManager.java
index 3fffda5..400f936 100644
--- a/nearby/service/java/com/android/server/nearby/provider/BroadcastProviderManager.java
+++ b/nearby/service/java/com/android/server/nearby/provider/BroadcastProviderManager.java
@@ -16,6 +16,9 @@
package com.android.server.nearby.provider;
+import static com.android.server.nearby.NearbyService.SUPPORT_TEST_APP;
+
+import android.annotation.Nullable;
import android.content.Context;
import android.nearby.BroadcastCallback;
import android.nearby.BroadcastRequest;
@@ -27,6 +30,8 @@
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.nearby.NearbyConfiguration;
import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.presence.Advertisement;
+import com.android.server.nearby.presence.ExtendedAdvertisement;
import com.android.server.nearby.presence.FastAdvertisement;
import com.android.server.nearby.util.ForegroundThread;
@@ -66,10 +71,12 @@
public void startBroadcast(BroadcastRequest broadcastRequest, IBroadcastListener listener) {
synchronized (mLock) {
mExecutor.execute(() -> {
- NearbyConfiguration configuration = new NearbyConfiguration();
- if (!configuration.isPresenceBroadcastLegacyEnabled()) {
- reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
- return;
+ if (!SUPPORT_TEST_APP) {
+ NearbyConfiguration configuration = new NearbyConfiguration();
+ if (!configuration.isPresenceBroadcastLegacyEnabled()) {
+ reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
+ return;
+ }
}
if (broadcastRequest.getType() != BroadcastRequest.BROADCAST_TYPE_NEARBY_PRESENCE) {
reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
@@ -77,25 +84,37 @@
}
PresenceBroadcastRequest presenceBroadcastRequest =
(PresenceBroadcastRequest) broadcastRequest;
- if (presenceBroadcastRequest.getVersion() != BroadcastRequest.PRESENCE_VERSION_V0) {
+ Advertisement advertisement = getAdvertisement(presenceBroadcastRequest);
+ if (advertisement == null) {
+ Log.e(TAG, "Failed to start broadcast because broadcastRequest is illegal.");
reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
return;
}
- FastAdvertisement fastAdvertisement = FastAdvertisement.createFromRequest(
- presenceBroadcastRequest);
- byte[] advertisementPackets = fastAdvertisement.toBytes();
mBroadcastListener = listener;
- mBleBroadcastProvider.start(advertisementPackets, this);
+ mBleBroadcastProvider.start(presenceBroadcastRequest.getVersion(),
+ advertisement.toBytes(), this);
});
}
}
+ @Nullable
+ private Advertisement getAdvertisement(PresenceBroadcastRequest request) {
+ switch (request.getVersion()) {
+ case BroadcastRequest.PRESENCE_VERSION_V0:
+ return FastAdvertisement.createFromRequest(request);
+ case BroadcastRequest.PRESENCE_VERSION_V1:
+ return ExtendedAdvertisement.createFromRequest(request);
+ default:
+ return null;
+ }
+ }
+
/**
* Stops the nearby broadcast.
*/
public void stopBroadcast(IBroadcastListener listener) {
synchronized (mLock) {
- if (!mNearbyConfiguration.isPresenceBroadcastLegacyEnabled()) {
+ if (!SUPPORT_TEST_APP && !mNearbyConfiguration.isPresenceBroadcastLegacyEnabled()) {
reportBroadcastStatus(listener, BroadcastCallback.STATUS_FAILURE);
return;
}
diff --git a/nearby/service/java/com/android/server/nearby/provider/ChreCommunication.java b/nearby/service/java/com/android/server/nearby/provider/ChreCommunication.java
index 5077ffe..b24e7d9 100644
--- a/nearby/service/java/com/android/server/nearby/provider/ChreCommunication.java
+++ b/nearby/service/java/com/android/server/nearby/provider/ChreCommunication.java
@@ -19,15 +19,18 @@
import static com.android.server.nearby.NearbyService.TAG;
import android.annotation.Nullable;
+import android.content.Context;
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.NanoAppMessage;
import android.hardware.location.NanoAppState;
import android.util.Log;
-import com.android.server.nearby.injector.ContextHubManagerAdapter;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.nearby.NearbyConfiguration;
import com.android.server.nearby.injector.Injector;
import com.google.common.base.Preconditions;
@@ -63,19 +66,28 @@
}
private final Injector mInjector;
+ private final Context mContext;
private final Executor mExecutor;
private boolean mStarted = false;
+ // null when CHRE availability result has not been returned
+ @Nullable private Boolean mChreSupport = null;
@Nullable private ContextHubCommsCallback mCallback;
@Nullable private ContextHubClient mContextHubClient;
- public ChreCommunication(Injector injector, Executor executor) {
+ public ChreCommunication(Injector injector, Context context, Executor executor) {
mInjector = injector;
+ mContext = context;
mExecutor = executor;
}
- public boolean available() {
- return mContextHubClient != null;
+ /**
+ * @return {@code true} if NanoApp is available and {@code null} when CHRE availability result
+ * has not been returned
+ */
+ @Nullable
+ public Boolean available() {
+ return mChreSupport;
}
/**
@@ -86,12 +98,12 @@
* contexthub.
*/
public synchronized void start(ContextHubCommsCallback callback, Set<Long> nanoAppIds) {
- ContextHubManagerAdapter manager = mInjector.getContextHubManagerAdapter();
+ ContextHubManager manager = mInjector.getContextHubManager();
if (manager == null) {
Log.e(TAG, "ContexHub not available in this device");
return;
} else {
- Log.i(TAG, "Start ChreCommunication");
+ Log.i(TAG, "[ChreCommunication] Start ChreCommunication");
}
Preconditions.checkNotNull(callback);
Preconditions.checkArgument(!nanoAppIds.isEmpty());
@@ -134,6 +146,7 @@
if (mContextHubClient != null) {
mContextHubClient.close();
mContextHubClient = null;
+ mChreSupport = null;
}
}
@@ -172,7 +185,8 @@
mCallback.onNanoAppRestart(nanoAppId);
}
- private static String contextHubTransactionResultToString(int result) {
+ @VisibleForTesting
+ static String contextHubTransactionResultToString(int result) {
switch (result) {
case ContextHubTransaction.RESULT_SUCCESS:
return "RESULT_SUCCESS";
@@ -207,13 +221,13 @@
private final ContextHubInfo mQueriedContextHub;
private final List<ContextHubInfo> mContextHubs;
private final Set<Long> mNanoAppIds;
- private final ContextHubManagerAdapter mManager;
+ private final ContextHubManager mManager;
OnQueryCompleteListener(
ContextHubInfo queriedContextHub,
List<ContextHubInfo> contextHubs,
Set<Long> nanoAppIds,
- ContextHubManagerAdapter manager) {
+ ContextHubManager manager) {
this.mQueriedContextHub = queriedContextHub;
this.mContextHubs = contextHubs;
this.mNanoAppIds = nanoAppIds;
@@ -233,19 +247,27 @@
if (response.getResult() == ContextHubTransaction.RESULT_SUCCESS) {
for (NanoAppState state : response.getContents()) {
+ long version = state.getNanoAppVersion();
+ NearbyConfiguration configuration = new NearbyConfiguration();
+ long minVersion = configuration.getNanoAppMinVersion();
+ if (version < minVersion) {
+ Log.w(TAG, String.format("Current nano app version is %s, which does not "
+ + "meet minimum version required %s", version, minVersion));
+ continue;
+ }
if (mNanoAppIds.contains(state.getNanoAppId())) {
Log.i(
TAG,
String.format(
"Found valid contexthub: %s", mQueriedContextHub.getId()));
- mContextHubClient =
- mManager.createClient(
- mQueriedContextHub, ChreCommunication.this, mExecutor);
+ mContextHubClient = mManager.createClient(mContext, mQueriedContextHub,
+ mExecutor, ChreCommunication.this);
+ mChreSupport = true;
mCallback.started(true);
return;
}
}
- Log.e(
+ Log.i(
TAG,
String.format(
"Didn't find the nanoapp on contexthub: %s",
@@ -263,6 +285,7 @@
// there isn't a valid context available on this device.
if (mContextHubs.isEmpty()) {
mCallback.started(false);
+ mChreSupport = false;
}
}
}
diff --git a/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java b/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
index f20c6d8..6aefae9 100644
--- a/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/ChreDiscoveryProvider.java
@@ -20,20 +20,32 @@
import static com.android.server.nearby.NearbyService.TAG;
+import static service.proto.Blefilter.DataElement.ElementType.DE_BATTERY_STATUS;
+import static service.proto.Blefilter.DataElement.ElementType.DE_CONNECTION_STATUS;
+import static service.proto.Blefilter.DataElement.ElementType.DE_FAST_PAIR_ACCOUNT_KEY;
+
+import android.annotation.Nullable;
+import android.content.BroadcastReceiver;
import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
import android.hardware.location.NanoAppMessage;
+import android.nearby.DataElement;
import android.nearby.NearbyDevice;
import android.nearby.NearbyDeviceParcelable;
+import android.nearby.PresenceDevice;
import android.nearby.PresenceScanFilter;
import android.nearby.PublicCredential;
import android.nearby.ScanFilter;
import android.util.Log;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
-import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.ByteString;
import java.util.Collections;
+import java.util.List;
import java.util.concurrent.Executor;
import service.proto.Blefilter;
@@ -47,47 +59,95 @@
@VisibleForTesting public static final int NANOAPP_MESSAGE_TYPE_FILTER = 3;
/** @hide */
@VisibleForTesting public static final int NANOAPP_MESSAGE_TYPE_FILTER_RESULT = 4;
+ /** @hide */
+ @VisibleForTesting public static final int NANOAPP_MESSAGE_TYPE_CONFIG = 5;
- private static final int PRESENCE_UUID = 0xFCF1;
+ private static final int FP_ACCOUNT_KEY_LENGTH = 16;
- private ChreCommunication mChreCommunication;
- private ChreCallback mChreCallback;
+ private final ChreCommunication mChreCommunication;
+ private final ChreCallback mChreCallback;
+ private final Object mLock = new Object();
+
private boolean mChreStarted = false;
private Blefilter.BleFilters mFilters = null;
- private int mFilterId;
+ private Context mContext;
+ private final IntentFilter mIntentFilter;
+ // Null when the filters are never set
+ @GuardedBy("mLock")
+ @Nullable
+ private List<ScanFilter> mScanFilters;
+
+ private final BroadcastReceiver mScreenBroadcastReceiver =
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Boolean screenOn = intent.getAction().equals(Intent.ACTION_SCREEN_ON)
+ || intent.getAction().equals(Intent.ACTION_USER_PRESENT);
+ Log.d(TAG, String.format(
+ "[ChreDiscoveryProvider] update nanoapp screen status: %B", screenOn));
+ sendScreenUpdate(screenOn);
+ }
+ };
public ChreDiscoveryProvider(
Context context, ChreCommunication chreCommunication, Executor executor) {
super(context, executor);
+ mContext = context;
mChreCommunication = chreCommunication;
mChreCallback = new ChreCallback();
- mFilterId = 0;
+ mIntentFilter = new IntentFilter();
+ }
+
+ /** Initialize the CHRE discovery provider. */
+ public void init() {
+ mChreCommunication.start(mChreCallback, Collections.singleton(NANOAPP_ID));
}
@Override
protected void onStart() {
Log.d(TAG, "Start CHRE scan");
- mChreCommunication.start(mChreCallback, Collections.singleton(NANOAPP_ID));
- updateFilters();
+ synchronized (mLock) {
+ updateFiltersLocked();
+ }
}
@Override
protected void onStop() {
- mChreStarted = false;
- mChreCommunication.stop();
+ Log.d(TAG, "Stop CHRE scan");
+ synchronized (mLock) {
+ if (mScanFilters != null) {
+ mScanFilters = null;
+ }
+ updateFiltersLocked();
+ }
}
@Override
- protected void invalidateScanMode() {
- onStop();
- onStart();
+ protected void onSetScanFilters(List<ScanFilter> filters) {
+ synchronized (mLock) {
+ mScanFilters = filters == null ? null : List.copyOf(filters);
+ updateFiltersLocked();
+ }
}
- public boolean available() {
+ /**
+ * @return {@code true} if CHRE is available and {@code null} when CHRE availability result
+ * has not been returned
+ */
+ @Nullable
+ public Boolean available() {
return mChreCommunication.available();
}
- private synchronized void updateFilters() {
+ @VisibleForTesting
+ List<ScanFilter> getFiltersLocked() {
+ synchronized (mLock) {
+ return mScanFilters == null ? null : List.copyOf(mScanFilters);
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void updateFiltersLocked() {
if (mScanFilters == null) {
Log.e(TAG, "ScanFilters not set.");
return;
@@ -95,22 +155,47 @@
Blefilter.BleFilters.Builder filtersBuilder = Blefilter.BleFilters.newBuilder();
for (ScanFilter scanFilter : mScanFilters) {
PresenceScanFilter presenceScanFilter = (PresenceScanFilter) scanFilter;
- Blefilter.BleFilter filter =
- Blefilter.BleFilter.newBuilder()
- .setId(mFilterId)
- .setUuid(PRESENCE_UUID)
- .setIntent(presenceScanFilter.getPresenceActions().get(0))
- .build();
- filtersBuilder.addFilter(filter);
- mFilterId++;
+ Blefilter.BleFilter.Builder filterBuilder = Blefilter.BleFilter.newBuilder();
+ for (PublicCredential credential : presenceScanFilter.getCredentials()) {
+ filterBuilder.addCertificate(toProtoPublicCredential(credential));
+ }
+ for (DataElement dataElement : presenceScanFilter.getExtendedProperties()) {
+ if (dataElement.getKey() == DataElement.DataType.ACCOUNT_KEY_DATA) {
+ filterBuilder.addDataElement(toProtoDataElement(dataElement));
+ }
+ }
+ if (!presenceScanFilter.getPresenceActions().isEmpty()) {
+ filterBuilder.setIntent(presenceScanFilter.getPresenceActions().get(0));
+ }
+ filtersBuilder.addFilter(filterBuilder.build());
}
- mFilters = filtersBuilder.build();
if (mChreStarted) {
- sendFilters(mFilters);
+ sendFilters(filtersBuilder.build());
mFilters = null;
}
}
+ private Blefilter.PublicateCertificate toProtoPublicCredential(PublicCredential credential) {
+ Log.d(TAG, String.format("Returns a PublicCertificate with authenticity key size %d and"
+ + " encrypted metadata key tag size %d", credential.getAuthenticityKey().length,
+ credential.getEncryptedMetadataKeyTag().length));
+ return Blefilter.PublicateCertificate.newBuilder()
+ .setAuthenticityKey(ByteString.copyFrom(credential.getAuthenticityKey()))
+ .setMetadataEncryptionKeyTag(
+ ByteString.copyFrom(credential.getEncryptedMetadataKeyTag()))
+ .build();
+ }
+
+ private Blefilter.DataElement toProtoDataElement(DataElement dataElement) {
+ return Blefilter.DataElement.newBuilder()
+ .setKey(
+ Blefilter.DataElement.ElementType
+ .DE_FAST_PAIR_ACCOUNT_KEY)
+ .setValue(ByteString.copyFrom(dataElement.getValue()))
+ .setValueLength(FP_ACCOUNT_KEY_LENGTH)
+ .build();
+ }
+
private void sendFilters(Blefilter.BleFilters filters) {
NanoAppMessage message =
NanoAppMessage.createMessageToNanoApp(
@@ -120,6 +205,16 @@
}
}
+ private void sendScreenUpdate(Boolean screenOn) {
+ Blefilter.BleConfig config = Blefilter.BleConfig.newBuilder().setScreenOn(screenOn).build();
+ NanoAppMessage message =
+ NanoAppMessage.createMessageToNanoApp(
+ NANOAPP_ID, NANOAPP_MESSAGE_TYPE_CONFIG, config.toByteArray());
+ if (!mChreCommunication.sendMessageToNanoApp(message)) {
+ Log.e(TAG, "Failed to send config to CHRE.");
+ }
+ }
+
private class ChreCallback implements ChreCommunication.ContextHubCommsCallback {
@Override
@@ -127,6 +222,10 @@
if (success) {
synchronized (ChreDiscoveryProvider.this) {
Log.i(TAG, "CHRE communication started");
+ mIntentFilter.addAction(Intent.ACTION_SCREEN_ON);
+ mIntentFilter.addAction(Intent.ACTION_USER_PRESENT);
+ mIntentFilter.addAction(Intent.ACTION_SCREEN_OFF);
+ mContext.registerReceiver(mScreenBroadcastReceiver, mIntentFilter);
mChreStarted = true;
if (mFilters != null) {
sendFilters(mFilters);
@@ -163,15 +262,81 @@
Blefilter.BleFilterResults results =
Blefilter.BleFilterResults.parseFrom(message.getMessageBody());
for (Blefilter.BleFilterResult filterResult : results.getResultList()) {
- Blefilter.PublicCredential credential = filterResult.getPublicCredential();
+ // TODO(b/234653356): There are some duplicate fields set both in
+ // PresenceDevice and NearbyDeviceParcelable, cleanup is needed.
+ byte[] salt = {1};
+ byte[] secretId = {1};
+ byte[] authenticityKey = {1};
+ byte[] publicKey = {1};
+ byte[] encryptedMetaData = {1};
+ byte[] encryptedMetaDataTag = {1};
+ if (filterResult.hasPublicCredential()) {
+ Blefilter.PublicCredential credential =
+ filterResult.getPublicCredential();
+ secretId = credential.getSecretId().toByteArray();
+ authenticityKey = credential.getAuthenticityKey().toByteArray();
+ publicKey = credential.getPublicKey().toByteArray();
+ encryptedMetaData = credential.getEncryptedMetadata().toByteArray();
+ encryptedMetaDataTag =
+ credential.getEncryptedMetadataTag().toByteArray();
+ }
+ PresenceDevice.Builder presenceDeviceBuilder =
+ new PresenceDevice.Builder(
+ String.valueOf(filterResult.hashCode()),
+ salt,
+ secretId,
+ encryptedMetaData)
+ .setRssi(filterResult.getRssi())
+ .addMedium(NearbyDevice.Medium.BLE);
+ // Data Elements reported from nanoapp added to Data Elements.
+ // i.e. Fast Pair account keys, connection status and battery
+ for (Blefilter.DataElement element : filterResult.getDataElementList()) {
+ addDataElementsToPresenceDevice(element, presenceDeviceBuilder);
+ }
+ // BlE address appended to Data Element.
+ if (filterResult.hasBluetoothAddress()) {
+ presenceDeviceBuilder.addExtendedProperty(
+ new DataElement(
+ DataElement.DataType.BLE_ADDRESS,
+ filterResult.getBluetoothAddress().toByteArray()));
+ }
+ // BlE TX Power appended to Data Element.
+ if (filterResult.hasTxPower()) {
+ presenceDeviceBuilder.addExtendedProperty(
+ new DataElement(
+ DataElement.DataType.TX_POWER,
+ new byte[]{(byte) filterResult.getTxPower()}));
+ }
+ // BLE Service data appended to Data Elements.
+ if (filterResult.hasBleServiceData()) {
+ // Retrieves the length of the service data from the first byte,
+ // and then skips the first byte and returns data[1 .. dataLength)
+ // as the DataElement value.
+ int dataLength = Byte.toUnsignedInt(
+ filterResult.getBleServiceData().byteAt(0));
+ presenceDeviceBuilder.addExtendedProperty(
+ new DataElement(
+ DataElement.DataType.BLE_SERVICE_DATA,
+ filterResult.getBleServiceData()
+ .substring(1, 1 + dataLength).toByteArray()));
+ }
+ // Add action
+ if (filterResult.hasIntent()) {
+ presenceDeviceBuilder.addExtendedProperty(
+ new DataElement(
+ DataElement.DataType.ACTION,
+ new byte[]{(byte) filterResult.getIntent()}));
+ }
+
PublicCredential publicCredential =
new PublicCredential.Builder(
- credential.getSecretId().toByteArray(),
- credential.getAuthenticityKey().toByteArray(),
- credential.getPublicKey().toByteArray(),
- credential.getEncryptedMetadata().toByteArray(),
- credential.getEncryptedMetadataTag().toByteArray())
+ secretId,
+ authenticityKey,
+ publicKey,
+ encryptedMetaData,
+ encryptedMetaDataTag)
.build();
+
NearbyDeviceParcelable device =
new NearbyDeviceParcelable.Builder()
.setScanType(SCAN_TYPE_NEARBY_PRESENCE)
@@ -180,15 +345,40 @@
.setRssi(filterResult.getRssi())
.setAction(filterResult.getIntent())
.setPublicCredential(publicCredential)
+ .setPresenceDevice(presenceDeviceBuilder.build())
+ .setEncryptionKeyTag(encryptedMetaDataTag)
.build();
mExecutor.execute(() -> mListener.onNearbyDeviceDiscovered(device));
}
- } catch (InvalidProtocolBufferException e) {
- Log.e(
- TAG,
- String.format("Failed to decode the filter result %s", e.toString()));
+ } catch (Exception e) {
+ Log.e(TAG, String.format("Failed to decode the filter result %s", e));
}
}
}
+
+ private void addDataElementsToPresenceDevice(Blefilter.DataElement element,
+ PresenceDevice.Builder presenceDeviceBuilder) {
+ int endIndex = element.hasValueLength() ? element.getValueLength() :
+ element.getValue().size();
+ switch (element.getKey()) {
+ case DE_FAST_PAIR_ACCOUNT_KEY:
+ presenceDeviceBuilder.addExtendedProperty(
+ new DataElement(DataElement.DataType.ACCOUNT_KEY_DATA,
+ element.getValue().substring(0, endIndex).toByteArray()));
+ break;
+ case DE_CONNECTION_STATUS:
+ presenceDeviceBuilder.addExtendedProperty(
+ new DataElement(DataElement.DataType.CONNECTION_STATUS,
+ element.getValue().substring(0, endIndex).toByteArray()));
+ break;
+ case DE_BATTERY_STATUS:
+ presenceDeviceBuilder.addExtendedProperty(
+ new DataElement(DataElement.DataType.BATTERY,
+ element.getValue().substring(0, endIndex).toByteArray()));
+ break;
+ default:
+ break;
+ }
+ }
}
}
diff --git a/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java b/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java
index bdeab51..41d5686 100644
--- a/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java
+++ b/nearby/service/java/com/android/server/nearby/provider/DiscoveryProviderManager.java
@@ -23,8 +23,10 @@
import android.annotation.Nullable;
import android.app.AppOpsManager;
import android.content.Context;
+import android.nearby.DataElement;
import android.nearby.IScanListener;
import android.nearby.NearbyDeviceParcelable;
+import android.nearby.NearbyManager;
import android.nearby.PresenceScanFilter;
import android.nearby.ScanFilter;
import android.nearby.ScanRequest;
@@ -33,6 +35,7 @@
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.nearby.injector.Injector;
import com.android.server.nearby.metrics.NearbyMetrics;
import com.android.server.nearby.presence.PresenceDiscoveryResult;
@@ -54,7 +57,9 @@
protected final Object mLock = new Object();
private final Context mContext;
private final BleDiscoveryProvider mBleDiscoveryProvider;
- @Nullable private final ChreDiscoveryProvider mChreDiscoveryProvider;
+ @VisibleForTesting
+ @Nullable
+ final ChreDiscoveryProvider mChreDiscoveryProvider;
private @ScanRequest.ScanMode int mScanMode;
private final Injector mInjector;
@@ -92,10 +97,9 @@
scanFilter.getType()
== SCAN_TYPE_NEARBY_PRESENCE)
.collect(Collectors.toList());
- Log.i(
- TAG,
- String.format("match with filters size: %d", presenceFilters.size()));
if (!presenceFilterMatches(nearbyDevice, presenceFilters)) {
+ Log.d(TAG, "presence filter does not match for "
+ + "the scanned Presence Device");
continue;
}
}
@@ -119,40 +123,73 @@
Executor executor = Executors.newSingleThreadExecutor();
mChreDiscoveryProvider =
new ChreDiscoveryProvider(
- mContext, new ChreCommunication(injector, executor), executor);
+ mContext, new ChreCommunication(injector, mContext, executor), executor);
mScanTypeScanListenerRecordMap = new HashMap<>();
mInjector = injector;
}
+ @VisibleForTesting
+ DiscoveryProviderManager(Context context, Injector injector,
+ BleDiscoveryProvider bleDiscoveryProvider,
+ ChreDiscoveryProvider chreDiscoveryProvider,
+ Map<IBinder, ScanListenerRecord> scanTypeScanListenerRecordMap) {
+ mContext = context;
+ mInjector = injector;
+ mBleDiscoveryProvider = bleDiscoveryProvider;
+ mChreDiscoveryProvider = chreDiscoveryProvider;
+ mScanTypeScanListenerRecordMap = scanTypeScanListenerRecordMap;
+ }
+
+ /** Called after boot completed. */
+ public void init() {
+ if (mInjector.getContextHubManager() != null) {
+ mChreDiscoveryProvider.init();
+ }
+ mChreDiscoveryProvider.getController().setListener(this);
+ }
+
/**
* Registers the listener in the manager and starts scan according to the requested scan mode.
*/
- public boolean registerScanListener(ScanRequest scanRequest, IScanListener listener,
+ @NearbyManager.ScanStatus
+ public int registerScanListener(ScanRequest scanRequest, IScanListener listener,
CallerIdentity callerIdentity) {
synchronized (mLock) {
+ ScanListenerDeathRecipient deathRecipient = (listener != null)
+ ? new ScanListenerDeathRecipient(listener) : null;
IBinder listenerBinder = listener.asBinder();
+ if (listenerBinder != null && deathRecipient != null) {
+ try {
+ listenerBinder.linkToDeath(deathRecipient, 0);
+ } catch (RemoteException e) {
+ throw new IllegalArgumentException("Can't link to scan listener's death");
+ }
+ }
if (mScanTypeScanListenerRecordMap.containsKey(listener.asBinder())) {
ScanRequest savedScanRequest =
mScanTypeScanListenerRecordMap.get(listenerBinder).getScanRequest();
if (scanRequest.equals(savedScanRequest)) {
Log.d(TAG, "Already registered the scanRequest: " + scanRequest);
- return true;
+ return NearbyManager.ScanStatus.SUCCESS;
}
}
ScanListenerRecord scanListenerRecord =
- new ScanListenerRecord(scanRequest, listener, callerIdentity);
+ new ScanListenerRecord(scanRequest, listener, callerIdentity, deathRecipient);
mScanTypeScanListenerRecordMap.put(listenerBinder, scanListenerRecord);
- if (!startProviders(scanRequest)) {
- return false;
+ Boolean started = startProviders(scanRequest);
+ if (started == null) {
+ return NearbyManager.ScanStatus.UNKNOWN;
}
-
+ if (!started) {
+ return NearbyManager.ScanStatus.ERROR;
+ }
NearbyMetrics.logScanStarted(scanListenerRecord.hashCode(), scanRequest);
if (mScanMode < scanRequest.getScanMode()) {
mScanMode = scanRequest.getScanMode();
invalidateProviderScanMode();
}
- return true;
+ return NearbyManager.ScanStatus.SUCCESS;
}
}
@@ -172,6 +209,10 @@
ScanListenerRecord removedRecord =
mScanTypeScanListenerRecordMap.remove(listenerBinder);
+ ScanListenerDeathRecipient deathRecipient = removedRecord.getDeathRecipient();
+ if (listenerBinder != null && deathRecipient != null) {
+ listenerBinder.unlinkToDeath(removedRecord.getDeathRecipient(), 0);
+ }
Log.v(TAG, "DiscoveryProviderManager unregistered scan listener.");
NearbyMetrics.logScanStopped(removedRecord.hashCode(), removedRecord.getScanRequest());
if (mScanTypeScanListenerRecordMap.isEmpty()) {
@@ -203,34 +244,91 @@
}
}
- // Returns false when fail to start all the providers. Returns true if any one of the provider
- // starts successfully.
- private boolean startProviders(ScanRequest scanRequest) {
- if (scanRequest.isBleEnabled()) {
- if (mChreDiscoveryProvider.available()
- && scanRequest.getScanType() == SCAN_TYPE_NEARBY_PRESENCE) {
- startChreProvider();
- } else {
- startBleProvider(scanRequest);
+ /**
+ * @return {@code null} when all providers are initializing
+ * {@code false} when fail to start all the providers
+ * {@code true} when any one of the provider starts successfully
+ */
+ @VisibleForTesting
+ @Nullable
+ Boolean startProviders(ScanRequest scanRequest) {
+ if (!scanRequest.isBleEnabled()) {
+ Log.w(TAG, "failed to start any provider because client disabled BLE");
+ return false;
+ }
+ List<ScanFilter> scanFilters = getPresenceScanFilters();
+ boolean chreOnly = isChreOnly(scanFilters);
+ Boolean chreAvailable = mChreDiscoveryProvider.available();
+ if (chreAvailable == null) {
+ if (chreOnly) {
+ Log.w(TAG, "client wants CHRE only and Nearby service is still querying CHRE"
+ + " status");
+ return null;
}
+ startBleProvider(scanFilters);
return true;
}
+
+ if (!chreAvailable) {
+ if (chreOnly) {
+ Log.w(TAG, "failed to start any provider because client wants CHRE only and CHRE"
+ + " is not available");
+ return false;
+ }
+ startBleProvider(scanFilters);
+ return true;
+ }
+
+ if (scanRequest.getScanType() == SCAN_TYPE_NEARBY_PRESENCE) {
+ startChreProvider(scanFilters);
+ return true;
+ }
+
+ startBleProvider(scanFilters);
+ return true;
+ }
+
+ private static boolean isChreOnly(List<ScanFilter> scanFilters) {
+ for (ScanFilter scanFilter : scanFilters) {
+ List<DataElement> dataElements =
+ ((PresenceScanFilter) scanFilter).getExtendedProperties();
+ for (DataElement dataElement : dataElements) {
+ if (dataElement.getKey() != DataElement.DataType.SCAN_MODE) {
+ continue;
+ }
+ byte[] scanModeValue = dataElement.getValue();
+ if (scanModeValue == null || scanModeValue.length == 0) {
+ break;
+ }
+ if (Byte.toUnsignedInt(scanModeValue[0]) == ScanRequest.SCAN_MODE_CHRE_ONLY) {
+ return true;
+ }
+ }
+
+ }
return false;
}
- private void startBleProvider(ScanRequest scanRequest) {
+ private void startBleProvider(List<ScanFilter> scanFilters) {
if (!mBleDiscoveryProvider.getController().isStarted()) {
Log.d(TAG, "DiscoveryProviderManager starts Ble scanning.");
- mBleDiscoveryProvider.getController().start();
mBleDiscoveryProvider.getController().setListener(this);
- mBleDiscoveryProvider.getController().setProviderScanMode(scanRequest.getScanMode());
+ mBleDiscoveryProvider.getController().setProviderScanMode(mScanMode);
+ mBleDiscoveryProvider.getController().setProviderScanFilters(scanFilters);
+ mBleDiscoveryProvider.getController().start();
}
}
- private void startChreProvider() {
+ @VisibleForTesting
+ void startChreProvider(List<ScanFilter> scanFilters) {
Log.d(TAG, "DiscoveryProviderManager starts CHRE scanning.");
+ mChreDiscoveryProvider.getController().setProviderScanFilters(scanFilters);
+ mChreDiscoveryProvider.getController().setProviderScanMode(mScanMode);
+ mChreDiscoveryProvider.getController().start();
+ }
+
+ private List<ScanFilter> getPresenceScanFilters() {
synchronized (mLock) {
- mChreDiscoveryProvider.getController().setListener(this);
List<ScanFilter> scanFilters = new ArrayList();
for (IBinder listenerBinder : mScanTypeScanListenerRecordMap.keySet()) {
ScanListenerRecord record = mScanTypeScanListenerRecordMap.get(listenerBinder);
@@ -242,9 +340,7 @@
.collect(Collectors.toList());
scanFilters.addAll(presenceFilters);
}
- mChreDiscoveryProvider.getController().setProviderScanFilters(scanFilters);
- mChreDiscoveryProvider.getController().setProviderScanMode(mScanMode);
- mChreDiscoveryProvider.getController().start();
+ return scanFilters;
}
}
@@ -257,11 +353,13 @@
mBleDiscoveryProvider.getController().stop();
}
- private void stopChreProvider() {
+ @VisibleForTesting
+ protected void stopChreProvider() {
mChreDiscoveryProvider.getController().stop();
}
- private void invalidateProviderScanMode() {
+ @VisibleForTesting
+ void invalidateProviderScanMode() {
if (mBleDiscoveryProvider.getController().isStarted()) {
mBleDiscoveryProvider.getController().setProviderScanMode(mScanMode);
} else {
@@ -272,7 +370,8 @@
}
}
- private static boolean presenceFilterMatches(
+ @VisibleForTesting
+ static boolean presenceFilterMatches(
NearbyDeviceParcelable device, List<ScanFilter> scanFilters) {
if (scanFilters.isEmpty()) {
return true;
@@ -287,7 +386,22 @@
return false;
}
- private static class ScanListenerRecord {
+ class ScanListenerDeathRecipient implements IBinder.DeathRecipient {
+ public IScanListener listener;
+
+ ScanListenerDeathRecipient(IScanListener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ public void binderDied() {
+ Log.d(TAG, "Binder is dead - unregistering scan listener");
+ unregisterScanListener(listener);
+ }
+ }
+
+ @VisibleForTesting
+ static class ScanListenerRecord {
private final ScanRequest mScanRequest;
@@ -295,11 +409,14 @@
private final CallerIdentity mCallerIdentity;
+ private final ScanListenerDeathRecipient mDeathRecipient;
+
ScanListenerRecord(ScanRequest scanRequest, IScanListener iScanListener,
- CallerIdentity callerIdentity) {
+ CallerIdentity callerIdentity, ScanListenerDeathRecipient deathRecipient) {
mScanListener = iScanListener;
mScanRequest = scanRequest;
mCallerIdentity = callerIdentity;
+ mDeathRecipient = deathRecipient;
}
IScanListener getScanListener() {
@@ -314,6 +431,10 @@
return mCallerIdentity;
}
+ ScanListenerDeathRecipient getDeathRecipient() {
+ return mDeathRecipient;
+ }
+
@Override
public boolean equals(Object other) {
if (other instanceof ScanListenerRecord) {
diff --git a/nearby/service/java/com/android/server/nearby/provider/FastPairDataProvider.java b/nearby/service/java/com/android/server/nearby/provider/FastPairDataProvider.java
index 0f99a2f..d925f07 100644
--- a/nearby/service/java/com/android/server/nearby/provider/FastPairDataProvider.java
+++ b/nearby/service/java/com/android/server/nearby/provider/FastPairDataProvider.java
@@ -30,7 +30,7 @@
import androidx.annotation.WorkerThread;
-import com.android.server.nearby.common.bloomfilter.BloomFilter;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.nearby.fastpair.footprint.FastPairUploadInfo;
import java.util.ArrayList;
@@ -80,6 +80,11 @@
}
}
+ @VisibleForTesting
+ void setProxyDataProvider(ProxyFastPairDataProvider proxyFastPairDataProvider) {
+ this.mProxyFastPairDataProvider = proxyFastPairDataProvider;
+ }
+
/**
* Loads FastPairAntispoofKeyDeviceMetadata.
*
@@ -136,14 +141,6 @@
}
/**
- * Get recognized device from bloom filter.
- */
- public Data.FastPairDeviceWithAccountKey getRecognizedDevice(BloomFilter bloomFilter,
- byte[] salt) {
- return Data.FastPairDeviceWithAccountKey.newBuilder().build();
- }
-
- /**
* Loads FastPair device accountKeys for a given account, but not other detailed fields.
*
* @throws IllegalStateException If ProxyFastPairDataProvider is not available.
diff --git a/nearby/service/java/com/android/server/nearby/util/DataUtils.java b/nearby/service/java/com/android/server/nearby/util/DataUtils.java
index 8bb83e9..c3bae08 100644
--- a/nearby/service/java/com/android/server/nearby/util/DataUtils.java
+++ b/nearby/service/java/com/android/server/nearby/util/DataUtils.java
@@ -57,11 +57,9 @@
*/
public static String toString(ScanFastPairStoreItem item) {
return "ScanFastPairStoreItem=[address:" + item.getAddress()
- + ", actionUr:" + item.getActionUrl()
+ + ", actionUrl:" + item.getActionUrl()
+ ", deviceName:" + item.getDeviceName()
- + ", iconPng:" + item.getIconPng()
+ ", iconFifeUrl:" + item.getIconFifeUrl()
- + ", antiSpoofingKeyPair:" + item.getAntiSpoofingPublicKey()
+ ", fastPairStrings:" + toString(item.getFastPairStrings())
+ "]";
}
diff --git a/nearby/service/java/com/android/server/nearby/util/FastPairDecoder.java b/nearby/service/java/com/android/server/nearby/util/FastPairDecoder.java
deleted file mode 100644
index 6021ff6..0000000
--- a/nearby/service/java/com/android/server/nearby/util/FastPairDecoder.java
+++ /dev/null
@@ -1,258 +0,0 @@
-/*
- * 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.util;
-
-import android.annotation.Nullable;
-import android.bluetooth.le.ScanRecord;
-import android.os.ParcelUuid;
-import android.util.SparseArray;
-
-import com.android.server.nearby.common.ble.BleFilter;
-import com.android.server.nearby.common.ble.BleRecord;
-
-import java.util.Arrays;
-
-/**
- * Parses Fast Pair information out of {@link BleRecord}s.
- *
- * <p>There are 2 different packet formats that are supported, which is used can be determined by
- * packet length:
- *
- * <p>For 3-byte packets, the full packet is the model ID.
- *
- * <p>For all other packets, the first byte is the header, followed by the model ID, followed by
- * zero or more extra fields. Each field has its own header byte followed by the field value. The
- * packet header is formatted as 0bVVVLLLLR (V = version, L = model ID length, R = reserved) and
- * each extra field header is 0bLLLLTTTT (L = field length, T = field type).
- */
-public class FastPairDecoder {
-
- private static final int FIELD_TYPE_BLOOM_FILTER = 0;
- private static final int FIELD_TYPE_BLOOM_FILTER_SALT = 1;
- private static final int FIELD_TYPE_BLOOM_FILTER_NO_NOTIFICATION = 2;
- private static final int FIELD_TYPE_BATTERY = 3;
- private static final int FIELD_TYPE_BATTERY_NO_NOTIFICATION = 4;
- public static final int FIELD_TYPE_CONNECTION_STATE = 5;
- private static final int FIELD_TYPE_RANDOM_RESOLVABLE_DATA = 6;
-
-
- /** FE2C is the 16-bit Service UUID. The rest is the base UUID. See BluetoothUuid (hidden). */
- private static final ParcelUuid FAST_PAIR_SERVICE_PARCEL_UUID =
- ParcelUuid.fromString("0000FE2C-0000-1000-8000-00805F9B34FB");
-
- /** The filter you use to scan for Fast Pair BLE advertisements. */
- public static final BleFilter FILTER =
- new BleFilter.Builder().setServiceData(FAST_PAIR_SERVICE_PARCEL_UUID,
- new byte[0]).build();
-
- // NOTE: Ensure that all bitmasks are always ints, not bytes so that bitshifting works correctly
- // without needing worry about signing errors.
- private static final int HEADER_VERSION_BITMASK = 0b11100000;
- private static final int HEADER_LENGTH_BITMASK = 0b00011110;
- private static final int HEADER_VERSION_OFFSET = 5;
- private static final int HEADER_LENGTH_OFFSET = 1;
-
- private static final int EXTRA_FIELD_LENGTH_BITMASK = 0b11110000;
- private static final int EXTRA_FIELD_TYPE_BITMASK = 0b00001111;
- private static final int EXTRA_FIELD_LENGTH_OFFSET = 4;
- private static final int EXTRA_FIELD_TYPE_OFFSET = 0;
-
- private static final int MIN_ID_LENGTH = 3;
- private static final int MAX_ID_LENGTH = 14;
- private static final int HEADER_INDEX = 0;
- private static final int HEADER_LENGTH = 1;
- private static final int FIELD_HEADER_LENGTH = 1;
-
- // Not using java.util.IllegalFormatException because it is unchecked.
- private static class IllegalFormatException extends Exception {
- private IllegalFormatException(String message) {
- super(message);
- }
- }
-
- /**
- * Gets model id data from broadcast
- */
- @Nullable
- public static byte[] getModelId(@Nullable byte[] serviceData) {
- if (serviceData == null) {
- return null;
- }
-
- if (serviceData.length >= MIN_ID_LENGTH) {
- if (serviceData.length == MIN_ID_LENGTH) {
- // If the length == 3, all bytes are the ID. See flag docs for more about
- // endianness.
- return serviceData;
- } else {
- // Otherwise, the first byte is a header which contains the length of the big-endian
- // model ID that follows. The model ID will be trimmed if it contains leading zeros.
- int idIndex = 1;
- int end = idIndex + getIdLength(serviceData);
- while (serviceData[idIndex] == 0 && end - idIndex > MIN_ID_LENGTH) {
- idIndex++;
- }
- return Arrays.copyOfRange(serviceData, idIndex, end);
- }
- }
- return null;
- }
-
- /** Gets the FastPair service data array if available, otherwise returns null. */
- @Nullable
- public static byte[] getServiceDataArray(BleRecord bleRecord) {
- return bleRecord.getServiceData(FAST_PAIR_SERVICE_PARCEL_UUID);
- }
-
- /** Gets the FastPair service data array if available, otherwise returns null. */
- @Nullable
- public static byte[] getServiceDataArray(ScanRecord scanRecord) {
- return scanRecord.getServiceData(FAST_PAIR_SERVICE_PARCEL_UUID);
- }
-
- /** Gets the bloom filter from the extra fields if available, otherwise returns null. */
- @Nullable
- public static byte[] getBloomFilter(@Nullable byte[] serviceData) {
- return getExtraField(serviceData, FIELD_TYPE_BLOOM_FILTER);
- }
-
- /** Gets the bloom filter salt from the extra fields if available, otherwise returns null. */
- @Nullable
- public static byte[] getBloomFilterSalt(byte[] serviceData) {
- return getExtraField(serviceData, FIELD_TYPE_BLOOM_FILTER_SALT);
- }
-
- /**
- * Gets the suppress notification with bloom filter from the extra fields if available,
- * otherwise returns null.
- */
- @Nullable
- public static byte[] getBloomFilterNoNotification(@Nullable byte[] serviceData) {
- return getExtraField(serviceData, FIELD_TYPE_BLOOM_FILTER_NO_NOTIFICATION);
- }
-
- /**
- * Get random resolvableData
- */
- @Nullable
- public static byte[] getRandomResolvableData(byte[] serviceData) {
- return getExtraField(serviceData, FIELD_TYPE_RANDOM_RESOLVABLE_DATA);
- }
-
- @Nullable
- private static byte[] getExtraField(@Nullable byte[] serviceData, int fieldId) {
- if (serviceData == null || serviceData.length < HEADER_INDEX + HEADER_LENGTH) {
- return null;
- }
- try {
- return getExtraFields(serviceData).get(fieldId);
- } catch (IllegalFormatException e) {
- return null;
- }
- }
-
- /** Gets extra field data at the end of the packet, defined by the extra field header. */
- private static SparseArray<byte[]> getExtraFields(byte[] serviceData)
- throws IllegalFormatException {
- SparseArray<byte[]> extraFields = new SparseArray<>();
- if (getVersion(serviceData) != 0) {
- return extraFields;
- }
- int headerIndex = getFirstExtraFieldHeaderIndex(serviceData);
- while (headerIndex < serviceData.length) {
- int length = getExtraFieldLength(serviceData, headerIndex);
- int index = headerIndex + FIELD_HEADER_LENGTH;
- int type = getExtraFieldType(serviceData, headerIndex);
- int end = index + length;
- if (extraFields.get(type) == null) {
- if (end <= serviceData.length) {
- extraFields.put(type, Arrays.copyOfRange(serviceData, index, end));
- } else {
- throw new IllegalFormatException(
- "Invalid length, " + end + " is longer than service data size "
- + serviceData.length);
- }
- }
- headerIndex = end;
- }
- return extraFields;
- }
-
- /** Checks whether or not a valid ID is included in the service data packet. */
- public static boolean hasBeaconIdBytes(BleRecord bleRecord) {
- byte[] serviceData = bleRecord.getServiceData(FAST_PAIR_SERVICE_PARCEL_UUID);
- return checkModelId(serviceData);
- }
-
- /** Check whether byte array is FastPair model id or not. */
- public static boolean checkModelId(@Nullable byte[] scanResult) {
- return scanResult != null
- // The 3-byte format has no header byte (all bytes are the ID).
- && (scanResult.length == MIN_ID_LENGTH
- // Header byte exists. We support only format version 0. (A different version
- // indicates
- // a breaking change in the format.)
- || (scanResult.length > MIN_ID_LENGTH
- && getVersion(scanResult) == 0
- && isIdLengthValid(scanResult)));
- }
-
- /** Checks whether or not bloom filter is included in the service data packet. */
- public static boolean hasBloomFilter(BleRecord bleRecord) {
- return (getBloomFilter(getServiceDataArray(bleRecord)) != null
- || getBloomFilterNoNotification(getServiceDataArray(bleRecord)) != null);
- }
-
- /** Checks whether or not bloom filter is included in the service data packet. */
- public static boolean hasBloomFilter(ScanRecord scanRecord) {
- return (getBloomFilter(getServiceDataArray(scanRecord)) != null
- || getBloomFilterNoNotification(getServiceDataArray(scanRecord)) != null);
- }
-
- private static int getVersion(byte[] serviceData) {
- return serviceData.length == MIN_ID_LENGTH
- ? 0
- : (serviceData[HEADER_INDEX] & HEADER_VERSION_BITMASK) >> HEADER_VERSION_OFFSET;
- }
-
- private static int getIdLength(byte[] serviceData) {
- return serviceData.length == MIN_ID_LENGTH
- ? MIN_ID_LENGTH
- : (serviceData[HEADER_INDEX] & HEADER_LENGTH_BITMASK) >> HEADER_LENGTH_OFFSET;
- }
-
- private static int getFirstExtraFieldHeaderIndex(byte[] serviceData) {
- return HEADER_INDEX + HEADER_LENGTH + getIdLength(serviceData);
- }
-
- private static int getExtraFieldLength(byte[] serviceData, int extraFieldIndex) {
- return (serviceData[extraFieldIndex] & EXTRA_FIELD_LENGTH_BITMASK)
- >> EXTRA_FIELD_LENGTH_OFFSET;
- }
-
- private static int getExtraFieldType(byte[] serviceData, int extraFieldIndex) {
- return (serviceData[extraFieldIndex] & EXTRA_FIELD_TYPE_BITMASK) >> EXTRA_FIELD_TYPE_OFFSET;
- }
-
- private static boolean isIdLengthValid(byte[] serviceData) {
- int idLength = getIdLength(serviceData);
- return MIN_ID_LENGTH <= idLength
- && idLength <= MAX_ID_LENGTH
- && idLength + HEADER_LENGTH <= serviceData.length;
- }
-}
-
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/Cryptor.java b/nearby/service/java/com/android/server/nearby/util/encryption/Cryptor.java
new file mode 100644
index 0000000..3c5132d
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/util/encryption/Cryptor.java
@@ -0,0 +1,149 @@
+/*
+ * 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.util.encryption;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+/** Class for encryption/decryption functionality. */
+public abstract class Cryptor {
+
+ /** AES only supports key sizes of 16, 24 or 32 bytes. */
+ static final int AUTHENTICITY_KEY_BYTE_SIZE = 16;
+
+ private static final String HMAC_SHA256_ALGORITHM = "HmacSHA256";
+
+ /**
+ * Encrypt the provided data blob.
+ *
+ * @param data data blob to be encrypted.
+ * @param salt used for IV
+ * @param secretKeyBytes secrete key accessed from credentials
+ * @return encrypted data, {@code null} if failed to encrypt.
+ */
+ @Nullable
+ public byte[] encrypt(byte[] data, byte[] salt, byte[] secretKeyBytes) {
+ return data;
+ }
+
+ /**
+ * Decrypt the original data blob from the provided byte array.
+ *
+ * @param encryptedData data blob to be decrypted.
+ * @param salt used for IV
+ * @param secretKeyBytes secrete key accessed from credentials
+ * @return decrypted data, {@code null} if failed to decrypt.
+ */
+ @Nullable
+ public byte[] decrypt(byte[] encryptedData, byte[] salt, byte[] secretKeyBytes) {
+ return encryptedData;
+ }
+
+ /**
+ * Generates a digital signature for the data.
+ *
+ * @return signature {@code null} if failed to sign
+ */
+ @Nullable
+ public byte[] sign(byte[] data, byte[] key) {
+ return new byte[0];
+ }
+
+ /**
+ * Verifies the signature generated by data and key, with the original signed data
+ */
+ public boolean verify(byte[] data, byte[] key, byte[] signature) {
+ return true;
+ }
+
+ /**
+ * @return length of the signature generated
+ */
+ public int getSignatureLength() {
+ return 0;
+ }
+
+ /**
+ * A HAMC sha256 based HKDF algorithm to pseudo randomly hash data and salt into a byte array of
+ * given size.
+ */
+ // Based on google3/third_party/tink/java/src/main/java/com/google/crypto/tink/subtle/Hkdf.java
+ @Nullable
+ static byte[] computeHkdf(byte[] ikm, byte[] salt, int size) {
+ Mac mac;
+ try {
+ mac = Mac.getInstance(HMAC_SHA256_ALGORITHM);
+ } catch (NoSuchAlgorithmException e) {
+ Log.w(TAG, "HMAC_SHA256_ALGORITHM is not supported.", e);
+ return null;
+ }
+
+ if (size > 255 * mac.getMacLength()) {
+ Log.w(TAG, "Size too large.");
+ return null;
+ }
+
+ if (salt.length == 0) {
+ Log.w(TAG, "Salt cannot be empty.");
+ return null;
+ }
+
+ try {
+ mac.init(new SecretKeySpec(salt, HMAC_SHA256_ALGORITHM));
+ } catch (InvalidKeyException e) {
+ Log.w(TAG, "Invalid key.", e);
+ return null;
+ }
+
+ byte[] prk = mac.doFinal(ikm);
+ byte[] result = new byte[size];
+ try {
+ mac.init(new SecretKeySpec(prk, HMAC_SHA256_ALGORITHM));
+ } catch (InvalidKeyException e) {
+ Log.w(TAG, "Invalid key.", e);
+ return null;
+ }
+
+ byte[] digest = new byte[0];
+ int ctr = 1;
+ int pos = 0;
+ while (true) {
+ mac.update(digest);
+ mac.update((byte) ctr);
+ digest = mac.doFinal();
+ if (pos + digest.length < size) {
+ System.arraycopy(digest, 0, result, pos, digest.length);
+ pos += digest.length;
+ ctr++;
+ } else {
+ System.arraycopy(digest, 0, result, pos, size - pos);
+ break;
+ }
+ }
+
+ return result;
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpFake.java b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpFake.java
new file mode 100644
index 0000000..1c0ec9e
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpFake.java
@@ -0,0 +1,39 @@
+/*
+ * 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.util.encryption;
+
+import androidx.annotation.Nullable;
+
+/**
+ * A Cryptor that returns the original data without actual encryption
+ */
+public class CryptorImpFake extends Cryptor {
+ // Lazily instantiated when {@link #getInstance()} is called.
+ @Nullable
+ private static CryptorImpFake sCryptor;
+
+ /** Returns an instance of CryptorImpFake. */
+ public static CryptorImpFake getInstance() {
+ if (sCryptor == null) {
+ sCryptor = new CryptorImpFake();
+ }
+ return sCryptor;
+ }
+
+ private CryptorImpFake() {
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpIdentityV1.java b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpIdentityV1.java
new file mode 100644
index 0000000..b0e19b4
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpIdentityV1.java
@@ -0,0 +1,208 @@
+/*
+ * 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.util.encryption;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.security.keystore.KeyProperties;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * {@link android.nearby.BroadcastRequest#PRESENCE_VERSION_V1} for identity
+ * encryption and decryption.
+ */
+public class CryptorImpIdentityV1 extends Cryptor {
+
+ // 3 16 byte arrays known by both the encryptor and decryptor.
+ private static final byte[] EK_IV =
+ new byte[] {14, -123, -39, 42, 109, 127, 83, 27, 27, 11, 91, -38, 92, 17, -84, 66};
+ private static final byte[] ESALT_IV =
+ new byte[] {46, 83, -19, 10, -127, -31, -31, 12, 31, 76, 63, -9, 33, -66, 15, -10};
+ private static final byte[] KTAG_IV =
+ {-22, -83, -6, 67, 16, -99, -13, -9, 8, -3, -16, 37, -75, 47, 1, -56};
+
+ /** Length of encryption key required by AES/GCM encryption. */
+ private static final int ENCRYPTION_KEY_SIZE = 32;
+
+ /** Length of salt required by AES/GCM encryption. */
+ private static final int AES_CTR_IV_SIZE = 16;
+
+ /** Length HMAC tag */
+ public static final int HMAC_TAG_SIZE = 8;
+
+ /**
+ * In the form of "algorithm/mode/padding". Must be the same across broadcast and scan devices.
+ */
+ private static final String CIPHER_ALGORITHM = "AES/CTR/NoPadding";
+
+ @VisibleForTesting
+ static final String ENCRYPT_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES;
+
+ // Lazily instantiated when {@link #getInstance()} is called.
+ @Nullable private static CryptorImpIdentityV1 sCryptor;
+
+ /** Returns an instance of CryptorImpIdentityV1. */
+ public static CryptorImpIdentityV1 getInstance() {
+ if (sCryptor == null) {
+ sCryptor = new CryptorImpIdentityV1();
+ }
+ return sCryptor;
+ }
+
+ @Nullable
+ @Override
+ public byte[] encrypt(byte[] data, byte[] salt, byte[] authenticityKey) {
+ if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
+ Log.w(TAG, "Illegal authenticity key size");
+ return null;
+ }
+
+ // Generates a 32 bytes encryption key from authenticity_key
+ byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, EK_IV, ENCRYPTION_KEY_SIZE);
+ if (encryptionKey == null) {
+ Log.e(TAG, "Failed to generate encryption key.");
+ return null;
+ }
+
+ // Encrypts the data using the encryption key
+ SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
+ Cipher cipher;
+ try {
+ cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+ Log.e(TAG, "Failed to encrypt with secret key.", e);
+ return null;
+ }
+ byte[] esalt = Cryptor.computeHkdf(salt, ESALT_IV, AES_CTR_IV_SIZE);
+ if (esalt == null) {
+ Log.e(TAG, "Failed to generate salt.");
+ return null;
+ }
+ try {
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(esalt));
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+ Log.e(TAG, "Failed to initialize cipher.", e);
+ return null;
+ }
+ try {
+ return cipher.doFinal(data);
+ } catch (IllegalBlockSizeException | BadPaddingException e) {
+ Log.e(TAG, "Failed to encrypt with secret key.", e);
+ return null;
+ }
+ }
+
+ @Nullable
+ @Override
+ public byte[] decrypt(byte[] encryptedData, byte[] salt, byte[] authenticityKey) {
+ if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
+ Log.w(TAG, "Illegal authenticity key size");
+ return null;
+ }
+
+ // Generates a 32 bytes encryption key from authenticity_key
+ byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, EK_IV, ENCRYPTION_KEY_SIZE);
+ if (encryptionKey == null) {
+ Log.e(TAG, "Failed to generate encryption key.");
+ return null;
+ }
+
+ // Decrypts the data using the encryption key
+ SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
+ Cipher cipher;
+ try {
+ cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+ Log.e(TAG, "Failed to get cipher instance.", e);
+ return null;
+ }
+ byte[] esalt = Cryptor.computeHkdf(salt, ESALT_IV, AES_CTR_IV_SIZE);
+ if (esalt == null) {
+ return null;
+ }
+ try {
+ cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(esalt));
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+ Log.e(TAG, "Failed to initialize cipher.", e);
+ return null;
+ }
+
+ try {
+ return cipher.doFinal(encryptedData);
+ } catch (IllegalBlockSizeException | BadPaddingException e) {
+ Log.e(TAG, "Failed to decrypt bytes with secret key.", e);
+ return null;
+ }
+ }
+
+ /**
+ * Generates a digital signature for the data.
+ *
+ * @return signature {@code null} if failed to sign
+ */
+ @Nullable
+ @Override
+ public byte[] sign(byte[] data, byte[] salt) {
+ if (data == null) {
+ Log.e(TAG, "Not generate HMAC tag because of invalid data input.");
+ return null;
+ }
+
+ // Generates a 8 bytes HMAC tag
+ return Cryptor.computeHkdf(data, salt, HMAC_TAG_SIZE);
+ }
+
+ /**
+ * Generates a digital signature for the data.
+ * Uses KTAG_IV as salt value.
+ */
+ @Nullable
+ public byte[] sign(byte[] data) {
+ // Generates a 8 bytes HMAC tag
+ return sign(data, KTAG_IV);
+ }
+
+ @Override
+ public boolean verify(byte[] data, byte[] key, byte[] signature) {
+ return Arrays.equals(sign(data, key), signature);
+ }
+
+ /**
+ * Verifies the signature generated by data and key, with the original signed data. Uses
+ * KTAG_IV as salt value.
+ */
+ public boolean verify(byte[] data, byte[] signature) {
+ return verify(data, KTAG_IV, signature);
+ }
+}
diff --git a/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpV1.java b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpV1.java
new file mode 100644
index 0000000..15073fb
--- /dev/null
+++ b/nearby/service/java/com/android/server/nearby/util/encryption/CryptorImpV1.java
@@ -0,0 +1,212 @@
+/*
+ * 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.util.encryption;
+
+import static com.android.server.nearby.NearbyService.TAG;
+
+import android.security.keystore.KeyProperties;
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * {@link android.nearby.BroadcastRequest#PRESENCE_VERSION_V1} for encryption and decryption.
+ */
+public class CryptorImpV1 extends Cryptor {
+
+ /**
+ * In the form of "algorithm/mode/padding". Must be the same across broadcast and scan devices.
+ */
+ private static final String CIPHER_ALGORITHM = "AES/CTR/NoPadding";
+
+ @VisibleForTesting
+ static final String ENCRYPT_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES;
+
+ /** Length of encryption key required by AES/GCM encryption. */
+ private static final int ENCRYPTION_KEY_SIZE = 32;
+
+ /** Length of salt required by AES/GCM encryption. */
+ private static final int AES_CTR_IV_SIZE = 16;
+
+ /** Length HMAC tag */
+ public static final int HMAC_TAG_SIZE = 16;
+
+ // 3 16 byte arrays known by both the encryptor and decryptor.
+ private static final byte[] AK_IV =
+ new byte[] {12, -59, 19, 23, 96, 57, -59, 19, 117, -31, -116, -61, 86, -25, -33, -78};
+ private static final byte[] ASALT_IV =
+ new byte[] {111, 48, -83, -79, -10, -102, -16, 73, 43, 55, 102, -127, 58, -19, -113, 4};
+ private static final byte[] HK_IV =
+ new byte[] {12, -59, 19, 23, 96, 57, -59, 19, 117, -31, -116, -61, 86, -25, -33, -78};
+
+ // Lazily instantiated when {@link #getInstance()} is called.
+ @Nullable private static CryptorImpV1 sCryptor;
+
+ /** Returns an instance of CryptorImpV1. */
+ public static CryptorImpV1 getInstance() {
+ if (sCryptor == null) {
+ sCryptor = new CryptorImpV1();
+ }
+ return sCryptor;
+ }
+
+ private CryptorImpV1() {
+ }
+
+ @Nullable
+ @Override
+ public byte[] encrypt(byte[] data, byte[] salt, byte[] authenticityKey) {
+ if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
+ Log.w(TAG, "Illegal authenticity key size");
+ return null;
+ }
+
+ // Generates a 32 bytes encryption key from authenticity_key
+ byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, AK_IV, ENCRYPTION_KEY_SIZE);
+ if (encryptionKey == null) {
+ Log.e(TAG, "Failed to generate encryption key.");
+ return null;
+ }
+
+ // Encrypts the data using the encryption key
+ SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
+ Cipher cipher;
+ try {
+ cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+ Log.e(TAG, "Failed to encrypt with secret key.", e);
+ return null;
+ }
+ byte[] asalt = Cryptor.computeHkdf(salt, ASALT_IV, AES_CTR_IV_SIZE);
+ if (asalt == null) {
+ Log.e(TAG, "Failed to generate salt.");
+ return null;
+ }
+ try {
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(asalt));
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+ Log.e(TAG, "Failed to initialize cipher.", e);
+ return null;
+ }
+ try {
+ return cipher.doFinal(data);
+ } catch (IllegalBlockSizeException | BadPaddingException e) {
+ Log.e(TAG, "Failed to encrypt with secret key.", e);
+ return null;
+ }
+ }
+
+ @Nullable
+ @Override
+ public byte[] decrypt(byte[] encryptedData, byte[] salt, byte[] authenticityKey) {
+ if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
+ Log.w(TAG, "Illegal authenticity key size");
+ return null;
+ }
+
+ // Generates a 32 bytes encryption key from authenticity_key
+ byte[] encryptionKey = Cryptor.computeHkdf(authenticityKey, AK_IV, ENCRYPTION_KEY_SIZE);
+ if (encryptionKey == null) {
+ Log.e(TAG, "Failed to generate encryption key.");
+ return null;
+ }
+
+ // Decrypts the data using the encryption key
+ SecretKey secretKey = new SecretKeySpec(encryptionKey, ENCRYPT_ALGORITHM);
+ Cipher cipher;
+ try {
+ cipher = Cipher.getInstance(CIPHER_ALGORITHM);
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+ Log.e(TAG, "Failed to get cipher instance.", e);
+ return null;
+ }
+ byte[] asalt = Cryptor.computeHkdf(salt, ASALT_IV, AES_CTR_IV_SIZE);
+ if (asalt == null) {
+ return null;
+ }
+ try {
+ cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(asalt));
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+ Log.e(TAG, "Failed to initialize cipher.", e);
+ return null;
+ }
+
+ try {
+ return cipher.doFinal(encryptedData);
+ } catch (IllegalBlockSizeException | BadPaddingException e) {
+ Log.e(TAG, "Failed to decrypt bytes with secret key.", e);
+ return null;
+ }
+ }
+
+ @Override
+ @Nullable
+ public byte[] sign(byte[] data, byte[] key) {
+ return generateHmacTag(data, key);
+ }
+
+ @Override
+ public int getSignatureLength() {
+ return HMAC_TAG_SIZE;
+ }
+
+ @Override
+ public boolean verify(byte[] data, byte[] key, byte[] signature) {
+ return Arrays.equals(sign(data, key), signature);
+ }
+
+ /** Generates a 16 bytes HMAC tag. This is used for decryptor to verify if the computed HMAC tag
+ * is equal to HMAC tag in advertisement to see data integrity. */
+ @Nullable
+ @VisibleForTesting
+ byte[] generateHmacTag(byte[] data, byte[] authenticityKey) {
+ if (data == null || authenticityKey == null) {
+ Log.e(TAG, "Not generate HMAC tag because of invalid data input.");
+ return null;
+ }
+
+ if (authenticityKey.length != AUTHENTICITY_KEY_BYTE_SIZE) {
+ Log.e(TAG, "Illegal authenticity key size");
+ return null;
+ }
+
+ // Generates a 32 bytes HMAC key from authenticity_key
+ byte[] hmacKey = Cryptor.computeHkdf(authenticityKey, HK_IV, AES_CTR_IV_SIZE);
+ if (hmacKey == null) {
+ Log.e(TAG, "Failed to generate HMAC key.");
+ return null;
+ }
+
+ // Generates a 16 bytes HMAC tag from authenticity_key
+ return Cryptor.computeHkdf(data, hmacKey, HMAC_TAG_SIZE);
+ }
+}
diff --git a/nearby/service/proto/src/presence/blefilter.proto b/nearby/service/proto/src/presence/blefilter.proto
index 9f75d34..6e1ba6d 100644
--- a/nearby/service/proto/src/presence/blefilter.proto
+++ b/nearby/service/proto/src/presence/blefilter.proto
@@ -47,6 +47,7 @@
optional bytes metadata_encryption_key_tag = 2;
}
+// Public credential returned in BleFilterResult.
message PublicCredential {
optional bytes secret_id = 1;
optional bytes authenticity_key = 2;
@@ -55,6 +56,20 @@
optional bytes encrypted_metadata_tag = 5;
}
+message DataElement {
+ enum ElementType {
+ DE_NONE = 0;
+ DE_FAST_PAIR_ACCOUNT_KEY = 9;
+ DE_CONNECTION_STATUS = 10;
+ DE_BATTERY_STATUS = 11;
+ }
+
+ optional ElementType key = 1;
+ optional bytes value = 2;
+ optional uint32 value_length = 3;
+}
+
+// A single filter used to filter BLE events.
message BleFilter {
optional uint32 id = 1; // Required, unique id of this filter.
// Maximum delay to notify the client after an event occurs.
@@ -71,7 +86,9 @@
// the period of latency defined above.
optional float distance_m = 7;
// Used to verify the list of trusted devices.
- repeated PublicateCertificate certficate = 8;
+ repeated PublicateCertificate certificate = 8;
+ // Data Elements for extended properties.
+ repeated DataElement data_element = 9;
}
message BleFilters {
@@ -80,14 +97,33 @@
// FilterResult is returned to host when a BLE event matches a Filter.
message BleFilterResult {
+ enum ResultType {
+ RESULT_NONE = 0;
+ RESULT_PRESENCE = 1;
+ RESULT_FAST_PAIR = 2;
+ }
+
optional uint32 id = 1; // id of the matched Filter.
- optional uint32 tx_power = 2;
- optional uint32 rssi = 3;
+ optional int32 tx_power = 2;
+ optional int32 rssi = 3;
optional uint32 intent = 4;
optional bytes bluetooth_address = 5;
optional PublicCredential public_credential = 6;
+ repeated DataElement data_element = 7;
+ optional bytes ble_service_data = 8;
+ optional ResultType result_type = 9;
}
message BleFilterResults {
repeated BleFilterResult result = 1;
}
+
+message BleConfig {
+ // True to start BLE scan. Otherwise, stop BLE scan.
+ optional bool start_scan = 1;
+ // True when screen is turned on. Otherwise, set to false when screen is
+ // turned off.
+ optional bool screen_on = 2;
+ // Fast Pair cache expires after this time period.
+ optional uint64 fast_pair_cache_expire_time_sec = 3;
+}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/CredentialElementTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/CredentialElementTest.java
index aacb6d8..a2da967 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/CredentialElementTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/CredentialElementTest.java
@@ -42,7 +42,6 @@
@SdkSuppress(minSdkVersion = 32, codeName = "T")
public void testBuilder() {
CredentialElement element = new CredentialElement(KEY, VALUE);
-
assertThat(element.getKey()).isEqualTo(KEY);
assertThat(Arrays.equals(element.getValue(), VALUE)).isTrue();
}
@@ -58,9 +57,31 @@
CredentialElement elementFromParcel = element.CREATOR.createFromParcel(
parcel);
parcel.recycle();
-
assertThat(elementFromParcel.getKey()).isEqualTo(KEY);
assertThat(Arrays.equals(elementFromParcel.getValue(), VALUE)).isTrue();
}
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void describeContents() {
+ CredentialElement element = new CredentialElement(KEY, VALUE);
+ assertThat(element.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testEqual() {
+ CredentialElement element1 = new CredentialElement(KEY, VALUE);
+ CredentialElement element2 = new CredentialElement(KEY, VALUE);
+ assertThat(element1.equals(element2)).isTrue();
+ assertThat(element1.hashCode()).isEqualTo(element2.hashCode());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testCreatorNewArray() {
+ CredentialElement [] elements =
+ CredentialElement.CREATOR.newArray(2);
+ assertThat(elements.length).isEqualTo(2);
+ }
}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/DataElementTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/DataElementTest.java
index 3654d0d..84814ae 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/DataElementTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/DataElementTest.java
@@ -16,6 +16,13 @@
package android.nearby.cts;
+import static android.nearby.DataElement.DataType.PRIVATE_IDENTITY;
+import static android.nearby.DataElement.DataType.PROVISIONED_IDENTITY;
+import static android.nearby.DataElement.DataType.PUBLIC_IDENTITY;
+import static android.nearby.DataElement.DataType.SALT;
+import static android.nearby.DataElement.DataType.TRUSTED_IDENTITY;
+import static android.nearby.DataElement.DataType.TX_POWER;
+
import static com.google.common.truth.Truth.assertThat;
import android.nearby.DataElement;
@@ -31,7 +38,6 @@
import java.util.Arrays;
-
@RunWith(AndroidJUnit4.class)
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
public class DataElementTest {
@@ -63,4 +69,59 @@
assertThat(elementFromParcel.getKey()).isEqualTo(KEY);
assertThat(Arrays.equals(elementFromParcel.getValue(), VALUE)).isTrue();
}
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void describeContents() {
+ DataElement dataElement = new DataElement(KEY, VALUE);
+ assertThat(dataElement.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testCreatorNewArray() {
+ DataElement[] elements =
+ DataElement.CREATOR.newArray(2);
+ assertThat(elements.length).isEqualTo(2);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testEquals() {
+ DataElement dataElement = new DataElement(KEY, VALUE);
+ DataElement dataElement2 = new DataElement(KEY, VALUE);
+
+ assertThat(dataElement.equals(dataElement2)).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testIsIdentity() {
+ DataElement privateIdentity = new DataElement(PRIVATE_IDENTITY, new byte[]{1, 2, 3});
+ DataElement trustedIdentity = new DataElement(TRUSTED_IDENTITY, new byte[]{1, 2, 3});
+ DataElement publicIdentity = new DataElement(PUBLIC_IDENTITY, new byte[]{1, 2, 3});
+ DataElement provisionedIdentity =
+ new DataElement(PROVISIONED_IDENTITY, new byte[]{1, 2, 3});
+
+ DataElement salt = new DataElement(SALT, new byte[]{1, 2, 3});
+ DataElement txPower = new DataElement(TX_POWER, new byte[]{1, 2, 3});
+
+ assertThat(privateIdentity.isIdentityDataType()).isTrue();
+ assertThat(trustedIdentity.isIdentityDataType()).isTrue();
+ assertThat(publicIdentity.isIdentityDataType()).isTrue();
+ assertThat(provisionedIdentity.isIdentityDataType()).isTrue();
+ assertThat(salt.isIdentityDataType()).isFalse();
+ assertThat(txPower.isIdentityDataType()).isFalse();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_notEquals() {
+ DataElement dataElement = new DataElement(KEY, VALUE);
+ DataElement dataElement2 = new DataElement(KEY, new byte[]{1, 2, 1, 1});
+ DataElement dataElement3 = new DataElement(6, VALUE);
+
+ assertThat(dataElement.equals(dataElement2)).isFalse();
+ assertThat(dataElement.equals(dataElement3)).isFalse();
+ }
}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceParcelableTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceParcelableTest.java
index 654b852..2907e56 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceParcelableTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceParcelableTest.java
@@ -22,6 +22,7 @@
import android.nearby.NearbyDevice;
import android.nearby.NearbyDeviceParcelable;
+import android.nearby.PublicCredential;
import android.os.Build;
import android.os.Parcel;
@@ -41,8 +42,11 @@
private static final String BLUETOOTH_ADDRESS = "00:11:22:33:FF:EE";
private static final byte[] SCAN_DATA = new byte[] {1, 2, 3, 4};
+ private static final byte[] SALT = new byte[] {1, 2, 3, 4};
private static final String FAST_PAIR_MODEL_ID = "1234";
private static final int RSSI = -60;
+ private static final int TX_POWER = -10;
+ private static final int ACTION = 1;
private NearbyDeviceParcelable.Builder mBuilder;
@@ -61,20 +65,36 @@
@Test
@SdkSuppress(minSdkVersion = 33, codeName = "T")
- public void test_defaultNullFields() {
+ public void testNullFields() {
+ PublicCredential publicCredential =
+ new PublicCredential.Builder(
+ new byte[] {1},
+ new byte[] {2},
+ new byte[] {3},
+ new byte[] {4},
+ new byte[] {5})
+ .build();
NearbyDeviceParcelable nearbyDeviceParcelable =
new NearbyDeviceParcelable.Builder()
.setMedium(NearbyDevice.Medium.BLE)
+ .setPublicCredential(publicCredential)
+ .setAction(ACTION)
.setRssi(RSSI)
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .setTxPower(TX_POWER)
+ .setSalt(SALT)
.build();
assertThat(nearbyDeviceParcelable.getName()).isNull();
assertThat(nearbyDeviceParcelable.getFastPairModelId()).isNull();
assertThat(nearbyDeviceParcelable.getBluetoothAddress()).isNull();
assertThat(nearbyDeviceParcelable.getData()).isNull();
-
assertThat(nearbyDeviceParcelable.getMedium()).isEqualTo(NearbyDevice.Medium.BLE);
assertThat(nearbyDeviceParcelable.getRssi()).isEqualTo(RSSI);
+ assertThat(nearbyDeviceParcelable.getAction()).isEqualTo(ACTION);
+ assertThat(nearbyDeviceParcelable.getPublicCredential()).isEqualTo(publicCredential);
+ assertThat(nearbyDeviceParcelable.getSalt()).isEqualTo(SALT);
+ assertThat(nearbyDeviceParcelable.getTxPower()).isEqualTo(TX_POWER);
}
@Test
@@ -114,7 +134,6 @@
@SdkSuppress(minSdkVersion = 33, codeName = "T")
public void testWriteParcel_nullBluetoothAddress() {
NearbyDeviceParcelable nearbyDeviceParcelable = mBuilder.setBluetoothAddress(null).build();
-
Parcel parcel = Parcel.obtain();
nearbyDeviceParcelable.writeToParcel(parcel, 0);
parcel.setDataPosition(0);
@@ -124,4 +143,36 @@
assertThat(actualNearbyDevice.getBluetoothAddress()).isNull();
}
+
+ @Test
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
+ public void describeContents() {
+ NearbyDeviceParcelable nearbyDeviceParcelable = mBuilder.setBluetoothAddress(null).build();
+ assertThat(nearbyDeviceParcelable.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
+ public void testEqual() {
+ PublicCredential publicCredential =
+ new PublicCredential.Builder(
+ new byte[] {1},
+ new byte[] {2},
+ new byte[] {3},
+ new byte[] {4},
+ new byte[] {5})
+ .build();
+ NearbyDeviceParcelable nearbyDeviceParcelable1 =
+ mBuilder.setPublicCredential(publicCredential).build();
+ NearbyDeviceParcelable nearbyDeviceParcelable2 =
+ mBuilder.setPublicCredential(publicCredential).build();
+ assertThat(nearbyDeviceParcelable1.equals(nearbyDeviceParcelable2)).isTrue();
+ }
+
+ @Test
+ public void testCreatorNewArray() {
+ NearbyDeviceParcelable[] nearbyDeviceParcelables =
+ NearbyDeviceParcelable.CREATOR.newArray(2);
+ assertThat(nearbyDeviceParcelables.length).isEqualTo(2);
+ }
}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceTest.java
index f37800a..8ca5a94 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyDeviceTest.java
@@ -16,6 +16,8 @@
package android.nearby.cts;
+import static android.nearby.NearbyDevice.Medium.BLE;
+
import android.annotation.TargetApi;
import android.nearby.FastPairDevice;
import android.nearby.NearbyDevice;
@@ -34,13 +36,18 @@
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@TargetApi(Build.VERSION_CODES.TIRAMISU)
public class NearbyDeviceTest {
+ private static final String NAME = "NearbyDevice";
+ private static final String MODEL_ID = "112233";
+ private static final int TX_POWER = -10;
+ private static final int RSSI = -60;
+ private static final String BLUETOOTH_ADDRESS = "00:11:22:33:FF:EE";
+ private static final byte[] SCAN_DATA = new byte[] {1, 2, 3, 4};
@Test
@SdkSuppress(minSdkVersion = 32, codeName = "T")
public void test_isValidMedium() {
assertThat(NearbyDevice.isValidMedium(1)).isTrue();
assertThat(NearbyDevice.isValidMedium(2)).isTrue();
-
assertThat(NearbyDevice.isValidMedium(0)).isFalse();
assertThat(NearbyDevice.isValidMedium(3)).isFalse();
}
@@ -49,11 +56,55 @@
@SdkSuppress(minSdkVersion = 32, codeName = "T")
public void test_getMedium_fromChild() {
FastPairDevice fastPairDevice = new FastPairDevice.Builder()
- .addMedium(NearbyDevice.Medium.BLE)
- .setRssi(-60)
+ .addMedium(BLE)
+ .setRssi(RSSI)
.build();
assertThat(fastPairDevice.getMediums()).contains(1);
- assertThat(fastPairDevice.getRssi()).isEqualTo(-60);
+ assertThat(fastPairDevice.getRssi()).isEqualTo(RSSI);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testEqual() {
+ FastPairDevice fastPairDevice1 = new FastPairDevice.Builder()
+ .setModelId(MODEL_ID)
+ .setTxPower(TX_POWER)
+ .setBluetoothAddress(BLUETOOTH_ADDRESS)
+ .setData(SCAN_DATA)
+ .setRssi(RSSI)
+ .addMedium(BLE)
+ .setName(NAME)
+ .build();
+ FastPairDevice fastPairDevice2 = new FastPairDevice.Builder()
+ .setModelId(MODEL_ID)
+ .setTxPower(TX_POWER)
+ .setBluetoothAddress(BLUETOOTH_ADDRESS)
+ .setData(SCAN_DATA)
+ .setRssi(RSSI)
+ .addMedium(BLE)
+ .setName(NAME)
+ .build();
+
+ assertThat(fastPairDevice1.equals(fastPairDevice1)).isTrue();
+ assertThat(fastPairDevice1.equals(fastPairDevice2)).isTrue();
+ assertThat(fastPairDevice1.equals(null)).isFalse();
+ assertThat(fastPairDevice1.hashCode()).isEqualTo(fastPairDevice2.hashCode());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testToString() {
+ FastPairDevice fastPairDevice1 = new FastPairDevice.Builder()
+ .addMedium(BLE)
+ .setRssi(RSSI)
+ .setModelId(MODEL_ID)
+ .setTxPower(TX_POWER)
+ .setBluetoothAddress(BLUETOOTH_ADDRESS)
+ .build();
+
+ assertThat(fastPairDevice1.toString())
+ .isEqualTo("FastPairDevice [medium={BLE} rssi=-60 "
+ + "txPower=-10 modelId=112233 bluetoothAddress=00:11:22:33:FF:EE]");
}
}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
index 7696a61..462d05a 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/NearbyManagerTest.java
@@ -158,6 +158,15 @@
mNearbyManager.stopBroadcast(callback);
}
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void setFastPairScanEnabled() {
+ mNearbyManager.setFastPairScanEnabled(mContext, true);
+ assertThat(mNearbyManager.getFastPairScanEnabled(mContext)).isTrue();
+ mNearbyManager.setFastPairScanEnabled(mContext, false);
+ assertThat(mNearbyManager.getFastPairScanEnabled(mContext)).isFalse();
+ }
+
private void enableBluetooth() {
BluetoothManager manager = mContext.getSystemService(BluetoothManager.class);
BluetoothAdapter bluetoothAdapter = manager.getAdapter();
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceBroadcastRequestTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceBroadcastRequestTest.java
index eaa5ca1..71be889 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceBroadcastRequestTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceBroadcastRequestTest.java
@@ -114,4 +114,18 @@
assertThat(parcelRequest.getType()).isEqualTo(BROADCAST_TYPE_NEARBY_PRESENCE);
}
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void describeContents() {
+ PresenceBroadcastRequest broadcastRequest = mBuilder.build();
+ assertThat(broadcastRequest.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ public void testCreatorNewArray() {
+ PresenceBroadcastRequest[] presenceBroadcastRequests =
+ PresenceBroadcastRequest.CREATOR.newArray(2);
+ assertThat(presenceBroadcastRequests.length).isEqualTo(2);
+ }
}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceDeviceTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceDeviceTest.java
index 94f8fe7..ea1de6b 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceDeviceTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceDeviceTest.java
@@ -104,4 +104,24 @@
assertThat(parcelDevice.getMediums()).containsExactly(MEDIUM);
assertThat(parcelDevice.getName()).isEqualTo(DEVICE_NAME);
}
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void describeContents() {
+ PresenceDevice device =
+ new PresenceDevice.Builder(DEVICE_ID, SALT, SECRET_ID, ENCRYPTED_IDENTITY)
+ .addExtendedProperty(new DataElement(KEY, VALUE))
+ .setRssi(RSSI)
+ .addMedium(MEDIUM)
+ .setName(DEVICE_NAME)
+ .build();
+ assertThat(device.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ public void testCreatorNewArray() {
+ PresenceDevice[] devices =
+ PresenceDevice.CREATOR.newArray(2);
+ assertThat(devices.length).isEqualTo(2);
+ }
}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceScanFilterTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceScanFilterTest.java
index cecdfd2..77e7dc5 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceScanFilterTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PresenceScanFilterTest.java
@@ -90,5 +90,21 @@
assertThat(parcelFilter.getType()).isEqualTo(ScanRequest.SCAN_TYPE_NEARBY_PRESENCE);
assertThat(parcelFilter.getMaxPathLoss()).isEqualTo(RSSI);
assertThat(parcelFilter.getPresenceActions()).containsExactly(ACTION);
+ assertThat(parcelFilter.getExtendedProperties().get(0).getKey()).isEqualTo(KEY);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void describeContents() {
+ PresenceScanFilter filter = mBuilder.build();
+ assertThat(filter.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testCreatorNewArray() {
+ PresenceScanFilter[] filters =
+ PresenceScanFilter.CREATOR.newArray(2);
+ assertThat(filters.length).isEqualTo(2);
}
}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PrivateCredentialTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PrivateCredentialTest.java
index f05f65f..fa8c954 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/PrivateCredentialTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PrivateCredentialTest.java
@@ -99,4 +99,19 @@
assertThat(credentialElement.getKey()).isEqualTo(KEY);
assertThat(Arrays.equals(credentialElement.getValue(), VALUE)).isTrue();
}
+
+ @Test
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
+ public void describeContents() {
+ PrivateCredential credential = mBuilder.build();
+ assertThat(credential.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 33, codeName = "T")
+ public void testCreatorNewArray() {
+ PrivateCredential[] credentials =
+ PrivateCredential.CREATOR.newArray(2);
+ assertThat(credentials.length).isEqualTo(2);
+ }
}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/PublicCredentialTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/PublicCredentialTest.java
index 11bbacc..774e897 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/PublicCredentialTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/PublicCredentialTest.java
@@ -135,6 +135,7 @@
.setIdentityType(IDENTITY_TYPE_PRIVATE)
.build();
assertThat(credentialOne.equals((Object) credentialTwo)).isTrue();
+ assertThat(credentialOne.equals(null)).isFalse();
}
@Test
@@ -161,4 +162,19 @@
.build();
assertThat(credentialOne.equals((Object) credentialTwo)).isFalse();
}
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void describeContents() {
+ PublicCredential credential = mBuilder.build();
+ assertThat(credential.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testCreatorNewArray() {
+ PublicCredential[] credentials =
+ PublicCredential.CREATOR.newArray(2);
+ assertThat(credentials.length).isEqualTo(2);
+ }
}
diff --git a/nearby/tests/cts/fastpair/src/android/nearby/cts/ScanRequestTest.java b/nearby/tests/cts/fastpair/src/android/nearby/cts/ScanRequestTest.java
index 21f3d28..de4b1c3 100644
--- a/nearby/tests/cts/fastpair/src/android/nearby/cts/ScanRequestTest.java
+++ b/nearby/tests/cts/fastpair/src/android/nearby/cts/ScanRequestTest.java
@@ -171,6 +171,23 @@
assertThat(request.getScanFilters().get(0).getMaxPathLoss()).isEqualTo(RSSI);
}
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void describeContents() {
+ ScanRequest request = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_FAST_PAIR)
+ .build();
+ assertThat(request.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testCreatorNewArray() {
+ ScanRequest[] requests =
+ ScanRequest.CREATOR.newArray(2);
+ assertThat(requests.length).isEqualTo(2);
+ }
+
private static PresenceScanFilter getPresenceScanFilter() {
final byte[] secretId = new byte[]{1, 2, 3, 4};
final byte[] authenticityKey = new byte[]{0, 1, 1, 1};
diff --git a/nearby/tests/unit/AndroidManifest.xml b/nearby/tests/unit/AndroidManifest.xml
index 9f58baf..7dcb263 100644
--- a/nearby/tests/unit/AndroidManifest.xml
+++ b/nearby/tests/unit/AndroidManifest.xml
@@ -23,6 +23,7 @@
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
+ <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<application android:debuggable="true">
<uses-library android:name="android.test.runner" />
diff --git a/nearby/tests/unit/src/android/nearby/FastPairAntispoofKeyDeviceMetadataTest.java b/nearby/tests/unit/src/android/nearby/FastPairAntispoofKeyDeviceMetadataTest.java
new file mode 100644
index 0000000..d095529
--- /dev/null
+++ b/nearby/tests/unit/src/android/nearby/FastPairAntispoofKeyDeviceMetadataTest.java
@@ -0,0 +1,177 @@
+/*
+ * 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 android.nearby;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class FastPairAntispoofKeyDeviceMetadataTest {
+
+ private static final int BLE_TX_POWER = 5;
+ private static final String CONNECT_SUCCESS_COMPANION_APP_INSTALLED =
+ "CONNECT_SUCCESS_COMPANION_APP_INSTALLED";
+ private static final String CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED =
+ "CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED";
+ private static final float DELTA = 0.001f;
+ private static final int DEVICE_TYPE = 7;
+ private static final String DOWNLOAD_COMPANION_APP_DESCRIPTION =
+ "DOWNLOAD_COMPANION_APP_DESCRIPTION";
+ private static final String FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION =
+ "FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION";
+ private static final byte[] IMAGE = new byte[] {7, 9};
+ private static final String IMAGE_URL = "IMAGE_URL";
+ private static final String INITIAL_NOTIFICATION_DESCRIPTION =
+ "INITIAL_NOTIFICATION_DESCRIPTION";
+ private static final String INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT =
+ "INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT";
+ private static final String INITIAL_PAIRING_DESCRIPTION = "INITIAL_PAIRING_DESCRIPTION";
+ private static final String INTENT_URI = "INTENT_URI";
+ private static final String OPEN_COMPANION_APP_DESCRIPTION = "OPEN_COMPANION_APP_DESCRIPTION";
+ private static final String RETRO_ACTIVE_PAIRING_DESCRIPTION =
+ "RETRO_ACTIVE_PAIRING_DESCRIPTION";
+ private static final String SUBSEQUENT_PAIRING_DESCRIPTION = "SUBSEQUENT_PAIRING_DESCRIPTION";
+ private static final float TRIGGER_DISTANCE = 111;
+ private static final String TRUE_WIRELESS_IMAGE_URL_CASE = "TRUE_WIRELESS_IMAGE_URL_CASE";
+ private static final String TRUE_WIRELESS_IMAGE_URL_LEFT_BUD =
+ "TRUE_WIRELESS_IMAGE_URL_LEFT_BUD";
+ private static final String TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD =
+ "TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD";
+ private static final String UNABLE_TO_CONNECT_DESCRIPTION = "UNABLE_TO_CONNECT_DESCRIPTION";
+ private static final String UNABLE_TO_CONNECT_TITLE = "UNABLE_TO_CONNECT_TITLE";
+ private static final String UPDATE_COMPANION_APP_DESCRIPTION =
+ "UPDATE_COMPANION_APP_DESCRIPTION";
+ private static final String WAIT_LAUNCH_COMPANION_APP_DESCRIPTION =
+ "WAIT_LAUNCH_COMPANION_APP_DESCRIPTION";
+ private static final byte[] ANTI_SPOOFING_KEY = new byte[] {4, 5, 6};
+ private static final String NAME = "NAME";
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testSetGetFastPairAntispoofKeyDeviceMetadataNotNull() {
+ FastPairDeviceMetadata fastPairDeviceMetadata = genFastPairDeviceMetadata();
+ FastPairAntispoofKeyDeviceMetadata fastPairAntispoofKeyDeviceMetadata =
+ genFastPairAntispoofKeyDeviceMetadata(ANTI_SPOOFING_KEY, fastPairDeviceMetadata);
+
+ assertThat(fastPairAntispoofKeyDeviceMetadata.getAntispoofPublicKey()).isEqualTo(
+ ANTI_SPOOFING_KEY);
+ ensureFastPairDeviceMetadataAsExpected(
+ fastPairAntispoofKeyDeviceMetadata.getFastPairDeviceMetadata());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testSetGetFastPairAntispoofKeyDeviceMetadataNull() {
+ FastPairAntispoofKeyDeviceMetadata fastPairAntispoofKeyDeviceMetadata =
+ genFastPairAntispoofKeyDeviceMetadata(null, null);
+ assertThat(fastPairAntispoofKeyDeviceMetadata.getAntispoofPublicKey()).isEqualTo(
+ null);
+ assertThat(fastPairAntispoofKeyDeviceMetadata.getFastPairDeviceMetadata()).isEqualTo(
+ null);
+ }
+
+ /* Verifies DeviceMetadata. */
+ private static void ensureFastPairDeviceMetadataAsExpected(FastPairDeviceMetadata metadata) {
+ assertThat(metadata.getBleTxPower()).isEqualTo(BLE_TX_POWER);
+ assertThat(metadata.getConnectSuccessCompanionAppInstalled())
+ .isEqualTo(CONNECT_SUCCESS_COMPANION_APP_INSTALLED);
+ assertThat(metadata.getConnectSuccessCompanionAppNotInstalled())
+ .isEqualTo(CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED);
+ assertThat(metadata.getDeviceType()).isEqualTo(DEVICE_TYPE);
+ assertThat(metadata.getDownloadCompanionAppDescription())
+ .isEqualTo(DOWNLOAD_COMPANION_APP_DESCRIPTION);
+ assertThat(metadata.getFailConnectGoToSettingsDescription())
+ .isEqualTo(FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION);
+ assertThat(metadata.getImage()).isEqualTo(IMAGE);
+ assertThat(metadata.getImageUrl()).isEqualTo(IMAGE_URL);
+ assertThat(metadata.getInitialNotificationDescription())
+ .isEqualTo(INITIAL_NOTIFICATION_DESCRIPTION);
+ assertThat(metadata.getInitialNotificationDescriptionNoAccount())
+ .isEqualTo(INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT);
+ assertThat(metadata.getInitialPairingDescription()).isEqualTo(INITIAL_PAIRING_DESCRIPTION);
+ assertThat(metadata.getIntentUri()).isEqualTo(INTENT_URI);
+ assertThat(metadata.getName()).isEqualTo(NAME);
+ assertThat(metadata.getOpenCompanionAppDescription())
+ .isEqualTo(OPEN_COMPANION_APP_DESCRIPTION);
+ assertThat(metadata.getRetroactivePairingDescription())
+ .isEqualTo(RETRO_ACTIVE_PAIRING_DESCRIPTION);
+ assertThat(metadata.getSubsequentPairingDescription())
+ .isEqualTo(SUBSEQUENT_PAIRING_DESCRIPTION);
+ assertThat(metadata.getTriggerDistance()).isWithin(DELTA).of(TRIGGER_DISTANCE);
+ assertThat(metadata.getTrueWirelessImageUrlCase()).isEqualTo(TRUE_WIRELESS_IMAGE_URL_CASE);
+ assertThat(metadata.getTrueWirelessImageUrlLeftBud())
+ .isEqualTo(TRUE_WIRELESS_IMAGE_URL_LEFT_BUD);
+ assertThat(metadata.getTrueWirelessImageUrlRightBud())
+ .isEqualTo(TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD);
+ assertThat(metadata.getUnableToConnectDescription())
+ .isEqualTo(UNABLE_TO_CONNECT_DESCRIPTION);
+ assertThat(metadata.getUnableToConnectTitle()).isEqualTo(UNABLE_TO_CONNECT_TITLE);
+ assertThat(metadata.getUpdateCompanionAppDescription())
+ .isEqualTo(UPDATE_COMPANION_APP_DESCRIPTION);
+ assertThat(metadata.getWaitLaunchCompanionAppDescription())
+ .isEqualTo(WAIT_LAUNCH_COMPANION_APP_DESCRIPTION);
+ }
+
+ /* Generates FastPairAntispoofKeyDeviceMetadata. */
+ private static FastPairAntispoofKeyDeviceMetadata genFastPairAntispoofKeyDeviceMetadata(
+ byte[] antispoofPublicKey, FastPairDeviceMetadata deviceMetadata) {
+ FastPairAntispoofKeyDeviceMetadata.Builder builder =
+ new FastPairAntispoofKeyDeviceMetadata.Builder();
+ builder.setAntispoofPublicKey(antispoofPublicKey);
+ builder.setFastPairDeviceMetadata(deviceMetadata);
+
+ return builder.build();
+ }
+
+ /* Generates FastPairDeviceMetadata. */
+ private static FastPairDeviceMetadata genFastPairDeviceMetadata() {
+ FastPairDeviceMetadata.Builder builder = new FastPairDeviceMetadata.Builder();
+ builder.setBleTxPower(BLE_TX_POWER);
+ builder.setConnectSuccessCompanionAppInstalled(CONNECT_SUCCESS_COMPANION_APP_INSTALLED);
+ builder.setConnectSuccessCompanionAppNotInstalled(
+ CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED);
+ builder.setDeviceType(DEVICE_TYPE);
+ builder.setDownloadCompanionAppDescription(DOWNLOAD_COMPANION_APP_DESCRIPTION);
+ builder.setFailConnectGoToSettingsDescription(FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION);
+ builder.setImage(IMAGE);
+ builder.setImageUrl(IMAGE_URL);
+ builder.setInitialNotificationDescription(INITIAL_NOTIFICATION_DESCRIPTION);
+ builder.setInitialNotificationDescriptionNoAccount(
+ INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT);
+ builder.setInitialPairingDescription(INITIAL_PAIRING_DESCRIPTION);
+ builder.setIntentUri(INTENT_URI);
+ builder.setName(NAME);
+ builder.setOpenCompanionAppDescription(OPEN_COMPANION_APP_DESCRIPTION);
+ builder.setRetroactivePairingDescription(RETRO_ACTIVE_PAIRING_DESCRIPTION);
+ builder.setSubsequentPairingDescription(SUBSEQUENT_PAIRING_DESCRIPTION);
+ builder.setTriggerDistance(TRIGGER_DISTANCE);
+ builder.setTrueWirelessImageUrlCase(TRUE_WIRELESS_IMAGE_URL_CASE);
+ builder.setTrueWirelessImageUrlLeftBud(TRUE_WIRELESS_IMAGE_URL_LEFT_BUD);
+ builder.setTrueWirelessImageUrlRightBud(TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD);
+ builder.setUnableToConnectDescription(UNABLE_TO_CONNECT_DESCRIPTION);
+ builder.setUnableToConnectTitle(UNABLE_TO_CONNECT_TITLE);
+ builder.setUpdateCompanionAppDescription(UPDATE_COMPANION_APP_DESCRIPTION);
+ builder.setWaitLaunchCompanionAppDescription(WAIT_LAUNCH_COMPANION_APP_DESCRIPTION);
+
+ return builder.build();
+ }
+}
diff --git a/nearby/tests/unit/src/android/nearby/FastPairDataProviderServiceTest.java b/nearby/tests/unit/src/android/nearby/FastPairDataProviderServiceTest.java
new file mode 100644
index 0000000..b3f2442
--- /dev/null
+++ b/nearby/tests/unit/src/android/nearby/FastPairDataProviderServiceTest.java
@@ -0,0 +1,966 @@
+/*
+ * 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 android.nearby;
+
+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.verify;
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.accounts.Account;
+import android.content.Intent;
+import android.nearby.aidl.ByteArrayParcel;
+import android.nearby.aidl.FastPairAccountDevicesMetadataRequestParcel;
+import android.nearby.aidl.FastPairAccountKeyDeviceMetadataParcel;
+import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataParcel;
+import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataRequestParcel;
+import android.nearby.aidl.FastPairDeviceMetadataParcel;
+import android.nearby.aidl.FastPairDiscoveryItemParcel;
+import android.nearby.aidl.FastPairEligibleAccountParcel;
+import android.nearby.aidl.FastPairEligibleAccountsRequestParcel;
+import android.nearby.aidl.FastPairManageAccountDeviceRequestParcel;
+import android.nearby.aidl.FastPairManageAccountRequestParcel;
+import android.nearby.aidl.IFastPairAccountDevicesMetadataCallback;
+import android.nearby.aidl.IFastPairAntispoofKeyDeviceMetadataCallback;
+import android.nearby.aidl.IFastPairDataProvider;
+import android.nearby.aidl.IFastPairEligibleAccountsCallback;
+import android.nearby.aidl.IFastPairManageAccountCallback;
+import android.nearby.aidl.IFastPairManageAccountDeviceCallback;
+
+import androidx.annotation.NonNull;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+
+@RunWith(AndroidJUnit4.class)
+public class FastPairDataProviderServiceTest {
+
+ private static final String TAG = "FastPairDataProviderServiceTest";
+
+ private static final int BLE_TX_POWER = 5;
+ private static final String CONNECT_SUCCESS_COMPANION_APP_INSTALLED =
+ "CONNECT_SUCCESS_COMPANION_APP_INSTALLED";
+ private static final String CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED =
+ "CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED";
+ private static final float DELTA = 0.001f;
+ private static final int DEVICE_TYPE = 7;
+ private static final String DOWNLOAD_COMPANION_APP_DESCRIPTION =
+ "DOWNLOAD_COMPANION_APP_DESCRIPTION";
+ private static final Account ELIGIBLE_ACCOUNT_1 = new Account("abc@google.com", "type1");
+ private static final boolean ELIGIBLE_ACCOUNT_1_OPT_IN = true;
+ private static final Account ELIGIBLE_ACCOUNT_2 = new Account("def@gmail.com", "type2");
+ private static final boolean ELIGIBLE_ACCOUNT_2_OPT_IN = false;
+ private static final Account MANAGE_ACCOUNT = new Account("ghi@gmail.com", "type3");
+ private static final Account ACCOUNTDEVICES_METADATA_ACCOUNT =
+ new Account("jk@gmail.com", "type4");
+ private static final int NUM_ACCOUNT_DEVICES = 2;
+
+ private static final int ERROR_CODE_BAD_REQUEST =
+ FastPairDataProviderService.ERROR_CODE_BAD_REQUEST;
+ private static final int MANAGE_ACCOUNT_REQUEST_TYPE =
+ FastPairDataProviderService.MANAGE_REQUEST_ADD;
+ private static final String ERROR_STRING = "ERROR_STRING";
+ private static final String FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION =
+ "FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION";
+ private static final byte[] IMAGE = new byte[] {7, 9};
+ private static final String IMAGE_URL = "IMAGE_URL";
+ private static final String INITIAL_NOTIFICATION_DESCRIPTION =
+ "INITIAL_NOTIFICATION_DESCRIPTION";
+ private static final String INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT =
+ "INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT";
+ private static final String INITIAL_PAIRING_DESCRIPTION = "INITIAL_PAIRING_DESCRIPTION";
+ private static final String INTENT_URI = "INTENT_URI";
+ private static final String OPEN_COMPANION_APP_DESCRIPTION = "OPEN_COMPANION_APP_DESCRIPTION";
+ private static final String RETRO_ACTIVE_PAIRING_DESCRIPTION =
+ "RETRO_ACTIVE_PAIRING_DESCRIPTION";
+ private static final String SUBSEQUENT_PAIRING_DESCRIPTION = "SUBSEQUENT_PAIRING_DESCRIPTION";
+ private static final float TRIGGER_DISTANCE = 111;
+ private static final String TRUE_WIRELESS_IMAGE_URL_CASE = "TRUE_WIRELESS_IMAGE_URL_CASE";
+ private static final String TRUE_WIRELESS_IMAGE_URL_LEFT_BUD =
+ "TRUE_WIRELESS_IMAGE_URL_LEFT_BUD";
+ private static final String TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD =
+ "TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD";
+ private static final String UNABLE_TO_CONNECT_DESCRIPTION = "UNABLE_TO_CONNECT_DESCRIPTION";
+ private static final String UNABLE_TO_CONNECT_TITLE = "UNABLE_TO_CONNECT_TITLE";
+ private static final String UPDATE_COMPANION_APP_DESCRIPTION =
+ "UPDATE_COMPANION_APP_DESCRIPTION";
+ private static final String WAIT_LAUNCH_COMPANION_APP_DESCRIPTION =
+ "WAIT_LAUNCH_COMPANION_APP_DESCRIPTION";
+ private static final byte[] ACCOUNT_KEY = new byte[] {3};
+ private static final byte[] ACCOUNT_KEY_2 = new byte[] {9, 3};
+ private static final byte[] SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS = new byte[] {2, 8};
+ private static final byte[] REQUEST_MODEL_ID = new byte[] {1, 2, 3};
+ private static final byte[] ANTI_SPOOFING_KEY = new byte[] {4, 5, 6};
+ private static final String ACTION_URL = "ACTION_URL";
+ private static final int ACTION_URL_TYPE = 5;
+ private static final String APP_NAME = "APP_NAME";
+ private static final byte[] AUTHENTICATION_PUBLIC_KEY_SEC_P256R1 = new byte[] {5, 7};
+ private static final String DESCRIPTION = "DESCRIPTION";
+ private static final String DEVICE_NAME = "DEVICE_NAME";
+ private static final String DISPLAY_URL = "DISPLAY_URL";
+ private static final long FIRST_OBSERVATION_TIMESTAMP_MILLIS = 8393L;
+ private static final String ICON_FIFE_URL = "ICON_FIFE_URL";
+ private static final byte[] ICON_PNG = new byte[]{2, 5};
+ private static final String ID = "ID";
+ private static final long LAST_OBSERVATION_TIMESTAMP_MILLIS = 934234L;
+ private static final String MAC_ADDRESS = "MAC_ADDRESS";
+ private static final String NAME = "NAME";
+ private static final String PACKAGE_NAME = "PACKAGE_NAME";
+ private static final long PENDING_APP_INSTALL_TIMESTAMP_MILLIS = 832393L;
+ private static final int RSSI = 9;
+ private static final int STATE = 63;
+ private static final String TITLE = "TITLE";
+ private static final String TRIGGER_ID = "TRIGGER_ID";
+ private static final int TX_POWER = 62;
+
+ private static final int ELIGIBLE_ACCOUNTS_NUM = 2;
+ private static final ImmutableList<FastPairEligibleAccount> ELIGIBLE_ACCOUNTS =
+ ImmutableList.of(
+ genHappyPathFastPairEligibleAccount(ELIGIBLE_ACCOUNT_1,
+ ELIGIBLE_ACCOUNT_1_OPT_IN),
+ genHappyPathFastPairEligibleAccount(ELIGIBLE_ACCOUNT_2,
+ ELIGIBLE_ACCOUNT_2_OPT_IN));
+ private static final int ACCOUNTKEY_DEVICE_NUM = 2;
+ private static final ImmutableList<FastPairAccountKeyDeviceMetadata>
+ FAST_PAIR_ACCOUNT_DEVICES_METADATA =
+ ImmutableList.of(
+ genHappyPathFastPairAccountkeyDeviceMetadata(),
+ genHappyPathFastPairAccountkeyDeviceMetadata());
+
+ private static final FastPairAntispoofKeyDeviceMetadataRequestParcel
+ FAST_PAIR_ANTI_SPOOF_KEY_DEVICE_METADATA_REQUEST_PARCEL =
+ genFastPairAntispoofKeyDeviceMetadataRequestParcel();
+ private static final FastPairAccountDevicesMetadataRequestParcel
+ FAST_PAIR_ACCOUNT_DEVICES_METADATA_REQUEST_PARCEL =
+ genFastPairAccountDevicesMetadataRequestParcel();
+ private static final FastPairEligibleAccountsRequestParcel
+ FAST_PAIR_ELIGIBLE_ACCOUNTS_REQUEST_PARCEL =
+ genFastPairEligibleAccountsRequestParcel();
+ private static final FastPairManageAccountRequestParcel
+ FAST_PAIR_MANAGE_ACCOUNT_REQUEST_PARCEL =
+ genFastPairManageAccountRequestParcel();
+ private static final FastPairManageAccountDeviceRequestParcel
+ FAST_PAIR_MANAGE_ACCOUNT_DEVICE_REQUEST_PARCEL =
+ genFastPairManageAccountDeviceRequestParcel();
+ private static final FastPairAntispoofKeyDeviceMetadata
+ HAPPY_PATH_FAST_PAIR_ANTI_SPOOF_KEY_DEVICE_METADATA =
+ genHappyPathFastPairAntispoofKeyDeviceMetadata();
+
+ @Captor private ArgumentCaptor<FastPairEligibleAccountParcel[]>
+ mFastPairEligibleAccountParcelsArgumentCaptor;
+ @Captor private ArgumentCaptor<FastPairAccountKeyDeviceMetadataParcel[]>
+ mFastPairAccountKeyDeviceMetadataParcelsArgumentCaptor;
+
+ @Mock private FastPairDataProviderService mMockFastPairDataProviderService;
+ @Mock private IFastPairAntispoofKeyDeviceMetadataCallback.Stub
+ mAntispoofKeyDeviceMetadataCallback;
+ @Mock private IFastPairAccountDevicesMetadataCallback.Stub mAccountDevicesMetadataCallback;
+ @Mock private IFastPairEligibleAccountsCallback.Stub mEligibleAccountsCallback;
+ @Mock private IFastPairManageAccountCallback.Stub mManageAccountCallback;
+ @Mock private IFastPairManageAccountDeviceCallback.Stub mManageAccountDeviceCallback;
+
+ private MyHappyPathProvider mHappyPathFastPairDataProvider;
+ private MyErrorPathProvider mErrorPathFastPairDataProvider;
+
+ @Before
+ public void setUp() throws Exception {
+ initMocks(this);
+
+ mHappyPathFastPairDataProvider =
+ new MyHappyPathProvider(TAG, mMockFastPairDataProviderService);
+ mErrorPathFastPairDataProvider =
+ new MyErrorPathProvider(TAG, mMockFastPairDataProviderService);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testHappyPathLoadFastPairAntispoofKeyDeviceMetadata() throws Exception {
+ // AOSP sends calls to OEM via Parcelable.
+ mHappyPathFastPairDataProvider.asProvider().loadFastPairAntispoofKeyDeviceMetadata(
+ FAST_PAIR_ANTI_SPOOF_KEY_DEVICE_METADATA_REQUEST_PARCEL,
+ mAntispoofKeyDeviceMetadataCallback);
+
+ // OEM receives request and verifies that it is as expected.
+ final ArgumentCaptor<FastPairDataProviderService.FastPairAntispoofKeyDeviceMetadataRequest>
+ fastPairAntispoofKeyDeviceMetadataRequestCaptor =
+ ArgumentCaptor.forClass(
+ FastPairDataProviderService.FastPairAntispoofKeyDeviceMetadataRequest.class
+ );
+ verify(mMockFastPairDataProviderService).onLoadFastPairAntispoofKeyDeviceMetadata(
+ fastPairAntispoofKeyDeviceMetadataRequestCaptor.capture(),
+ any(FastPairDataProviderService.FastPairAntispoofKeyDeviceMetadataCallback.class));
+ ensureHappyPathAsExpected(fastPairAntispoofKeyDeviceMetadataRequestCaptor.getValue());
+
+ // AOSP receives responses and verifies that it is as expected.
+ final ArgumentCaptor<FastPairAntispoofKeyDeviceMetadataParcel>
+ fastPairAntispoofKeyDeviceMetadataParcelCaptor =
+ ArgumentCaptor.forClass(FastPairAntispoofKeyDeviceMetadataParcel.class);
+ verify(mAntispoofKeyDeviceMetadataCallback).onFastPairAntispoofKeyDeviceMetadataReceived(
+ fastPairAntispoofKeyDeviceMetadataParcelCaptor.capture());
+ ensureHappyPathAsExpected(fastPairAntispoofKeyDeviceMetadataParcelCaptor.getValue());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testHappyPathLoadFastPairAccountDevicesMetadata() throws Exception {
+ // AOSP sends calls to OEM via Parcelable.
+ mHappyPathFastPairDataProvider.asProvider().loadFastPairAccountDevicesMetadata(
+ FAST_PAIR_ACCOUNT_DEVICES_METADATA_REQUEST_PARCEL,
+ mAccountDevicesMetadataCallback);
+
+ // OEM receives request and verifies that it is as expected.
+ final ArgumentCaptor<FastPairDataProviderService.FastPairAccountDevicesMetadataRequest>
+ fastPairAccountDevicesMetadataRequestCaptor =
+ ArgumentCaptor.forClass(
+ FastPairDataProviderService.FastPairAccountDevicesMetadataRequest.class);
+ verify(mMockFastPairDataProviderService).onLoadFastPairAccountDevicesMetadata(
+ fastPairAccountDevicesMetadataRequestCaptor.capture(),
+ any(FastPairDataProviderService.FastPairAccountDevicesMetadataCallback.class));
+ ensureHappyPathAsExpected(fastPairAccountDevicesMetadataRequestCaptor.getValue());
+
+ // AOSP receives responses and verifies that it is as expected.
+ verify(mAccountDevicesMetadataCallback).onFastPairAccountDevicesMetadataReceived(
+ mFastPairAccountKeyDeviceMetadataParcelsArgumentCaptor.capture());
+ ensureHappyPathAsExpected(
+ mFastPairAccountKeyDeviceMetadataParcelsArgumentCaptor.getValue());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testHappyPathLoadFastPairEligibleAccounts() throws Exception {
+ // AOSP sends calls to OEM via Parcelable.
+ mHappyPathFastPairDataProvider.asProvider().loadFastPairEligibleAccounts(
+ FAST_PAIR_ELIGIBLE_ACCOUNTS_REQUEST_PARCEL,
+ mEligibleAccountsCallback);
+
+ // OEM receives request and verifies that it is as expected.
+ final ArgumentCaptor<FastPairDataProviderService.FastPairEligibleAccountsRequest>
+ fastPairEligibleAccountsRequestCaptor =
+ ArgumentCaptor.forClass(
+ FastPairDataProviderService.FastPairEligibleAccountsRequest.class);
+ verify(mMockFastPairDataProviderService).onLoadFastPairEligibleAccounts(
+ fastPairEligibleAccountsRequestCaptor.capture(),
+ any(FastPairDataProviderService.FastPairEligibleAccountsCallback.class));
+ ensureHappyPathAsExpected(fastPairEligibleAccountsRequestCaptor.getValue());
+
+ // AOSP receives responses and verifies that it is as expected.
+ verify(mEligibleAccountsCallback).onFastPairEligibleAccountsReceived(
+ mFastPairEligibleAccountParcelsArgumentCaptor.capture());
+ ensureHappyPathAsExpected(mFastPairEligibleAccountParcelsArgumentCaptor.getValue());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testHappyPathManageFastPairAccount() throws Exception {
+ // AOSP sends calls to OEM via Parcelable.
+ mHappyPathFastPairDataProvider.asProvider().manageFastPairAccount(
+ FAST_PAIR_MANAGE_ACCOUNT_REQUEST_PARCEL,
+ mManageAccountCallback);
+
+ // OEM receives request and verifies that it is as expected.
+ final ArgumentCaptor<FastPairDataProviderService.FastPairManageAccountRequest>
+ fastPairManageAccountRequestCaptor =
+ ArgumentCaptor.forClass(
+ FastPairDataProviderService.FastPairManageAccountRequest.class);
+ verify(mMockFastPairDataProviderService).onManageFastPairAccount(
+ fastPairManageAccountRequestCaptor.capture(),
+ any(FastPairDataProviderService.FastPairManageActionCallback.class));
+ ensureHappyPathAsExpected(fastPairManageAccountRequestCaptor.getValue());
+
+ // AOSP receives SUCCESS response.
+ verify(mManageAccountCallback).onSuccess();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testHappyPathManageFastPairAccountDevice() throws Exception {
+ // AOSP sends calls to OEM via Parcelable.
+ mHappyPathFastPairDataProvider.asProvider().manageFastPairAccountDevice(
+ FAST_PAIR_MANAGE_ACCOUNT_DEVICE_REQUEST_PARCEL,
+ mManageAccountDeviceCallback);
+
+ // OEM receives request and verifies that it is as expected.
+ final ArgumentCaptor<FastPairDataProviderService.FastPairManageAccountDeviceRequest>
+ fastPairManageAccountDeviceRequestCaptor =
+ ArgumentCaptor.forClass(
+ FastPairDataProviderService.FastPairManageAccountDeviceRequest.class);
+ verify(mMockFastPairDataProviderService).onManageFastPairAccountDevice(
+ fastPairManageAccountDeviceRequestCaptor.capture(),
+ any(FastPairDataProviderService.FastPairManageActionCallback.class));
+ ensureHappyPathAsExpected(fastPairManageAccountDeviceRequestCaptor.getValue());
+
+ // AOSP receives SUCCESS response.
+ verify(mManageAccountDeviceCallback).onSuccess();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testErrorPathLoadFastPairAntispoofKeyDeviceMetadata() throws Exception {
+ mErrorPathFastPairDataProvider.asProvider().loadFastPairAntispoofKeyDeviceMetadata(
+ FAST_PAIR_ANTI_SPOOF_KEY_DEVICE_METADATA_REQUEST_PARCEL,
+ mAntispoofKeyDeviceMetadataCallback);
+ verify(mMockFastPairDataProviderService).onLoadFastPairAntispoofKeyDeviceMetadata(
+ any(FastPairDataProviderService.FastPairAntispoofKeyDeviceMetadataRequest.class),
+ any(FastPairDataProviderService.FastPairAntispoofKeyDeviceMetadataCallback.class));
+ verify(mAntispoofKeyDeviceMetadataCallback).onError(
+ eq(ERROR_CODE_BAD_REQUEST), eq(ERROR_STRING));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testErrorPathLoadFastPairAccountDevicesMetadata() throws Exception {
+ mErrorPathFastPairDataProvider.asProvider().loadFastPairAccountDevicesMetadata(
+ FAST_PAIR_ACCOUNT_DEVICES_METADATA_REQUEST_PARCEL,
+ mAccountDevicesMetadataCallback);
+ verify(mMockFastPairDataProviderService).onLoadFastPairAccountDevicesMetadata(
+ any(FastPairDataProviderService.FastPairAccountDevicesMetadataRequest.class),
+ any(FastPairDataProviderService.FastPairAccountDevicesMetadataCallback.class));
+ verify(mAccountDevicesMetadataCallback).onError(
+ eq(ERROR_CODE_BAD_REQUEST), eq(ERROR_STRING));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testErrorPathLoadFastPairEligibleAccounts() throws Exception {
+ mErrorPathFastPairDataProvider.asProvider().loadFastPairEligibleAccounts(
+ FAST_PAIR_ELIGIBLE_ACCOUNTS_REQUEST_PARCEL,
+ mEligibleAccountsCallback);
+ verify(mMockFastPairDataProviderService).onLoadFastPairEligibleAccounts(
+ any(FastPairDataProviderService.FastPairEligibleAccountsRequest.class),
+ any(FastPairDataProviderService.FastPairEligibleAccountsCallback.class));
+ verify(mEligibleAccountsCallback).onError(
+ eq(ERROR_CODE_BAD_REQUEST), eq(ERROR_STRING));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testErrorPathManageFastPairAccount() throws Exception {
+ mErrorPathFastPairDataProvider.asProvider().manageFastPairAccount(
+ FAST_PAIR_MANAGE_ACCOUNT_REQUEST_PARCEL,
+ mManageAccountCallback);
+ verify(mMockFastPairDataProviderService).onManageFastPairAccount(
+ any(FastPairDataProviderService.FastPairManageAccountRequest.class),
+ any(FastPairDataProviderService.FastPairManageActionCallback.class));
+ verify(mManageAccountCallback).onError(eq(ERROR_CODE_BAD_REQUEST), eq(ERROR_STRING));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testErrorPathManageFastPairAccountDevice() throws Exception {
+ mErrorPathFastPairDataProvider.asProvider().manageFastPairAccountDevice(
+ FAST_PAIR_MANAGE_ACCOUNT_DEVICE_REQUEST_PARCEL,
+ mManageAccountDeviceCallback);
+ verify(mMockFastPairDataProviderService).onManageFastPairAccountDevice(
+ any(FastPairDataProviderService.FastPairManageAccountDeviceRequest.class),
+ any(FastPairDataProviderService.FastPairManageActionCallback.class));
+ verify(mManageAccountDeviceCallback).onError(eq(ERROR_CODE_BAD_REQUEST), eq(ERROR_STRING));
+ }
+
+ public static class MyHappyPathProvider extends FastPairDataProviderService {
+
+ private final FastPairDataProviderService mMockFastPairDataProviderService;
+
+ public MyHappyPathProvider(@NonNull String tag, FastPairDataProviderService mock) {
+ super(tag);
+ mMockFastPairDataProviderService = mock;
+ }
+
+ public IFastPairDataProvider asProvider() {
+ Intent intent = new Intent();
+ return IFastPairDataProvider.Stub.asInterface(onBind(intent));
+ }
+
+ @Override
+ public void onLoadFastPairAntispoofKeyDeviceMetadata(
+ @NonNull FastPairAntispoofKeyDeviceMetadataRequest request,
+ @NonNull FastPairAntispoofKeyDeviceMetadataCallback callback) {
+ mMockFastPairDataProviderService.onLoadFastPairAntispoofKeyDeviceMetadata(
+ request, callback);
+ callback.onFastPairAntispoofKeyDeviceMetadataReceived(
+ HAPPY_PATH_FAST_PAIR_ANTI_SPOOF_KEY_DEVICE_METADATA);
+ }
+
+ @Override
+ public void onLoadFastPairAccountDevicesMetadata(
+ @NonNull FastPairAccountDevicesMetadataRequest request,
+ @NonNull FastPairAccountDevicesMetadataCallback callback) {
+ mMockFastPairDataProviderService.onLoadFastPairAccountDevicesMetadata(
+ request, callback);
+ callback.onFastPairAccountDevicesMetadataReceived(FAST_PAIR_ACCOUNT_DEVICES_METADATA);
+ }
+
+ @Override
+ public void onLoadFastPairEligibleAccounts(
+ @NonNull FastPairEligibleAccountsRequest request,
+ @NonNull FastPairEligibleAccountsCallback callback) {
+ mMockFastPairDataProviderService.onLoadFastPairEligibleAccounts(
+ request, callback);
+ callback.onFastPairEligibleAccountsReceived(ELIGIBLE_ACCOUNTS);
+ }
+
+ @Override
+ public void onManageFastPairAccount(
+ @NonNull FastPairManageAccountRequest request,
+ @NonNull FastPairManageActionCallback callback) {
+ mMockFastPairDataProviderService.onManageFastPairAccount(request, callback);
+ callback.onSuccess();
+ }
+
+ @Override
+ public void onManageFastPairAccountDevice(
+ @NonNull FastPairManageAccountDeviceRequest request,
+ @NonNull FastPairManageActionCallback callback) {
+ mMockFastPairDataProviderService.onManageFastPairAccountDevice(request, callback);
+ callback.onSuccess();
+ }
+ }
+
+ public static class MyErrorPathProvider extends FastPairDataProviderService {
+
+ private final FastPairDataProviderService mMockFastPairDataProviderService;
+
+ public MyErrorPathProvider(@NonNull String tag, FastPairDataProviderService mock) {
+ super(tag);
+ mMockFastPairDataProviderService = mock;
+ }
+
+ public IFastPairDataProvider asProvider() {
+ Intent intent = new Intent();
+ return IFastPairDataProvider.Stub.asInterface(onBind(intent));
+ }
+
+ @Override
+ public void onLoadFastPairAntispoofKeyDeviceMetadata(
+ @NonNull FastPairAntispoofKeyDeviceMetadataRequest request,
+ @NonNull FastPairAntispoofKeyDeviceMetadataCallback callback) {
+ mMockFastPairDataProviderService.onLoadFastPairAntispoofKeyDeviceMetadata(
+ request, callback);
+ callback.onError(ERROR_CODE_BAD_REQUEST, ERROR_STRING);
+ }
+
+ @Override
+ public void onLoadFastPairAccountDevicesMetadata(
+ @NonNull FastPairAccountDevicesMetadataRequest request,
+ @NonNull FastPairAccountDevicesMetadataCallback callback) {
+ mMockFastPairDataProviderService.onLoadFastPairAccountDevicesMetadata(
+ request, callback);
+ callback.onError(ERROR_CODE_BAD_REQUEST, ERROR_STRING);
+ }
+
+ @Override
+ public void onLoadFastPairEligibleAccounts(
+ @NonNull FastPairEligibleAccountsRequest request,
+ @NonNull FastPairEligibleAccountsCallback callback) {
+ mMockFastPairDataProviderService.onLoadFastPairEligibleAccounts(request, callback);
+ callback.onError(ERROR_CODE_BAD_REQUEST, ERROR_STRING);
+ }
+
+ @Override
+ public void onManageFastPairAccount(
+ @NonNull FastPairManageAccountRequest request,
+ @NonNull FastPairManageActionCallback callback) {
+ mMockFastPairDataProviderService.onManageFastPairAccount(request, callback);
+ callback.onError(ERROR_CODE_BAD_REQUEST, ERROR_STRING);
+ }
+
+ @Override
+ public void onManageFastPairAccountDevice(
+ @NonNull FastPairManageAccountDeviceRequest request,
+ @NonNull FastPairManageActionCallback callback) {
+ mMockFastPairDataProviderService.onManageFastPairAccountDevice(request, callback);
+ callback.onError(ERROR_CODE_BAD_REQUEST, ERROR_STRING);
+ }
+ }
+
+ /* Generates AntispoofKeyDeviceMetadataRequestParcel. */
+ private static FastPairAntispoofKeyDeviceMetadataRequestParcel
+ genFastPairAntispoofKeyDeviceMetadataRequestParcel() {
+ FastPairAntispoofKeyDeviceMetadataRequestParcel requestParcel =
+ new FastPairAntispoofKeyDeviceMetadataRequestParcel();
+ requestParcel.modelId = REQUEST_MODEL_ID;
+
+ return requestParcel;
+ }
+
+ /* Generates AccountDevicesMetadataRequestParcel. */
+ private static FastPairAccountDevicesMetadataRequestParcel
+ genFastPairAccountDevicesMetadataRequestParcel() {
+ FastPairAccountDevicesMetadataRequestParcel requestParcel =
+ new FastPairAccountDevicesMetadataRequestParcel();
+
+ requestParcel.account = ACCOUNTDEVICES_METADATA_ACCOUNT;
+ requestParcel.deviceAccountKeys = new ByteArrayParcel[NUM_ACCOUNT_DEVICES];
+ requestParcel.deviceAccountKeys[0] = new ByteArrayParcel();
+ requestParcel.deviceAccountKeys[1] = new ByteArrayParcel();
+ requestParcel.deviceAccountKeys[0].byteArray = ACCOUNT_KEY;
+ requestParcel.deviceAccountKeys[1].byteArray = ACCOUNT_KEY_2;
+
+ return requestParcel;
+ }
+
+ /* Generates FastPairEligibleAccountsRequestParcel. */
+ private static FastPairEligibleAccountsRequestParcel
+ genFastPairEligibleAccountsRequestParcel() {
+ FastPairEligibleAccountsRequestParcel requestParcel =
+ new FastPairEligibleAccountsRequestParcel();
+ // No fields since FastPairEligibleAccountsRequestParcel is just a place holder now.
+ return requestParcel;
+ }
+
+ /* Generates FastPairManageAccountRequestParcel. */
+ private static FastPairManageAccountRequestParcel
+ genFastPairManageAccountRequestParcel() {
+ FastPairManageAccountRequestParcel requestParcel =
+ new FastPairManageAccountRequestParcel();
+ requestParcel.account = MANAGE_ACCOUNT;
+ requestParcel.requestType = MANAGE_ACCOUNT_REQUEST_TYPE;
+
+ return requestParcel;
+ }
+
+ /* Generates FastPairManageAccountDeviceRequestParcel. */
+ private static FastPairManageAccountDeviceRequestParcel
+ genFastPairManageAccountDeviceRequestParcel() {
+ FastPairManageAccountDeviceRequestParcel requestParcel =
+ new FastPairManageAccountDeviceRequestParcel();
+ requestParcel.account = MANAGE_ACCOUNT;
+ requestParcel.requestType = MANAGE_ACCOUNT_REQUEST_TYPE;
+ requestParcel.accountKeyDeviceMetadata =
+ genHappyPathFastPairAccountkeyDeviceMetadataParcel();
+
+ return requestParcel;
+ }
+
+ /* Generates Happy Path AntispoofKeyDeviceMetadata. */
+ private static FastPairAntispoofKeyDeviceMetadata
+ genHappyPathFastPairAntispoofKeyDeviceMetadata() {
+ FastPairAntispoofKeyDeviceMetadata.Builder builder =
+ new FastPairAntispoofKeyDeviceMetadata.Builder();
+ builder.setAntispoofPublicKey(ANTI_SPOOFING_KEY);
+ builder.setFastPairDeviceMetadata(genHappyPathFastPairDeviceMetadata());
+
+ return builder.build();
+ }
+
+ /* Generates Happy Path FastPairAccountKeyDeviceMetadata. */
+ private static FastPairAccountKeyDeviceMetadata
+ genHappyPathFastPairAccountkeyDeviceMetadata() {
+ FastPairAccountKeyDeviceMetadata.Builder builder =
+ new FastPairAccountKeyDeviceMetadata.Builder();
+ builder.setDeviceAccountKey(ACCOUNT_KEY);
+ builder.setFastPairDeviceMetadata(genHappyPathFastPairDeviceMetadata());
+ builder.setSha256DeviceAccountKeyPublicAddress(SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS);
+ builder.setFastPairDiscoveryItem(genHappyPathFastPairDiscoveryItem());
+
+ return builder.build();
+ }
+
+ /* Generates Happy Path FastPairAccountKeyDeviceMetadataParcel. */
+ private static FastPairAccountKeyDeviceMetadataParcel
+ genHappyPathFastPairAccountkeyDeviceMetadataParcel() {
+ FastPairAccountKeyDeviceMetadataParcel parcel =
+ new FastPairAccountKeyDeviceMetadataParcel();
+ parcel.deviceAccountKey = ACCOUNT_KEY;
+ parcel.metadata = genHappyPathFastPairDeviceMetadataParcel();
+ parcel.sha256DeviceAccountKeyPublicAddress = SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS;
+ parcel.discoveryItem = genHappyPathFastPairDiscoveryItemParcel();
+
+ return parcel;
+ }
+
+ /* Generates Happy Path DiscoveryItem. */
+ private static FastPairDiscoveryItem genHappyPathFastPairDiscoveryItem() {
+ FastPairDiscoveryItem.Builder builder = new FastPairDiscoveryItem.Builder();
+
+ builder.setActionUrl(ACTION_URL);
+ builder.setActionUrlType(ACTION_URL_TYPE);
+ builder.setAppName(APP_NAME);
+ builder.setAuthenticationPublicKeySecp256r1(AUTHENTICATION_PUBLIC_KEY_SEC_P256R1);
+ builder.setDescription(DESCRIPTION);
+ builder.setDeviceName(DEVICE_NAME);
+ builder.setDisplayUrl(DISPLAY_URL);
+ builder.setFirstObservationTimestampMillis(FIRST_OBSERVATION_TIMESTAMP_MILLIS);
+ builder.setIconFfeUrl(ICON_FIFE_URL);
+ builder.setIconPng(ICON_PNG);
+ builder.setId(ID);
+ builder.setLastObservationTimestampMillis(LAST_OBSERVATION_TIMESTAMP_MILLIS);
+ builder.setMacAddress(MAC_ADDRESS);
+ builder.setPackageName(PACKAGE_NAME);
+ builder.setPendingAppInstallTimestampMillis(PENDING_APP_INSTALL_TIMESTAMP_MILLIS);
+ builder.setRssi(RSSI);
+ builder.setState(STATE);
+ builder.setTitle(TITLE);
+ builder.setTriggerId(TRIGGER_ID);
+ builder.setTxPower(TX_POWER);
+
+ return builder.build();
+ }
+
+ /* Generates Happy Path DiscoveryItemParcel. */
+ private static FastPairDiscoveryItemParcel genHappyPathFastPairDiscoveryItemParcel() {
+ FastPairDiscoveryItemParcel parcel = new FastPairDiscoveryItemParcel();
+
+ parcel.actionUrl = ACTION_URL;
+ parcel.actionUrlType = ACTION_URL_TYPE;
+ parcel.appName = APP_NAME;
+ parcel.authenticationPublicKeySecp256r1 = AUTHENTICATION_PUBLIC_KEY_SEC_P256R1;
+ parcel.description = DESCRIPTION;
+ parcel.deviceName = DEVICE_NAME;
+ parcel.displayUrl = DISPLAY_URL;
+ parcel.firstObservationTimestampMillis = FIRST_OBSERVATION_TIMESTAMP_MILLIS;
+ parcel.iconFifeUrl = ICON_FIFE_URL;
+ parcel.iconPng = ICON_PNG;
+ parcel.id = ID;
+ parcel.lastObservationTimestampMillis = LAST_OBSERVATION_TIMESTAMP_MILLIS;
+ parcel.macAddress = MAC_ADDRESS;
+ parcel.packageName = PACKAGE_NAME;
+ parcel.pendingAppInstallTimestampMillis = PENDING_APP_INSTALL_TIMESTAMP_MILLIS;
+ parcel.rssi = RSSI;
+ parcel.state = STATE;
+ parcel.title = TITLE;
+ parcel.triggerId = TRIGGER_ID;
+ parcel.txPower = TX_POWER;
+
+ return parcel;
+ }
+
+ /* Generates Happy Path DeviceMetadata. */
+ private static FastPairDeviceMetadata genHappyPathFastPairDeviceMetadata() {
+ FastPairDeviceMetadata.Builder builder = new FastPairDeviceMetadata.Builder();
+ builder.setBleTxPower(BLE_TX_POWER);
+ builder.setConnectSuccessCompanionAppInstalled(CONNECT_SUCCESS_COMPANION_APP_INSTALLED);
+ builder.setConnectSuccessCompanionAppNotInstalled(
+ CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED);
+ builder.setDeviceType(DEVICE_TYPE);
+ builder.setDownloadCompanionAppDescription(DOWNLOAD_COMPANION_APP_DESCRIPTION);
+ builder.setFailConnectGoToSettingsDescription(FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION);
+ builder.setImage(IMAGE);
+ builder.setImageUrl(IMAGE_URL);
+ builder.setInitialNotificationDescription(INITIAL_NOTIFICATION_DESCRIPTION);
+ builder.setInitialNotificationDescriptionNoAccount(
+ INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT);
+ builder.setInitialPairingDescription(INITIAL_PAIRING_DESCRIPTION);
+ builder.setIntentUri(INTENT_URI);
+ builder.setName(NAME);
+ builder.setOpenCompanionAppDescription(OPEN_COMPANION_APP_DESCRIPTION);
+ builder.setRetroactivePairingDescription(RETRO_ACTIVE_PAIRING_DESCRIPTION);
+ builder.setSubsequentPairingDescription(SUBSEQUENT_PAIRING_DESCRIPTION);
+ builder.setTriggerDistance(TRIGGER_DISTANCE);
+ builder.setTrueWirelessImageUrlCase(TRUE_WIRELESS_IMAGE_URL_CASE);
+ builder.setTrueWirelessImageUrlLeftBud(TRUE_WIRELESS_IMAGE_URL_LEFT_BUD);
+ builder.setTrueWirelessImageUrlRightBud(TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD);
+ builder.setUnableToConnectDescription(UNABLE_TO_CONNECT_DESCRIPTION);
+ builder.setUnableToConnectTitle(UNABLE_TO_CONNECT_TITLE);
+ builder.setUpdateCompanionAppDescription(UPDATE_COMPANION_APP_DESCRIPTION);
+ builder.setWaitLaunchCompanionAppDescription(WAIT_LAUNCH_COMPANION_APP_DESCRIPTION);
+
+ return builder.build();
+ }
+
+ /* Generates Happy Path DeviceMetadataParcel. */
+ private static FastPairDeviceMetadataParcel genHappyPathFastPairDeviceMetadataParcel() {
+ FastPairDeviceMetadataParcel parcel = new FastPairDeviceMetadataParcel();
+
+ parcel.bleTxPower = BLE_TX_POWER;
+ parcel.connectSuccessCompanionAppInstalled = CONNECT_SUCCESS_COMPANION_APP_INSTALLED;
+ parcel.connectSuccessCompanionAppNotInstalled =
+ CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED;
+ parcel.deviceType = DEVICE_TYPE;
+ parcel.downloadCompanionAppDescription = DOWNLOAD_COMPANION_APP_DESCRIPTION;
+ parcel.failConnectGoToSettingsDescription = FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION;
+ parcel.image = IMAGE;
+ parcel.imageUrl = IMAGE_URL;
+ parcel.initialNotificationDescription = INITIAL_NOTIFICATION_DESCRIPTION;
+ parcel.initialNotificationDescriptionNoAccount =
+ INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT;
+ parcel.initialPairingDescription = INITIAL_PAIRING_DESCRIPTION;
+ parcel.intentUri = INTENT_URI;
+ parcel.name = NAME;
+ parcel.openCompanionAppDescription = OPEN_COMPANION_APP_DESCRIPTION;
+ parcel.retroactivePairingDescription = RETRO_ACTIVE_PAIRING_DESCRIPTION;
+ parcel.subsequentPairingDescription = SUBSEQUENT_PAIRING_DESCRIPTION;
+ parcel.triggerDistance = TRIGGER_DISTANCE;
+ parcel.trueWirelessImageUrlCase = TRUE_WIRELESS_IMAGE_URL_CASE;
+ parcel.trueWirelessImageUrlLeftBud = TRUE_WIRELESS_IMAGE_URL_LEFT_BUD;
+ parcel.trueWirelessImageUrlRightBud = TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD;
+ parcel.unableToConnectDescription = UNABLE_TO_CONNECT_DESCRIPTION;
+ parcel.unableToConnectTitle = UNABLE_TO_CONNECT_TITLE;
+ parcel.updateCompanionAppDescription = UPDATE_COMPANION_APP_DESCRIPTION;
+ parcel.waitLaunchCompanionAppDescription = WAIT_LAUNCH_COMPANION_APP_DESCRIPTION;
+
+ return parcel;
+ }
+
+ /* Generates Happy Path FastPairEligibleAccount. */
+ private static FastPairEligibleAccount genHappyPathFastPairEligibleAccount(
+ Account account, boolean optIn) {
+ FastPairEligibleAccount.Builder builder = new FastPairEligibleAccount.Builder();
+ builder.setAccount(account);
+ builder.setOptIn(optIn);
+
+ return builder.build();
+ }
+
+ /* Verifies Happy Path AntispoofKeyDeviceMetadataRequest. */
+ private static void ensureHappyPathAsExpected(
+ FastPairDataProviderService.FastPairAntispoofKeyDeviceMetadataRequest request) {
+ assertThat(request.getModelId()).isEqualTo(REQUEST_MODEL_ID);
+ }
+
+ /* Verifies Happy Path AccountDevicesMetadataRequest. */
+ private static void ensureHappyPathAsExpected(
+ FastPairDataProviderService.FastPairAccountDevicesMetadataRequest request) {
+ assertThat(request.getAccount()).isEqualTo(ACCOUNTDEVICES_METADATA_ACCOUNT);
+ assertThat(request.getDeviceAccountKeys().size()).isEqualTo(ACCOUNTKEY_DEVICE_NUM);
+ assertThat(request.getDeviceAccountKeys()).contains(ACCOUNT_KEY);
+ assertThat(request.getDeviceAccountKeys()).contains(ACCOUNT_KEY_2);
+ }
+
+ /* Verifies Happy Path FastPairEligibleAccountsRequest. */
+ @SuppressWarnings("UnusedVariable")
+ private static void ensureHappyPathAsExpected(
+ FastPairDataProviderService.FastPairEligibleAccountsRequest request) {
+ // No fields since FastPairEligibleAccountsRequest is just a place holder now.
+ }
+
+ /* Verifies Happy Path FastPairManageAccountRequest. */
+ private static void ensureHappyPathAsExpected(
+ FastPairDataProviderService.FastPairManageAccountRequest request) {
+ assertThat(request.getAccount()).isEqualTo(MANAGE_ACCOUNT);
+ assertThat(request.getRequestType()).isEqualTo(MANAGE_ACCOUNT_REQUEST_TYPE);
+ }
+
+ /* Verifies Happy Path FastPairManageAccountDeviceRequest. */
+ private static void ensureHappyPathAsExpected(
+ FastPairDataProviderService.FastPairManageAccountDeviceRequest request) {
+ assertThat(request.getAccount()).isEqualTo(MANAGE_ACCOUNT);
+ assertThat(request.getRequestType()).isEqualTo(MANAGE_ACCOUNT_REQUEST_TYPE);
+ ensureHappyPathAsExpected(request.getAccountKeyDeviceMetadata());
+ }
+
+ /* Verifies Happy Path AntispoofKeyDeviceMetadataParcel. */
+ private static void ensureHappyPathAsExpected(
+ FastPairAntispoofKeyDeviceMetadataParcel metadataParcel) {
+ assertThat(metadataParcel).isNotNull();
+ assertThat(metadataParcel.antispoofPublicKey).isEqualTo(ANTI_SPOOFING_KEY);
+ ensureHappyPathAsExpected(metadataParcel.deviceMetadata);
+ }
+
+ /* Verifies Happy Path FastPairAccountKeyDeviceMetadataParcel[]. */
+ private static void ensureHappyPathAsExpected(
+ FastPairAccountKeyDeviceMetadataParcel[] metadataParcels) {
+ assertThat(metadataParcels).isNotNull();
+ assertThat(metadataParcels).hasLength(ACCOUNTKEY_DEVICE_NUM);
+ for (FastPairAccountKeyDeviceMetadataParcel parcel: metadataParcels) {
+ ensureHappyPathAsExpected(parcel);
+ }
+ }
+
+ /* Verifies Happy Path FastPairAccountKeyDeviceMetadataParcel. */
+ private static void ensureHappyPathAsExpected(
+ FastPairAccountKeyDeviceMetadataParcel metadataParcel) {
+ assertThat(metadataParcel).isNotNull();
+ assertThat(metadataParcel.deviceAccountKey).isEqualTo(ACCOUNT_KEY);
+ assertThat(metadataParcel.sha256DeviceAccountKeyPublicAddress)
+ .isEqualTo(SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS);
+ ensureHappyPathAsExpected(metadataParcel.metadata);
+ ensureHappyPathAsExpected(metadataParcel.discoveryItem);
+ }
+
+ /* Verifies Happy Path FastPairAccountKeyDeviceMetadata. */
+ private static void ensureHappyPathAsExpected(
+ FastPairAccountKeyDeviceMetadata metadata) {
+ assertThat(metadata.getDeviceAccountKey()).isEqualTo(ACCOUNT_KEY);
+ assertThat(metadata.getSha256DeviceAccountKeyPublicAddress())
+ .isEqualTo(SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS);
+ ensureHappyPathAsExpected(metadata.getFastPairDeviceMetadata());
+ ensureHappyPathAsExpected(metadata.getFastPairDiscoveryItem());
+ }
+
+ /* Verifies Happy Path DeviceMetadataParcel. */
+ private static void ensureHappyPathAsExpected(FastPairDeviceMetadataParcel metadataParcel) {
+ assertThat(metadataParcel).isNotNull();
+ assertThat(metadataParcel.bleTxPower).isEqualTo(BLE_TX_POWER);
+
+ assertThat(metadataParcel.connectSuccessCompanionAppInstalled).isEqualTo(
+ CONNECT_SUCCESS_COMPANION_APP_INSTALLED);
+ assertThat(metadataParcel.connectSuccessCompanionAppNotInstalled).isEqualTo(
+ CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED);
+
+ assertThat(metadataParcel.deviceType).isEqualTo(DEVICE_TYPE);
+ assertThat(metadataParcel.downloadCompanionAppDescription).isEqualTo(
+ DOWNLOAD_COMPANION_APP_DESCRIPTION);
+
+ assertThat(metadataParcel.failConnectGoToSettingsDescription).isEqualTo(
+ FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION);
+
+ assertThat(metadataParcel.image).isEqualTo(IMAGE);
+ assertThat(metadataParcel.imageUrl).isEqualTo(IMAGE_URL);
+ assertThat(metadataParcel.initialNotificationDescription).isEqualTo(
+ INITIAL_NOTIFICATION_DESCRIPTION);
+ assertThat(metadataParcel.initialNotificationDescriptionNoAccount).isEqualTo(
+ INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT);
+ assertThat(metadataParcel.initialPairingDescription).isEqualTo(INITIAL_PAIRING_DESCRIPTION);
+ assertThat(metadataParcel.intentUri).isEqualTo(INTENT_URI);
+
+ assertThat(metadataParcel.name).isEqualTo(NAME);
+
+ assertThat(metadataParcel.openCompanionAppDescription).isEqualTo(
+ OPEN_COMPANION_APP_DESCRIPTION);
+
+ assertThat(metadataParcel.retroactivePairingDescription).isEqualTo(
+ RETRO_ACTIVE_PAIRING_DESCRIPTION);
+
+ assertThat(metadataParcel.subsequentPairingDescription).isEqualTo(
+ SUBSEQUENT_PAIRING_DESCRIPTION);
+
+ assertThat(metadataParcel.triggerDistance).isWithin(DELTA).of(TRIGGER_DISTANCE);
+ assertThat(metadataParcel.trueWirelessImageUrlCase).isEqualTo(TRUE_WIRELESS_IMAGE_URL_CASE);
+ assertThat(metadataParcel.trueWirelessImageUrlLeftBud).isEqualTo(
+ TRUE_WIRELESS_IMAGE_URL_LEFT_BUD);
+ assertThat(metadataParcel.trueWirelessImageUrlRightBud).isEqualTo(
+ TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD);
+
+ assertThat(metadataParcel.unableToConnectDescription).isEqualTo(
+ UNABLE_TO_CONNECT_DESCRIPTION);
+ assertThat(metadataParcel.unableToConnectTitle).isEqualTo(UNABLE_TO_CONNECT_TITLE);
+ assertThat(metadataParcel.updateCompanionAppDescription).isEqualTo(
+ UPDATE_COMPANION_APP_DESCRIPTION);
+
+ assertThat(metadataParcel.waitLaunchCompanionAppDescription).isEqualTo(
+ WAIT_LAUNCH_COMPANION_APP_DESCRIPTION);
+ }
+
+ /* Verifies Happy Path DeviceMetadata. */
+ private static void ensureHappyPathAsExpected(FastPairDeviceMetadata metadata) {
+ assertThat(metadata.getBleTxPower()).isEqualTo(BLE_TX_POWER);
+ assertThat(metadata.getConnectSuccessCompanionAppInstalled())
+ .isEqualTo(CONNECT_SUCCESS_COMPANION_APP_INSTALLED);
+ assertThat(metadata.getConnectSuccessCompanionAppNotInstalled())
+ .isEqualTo(CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED);
+ assertThat(metadata.getDeviceType()).isEqualTo(DEVICE_TYPE);
+ assertThat(metadata.getDownloadCompanionAppDescription())
+ .isEqualTo(DOWNLOAD_COMPANION_APP_DESCRIPTION);
+ assertThat(metadata.getFailConnectGoToSettingsDescription())
+ .isEqualTo(FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION);
+ assertThat(metadata.getImage()).isEqualTo(IMAGE);
+ assertThat(metadata.getImageUrl()).isEqualTo(IMAGE_URL);
+ assertThat(metadata.getInitialNotificationDescription())
+ .isEqualTo(INITIAL_NOTIFICATION_DESCRIPTION);
+ assertThat(metadata.getInitialNotificationDescriptionNoAccount())
+ .isEqualTo(INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT);
+ assertThat(metadata.getInitialPairingDescription()).isEqualTo(INITIAL_PAIRING_DESCRIPTION);
+ assertThat(metadata.getIntentUri()).isEqualTo(INTENT_URI);
+ assertThat(metadata.getName()).isEqualTo(NAME);
+ assertThat(metadata.getOpenCompanionAppDescription())
+ .isEqualTo(OPEN_COMPANION_APP_DESCRIPTION);
+ assertThat(metadata.getRetroactivePairingDescription())
+ .isEqualTo(RETRO_ACTIVE_PAIRING_DESCRIPTION);
+ assertThat(metadata.getSubsequentPairingDescription())
+ .isEqualTo(SUBSEQUENT_PAIRING_DESCRIPTION);
+ assertThat(metadata.getTriggerDistance()).isWithin(DELTA).of(TRIGGER_DISTANCE);
+ assertThat(metadata.getTrueWirelessImageUrlCase()).isEqualTo(TRUE_WIRELESS_IMAGE_URL_CASE);
+ assertThat(metadata.getTrueWirelessImageUrlLeftBud())
+ .isEqualTo(TRUE_WIRELESS_IMAGE_URL_LEFT_BUD);
+ assertThat(metadata.getTrueWirelessImageUrlRightBud())
+ .isEqualTo(TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD);
+ assertThat(metadata.getUnableToConnectDescription())
+ .isEqualTo(UNABLE_TO_CONNECT_DESCRIPTION);
+ assertThat(metadata.getUnableToConnectTitle()).isEqualTo(UNABLE_TO_CONNECT_TITLE);
+ assertThat(metadata.getUpdateCompanionAppDescription())
+ .isEqualTo(UPDATE_COMPANION_APP_DESCRIPTION);
+ assertThat(metadata.getWaitLaunchCompanionAppDescription())
+ .isEqualTo(WAIT_LAUNCH_COMPANION_APP_DESCRIPTION);
+ }
+
+ /* Verifies Happy Path FastPairDiscoveryItemParcel. */
+ private static void ensureHappyPathAsExpected(FastPairDiscoveryItemParcel itemParcel) {
+ assertThat(itemParcel.actionUrl).isEqualTo(ACTION_URL);
+ assertThat(itemParcel.actionUrlType).isEqualTo(ACTION_URL_TYPE);
+ assertThat(itemParcel.appName).isEqualTo(APP_NAME);
+ assertThat(itemParcel.authenticationPublicKeySecp256r1)
+ .isEqualTo(AUTHENTICATION_PUBLIC_KEY_SEC_P256R1);
+ assertThat(itemParcel.description).isEqualTo(DESCRIPTION);
+ assertThat(itemParcel.deviceName).isEqualTo(DEVICE_NAME);
+ assertThat(itemParcel.displayUrl).isEqualTo(DISPLAY_URL);
+ assertThat(itemParcel.firstObservationTimestampMillis)
+ .isEqualTo(FIRST_OBSERVATION_TIMESTAMP_MILLIS);
+ assertThat(itemParcel.iconFifeUrl).isEqualTo(ICON_FIFE_URL);
+ assertThat(itemParcel.iconPng).isEqualTo(ICON_PNG);
+ assertThat(itemParcel.id).isEqualTo(ID);
+ assertThat(itemParcel.lastObservationTimestampMillis)
+ .isEqualTo(LAST_OBSERVATION_TIMESTAMP_MILLIS);
+ assertThat(itemParcel.macAddress).isEqualTo(MAC_ADDRESS);
+ assertThat(itemParcel.packageName).isEqualTo(PACKAGE_NAME);
+ assertThat(itemParcel.pendingAppInstallTimestampMillis)
+ .isEqualTo(PENDING_APP_INSTALL_TIMESTAMP_MILLIS);
+ assertThat(itemParcel.rssi).isEqualTo(RSSI);
+ assertThat(itemParcel.state).isEqualTo(STATE);
+ assertThat(itemParcel.title).isEqualTo(TITLE);
+ assertThat(itemParcel.triggerId).isEqualTo(TRIGGER_ID);
+ assertThat(itemParcel.txPower).isEqualTo(TX_POWER);
+ }
+
+ /* Verifies Happy Path FastPairDiscoveryItem. */
+ private static void ensureHappyPathAsExpected(FastPairDiscoveryItem item) {
+ assertThat(item.getActionUrl()).isEqualTo(ACTION_URL);
+ assertThat(item.getActionUrlType()).isEqualTo(ACTION_URL_TYPE);
+ assertThat(item.getAppName()).isEqualTo(APP_NAME);
+ assertThat(item.getAuthenticationPublicKeySecp256r1())
+ .isEqualTo(AUTHENTICATION_PUBLIC_KEY_SEC_P256R1);
+ assertThat(item.getDescription()).isEqualTo(DESCRIPTION);
+ assertThat(item.getDeviceName()).isEqualTo(DEVICE_NAME);
+ assertThat(item.getDisplayUrl()).isEqualTo(DISPLAY_URL);
+ assertThat(item.getFirstObservationTimestampMillis())
+ .isEqualTo(FIRST_OBSERVATION_TIMESTAMP_MILLIS);
+ assertThat(item.getIconFfeUrl()).isEqualTo(ICON_FIFE_URL);
+ assertThat(item.getIconPng()).isEqualTo(ICON_PNG);
+ assertThat(item.getId()).isEqualTo(ID);
+ assertThat(item.getLastObservationTimestampMillis())
+ .isEqualTo(LAST_OBSERVATION_TIMESTAMP_MILLIS);
+ assertThat(item.getMacAddress()).isEqualTo(MAC_ADDRESS);
+ assertThat(item.getPackageName()).isEqualTo(PACKAGE_NAME);
+ assertThat(item.getPendingAppInstallTimestampMillis())
+ .isEqualTo(PENDING_APP_INSTALL_TIMESTAMP_MILLIS);
+ assertThat(item.getRssi()).isEqualTo(RSSI);
+ assertThat(item.getState()).isEqualTo(STATE);
+ assertThat(item.getTitle()).isEqualTo(TITLE);
+ assertThat(item.getTriggerId()).isEqualTo(TRIGGER_ID);
+ assertThat(item.getTxPower()).isEqualTo(TX_POWER);
+ }
+
+ /* Verifies Happy Path EligibleAccountParcel[]. */
+ private static void ensureHappyPathAsExpected(FastPairEligibleAccountParcel[] accountsParcel) {
+ assertThat(accountsParcel).hasLength(ELIGIBLE_ACCOUNTS_NUM);
+
+ assertThat(accountsParcel[0].account).isEqualTo(ELIGIBLE_ACCOUNT_1);
+ assertThat(accountsParcel[0].optIn).isEqualTo(ELIGIBLE_ACCOUNT_1_OPT_IN);
+
+ assertThat(accountsParcel[1].account).isEqualTo(ELIGIBLE_ACCOUNT_2);
+ assertThat(accountsParcel[1].optIn).isEqualTo(ELIGIBLE_ACCOUNT_2_OPT_IN);
+ }
+}
diff --git a/nearby/tests/unit/src/android/nearby/FastPairDeviceTest.java b/nearby/tests/unit/src/android/nearby/FastPairDeviceTest.java
new file mode 100644
index 0000000..edda3c2
--- /dev/null
+++ b/nearby/tests/unit/src/android/nearby/FastPairDeviceTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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 android.nearby;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Parcel;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class FastPairDeviceTest {
+ private static final String NAME = "name";
+ private static final byte[] DATA = new byte[] {0x01, 0x02};
+ private static final String MODEL_ID = "112233";
+ private static final int RSSI = -80;
+ private static final int TX_POWER = -10;
+ private static final String MAC_ADDRESS = "00:11:22:33:44:55";
+ private static List<Integer> sMediums = new ArrayList<Integer>(List.of(1));
+ private static FastPairDevice sDevice;
+
+
+ @Before
+ public void setup() {
+ sDevice = new FastPairDevice(NAME, sMediums, RSSI, TX_POWER, MODEL_ID, MAC_ADDRESS, DATA);
+ }
+
+ @Test
+ public void testParcelable() {
+ Parcel dest = Parcel.obtain();
+ sDevice.writeToParcel(dest, 0);
+ dest.setDataPosition(0);
+ FastPairDevice compareDevice = FastPairDevice.CREATOR.createFromParcel(dest);
+ assertThat(compareDevice.getName()).isEqualTo(NAME);
+ assertThat(compareDevice.getMediums()).isEqualTo(sMediums);
+ assertThat(compareDevice.getRssi()).isEqualTo(RSSI);
+ assertThat(compareDevice.getTxPower()).isEqualTo(TX_POWER);
+ assertThat(compareDevice.getModelId()).isEqualTo(MODEL_ID);
+ assertThat(compareDevice.getBluetoothAddress()).isEqualTo(MAC_ADDRESS);
+ assertThat(compareDevice.getData()).isEqualTo(DATA);
+ assertThat(compareDevice.equals(sDevice)).isTrue();
+ assertThat(compareDevice.hashCode()).isEqualTo(sDevice.hashCode());
+ }
+
+ @Test
+ public void describeContents() {
+ assertThat(sDevice.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ public void testToString() {
+ assertThat(sDevice.toString()).isEqualTo(
+ "FastPairDevice [name=name, medium={BLE} "
+ + "rssi=-80 txPower=-10 "
+ + "modelId=112233 bluetoothAddress=00:11:22:33:44:55]");
+ }
+
+ @Test
+ public void testCreatorNewArray() {
+ FastPairDevice[] fastPairDevices = FastPairDevice.CREATOR.newArray(2);
+ assertThat(fastPairDevices.length).isEqualTo(2);
+ }
+
+ @Test
+ public void testBuilder() {
+ FastPairDevice.Builder builder = new FastPairDevice.Builder();
+ FastPairDevice compareDevice = builder.setName(NAME)
+ .addMedium(1)
+ .setBluetoothAddress(MAC_ADDRESS)
+ .setRssi(RSSI)
+ .setTxPower(TX_POWER)
+ .setData(DATA)
+ .setModelId(MODEL_ID)
+ .build();
+ assertThat(compareDevice.getName()).isEqualTo(NAME);
+ assertThat(compareDevice.getMediums()).isEqualTo(sMediums);
+ assertThat(compareDevice.getRssi()).isEqualTo(RSSI);
+ assertThat(compareDevice.getTxPower()).isEqualTo(TX_POWER);
+ assertThat(compareDevice.getModelId()).isEqualTo(MODEL_ID);
+ assertThat(compareDevice.getBluetoothAddress()).isEqualTo(MAC_ADDRESS);
+ assertThat(compareDevice.getData()).isEqualTo(DATA);
+ }
+}
diff --git a/nearby/tests/unit/src/android/nearby/FastPairEligibleAccountTest.java b/nearby/tests/unit/src/android/nearby/FastPairEligibleAccountTest.java
new file mode 100644
index 0000000..da5a518
--- /dev/null
+++ b/nearby/tests/unit/src/android/nearby/FastPairEligibleAccountTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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 android.nearby;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.accounts.Account;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class FastPairEligibleAccountTest {
+
+ private static final Account ACCOUNT = new Account("abc@google.com", "type1");
+ private static final Account ACCOUNT_NULL = null;
+
+ private static final boolean OPT_IN_TRUE = true;
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testSetGetFastPairEligibleAccountNotNull() {
+ FastPairEligibleAccount eligibleAccount =
+ genFastPairEligibleAccount(ACCOUNT, OPT_IN_TRUE);
+
+ assertThat(eligibleAccount.getAccount()).isEqualTo(ACCOUNT);
+ assertThat(eligibleAccount.isOptIn()).isEqualTo(OPT_IN_TRUE);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testSetGetFastPairEligibleAccountNull() {
+ FastPairEligibleAccount eligibleAccount =
+ genFastPairEligibleAccount(ACCOUNT_NULL, OPT_IN_TRUE);
+
+ assertThat(eligibleAccount.getAccount()).isEqualTo(ACCOUNT_NULL);
+ assertThat(eligibleAccount.isOptIn()).isEqualTo(OPT_IN_TRUE);
+ }
+
+ /* Generates FastPairEligibleAccount. */
+ private static FastPairEligibleAccount genFastPairEligibleAccount(
+ Account account, boolean optIn) {
+ FastPairEligibleAccount.Builder builder = new FastPairEligibleAccount.Builder();
+ builder.setAccount(account);
+ builder.setOptIn(optIn);
+
+ return builder.build();
+ }
+}
diff --git a/nearby/tests/unit/src/android/nearby/PairStatusMetadataTest.java b/nearby/tests/unit/src/android/nearby/PairStatusMetadataTest.java
new file mode 100644
index 0000000..7bc6519
--- /dev/null
+++ b/nearby/tests/unit/src/android/nearby/PairStatusMetadataTest.java
@@ -0,0 +1,105 @@
+/*
+ * 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 android.nearby;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.Parcel;
+
+import org.junit.Test;
+
+public class PairStatusMetadataTest {
+ private static final int UNKNOWN = 1000;
+ private static final int SUCCESS = 1001;
+ private static final int FAIL = 1002;
+ private static final int DISMISS = 1003;
+
+ @Test
+ public void statusToString() {
+ assertThat(PairStatusMetadata.statusToString(UNKNOWN)).isEqualTo("UNKNOWN");
+ assertThat(PairStatusMetadata.statusToString(SUCCESS)).isEqualTo("SUCCESS");
+ assertThat(PairStatusMetadata.statusToString(FAIL)).isEqualTo("FAIL");
+ assertThat(PairStatusMetadata.statusToString(DISMISS)).isEqualTo("DISMISS");
+ }
+
+ @Test
+ public void getStatus() {
+ PairStatusMetadata pairStatusMetadata = new PairStatusMetadata(SUCCESS);
+ assertThat(pairStatusMetadata.getStatus()).isEqualTo(1001);
+ pairStatusMetadata = new PairStatusMetadata(FAIL);
+ assertThat(pairStatusMetadata.getStatus()).isEqualTo(1002);
+ pairStatusMetadata = new PairStatusMetadata(DISMISS);
+ assertThat(pairStatusMetadata.getStatus()).isEqualTo(1003);
+ pairStatusMetadata = new PairStatusMetadata(UNKNOWN);
+ assertThat(pairStatusMetadata.getStatus()).isEqualTo(1000);
+ }
+
+ @Test
+ public void testToString() {
+ PairStatusMetadata pairStatusMetadata = new PairStatusMetadata(SUCCESS);
+ assertThat(pairStatusMetadata.toString())
+ .isEqualTo("PairStatusMetadata[ status=SUCCESS]");
+ pairStatusMetadata = new PairStatusMetadata(FAIL);
+ assertThat(pairStatusMetadata.toString())
+ .isEqualTo("PairStatusMetadata[ status=FAIL]");
+ pairStatusMetadata = new PairStatusMetadata(DISMISS);
+ assertThat(pairStatusMetadata.toString())
+ .isEqualTo("PairStatusMetadata[ status=DISMISS]");
+ pairStatusMetadata = new PairStatusMetadata(UNKNOWN);
+ assertThat(pairStatusMetadata.toString())
+ .isEqualTo("PairStatusMetadata[ status=UNKNOWN]");
+ }
+
+ @Test
+ public void testEquals() {
+ PairStatusMetadata pairStatusMetadata = new PairStatusMetadata(SUCCESS);
+ PairStatusMetadata pairStatusMetadata1 = new PairStatusMetadata(SUCCESS);
+ PairStatusMetadata pairStatusMetadata2 = new PairStatusMetadata(UNKNOWN);
+ assertThat(pairStatusMetadata.equals(pairStatusMetadata1)).isTrue();
+ assertThat(pairStatusMetadata.equals(pairStatusMetadata2)).isFalse();
+ assertThat(pairStatusMetadata.hashCode()).isEqualTo(pairStatusMetadata1.hashCode());
+ }
+
+ @Test
+ public void testParcelable() {
+ PairStatusMetadata pairStatusMetadata = new PairStatusMetadata(SUCCESS);
+ Parcel dest = Parcel.obtain();
+ pairStatusMetadata.writeToParcel(dest, 0);
+ dest.setDataPosition(0);
+ PairStatusMetadata comparStatusMetadata =
+ PairStatusMetadata.CREATOR.createFromParcel(dest);
+ assertThat(pairStatusMetadata.equals(comparStatusMetadata)).isTrue();
+ }
+
+ @Test
+ public void testCreatorNewArray() {
+ PairStatusMetadata[] pairStatusMetadatas = PairStatusMetadata.CREATOR.newArray(2);
+ assertThat(pairStatusMetadatas.length).isEqualTo(2);
+ }
+
+ @Test
+ public void describeContents() {
+ PairStatusMetadata pairStatusMetadata = new PairStatusMetadata(SUCCESS);
+ assertThat(pairStatusMetadata.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ public void getStability() {
+ PairStatusMetadata pairStatusMetadata = new PairStatusMetadata(SUCCESS);
+ assertThat(pairStatusMetadata.getStability()).isEqualTo(0);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleFilterTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleFilterTest.java
index 1d3653b..c4a9729 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleFilterTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleFilterTest.java
@@ -20,7 +20,11 @@
import static org.junit.Assert.fail;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothAssignedNumbers;
import android.bluetooth.BluetoothDevice;
+import android.bluetooth.le.ScanFilter;
+import android.os.Parcel;
import android.os.ParcelUuid;
import android.util.SparseArray;
@@ -44,6 +48,83 @@
public static final ParcelUuid EDDYSTONE_SERVICE_DATA_PARCELUUID =
ParcelUuid.fromString("0000FEAA-0000-1000-8000-00805F9B34FB");
+ private final BleFilter mEddystoneFilter = createEddystoneFilter();
+ private final BleFilter mEddystoneUidFilter = createEddystoneUidFilter();
+ private final BleFilter mEddystoneUrlFilter = createEddystoneUrlFilter();
+ private final BleFilter mEddystoneEidFilter = createEddystoneEidFilter();
+ private final BleFilter mIBeaconWithoutUuidFilter = createIBeaconWithoutUuidFilter();
+ private final BleFilter mIBeaconWithUuidFilter = createIBeaconWithUuidFilter();
+ private final BleFilter mChromecastFilter =
+ new BleFilter.Builder().setServiceUuid(
+ new ParcelUuid(UUID.fromString("0000FEA0-0000-1000-8000-00805F9B34FB")))
+ .build();
+ private final BleFilter mEddystoneWithDeviceNameFilter =
+ new BleFilter.Builder()
+ .setServiceUuid(EDDYSTONE_SERVICE_DATA_PARCELUUID)
+ .setDeviceName("BERT")
+ .build();
+ private final BleFilter mEddystoneWithDeviceAddressFilter =
+ new BleFilter.Builder()
+ .setServiceUuid(EDDYSTONE_SERVICE_DATA_PARCELUUID)
+ .setDeviceAddress("00:11:22:33:AA:BB")
+ .build();
+ private final BleFilter mServiceUuidWithMaskFilter1 =
+ new BleFilter.Builder()
+ .setServiceUuid(
+ new ParcelUuid(UUID.fromString("0000FEA0-0000-1000-8000-00805F9B34FB")),
+ new ParcelUuid(UUID.fromString("0000000-0000-000-FFFF-FFFFFFFFFFFF")))
+ .build();
+ private final BleFilter mServiceUuidWithMaskFilter2 =
+ new BleFilter.Builder()
+ .setServiceUuid(
+ new ParcelUuid(UUID.fromString("0000FEA0-0000-1000-8000-00805F9B34FB")),
+ new ParcelUuid(UUID.fromString("FFFFFFF-FFFF-FFF-FFFF-FFFFFFFFFFFF")))
+ .build();
+
+ private final BleFilter mSmartSetupFilter =
+ new BleFilter.Builder()
+ .setManufacturerData(
+ BluetoothAssignedNumbers.GOOGLE,
+ new byte[] {0x00, 0x10},
+ new byte[] {0x00, (byte) 0xFF})
+ .build();
+ private final BleFilter mWearFilter =
+ new BleFilter.Builder()
+ .setManufacturerData(
+ BluetoothAssignedNumbers.GOOGLE,
+ new byte[] {0x00, 0x00, 0x00},
+ new byte[] {0x00, 0x00, (byte) 0xFF})
+ .build();
+ private final BleFilter mFakeSmartSetupSubsetFilter =
+ new BleFilter.Builder()
+ .setManufacturerData(
+ BluetoothAssignedNumbers.GOOGLE,
+ new byte[] {0x00, 0x10, 0x50},
+ new byte[] {0x00, (byte) 0xFF, (byte) 0xFF})
+ .build();
+ private final BleFilter mFakeSmartSetupNotSubsetFilter =
+ new BleFilter.Builder()
+ .setManufacturerData(
+ BluetoothAssignedNumbers.GOOGLE,
+ new byte[] {0x00, 0x10, 0x50},
+ new byte[] {0x00, (byte) 0x00, (byte) 0xFF})
+ .build();
+
+ private final BleFilter mFakeFilter1 =
+ new BleFilter.Builder()
+ .setServiceData(
+ ParcelUuid.fromString("0000110B-0000-1000-8000-00805F9B34FB"),
+ new byte[] {0x51, 0x64},
+ new byte[] {0x00, (byte) 0xFF})
+ .build();
+ private final BleFilter mFakeFilter2 =
+ new BleFilter.Builder()
+ .setServiceData(
+ ParcelUuid.fromString("0000110B-0000-1000-8000-00805F9B34FB"),
+ new byte[] {0x51, 0x64, 0x34},
+ new byte[] {0x00, (byte) 0xFF, (byte) 0xFF})
+ .build();
+
private ParcelUuid mServiceDataUuid;
private BleSighting mBleSighting;
private BleFilter.Builder mFilterBuilder;
@@ -229,6 +310,16 @@
}
@Test
+ public void serviceDataUuidNotInBleRecord() {
+ byte[] bleRecord = FastPairTestData.eir_1;
+ byte[] serviceData = {(byte) 0xe0, (byte) 0x00};
+
+ // Verify Service Data with 2-byte UUID, no data, and NOT in scan record
+ BleFilter filter = mFilterBuilder.setServiceData(mServiceDataUuid, serviceData).build();
+ assertThat(matches(filter, null, 0, bleRecord)).isFalse();
+ }
+
+ @Test
public void serviceDataMask() {
byte[] bleRecord = FastPairTestData.sd1;
BleFilter filter;
@@ -263,6 +354,18 @@
mFilterBuilder.setServiceData(mServiceDataUuid, serviceData, mask).build();
}
+ @Test
+ public void serviceDataMaskNotInBleRecord() {
+ byte[] bleRecord = FastPairTestData.eir_1;
+ BleFilter filter;
+
+ // Verify matching partial manufacturer with data and mask
+ byte[] serviceData1 = {(byte) 0xe0, (byte) 0x00, (byte) 0x15};
+ byte[] mask1 = {(byte) 0xff, (byte) 0xff, (byte) 0xff};
+ filter = mFilterBuilder.setServiceData(mServiceDataUuid, serviceData1, mask1).build();
+ assertThat(matches(filter, null, 0, bleRecord)).isFalse();
+ }
+
@Test
public void deviceNameTest() {
@@ -280,12 +383,241 @@
assertThat(matches(filter, null, 0, bleRecord)).isFalse();
}
+ @Test
+ public void deviceNameNotInBleRecord() {
+ // Verify the name filter does not match
+ byte[] bleRecord = FastPairTestData.eir_1;
+ BleFilter filter = mFilterBuilder.setDeviceName("Pedometer").build();
+ assertThat(matches(filter, null, 0, bleRecord)).isFalse();
+ }
+
+ @Test
+ public void serviceUuid() {
+ byte[] bleRecord = FastPairTestData.eddystone_header_and_uuid;
+ ParcelUuid uuid = ParcelUuid.fromString("0000FEAA-0000-1000-8000-00805F9B34FB");
+
+ BleFilter filter = mFilterBuilder.setServiceUuid(uuid).build();
+ assertMatches(filter, null, 0, bleRecord);
+ }
+
+ @Test
+ public void serviceUuidNoMatch() {
+ // Verify the name filter does not match
+ byte[] bleRecord = FastPairTestData.eddystone_header_and_uuid;
+ ParcelUuid uuid = ParcelUuid.fromString("00001804-0000-1000-8000-000000000000");
+
+ BleFilter filter = mFilterBuilder.setServiceUuid(uuid).build();
+ assertThat(matches(filter, null, 0, bleRecord)).isFalse();
+ }
+
+ @Test
+ public void serviceUuidNotInBleRecord() {
+ // Verify the name filter does not match
+ byte[] bleRecord = FastPairTestData.eir_1;
+ ParcelUuid uuid = ParcelUuid.fromString("00001804-0000-1000-8000-000000000000");
+
+ BleFilter filter = mFilterBuilder.setServiceUuid(uuid).build();
+ assertThat(matches(filter, null, 0, bleRecord)).isFalse();
+ }
+
+ @Test
+ public void serviceUuidMask() {
+ byte[] bleRecord = FastPairTestData.eddystone_header_and_uuid;
+ ParcelUuid uuid = ParcelUuid.fromString("0000FEAA-0000-1000-8000-00805F9B34FB");
+ ParcelUuid mask = ParcelUuid.fromString("00000000-0000-0000-0000-FFFFFFFFFFFF");
+ BleFilter filter = mFilterBuilder.setServiceUuid(uuid, mask).build();
+ assertMatches(filter, null, 0, bleRecord);
+ }
+
+
+ @Test
+ public void macAddress() {
+ byte[] bleRecord = FastPairTestData.eddystone_header_and_uuid;
+ String macAddress = "00:11:22:33:AA:BB";
+ BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+
+ BluetoothDevice device = adapter.getRemoteDevice(macAddress);
+ BleFilter filter = mFilterBuilder.setDeviceAddress(macAddress).build();
+ assertMatches(filter, device, 0, bleRecord);
+ }
+
+ @Test
+ public void macAddressNoMatch() {
+ byte[] bleRecord = FastPairTestData.eddystone_header_and_uuid;
+ String macAddress = "00:11:22:33:AA:00";
+ BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+
+ BluetoothDevice device = adapter.getRemoteDevice("00:11:22:33:AA:BB");
+ BleFilter filter = mFilterBuilder.setDeviceAddress(macAddress).build();
+ assertThat(matches(filter, device, 0, bleRecord)).isFalse();
+ }
+
+ @Test
+ public void eddystoneIsSuperset() {
+ // Verify eddystone subtypes pass.
+ assertThat(mEddystoneFilter.isSuperset(mEddystoneFilter)).isTrue();
+ assertThat(mEddystoneUidFilter.isSuperset(mEddystoneUidFilter)).isTrue();
+ assertThat(mEddystoneFilter.isSuperset(mEddystoneUidFilter)).isTrue();
+ assertThat(mEddystoneFilter.isSuperset(mEddystoneEidFilter)).isTrue();
+ assertThat(mEddystoneFilter.isSuperset(mEddystoneUrlFilter)).isTrue();
+
+ // Non-eddystone beacon filters should never be supersets.
+ assertThat(mEddystoneFilter.isSuperset(mIBeaconWithoutUuidFilter)).isFalse();
+ assertThat(mEddystoneFilter.isSuperset(mWearFilter)).isFalse();
+ assertThat(mEddystoneFilter.isSuperset(mSmartSetupFilter)).isFalse();
+ assertThat(mEddystoneFilter.isSuperset(mChromecastFilter)).isFalse();
+ assertThat(mEddystoneFilter.isSuperset(mFakeFilter1)).isFalse();
+ assertThat(mEddystoneFilter.isSuperset(mFakeFilter2)).isFalse();
+
+ assertThat(mEddystoneUidFilter.isSuperset(mWearFilter)).isFalse();
+ assertThat(mEddystoneUidFilter.isSuperset(mSmartSetupFilter)).isFalse();
+ assertThat(mEddystoneUidFilter.isSuperset(mChromecastFilter)).isFalse();
+ assertThat(mEddystoneUidFilter.isSuperset(mFakeFilter1)).isFalse();
+ assertThat(mEddystoneUidFilter.isSuperset(mFakeFilter2)).isFalse();
+ }
+
+ @Test
+ public void iBeaconIsSuperset() {
+ // Verify that an iBeacon filter is a superset of itself and any filters that specify UUIDs.
+ assertThat(mIBeaconWithoutUuidFilter.isSuperset(mIBeaconWithoutUuidFilter)).isTrue();
+ assertThat(mIBeaconWithoutUuidFilter.isSuperset(mIBeaconWithUuidFilter)).isTrue();
+
+ // Non-iBeacon filters should never be supersets.
+ assertThat(mIBeaconWithoutUuidFilter.isSuperset(mEddystoneEidFilter)).isFalse();
+ assertThat(mIBeaconWithoutUuidFilter.isSuperset(mEddystoneUrlFilter)).isFalse();
+ assertThat(mIBeaconWithoutUuidFilter.isSuperset(mEddystoneUidFilter)).isFalse();
+ assertThat(mIBeaconWithoutUuidFilter.isSuperset(mWearFilter)).isFalse();
+ assertThat(mIBeaconWithoutUuidFilter.isSuperset(mSmartSetupFilter)).isFalse();
+ assertThat(mIBeaconWithoutUuidFilter.isSuperset(mChromecastFilter)).isFalse();
+ assertThat(mIBeaconWithoutUuidFilter.isSuperset(mFakeFilter1)).isFalse();
+ assertThat(mIBeaconWithoutUuidFilter.isSuperset(mFakeFilter2)).isFalse();
+ }
+
+ @Test
+ public void mixedFilterIsSuperset() {
+ // Compare service data vs manufacturer data filters to verify we detect supersets
+ // correctly in filters that aren't for iBeacon and Eddystone.
+ assertThat(mWearFilter.isSuperset(mIBeaconWithoutUuidFilter)).isFalse();
+ assertThat(mSmartSetupFilter.isSuperset(mIBeaconWithoutUuidFilter)).isFalse();
+ assertThat(mChromecastFilter.isSuperset(mIBeaconWithoutUuidFilter)).isFalse();
+
+ assertThat(mWearFilter.isSuperset(mEddystoneFilter)).isFalse();
+ assertThat(mSmartSetupFilter.isSuperset(mEddystoneFilter)).isFalse();
+ assertThat(mChromecastFilter.isSuperset(mEddystoneFilter)).isFalse();
+
+ assertThat(mWearFilter.isSuperset(mEddystoneUidFilter)).isFalse();
+ assertThat(mSmartSetupFilter.isSuperset(mEddystoneUidFilter)).isFalse();
+ assertThat(mChromecastFilter.isSuperset(mEddystoneUidFilter)).isFalse();
+
+ assertThat(mWearFilter.isSuperset(mEddystoneEidFilter)).isFalse();
+ assertThat(mSmartSetupFilter.isSuperset(mEddystoneEidFilter)).isFalse();
+ assertThat(mChromecastFilter.isSuperset(mEddystoneEidFilter)).isFalse();
+
+ assertThat(mWearFilter.isSuperset(mEddystoneUrlFilter)).isFalse();
+ assertThat(mSmartSetupFilter.isSuperset(mEddystoneUrlFilter)).isFalse();
+ assertThat(mChromecastFilter.isSuperset(mEddystoneUrlFilter)).isFalse();
+
+ assertThat(mWearFilter.isSuperset(mIBeaconWithUuidFilter)).isFalse();
+ assertThat(mSmartSetupFilter.isSuperset(mIBeaconWithUuidFilter)).isFalse();
+ assertThat(mChromecastFilter.isSuperset(mIBeaconWithUuidFilter)).isFalse();
+
+ assertThat(mWearFilter.isSuperset(mChromecastFilter)).isFalse();
+ assertThat(mSmartSetupFilter.isSuperset(mChromecastFilter)).isFalse();
+ assertThat(mSmartSetupFilter.isSuperset(mWearFilter)).isFalse();
+ assertThat(mChromecastFilter.isSuperset(mWearFilter)).isFalse();
+
+ assertThat(mFakeFilter1.isSuperset(mFakeFilter2)).isTrue();
+ assertThat(mFakeFilter2.isSuperset(mFakeFilter1)).isFalse();
+ assertThat(mSmartSetupFilter.isSuperset(mFakeSmartSetupSubsetFilter)).isTrue();
+ assertThat(mSmartSetupFilter.isSuperset(mFakeSmartSetupNotSubsetFilter)).isFalse();
+
+ assertThat(mEddystoneFilter.isSuperset(mEddystoneWithDeviceNameFilter)).isTrue();
+ assertThat(mEddystoneFilter.isSuperset(mEddystoneWithDeviceAddressFilter)).isTrue();
+ assertThat(mEddystoneWithDeviceAddressFilter.isSuperset(mEddystoneFilter)).isFalse();
+
+ assertThat(mChromecastFilter.isSuperset(mServiceUuidWithMaskFilter1)).isTrue();
+ assertThat(mServiceUuidWithMaskFilter2.isSuperset(mServiceUuidWithMaskFilter1)).isFalse();
+ assertThat(mServiceUuidWithMaskFilter1.isSuperset(mServiceUuidWithMaskFilter2)).isTrue();
+ assertThat(mEddystoneFilter.isSuperset(mServiceUuidWithMaskFilter1)).isFalse();
+ }
+
+ @Test
+ public void toOsFilter_getTheSameFilterParameter() {
+ BleFilter nearbyFilter = createTestFilter();
+ ScanFilter osFilter = nearbyFilter.toOsFilter();
+ assertFilterValuesEqual(nearbyFilter, osFilter);
+ }
+
+ @Test
+ public void describeContents() {
+ BleFilter nearbyFilter = createTestFilter();
+ assertThat(nearbyFilter.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ public void testHashCode() {
+ BleFilter nearbyFilter = createTestFilter();
+ BleFilter compareFilter = new BleFilter("BERT", "00:11:22:33:AA:BB",
+ new ParcelUuid(UUID.fromString("0000FEA0-0000-1000-8000-00805F9B34FB")),
+ new ParcelUuid(UUID.fromString("FFFFFFF-FFFF-FFF-FFFF-FFFFFFFFFFFF")),
+ ParcelUuid.fromString("0000110B-0000-1000-8000-00805F9B34FB"),
+ new byte[] {0x51, 0x64}, new byte[] {0x00, (byte) 0xFF},
+ BluetoothAssignedNumbers.GOOGLE, new byte[] {0x00, 0x10},
+ new byte[] {0x00, (byte) 0xFF});
+ assertThat(nearbyFilter.hashCode()).isEqualTo(compareFilter.hashCode());
+ }
+
+ @Test
+ public void testToString() {
+ BleFilter nearbyFilter = createTestFilter();
+ assertThat(nearbyFilter.toString()).isEqualTo("BleFilter [deviceName=BERT,"
+ + " deviceAddress=00:11:22:33:AA:BB, uuid=0000fea0-0000-1000-8000-00805f9b34fb,"
+ + " uuidMask=0fffffff-ffff-0fff-ffff-ffffffffffff,"
+ + " serviceDataUuid=0000110b-0000-1000-8000-00805f9b34fb,"
+ + " serviceData=[81, 100], serviceDataMask=[0, -1],"
+ + " manufacturerId=224, manufacturerData=[0, 16], manufacturerDataMask=[0, -1]]");
+ }
+
+ @Test
+ public void testParcel() {
+ BleFilter nearbyFilter = createTestFilter();
+ Parcel parcel = Parcel.obtain();
+ nearbyFilter.writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ BleFilter compareFilter = BleFilter.CREATOR.createFromParcel(
+ parcel);
+ parcel.recycle();
+ assertThat(compareFilter.getDeviceName()).isEqualTo("BERT");
+ }
+
+ @Test
+ public void testCreatorNewArray() {
+ BleFilter[] nearbyFilters = BleFilter.CREATOR.newArray(2);
+ assertThat(nearbyFilters.length).isEqualTo(2);
+ }
+
private static boolean matches(
BleFilter filter, BluetoothDevice device, int rssi, byte[] bleRecord) {
return filter.matches(new BleSighting(device,
bleRecord, rssi, 0 /* timestampNanos */));
}
+ private static void assertFilterValuesEqual(BleFilter nearbyFilter, ScanFilter osFilter) {
+ assertThat(osFilter.getDeviceAddress()).isEqualTo(nearbyFilter.getDeviceAddress());
+ assertThat(osFilter.getDeviceName()).isEqualTo(nearbyFilter.getDeviceName());
+
+ assertThat(osFilter.getManufacturerData()).isEqualTo(nearbyFilter.getManufacturerData());
+ assertThat(osFilter.getManufacturerDataMask())
+ .isEqualTo(nearbyFilter.getManufacturerDataMask());
+ assertThat(osFilter.getManufacturerId()).isEqualTo(nearbyFilter.getManufacturerId());
+
+ assertThat(osFilter.getServiceData()).isEqualTo(nearbyFilter.getServiceData());
+ assertThat(osFilter.getServiceDataMask()).isEqualTo(nearbyFilter.getServiceDataMask());
+ assertThat(osFilter.getServiceDataUuid()).isEqualTo(nearbyFilter.getServiceDataUuid());
+
+ assertThat(osFilter.getServiceUuid()).isEqualTo(nearbyFilter.getServiceUuid());
+ assertThat(osFilter.getServiceUuidMask()).isEqualTo(nearbyFilter.getServiceUuidMask());
+ }
private static void assertMatches(
BleFilter filter, BluetoothDevice device, int rssi, byte[] bleRecordBytes) {
@@ -325,10 +657,10 @@
// UUID match.
if (filter.getServiceUuid() != null
- && !matchesServiceUuids(filter.getServiceUuid(), filter.getServiceUuidMask(),
- bleRecord.getServiceUuids())) {
- fail("The filter specifies a service UUID but it doesn't match "
- + "what's in the scan record");
+ && !matchesServiceUuids(filter.getServiceUuid(),
+ filter.getServiceUuidMask(), bleRecord.getServiceUuids())) {
+ fail("The filter specifies a service UUID "
+ + "but it doesn't match what's in the scan record");
}
// Service data match
@@ -401,6 +733,95 @@
}
}
+ private static String byteString(Map<ParcelUuid, byte[]> bytesMap) {
+ StringBuilder builder = new StringBuilder();
+ for (Map.Entry<ParcelUuid, byte[]> entry : bytesMap.entrySet()) {
+ builder.append(builder.toString().isEmpty() ? " " : "\n ");
+ builder.append(entry.getKey().toString());
+ builder.append(" --> ");
+ builder.append(byteString(entry.getValue()));
+ }
+ return builder.toString();
+ }
+
+ private static String byteString(SparseArray<byte[]> bytesArray) {
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < bytesArray.size(); i++) {
+ builder.append(builder.toString().isEmpty() ? " " : "\n ");
+ builder.append(byteString(bytesArray.valueAt(i)));
+ }
+ return builder.toString();
+ }
+
+ private static BleFilter createTestFilter() {
+ BleFilter.Builder builder = new BleFilter.Builder();
+ builder
+ .setServiceUuid(
+ new ParcelUuid(UUID.fromString("0000FEA0-0000-1000-8000-00805F9B34FB")),
+ new ParcelUuid(UUID.fromString("FFFFFFF-FFFF-FFF-FFFF-FFFFFFFFFFFF")))
+ .setDeviceAddress("00:11:22:33:AA:BB")
+ .setDeviceName("BERT")
+ .setManufacturerData(
+ BluetoothAssignedNumbers.GOOGLE,
+ new byte[] {0x00, 0x10},
+ new byte[] {0x00, (byte) 0xFF})
+ .setServiceData(
+ ParcelUuid.fromString("0000110B-0000-1000-8000-00805F9B34FB"),
+ new byte[] {0x51, 0x64},
+ new byte[] {0x00, (byte) 0xFF});
+ return builder.build();
+ }
+
+ // ref to beacon.decode.BeaconFilterBuilder.eddystoneFilter()
+ private static BleFilter createEddystoneFilter() {
+ return new BleFilter.Builder().setServiceUuid(EDDYSTONE_SERVICE_DATA_PARCELUUID).build();
+ }
+ // ref to beacon.decode.BeaconFilterBuilder.eddystoneUidFilter()
+ private static BleFilter createEddystoneUidFilter() {
+ return new BleFilter.Builder()
+ .setServiceUuid(EDDYSTONE_SERVICE_DATA_PARCELUUID)
+ .setServiceData(
+ EDDYSTONE_SERVICE_DATA_PARCELUUID, new byte[] {(short) 0x00},
+ new byte[] {(byte) 0xf0})
+ .build();
+ }
+
+ // ref to beacon.decode.BeaconFilterBuilder.eddystoneUrlFilter()
+ private static BleFilter createEddystoneUrlFilter() {
+ return new BleFilter.Builder()
+ .setServiceUuid(EDDYSTONE_SERVICE_DATA_PARCELUUID)
+ .setServiceData(
+ EDDYSTONE_SERVICE_DATA_PARCELUUID,
+ new byte[] {(short) 0x10}, new byte[] {(byte) 0xf0})
+ .build();
+ }
+
+ // ref to beacon.decode.BeaconFilterBuilder.eddystoneEidFilter()
+ private static BleFilter createEddystoneEidFilter() {
+ return new BleFilter.Builder()
+ .setServiceUuid(EDDYSTONE_SERVICE_DATA_PARCELUUID)
+ .setServiceData(
+ EDDYSTONE_SERVICE_DATA_PARCELUUID,
+ new byte[] {(short) 0x30}, new byte[] {(byte) 0xf0})
+ .build();
+ }
+
+ // ref to beacon.decode.BeaconFilterBuilder.iBeaconWithoutUuidFilter()
+ private static BleFilter createIBeaconWithoutUuidFilter() {
+ byte[] data = {(byte) 0x02, (byte) 0x15};
+ byte[] mask = {(byte) 0xff, (byte) 0xff};
+
+ return new BleFilter.Builder().setManufacturerData((short) 0x004C, data, mask).build();
+ }
+
+ // ref to beacon.decode.BeaconFilterBuilder.iBeaconWithUuidFilter()
+ private static BleFilter createIBeaconWithUuidFilter() {
+ byte[] data = getFilterData(ParcelUuid.fromString("0000FEAA-0000-1000-8000-00805F9B34FB"));
+ byte[] mask = getFilterMask(ParcelUuid.fromString("0000FEAA-0000-1000-8000-00805F9B34FB"));
+
+ return new BleFilter.Builder().setManufacturerData((short) 0x004C, data, mask).build();
+ }
+
// Ref to beacon.decode.AppleBeaconDecoder.getFilterData
private static byte[] getFilterData(ParcelUuid uuid) {
byte[] data = new byte[18];
@@ -418,6 +839,20 @@
return data;
}
+ // Ref to beacon.decode.AppleBeaconDecoder.getFilterMask
+ private static byte[] getFilterMask(ParcelUuid uuid) {
+ byte[] mask = new byte[18];
+ mask[0] = (byte) 0xff;
+ mask[1] = (byte) 0xff;
+ // Check if UUID is needed in data
+ if (uuid != null) {
+ for (int i = 0; i < 16; i++) {
+ mask[i + 2] = (byte) 0xff;
+ }
+ }
+ return mask;
+ }
+
// Ref to beacon.decode.AppleBeaconDecoder.uuidToByteArray
private static byte[] uuidToByteArray(ParcelUuid uuid) {
ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
@@ -453,24 +888,4 @@
return ((uuid.getMostSignificantBits() & mask.getMostSignificantBits())
== (data.getMostSignificantBits() & mask.getMostSignificantBits()));
}
-
- private static String byteString(Map<ParcelUuid, byte[]> bytesMap) {
- StringBuilder builder = new StringBuilder();
- for (Map.Entry<ParcelUuid, byte[]> entry : bytesMap.entrySet()) {
- builder.append(builder.toString().isEmpty() ? " " : "\n ");
- builder.append(entry.getKey().toString());
- builder.append(" --> ");
- builder.append(byteString(entry.getValue()));
- }
- return builder.toString();
- }
-
- private static String byteString(SparseArray<byte[]> bytesArray) {
- StringBuilder builder = new StringBuilder();
- for (int i = 0; i < bytesArray.size(); i++) {
- builder.append(builder.toString().isEmpty() ? " " : "\n ");
- builder.append(byteString(bytesArray.valueAt(i)));
- }
- return builder.toString();
- }
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleRecordTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleRecordTest.java
index 5da98e2..3f9a259 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleRecordTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleRecordTest.java
@@ -34,6 +34,9 @@
import static com.google.common.truth.Truth.assertThat;
+import android.os.ParcelUuid;
+import android.util.SparseArray;
+
import androidx.test.filters.SdkSuppress;
import org.junit.Test;
@@ -238,7 +241,6 @@
BleRecord record = BleRecord.parseFromBytes(BEACON);
BleRecord record2 = BleRecord.parseFromBytes(SAME_BEACON);
-
assertThat(record).isEqualTo(record2);
// Different items.
@@ -246,5 +248,48 @@
assertThat(record).isNotEqualTo(record2);
assertThat(record.hashCode()).isNotEqualTo(record2.hashCode());
}
+
+ @Test
+ public void testFields() {
+ BleRecord record = BleRecord.parseFromBytes(BEACON);
+ assertThat(byteString(record.getManufacturerSpecificData()))
+ .isEqualTo(" 0215F7826DA64FA24E988024BC5B71E0893E44D02522B3");
+ assertThat(
+ byteString(record.getServiceData(
+ ParcelUuid.fromString("000000E0-0000-1000-8000-00805F9B34FB"))))
+ .isEqualTo("[null]");
+ assertThat(record.getTxPowerLevel()).isEqualTo(-12);
+ assertThat(record.toString()).isEqualTo(
+ "BleRecord [advertiseFlags=6, serviceUuids=[], "
+ + "manufacturerSpecificData={76=[2, 21, -9, -126, 109, -90, 79, -94, 78,"
+ + " -104, -128, 36, -68, 91, 113, -32, -119, 62, 68, -48, 37, 34, -77]},"
+ + " serviceData={0000d00d-0000-1000-8000-00805f9b34fb"
+ + "=[116, 109, 77, 107, 50, 54, 100]},"
+ + " txPowerLevel=-12, deviceName=Kontakt]");
+ }
+
+ private static String byteString(SparseArray<byte[]> bytesArray) {
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < bytesArray.size(); i++) {
+ builder.append(builder.toString().isEmpty() ? " " : "\n ");
+ builder.append(byteString(bytesArray.valueAt(i)));
+ }
+ return builder.toString();
+ }
+
+ private static String byteString(byte[] bytes) {
+ if (bytes == null) {
+ return "[null]";
+ } else {
+ final char[] hexArray = "0123456789ABCDEF".toCharArray();
+ char[] hexChars = new char[bytes.length * 2];
+ for (int i = 0; i < bytes.length; i++) {
+ int v = bytes[i] & 0xFF;
+ hexChars[i * 2] = hexArray[v >>> 4];
+ hexChars[i * 2 + 1] = hexArray[v & 0x0F];
+ }
+ return new String(hexChars);
+ }
+ }
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleSightingTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleSightingTest.java
new file mode 100644
index 0000000..d259851
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/ble/BleSightingTest.java
@@ -0,0 +1,155 @@
+/*
+ * 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.common.ble;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.os.Parcel;
+
+import org.junit.Test;
+
+import java.util.concurrent.TimeUnit;
+
+/** Test for Bluetooth LE {@link BleSighting}. */
+public class BleSightingTest {
+ private static final String DEVICE_NAME = "device1";
+ private static final String OTHER_DEVICE_NAME = "device2";
+ private static final long TIME_EPOCH_MILLIS = 123456;
+ private static final long OTHER_TIME_EPOCH_MILLIS = 456789;
+ private static final int RSSI = 1;
+ private static final int OTHER_RSSI = 2;
+
+ private final BluetoothDevice mBluetoothDevice1 =
+ BluetoothAdapter.getDefaultAdapter().getRemoteDevice("00:11:22:33:44:55");
+ private final BluetoothDevice mBluetoothDevice2 =
+ BluetoothAdapter.getDefaultAdapter().getRemoteDevice("AA:BB:CC:DD:EE:FF");
+
+
+ @Test
+ public void testEquals() {
+ BleSighting sighting =
+ buildBleSighting(mBluetoothDevice1, DEVICE_NAME, TIME_EPOCH_MILLIS, RSSI);
+ BleSighting sighting2 =
+ buildBleSighting(mBluetoothDevice1, DEVICE_NAME, TIME_EPOCH_MILLIS, RSSI);
+ assertThat(sighting.equals(sighting2)).isTrue();
+ assertThat(sighting2.equals(sighting)).isTrue();
+ assertThat(sighting.hashCode()).isEqualTo(sighting2.hashCode());
+
+ // Transitive property.
+ BleSighting sighting3 =
+ buildBleSighting(mBluetoothDevice1, DEVICE_NAME, TIME_EPOCH_MILLIS, RSSI);
+ assertThat(sighting2.equals(sighting3)).isTrue();
+ assertThat(sighting.equals(sighting3)).isTrue();
+
+ // Set different values for each field, one at a time.
+ sighting2 = buildBleSighting(mBluetoothDevice2, DEVICE_NAME, TIME_EPOCH_MILLIS, RSSI);
+ assertSightingsNotEquals(sighting, sighting2);
+
+ sighting2 = buildBleSighting(mBluetoothDevice1, OTHER_DEVICE_NAME, TIME_EPOCH_MILLIS, RSSI);
+ assertSightingsNotEquals(sighting, sighting2);
+
+ sighting2 = buildBleSighting(mBluetoothDevice1, DEVICE_NAME, OTHER_TIME_EPOCH_MILLIS, RSSI);
+ assertSightingsNotEquals(sighting, sighting2);
+
+ sighting2 = buildBleSighting(mBluetoothDevice1, DEVICE_NAME, TIME_EPOCH_MILLIS, OTHER_RSSI);
+ assertSightingsNotEquals(sighting, sighting2);
+ }
+
+ @Test
+ public void getNormalizedRSSI_usingNearbyRssiOffset_getCorrectValue() {
+ BleSighting sighting =
+ buildBleSighting(mBluetoothDevice1, DEVICE_NAME, TIME_EPOCH_MILLIS, RSSI);
+
+ int defaultRssiOffset = 3;
+ assertThat(sighting.getNormalizedRSSI()).isEqualTo(RSSI + defaultRssiOffset);
+ }
+
+ @Test
+ public void testFields() {
+ BleSighting sighting =
+ buildBleSighting(mBluetoothDevice1, DEVICE_NAME, TIME_EPOCH_MILLIS, RSSI);
+ assertThat(byteString(sighting.getBleRecordBytes()))
+ .isEqualTo("080964657669636531");
+ assertThat(sighting.getRssi()).isEqualTo(RSSI);
+ assertThat(sighting.getTimestampMillis()).isEqualTo(TIME_EPOCH_MILLIS);
+ assertThat(sighting.getTimestampNanos())
+ .isEqualTo(TimeUnit.MILLISECONDS.toNanos(TIME_EPOCH_MILLIS));
+ assertThat(sighting.toString()).isEqualTo(
+ "BleSighting{device=00:11:22:33:44:55,"
+ + " bleRecord=BleRecord [advertiseFlags=-1,"
+ + " serviceUuids=[],"
+ + " manufacturerSpecificData={}, serviceData={},"
+ + " txPowerLevel=-2147483648,"
+ + " deviceName=device1],"
+ + " rssi=1,"
+ + " timestampNanos=123456000000}");
+ }
+
+ @Test
+ public void testParcelable() {
+ BleSighting sighting =
+ buildBleSighting(mBluetoothDevice1, DEVICE_NAME, TIME_EPOCH_MILLIS, RSSI);
+ Parcel dest = Parcel.obtain();
+ sighting.writeToParcel(dest, 0);
+ dest.setDataPosition(0);
+ assertThat(sighting.getRssi()).isEqualTo(RSSI);
+ }
+
+ @Test
+ public void testCreatorNewArray() {
+ BleSighting[] sightings =
+ BleSighting.CREATOR.newArray(2);
+ assertThat(sightings.length).isEqualTo(2);
+ }
+
+ private static String byteString(byte[] bytes) {
+ if (bytes == null) {
+ return "[null]";
+ } else {
+ final char[] hexArray = "0123456789ABCDEF".toCharArray();
+ char[] hexChars = new char[bytes.length * 2];
+ for (int i = 0; i < bytes.length; i++) {
+ int v = bytes[i] & 0xFF;
+ hexChars[i * 2] = hexArray[v >>> 4];
+ hexChars[i * 2 + 1] = hexArray[v & 0x0F];
+ }
+ return new String(hexChars);
+ }
+ }
+
+ /** Builds a BleSighting instance which will correctly match filters by device name. */
+ private static BleSighting buildBleSighting(
+ BluetoothDevice bluetoothDevice, String deviceName, long timeEpochMillis, int rssi) {
+ byte[] nameBytes = deviceName.getBytes(UTF_8);
+ byte[] bleRecordBytes = new byte[nameBytes.length + 2];
+ bleRecordBytes[0] = (byte) (nameBytes.length + 1);
+ bleRecordBytes[1] = 0x09; // Value of private BleRecord.DATA_TYPE_LOCAL_NAME_COMPLETE;
+ System.arraycopy(nameBytes, 0, bleRecordBytes, 2, nameBytes.length);
+
+ return new BleSighting(bluetoothDevice, bleRecordBytes,
+ rssi, TimeUnit.MILLISECONDS.toNanos(timeEpochMillis));
+ }
+
+ private static void assertSightingsNotEquals(BleSighting sighting1, BleSighting sighting2) {
+ assertThat(sighting1.equals(sighting2)).isFalse();
+ assertThat(sighting1.hashCode()).isNotEqualTo(sighting2.hashCode());
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/ble/decode/BeaconDecoderTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/ble/decode/BeaconDecoderTest.java
new file mode 100644
index 0000000..9a9181d
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/ble/decode/BeaconDecoderTest.java
@@ -0,0 +1,37 @@
+/*
+ * 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.common.ble.decode;
+
+import static com.android.server.nearby.common.ble.BleRecord.parseFromBytes;
+import static com.android.server.nearby.common.ble.testing.FastPairTestData.getFastPairRecord;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class BeaconDecoderTest {
+ private BeaconDecoder mBeaconDecoder = new FastPairDecoder();;
+
+ @Test
+ public void testFields() {
+ assertThat(mBeaconDecoder.getTelemetry(parseFromBytes(getFastPairRecord()))).isNull();
+ assertThat(mBeaconDecoder.getUrl(parseFromBytes(getFastPairRecord()))).isNull();
+ assertThat(mBeaconDecoder.supportsBeaconIdAndTxPower(parseFromBytes(getFastPairRecord())))
+ .isTrue();
+ assertThat(mBeaconDecoder.supportsTxPower()).isTrue();
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/ble/decode/FastPairDecoderTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/ble/decode/FastPairDecoderTest.java
index 1ad04f8..6552699 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/common/ble/decode/FastPairDecoderTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/ble/decode/FastPairDecoderTest.java
@@ -17,15 +17,23 @@
package com.android.server.nearby.common.ble.decode;
import static com.android.server.nearby.common.ble.BleRecord.parseFromBytes;
+import static com.android.server.nearby.common.ble.testing.FastPairTestData.DEVICE_ADDRESS;
import static com.android.server.nearby.common.ble.testing.FastPairTestData.FAST_PAIR_MODEL_ID;
+import static com.android.server.nearby.common.ble.testing.FastPairTestData.FAST_PAIR_SHARED_ACCOUNT_KEY_RECORD;
+import static com.android.server.nearby.common.ble.testing.FastPairTestData.RSSI;
import static com.android.server.nearby.common.ble.testing.FastPairTestData.getFastPairRecord;
import static com.android.server.nearby.common.ble.testing.FastPairTestData.newFastPairRecord;
import static com.google.common.truth.Truth.assertThat;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.android.server.nearby.common.ble.BleRecord;
+import com.android.server.nearby.common.ble.BleSighting;
+import com.android.server.nearby.common.ble.testing.FastPairTestData;
import com.android.server.nearby.util.Hex;
import com.google.common.primitives.Bytes;
@@ -34,12 +42,13 @@
import org.junit.runner.RunWith;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
+import java.util.concurrent.TimeUnit;
@RunWith(AndroidJUnit4.class)
public class FastPairDecoderTest {
private static final String LONG_MODEL_ID = "1122334455667788";
- private final FastPairDecoder mDecoder = new FastPairDecoder();
// Bits 3-6 are model ID length bits = 0b1000 = 8
private static final byte LONG_MODEL_ID_HEADER = 0b00010000;
private static final String PADDED_LONG_MODEL_ID = "00001111";
@@ -49,74 +58,239 @@
private static final byte MODEL_ID_HEADER = 0b00000110;
private static final String MODEL_ID = "112233";
private static final byte BLOOM_FILTER_HEADER = 0b01100000;
+ private static final byte BLOOM_FILTER_NO_NOTIFICATION_HEADER = 0b01100010;
private static final String BLOOM_FILTER = "112233445566";
+ private static final byte LONG_BLOOM_FILTER_HEADER = (byte) 0b10100000;
+ private static final String LONG_BLOOM_FILTER = "00112233445566778899";
private static final byte BLOOM_FILTER_SALT_HEADER = 0b00010001;
private static final String BLOOM_FILTER_SALT = "01";
+ private static final byte BATTERY_HEADER = 0b00110011;
+ private static final byte BATTERY_NO_NOTIFICATION_HEADER = 0b00110100;
+ private static final String BATTERY = "01048F";
private static final byte RANDOM_RESOLVABLE_DATA_HEADER = 0b01000110;
private static final String RANDOM_RESOLVABLE_DATA = "11223344";
- private static final byte BLOOM_FILTER_NO_NOTIFICATION_HEADER = 0b01100010;
+ private final FastPairDecoder mDecoder = new FastPairDecoder();
+ private final BluetoothDevice mBluetoothDevice =
+ BluetoothAdapter.getDefaultAdapter().getRemoteDevice(DEVICE_ADDRESS);
+
+ @Test
+ public void filter() {
+ assertThat(FastPairDecoder.FILTER.matches(bleSighting(getFastPairRecord()))).isTrue();
+ assertThat(FastPairDecoder.FILTER.matches(bleSighting(FAST_PAIR_SHARED_ACCOUNT_KEY_RECORD)))
+ .isTrue();
+
+ // Any ID is a valid frame.
+ assertThat(FastPairDecoder.FILTER.matches(
+ bleSighting(newFastPairRecord(Hex.stringToBytes("000001"))))).isTrue();
+ assertThat(FastPairDecoder.FILTER.matches(
+ bleSighting(newFastPairRecord(Hex.stringToBytes("098FEC"))))).isTrue();
+ assertThat(FastPairDecoder.FILTER.matches(
+ bleSighting(FastPairTestData.newFastPairRecord(
+ LONG_MODEL_ID_HEADER, Hex.stringToBytes(LONG_MODEL_ID))))).isTrue();
+ }
@Test
public void getModelId() {
assertThat(mDecoder.getBeaconIdBytes(parseFromBytes(getFastPairRecord())))
.isEqualTo(FAST_PAIR_MODEL_ID);
- FastPairServiceData fastPairServiceData1 =
- new FastPairServiceData(LONG_MODEL_ID_HEADER,
- LONG_MODEL_ID);
- assertThat(
- mDecoder.getBeaconIdBytes(
- newBleRecord(fastPairServiceData1.createServiceData())))
- .isEqualTo(Hex.stringToBytes(LONG_MODEL_ID));
FastPairServiceData fastPairServiceData =
- new FastPairServiceData(PADDED_LONG_MODEL_ID_HEADER,
- PADDED_LONG_MODEL_ID);
+ new FastPairServiceData(LONG_MODEL_ID_HEADER, LONG_MODEL_ID);
assertThat(
mDecoder.getBeaconIdBytes(
newBleRecord(fastPairServiceData.createServiceData())))
+ .isEqualTo(Hex.stringToBytes(LONG_MODEL_ID));
+
+ FastPairServiceData fastPairServiceData1 =
+ new FastPairServiceData(PADDED_LONG_MODEL_ID_HEADER, PADDED_LONG_MODEL_ID);
+ assertThat(
+ mDecoder.getBeaconIdBytes(
+ newBleRecord(fastPairServiceData1.createServiceData())))
.isEqualTo(Hex.stringToBytes(TRIMMED_LONG_MODEL_ID));
}
@Test
- public void getBloomFilter() {
- FastPairServiceData fastPairServiceData = new FastPairServiceData(MODEL_ID_HEADER,
- MODEL_ID);
- fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
- fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
- assertThat(FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
- .isEqualTo(Hex.stringToBytes(BLOOM_FILTER));
+ public void getBeaconIdType() {
+ assertThat(mDecoder.getBeaconIdType()).isEqualTo(1);
}
@Test
- public void getBloomFilter_smallModelId() {
- FastPairServiceData fastPairServiceData = new FastPairServiceData(null, MODEL_ID);
- assertThat(FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
+ public void getCalibratedBeaconTxPower() {
+ FastPairServiceData fastPairServiceData =
+ new FastPairServiceData(LONG_MODEL_ID_HEADER, LONG_MODEL_ID);
+ assertThat(
+ mDecoder.getCalibratedBeaconTxPower(
+ newBleRecord(fastPairServiceData.createServiceData())))
.isNull();
}
@Test
- public void getBloomFilterSalt_modelIdAndMultipleExtraFields() {
- FastPairServiceData fastPairServiceData = new FastPairServiceData(MODEL_ID_HEADER,
- MODEL_ID);
- fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
- fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_SALT_HEADER);
- fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
- fastPairServiceData.mExtraFields.add(BLOOM_FILTER_SALT);
+ public void getServiceDataArray() {
+ FastPairServiceData fastPairServiceData =
+ new FastPairServiceData(LONG_MODEL_ID_HEADER, LONG_MODEL_ID);
assertThat(
- FastPairDecoder.getBloomFilterSalt(fastPairServiceData.createServiceData()))
- .isEqualTo(Hex.stringToBytes(BLOOM_FILTER_SALT));
+ mDecoder.getServiceDataArray(
+ newBleRecord(fastPairServiceData.createServiceData())))
+ .isEqualTo(Hex.stringToBytes("101122334455667788"));
}
@Test
- public void getRandomResolvableData_whenContainConnectionState() {
- FastPairServiceData fastPairServiceData = new FastPairServiceData(MODEL_ID_HEADER,
- MODEL_ID);
- fastPairServiceData.mExtraFieldHeaders.add(RANDOM_RESOLVABLE_DATA_HEADER);
- fastPairServiceData.mExtraFields.add(RANDOM_RESOLVABLE_DATA);
+ public void hasBloomFilter() {
+ FastPairServiceData fastPairServiceData =
+ new FastPairServiceData(LONG_MODEL_ID_HEADER, LONG_MODEL_ID);
assertThat(
- FastPairDecoder.getRandomResolvableData(fastPairServiceData
- .createServiceData()))
- .isEqualTo(Hex.stringToBytes(RANDOM_RESOLVABLE_DATA));
+ mDecoder.hasBloomFilter(
+ newBleRecord(fastPairServiceData.createServiceData())))
+ .isFalse();
+ }
+
+ @Test
+ public void hasModelId_allCases() {
+ // One type of the format is just the 3-byte model ID. This format has no header byte (all 3
+ // service data bytes are the model ID in little endian).
+ assertThat(hasModelId("112233", mDecoder)).isTrue();
+
+ // If the model ID is shorter than 3 bytes, then return null.
+ assertThat(hasModelId("11", mDecoder)).isFalse();
+
+ // If the data is longer than 3 bytes,
+ // byte 0 must be 0bVVVLLLLR (version, ID length, reserved).
+ FastPairServiceData fastPairServiceData =
+ new FastPairServiceData((byte) 0b00001000, "11223344");
+ assertThat(
+ FastPairDecoder.hasBeaconIdBytes(
+ newBleRecord(fastPairServiceData.createServiceData()))).isTrue();
+
+ FastPairServiceData fastPairServiceData1 =
+ new FastPairServiceData((byte) 0b00001010, "1122334455");
+ assertThat(
+ FastPairDecoder.hasBeaconIdBytes(
+ newBleRecord(fastPairServiceData1.createServiceData()))).isTrue();
+
+ // Length bits correct, but version bits != version 0 (only supported version).
+ FastPairServiceData fastPairServiceData2 =
+ new FastPairServiceData((byte) 0b00101000, "11223344");
+ assertThat(
+ FastPairDecoder.hasBeaconIdBytes(
+ newBleRecord(fastPairServiceData2.createServiceData()))).isFalse();
+
+ // Version bits correct, but length bits incorrect (too big, too small).
+ FastPairServiceData fastPairServiceData3 =
+ new FastPairServiceData((byte) 0b00001010, "11223344");
+ assertThat(
+ FastPairDecoder.hasBeaconIdBytes(
+ newBleRecord(fastPairServiceData3.createServiceData()))).isFalse();
+
+ FastPairServiceData fastPairServiceData4 =
+ new FastPairServiceData((byte) 0b00000010, "11223344");
+ assertThat(
+ FastPairDecoder.hasBeaconIdBytes(
+ newBleRecord(fastPairServiceData4.createServiceData()))).isFalse();
+ }
+
+ @Test
+ public void getBatteryLevel() {
+ FastPairServiceData fastPairServiceData =
+ new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+ fastPairServiceData.mExtraFieldHeaders.add(BATTERY_HEADER);
+ fastPairServiceData.mExtraFields.add(BATTERY);
+ assertThat(
+ FastPairDecoder.getBatteryLevel(fastPairServiceData.createServiceData()))
+ .isEqualTo(Hex.stringToBytes(BATTERY));
+ }
+
+ @Test
+ public void getBatteryLevel_notIncludedInPacket() {
+ FastPairServiceData fastPairServiceData =
+ new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+ fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
+ fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
+ assertThat(
+ FastPairDecoder.getBatteryLevel(fastPairServiceData.createServiceData())).isNull();
+ }
+
+ @Test
+ public void getBatteryLevel_noModelId() {
+ FastPairServiceData fastPairServiceData =
+ new FastPairServiceData((byte) 0b00000000, null);
+ fastPairServiceData.mExtraFieldHeaders.add(BATTERY_HEADER);
+ fastPairServiceData.mExtraFields.add(BATTERY);
+ assertThat(
+ FastPairDecoder.getBatteryLevel(fastPairServiceData.createServiceData()))
+ .isEqualTo(Hex.stringToBytes(BATTERY));
+ }
+
+ @Test
+ public void getBatteryLevel_multipelExtraField() {
+ FastPairServiceData fastPairServiceData =
+ new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+ fastPairServiceData.mExtraFieldHeaders.add(BATTERY_HEADER);
+ fastPairServiceData.mExtraFields.add(BATTERY);
+ fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
+ fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
+ assertThat(
+ FastPairDecoder.getBatteryLevel(fastPairServiceData.createServiceData()))
+ .isEqualTo(Hex.stringToBytes(BATTERY));
+ }
+
+ @Test
+ public void getBatteryLevelNoNotification() {
+ FastPairServiceData fastPairServiceData =
+ new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+ fastPairServiceData.mExtraFieldHeaders.add(BATTERY_NO_NOTIFICATION_HEADER);
+ fastPairServiceData.mExtraFields.add(BATTERY);
+ assertThat(
+ FastPairDecoder.getBatteryLevelNoNotification(
+ fastPairServiceData.createServiceData()))
+ .isEqualTo(Hex.stringToBytes(BATTERY));
+ }
+
+ @Test
+ public void getBatteryLevelNoNotification_notIncludedInPacket() {
+ FastPairServiceData fastPairServiceData =
+ new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+ fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
+ fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
+ assertThat(
+ FastPairDecoder.getBatteryLevelNoNotification(
+ fastPairServiceData.createServiceData())).isNull();
+ }
+
+ @Test
+ public void getBatteryLevelNoNotification_noModelId() {
+ FastPairServiceData fastPairServiceData =
+ new FastPairServiceData((byte) 0b00000000, null);
+ fastPairServiceData.mExtraFieldHeaders.add(BATTERY_NO_NOTIFICATION_HEADER);
+ fastPairServiceData.mExtraFields.add(BATTERY);
+ assertThat(
+ FastPairDecoder.getBatteryLevelNoNotification(
+ fastPairServiceData.createServiceData()))
+ .isEqualTo(Hex.stringToBytes(BATTERY));
+ }
+
+ @Test
+ public void getBatteryLevelNoNotification_multipleExtraField() {
+ FastPairServiceData fastPairServiceData =
+ new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+ fastPairServiceData.mExtraFieldHeaders.add(BATTERY_NO_NOTIFICATION_HEADER);
+ fastPairServiceData.mExtraFields.add(BATTERY);
+ fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
+ fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
+ assertThat(
+ FastPairDecoder.getBatteryLevelNoNotification(
+ fastPairServiceData.createServiceData()))
+ .isEqualTo(Hex.stringToBytes(BATTERY));
+ }
+
+ @Test
+ public void getBloomFilter() {
+ FastPairServiceData fastPairServiceData =
+ new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+ fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
+ fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
+ assertThat(
+ FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
+ .isEqualTo(Hex.stringToBytes(BLOOM_FILTER));
}
@Test
@@ -125,14 +299,222 @@
new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_NO_NOTIFICATION_HEADER);
fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
- assertThat(FastPairDecoder.getBloomFilterNoNotification(fastPairServiceData
- .createServiceData())).isEqualTo(Hex.stringToBytes(BLOOM_FILTER));
+ assertThat(
+ FastPairDecoder.getBloomFilterNoNotification(
+ fastPairServiceData.createServiceData()))
+ .isEqualTo(Hex.stringToBytes(BLOOM_FILTER));
+ }
+
+ @Test
+ public void getBloomFilter_smallModelId() {
+ FastPairServiceData fastPairServiceData =
+ new FastPairServiceData(null, MODEL_ID);
+ assertThat(
+ FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
+ .isNull();
+ }
+
+ @Test
+ public void getBloomFilter_headerVersionBitsNotZero() {
+ // Header bits are defined as 0bVVVLLLLR (V=version, L=length, R=reserved), must be zero.
+ FastPairServiceData fastPairServiceData =
+ new FastPairServiceData((byte) 0b00100000, MODEL_ID);
+ fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
+ fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
+ assertThat(
+ FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
+ .isNull();
+ }
+
+ @Test
+ public void getBloomFilter_noExtraFieldBytesIncluded() {
+ FastPairServiceData fastPairServiceData =
+ new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+ fastPairServiceData.mExtraFieldHeaders.add(null);
+ fastPairServiceData.mExtraFields.add(null);
+ assertThat(
+ FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
+ .isNull();
+ }
+
+ @Test
+ public void getBloomFilter_extraFieldLengthIsZero() {
+ // The extra field header is formatted as 0bLLLLTTTT (L=length, T=type).
+ FastPairServiceData fastPairServiceData =
+ new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+ fastPairServiceData.mExtraFieldHeaders.add((byte) 0b00000000);
+ fastPairServiceData.mExtraFields.add(null);
+ assertThat(
+ FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
+ .hasLength(0);
+ }
+
+ @Test
+ public void getBloomFilter_extraFieldLengthLongerThanPacket() {
+ FastPairServiceData fastPairServiceData =
+ new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+ fastPairServiceData.mExtraFieldHeaders.add((byte) 0b11110000);
+ fastPairServiceData.mExtraFields.add("1122");
+ assertThat(
+ FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
+ .isNull();
+ }
+
+ @Test
+ public void getBloomFilter_secondExtraFieldLengthLongerThanPacket() {
+ FastPairServiceData fastPairServiceData =
+ new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+ fastPairServiceData.mExtraFieldHeaders.add((byte) 0b00100000);
+ fastPairServiceData.mExtraFields.add("1122");
+ fastPairServiceData.mExtraFieldHeaders.add((byte) 0b11110001);
+ fastPairServiceData.mExtraFields.add("3344");
+ assertThat(
+ FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData())).isNull();
+ }
+
+ @Test
+ public void getBloomFilter_typeIsWrong() {
+ FastPairServiceData fastPairServiceData =
+ new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+ fastPairServiceData.mExtraFieldHeaders.add((byte) 0b01100001);
+ fastPairServiceData.mExtraFields.add("112233445566");
+ assertThat(
+ FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
+ .isNull();
+ }
+
+ @Test
+ public void getBloomFilter_noModelId() {
+ FastPairServiceData fastPairServiceData =
+ new FastPairServiceData((byte) 0b00000000, null);
+ fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
+ fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
+ assertThat(
+ FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
+ .isEqualTo(Hex.stringToBytes(BLOOM_FILTER));
+ }
+
+ @Test
+ public void getBloomFilter_noModelIdAndMultipleExtraFields() {
+ FastPairServiceData fastPairServiceData =
+ new FastPairServiceData((byte) 0b00000000, null);
+ fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
+ fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
+ fastPairServiceData.mExtraFieldHeaders.add((byte) 0b00010001);
+ fastPairServiceData.mExtraFields.add("00");
+ assertThat(
+ FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
+ .isEqualTo(Hex.stringToBytes(BLOOM_FILTER));
+ }
+
+ @Test
+ public void getBloomFilter_modelIdAndMultipleExtraFields() {
+ FastPairServiceData fastPairServiceData =
+ new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+ fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
+ fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
+ fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_SALT_HEADER);
+ fastPairServiceData.mExtraFields.add(BLOOM_FILTER_SALT);
+ assertThat(
+ FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
+ .isEqualTo(Hex.stringToBytes(BLOOM_FILTER));
+ }
+
+ @Test
+ public void getBloomFilterSalt_modelIdAndMultipleExtraFields() {
+ FastPairServiceData fastPairServiceData =
+ new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+ fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
+ fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
+ fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_SALT_HEADER);
+ fastPairServiceData.mExtraFields.add(BLOOM_FILTER_SALT);
+ assertThat(
+ FastPairDecoder.getBloomFilterSalt(fastPairServiceData.createServiceData()))
+ .isEqualTo(Hex.stringToBytes(BLOOM_FILTER_SALT));
+ }
+
+ @Test
+ public void getBloomFilter_modelIdAndMultipleExtraFieldsWithBloomFilterLast() {
+ FastPairServiceData fastPairServiceData =
+ new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+ fastPairServiceData.mExtraFieldHeaders.add((byte) 0b00010001);
+ fastPairServiceData.mExtraFields.add("1A");
+ fastPairServiceData.mExtraFieldHeaders.add((byte) 0b00100010);
+ fastPairServiceData.mExtraFields.add("2CFE");
+ fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
+ fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
+ assertThat(
+ FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
+ .isEqualTo(Hex.stringToBytes(BLOOM_FILTER));
+ }
+
+ @Test
+ public void getBloomFilter_modelIdAndMultipleExtraFieldsWithSameType() {
+ FastPairServiceData fastPairServiceData =
+ new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+ fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
+ fastPairServiceData.mExtraFields.add(BLOOM_FILTER);
+ fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
+ fastPairServiceData.mExtraFields.add("000000000000");
+ assertThat(
+ FastPairDecoder.getBloomFilter(fastPairServiceData.createServiceData()))
+ .isEqualTo(Hex.stringToBytes(BLOOM_FILTER));
+ }
+
+ @Test
+ public void getBloomFilter_longExtraField() {
+ FastPairServiceData fastPairServiceData =
+ new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+ fastPairServiceData.mExtraFieldHeaders.add(LONG_BLOOM_FILTER_HEADER);
+ fastPairServiceData.mExtraFields.add(LONG_BLOOM_FILTER);
+ fastPairServiceData.mExtraFieldHeaders.add(BLOOM_FILTER_HEADER);
+ fastPairServiceData.mExtraFields.add("000000000000");
+ assertThat(
+ FastPairDecoder.getBloomFilter(
+ fastPairServiceData.createServiceData()))
+ .isEqualTo(Hex.stringToBytes(LONG_BLOOM_FILTER));
+ }
+
+ @Test
+ public void getRandomResolvableData_whenNoConnectionState() {
+ FastPairServiceData fastPairServiceData =
+ new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+ assertThat(
+ FastPairDecoder.getRandomResolvableData(
+ fastPairServiceData.createServiceData()))
+ .isEqualTo(null);
+ }
+
+ @Test
+ public void getRandomResolvableData_whenContainConnectionState() {
+ FastPairServiceData fastPairServiceData =
+ new FastPairServiceData(MODEL_ID_HEADER, MODEL_ID);
+ fastPairServiceData.mExtraFieldHeaders.add(RANDOM_RESOLVABLE_DATA_HEADER);
+ fastPairServiceData.mExtraFields.add(RANDOM_RESOLVABLE_DATA);
+ assertThat(
+ FastPairDecoder.getRandomResolvableData(
+ fastPairServiceData.createServiceData()))
+ .isEqualTo(Hex.stringToBytes(RANDOM_RESOLVABLE_DATA));
}
private static BleRecord newBleRecord(byte[] serviceDataBytes) {
return parseFromBytes(newFastPairRecord(serviceDataBytes));
}
- class FastPairServiceData {
+
+ private static boolean hasModelId(String modelId, FastPairDecoder decoder) {
+ byte[] modelIdBytes = Hex.stringToBytes(modelId);
+ BleRecord bleRecord =
+ parseFromBytes(FastPairTestData.newFastPairRecord((byte) 0, modelIdBytes));
+ return FastPairDecoder.hasBeaconIdBytes(bleRecord)
+ && Arrays.equals(decoder.getBeaconIdBytes(bleRecord), modelIdBytes);
+ }
+
+ private BleSighting bleSighting(byte[] frame) {
+ return new BleSighting(mBluetoothDevice, frame, RSSI,
+ TimeUnit.MILLISECONDS.toNanos(System.currentTimeMillis()));
+ }
+
+ static class FastPairServiceData {
private Byte mHeader;
private String mModelId;
List<Byte> mExtraFieldHeaders = new ArrayList<>();
@@ -164,6 +546,4 @@
return serviceData;
}
}
-
-
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/ble/util/RangingUtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/ble/util/RangingUtilsTest.java
index ebe72b3..29f8d4e 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/common/ble/util/RangingUtilsTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/ble/util/RangingUtilsTest.java
@@ -48,8 +48,10 @@
// RSSI expected at 1 meter based on the calibrated tx field of -41dBm
// Using params: distance (m), int calibratedTxPower (dBm),
int rssi = RangingUtils.rssiFromTargetDistance(1.0, -41);
+ int rssi1 = RangingUtils.rssiFromTargetDistance(0.0, -41);
assertThat(rssi).isEqualTo(-82);
+ assertThat(rssi1).isEqualTo(-41);
}
@Test
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/ble/util/StringUtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/ble/util/StringUtilsTest.java
new file mode 100644
index 0000000..6b20fdd
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/ble/util/StringUtilsTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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.common.ble.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.os.ParcelUuid;
+import android.util.SparseArray;
+
+import com.android.server.nearby.common.ble.BleRecord;
+
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class StringUtilsTest {
+ // iBeacon (Apple) Packet 1
+ private static final byte[] BEACON = {
+ // Flags
+ (byte) 0x02,
+ (byte) 0x01,
+ (byte) 0x06,
+ // Manufacturer-specific data header
+ (byte) 0x1a,
+ (byte) 0xff,
+ (byte) 0x4c,
+ (byte) 0x00,
+ // iBeacon Type
+ (byte) 0x02,
+ // Frame length
+ (byte) 0x15,
+ // iBeacon Proximity UUID
+ (byte) 0xf7,
+ (byte) 0x82,
+ (byte) 0x6d,
+ (byte) 0xa6,
+ (byte) 0x4f,
+ (byte) 0xa2,
+ (byte) 0x4e,
+ (byte) 0x98,
+ (byte) 0x80,
+ (byte) 0x24,
+ (byte) 0xbc,
+ (byte) 0x5b,
+ (byte) 0x71,
+ (byte) 0xe0,
+ (byte) 0x89,
+ (byte) 0x3e,
+ // iBeacon Instance ID (Major/Minor)
+ (byte) 0x44,
+ (byte) 0xd0,
+ (byte) 0x25,
+ (byte) 0x22,
+ // Tx Power
+ (byte) 0xb3,
+ // RSP
+ (byte) 0x08,
+ (byte) 0x09,
+ (byte) 0x4b,
+ (byte) 0x6f,
+ (byte) 0x6e,
+ (byte) 0x74,
+ (byte) 0x61,
+ (byte) 0x6b,
+ (byte) 0x74,
+ (byte) 0x02,
+ (byte) 0x0a,
+ (byte) 0xf4,
+ (byte) 0x0a,
+ (byte) 0x16,
+ (byte) 0x0d,
+ (byte) 0xd0,
+ (byte) 0x74,
+ (byte) 0x6d,
+ (byte) 0x4d,
+ (byte) 0x6b,
+ (byte) 0x32,
+ (byte) 0x36,
+ (byte) 0x64,
+ (byte) 0x00,
+ (byte) 0x00,
+ (byte) 0x00,
+ (byte) 0x00,
+ (byte) 0x00,
+ (byte) 0x00,
+ (byte) 0x00,
+ (byte) 0x00,
+ (byte) 0x00
+ };
+
+ @Test
+ public void testToString() {
+ BleRecord record = BleRecord.parseFromBytes(BEACON);
+ assertThat(StringUtils.toString((SparseArray<byte[]>) null)).isEqualTo("null");
+ assertThat(StringUtils.toString(new SparseArray<byte[]>())).isEqualTo("{}");
+ assertThat(StringUtils
+ .toString((Map<ParcelUuid, byte[]>) null)).isEqualTo("null");
+ assertThat(StringUtils
+ .toString(new HashMap<ParcelUuid, byte[]>())).isEqualTo("{}");
+ assertThat(StringUtils.toString(record.getManufacturerSpecificData()))
+ .isEqualTo("{76=[2, 21, -9, -126, 109, -90, 79, -94, 78, -104, -128,"
+ + " 36, -68, 91, 113, -32, -119, 62, 68, -48, 37, 34, -77]}");
+ assertThat(StringUtils.toString(record.getServiceData()))
+ .isEqualTo("{0000d00d-0000-1000-8000-00805f9b34fb="
+ + "[116, 109, 77, 107, 50, 54, 100]}");
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bloomfilter/BloomFilterTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bloomfilter/BloomFilterTest.java
new file mode 100644
index 0000000..30df81f
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bloomfilter/BloomFilterTest.java
@@ -0,0 +1,307 @@
+/*
+ * 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.common.bloomfilter;
+
+import static com.google.common.io.BaseEncoding.base16;
+import static com.google.common.primitives.Bytes.concat;
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+
+/**
+ * Unit-tests for the {@link BloomFilter} class.
+ */
+public class BloomFilterTest {
+ private static final int BYTE_ARRAY_LENGTH = 100;
+
+ private final BloomFilter mBloomFilter =
+ new BloomFilter(new byte[BYTE_ARRAY_LENGTH], newHasher());
+
+ public BloomFilter.Hasher newHasher() {
+ return new FastPairBloomFilterHasher();
+ }
+
+ @Test
+ public void emptyFilter_returnsEmptyArray() throws Exception {
+ assertThat(mBloomFilter.asBytes()).isEqualTo(new byte[BYTE_ARRAY_LENGTH]);
+ }
+
+ @Test
+ public void emptyFilter_neverContains() throws Exception {
+ assertThat(mBloomFilter.possiblyContains(element(1))).isFalse();
+ assertThat(mBloomFilter.possiblyContains(element(2))).isFalse();
+ assertThat(mBloomFilter.possiblyContains(element(3))).isFalse();
+ }
+
+ @Test
+ public void add() throws Exception {
+ assertThat(mBloomFilter.possiblyContains(element(1))).isFalse();
+ mBloomFilter.add(element(1));
+ assertThat(mBloomFilter.possiblyContains(element(1))).isTrue();
+ }
+
+ @Test
+ public void add_onlyGivenArgAdded() throws Exception {
+ mBloomFilter.add(element(1));
+ assertThat(mBloomFilter.possiblyContains(element(1))).isTrue();
+ assertThat(mBloomFilter.possiblyContains(element(2))).isFalse();
+ assertThat(mBloomFilter.possiblyContains(element(3))).isFalse();
+ }
+
+ @Test
+ public void add_multipleArgs() throws Exception {
+ mBloomFilter.add(element(1));
+ mBloomFilter.add(element(2));
+ assertThat(mBloomFilter.possiblyContains(element(1))).isTrue();
+ assertThat(mBloomFilter.possiblyContains(element(2))).isTrue();
+ assertThat(mBloomFilter.possiblyContains(element(3))).isFalse();
+ }
+
+ /**
+ * This test was added because of a bug where the BloomFilter doesn't utilize all bits given.
+ * Functionally, the filter still works, but we just have a much higher false positive rate. The
+ * bug was caused by confusing bit length and byte length, which made our BloomFilter only set
+ * bits on the first byteLength (bitLength / 8) bits rather than the whole bitLength bits.
+ *
+ * <p>Here, we're verifying that the bits set are somewhat scattered. So instead of something
+ * like [ 0, 1, 1, 0, 0, 0, 0, ..., 0 ], we should be getting something like
+ * [ 0, 1, 0, 0, 1, 1, 0, 0,0, 1, ..., 1, 0].
+ */
+ @Test
+ public void randomness_noEndBias() throws Exception {
+ // Add one element to our BloomFilter.
+ mBloomFilter.add(element(1));
+
+ // Record the amount of non-zero bytes and the longest streak of zero bytes in the resulting
+ // BloomFilter. This is an approximation of reasonable distribution since we're recording by
+ // bytes instead of bits.
+ int nonZeroCount = 0;
+ int longestZeroStreak = 0;
+ int currentZeroStreak = 0;
+ for (byte b : mBloomFilter.asBytes()) {
+ if (b == 0) {
+ currentZeroStreak++;
+ } else {
+ // Increment the number of non-zero bytes we've seen, update the longest zero
+ // streak, and then reset the current zero streak.
+ nonZeroCount++;
+ longestZeroStreak = Math.max(longestZeroStreak, currentZeroStreak);
+ currentZeroStreak = 0;
+ }
+ }
+ // Update the longest zero streak again for the tail case.
+ longestZeroStreak = Math.max(longestZeroStreak, currentZeroStreak);
+
+ // Since randomness is hard to measure within one unit test, we instead do a valid check.
+ // All non-zero bytes should not be packed into one end of the array.
+ //
+ // In this case, the size of one end is approximated to be:
+ // BYTE_ARRAY_LENGTH / nonZeroCount.
+ // Therefore, the longest zero streak should be less than:
+ // BYTE_ARRAY_LENGTH - one end of the array.
+ int longestAcceptableZeroStreak = BYTE_ARRAY_LENGTH - (BYTE_ARRAY_LENGTH / nonZeroCount);
+ assertThat(longestZeroStreak).isAtMost(longestAcceptableZeroStreak);
+ }
+
+ @Test
+ public void randomness_falsePositiveRate() throws Exception {
+ // Create a new BloomFilter with a length of only 10 bytes.
+ BloomFilter bloomFilter = new BloomFilter(new byte[10], newHasher());
+
+ // Add 5 distinct elements to the BloomFilter.
+ for (int i = 0; i < 5; i++) {
+ bloomFilter.add(element(i));
+ }
+
+ // Now test 100 other elements and record the number of false positives.
+ int falsePositives = 0;
+ for (int i = 5; i < 105; i++) {
+ falsePositives += bloomFilter.possiblyContains(element(i)) ? 1 : 0;
+ }
+
+ // We expect the false positive rate to be 3% with 5 elements in a 10 byte filter. Thus,
+ // we give a little leeway and verify that the false positive rate is no more than 5%.
+ assertWithMessage(
+ String.format(
+ "False positive rate too large. Expected <= 5%%, but got %d%%.",
+ falsePositives))
+ .that(falsePositives <= 5)
+ .isTrue();
+ System.out.printf("False positive rate: %d%%%n", falsePositives);
+ }
+
+
+ private String element(int index) {
+ return "ELEMENT_" + index;
+ }
+
+ @Test
+ public void specificBitPattern() throws Exception {
+ // Create a new BloomFilter along with a fixed set of elements
+ // and bit patterns to verify with.
+ BloomFilter bloomFilter = new BloomFilter(new byte[6], newHasher());
+ // Combine an account key and mac address.
+ byte[] element =
+ concat(
+ base16().decode("11223344556677889900AABBCCDDEEFF"),
+ base16().withSeparator(":", 2).decode("84:68:3E:00:02:11"));
+ byte[] expectedBitPattern = new byte[] {0x50, 0x00, 0x04, 0x15, 0x08, 0x01};
+
+ // Add the fixed elements to the filter.
+ bloomFilter.add(element);
+
+ // Verify that the resulting bytes match the expected one.
+ byte[] bloomFilterBytes = bloomFilter.asBytes();
+ assertWithMessage(
+ "Unexpected bit pattern. Expected %s, but got %s.",
+ base16().encode(expectedBitPattern), base16().encode(bloomFilterBytes))
+ .that(Arrays.equals(expectedBitPattern, bloomFilterBytes))
+ .isTrue();
+
+ // Verify that the expected bit pattern creates a BloomFilter containing all fixed elements.
+ bloomFilter = new BloomFilter(expectedBitPattern, newHasher());
+ assertThat(bloomFilter.possiblyContains(element)).isTrue();
+ }
+
+ // This test case has been on the spec,
+ // https://devsite.googleplex.com/nearby/fast-pair/spec#test_cases.
+ // Explicitly adds it here, and we can easily change the parameters (e.g. account key, ble
+ // address) to clarify test results with partners.
+ @Test
+ public void specificBitPattern_hasOneAccountKey() {
+ BloomFilter bloomFilter1 = new BloomFilter(new byte[4], newHasher());
+ BloomFilter bloomFilter2 = new BloomFilter(new byte[4], newHasher());
+ byte[] accountKey = base16().decode("11223344556677889900AABBCCDDEEFF");
+ byte[] salt1 = base16().decode("C7");
+ byte[] salt2 = base16().decode("C7C8");
+
+ // Add the fixed elements to the filter.
+ bloomFilter1.add(concat(accountKey, salt1));
+ bloomFilter2.add(concat(accountKey, salt2));
+
+ assertThat(bloomFilter1.asBytes()).isEqualTo(base16().decode("0A428810"));
+ assertThat(bloomFilter2.asBytes()).isEqualTo(base16().decode("020C802A"));
+ }
+
+ // Adds this test case to spec. We can easily change the parameters (e.g. account key, ble
+ // address, battery data) to clarify test results with partners.
+ @Test
+ public void specificBitPattern_hasOneAccountKey_withBatteryData() {
+ BloomFilter bloomFilter1 = new BloomFilter(new byte[4], newHasher());
+ BloomFilter bloomFilter2 = new BloomFilter(new byte[4], newHasher());
+ byte[] accountKey = base16().decode("11223344556677889900AABBCCDDEEFF");
+ byte[] salt1 = base16().decode("C7");
+ byte[] salt2 = base16().decode("C7C8");
+ byte[] batteryData = {
+ 0b00110011, // length = 3, show UI indication.
+ 0b01000000, // Left bud: not charging, battery level = 64.
+ 0b01000000, // Right bud: not charging, battery level = 64.
+ 0b01000000 // Case: not charging, battery level = 64.
+ };
+
+ // Adds battery data to build bloom filter.
+ bloomFilter1.add(concat(accountKey, salt1, batteryData));
+ bloomFilter2.add(concat(accountKey, salt2, batteryData));
+
+ assertThat(bloomFilter1.asBytes()).isEqualTo(base16().decode("4A00F000"));
+ assertThat(bloomFilter2.asBytes()).isEqualTo(base16().decode("0101460A"));
+ }
+
+ // This test case has been on the spec,
+ // https://devsite.googleplex.com/nearby/fast-pair/spec#test_cases.
+ // Explicitly adds it here, and we can easily change the parameters (e.g. account key, ble
+ // address) to clarify test results with partners.
+ @Test
+ public void specificBitPattern_hasTwoAccountKeys() {
+ BloomFilter bloomFilter1 = new BloomFilter(new byte[5], newHasher());
+ BloomFilter bloomFilter2 = new BloomFilter(new byte[5], newHasher());
+ byte[] accountKey1 = base16().decode("11223344556677889900AABBCCDDEEFF");
+ byte[] accountKey2 = base16().decode("11112222333344445555666677778888");
+ byte[] salt1 = base16().decode("C7");
+ byte[] salt2 = base16().decode("C7C8");
+
+ // Adds the fixed elements to the filter.
+ bloomFilter1.add(concat(accountKey1, salt1));
+ bloomFilter1.add(concat(accountKey2, salt1));
+ bloomFilter2.add(concat(accountKey1, salt2));
+ bloomFilter2.add(concat(accountKey2, salt2));
+
+ assertThat(bloomFilter1.asBytes()).isEqualTo(base16().decode("2FBA064200"));
+ assertThat(bloomFilter2.asBytes()).isEqualTo(base16().decode("844A62208B"));
+ }
+
+ // Adds this test case to spec. We can easily change the parameters (e.g. account keys, ble
+ // address, battery data) to clarify test results with partners.
+ @Test
+ public void specificBitPattern_hasTwoAccountKeys_withBatteryData() {
+ BloomFilter bloomFilter1 = new BloomFilter(new byte[5], newHasher());
+ BloomFilter bloomFilter2 = new BloomFilter(new byte[5], newHasher());
+ byte[] accountKey1 = base16().decode("11223344556677889900AABBCCDDEEFF");
+ byte[] accountKey2 = base16().decode("11112222333344445555666677778888");
+ byte[] salt1 = base16().decode("C7");
+ byte[] salt2 = base16().decode("C7C8");
+ byte[] batteryData = {
+ 0b00110011, // length = 3, show UI indication.
+ 0b01000000, // Left bud: not charging, battery level = 64.
+ 0b01000000, // Right bud: not charging, battery level = 64.
+ 0b01000000 // Case: not charging, battery level = 64.
+ };
+
+ // Adds battery data to build bloom filter.
+ bloomFilter1.add(concat(accountKey1, salt1, batteryData));
+ bloomFilter1.add(concat(accountKey2, salt1, batteryData));
+ bloomFilter2.add(concat(accountKey1, salt2, batteryData));
+ bloomFilter2.add(concat(accountKey2, salt2, batteryData));
+
+ assertThat(bloomFilter1.asBytes()).isEqualTo(base16().decode("102256C04D"));
+ assertThat(bloomFilter2.asBytes()).isEqualTo(base16().decode("461524D008"));
+ }
+
+ // Adds this test case to spec. We can easily change the parameters (e.g. account keys, ble
+ // address, battery data and battery remaining time) to clarify test results with partners.
+ @Test
+ public void specificBitPattern_hasTwoAccountKeys_withBatteryLevelAndRemainingTime() {
+ BloomFilter bloomFilter1 = new BloomFilter(new byte[5], newHasher());
+ BloomFilter bloomFilter2 = new BloomFilter(new byte[5], newHasher());
+ byte[] accountKey1 = base16().decode("11223344556677889900AABBCCDDEEFF");
+ byte[] accountKey2 = base16().decode("11112222333344445555666677778888");
+ byte[] salt1 = base16().decode("C7");
+ byte[] salt2 = base16().decode("C7C8");
+ byte[] batteryData = {
+ 0b00110011, // length = 3, show UI indication.
+ 0b01000000, // Left bud: not charging, battery level = 64.
+ 0b01000000, // Right bud: not charging, battery level = 64.
+ 0b01000000 // Case: not charging, battery level = 64.
+ };
+ byte[] batteryRemainingTime = {
+ 0b00010101, // length = 1, type = 0b0101 (remaining battery time).
+ 0x1E, // remaining battery time (in minutes) = 30 minutes.
+ };
+
+ // Adds battery data to build bloom filter.
+ bloomFilter1.add(concat(accountKey1, salt1, batteryData, batteryRemainingTime));
+ bloomFilter1.add(concat(accountKey2, salt1, batteryData, batteryRemainingTime));
+ bloomFilter2.add(concat(accountKey1, salt2, batteryData, batteryRemainingTime));
+ bloomFilter2.add(concat(accountKey2, salt2, batteryData, batteryRemainingTime));
+
+ assertThat(bloomFilter1.asBytes()).isEqualTo(base16().decode("32A086B41A"));
+ assertThat(bloomFilter2.asBytes()).isEqualTo(base16().decode("C2A042043E"));
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bloomfilter/FastPairBloomFilterHasherTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bloomfilter/FastPairBloomFilterHasherTest.java
new file mode 100644
index 0000000..92c3b1a
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bloomfilter/FastPairBloomFilterHasherTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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.common.bloomfilter;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import org.junit.Test;
+
+import java.nio.charset.Charset;
+
+public class FastPairBloomFilterHasherTest {
+ private static final Charset CHARSET = UTF_8;
+ private static FastPairBloomFilterHasher sFastPairBloomFilterHasher =
+ new FastPairBloomFilterHasher();
+ @Test
+ public void getHashes() {
+ int[] hashe1 = sFastPairBloomFilterHasher.getHashes(element(1).getBytes(CHARSET));
+ int[] hashe2 = sFastPairBloomFilterHasher.getHashes(element(1).getBytes(CHARSET));
+ int[] hashe3 = sFastPairBloomFilterHasher.getHashes(element(2).getBytes(CHARSET));
+ assertThat(hashe1).isEqualTo(hashe2);
+ assertThat(hashe1).isNotEqualTo(hashe3);
+ }
+
+ private String element(int index) {
+ return "ELEMENT_" + index;
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/AdditionalDataEncoderTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/AdditionalDataEncoderTest.java
index 28d2fca..cf40fc6 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/AdditionalDataEncoderTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/AdditionalDataEncoderTest.java
@@ -63,6 +63,21 @@
@Test
@SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void inputNullSecretKeyToEncode_mustThrowException() {
+ byte[] rawData = base16().decode("00112233445566778899AABBCCDDEEFF");
+
+ GeneralSecurityException exception =
+ assertThrows(
+ GeneralSecurityException.class,
+ () -> AdditionalDataEncoder.encodeAdditionalDataPacket(null, rawData));
+
+ assertThat(exception)
+ .hasMessageThat()
+ .contains("Incorrect secret for encoding additional data packet");
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
public void inputIncorrectKeySizeToEncode_mustThrowException() {
byte[] secret = new byte[KEY_LENGTH - 1];
byte[] rawData = base16().decode("00112233445566778899AABBCCDDEEFF");
@@ -79,6 +94,54 @@
@Test
@SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void inputNullAdditionalDataToEncode_mustThrowException()
+ throws GeneralSecurityException {
+ byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
+
+ GeneralSecurityException exception =
+ assertThrows(
+ GeneralSecurityException.class,
+ () -> AdditionalDataEncoder.encodeAdditionalDataPacket(secret, null));
+
+ assertThat(exception)
+ .hasMessageThat()
+ .contains("Invalid data for encoding additional data packet");
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void inputInvalidAdditionalDataSizeToEncode_mustThrowException()
+ throws GeneralSecurityException {
+ byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
+ byte[] rawData = base16().decode("");
+ GeneralSecurityException exception =
+ assertThrows(
+ GeneralSecurityException.class,
+ () -> AdditionalDataEncoder.encodeAdditionalDataPacket(secret, rawData));
+
+ assertThat(exception)
+ .hasMessageThat()
+ .contains("Invalid data for encoding additional data packet");
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void inputTooLargeAdditionalDataSizeToEncode_mustThrowException()
+ throws GeneralSecurityException {
+ byte[] secret = AesCtrMultipleBlockEncryption.generateKey();
+ byte[] rawData = new byte[MAX_LENGTH_OF_DATA + 1];
+ GeneralSecurityException exception =
+ assertThrows(
+ GeneralSecurityException.class,
+ () -> AdditionalDataEncoder.encodeAdditionalDataPacket(secret, rawData));
+
+ assertThat(exception)
+ .hasMessageThat()
+ .contains("Invalid data for encoding additional data packet");
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
public void inputIncorrectKeySizeToDecode_mustThrowException() {
byte[] secret = new byte[KEY_LENGTH - 1];
byte[] packet = base16().decode("01234567890123456789");
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAudioPairerTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAudioPairerTest.java
index 0a56f2f..6871024 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAudioPairerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothAudioPairerTest.java
@@ -84,6 +84,23 @@
}
@SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void getDevice() {
+ Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ try {
+ assertThat(new BluetoothAudioPairer(
+ context,
+ BLUETOOTH_DEVICE,
+ Preferences.builder().build(),
+ new EventLoggerWrapper(new TestEventLogger()),
+ null /* KeyBasePairingInfo */,
+ null /*PasskeyConfirmationHandler */,
+ new TimingLogger(EVENT_NAME, Preferences.builder().build())).getDevice())
+ .isEqualTo(BLUETOOTH_DEVICE);
+ } catch (PairingException e) {
+ }
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
public void testBluetoothAudioPairerUnpairNoCrash() {
Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
try {
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothClassicPairerTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothClassicPairerTest.java
new file mode 100644
index 0000000..48f4b9b
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BluetoothClassicPairerTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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.common.bluetooth.fastpair;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+
+public class BluetoothClassicPairerTest {
+ @Mock
+ PasskeyConfirmationHandler mPasskeyConfirmationHandler;
+ private static final BluetoothDevice BLUETOOTH_DEVICE = BluetoothAdapter.getDefaultAdapter()
+ .getRemoteDevice("11:22:33:44:55:66");
+ private BluetoothClassicPairer mBluetoothClassicPairer;
+
+ @Before
+ public void setUp() {
+ mBluetoothClassicPairer = new BluetoothClassicPairer(
+ ApplicationProvider.getApplicationContext(),
+ BLUETOOTH_DEVICE,
+ Preferences.builder().build(),
+ mPasskeyConfirmationHandler);
+ }
+
+ @Test
+ public void pair() throws PairingException {
+ PairingException exception =
+ assertThrows(
+ PairingException.class,
+ () -> mBluetoothClassicPairer.pair());
+
+ assertThat(exception)
+ .hasMessageThat()
+ .contains("BluetoothClassicPairer, createBond");
+ assertThat(mBluetoothClassicPairer.isPaired()).isFalse();
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BytesTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BytesTest.java
new file mode 100644
index 0000000..9450d60
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/BytesTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.common.bluetooth.fastpair;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SdkSuppress;
+
+import junit.framework.TestCase;
+
+import java.nio.ByteOrder;
+import java.util.Arrays;
+
+/**
+ * Unit tests for {@link Bytes}.
+ */
+public class BytesTest extends TestCase {
+
+ private static final Bytes.Value VALUE1 =
+ new Bytes.Value(new byte[]{1, 2}, ByteOrder.BIG_ENDIAN);
+ private static final Bytes.Value VALUE2 =
+ new Bytes.Value(new byte[]{1, 2}, ByteOrder.BIG_ENDIAN);
+ private static final Bytes.Value VALUE3 =
+ new Bytes.Value(new byte[]{1, 3}, ByteOrder.BIG_ENDIAN);
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testEquals_asExpected() {
+ assertThat(VALUE1.equals(VALUE2)).isTrue();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testNotEquals_asExpected() {
+ assertThat(VALUE1.equals(VALUE3)).isFalse();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testGetBytes_asExpected() {
+ assertThat(Arrays.equals(VALUE1.getBytes(ByteOrder.BIG_ENDIAN), new byte[]{1, 2})).isTrue();
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testToString() {
+ assertThat(VALUE1.toString()).isEqualTo("0102");
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testReverse() {
+ assertThat(VALUE1.reverse(new byte[]{1, 2})).isEqualTo(new byte[]{2, 1});
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/ConstantsTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/ConstantsTest.java
index f7ffa24..6684fbc 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/ConstantsTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/ConstantsTest.java
@@ -33,6 +33,8 @@
import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic;
import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattConnection;
+import com.google.common.collect.ImmutableList;
+
import junit.framework.TestCase;
import org.mockito.Mock;
@@ -44,6 +46,8 @@
*/
public class ConstantsTest extends TestCase {
+ private static final int PASSKEY = 32689;
+
@Mock
private BluetoothGattConnection mMockGattConnection;
@@ -78,4 +82,62 @@
assertThat(KeyBasedPairingCharacteristic.getId(mMockGattConnection))
.isEqualTo(OLD_KEY_BASE_PAIRING_CHARACTERISTICS);
}
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_accountKeyCharacteristic_notCrash() throws BluetoothException {
+ Constants.FastPairService.AccountKeyCharacteristic.getId(mMockGattConnection);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_additionalDataCharacteristic_notCrash() throws BluetoothException {
+ Constants.FastPairService.AdditionalDataCharacteristic.getId(mMockGattConnection);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_nameCharacteristic_notCrash() throws BluetoothException {
+ Constants.FastPairService.NameCharacteristic.getId(mMockGattConnection);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_passKeyCharacteristic_encryptDecryptSuccessfully()
+ throws java.security.GeneralSecurityException {
+ byte[] secret = AesEcbSingleBlockEncryption.generateKey();
+
+ Constants.FastPairService.PasskeyCharacteristic.getId(mMockGattConnection);
+ assertThat(
+ Constants.FastPairService.PasskeyCharacteristic.decrypt(
+ Constants.FastPairService.PasskeyCharacteristic.Type.SEEKER,
+ secret,
+ Constants.FastPairService.PasskeyCharacteristic.encrypt(
+ Constants.FastPairService.PasskeyCharacteristic.Type.SEEKER,
+ secret,
+ PASSKEY))
+ ).isEqualTo(PASSKEY);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_beaconActionsCharacteristic_notCrash() throws BluetoothException {
+ Constants.FastPairService.BeaconActionsCharacteristic.getId(mMockGattConnection);
+ for (byte b : ImmutableList.of(
+ (byte) Constants.FastPairService.BeaconActionsCharacteristic.BeaconActionType
+ .READ_BEACON_PARAMETERS,
+ (byte) Constants.FastPairService.BeaconActionsCharacteristic.BeaconActionType
+ .READ_PROVISIONING_STATE,
+ (byte) Constants.FastPairService.BeaconActionsCharacteristic.BeaconActionType
+ .SET_EPHEMERAL_IDENTITY_KEY,
+ (byte) Constants.FastPairService.BeaconActionsCharacteristic.BeaconActionType
+ .CLEAR_EPHEMERAL_IDENTITY_KEY,
+ (byte) Constants.FastPairService.BeaconActionsCharacteristic.BeaconActionType
+ .READ_EPHEMERAL_IDENTITY_KEY,
+ (byte) Constants.FastPairService.BeaconActionsCharacteristic.BeaconActionType
+ .RING,
+ (byte) Constants.FastPairService.BeaconActionsCharacteristic.BeaconActionType
+ .READ_RINGING_STATE,
+ (byte) Constants.FastPairService.BeaconActionsCharacteristic.BeaconActionType
+ .UNKNOWN
+ )) {
+ assertThat(Constants.FastPairService.BeaconActionsCharacteristic
+ .valueOf(b)).isEqualTo(b);
+ }
+ }
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/CreateBondExceptionTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/CreateBondExceptionTest.java
new file mode 100644
index 0000000..052e696
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/CreateBondExceptionTest.java
@@ -0,0 +1,44 @@
+/*
+ * 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.common.bluetooth.fastpair;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SdkSuppress;
+
+import com.android.server.nearby.common.bluetooth.BluetoothException;
+import com.android.server.nearby.intdefs.FastPairEventIntDefs;
+
+import junit.framework.TestCase;
+
+/**
+ * Unit tests for {@link CreateBondException}.
+ */
+public class CreateBondExceptionTest extends TestCase {
+
+ private static final String FORMAT = "FORMAT";
+ private static final int REASON = 0;
+ private static final CreateBondException EXCEPTION = new CreateBondException(
+ FastPairEventIntDefs.CreateBondErrorCode.INCORRECT_VARIANT, REASON, FORMAT);
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_getter_asExpected() throws BluetoothException {
+ assertThat(EXCEPTION.getErrorCode()).isEqualTo(
+ FastPairEventIntDefs.CreateBondErrorCode.INCORRECT_VARIANT);
+ assertThat(EXCEPTION.getReason()).isSameInstanceAs(REASON);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/DeviceIntentReceiverTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/DeviceIntentReceiverTest.java
new file mode 100644
index 0000000..94bf111
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/DeviceIntentReceiverTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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.common.bluetooth.fastpair;
+
+import static org.mockito.MockitoAnnotations.initMocks;
+
+import android.bluetooth.BluetoothDevice;
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import junit.framework.TestCase;
+
+import org.mockito.Mock;
+
+/**
+ * Unit tests for {@link DeviceIntentReceiver}.
+ */
+public class DeviceIntentReceiverTest extends TestCase {
+ @Mock Preferences mPreferences;
+ @Mock BluetoothDevice mBluetoothDevice;
+
+ private DeviceIntentReceiver mDeviceIntentReceiver;
+ private Intent mIntent;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ initMocks(this);
+
+ Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ mDeviceIntentReceiver = DeviceIntentReceiver.oneShotReceiver(
+ context, mPreferences, mBluetoothDevice);
+
+ mIntent = new Intent().putExtra(BluetoothDevice.EXTRA_DEVICE, mBluetoothDevice);
+ }
+
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_onReceive_notCrash() throws Exception {
+ mDeviceIntentReceiver.onReceive(mIntent);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/EventTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/EventTest.java
index 28e925f..1b63ad0 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/EventTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/EventTest.java
@@ -58,6 +58,13 @@
.setBluetoothDevice(BLUETOOTH_DEVICE)
.setProfile(PROFILE)
.build();
+ assertThat(event.hasBluetoothDevice()).isTrue();
+ assertThat(event.hasProfile()).isTrue();
+ assertThat(event.isFailure()).isTrue();
+ assertThat(event.toString()).isEqualTo(
+ "Event{eventCode=1120, timestamp=1234, profile=1, "
+ + "bluetoothDevice=11:22:33:44:55:66, "
+ + "exception=java.lang.Exception: Test exception}");
Parcel parcel = Parcel.obtain();
event.writeToParcel(parcel, event.describeContents());
@@ -70,5 +77,8 @@
assertThat(result.getEventCode()).isEqualTo(event.getEventCode());
assertThat(result.getBluetoothDevice()).isEqualTo(event.getBluetoothDevice());
assertThat(result.getProfile()).isEqualTo(event.getProfile());
+ assertThat(result.equals(event)).isTrue();
+
+ assertThat(Event.CREATOR.newArray(10)).isNotEmpty();
}
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/FastPairDualConnectionTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/FastPairDualConnectionTest.java
index a103a72..63c39b2 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/FastPairDualConnectionTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/FastPairDualConnectionTest.java
@@ -23,9 +23,11 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyShort;
import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.when;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
@@ -45,6 +47,7 @@
import junit.framework.TestCase;
+import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -75,6 +78,8 @@
private TestEventLogger mEventLogger;
@Mock private TimingLogger mTimingLogger;
@Mock private BluetoothAudioPairer mBluetoothAudioPairer;
+ @Mock private android.bluetooth.BluetoothAdapter mBluetoothAdapter;
+ @Mock FastPairDualConnection mFastPairDualConnection;
@Override
public void setUp() throws Exception {
@@ -287,6 +292,23 @@
}
}
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testReflectionException()
+ throws BluetoothException, ReflectionException, ExecutionException,
+ InterruptedException, PairingException, TimeoutException {
+ when(mFastPairDualConnection.pair())
+ .thenThrow(new ReflectionException(
+ new NoSuchMethodException("testReflectionException")));
+ ReflectionException exception =
+ assertThrows(
+ ReflectionException.class,
+ () -> mFastPairDualConnection.pair());
+ assertThat(exception)
+ .hasMessageThat()
+ .contains("testReflectionException");
+ }
+
@SdkSuppress(minSdkVersion = 32, codeName = "T")
public void testHistoryItem() {
FastPairDualConnection connection = new FastPairDualConnection(
@@ -307,6 +329,11 @@
.isFalse();
}
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testGetLeState() throws ReflectionException {
+ FastPairDualConnection.getLeState(mBluetoothAdapter);
+ }
+
static class TestEventLogger implements EventLogger {
private List<Item> mLogs = new ArrayList<>();
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/HandshakeHandlerTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/HandshakeHandlerTest.java
new file mode 100644
index 0000000..e53e60f
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/HandshakeHandlerTest.java
@@ -0,0 +1,524 @@
+/*
+ * 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.common.bluetooth.fastpair;
+
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.BLUETOOTH_ADDRESS_LENGTH;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.ActionOverBleFlag.ADDITIONAL_DATA_CHARACTERISTIC;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.ActionOverBleFlag.DEVICE_ACTION;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.KeyBasedPairingRequestFlag.PROVIDER_INITIATES_BONDING;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.KeyBasedPairingRequestFlag.REQUEST_DEVICE_NAME;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.KeyBasedPairingRequestFlag.REQUEST_DISCOVERABLE;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.KeyBasedPairingRequestFlag.REQUEST_RETROACTIVE_PAIR;
+import static com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request.ADDITIONAL_DATA_TYPE_INDEX;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.when;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.annotation.Nullable;
+import androidx.core.util.Consumer;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import com.android.server.nearby.common.bluetooth.BluetoothException;
+import com.android.server.nearby.common.bluetooth.BluetoothGattException;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.AdditionalDataCharacteristic.AdditionalDataType;
+import com.android.server.nearby.common.bluetooth.fastpair.Constants.FastPairService.KeyBasedPairingCharacteristic.Request;
+import com.android.server.nearby.common.bluetooth.gatt.BluetoothGattConnection;
+import com.android.server.nearby.common.bluetooth.testability.android.bluetooth.BluetoothAdapter;
+import com.android.server.nearby.common.bluetooth.util.BluetoothOperationExecutor;
+import com.android.server.nearby.intdefs.NearbyEventIntDefs;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.io.BaseEncoding;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.security.GeneralSecurityException;
+import java.time.Duration;
+import java.util.Arrays;
+
+/**
+ * Unit tests for {@link HandshakeHandler}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class HandshakeHandlerTest {
+
+ public static final byte[] PUBLIC_KEY =
+ BaseEncoding.base64().decode(
+ "d2JTfvfdS6u7LmGfMOmco3C7ra3lW1k17AOly0LrBydDZURacfTY"
+ + "IMmo5K1ejfD9e8b6qHsDTNzselhifi10kQ==");
+ private static final String SEEKER_ADDRESS = "A1:A2:A3:A4:A5:A6";
+ private static final String PROVIDER_BLE_ADDRESS = "11:22:33:44:55:66";
+ /**
+ * The random-resolvable private address (RPA) is sometimes used when advertising over BLE, to
+ * hide the static public address (otherwise, the Fast Pair device would b
+ * identifiable/trackable whenever it's BLE advertising).
+ */
+ private static final String RANDOM_PRIVATE_ADDRESS = "BB:BB:BB:BB:BB:1E";
+ private static final byte[] SHARED_SECRET =
+ BaseEncoding.base16().decode("0123456789ABCDEF0123456789ABCDEF");
+
+ @Mock EventLoggerWrapper mEventLoggerWrapper;
+ @Mock BluetoothGattConnection mBluetoothGattConnection;
+ @Mock BluetoothGattConnection.ChangeObserver mChangeObserver;
+ @Mock private Consumer<Integer> mRescueFromError;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void handshakeGattError_noRetryError_failed() throws BluetoothException {
+ HandshakeHandler.KeyBasedPairingRequest keyBasedPairingRequest =
+ new HandshakeHandler.KeyBasedPairingRequest.Builder()
+ .setVerificationData(BluetoothAddress.decode(PROVIDER_BLE_ADDRESS))
+ .build();
+ BluetoothGattException exception =
+ new BluetoothGattException("Exception for no retry", 257);
+ when(mChangeObserver.waitForUpdate(anyLong())).thenThrow(exception);
+ GattConnectionManager gattConnectionManager =
+ createGattConnectionManager(Preferences.builder(), () -> {});
+ gattConnectionManager.setGattConnection(mBluetoothGattConnection);
+ when(mBluetoothGattConnection.enableNotification(any(), any()))
+ .thenReturn(mChangeObserver);
+ InOrder inOrder = inOrder(mEventLoggerWrapper);
+
+ assertThrows(
+ BluetoothGattException.class,
+ () ->
+ getHandshakeHandler(gattConnectionManager, address -> address)
+ .doHandshakeWithRetryAndSignalLostCheck(
+ PUBLIC_KEY,
+ keyBasedPairingRequest,
+ mRescueFromError));
+
+ inOrder.verify(mEventLoggerWrapper).setCurrentEvent(
+ NearbyEventIntDefs.EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION);
+ inOrder.verify(mEventLoggerWrapper).logCurrentEventFailed(exception);
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void handshakeGattError_retryAndNoCount_throwException() throws BluetoothException {
+ HandshakeHandler.KeyBasedPairingRequest keyBasedPairingRequest =
+ new HandshakeHandler.KeyBasedPairingRequest.Builder()
+ .setVerificationData(BluetoothAddress.decode(PROVIDER_BLE_ADDRESS))
+ .build();
+ BluetoothGattException exception = new BluetoothGattException("Exception for retry", 133);
+ when(mChangeObserver.waitForUpdate(anyLong())).thenThrow(exception);
+ GattConnectionManager gattConnectionManager =
+ createGattConnectionManager(Preferences.builder(), () -> {});
+ gattConnectionManager.setGattConnection(mBluetoothGattConnection);
+ when(mBluetoothGattConnection.enableNotification(any(), any()))
+ .thenReturn(mChangeObserver);
+ InOrder inOrder = inOrder(mEventLoggerWrapper);
+
+ HandshakeHandler.HandshakeException handshakeException =
+ assertThrows(
+ HandshakeHandler.HandshakeException.class,
+ () -> getHandshakeHandler(gattConnectionManager, address -> address)
+ .doHandshakeWithRetryAndSignalLostCheck(
+ PUBLIC_KEY, keyBasedPairingRequest, mRescueFromError));
+
+ inOrder.verify(mEventLoggerWrapper)
+ .setCurrentEvent(NearbyEventIntDefs.EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION);
+ inOrder.verify(mEventLoggerWrapper).logCurrentEventFailed(exception);
+ inOrder.verify(mEventLoggerWrapper)
+ .setCurrentEvent(NearbyEventIntDefs.EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION);
+ inOrder.verify(mEventLoggerWrapper).logCurrentEventFailed(exception);
+ inOrder.verify(mEventLoggerWrapper)
+ .setCurrentEvent(NearbyEventIntDefs.EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION);
+ inOrder.verify(mEventLoggerWrapper).logCurrentEventFailed(exception);
+ inOrder.verify(mEventLoggerWrapper)
+ .setCurrentEvent(NearbyEventIntDefs.EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION);
+ inOrder.verify(mEventLoggerWrapper).logCurrentEventFailed(exception);
+ inOrder.verifyNoMoreInteractions();
+ assertThat(handshakeException.getOriginalException()).isEqualTo(exception);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void handshakeGattError_noRetryOnTimeout_throwException() throws BluetoothException {
+ HandshakeHandler.KeyBasedPairingRequest keyBasedPairingRequest =
+ new HandshakeHandler.KeyBasedPairingRequest.Builder()
+ .setVerificationData(BluetoothAddress.decode(PROVIDER_BLE_ADDRESS))
+ .build();
+ BluetoothOperationExecutor.BluetoothOperationTimeoutException exception =
+ new BluetoothOperationExecutor.BluetoothOperationTimeoutException("Test timeout");
+ when(mChangeObserver.waitForUpdate(anyLong())).thenThrow(exception);
+ GattConnectionManager gattConnectionManager =
+ createGattConnectionManager(Preferences.builder(), () -> {});
+ gattConnectionManager.setGattConnection(mBluetoothGattConnection);
+ when(mBluetoothGattConnection.enableNotification(any(), any()))
+ .thenReturn(mChangeObserver);
+ InOrder inOrder = inOrder(mEventLoggerWrapper);
+
+ assertThrows(
+ HandshakeHandler.HandshakeException.class,
+ () ->
+ new HandshakeHandler(
+ gattConnectionManager,
+ PROVIDER_BLE_ADDRESS,
+ Preferences.builder().setRetrySecretHandshakeTimeout(false).build(),
+ mEventLoggerWrapper,
+ address -> address)
+ .doHandshakeWithRetryAndSignalLostCheck(
+ PUBLIC_KEY, keyBasedPairingRequest, mRescueFromError));
+
+ inOrder.verify(mEventLoggerWrapper)
+ .setCurrentEvent(NearbyEventIntDefs.EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION);
+ inOrder.verify(mEventLoggerWrapper).logCurrentEventFailed(exception);
+ inOrder.verifyNoMoreInteractions();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void handshakeGattError_signalLost() throws BluetoothException {
+ HandshakeHandler.KeyBasedPairingRequest keyBasedPairingRequest =
+ new HandshakeHandler.KeyBasedPairingRequest.Builder()
+ .setVerificationData(BluetoothAddress.decode(PROVIDER_BLE_ADDRESS))
+ .build();
+ BluetoothGattException exception = new BluetoothGattException("Exception for retry", 133);
+ when(mChangeObserver.waitForUpdate(anyLong())).thenThrow(exception);
+ GattConnectionManager gattConnectionManager =
+ createGattConnectionManager(Preferences.builder(), () -> {});
+ gattConnectionManager.setGattConnection(mBluetoothGattConnection);
+ when(mBluetoothGattConnection.enableNotification(any(), any()))
+ .thenReturn(mChangeObserver);
+ InOrder inOrder = inOrder(mEventLoggerWrapper);
+
+ SignalLostException signalLostException =
+ assertThrows(
+ SignalLostException.class,
+ () -> getHandshakeHandler(gattConnectionManager, address -> null)
+ .doHandshakeWithRetryAndSignalLostCheck(
+ PUBLIC_KEY, keyBasedPairingRequest, mRescueFromError));
+
+ inOrder.verify(mEventLoggerWrapper)
+ .setCurrentEvent(NearbyEventIntDefs.EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION);
+ inOrder.verify(mEventLoggerWrapper).logCurrentEventFailed(exception);
+ assertThat(signalLostException).hasCauseThat().isEqualTo(exception);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void handshakeGattError_addressRotate() throws BluetoothException {
+ HandshakeHandler.KeyBasedPairingRequest keyBasedPairingRequest =
+ new HandshakeHandler.KeyBasedPairingRequest.Builder()
+ .setVerificationData(BluetoothAddress.decode(PROVIDER_BLE_ADDRESS))
+ .build();
+ BluetoothGattException exception = new BluetoothGattException("Exception for retry", 133);
+ when(mChangeObserver.waitForUpdate(anyLong())).thenThrow(exception);
+ GattConnectionManager gattConnectionManager =
+ createGattConnectionManager(Preferences.builder(), () -> {});
+ gattConnectionManager.setGattConnection(mBluetoothGattConnection);
+ when(mBluetoothGattConnection.enableNotification(any(), any()))
+ .thenReturn(mChangeObserver);
+ InOrder inOrder = inOrder(mEventLoggerWrapper);
+
+ SignalRotatedException signalRotatedException =
+ assertThrows(
+ SignalRotatedException.class,
+ () -> getHandshakeHandler(
+ gattConnectionManager, address -> "AA:BB:CC:DD:EE:FF")
+ .doHandshakeWithRetryAndSignalLostCheck(
+ PUBLIC_KEY, keyBasedPairingRequest, mRescueFromError));
+
+ inOrder.verify(mEventLoggerWrapper).setCurrentEvent(
+ NearbyEventIntDefs.EventCode.SECRET_HANDSHAKE_GATT_COMMUNICATION);
+ inOrder.verify(mEventLoggerWrapper).logCurrentEventFailed(exception);
+ assertThat(signalRotatedException.getNewAddress()).isEqualTo("AA:BB:CC:DD:EE:FF");
+ assertThat(signalRotatedException).hasCauseThat().isEqualTo(exception);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void constructBytes_setRetroactiveFlag_decodeCorrectly() throws
+ GeneralSecurityException {
+ HandshakeHandler.KeyBasedPairingRequest keyBasedPairingRequest =
+ new HandshakeHandler.KeyBasedPairingRequest.Builder()
+ .setVerificationData(BluetoothAddress.decode(PROVIDER_BLE_ADDRESS))
+ .addFlag(REQUEST_RETROACTIVE_PAIR)
+ .setSeekerPublicAddress(BluetoothAddress.decode(SEEKER_ADDRESS))
+ .build();
+
+ byte[] encryptedRawMessage =
+ AesEcbSingleBlockEncryption.encrypt(
+ SHARED_SECRET, keyBasedPairingRequest.getBytes());
+ HandshakeRequest handshakeRequest =
+ new HandshakeRequest(SHARED_SECRET, encryptedRawMessage);
+
+ assertThat(handshakeRequest.getType())
+ .isEqualTo(HandshakeRequest.Type.KEY_BASED_PAIRING_REQUEST);
+ assertThat(handshakeRequest.requestRetroactivePair()).isTrue();
+ assertThat(handshakeRequest.getVerificationData())
+ .isEqualTo(BluetoothAddress.decode(PROVIDER_BLE_ADDRESS));
+ assertThat(handshakeRequest.getSeekerPublicAddress())
+ .isEqualTo(BluetoothAddress.decode(SEEKER_ADDRESS));
+ assertThat(handshakeRequest.requestDeviceName()).isFalse();
+ assertThat(handshakeRequest.requestDiscoverable()).isFalse();
+ assertThat(handshakeRequest.requestProviderInitialBonding()).isFalse();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void getTimeout_notOverShortRetryMaxSpentTime_getShort() {
+ Preferences preferences = Preferences.builder().build();
+
+ assertThat(getHandshakeHandler(/* getEnable128BitCustomGattCharacteristicsId= */ true)
+ .getTimeoutMs(
+ preferences.getSecretHandshakeShortTimeoutRetryMaxSpentTimeMs()
+ - 1))
+ .isEqualTo(preferences.getSecretHandshakeShortTimeoutMs());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void getTimeout_overShortRetryMaxSpentTime_getLong() {
+ Preferences preferences = Preferences.builder().build();
+
+ assertThat(getHandshakeHandler(/* getEnable128BitCustomGattCharacteristicsId= */ true)
+ .getTimeoutMs(
+ preferences.getSecretHandshakeShortTimeoutRetryMaxSpentTimeMs()
+ + 1))
+ .isEqualTo(preferences.getSecretHandshakeLongTimeoutMs());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void getTimeout_retryNotEnabled_getOrigin() {
+ Preferences preferences = Preferences.builder().build();
+
+ assertThat(
+ new HandshakeHandler(
+ createGattConnectionManager(Preferences.builder(), () -> {}),
+ PROVIDER_BLE_ADDRESS,
+ Preferences.builder()
+ .setRetryGattConnectionAndSecretHandshake(false).build(),
+ mEventLoggerWrapper,
+ /* fastPairSignalChecker= */ null)
+ .getTimeoutMs(0))
+ .isEqualTo(Duration.ofSeconds(
+ preferences.getGattOperationTimeoutSeconds()).toMillis());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void triggersActionOverBle_notCrash() {
+ HandshakeHandler.ActionOverBle.Builder actionOverBleBuilder =
+ new HandshakeHandler.ActionOverBle.Builder()
+ .addFlag(
+ Constants.FastPairService.KeyBasedPairingCharacteristic
+ .ActionOverBleFlag.ADDITIONAL_DATA_CHARACTERISTIC)
+ .setVerificationData(BluetoothAddress.decode(RANDOM_PRIVATE_ADDRESS))
+ .setAdditionalDataType(AdditionalDataType.PERSONALIZED_NAME)
+ .setEvent(0, 0)
+ .setEventAdditionalData(new byte[]{1})
+ .getThis();
+ HandshakeHandler.ActionOverBle actionOverBle = actionOverBleBuilder.build();
+ assertThat(actionOverBle.getBytes().length).isEqualTo(16);
+ assertThat(
+ Arrays.equals(
+ Arrays.copyOfRange(actionOverBle.getBytes(), 0, 12),
+ new byte[]{
+ (byte) 16, (byte) -64, (byte) -69, (byte) -69,
+ (byte) -69, (byte) -69, (byte) -69, (byte) 30,
+ (byte) 0, (byte) 0, (byte) 1, (byte) 1}))
+ .isTrue();
+ }
+
+ private GattConnectionManager createGattConnectionManager(
+ Preferences.Builder prefs, ToggleBluetoothTask toggleBluetooth) {
+ return new GattConnectionManager(
+ ApplicationProvider.getApplicationContext(),
+ prefs.build(),
+ new EventLoggerWrapper(null),
+ BluetoothAdapter.getDefaultAdapter(),
+ toggleBluetooth,
+ PROVIDER_BLE_ADDRESS,
+ new TimingLogger("GattConnectionManager", prefs.build()),
+ /* fastPairSignalChecker= */ null,
+ /* setMtu= */ false);
+ }
+
+ private HandshakeHandler getHandshakeHandler(
+ GattConnectionManager gattConnectionManager,
+ @Nullable FastPairConnection.FastPairSignalChecker fastPairSignalChecker) {
+ return new HandshakeHandler(
+ gattConnectionManager,
+ PROVIDER_BLE_ADDRESS,
+ Preferences.builder()
+ .setGattConnectionAndSecretHandshakeNoRetryGattError(ImmutableSet.of(257))
+ .setRetrySecretHandshakeTimeout(true)
+ .build(),
+ mEventLoggerWrapper,
+ fastPairSignalChecker);
+ }
+
+ private HandshakeHandler getHandshakeHandler(
+ boolean getEnable128BitCustomGattCharacteristicsId) {
+ return new HandshakeHandler(
+ createGattConnectionManager(Preferences.builder(), () -> {}),
+ PROVIDER_BLE_ADDRESS,
+ Preferences.builder()
+ .setGattOperationTimeoutSeconds(5)
+ .setEnable128BitCustomGattCharacteristicsId(
+ getEnable128BitCustomGattCharacteristicsId)
+ .build(),
+ mEventLoggerWrapper,
+ /* fastPairSignalChecker= */ null);
+ }
+
+ private static class HandshakeRequest {
+
+ /**
+ * 16 bytes data: 1-byte for type, 1-byte for flags, 6-bytes for provider's BLE address, 8
+ * bytes optional data.
+ *
+ * @see {go/fast-pair-spec-handshake-message1}
+ */
+ private final byte[] mDecryptedMessage;
+
+ HandshakeRequest(byte[] key, byte[] encryptedPairingRequest)
+ throws GeneralSecurityException {
+ mDecryptedMessage = AesEcbSingleBlockEncryption.decrypt(key, encryptedPairingRequest);
+ }
+
+ /**
+ * Gets the type of this handshake request. Currently, we have 2 types: 0x00 for Key-based
+ * Pairing Request and 0x10 for Action Request.
+ */
+ public Type getType() {
+ return Type.valueOf(mDecryptedMessage[Request.TYPE_INDEX]);
+ }
+
+ /**
+ * Gets verification data of this handshake request.
+ * Currently, we use Provider's BLE address.
+ */
+ public byte[] getVerificationData() {
+ return Arrays.copyOfRange(
+ mDecryptedMessage,
+ Request.VERIFICATION_DATA_INDEX,
+ Request.VERIFICATION_DATA_INDEX + Request.VERIFICATION_DATA_LENGTH);
+ }
+
+ /** Gets Seeker's public address of the handshake request. */
+ public byte[] getSeekerPublicAddress() {
+ return Arrays.copyOfRange(
+ mDecryptedMessage,
+ Request.SEEKER_PUBLIC_ADDRESS_INDEX,
+ Request.SEEKER_PUBLIC_ADDRESS_INDEX + BLUETOOTH_ADDRESS_LENGTH);
+ }
+
+ /** Checks whether the Seeker request discoverability from flags byte. */
+ public boolean requestDiscoverable() {
+ return (getFlags() & REQUEST_DISCOVERABLE) != 0;
+ }
+
+ /**
+ * Checks whether the Seeker requests that the Provider shall initiate bonding from
+ * flags byte.
+ */
+ public boolean requestProviderInitialBonding() {
+ return (getFlags() & PROVIDER_INITIATES_BONDING) != 0;
+ }
+
+ /** Checks whether the Seeker requests that the Provider shall notify the existing name. */
+ public boolean requestDeviceName() {
+ return (getFlags() & REQUEST_DEVICE_NAME) != 0;
+ }
+
+ /** Checks whether this is for retroactively writing account key. */
+ public boolean requestRetroactivePair() {
+ return (getFlags() & REQUEST_RETROACTIVE_PAIR) != 0;
+ }
+
+ /** Gets the flags of this handshake request. */
+ private byte getFlags() {
+ return mDecryptedMessage[Request.FLAGS_INDEX];
+ }
+
+ /** Checks whether the Seeker requests a device action. */
+ public boolean requestDeviceAction() {
+ return (getFlags() & DEVICE_ACTION) != 0;
+ }
+
+ /**
+ * Checks whether the Seeker requests an action which will be followed by an additional
+ * data.
+ */
+ public boolean requestFollowedByAdditionalData() {
+ return (getFlags() & ADDITIONAL_DATA_CHARACTERISTIC) != 0;
+ }
+
+ /** Gets the {@link AdditionalDataType} of this handshake request. */
+ @AdditionalDataType
+ public int getAdditionalDataType() {
+ if (!requestFollowedByAdditionalData()
+ || mDecryptedMessage.length <= ADDITIONAL_DATA_TYPE_INDEX) {
+ return AdditionalDataType.UNKNOWN;
+ }
+ return mDecryptedMessage[ADDITIONAL_DATA_TYPE_INDEX];
+ }
+
+ /** Enumerates the handshake message types. */
+ public enum Type {
+ KEY_BASED_PAIRING_REQUEST(Request.TYPE_KEY_BASED_PAIRING_REQUEST),
+ ACTION_OVER_BLE(Request.TYPE_ACTION_OVER_BLE),
+ UNKNOWN((byte) 0xFF);
+
+ private final byte mValue;
+
+ Type(byte type) {
+ mValue = type;
+ }
+
+ public static Type valueOf(byte value) {
+ for (Type type : Type.values()) {
+ if (type.getValue() == value) {
+ return type;
+ }
+ }
+ return UNKNOWN;
+ }
+
+ public byte getValue() {
+ return mValue;
+ }
+ }
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/HeadsetPieceTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/HeadsetPieceTest.java
index 670b2ca..32e62a3 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/HeadsetPieceTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/HeadsetPieceTest.java
@@ -196,5 +196,63 @@
assertThat(headsetPiece.isBatteryLow()).isFalse();
}
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void describeContents() {
+ HeadsetPiece headsetPiece =
+ HeadsetPiece.builder()
+ .setLowLevelThreshold(30)
+ .setBatteryLevel(18)
+ .setImageUrl("http://fake.image.path/image.png")
+ .setCharging(true)
+ .build();
+ assertThat(headsetPiece.describeContents()).isEqualTo(0);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void hashcode() {
+ HeadsetPiece headsetPiece =
+ HeadsetPiece.builder()
+ .setLowLevelThreshold(30)
+ .setBatteryLevel(18)
+ .setImageUrl("http://fake.image.path/image.png")
+ .setCharging(true)
+ .build();
+ HeadsetPiece headsetPiece1 =
+ HeadsetPiece.builder()
+ .setLowLevelThreshold(30)
+ .setBatteryLevel(18)
+ .setImageUrl("http://fake.image.path/image.png")
+ .setCharging(true)
+ .build();
+ assertThat(headsetPiece.hashCode()).isEqualTo(headsetPiece1.hashCode());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testToString() {
+ HeadsetPiece headsetPiece =
+ HeadsetPiece.builder()
+ .setLowLevelThreshold(30)
+ .setBatteryLevel(18)
+ .setImageUrl("http://fake.image.path/image.png")
+ .setCharging(true)
+ .build();
+ assertThat(headsetPiece.toString())
+ .isEqualTo("HeadsetPiece{lowLevelThreshold=30,"
+ + " batteryLevel=18,"
+ + " imageUrl=http://fake.image.path/image.png,"
+ + " charging=true,"
+ + " imageContentUri=null}");
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testCreatorNewArray() {
+ HeadsetPiece[] headsetPieces =
+ HeadsetPiece.CREATOR.newArray(2);
+ assertThat(headsetPieces.length).isEqualTo(2);
+ }
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/LtvTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/LtvTest.java
new file mode 100644
index 0000000..81a5d92
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/LtvTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.common.bluetooth.fastpair;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Unit tests for {@link Ltv}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class LtvTest {
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testParseEmpty_throwsException() throws Ltv.ParseException {
+ assertThrows(Ltv.ParseException.class,
+ () -> Ltv.parse(new byte[]{0}));
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testParse_finishesSuccessfully() throws Ltv.ParseException {
+ assertThat(Ltv.parse(new byte[]{3, 4, 5, 6})).isNotEmpty();
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/PairingProgressListenerTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/PairingProgressListenerTest.java
new file mode 100644
index 0000000..a58a92d
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/PairingProgressListenerTest.java
@@ -0,0 +1,38 @@
+/*
+ * 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.common.bluetooth.fastpair;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class PairingProgressListenerTest {
+
+ @Test
+ public void testFromOrdinal() {
+ assertThat(PairingProgressListener.fromOrdinal(0)).isEqualTo(
+ PairingProgressListener.PairingEvent.START);
+ assertThat(PairingProgressListener.fromOrdinal(1)).isEqualTo(
+ PairingProgressListener.PairingEvent.SUCCESS);
+ assertThat(PairingProgressListener.fromOrdinal(2)).isEqualTo(
+ PairingProgressListener.PairingEvent.FAILED);
+ assertThat(PairingProgressListener.fromOrdinal(3)).isEqualTo(
+ PairingProgressListener.PairingEvent.UNKNOWN);
+ assertThat(PairingProgressListener.fromOrdinal(4)).isEqualTo(
+ PairingProgressListener.PairingEvent.UNKNOWN);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/PreferencesTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/PreferencesTest.java
index b40a5a5..378e3b9 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/PreferencesTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/PreferencesTest.java
@@ -1266,12 +1266,38 @@
public void testExtraLoggingInformation() {
Preferences prefs =
Preferences.builder().setExtraLoggingInformation(FIRST_EXTRA_LOGGING_INFO).build();
- assertThat(prefs.getExtraLoggingInformation()).isEqualTo(FIRST_EXTRA_LOGGING_INFO);
- assertThat(prefs.toBuilder().build().getExtraLoggingInformation())
- .isEqualTo(FIRST_EXTRA_LOGGING_INFO);
-
Preferences prefs2 =
Preferences.builder().setExtraLoggingInformation(SECOND_EXTRA_LOGGING_INFO).build();
+ Preferences prefs3 =
+ Preferences.builder().setExtraLoggingInformation(FIRST_EXTRA_LOGGING_INFO).build();
+
+ assertThat(prefs.getExtraLoggingInformation()).isEqualTo(FIRST_EXTRA_LOGGING_INFO);
assertThat(prefs2.getExtraLoggingInformation()).isEqualTo(SECOND_EXTRA_LOGGING_INFO);
+ assertThat(prefs.toBuilder().build().getExtraLoggingInformation())
+ .isEqualTo(FIRST_EXTRA_LOGGING_INFO);
+ // Test equal()
+ assertThat(prefs.getExtraLoggingInformation().equals(null)).isFalse();
+ assertThat(prefs.getExtraLoggingInformation()
+ .equals(prefs2.getExtraLoggingInformation())).isFalse();
+ assertThat(prefs.getExtraLoggingInformation()
+ .equals(prefs3.getExtraLoggingInformation())).isTrue();
+ // Test getModelId()
+ assertThat(prefs.getExtraLoggingInformation().getModelId())
+ .isEqualTo(prefs3.getExtraLoggingInformation().getModelId());
+ assertThat(prefs.getExtraLoggingInformation().getModelId())
+ .isNotEqualTo(prefs2.getExtraLoggingInformation().getModelId());
+ // Test hashCode()
+ assertThat(prefs.getExtraLoggingInformation().hashCode())
+ .isEqualTo(prefs3.getExtraLoggingInformation().hashCode());
+ assertThat(prefs.getExtraLoggingInformation().hashCode())
+ .isNotEqualTo(prefs2.getExtraLoggingInformation().hashCode());
+ // Test toBuilder()
+
+ assertThat(prefs.getExtraLoggingInformation()
+ .toBuilder().setModelId("000007").build().getModelId())
+ .isEqualTo("000007");
+ // Test toString()
+ assertThat(prefs.getExtraLoggingInformation().toString())
+ .isEqualTo("ExtraLoggingInformation{modelId=000006}");
}
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/TdsExceptionTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/TdsExceptionTest.java
new file mode 100644
index 0000000..e8b9480
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/fastpair/TdsExceptionTest.java
@@ -0,0 +1,34 @@
+/*
+ * 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.common.bluetooth.fastpair;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.server.nearby.intdefs.FastPairEventIntDefs;
+
+import org.junit.Test;
+
+public class TdsExceptionTest {
+ @Test
+ public void testGetErrorCode() {
+ TdsException tdsException = new TdsException(
+ FastPairEventIntDefs.BrEdrHandoverErrorCode.BLUETOOTH_MAC_INVALID, "format");
+ assertThat(tdsException.getErrorCode())
+ .isEqualTo(FastPairEventIntDefs.BrEdrHandoverErrorCode.BLUETOOTH_MAC_INVALID);
+
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothAdapterTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothAdapterTest.java
new file mode 100644
index 0000000..a9ab7da
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothAdapterTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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.common.bluetooth.testability.android.bluetooth;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Unit tests for {@link BluetoothAdapter}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BluetoothAdapterTest {
+
+ private static final byte[] BYTES = new byte[]{0, 1, 2, 3, 4, 5};
+ private static final String ADDRESS = "00:11:22:33:AA:BB";
+
+ @Mock private android.bluetooth.BluetoothAdapter mBluetoothAdapter;
+ @Mock private android.bluetooth.BluetoothDevice mBluetoothDevice;
+ @Mock private android.bluetooth.le.BluetoothLeAdvertiser mBluetoothLeAdvertiser;
+ @Mock private android.bluetooth.le.BluetoothLeScanner mBluetoothLeScanner;
+
+ BluetoothAdapter mTestabilityBluetoothAdapter;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mTestabilityBluetoothAdapter = BluetoothAdapter.wrap(mBluetoothAdapter);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testWrapNullAdapter_isNull() {
+ assertThat(BluetoothAdapter.wrap(null)).isNull();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testWrapNonNullAdapter_isNotNull_unWrapSame() {
+ assertThat(mTestabilityBluetoothAdapter).isNotNull();
+ assertThat(mTestabilityBluetoothAdapter.unwrap()).isSameInstanceAs(mBluetoothAdapter);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testDisable_callsWrapped() {
+ when(mBluetoothAdapter.disable()).thenReturn(true);
+ assertThat(mTestabilityBluetoothAdapter.disable()).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testEnable_callsWrapped() {
+ when(mBluetoothAdapter.enable()).thenReturn(true);
+ assertThat(mTestabilityBluetoothAdapter.enable()).isTrue();
+ when(mBluetoothAdapter.isEnabled()).thenReturn(true);
+ assertThat(mTestabilityBluetoothAdapter.isEnabled()).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testGetBluetoothLeAdvertiser_callsWrapped() {
+ when(mBluetoothAdapter.getBluetoothLeAdvertiser()).thenReturn(mBluetoothLeAdvertiser);
+ assertThat(mTestabilityBluetoothAdapter.getBluetoothLeAdvertiser().unwrap())
+ .isSameInstanceAs(mBluetoothLeAdvertiser);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testGetBluetoothLeScanner_callsWrapped() {
+ when(mBluetoothAdapter.getBluetoothLeScanner()).thenReturn(mBluetoothLeScanner);
+ assertThat(mTestabilityBluetoothAdapter.getBluetoothLeScanner().unwrap())
+ .isSameInstanceAs(mBluetoothLeScanner);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testGetBondedDevices_callsWrapped() {
+ when(mBluetoothAdapter.getBondedDevices()).thenReturn(null);
+ assertThat(mTestabilityBluetoothAdapter.getBondedDevices()).isNull();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testIsDiscovering_pcallsWrapped() {
+ when(mBluetoothAdapter.isDiscovering()).thenReturn(true);
+ assertThat(mTestabilityBluetoothAdapter.isDiscovering()).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testStartDiscovery_callsWrapped() {
+ when(mBluetoothAdapter.startDiscovery()).thenReturn(true);
+ assertThat(mTestabilityBluetoothAdapter.startDiscovery()).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testCancelDiscovery_callsWrapped() {
+ when(mBluetoothAdapter.cancelDiscovery()).thenReturn(true);
+ assertThat(mTestabilityBluetoothAdapter.cancelDiscovery()).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testGetRemoteDeviceBytes_callsWrapped() {
+ when(mBluetoothAdapter.getRemoteDevice(BYTES)).thenReturn(mBluetoothDevice);
+ assertThat(mTestabilityBluetoothAdapter.getRemoteDevice(BYTES).unwrap())
+ .isSameInstanceAs(mBluetoothDevice);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testGetRemoteDeviceString_callsWrapped() {
+ when(mBluetoothAdapter.getRemoteDevice(ADDRESS)).thenReturn(mBluetoothDevice);
+ assertThat(mTestabilityBluetoothAdapter.getRemoteDevice(ADDRESS).unwrap())
+ .isSameInstanceAs(mBluetoothDevice);
+
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothDeviceTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothDeviceTest.java
new file mode 100644
index 0000000..d494024
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothDeviceTest.java
@@ -0,0 +1,206 @@
+/*
+ * 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.common.bluetooth.testability.android.bluetooth;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.IOException;
+import java.util.UUID;
+
+/**
+ * Unit tests for {@link BluetoothDevice}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BluetoothDeviceTest {
+ private static final UUID UUID_CONST = UUID.randomUUID();
+ private static final String ADDRESS = "ADDRESS";
+ private static final String STRING = "STRING";
+
+ @Mock private android.bluetooth.BluetoothDevice mBluetoothDevice;
+ @Mock private android.bluetooth.BluetoothGatt mBluetoothGatt;
+ @Mock private android.bluetooth.BluetoothSocket mBluetoothSocket;
+ @Mock private android.bluetooth.BluetoothClass mBluetoothClass;
+
+ BluetoothDevice mTestabilityBluetoothDevice;
+ BluetoothGattCallback mTestBluetoothGattCallback;
+ Context mContext;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mTestabilityBluetoothDevice = BluetoothDevice.wrap(mBluetoothDevice);
+ mTestBluetoothGattCallback = new TestBluetoothGattCallback();
+ mContext = InstrumentationRegistry.getInstrumentation().getContext();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testWrapNonNullAdapter_isNotNull_unWrapSame() {
+ assertThat(mTestabilityBluetoothDevice).isNotNull();
+ assertThat(mTestabilityBluetoothDevice.unwrap()).isSameInstanceAs(mBluetoothDevice);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testEquality_asExpected() {
+ assertThat(mTestabilityBluetoothDevice.equals(null)).isFalse();
+ assertThat(mTestabilityBluetoothDevice.equals(mTestabilityBluetoothDevice)).isTrue();
+ assertThat(mTestabilityBluetoothDevice.equals(BluetoothDevice.wrap(mBluetoothDevice)))
+ .isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testHashCode_asExpected() {
+ assertThat(mTestabilityBluetoothDevice.hashCode())
+ .isEqualTo(BluetoothDevice.wrap(mBluetoothDevice).hashCode());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testConnectGattWithThreeParameters_callsWrapped() {
+ when(mBluetoothDevice
+ .connectGatt(mContext, true, mTestBluetoothGattCallback.unwrap()))
+ .thenReturn(mBluetoothGatt);
+ assertThat(mTestabilityBluetoothDevice
+ .connectGatt(mContext, true, mTestBluetoothGattCallback)
+ .unwrap())
+ .isSameInstanceAs(mBluetoothGatt);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testConnectGattWithFourParameters_callsWrapped() {
+ when(mBluetoothDevice
+ .connectGatt(mContext, true, mTestBluetoothGattCallback.unwrap(), 1))
+ .thenReturn(mBluetoothGatt);
+ assertThat(mTestabilityBluetoothDevice
+ .connectGatt(mContext, true, mTestBluetoothGattCallback, 1)
+ .unwrap())
+ .isSameInstanceAs(mBluetoothGatt);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testCreateRfcommSocketToServiceRecord_callsWrapped() throws IOException {
+ when(mBluetoothDevice.createRfcommSocketToServiceRecord(UUID_CONST))
+ .thenReturn(mBluetoothSocket);
+ assertThat(mTestabilityBluetoothDevice.createRfcommSocketToServiceRecord(UUID_CONST))
+ .isSameInstanceAs(mBluetoothSocket);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testCreateInsecureRfcommSocketToServiceRecord_callsWrapped() throws IOException {
+ when(mBluetoothDevice.createInsecureRfcommSocketToServiceRecord(UUID_CONST))
+ .thenReturn(mBluetoothSocket);
+ assertThat(mTestabilityBluetoothDevice
+ .createInsecureRfcommSocketToServiceRecord(UUID_CONST))
+ .isSameInstanceAs(mBluetoothSocket);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testSetPairingConfirmation_callsWrapped() throws IOException {
+ when(mBluetoothDevice.setPairingConfirmation(true)).thenReturn(true);
+ assertThat(mTestabilityBluetoothDevice.setPairingConfirmation(true)).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testFetchUuidsWithSdp_callsWrapped() throws IOException {
+ when(mBluetoothDevice.fetchUuidsWithSdp()).thenReturn(true);
+ assertThat(mTestabilityBluetoothDevice.fetchUuidsWithSdp()).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testCreateBond_callsWrapped() throws IOException {
+ when(mBluetoothDevice.createBond()).thenReturn(true);
+ assertThat(mTestabilityBluetoothDevice.createBond()).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testGetUuids_callsWrapped() throws IOException {
+ when(mBluetoothDevice.getUuids()).thenReturn(null);
+ assertThat(mTestabilityBluetoothDevice.getUuids()).isNull();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testGetBondState_callsWrapped() throws IOException {
+ when(mBluetoothDevice.getBondState()).thenReturn(1);
+ assertThat(mTestabilityBluetoothDevice.getBondState()).isEqualTo(1);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testGetAddress_callsWrapped() throws IOException {
+ when(mBluetoothDevice.getAddress()).thenReturn(ADDRESS);
+ assertThat(mTestabilityBluetoothDevice.getAddress()).isSameInstanceAs(ADDRESS);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testGetBluetoothClass_callsWrapped() throws IOException {
+ when(mBluetoothDevice.getBluetoothClass()).thenReturn(mBluetoothClass);
+ assertThat(mTestabilityBluetoothDevice.getBluetoothClass())
+ .isSameInstanceAs(mBluetoothClass);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testGetType_callsWrapped() throws IOException {
+ when(mBluetoothDevice.getType()).thenReturn(1);
+ assertThat(mTestabilityBluetoothDevice.getType()).isEqualTo(1);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testGetName_callsWrapped() throws IOException {
+ when(mBluetoothDevice.getName()).thenReturn(STRING);
+ assertThat(mTestabilityBluetoothDevice.getName()).isSameInstanceAs(STRING);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testToString_callsWrapped() {
+ when(mBluetoothDevice.toString()).thenReturn(STRING);
+ assertThat(mTestabilityBluetoothDevice.toString()).isSameInstanceAs(STRING);
+ }
+
+ private static class TestBluetoothGattCallback extends BluetoothGattCallback {}
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattCallbackTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattCallbackTest.java
new file mode 100644
index 0000000..26ae6d7
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattCallbackTest.java
@@ -0,0 +1,98 @@
+/*
+ * 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.common.bluetooth.testability.android.bluetooth;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+/**
+ * Unit tests for {@link BluetoothGattCallback}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BluetoothGattCallbackTest {
+ @Mock private android.bluetooth.BluetoothGatt mBluetoothGatt;
+ @Mock private android.bluetooth.BluetoothGattCharacteristic mBluetoothGattCharacteristic;
+ @Mock private android.bluetooth.BluetoothGattDescriptor mBluetoothGattDescriptor;
+
+ TestBluetoothGattCallback mTestBluetoothGattCallback = new TestBluetoothGattCallback();
+
+ @Test
+ public void testOnConnectionStateChange_notCrash() {
+ mTestBluetoothGattCallback.unwrap()
+ .onConnectionStateChange(mBluetoothGatt, 1, 1);
+ }
+
+ @Test
+ public void testOnServiceDiscovered_notCrash() {
+ mTestBluetoothGattCallback.unwrap().onServicesDiscovered(mBluetoothGatt, 1);
+ }
+
+ @Test
+ public void testOnCharacteristicRead_notCrash() {
+ mTestBluetoothGattCallback.unwrap().onCharacteristicRead(mBluetoothGatt,
+ mBluetoothGattCharacteristic, 1);
+ }
+
+ @Test
+ public void testOnCharacteristicWrite_notCrash() {
+ mTestBluetoothGattCallback.unwrap().onCharacteristicWrite(mBluetoothGatt,
+ mBluetoothGattCharacteristic, 1);
+ }
+
+ @Test
+ public void testOnDescriptionRead_notCrash() {
+ mTestBluetoothGattCallback.unwrap().onDescriptorRead(mBluetoothGatt,
+ mBluetoothGattDescriptor, 1);
+ }
+
+ @Test
+ public void testOnDescriptionWrite_notCrash() {
+ mTestBluetoothGattCallback.unwrap().onDescriptorWrite(mBluetoothGatt,
+ mBluetoothGattDescriptor, 1);
+ }
+
+ @Test
+ public void testOnReadRemoteRssi_notCrash() {
+ mTestBluetoothGattCallback.unwrap().onReadRemoteRssi(mBluetoothGatt, 1, 1);
+ }
+
+ @Test
+ public void testOnReliableWriteCompleted_notCrash() {
+ mTestBluetoothGattCallback.unwrap().onReliableWriteCompleted(mBluetoothGatt, 1);
+ }
+
+ @Test
+ public void testOnMtuChanged_notCrash() {
+ mTestBluetoothGattCallback.unwrap().onMtuChanged(mBluetoothGatt, 1, 1);
+ }
+
+ @Test
+ public void testOnCharacteristicChanged_notCrash() {
+ mTestBluetoothGattCallback.unwrap()
+ .onCharacteristicChanged(mBluetoothGatt, mBluetoothGattCharacteristic);
+ }
+
+ private static class TestBluetoothGattCallback extends BluetoothGattCallback { }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServerCallbackTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServerCallbackTest.java
new file mode 100644
index 0000000..fb99317
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServerCallbackTest.java
@@ -0,0 +1,121 @@
+/*
+ * 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.common.bluetooth.testability.android.bluetooth;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+
+/**
+ * Unit tests for {@link BluetoothGattServerCallback}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BluetoothGattServerCallbackTest {
+ @Mock
+ private android.bluetooth.BluetoothDevice mBluetoothDevice;
+ @Mock
+ private android.bluetooth.BluetoothGattService mBluetoothGattService;
+ @Mock
+ private android.bluetooth.BluetoothGattCharacteristic mBluetoothGattCharacteristic;
+ @Mock
+ private android.bluetooth.BluetoothGattDescriptor mBluetoothGattDescriptor;
+
+ TestBluetoothGattServerCallback
+ mTestBluetoothGattServerCallback = new TestBluetoothGattServerCallback();
+
+ @Test
+ public void testOnCharacteristicReadRequest_notCrash() {
+ mTestBluetoothGattServerCallback.unwrap().onCharacteristicReadRequest(
+ mBluetoothDevice, 1, 1, mBluetoothGattCharacteristic);
+ }
+
+ @Test
+ public void testOnCharacteristicWriteRequest_notCrash() {
+ mTestBluetoothGattServerCallback.unwrap().onCharacteristicWriteRequest(
+ mBluetoothDevice,
+ 1,
+ mBluetoothGattCharacteristic,
+ false,
+ true,
+ 1,
+ new byte[]{1});
+ }
+
+ @Test
+ public void testOnConnectionStateChange_notCrash() {
+ mTestBluetoothGattServerCallback.unwrap().onConnectionStateChange(
+ mBluetoothDevice,
+ 1,
+ 2);
+ }
+
+ @Test
+ public void testOnDescriptorReadRequest_notCrash() {
+ mTestBluetoothGattServerCallback.unwrap().onDescriptorReadRequest(
+ mBluetoothDevice,
+ 1,
+ 2, mBluetoothGattDescriptor);
+ }
+
+ @Test
+ public void testOnDescriptorWriteRequest_notCrash() {
+ mTestBluetoothGattServerCallback.unwrap().onDescriptorWriteRequest(
+ mBluetoothDevice,
+ 1,
+ mBluetoothGattDescriptor,
+ false,
+ true,
+ 2,
+ new byte[]{1});
+ }
+
+ @Test
+ public void testOnExecuteWrite_notCrash() {
+ mTestBluetoothGattServerCallback.unwrap().onExecuteWrite(
+ mBluetoothDevice,
+ 1,
+ false);
+ }
+
+ @Test
+ public void testOnMtuChanged_notCrash() {
+ mTestBluetoothGattServerCallback.unwrap().onMtuChanged(
+ mBluetoothDevice,
+ 1);
+ }
+
+ @Test
+ public void testOnNotificationSent_notCrash() {
+ mTestBluetoothGattServerCallback.unwrap().onNotificationSent(
+ mBluetoothDevice,
+ 1);
+ }
+
+ @Test
+ public void testOnServiceAdded_notCrash() {
+ mTestBluetoothGattServerCallback.unwrap().onServiceAdded(1, mBluetoothGattService);
+ }
+
+ private static class TestBluetoothGattServerCallback extends BluetoothGattServerCallback { }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServerTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServerTest.java
new file mode 100644
index 0000000..48283d1
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattServerTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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.common.bluetooth.testability.android.bluetooth;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.UUID;
+
+/**
+ * Unit tests for {@link BluetoothGattServer}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BluetoothGattServerTest {
+ private static final UUID UUID_CONST = UUID.randomUUID();
+ private static final byte[] BYTES = new byte[]{1, 2, 3};
+
+ @Mock private android.bluetooth.BluetoothDevice mBluetoothDevice;
+ @Mock private android.bluetooth.BluetoothGattServer mBluetoothGattServer;
+ @Mock private android.bluetooth.BluetoothGattService mBluetoothGattService;
+ @Mock private android.bluetooth.BluetoothGattCharacteristic mBluetoothGattCharacteristic;
+
+ BluetoothGattServer mTestabilityBluetoothGattServer;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mTestabilityBluetoothGattServer = BluetoothGattServer.wrap(mBluetoothGattServer);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testWrapNonNullAdapter_isNotNull_unWrapSame() {
+ assertThat(mTestabilityBluetoothGattServer).isNotNull();
+ assertThat(mTestabilityBluetoothGattServer.unwrap()).isSameInstanceAs(mBluetoothGattServer);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testConnect_callsWrapped() {
+ when(mBluetoothGattServer
+ .connect(mBluetoothDevice, true))
+ .thenReturn(true);
+ assertThat(mTestabilityBluetoothGattServer
+ .connect(BluetoothDevice.wrap(mBluetoothDevice), true))
+ .isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testAddService_callsWrapped() {
+ when(mBluetoothGattServer
+ .addService(mBluetoothGattService))
+ .thenReturn(true);
+ assertThat(mTestabilityBluetoothGattServer
+ .addService(mBluetoothGattService))
+ .isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testClearServices_callsWrapped() {
+ doNothing().when(mBluetoothGattServer).clearServices();
+ mTestabilityBluetoothGattServer.clearServices();
+ verify(mBluetoothGattServer).clearServices();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testClose_callsWrapped() {
+ doNothing().when(mBluetoothGattServer).close();
+ mTestabilityBluetoothGattServer.close();
+ verify(mBluetoothGattServer).close();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testNotifyCharacteristicChanged_callsWrapped() {
+ when(mBluetoothGattServer
+ .notifyCharacteristicChanged(
+ mBluetoothDevice,
+ mBluetoothGattCharacteristic,
+ true))
+ .thenReturn(true);
+ assertThat(mTestabilityBluetoothGattServer
+ .notifyCharacteristicChanged(
+ BluetoothDevice.wrap(mBluetoothDevice),
+ mBluetoothGattCharacteristic,
+ true))
+ .isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testSendResponse_callsWrapped() {
+ when(mBluetoothGattServer.sendResponse(
+ mBluetoothDevice, 1, 1, 1, BYTES)).thenReturn(true);
+ mTestabilityBluetoothGattServer.sendResponse(
+ BluetoothDevice.wrap(mBluetoothDevice), 1, 1, 1, BYTES);
+ verify(mBluetoothGattServer).sendResponse(
+ mBluetoothDevice, 1, 1, 1, BYTES);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testCancelConnection_callsWrapped() {
+ doNothing().when(mBluetoothGattServer).cancelConnection(mBluetoothDevice);
+ mTestabilityBluetoothGattServer.cancelConnection(BluetoothDevice.wrap(mBluetoothDevice));
+ verify(mBluetoothGattServer).cancelConnection(mBluetoothDevice);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testGetService_callsWrapped() {
+ when(mBluetoothGattServer.getService(UUID_CONST)).thenReturn(null);
+ assertThat(mTestabilityBluetoothGattServer.getService(UUID_CONST)).isNull();
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattWrapperTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattWrapperTest.java
new file mode 100644
index 0000000..a03a255
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/BluetoothGattWrapperTest.java
@@ -0,0 +1,190 @@
+/*
+ * 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.common.bluetooth.testability.android.bluetooth;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.UUID;
+
+/**
+ * Unit tests for {@link BluetoothGattWrapper}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BluetoothGattWrapperTest {
+ private static final UUID UUID_CONST = UUID.randomUUID();
+ private static final byte[] BYTES = new byte[]{1, 2, 3};
+
+ @Mock private android.bluetooth.BluetoothDevice mBluetoothDevice;
+ @Mock private android.bluetooth.BluetoothGatt mBluetoothGatt;
+ @Mock private android.bluetooth.BluetoothGattService mBluetoothGattService;
+ @Mock private android.bluetooth.BluetoothGattCharacteristic mBluetoothGattCharacteristic;
+ @Mock private android.bluetooth.BluetoothGattDescriptor mBluetoothGattDescriptor;
+
+ BluetoothGattWrapper mBluetoothGattWrapper;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mBluetoothGattWrapper = BluetoothGattWrapper.wrap(mBluetoothGatt);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testWrapNonNullAdapter_isNotNull_unWrapSame() {
+ assertThat(mBluetoothGattWrapper).isNotNull();
+ assertThat(mBluetoothGattWrapper.unwrap()).isSameInstanceAs(mBluetoothGatt);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testEquality_asExpected() {
+ assertThat(mBluetoothGattWrapper.equals(null)).isFalse();
+ assertThat(mBluetoothGattWrapper.equals(mBluetoothGattWrapper)).isTrue();
+ assertThat(mBluetoothGattWrapper.equals(BluetoothGattWrapper.wrap(mBluetoothGatt)))
+ .isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testGetDevice_callsWrapped() {
+ when(mBluetoothGatt.getDevice()).thenReturn(mBluetoothDevice);
+ assertThat(mBluetoothGattWrapper.getDevice().unwrap()).isSameInstanceAs(mBluetoothDevice);
+ }
+
+ @Test
+ public void testHashCode_asExpected() {
+ assertThat(mBluetoothGattWrapper.hashCode())
+ .isEqualTo(BluetoothGattWrapper.wrap(mBluetoothGatt).hashCode());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testGetServices_callsWrapped() {
+ when(mBluetoothGatt.getServices()).thenReturn(null);
+ assertThat(mBluetoothGattWrapper.getServices()).isNull();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testGetService_callsWrapped() {
+ when(mBluetoothGatt.getService(UUID_CONST)).thenReturn(mBluetoothGattService);
+ assertThat(mBluetoothGattWrapper.getService(UUID_CONST))
+ .isSameInstanceAs(mBluetoothGattService);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testDiscoverServices_callsWrapped() {
+ when(mBluetoothGatt.discoverServices()).thenReturn(true);
+ assertThat(mBluetoothGattWrapper.discoverServices()).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testReadCharacteristic_callsWrapped() {
+ when(mBluetoothGatt.readCharacteristic(mBluetoothGattCharacteristic)).thenReturn(true);
+ assertThat(mBluetoothGattWrapper.readCharacteristic(mBluetoothGattCharacteristic)).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testWriteCharacteristic_callsWrapped() {
+ when(mBluetoothGatt.writeCharacteristic(mBluetoothGattCharacteristic, BYTES, 1))
+ .thenReturn(1);
+ assertThat(mBluetoothGattWrapper.writeCharacteristic(
+ mBluetoothGattCharacteristic, BYTES, 1)).isEqualTo(1);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testReadDescriptor_callsWrapped() {
+ when(mBluetoothGatt.readDescriptor(mBluetoothGattDescriptor)).thenReturn(false);
+ assertThat(mBluetoothGattWrapper.readDescriptor(mBluetoothGattDescriptor)).isFalse();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testWriteDescriptor_callsWrapped() {
+ when(mBluetoothGatt.writeDescriptor(mBluetoothGattDescriptor, BYTES)).thenReturn(5);
+ assertThat(mBluetoothGattWrapper.writeDescriptor(mBluetoothGattDescriptor, BYTES))
+ .isEqualTo(5);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testReadRemoteRssi_callsWrapped() {
+ when(mBluetoothGatt.readRemoteRssi()).thenReturn(false);
+ assertThat(mBluetoothGattWrapper.readRemoteRssi()).isFalse();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testRequestConnectionPriority_callsWrapped() {
+ when(mBluetoothGatt.requestConnectionPriority(5)).thenReturn(false);
+ assertThat(mBluetoothGattWrapper.requestConnectionPriority(5)).isFalse();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testRequestMtu_callsWrapped() {
+ when(mBluetoothGatt.requestMtu(5)).thenReturn(false);
+ assertThat(mBluetoothGattWrapper.requestMtu(5)).isFalse();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testSetCharacteristicNotification_callsWrapped() {
+ when(mBluetoothGatt.setCharacteristicNotification(mBluetoothGattCharacteristic, true))
+ .thenReturn(false);
+ assertThat(mBluetoothGattWrapper
+ .setCharacteristicNotification(mBluetoothGattCharacteristic, true)).isFalse();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testDisconnect_callsWrapped() {
+ doNothing().when(mBluetoothGatt).disconnect();
+ mBluetoothGattWrapper.disconnect();
+ verify(mBluetoothGatt).disconnect();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testClose_callsWrapped() {
+ doNothing().when(mBluetoothGatt).close();
+ mBluetoothGattWrapper.close();
+ verify(mBluetoothGatt).close();
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothAdvertiserTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothAdvertiserTest.java
new file mode 100644
index 0000000..8468ed1
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothAdvertiserTest.java
@@ -0,0 +1,101 @@
+/*
+ * 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.common.bluetooth.testability.android.bluetooth.le;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.verify;
+
+import android.bluetooth.le.AdvertiseCallback;
+import android.bluetooth.le.AdvertiseData;
+import android.bluetooth.le.AdvertiseSettings;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Unit tests for {@link BluetoothLeAdvertiser}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BluetoothAdvertiserTest {
+ @Mock android.bluetooth.le.BluetoothLeAdvertiser mWrappedBluetoothLeAdvertiser;
+ @Mock AdvertiseSettings mAdvertiseSettings;
+ @Mock AdvertiseData mAdvertiseData;
+ @Mock AdvertiseCallback mAdvertiseCallback;
+
+ BluetoothLeAdvertiser mBluetoothLeAdvertiser;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mBluetoothLeAdvertiser = BluetoothLeAdvertiser.wrap(mWrappedBluetoothLeAdvertiser);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testWrapNullAdapter_isNull() {
+ assertThat(BluetoothLeAdvertiser.wrap(null)).isNull();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testWrapNonNullAdapter_isNotNull_unWrapSame() {
+ assertThat(mWrappedBluetoothLeAdvertiser).isNotNull();
+ assertThat(mBluetoothLeAdvertiser.unwrap()).isSameInstanceAs(mWrappedBluetoothLeAdvertiser);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testStartAdvertisingThreeParameters_callsWrapped() {
+ doNothing().when(mWrappedBluetoothLeAdvertiser)
+ .startAdvertising(mAdvertiseSettings, mAdvertiseData, mAdvertiseCallback);
+ mBluetoothLeAdvertiser
+ .startAdvertising(mAdvertiseSettings, mAdvertiseData, mAdvertiseCallback);
+ verify(mWrappedBluetoothLeAdvertiser).startAdvertising(
+ mAdvertiseSettings, mAdvertiseData, mAdvertiseCallback);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testStartAdvertisingFourParameters_callsWrapped() {
+ doNothing().when(mWrappedBluetoothLeAdvertiser).startAdvertising(
+ mAdvertiseSettings, mAdvertiseData, mAdvertiseData, mAdvertiseCallback);
+ mBluetoothLeAdvertiser.startAdvertising(
+ mAdvertiseSettings, mAdvertiseData, mAdvertiseData, mAdvertiseCallback);
+ verify(mWrappedBluetoothLeAdvertiser).startAdvertising(
+ mAdvertiseSettings, mAdvertiseData, mAdvertiseData, mAdvertiseCallback);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testStopAdvertising_callsWrapped() {
+ doNothing().when(mWrappedBluetoothLeAdvertiser).stopAdvertising(mAdvertiseCallback);
+ mBluetoothLeAdvertiser.stopAdvertising(mAdvertiseCallback);
+ verify(mWrappedBluetoothLeAdvertiser).stopAdvertising(mAdvertiseCallback);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeScannerTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeScannerTest.java
new file mode 100644
index 0000000..3fce54f
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/BluetoothLeScannerTest.java
@@ -0,0 +1,123 @@
+/*
+ * 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.common.bluetooth.testability.android.bluetooth.le;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.PendingIntent;
+import android.bluetooth.le.ScanFilter;
+import android.bluetooth.le.ScanSettings;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SdkSuppress;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Unit tests for {@link BluetoothLeScanner}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class BluetoothLeScannerTest {
+ @Mock android.bluetooth.le.BluetoothLeScanner mWrappedBluetoothLeScanner;
+ @Mock PendingIntent mPendingIntent;
+ @Mock ScanSettings mScanSettings;
+ @Mock ScanFilter mScanFilter;
+
+ TestScanCallback mTestScanCallback = new TestScanCallback();
+ BluetoothLeScanner mBluetoothLeScanner;
+ ImmutableList<ScanFilter> mImmutableScanFilterList;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mBluetoothLeScanner = BluetoothLeScanner.wrap(mWrappedBluetoothLeScanner);
+ mImmutableScanFilterList = ImmutableList.of(mScanFilter);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testWrapNullAdapter_isNull() {
+ assertThat(BluetoothLeAdvertiser.wrap(null)).isNull();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testWrapNonNullAdapter_isNotNull_unWrapSame() {
+ assertThat(mWrappedBluetoothLeScanner).isNotNull();
+ assertThat(mBluetoothLeScanner.unwrap()).isSameInstanceAs(mWrappedBluetoothLeScanner);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testStartScan_callsWrapped() {
+ doNothing().when(mWrappedBluetoothLeScanner).startScan(mTestScanCallback.unwrap());
+ mBluetoothLeScanner.startScan(mTestScanCallback);
+ verify(mWrappedBluetoothLeScanner).startScan(mTestScanCallback.unwrap());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testStartScanWithFiltersCallback_callsWrapped() {
+ doNothing().when(mWrappedBluetoothLeScanner)
+ .startScan(mImmutableScanFilterList, mScanSettings, mTestScanCallback.unwrap());
+ mBluetoothLeScanner.startScan(mImmutableScanFilterList, mScanSettings, mTestScanCallback);
+ verify(mWrappedBluetoothLeScanner)
+ .startScan(mImmutableScanFilterList, mScanSettings, mTestScanCallback.unwrap());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testStartScanWithFiltersCallbackIntent_callsWrapped() {
+ when(mWrappedBluetoothLeScanner.startScan(
+ mImmutableScanFilterList, mScanSettings, mPendingIntent)).thenReturn(1);
+ mBluetoothLeScanner.startScan(mImmutableScanFilterList, mScanSettings, mPendingIntent);
+ verify(mWrappedBluetoothLeScanner)
+ .startScan(mImmutableScanFilterList, mScanSettings, mPendingIntent);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testStopScan_callsWrapped() {
+ doNothing().when(mWrappedBluetoothLeScanner).stopScan(mTestScanCallback.unwrap());
+ mBluetoothLeScanner.stopScan(mTestScanCallback);
+ verify(mWrappedBluetoothLeScanner).stopScan(mTestScanCallback.unwrap());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testStopScanPendingIntent_callsWrapped() {
+ doNothing().when(mWrappedBluetoothLeScanner).stopScan(mPendingIntent);
+ mBluetoothLeScanner.stopScan(mPendingIntent);
+ verify(mWrappedBluetoothLeScanner).stopScan(mPendingIntent);
+ }
+
+ private static class TestScanCallback extends ScanCallback {};
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/ScanCallbackTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/ScanCallbackTest.java
new file mode 100644
index 0000000..6d68486
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/ScanCallbackTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.common.bluetooth.testability.android.bluetooth.le;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Unit tests for {@link ScanCallback}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ScanCallbackTest {
+ @Mock android.bluetooth.le.ScanResult mScanResult;
+
+ TestScanCallback mTestScanCallback = new TestScanCallback();
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @Test
+ public void testOnScanFailed_notCrash() {
+ mTestScanCallback.unwrap().onScanFailed(1);
+ }
+
+ @Test
+ public void testOnScanResult_notCrash() {
+ mTestScanCallback.unwrap().onScanResult(1, mScanResult);
+ }
+
+ @Test
+ public void testOnBatchScanResult_notCrash() {
+ mTestScanCallback.unwrap().onBatchScanResults(ImmutableList.of(mScanResult));
+ }
+
+ private static class TestScanCallback extends ScanCallback { }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/ScanResultTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/ScanResultTest.java
new file mode 100644
index 0000000..255c178
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/testability/android/bluetooth/le/ScanResultTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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.common.bluetooth.testability.android.bluetooth.le;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Unit tests for {@link ScanResult}.
+ */
+@Presubmit
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ScanResultTest {
+
+ @Mock android.bluetooth.le.ScanResult mWrappedScanResult;
+ @Mock android.bluetooth.le.ScanRecord mScanRecord;
+ @Mock android.bluetooth.BluetoothDevice mBluetoothDevice;
+ ScanResult mScanResult;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mScanResult = ScanResult.wrap(mWrappedScanResult);
+ }
+
+ @Test
+ public void testGetScanRecord_calledWrapped() {
+ when(mWrappedScanResult.getScanRecord()).thenReturn(mScanRecord);
+ assertThat(mScanResult.getScanRecord()).isSameInstanceAs(mScanRecord);
+ }
+
+ @Test
+ public void testGetRssi_calledWrapped() {
+ when(mWrappedScanResult.getRssi()).thenReturn(3);
+ assertThat(mScanResult.getRssi()).isEqualTo(3);
+ }
+
+ @Test
+ public void testGetTimestampNanos_calledWrapped() {
+ when(mWrappedScanResult.getTimestampNanos()).thenReturn(4L);
+ assertThat(mScanResult.getTimestampNanos()).isEqualTo(4L);
+ }
+
+ @Test
+ public void testGetDevice_calledWrapped() {
+ when(mWrappedScanResult.getDevice()).thenReturn(mBluetoothDevice);
+ assertThat(mScanResult.getDevice().unwrap()).isSameInstanceAs(mBluetoothDevice);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/util/BluetoothGattUtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/util/BluetoothGattUtilsTest.java
index 47182c3..7535c06 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/util/BluetoothGattUtilsTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/bluetooth/util/BluetoothGattUtilsTest.java
@@ -34,14 +34,12 @@
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
-import java.util.UUID;
/** Unit tests for {@link BluetoothGattUtils}. */
@Presubmit
@SmallTest
@RunWith(AndroidJUnit4.class)
public class BluetoothGattUtilsTest {
- private static final UUID TEST_UUID = UUID.randomUUID();
private static final ImmutableSet<String> GATT_HIDDEN_CONSTANTS = ImmutableSet.of(
"GATT_WRITE_REQUEST_BUSY", "GATT_WRITE_REQUEST_FAIL", "GATT_WRITE_REQUEST_SUCCESS");
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/eventloop/EventLoopTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/eventloop/EventLoopTest.java
index 70dcec8..bebb2f2 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/common/eventloop/EventLoopTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/eventloop/EventLoopTest.java
@@ -36,7 +36,6 @@
@Rule
public ExpectedException thrown = ExpectedException.none();
- /*
@Test
public void remove() {
mEventLoop.postRunnable(new NumberedRunnable(0));
@@ -44,10 +43,8 @@
mEventLoop.postRunnable(runnableToAddAndRemove);
mEventLoop.removeRunnable(runnableToAddAndRemove);
mEventLoop.postRunnable(new NumberedRunnable(2));
-
- assertThat(mExecutedRunnables).containsExactly(0, 2);
+ assertThat(mExecutedRunnables).doesNotContain(1);
}
- */
@Test
@SdkSuppress(minSdkVersion = 32, codeName = "T")
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/eventloop/HandlerEventLoopImplTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/eventloop/HandlerEventLoopImplTest.java
new file mode 100644
index 0000000..4775456
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/eventloop/HandlerEventLoopImplTest.java
@@ -0,0 +1,92 @@
+/*
+ * 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.common.eventloop;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class HandlerEventLoopImplTest {
+ private static final String TAG = "HandlerEventLoopImplTest";
+ private final HandlerEventLoopImpl mHandlerEventLoopImpl =
+ new HandlerEventLoopImpl(TAG);
+ private final List<Integer> mExecutedRunnables = new ArrayList<>();
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void remove() {
+ mHandlerEventLoopImpl.postRunnable(new NumberedRunnable(0));
+ NumberedRunnable runnableToAddAndRemove = new NumberedRunnable(1);
+ mHandlerEventLoopImpl.postRunnable(runnableToAddAndRemove);
+ mHandlerEventLoopImpl.removeRunnable(runnableToAddAndRemove);
+ mHandlerEventLoopImpl.postRunnable(new NumberedRunnable(2));
+ assertThat(mExecutedRunnables).doesNotContain(1);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void isPosted() {
+ NumberedRunnable runnable = new HandlerEventLoopImplTest.NumberedRunnable(0);
+ mHandlerEventLoopImpl.postRunnableDelayed(runnable, 10 * 1000L);
+ assertThat(mHandlerEventLoopImpl.isPosted(runnable)).isTrue();
+ mHandlerEventLoopImpl.removeRunnable(runnable);
+ assertThat(mHandlerEventLoopImpl.isPosted(runnable)).isFalse();
+
+ // Let a runnable execute, then verify that it's not posted.
+ mHandlerEventLoopImpl.postRunnable(runnable);
+ assertThat(mHandlerEventLoopImpl.isPosted(runnable)).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void postAndWaitAfterDestroy() throws InterruptedException {
+ mHandlerEventLoopImpl.destroy();
+ mHandlerEventLoopImpl.postAndWait(new HandlerEventLoopImplTest.NumberedRunnable(0));
+ assertThat(mExecutedRunnables).isEmpty();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void postEmptyQueueRunnable() {
+ mHandlerEventLoopImpl.postEmptyQueueRunnable(
+ new HandlerEventLoopImplTest.NumberedRunnable(0));
+ }
+
+ private class NumberedRunnable extends NamedRunnable {
+ private final int mId;
+
+ private NumberedRunnable(int id) {
+ super("NumberedRunnable:" + id);
+ this.mId = id;
+ }
+
+ @Override
+ public void run() {
+ // Note: when running in robolectric, this is not actually executed on a different
+ // thread, it's executed in the same thread the test runs in, so this is safe.
+ mExecutedRunnables.add(mId);
+ }
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/eventloop/NamedRunnableTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/eventloop/NamedRunnableTest.java
new file mode 100644
index 0000000..7005da1
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/eventloop/NamedRunnableTest.java
@@ -0,0 +1,36 @@
+/*
+ * 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.common.eventloop;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class NamedRunnableTest {
+ private static final String TAG = "NamedRunnableTest";
+
+ @Test
+ public void testToString() {
+ assertThat(mNamedRunnable.toString()).isEqualTo("Runnable[" + TAG + "]");
+ }
+
+ private final NamedRunnable mNamedRunnable = new NamedRunnable(TAG) {
+ @Override
+ public void run() {
+ }
+ };
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/fastpair/IconUtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/fastpair/IconUtilsTest.java
new file mode 100644
index 0000000..d39d9cc
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/fastpair/IconUtilsTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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.common.fastpair;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+
+import org.junit.Test;
+import org.mockito.Mock;
+
+public class IconUtilsTest {
+ private static final int MIN_ICON_SIZE = 16;
+ private static final int DESIRED_ICON_SIZE = 32;
+ @Mock
+ Context mContext;
+
+ @Test
+ public void isIconSizedCorrectly() {
+ // Null bitmap is not sized correctly
+ assertThat(IconUtils.isIconSizeCorrect(null)).isFalse();
+
+ int minIconSize = MIN_ICON_SIZE;
+ int desiredIconSize = DESIRED_ICON_SIZE;
+
+ // Bitmap that is 1x1 pixels is not sized correctly
+ Bitmap icon = Bitmap.createBitmap(1, 1, Bitmap.Config.ALPHA_8);
+ assertThat(IconUtils.isIconSizeCorrect(icon)).isFalse();
+
+ // Bitmap is categorized as small, and not regular
+ icon = Bitmap.createBitmap(minIconSize + 1,
+ minIconSize + 1, Bitmap.Config.ALPHA_8);
+ assertThat(IconUtils.isIconSizeCorrect(icon)).isTrue();
+ assertThat(IconUtils.isIconSizedSmall(icon)).isTrue();
+ assertThat(IconUtils.isIconSizedRegular(icon)).isFalse();
+
+ // Bitmap is categorized as regular, but not small
+ icon = Bitmap.createBitmap(desiredIconSize + 1,
+ desiredIconSize + 1, Bitmap.Config.ALPHA_8);
+ assertThat(IconUtils.isIconSizeCorrect(icon)).isTrue();
+ assertThat(IconUtils.isIconSizedSmall(icon)).isFalse();
+ assertThat(IconUtils.isIconSizedRegular(icon)).isTrue();
+ }
+
+ @Test
+ public void testAddWhiteCircleBackground() {
+ int minIconSize = MIN_ICON_SIZE;
+ Bitmap icon = Bitmap.createBitmap(minIconSize + 1, minIconSize + 1,
+ Bitmap.Config.ALPHA_8);
+
+ assertThat(
+ IconUtils.isIconSizeCorrect(IconUtils.addWhiteCircleBackground(mContext, icon)))
+ .isTrue();
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/locator/LocatorTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/locator/LocatorTest.java
new file mode 100644
index 0000000..c3a4e55
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/locator/LocatorTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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.common.locator;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import com.android.server.nearby.common.eventloop.EventLoop;
+import com.android.server.nearby.fastpair.FastPairAdvHandler;
+import com.android.server.nearby.fastpair.FastPairModule;
+import com.android.server.nearby.fastpair.cache.FastPairCacheManager;
+import com.android.server.nearby.fastpair.footprint.FootprintsDeviceManager;
+import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.MockitoAnnotations;
+
+import java.time.Clock;
+
+import src.com.android.server.nearby.fastpair.testing.MockingLocator;
+
+public class LocatorTest {
+ private Locator mLocator;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mLocator = src.com.android.server.nearby.fastpair.testing.MockingLocator.withMocksOnly(
+ ApplicationProvider.getApplicationContext());
+ mLocator.bind(new FastPairModule());
+ }
+
+ @Test
+ public void genericConstructor() {
+ assertThat(mLocator.get(FastPairCacheManager.class)).isNotNull();
+ assertThat(mLocator.get(FootprintsDeviceManager.class)).isNotNull();
+ assertThat(mLocator.get(EventLoop.class)).isNotNull();
+ assertThat(mLocator.get(FastPairHalfSheetManager.class)).isNotNull();
+ assertThat(mLocator.get(FastPairAdvHandler.class)).isNotNull();
+ assertThat(mLocator.get(Clock.class)).isNotNull();
+ }
+
+ @Test
+ public void genericDestroy() {
+ mLocator.destroy();
+ }
+
+ @Test
+ public void getOptional() {
+ assertThat(mLocator.getOptional(FastPairModule.class)).isNotNull();
+ mLocator.removeBindingForTest(FastPairModule.class);
+ assertThat(mLocator.getOptional(FastPairModule.class)).isNull();
+ }
+
+ @Test
+ public void getParent() {
+ assertThat(mLocator.getParent()).isNotNull();
+ }
+
+ @Test
+ public void getUnboundErrorMessage() {
+ assertThat(mLocator.getUnboundErrorMessage(FastPairModule.class))
+ .isEqualTo(
+ "Unbound type: com.android.server.nearby.fastpair.FastPairModule\n"
+ + "Searched locators:\n" + "android.app.Application ->\n"
+ + "android.app.Application ->\n" + "android.app.Application");
+ }
+
+ @Test
+ public void getContextForTest() {
+ src.com.android.server.nearby.fastpair.testing.MockingLocator mockingLocator =
+ new MockingLocator(ApplicationProvider.getApplicationContext(), mLocator);
+ assertThat(mockingLocator.getContextForTest()).isNotNull();
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/common/servicemonitor/PackageWatcherTest.java b/nearby/tests/unit/src/com/android/server/nearby/common/servicemonitor/PackageWatcherTest.java
new file mode 100644
index 0000000..eafc7db
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/common/servicemonitor/PackageWatcherTest.java
@@ -0,0 +1,45 @@
+/*
+ * 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.common.servicemonitor;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.content.Intent;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Test;
+
+public class PackageWatcherTest {
+ private PackageWatcher mPackageWatcher = new PackageWatcher() {
+ @Override
+ public void onSomePackagesChanged() {
+ }
+ };
+
+ @Test
+ public void getPackageName() {
+ Intent intent = new Intent("Action", null);
+ assertThat(mPackageWatcher.getPackageName(intent)).isNull();
+ }
+
+ @Test
+ public void onReceive() {
+ Intent intent = new Intent(Intent.ACTION_PACKAGES_UNSUSPENDED, null);
+ mPackageWatcher.onReceive(ApplicationProvider.getApplicationContext(), intent);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairAdvHandlerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairAdvHandlerTest.java
index 346a961..39ea5a9 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairAdvHandlerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/FastPairAdvHandlerTest.java
@@ -16,6 +16,8 @@
package com.android.server.nearby.fastpair;
+import static com.google.common.truth.Truth.assertThat;
+
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@@ -24,16 +26,22 @@
import android.content.Context;
import android.nearby.FastPairDevice;
+import com.android.server.nearby.common.bloomfilter.BloomFilter;
import com.android.server.nearby.common.locator.LocatorContextWrapper;
import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
import com.android.server.nearby.fastpair.notification.FastPairNotificationManager;
import com.android.server.nearby.provider.FastPairDataProvider;
+import com.google.common.collect.ImmutableList;
+import com.google.protobuf.ByteString;
+
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import service.proto.Cache;
+import service.proto.Data;
import service.proto.Rpcs;
public class FastPairAdvHandlerTest {
@@ -45,11 +53,21 @@
private FastPairHalfSheetManager mFastPairHalfSheetManager;
@Mock
private FastPairNotificationManager mFastPairNotificationManager;
+ @Mock
+ private BloomFilter mBloomFilter;
+ @Mock
+ Cache.StoredDiscoveryItem mStoredDiscoveryItem;
+ @Mock
+ Cache.StoredFastPairItem mStoredFastPairItem;
+ @Mock
+ Data.FastPairDeviceWithAccountKey mFastPairDeviceWithAccountKey;
+ private static final byte[] ACCOUNT_KEY = new byte[] {0, 1, 2};
private static final String BLUETOOTH_ADDRESS = "AA:BB:CC:DD";
private static final int CLOSE_RSSI = -80;
private static final int FAR_AWAY_RSSI = -120;
private static final int TX_POWER = -70;
private static final byte[] INITIAL_BYTE_ARRAY = new byte[]{0x01, 0x02, 0x03};
+ private static final byte[] SALT = new byte[]{0x01};
LocatorContextWrapper mLocatorContextWrapper;
FastPairAdvHandler mFastPairAdvHandler;
@@ -99,7 +117,7 @@
}
@Test
- public void testSubsequentBroadcast() {
+ public void testSubsequentBroadcast_notShowHalfSheet() {
byte[] fastPairRecordWithBloomFilter =
new byte[]{
(byte) 0x02,
@@ -132,4 +150,44 @@
verify(mFastPairHalfSheetManager, never()).showHalfSheet(any());
}
+
+ @Test
+ public void testFindRecognizedDevice_bloomFilterNotContains_notFound() {
+ when(mFastPairDeviceWithAccountKey.getAccountKey())
+ .thenReturn(ByteString.copyFrom(ACCOUNT_KEY), ByteString.copyFrom(ACCOUNT_KEY));
+ when(mBloomFilter.possiblyContains(any(byte[].class))).thenReturn(false);
+
+ assertThat(FastPairAdvHandler.findRecognizedDevice(
+ ImmutableList.of(mFastPairDeviceWithAccountKey), mBloomFilter, SALT)).isNull();
+ }
+
+ @Test
+ public void testFindRecognizedDevice_bloomFilterContains_found() {
+ when(mFastPairDeviceWithAccountKey.getAccountKey())
+ .thenReturn(ByteString.copyFrom(ACCOUNT_KEY), ByteString.copyFrom(ACCOUNT_KEY));
+ when(mBloomFilter.possiblyContains(any(byte[].class))).thenReturn(true);
+
+ assertThat(FastPairAdvHandler.findRecognizedDevice(
+ ImmutableList.of(mFastPairDeviceWithAccountKey), mBloomFilter, SALT)).isNotNull();
+ }
+
+ @Test
+ public void testFindRecognizedDeviceFromCachedItem_bloomFilterNotContains_notFound() {
+ when(mStoredFastPairItem.getAccountKey())
+ .thenReturn(ByteString.copyFrom(ACCOUNT_KEY), ByteString.copyFrom(ACCOUNT_KEY));
+ when(mBloomFilter.possiblyContains(any(byte[].class))).thenReturn(false);
+
+ assertThat(FastPairAdvHandler.findRecognizedDeviceFromCachedItem(
+ ImmutableList.of(mStoredFastPairItem), mBloomFilter, SALT)).isNull();
+ }
+
+ @Test
+ public void testFindRecognizedDeviceFromCachedItem_bloomFilterContains_found() {
+ when(mStoredFastPairItem.getAccountKey())
+ .thenReturn(ByteString.copyFrom(ACCOUNT_KEY), ByteString.copyFrom(ACCOUNT_KEY));
+ when(mBloomFilter.possiblyContains(any(byte[].class))).thenReturn(true);
+
+ assertThat(FastPairAdvHandler.findRecognizedDeviceFromCachedItem(
+ ImmutableList.of(mStoredFastPairItem), mBloomFilter, SALT)).isNotNull();
+ }
}
diff --git a/Cronet/tests/cts/src/android/net/http/cts/util/CronetCtsTestServer.kt b/nearby/tests/unit/src/com/android/server/nearby/fastpair/FlagUtilsTest.java
similarity index 64%
copy from Cronet/tests/cts/src/android/net/http/cts/util/CronetCtsTestServer.kt
copy to nearby/tests/unit/src/com/android/server/nearby/fastpair/FlagUtilsTest.java
index 3ccb571..9cf65f4 100644
--- a/Cronet/tests/cts/src/android/net/http/cts/util/CronetCtsTestServer.kt
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/FlagUtilsTest.java
@@ -14,13 +14,14 @@
* limitations under the License.
*/
-package android.net.http.cts.util
+package com.android.server.nearby.fastpair;
-import android.content.Context
-import android.webkit.cts.CtsTestServer
+import org.junit.Test;
-/** Extends CtsTestServer to handle POST requests and other cronet specific test requests */
-class CronetCtsTestServer(context: Context) : CtsTestServer(context) {
+public class FlagUtilsTest {
- val successUrl: String = getAssetUrl("html/hello_world.html")
+ @Test
+ public void testGetPreferencesBuilder_notCrash() {
+ FlagUtils.getPreferencesBuilder().build();
+ }
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/DiscoveryItemTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/DiscoveryItemTest.java
new file mode 100644
index 0000000..5d4ea22
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/DiscoveryItemTest.java
@@ -0,0 +1,234 @@
+/*
+ * 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.fastpair.cache;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.server.nearby.common.locator.LocatorContextWrapper;
+import com.android.server.nearby.fastpair.FastPairManager;
+import com.android.server.nearby.fastpair.testing.FakeDiscoveryItems;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import service.proto.Cache;
+
+/** Unit tests for {@link DiscoveryItem} */
+public class DiscoveryItemTest {
+ private static final String DEFAULT_MAC_ADDRESS = "00:11:22:33:44:55";
+ private static final String DEFAULT_DESCRIPITON = "description";
+ private static final long DEFAULT_TIMESTAMP = 1000000000L;
+ private static final String DEFAULT_TITLE = "title";
+ private static final String APP_NAME = "app_name";
+ private static final String ACTION_URL =
+ "intent:#Intent;action=com.android.server.nearby:ACTION_FAST_PAIR;"
+ + "package=com.google.android.gms;"
+ + "component=com.google.android.gms/"
+ + ".nearby.discovery.service.DiscoveryService;end";
+ private static final String DISPLAY_URL = "DISPLAY_URL";
+ private static final String TRIGGER_ID = "trigger.id";
+ private static final String FAST_PAIR_ID = "id";
+ private static final int RSSI = -80;
+ private static final int TX_POWER = -10;
+
+ @Mock private Context mContext;
+ private LocatorContextWrapper mLocatorContextWrapper;
+ private FastPairCacheManager mFastPairCacheManager;
+ private FastPairManager mFastPairManager;
+ private DiscoveryItem mDiscoveryItem;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mLocatorContextWrapper = new LocatorContextWrapper(mContext);
+ mFastPairManager = new FastPairManager(mLocatorContextWrapper);
+ mFastPairCacheManager = mLocatorContextWrapper.getLocator().get(FastPairCacheManager.class);
+ when(mContext.getContentResolver()).thenReturn(
+ InstrumentationRegistry.getInstrumentation().getContext().getContentResolver());
+ mDiscoveryItem =
+ FakeDiscoveryItems.newFastPairDiscoveryItem(mLocatorContextWrapper);
+ }
+
+ @Test
+ public void testMultipleFields() {
+ assertThat(mDiscoveryItem.getId()).isEqualTo(FAST_PAIR_ID);
+ assertThat(mDiscoveryItem.getDescription()).isEqualTo(DEFAULT_DESCRIPITON);
+ assertThat(mDiscoveryItem.getDisplayUrl()).isEqualTo(DISPLAY_URL);
+ assertThat(mDiscoveryItem.getTriggerId()).isEqualTo(TRIGGER_ID);
+ assertThat(mDiscoveryItem.getMacAddress()).isEqualTo(DEFAULT_MAC_ADDRESS);
+ assertThat(
+ mDiscoveryItem.getFirstObservationTimestampMillis()).isEqualTo(DEFAULT_TIMESTAMP);
+ assertThat(
+ mDiscoveryItem.getLastObservationTimestampMillis()).isEqualTo(DEFAULT_TIMESTAMP);
+ assertThat(mDiscoveryItem.getActionUrl()).isEqualTo(ACTION_URL);
+ assertThat(mDiscoveryItem.getAppName()).isEqualTo(APP_NAME);
+ assertThat(mDiscoveryItem.getRssi()).isEqualTo(RSSI);
+ assertThat(mDiscoveryItem.getTxPower()).isEqualTo(TX_POWER);
+ assertThat(mDiscoveryItem.getFastPairInformation()).isNull();
+ assertThat(mDiscoveryItem.getFastPairSecretKey()).isNull();
+ assertThat(mDiscoveryItem.getIcon()).isNull();
+ assertThat(mDiscoveryItem.getIconFifeUrl()).isNotNull();
+ assertThat(mDiscoveryItem.getState()).isNotNull();
+ assertThat(mDiscoveryItem.getTitle()).isNotNull();
+ assertThat(mDiscoveryItem.isApp()).isFalse();
+ assertThat(mDiscoveryItem.isDeletable(
+ 100000L, 0L)).isTrue();
+ assertThat(mDiscoveryItem.isDeviceType(Cache.NearbyType.NEARBY_CHROMECAST)).isTrue();
+ assertThat(mDiscoveryItem.isExpired(
+ 100000L, 0L)).isTrue();
+ assertThat(mDiscoveryItem.isFastPair()).isTrue();
+ assertThat(mDiscoveryItem.isPendingAppInstallValid(5)).isTrue();
+ assertThat(mDiscoveryItem.isPendingAppInstallValid(5,
+ FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID, null,
+ TRIGGER_ID, DEFAULT_MAC_ADDRESS, "", RSSI, TX_POWER))).isTrue();
+ assertThat(mDiscoveryItem.isTypeEnabled(Cache.NearbyType.NEARBY_CHROMECAST)).isTrue();
+ assertThat(mDiscoveryItem.toString()).isNotNull();
+ }
+
+ @Test
+ public void isMuted() {
+ assertThat(mDiscoveryItem.isMuted()).isFalse();
+ }
+
+ @Test
+ public void itemWithDefaultDescription_shouldShowUp() {
+ assertThat(mDiscoveryItem.isReadyForDisplay()).isFalse();
+
+ // Null description should not show up.
+ mDiscoveryItem.setStoredItemForTest(DiscoveryItem.newStoredDiscoveryItem());
+ mDiscoveryItem.setStoredItemForTest(
+ FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID, null,
+ TRIGGER_ID, DEFAULT_MAC_ADDRESS, "", RSSI, TX_POWER));
+ assertThat(mDiscoveryItem.isReadyForDisplay()).isFalse();
+
+ // Empty description should not show up.
+ mDiscoveryItem.setStoredItemForTest(
+ FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID, "",
+ TRIGGER_ID, DEFAULT_MAC_ADDRESS, DEFAULT_TITLE, RSSI, TX_POWER));
+ assertThat(mDiscoveryItem.isReadyForDisplay()).isFalse();
+ }
+
+ @Test
+ public void itemWithEmptyTitle_shouldNotShowUp() {
+ // Null title should not show up.
+ assertThat(mDiscoveryItem.isReadyForDisplay()).isFalse();
+ // Empty title should not show up.
+ mDiscoveryItem.setStoredItemForTest(
+ FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID, DEFAULT_DESCRIPITON,
+ TRIGGER_ID, DEFAULT_MAC_ADDRESS, "", RSSI, TX_POWER));
+ assertThat(mDiscoveryItem.isReadyForDisplay()).isFalse();
+
+ // Null title should not show up.
+ mDiscoveryItem.setStoredItemForTest(
+ FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID, DEFAULT_DESCRIPITON,
+ TRIGGER_ID, DEFAULT_MAC_ADDRESS, null, RSSI, TX_POWER));
+ assertThat(mDiscoveryItem.isReadyForDisplay()).isFalse();
+ }
+
+ @Test
+ public void itemWithRssiAndTxPower_shouldHaveCorrectEstimatedDistance() {
+ assertThat(mDiscoveryItem.getEstimatedDistance()).isWithin(0.01).of(28.18);
+ }
+
+ @Test
+ public void itemWithoutRssiOrTxPower_shouldNotHaveEstimatedDistance() {
+ mDiscoveryItem.setStoredItemForTest(
+ FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID, DEFAULT_DESCRIPITON,
+ TRIGGER_ID, DEFAULT_MAC_ADDRESS, "", 0, 0));
+ assertThat(mDiscoveryItem.getEstimatedDistance()).isWithin(0.01).of(0);
+ }
+
+ @Test
+ public void getUiHashCode_differentAddress_differentHash() {
+ mDiscoveryItem.setStoredItemForTest(
+ FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID, DEFAULT_DESCRIPITON,
+ TRIGGER_ID, "00:11:22:33:44:55", "", RSSI, TX_POWER));
+ DiscoveryItem compareTo =
+ FakeDiscoveryItems.newFastPairDiscoveryItem(mLocatorContextWrapper);
+ compareTo.setStoredItemForTest(
+ FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID, DEFAULT_DESCRIPITON,
+ TRIGGER_ID, "55:44:33:22:11:00", "", RSSI, TX_POWER));
+ assertThat(mDiscoveryItem.getUiHashCode()).isNotEqualTo(compareTo.getUiHashCode());
+ }
+
+ @Test
+ public void getUiHashCode_sameAddress_sameHash() {
+ mDiscoveryItem.setStoredItemForTest(
+ FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID, DEFAULT_DESCRIPITON,
+ TRIGGER_ID, "00:11:22:33:44:55", "", RSSI, TX_POWER));
+ DiscoveryItem compareTo =
+ FakeDiscoveryItems.newFastPairDiscoveryItem(mLocatorContextWrapper);
+ compareTo.setStoredItemForTest(
+ FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID, DEFAULT_DESCRIPITON,
+ TRIGGER_ID, "00:11:22:33:44:55", "", RSSI, TX_POWER));
+ assertThat(mDiscoveryItem.getUiHashCode()).isEqualTo(compareTo.getUiHashCode());
+ }
+
+ @Test
+ public void isFastPair() {
+ DiscoveryItem fastPairItem =
+ FakeDiscoveryItems.newFastPairDiscoveryItem(mLocatorContextWrapper);
+ assertThat(fastPairItem.isFastPair()).isTrue();
+ }
+
+ @Test
+ public void testEqual() {
+ DiscoveryItem fastPairItem =
+ FakeDiscoveryItems.newFastPairDiscoveryItem(mLocatorContextWrapper);
+ assertThat(mDiscoveryItem.equals(fastPairItem)).isTrue();
+ }
+
+ @Test
+ public void testCompareTo() {
+ DiscoveryItem fastPairItem =
+ FakeDiscoveryItems.newFastPairDiscoveryItem(mLocatorContextWrapper);
+ assertThat(mDiscoveryItem.compareTo(fastPairItem)).isEqualTo(0);
+ }
+
+
+ @Test
+ public void testCopyOfStoredItem() {
+ DiscoveryItem fastPairItem =
+ FakeDiscoveryItems.newFastPairDiscoveryItem(mLocatorContextWrapper);
+ fastPairItem.setStoredItemForTest(
+ FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID, DEFAULT_DESCRIPITON,
+ TRIGGER_ID, "00:11:22:33:44:55", "", RSSI, TX_POWER));
+ assertThat(mDiscoveryItem.equals(fastPairItem)).isFalse();
+ fastPairItem.setStoredItemForTest(mDiscoveryItem.getCopyOfStoredItem());
+ assertThat(mDiscoveryItem.equals(fastPairItem)).isTrue();
+ }
+
+ @Test
+ public void testStoredItemForTest() {
+ DiscoveryItem fastPairItem =
+ FakeDiscoveryItems.newFastPairDiscoveryItem(mLocatorContextWrapper);
+ fastPairItem.setStoredItemForTest(
+ FakeDiscoveryItems.newFastPairDeviceStoredItem(FAST_PAIR_ID, DEFAULT_DESCRIPITON,
+ TRIGGER_ID, "00:11:22:33:44:55", "", RSSI, TX_POWER));
+ assertThat(mDiscoveryItem.equals(fastPairItem)).isFalse();
+ fastPairItem.setStoredItemForTest(mDiscoveryItem.getStoredItemForTest());
+ assertThat(mDiscoveryItem.equals(fastPairItem)).isTrue();
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/FastPairCacheManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/FastPairCacheManagerTest.java
index fdda6f7..18f2cf6 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/FastPairCacheManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/FastPairCacheManagerTest.java
@@ -20,6 +20,7 @@
import static org.mockito.Mockito.when;
+import android.bluetooth.le.ScanResult;
import android.content.Context;
import androidx.test.core.app.ApplicationProvider;
@@ -50,6 +51,10 @@
DiscoveryItem mDiscoveryItem2;
@Mock
Cache.StoredFastPairItem mStoredFastPairItem;
+ @Mock
+ ScanResult mScanResult;
+
+ Context mContext;
Cache.StoredDiscoveryItem mStoredDiscoveryItem = Cache.StoredDiscoveryItem.newBuilder()
.setTriggerId(MODEL_ID)
.setAppName(APP_NAME).build();
@@ -60,12 +65,12 @@
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
+ mContext = ApplicationProvider.getApplicationContext();
}
@Test
@SdkSuppress(minSdkVersion = 32, codeName = "T")
public void notSaveRetrieveInfo() {
- Context mContext = ApplicationProvider.getApplicationContext();
when(mDiscoveryItem.getCopyOfStoredItem()).thenReturn(mStoredDiscoveryItem);
when(mDiscoveryItem.getTriggerId()).thenReturn(MODEL_ID);
@@ -78,7 +83,6 @@
@Test
@SdkSuppress(minSdkVersion = 32, codeName = "T")
public void saveRetrieveInfo() {
- Context mContext = ApplicationProvider.getApplicationContext();
when(mDiscoveryItem.getCopyOfStoredItem()).thenReturn(mStoredDiscoveryItem);
when(mDiscoveryItem.getTriggerId()).thenReturn(MODEL_ID);
when(mDiscoveryItem2.getCopyOfStoredItem()).thenReturn(mStoredDiscoveryItem2);
@@ -100,7 +104,6 @@
@Test
@SdkSuppress(minSdkVersion = 32, codeName = "T")
public void saveRetrieveInfoStoredFastPairItem() {
- Context mContext = ApplicationProvider.getApplicationContext();
Cache.StoredFastPairItem storedFastPairItem = Cache.StoredFastPairItem.newBuilder()
.setMacAddress(MAC_ADDRESS)
.setAccountKey(ACCOUNT_KEY)
@@ -118,7 +121,6 @@
@Test
@SdkSuppress(minSdkVersion = 32, codeName = "T")
public void checkGetAllFastPairItems() {
- Context mContext = ApplicationProvider.getApplicationContext();
Cache.StoredFastPairItem storedFastPairItem = Cache.StoredFastPairItem.newBuilder()
.setMacAddress(MAC_ADDRESS)
.setAccountKey(ACCOUNT_KEY)
@@ -139,5 +141,15 @@
assertThat(fastPairCacheManager.getAllSavedStoredFastPairItem().size())
.isEqualTo(1);
+
+ fastPairCacheManager.cleanUp();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void getDeviceFromScanResult_notCrash() {
+ FastPairCacheManager fastPairCacheManager = new FastPairCacheManager(mContext);
+ fastPairCacheManager.getDeviceFromScanResult(mScanResult);
+
}
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/FastPairDbHelperTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/FastPairDbHelperTest.java
new file mode 100644
index 0000000..c5428f5
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/cache/FastPairDbHelperTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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.fastpair.cache;
+
+import static org.junit.Assert.assertThrows;
+
+import android.content.Context;
+import android.database.sqlite.SQLiteException;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.MockitoAnnotations;
+
+public class FastPairDbHelperTest {
+
+ Context mContext;
+ FastPairDbHelper mFastPairDbHelper;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mContext = InstrumentationRegistry.getInstrumentation().getContext();
+ mFastPairDbHelper = new FastPairDbHelper(mContext);
+ }
+
+ @After
+ public void teardown() {
+ mFastPairDbHelper.close();
+ }
+
+ @Test
+ public void testUpgrade_notCrash() {
+ mFastPairDbHelper
+ .onUpgrade(mFastPairDbHelper.getWritableDatabase(), 1, 2);
+ }
+
+ @Test
+ public void testDowngrade_throwsException() {
+ assertThrows(
+ SQLiteException.class,
+ () -> mFastPairDbHelper.onDowngrade(
+ mFastPairDbHelper.getWritableDatabase(), 2, 1));
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManagerTest.java
index 58e4c47..b51a295 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/halfsheet/FastPairHalfSheetManagerTest.java
@@ -16,6 +16,8 @@
package com.android.server.nearby.fastpair.halfsheet;
+import static com.google.common.truth.Truth.assertThat;
+
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
@@ -25,6 +27,7 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
@@ -32,6 +35,8 @@
import android.content.pm.ResolveInfo;
import android.os.UserHandle;
+import androidx.test.platform.app.InstrumentationRegistry;
+
import com.android.server.nearby.common.locator.Locator;
import com.android.server.nearby.common.locator.LocatorContextWrapper;
import com.android.server.nearby.fastpair.FastPairController;
@@ -50,13 +55,13 @@
public class FastPairHalfSheetManagerTest {
private static final String BLEADDRESS = "11:22:44:66";
private static final String NAME = "device_name";
+ private static final int PASSKEY = 1234;
private FastPairHalfSheetManager mFastPairHalfSheetManager;
private Cache.ScanFastPairStoreItem mScanFastPairStoreItem;
+ @Mock private Context mContext;
@Mock
LocatorContextWrapper mContextWrapper;
@Mock
- ResolveInfo mResolveInfo;
- @Mock
PackageManager mPackageManager;
@Mock
Locator mLocator;
@@ -66,7 +71,8 @@
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
-
+ when(mContext.getContentResolver()).thenReturn(
+ InstrumentationRegistry.getInstrumentation().getContext().getContentResolver());
mScanFastPairStoreItem = Cache.ScanFastPairStoreItem.newBuilder()
.setAddress(BLEADDRESS)
.setDeviceName(NAME)
@@ -133,4 +139,24 @@
verify(mContextWrapper, never())
.startActivityAsUser(intentArgumentCaptor.capture(), eq(UserHandle.CURRENT));
}
+
+ @Test
+ public void getHalfSheetForegroundState() {
+ mFastPairHalfSheetManager =
+ new FastPairHalfSheetManager(mContextWrapper);
+ assertThat(mFastPairHalfSheetManager.getHalfSheetForegroundState()).isTrue();
+ }
+
+ @Test
+ public void testEmptyMethods() {
+ mFastPairHalfSheetManager =
+ new FastPairHalfSheetManager(mContextWrapper);
+ mFastPairHalfSheetManager.destroyBluetoothPairController();
+ mFastPairHalfSheetManager.disableDismissRunnable();
+ mFastPairHalfSheetManager.notifyPairingProcessDone(true, BLEADDRESS, null);
+ mFastPairHalfSheetManager.showPairingFailed();
+ mFastPairHalfSheetManager.showPairingHalfSheet(null);
+ mFastPairHalfSheetManager.showPairingSuccessHalfSheet(BLEADDRESS);
+ mFastPairHalfSheetManager.showPasskeyConfirmation(null, PASSKEY);
+ }
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationManagerTest.java
new file mode 100644
index 0000000..4fb6b37
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/notification/FastPairNotificationManagerTest.java
@@ -0,0 +1,76 @@
+/*
+ * 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.fastpair.notification;
+
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.server.nearby.common.locator.LocatorContextWrapper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class FastPairNotificationManagerTest {
+
+ @Mock private Context mContext;
+ private static final boolean USE_LARGE_ICON = true;
+ private static final int NOTIFICATION_ID = 1;
+ private static final String COMPANION_APP = "companionApp";
+ private static final int BATTERY_LEVEL = 1;
+ private static final String DEVICE_NAME = "deviceName";
+ private static final String ADDRESS = "address";
+ private FastPairNotificationManager mFastPairNotificationManager;
+ private LocatorContextWrapper mLocatorContextWrapper;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mLocatorContextWrapper = new LocatorContextWrapper(mContext);
+ when(mContext.getContentResolver()).thenReturn(
+ InstrumentationRegistry.getInstrumentation().getContext().getContentResolver());
+ mFastPairNotificationManager =
+ new FastPairNotificationManager(mLocatorContextWrapper, null,
+ USE_LARGE_ICON, NOTIFICATION_ID);
+ }
+
+ @Test
+ public void notifyPairingProcessDone() {
+ mFastPairNotificationManager.notifyPairingProcessDone(true, true,
+ "privateAddress", "publicAddress");
+ }
+
+ @Test
+ public void showConnectingNotification() {
+ mFastPairNotificationManager.showConnectingNotification();
+ }
+
+ @Test
+ public void showPairingFailedNotification() {
+ mFastPairNotificationManager.showPairingFailedNotification(new byte[]{1});
+ }
+
+ @Test
+ public void showPairingSucceededNotification() {
+ mFastPairNotificationManager.showPairingSucceededNotification(COMPANION_APP,
+ BATTERY_LEVEL, DEVICE_NAME, ADDRESS);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/HalfSheetPairingProgressHandlerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/HalfSheetPairingProgressHandlerTest.java
new file mode 100644
index 0000000..bfe009c
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/HalfSheetPairingProgressHandlerTest.java
@@ -0,0 +1,135 @@
+/*
+ * 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.fastpair.pairinghandler;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+
+import com.android.server.nearby.common.bluetooth.fastpair.FastPairConnection;
+import com.android.server.nearby.common.locator.Locator;
+import com.android.server.nearby.common.locator.LocatorContextWrapper;
+import com.android.server.nearby.fastpair.cache.DiscoveryItem;
+import com.android.server.nearby.fastpair.cache.FastPairCacheManager;
+import com.android.server.nearby.fastpair.footprint.FootprintsDeviceManager;
+import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
+import com.android.server.nearby.fastpair.testing.FakeDiscoveryItems;
+
+import com.google.protobuf.ByteString;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.time.Clock;
+
+import service.proto.Cache;
+import service.proto.Rpcs;
+
+public class HalfSheetPairingProgressHandlerTest {
+ @Mock
+ Locator mLocator;
+ @Mock
+ LocatorContextWrapper mContextWrapper;
+ @Mock
+ Clock mClock;
+ @Mock
+ FastPairCacheManager mFastPairCacheManager;
+ @Mock
+ FastPairConnection mFastPairConnection;
+ @Mock
+ FootprintsDeviceManager mFootprintsDeviceManager;
+
+ private static final String MAC_ADDRESS = "00:11:22:33:44:55";
+ private static final byte[] ACCOUNT_KEY = new byte[]{0x01, 0x02};
+ private static final int SUBSEQUENT_PAIR_START = 1310;
+ private static final int SUBSEQUENT_PAIR_END = 1320;
+ private static final int PASSKEY = 1234;
+ private static HalfSheetPairingProgressHandler sHalfSheetPairingProgressHandler;
+ private static DiscoveryItem sDiscoveryItem;
+ private static BluetoothDevice sBluetoothDevice;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ when(mContextWrapper.getLocator()).thenReturn(mLocator);
+ mLocator.overrideBindingForTest(FastPairCacheManager.class, mFastPairCacheManager);
+ mLocator.overrideBindingForTest(Clock.class, mClock);
+ FastPairHalfSheetManager mfastPairHalfSheetManager =
+ new FastPairHalfSheetManager(mContextWrapper);
+ mLocator.bind(FastPairHalfSheetManager.class, mfastPairHalfSheetManager);
+ when(mLocator.get(FastPairHalfSheetManager.class)).thenReturn(mfastPairHalfSheetManager);
+ sDiscoveryItem = FakeDiscoveryItems.newFastPairDiscoveryItem(mContextWrapper);
+ sDiscoveryItem.setStoredItemForTest(
+ sDiscoveryItem.getStoredItemForTest().toBuilder()
+ .setAuthenticationPublicKeySecp256R1(ByteString.copyFrom(ACCOUNT_KEY))
+ .setMacAddress(MAC_ADDRESS)
+ .setFastPairInformation(
+ Cache.FastPairInformation.newBuilder()
+ .setDeviceType(Rpcs.DeviceType.HEADPHONES).build())
+ .build());
+ sHalfSheetPairingProgressHandler =
+ new HalfSheetPairingProgressHandler(mContextWrapper, sDiscoveryItem,
+ sDiscoveryItem.getAppPackageName(), ACCOUNT_KEY);
+
+ sBluetoothDevice =
+ BluetoothAdapter.getDefaultAdapter().getRemoteDevice("00:11:22:33:44:55");
+ }
+
+ @Test
+ public void getPairEndEventCode() {
+ assertThat(sHalfSheetPairingProgressHandler
+ .getPairEndEventCode()).isEqualTo(SUBSEQUENT_PAIR_END);
+ }
+
+ @Test
+ public void getPairStartEventCode() {
+ assertThat(sHalfSheetPairingProgressHandler
+ .getPairStartEventCode()).isEqualTo(SUBSEQUENT_PAIR_START);
+ }
+
+ @Test
+ public void testOnHandlePasskeyConfirmation() {
+ sHalfSheetPairingProgressHandler.onHandlePasskeyConfirmation(sBluetoothDevice, PASSKEY);
+ }
+
+ @Test
+ public void testOnPairedCallbackCalled() {
+ sHalfSheetPairingProgressHandler.onPairedCallbackCalled(mFastPairConnection, ACCOUNT_KEY,
+ mFootprintsDeviceManager, MAC_ADDRESS);
+ }
+
+ @Test
+ public void testonPairingFailed() {
+ Throwable e = new Throwable("onPairingFailed");
+ sHalfSheetPairingProgressHandler.onPairingFailed(e);
+ }
+
+ @Test
+ public void testonPairingStarted() {
+ sHalfSheetPairingProgressHandler.onPairingStarted();
+ }
+
+ @Test
+ public void testonPairingSuccess() {
+ sHalfSheetPairingProgressHandler.onPairingSuccess(MAC_ADDRESS);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/NotificationPairingProgressHandlerTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/NotificationPairingProgressHandlerTest.java
new file mode 100644
index 0000000..24f296c
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/NotificationPairingProgressHandlerTest.java
@@ -0,0 +1,143 @@
+/*
+ * 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.fastpair.pairinghandler;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothManager;
+
+import androidx.annotation.Nullable;
+
+import com.android.server.nearby.common.bluetooth.fastpair.FastPairConnection;
+import com.android.server.nearby.common.locator.Locator;
+import com.android.server.nearby.common.locator.LocatorContextWrapper;
+import com.android.server.nearby.fastpair.cache.DiscoveryItem;
+import com.android.server.nearby.fastpair.cache.FastPairCacheManager;
+import com.android.server.nearby.fastpair.footprint.FootprintsDeviceManager;
+import com.android.server.nearby.fastpair.halfsheet.FastPairHalfSheetManager;
+import com.android.server.nearby.fastpair.notification.FastPairNotificationManager;
+import com.android.server.nearby.fastpair.testing.FakeDiscoveryItems;
+
+import com.google.protobuf.ByteString;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.time.Clock;
+
+import service.proto.Cache;
+import service.proto.Rpcs;
+
+public class NotificationPairingProgressHandlerTest {
+
+ @Mock
+ Locator mLocator;
+ @Mock
+ LocatorContextWrapper mContextWrapper;
+ @Mock
+ Clock mClock;
+ @Mock
+ FastPairCacheManager mFastPairCacheManager;
+ @Mock
+ FastPairConnection mFastPairConnection;
+ @Mock
+ FootprintsDeviceManager mFootprintsDeviceManager;
+ @Mock
+ android.bluetooth.BluetoothManager mBluetoothManager;
+
+ private static final String MAC_ADDRESS = "00:11:22:33:44:55";
+ private static final byte[] ACCOUNT_KEY = new byte[]{0x01, 0x02};
+ private static final int SUBSEQUENT_PAIR_START = 1310;
+ private static final int SUBSEQUENT_PAIR_END = 1320;
+ private static DiscoveryItem sDiscoveryItem;
+ private static NotificationPairingProgressHandler sNotificationPairingProgressHandler;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ when(mContextWrapper.getSystemService(BluetoothManager.class))
+ .thenReturn(mBluetoothManager);
+ when(mContextWrapper.getLocator()).thenReturn(mLocator);
+ mLocator.overrideBindingForTest(FastPairCacheManager.class,
+ mFastPairCacheManager);
+ mLocator.overrideBindingForTest(Clock.class, mClock);
+ sDiscoveryItem = FakeDiscoveryItems.newFastPairDiscoveryItem(mContextWrapper);
+ sDiscoveryItem.setStoredItemForTest(
+ sDiscoveryItem.getStoredItemForTest().toBuilder()
+ .setAuthenticationPublicKeySecp256R1(ByteString.copyFrom(ACCOUNT_KEY))
+ .setFastPairInformation(
+ Cache.FastPairInformation.newBuilder()
+ .setDeviceType(Rpcs.DeviceType.HEADPHONES).build())
+ .build());
+ sNotificationPairingProgressHandler = createProgressHandler(ACCOUNT_KEY, sDiscoveryItem);
+ }
+
+ @Test
+ public void getPairEndEventCode() {
+ assertThat(sNotificationPairingProgressHandler
+ .getPairEndEventCode()).isEqualTo(SUBSEQUENT_PAIR_END);
+ }
+
+ @Test
+ public void getPairStartEventCode() {
+ assertThat(sNotificationPairingProgressHandler
+ .getPairStartEventCode()).isEqualTo(SUBSEQUENT_PAIR_START);
+ }
+
+ @Test
+ public void onReadyToPair() {
+ sNotificationPairingProgressHandler.onReadyToPair();
+ }
+
+ @Test
+ public void onPairedCallbackCalled() {
+ sNotificationPairingProgressHandler.onPairedCallbackCalled(mFastPairConnection,
+ ACCOUNT_KEY, mFootprintsDeviceManager, MAC_ADDRESS);
+ }
+
+ @Test
+ public void onPairingFailed() {
+ Throwable e = new Throwable("Pairing Failed");
+ sNotificationPairingProgressHandler.onPairingFailed(e);
+ }
+
+ @Test
+ public void onPairingSuccess() {
+ sNotificationPairingProgressHandler.onPairingSuccess(sDiscoveryItem.getMacAddress());
+ }
+
+ private NotificationPairingProgressHandler createProgressHandler(
+ @Nullable byte[] accountKey, DiscoveryItem fastPairItem) {
+ FastPairNotificationManager fastPairNotificationManager =
+ new FastPairNotificationManager(mContextWrapper, fastPairItem, true);
+ FastPairHalfSheetManager fastPairHalfSheetManager =
+ new FastPairHalfSheetManager(mContextWrapper);
+ mLocator.overrideBindingForTest(FastPairHalfSheetManager.class, fastPairHalfSheetManager);
+ NotificationPairingProgressHandler mNotificationPairingProgressHandler =
+ new NotificationPairingProgressHandler(
+ mContextWrapper,
+ fastPairItem,
+ fastPairItem.getAppPackageName(),
+ accountKey,
+ fastPairNotificationManager);
+ return mNotificationPairingProgressHandler;
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBaseTest.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBaseTest.java
index 2ade5f2..a3eb50c 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBaseTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/pairinghandler/PairingProgressHandlerBaseTest.java
@@ -20,8 +20,14 @@
import static org.mockito.Mockito.when;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothManager;
+
import androidx.annotation.Nullable;
+import com.android.server.nearby.common.bluetooth.fastpair.FastPairConnection;
+import com.android.server.nearby.common.bluetooth.fastpair.Preferences;
import com.android.server.nearby.common.locator.Locator;
import com.android.server.nearby.common.locator.LocatorContextWrapper;
import com.android.server.nearby.fastpair.cache.DiscoveryItem;
@@ -42,8 +48,8 @@
import service.proto.Cache;
import service.proto.Rpcs;
-
public class PairingProgressHandlerBaseTest {
+
@Mock
Locator mLocator;
@Mock
@@ -54,24 +60,46 @@
FastPairCacheManager mFastPairCacheManager;
@Mock
FootprintsDeviceManager mFootprintsDeviceManager;
+ @Mock
+ FastPairConnection mFastPairConnection;
+ @Mock
+ BluetoothManager mBluetoothManager;
+
+ private static final String MAC_ADDRESS = "00:11:22:33:44:55";
private static final byte[] ACCOUNT_KEY = new byte[]{0x01, 0x02};
+ private static final int PASSKEY = 1234;
+ private static DiscoveryItem sDiscoveryItem;
+ private static PairingProgressHandlerBase sPairingProgressHandlerBase;
+ private static BluetoothDevice sBluetoothDevice;
@Before
public void setup() {
-
MockitoAnnotations.initMocks(this);
+ when(mContextWrapper.getSystemService(BluetoothManager.class))
+ .thenReturn(mBluetoothManager);
when(mContextWrapper.getLocator()).thenReturn(mLocator);
mLocator.overrideBindingForTest(FastPairCacheManager.class,
mFastPairCacheManager);
mLocator.overrideBindingForTest(Clock.class, mClock);
+ sBluetoothDevice =
+ BluetoothAdapter.getDefaultAdapter().getRemoteDevice("00:11:22:33:44:55");
+ sDiscoveryItem = FakeDiscoveryItems.newFastPairDiscoveryItem(mContextWrapper);
+ sDiscoveryItem.setStoredItemForTest(
+ sDiscoveryItem.getStoredItemForTest().toBuilder()
+ .setAuthenticationPublicKeySecp256R1(ByteString.copyFrom(ACCOUNT_KEY))
+ .setFastPairInformation(
+ Cache.FastPairInformation.newBuilder()
+ .setDeviceType(Rpcs.DeviceType.HEADPHONES).build())
+ .build());
+
+ sPairingProgressHandlerBase =
+ createProgressHandler(ACCOUNT_KEY, sDiscoveryItem, /* isRetroactivePair= */ false);
}
@Test
public void createHandler_halfSheetSubsequentPairing_notificationPairingHandlerCreated() {
-
- DiscoveryItem discoveryItem = FakeDiscoveryItems.newFastPairDiscoveryItem(mContextWrapper);
- discoveryItem.setStoredItemForTest(
- discoveryItem.getStoredItemForTest().toBuilder()
+ sDiscoveryItem.setStoredItemForTest(
+ sDiscoveryItem.getStoredItemForTest().toBuilder()
.setAuthenticationPublicKeySecp256R1(ByteString.copyFrom(ACCOUNT_KEY))
.setFastPairInformation(
Cache.FastPairInformation.newBuilder()
@@ -79,7 +107,7 @@
.build());
PairingProgressHandlerBase progressHandler =
- createProgressHandler(ACCOUNT_KEY, discoveryItem, /* isRetroactivePair= */ false);
+ createProgressHandler(ACCOUNT_KEY, sDiscoveryItem, /* isRetroactivePair= */ false);
assertThat(progressHandler).isInstanceOf(NotificationPairingProgressHandler.class);
}
@@ -87,20 +115,94 @@
@Test
public void createHandler_halfSheetInitialPairing_halfSheetPairingHandlerCreated() {
// No account key
- DiscoveryItem discoveryItem = FakeDiscoveryItems.newFastPairDiscoveryItem(mContextWrapper);
- discoveryItem.setStoredItemForTest(
- discoveryItem.getStoredItemForTest().toBuilder()
+ sDiscoveryItem.setStoredItemForTest(
+ sDiscoveryItem.getStoredItemForTest().toBuilder()
.setFastPairInformation(
Cache.FastPairInformation.newBuilder()
.setDeviceType(Rpcs.DeviceType.HEADPHONES).build())
.build());
PairingProgressHandlerBase progressHandler =
- createProgressHandler(null, discoveryItem, /* isRetroactivePair= */ false);
+ createProgressHandler(null, sDiscoveryItem, /* isRetroactivePair= */ false);
assertThat(progressHandler).isInstanceOf(HalfSheetPairingProgressHandler.class);
}
+ @Test
+ public void onPairingStarted() {
+ sPairingProgressHandlerBase.onPairingStarted();
+ }
+
+ @Test
+ public void onWaitForScreenUnlock() {
+ sPairingProgressHandlerBase.onWaitForScreenUnlock();
+ }
+
+ @Test
+ public void onScreenUnlocked() {
+ sPairingProgressHandlerBase.onScreenUnlocked();
+ }
+
+ @Test
+ public void onReadyToPair() {
+ sPairingProgressHandlerBase.onReadyToPair();
+ }
+
+ @Test
+ public void onSetupPreferencesBuilder() {
+ Preferences.Builder prefsBuilder =
+ Preferences.builder()
+ .setEnableBrEdrHandover(false)
+ .setIgnoreDiscoveryError(true);
+ sPairingProgressHandlerBase.onSetupPreferencesBuilder(prefsBuilder);
+ }
+
+ @Test
+ public void onPairingSetupCompleted() {
+ sPairingProgressHandlerBase.onPairingSetupCompleted();
+ }
+
+ @Test
+ public void onHandlePasskeyConfirmation() {
+ sPairingProgressHandlerBase.onHandlePasskeyConfirmation(sBluetoothDevice, PASSKEY);
+ }
+
+ @Test
+ public void getKeyForLocalCache() {
+ FastPairConnection.SharedSecret sharedSecret =
+ FastPairConnection.SharedSecret.create(ACCOUNT_KEY, sDiscoveryItem.getMacAddress());
+ sPairingProgressHandlerBase
+ .getKeyForLocalCache(ACCOUNT_KEY, mFastPairConnection, sharedSecret);
+ }
+
+ @Test
+ public void onPairedCallbackCalled() {
+ sPairingProgressHandlerBase.onPairedCallbackCalled(mFastPairConnection,
+ ACCOUNT_KEY, mFootprintsDeviceManager, MAC_ADDRESS);
+ }
+
+ @Test
+ public void onPairingFailed() {
+ Throwable e = new Throwable("Pairing Failed");
+ sPairingProgressHandlerBase.onPairingFailed(e);
+ }
+
+ @Test
+ public void onPairingSuccess() {
+ sPairingProgressHandlerBase.onPairingSuccess(sDiscoveryItem.getMacAddress());
+ }
+
+ @Test
+ public void optInFootprintsForInitialPairing() {
+ sPairingProgressHandlerBase.optInFootprintsForInitialPairing(
+ mFootprintsDeviceManager, sDiscoveryItem, ACCOUNT_KEY, null);
+ }
+
+ @Test
+ public void skipWaitingScreenUnlock() {
+ assertThat(sPairingProgressHandlerBase.skipWaitingScreenUnlock()).isFalse();
+ }
+
private PairingProgressHandlerBase createProgressHandler(
@Nullable byte[] accountKey, DiscoveryItem fastPairItem, boolean isRetroactivePair) {
FastPairNotificationManager fastPairNotificationManager =
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/FakeDiscoveryItems.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/FakeDiscoveryItems.java
index c406e47..cdec04d 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/FakeDiscoveryItems.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/FakeDiscoveryItems.java
@@ -22,10 +22,17 @@
import service.proto.Cache;
public class FakeDiscoveryItems {
- public static final String DEFAULT_MAC_ADDRESS = "00:11:22:33:44:55";
- public static final long DEFAULT_TIMESTAMP = 1000000000L;
- public static final String DEFAULT_DESCRIPITON = "description";
- public static final String TRIGGER_ID = "trigger.id";
+ private static final String DEFAULT_MAC_ADDRESS = "00:11:22:33:44:55";
+ private static final long DEFAULT_TIMESTAMP = 1000000000L;
+ private static final String DEFAULT_DESCRIPITON = "description";
+ private static final String APP_NAME = "app_name";
+ private static final String ACTION_URL =
+ "intent:#Intent;action=com.android.server.nearby:ACTION_FAST_PAIR;"
+ + "package=com.google.android.gms;"
+ + "component=com.google.android.gms/"
+ + ".nearby.discovery.service.DiscoveryService;end";
+ private static final String DISPLAY_URL = "DISPLAY_URL";
+ private static final String TRIGGER_ID = "trigger.id";
private static final String FAST_PAIR_ID = "id";
private static final int RSSI = -80;
private static final int TX_POWER = -10;
@@ -46,9 +53,36 @@
item.setMacAddress(DEFAULT_MAC_ADDRESS);
item.setFirstObservationTimestampMillis(DEFAULT_TIMESTAMP);
item.setLastObservationTimestampMillis(DEFAULT_TIMESTAMP);
+ item.setActionUrl(ACTION_URL);
+ item.setAppName(APP_NAME);
item.setRssi(RSSI);
item.setTxPower(TX_POWER);
+ item.setDisplayUrl(DISPLAY_URL);
return item.build();
}
+ public static Cache.StoredDiscoveryItem newFastPairDeviceStoredItem(String id,
+ String description, String triggerId, String macAddress, String title,
+ int rssi, int txPower) {
+ Cache.StoredDiscoveryItem.Builder item = Cache.StoredDiscoveryItem.newBuilder();
+ item.setState(Cache.StoredDiscoveryItem.State.STATE_ENABLED);
+ if (id != null) {
+ item.setId(id);
+ }
+ if (description != null) {
+ item.setDescription(description);
+ }
+ if (triggerId != null) {
+ item.setTriggerId(triggerId);
+ }
+ if (macAddress != null) {
+ item.setMacAddress(macAddress);
+ }
+ if (title != null) {
+ item.setTitle(title);
+ }
+ item.setRssi(rssi);
+ item.setTxPower(txPower);
+ return item.build();
+ }
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/MockingLocator.java b/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/MockingLocator.java
index b261b26..c9a4533 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/MockingLocator.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/fastpair/testing/MockingLocator.java
@@ -41,7 +41,7 @@
}
@SuppressWarnings("nullness") // due to passing in this before initialized.
- private MockingLocator(Context context, Locator locator) {
+ public MockingLocator(Context context, Locator locator) {
super(context, locator);
this.mLocatorContextWrapper = new LocatorContextWrapper(context, this);
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/injector/ContextHubManagerAdapterTest.java b/nearby/tests/unit/src/com/android/server/nearby/injector/ContextHubManagerAdapterTest.java
new file mode 100644
index 0000000..b577064
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/injector/ContextHubManagerAdapterTest.java
@@ -0,0 +1,48 @@
+/*
+ * 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.ContextHubInfo;
+import android.hardware.location.ContextHubManager;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class ContextHubManagerAdapterTest {
+ private ContextHubManagerAdapter mContextHubManagerAdapter;
+
+ @Mock
+ ContextHubManager mContextHubManager;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ mContextHubManagerAdapter = new ContextHubManagerAdapter(mContextHubManager);
+ }
+
+ @Test
+ public void getContextHubs() {
+ mContextHubManagerAdapter.getContextHubs();
+ }
+
+ @Test
+ public void queryNanoApps() {
+ mContextHubManagerAdapter.queryNanoApps(new ContextHubInfo());
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/DataElementHeaderTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/DataElementHeaderTest.java
new file mode 100644
index 0000000..e186709
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/DataElementHeaderTest.java
@@ -0,0 +1,179 @@
+/*
+ * 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.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+
+import android.nearby.BroadcastRequest;
+
+import org.junit.Test;
+
+import java.util.List;
+
+/**
+ * Unit test for {@link DataElementHeader}.
+ */
+public class DataElementHeaderTest {
+
+ private static final int VERSION = BroadcastRequest.PRESENCE_VERSION_V1;
+
+ @Test
+ public void test_illegalLength() {
+ assertThrows(IllegalArgumentException.class,
+ () -> new DataElementHeader(VERSION, 12, 128));
+ }
+
+ @Test
+ public void test_singeByteConversion() {
+ DataElementHeader header = new DataElementHeader(VERSION, 12, 3);
+ byte[] bytes = header.toBytes();
+ assertThat(bytes).isEqualTo(new byte[]{(byte) 0b00111100});
+
+ DataElementHeader afterConversionHeader = DataElementHeader.fromBytes(VERSION, bytes);
+ assertThat(afterConversionHeader.getDataLength()).isEqualTo(3);
+ assertThat(afterConversionHeader.getDataType()).isEqualTo(12);
+ }
+
+ @Test
+ public void test_multipleBytesConversion() {
+ DataElementHeader header = new DataElementHeader(VERSION, 6, 100);
+ DataElementHeader afterConversionHeader =
+ DataElementHeader.fromBytes(VERSION, header.toBytes());
+ assertThat(afterConversionHeader.getDataLength()).isEqualTo(100);
+ assertThat(afterConversionHeader.getDataType()).isEqualTo(6);
+ }
+
+ @Test
+ public void test_fromBytes() {
+ // Single byte case.
+ byte[] singleByte = new byte[]{(byte) 0b01011101};
+ DataElementHeader singeByteHeader = DataElementHeader.fromBytes(VERSION, singleByte);
+ assertThat(singeByteHeader.getDataLength()).isEqualTo(5);
+ assertThat(singeByteHeader.getDataType()).isEqualTo(13);
+
+ // Two bytes case.
+ byte[] twoBytes = new byte[]{(byte) 0b11011101, (byte) 0b01011101};
+ DataElementHeader twoBytesHeader = DataElementHeader.fromBytes(VERSION, twoBytes);
+ assertThat(twoBytesHeader.getDataLength()).isEqualTo(93);
+ assertThat(twoBytesHeader.getDataType()).isEqualTo(93);
+
+ // Three bytes case.
+ byte[] threeBytes = new byte[]{(byte) 0b11011101, (byte) 0b11111111, (byte) 0b01011101};
+ DataElementHeader threeBytesHeader = DataElementHeader.fromBytes(VERSION, threeBytes);
+ assertThat(threeBytesHeader.getDataLength()).isEqualTo(93);
+ assertThat(threeBytesHeader.getDataType()).isEqualTo(16349);
+
+ // Four bytes case.
+ byte[] fourBytes = new byte[]{
+ (byte) 0b11011101, (byte) 0b11111111, (byte) 0b11111111, (byte) 0b01011101};
+
+ DataElementHeader fourBytesHeader = DataElementHeader.fromBytes(VERSION, fourBytes);
+ assertThat(fourBytesHeader.getDataLength()).isEqualTo(93);
+ assertThat(fourBytesHeader.getDataType()).isEqualTo(2097117);
+ }
+
+ @Test
+ public void test_fromBytesIllegal_singleByte() {
+ assertThrows(IllegalArgumentException.class,
+ () -> DataElementHeader.fromBytes(VERSION, new byte[]{(byte) 0b11011101}));
+ }
+
+ @Test
+ public void test_fromBytesIllegal_twoBytes_wrongFirstByte() {
+ assertThrows(IllegalArgumentException.class,
+ () -> DataElementHeader.fromBytes(VERSION,
+ new byte[]{(byte) 0b01011101, (byte) 0b01011101}));
+ }
+
+ @Test
+ public void test_fromBytesIllegal_twoBytes_wrongLastByte() {
+ assertThrows(IllegalArgumentException.class,
+ () -> DataElementHeader.fromBytes(VERSION,
+ new byte[]{(byte) 0b11011101, (byte) 0b11011101}));
+ }
+
+ @Test
+ public void test_fromBytesIllegal_threeBytes() {
+ assertThrows(IllegalArgumentException.class,
+ () -> DataElementHeader.fromBytes(VERSION,
+ new byte[]{(byte) 0b11011101, (byte) 0b11011101, (byte) 0b11011101}));
+ }
+
+ @Test
+ public void test_multipleBytesConversion_largeNumber() {
+ DataElementHeader header = new DataElementHeader(VERSION, 22213546, 66);
+ DataElementHeader afterConversionHeader =
+ DataElementHeader.fromBytes(VERSION, header.toBytes());
+ assertThat(afterConversionHeader.getDataLength()).isEqualTo(66);
+ assertThat(afterConversionHeader.getDataType()).isEqualTo(22213546);
+ }
+
+ @Test
+ public void test_isExtending() {
+ assertThat(DataElementHeader.isExtending((byte) 0b10000100)).isTrue();
+ assertThat(DataElementHeader.isExtending((byte) 0b01110100)).isFalse();
+ assertThat(DataElementHeader.isExtending((byte) 0b00000000)).isFalse();
+ }
+
+ @Test
+ public void test_convertTag() {
+ assertThat(DataElementHeader.convertTag(true)).isEqualTo((byte) 128);
+ assertThat(DataElementHeader.convertTag(false)).isEqualTo(0);
+ }
+
+ @Test
+ public void test_getHeaderValue() {
+ assertThat(DataElementHeader.getHeaderValue((byte) 0b10000100)).isEqualTo(4);
+ assertThat(DataElementHeader.getHeaderValue((byte) 0b00000100)).isEqualTo(4);
+ assertThat(DataElementHeader.getHeaderValue((byte) 0b11010100)).isEqualTo(84);
+ assertThat(DataElementHeader.getHeaderValue((byte) 0b01010100)).isEqualTo(84);
+ }
+
+ @Test
+ public void test_convertTypeMultipleIntList() {
+ List<Byte> list = DataElementHeader.convertTypeMultipleBytes(128);
+ assertThat(list.size()).isEqualTo(2);
+ assertThat(list.get(0)).isEqualTo((byte) 0b10000001);
+ assertThat(list.get(1)).isEqualTo((byte) 0b00000000);
+
+ List<Byte> list2 = DataElementHeader.convertTypeMultipleBytes(10);
+ assertThat(list2.size()).isEqualTo(1);
+ assertThat(list2.get(0)).isEqualTo((byte) 0b00001010);
+
+ List<Byte> list3 = DataElementHeader.convertTypeMultipleBytes(5242398);
+ assertThat(list3.size()).isEqualTo(4);
+ assertThat(list3.get(0)).isEqualTo((byte) 0b10000010);
+ assertThat(list3.get(1)).isEqualTo((byte) 0b10111111);
+ assertThat(list3.get(2)).isEqualTo((byte) 0b11111100);
+ assertThat(list3.get(3)).isEqualTo((byte) 0b00011110);
+ }
+
+ @Test
+ public void test_getTypeMultipleBytes() {
+ byte[] inputBytes = new byte[]{(byte) 0b11011000, (byte) 0b10000000, (byte) 0b00001001};
+ // 0b101100000000000001001
+ assertThat(DataElementHeader.getTypeMultipleBytes(inputBytes)).isEqualTo(1441801);
+
+ byte[] inputBytes2 = new byte[]{(byte) 0b00010010};
+ assertThat(DataElementHeader.getTypeMultipleBytes(inputBytes2)).isEqualTo(18);
+
+ byte[] inputBytes3 = new byte[]{(byte) 0b10000001, (byte) 0b00000000};
+ assertThat(DataElementHeader.getTypeMultipleBytes(inputBytes3)).isEqualTo(128);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementTest.java
new file mode 100644
index 0000000..895df69
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementTest.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 static com.google.common.truth.Truth.assertThat;
+
+import android.nearby.BroadcastRequest;
+import android.nearby.DataElement;
+import android.nearby.PresenceBroadcastRequest;
+import android.nearby.PresenceCredential;
+import android.nearby.PrivateCredential;
+import android.nearby.PublicCredential;
+
+import com.android.server.nearby.util.encryption.CryptorImpIdentityV1;
+import com.android.server.nearby.util.encryption.CryptorImpV1;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class ExtendedAdvertisementTest {
+ private static final int IDENTITY_TYPE = PresenceCredential.IDENTITY_TYPE_PRIVATE;
+ private static final int DATA_TYPE_MODEL_ID = 7;
+ private static final int DATA_TYPE_BLE_ADDRESS = 101;
+ private static final int DATA_TYPE_PUBLIC_IDENTITY = 3;
+ private static final byte[] MODE_ID_DATA =
+ new byte[]{2, 1, 30, 2, 10, -16, 6, 22, 44, -2, -86, -69, -52};
+ private static final byte[] BLE_ADDRESS = new byte[]{124, 4, 56, 60, 120, -29, -90};
+ private static final DataElement MODE_ID_ADDRESS_ELEMENT =
+ new DataElement(DATA_TYPE_MODEL_ID, MODE_ID_DATA);
+ private static final DataElement BLE_ADDRESS_ELEMENT =
+ new DataElement(DATA_TYPE_BLE_ADDRESS, BLE_ADDRESS);
+
+ private static final byte[] IDENTITY =
+ new byte[]{1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4};
+ private static final int MEDIUM_TYPE_BLE = 0;
+ private static final byte[] SALT = {2, 3};
+ private static final int PRESENCE_ACTION_1 = 1;
+ private static final int PRESENCE_ACTION_2 = 2;
+
+ private static final byte[] SECRET_ID = new byte[]{1, 2, 3, 4};
+ private static final byte[] AUTHENTICITY_KEY =
+ new byte[]{-97, 10, 107, -86, 25, 65, -54, -95, -72, 59, 54, 93, 9, 3, -24, -88};
+ private static final byte[] PUBLIC_KEY =
+ new byte[] {
+ 48, 89, 48, 19, 6, 7, 42, -122, 72, -50, 61, 2, 1, 6, 8, 42, -122, 72, -50, 61,
+ 66, 0, 4, -56, -39, -92, 69, 0, 52, 23, 67, 83, -14, 75, 52, -14, -5, -41, 48,
+ -83, 31, 42, -39, 102, -13, 22, -73, -73, 86, 30, -96, -84, -13, 4, 122, 104,
+ -65, 64, 91, -109, -45, -35, -56, 55, -79, 47, -85, 27, -96, -119, -82, -80,
+ 123, 41, -119, -25, 1, -112, 112
+ };
+ private static final byte[] ENCRYPTED_METADATA_BYTES =
+ new byte[] {
+ -44, -25, -95, -124, -7, 90, 116, -8, 7, -120, -23, -22, -106, -44, -19, 61,
+ -18, 39, 29, 78, 108, -11, -39, 85, -30, 64, -99, 102, 65, 37, -42, 114, -37,
+ 88, -112, 8, -75, -53, 23, -16, -104, 67, 49, 48, -53, 73, -109, 44, -23, -11,
+ -118, -61, -37, -104, 60, 105, 115, 1, 56, -89, -107, -45, -116, -1, -25, 84,
+ -19, -128, 81, 11, 92, 77, -58, 82, 122, 123, 31, -87, -57, 70, 23, -81, 7, 2,
+ -114, -83, 74, 124, -68, -98, 47, 91, 9, 48, -67, 41, -7, -97, 78, 66, -65, 58,
+ -4, -46, -30, -85, -50, 100, 46, -66, -128, 7, 66, 9, 88, 95, 12, -13, 81, -91,
+ };
+ private static final byte[] METADATA_ENCRYPTION_KEY_TAG =
+ new byte[] {-126, -104, 1, -1, 26, -46, -68, -86};
+ private static final String DEVICE_NAME = "test_device";
+
+ private PresenceBroadcastRequest.Builder mBuilder;
+ private PrivateCredential mPrivateCredential;
+ private PublicCredential mPublicCredential;
+
+ @Before
+ public void setUp() {
+ mPrivateCredential =
+ new PrivateCredential.Builder(SECRET_ID, AUTHENTICITY_KEY, IDENTITY, DEVICE_NAME)
+ .setIdentityType(PresenceCredential.IDENTITY_TYPE_PRIVATE)
+ .build();
+ mPublicCredential =
+ new PublicCredential.Builder(SECRET_ID, AUTHENTICITY_KEY, PUBLIC_KEY,
+ ENCRYPTED_METADATA_BYTES, METADATA_ENCRYPTION_KEY_TAG)
+ .build();
+ mBuilder =
+ new PresenceBroadcastRequest.Builder(Collections.singletonList(MEDIUM_TYPE_BLE),
+ SALT, mPrivateCredential)
+ .setVersion(BroadcastRequest.PRESENCE_VERSION_V1)
+ .addAction(PRESENCE_ACTION_1)
+ .addAction(PRESENCE_ACTION_2)
+ .addExtendedProperty(new DataElement(DATA_TYPE_BLE_ADDRESS, BLE_ADDRESS))
+ .addExtendedProperty(new DataElement(DATA_TYPE_MODEL_ID, MODE_ID_DATA));
+ }
+
+ @Test
+ public void test_createFromRequest() {
+ ExtendedAdvertisement originalAdvertisement = ExtendedAdvertisement.createFromRequest(
+ mBuilder.build());
+
+ assertThat(originalAdvertisement.getActions())
+ .containsExactly(PRESENCE_ACTION_1, PRESENCE_ACTION_2);
+ assertThat(originalAdvertisement.getIdentity()).isEqualTo(IDENTITY);
+ assertThat(originalAdvertisement.getIdentityType()).isEqualTo(IDENTITY_TYPE);
+ assertThat(originalAdvertisement.getLength()).isEqualTo(66);
+ assertThat(originalAdvertisement.getVersion()).isEqualTo(
+ BroadcastRequest.PRESENCE_VERSION_V1);
+ assertThat(originalAdvertisement.getSalt()).isEqualTo(SALT);
+ assertThat(originalAdvertisement.getDataElements())
+ .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT);
+ }
+
+ @Test
+ public void test_createFromRequest_encodeAndDecode() {
+ ExtendedAdvertisement originalAdvertisement = ExtendedAdvertisement.createFromRequest(
+ mBuilder.build());
+
+ byte[] generatedBytes = originalAdvertisement.toBytes();
+
+ ExtendedAdvertisement newAdvertisement =
+ ExtendedAdvertisement.fromBytes(generatedBytes, mPublicCredential);
+
+ assertThat(newAdvertisement.getActions())
+ .containsExactly(PRESENCE_ACTION_1, PRESENCE_ACTION_2);
+ assertThat(newAdvertisement.getIdentity()).isEqualTo(IDENTITY);
+ assertThat(newAdvertisement.getIdentityType()).isEqualTo(IDENTITY_TYPE);
+ assertThat(newAdvertisement.getLength()).isEqualTo(66);
+ assertThat(newAdvertisement.getVersion()).isEqualTo(
+ BroadcastRequest.PRESENCE_VERSION_V1);
+ assertThat(newAdvertisement.getSalt()).isEqualTo(SALT);
+ assertThat(newAdvertisement.getDataElements())
+ .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT);
+ }
+
+ @Test
+ public void test_createFromRequest_invalidParameter() {
+ // invalid version
+ mBuilder.setVersion(BroadcastRequest.PRESENCE_VERSION_V0);
+ assertThat(ExtendedAdvertisement.createFromRequest(mBuilder.build())).isNull();
+
+ // invalid salt
+ PresenceBroadcastRequest.Builder builder =
+ new PresenceBroadcastRequest.Builder(Collections.singletonList(MEDIUM_TYPE_BLE),
+ new byte[]{1, 2, 3}, mPrivateCredential)
+ .setVersion(BroadcastRequest.PRESENCE_VERSION_V1)
+ .addAction(PRESENCE_ACTION_1);
+ assertThat(ExtendedAdvertisement.createFromRequest(builder.build())).isNull();
+
+ // invalid identity
+ PrivateCredential privateCredential =
+ new PrivateCredential.Builder(SECRET_ID,
+ AUTHENTICITY_KEY, new byte[]{1, 2, 3, 4}, DEVICE_NAME)
+ .setIdentityType(PresenceCredential.IDENTITY_TYPE_PRIVATE)
+ .build();
+ PresenceBroadcastRequest.Builder builder2 =
+ new PresenceBroadcastRequest.Builder(Collections.singletonList(MEDIUM_TYPE_BLE),
+ new byte[]{1, 2, 3}, privateCredential)
+ .setVersion(BroadcastRequest.PRESENCE_VERSION_V1)
+ .addAction(PRESENCE_ACTION_1);
+ assertThat(ExtendedAdvertisement.createFromRequest(builder2.build())).isNull();
+
+ // empty action
+ PresenceBroadcastRequest.Builder builder3 =
+ new PresenceBroadcastRequest.Builder(Collections.singletonList(MEDIUM_TYPE_BLE),
+ SALT, mPrivateCredential)
+ .setVersion(BroadcastRequest.PRESENCE_VERSION_V1);
+ assertThat(ExtendedAdvertisement.createFromRequest(builder3.build())).isNull();
+ }
+
+ @Test
+ public void test_toBytes() {
+ ExtendedAdvertisement adv = ExtendedAdvertisement.createFromRequest(mBuilder.build());
+ assertThat(adv.toBytes()).isEqualTo(getExtendedAdvertisementByteArray());
+ }
+
+ @Test
+ public void test_fromBytes() {
+ byte[] originalBytes = getExtendedAdvertisementByteArray();
+ ExtendedAdvertisement adv =
+ ExtendedAdvertisement.fromBytes(originalBytes, mPublicCredential);
+
+ assertThat(adv.getActions())
+ .containsExactly(PRESENCE_ACTION_1, PRESENCE_ACTION_2);
+ assertThat(adv.getIdentity()).isEqualTo(IDENTITY);
+ assertThat(adv.getIdentityType()).isEqualTo(IDENTITY_TYPE);
+ assertThat(adv.getLength()).isEqualTo(66);
+ assertThat(adv.getVersion()).isEqualTo(
+ BroadcastRequest.PRESENCE_VERSION_V1);
+ assertThat(adv.getSalt()).isEqualTo(SALT);
+ assertThat(adv.getDataElements())
+ .containsExactly(MODE_ID_ADDRESS_ELEMENT, BLE_ADDRESS_ELEMENT);
+ }
+
+ @Test
+ public void test_toString() {
+ ExtendedAdvertisement adv = ExtendedAdvertisement.createFromRequest(mBuilder.build());
+ assertThat(adv.toString()).isEqualTo("ExtendedAdvertisement:"
+ + "<VERSION: 1, length: 66, dataElementCount: 2, identityType: 1, "
+ + "identity: [1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4], salt: [2, 3],"
+ + " actions: [1, 2]>");
+ }
+
+ @Test
+ public void test_getDataElements_accordingToType() {
+ ExtendedAdvertisement adv = ExtendedAdvertisement.createFromRequest(mBuilder.build());
+ List<DataElement> dataElements = new ArrayList<>();
+
+ dataElements.add(BLE_ADDRESS_ELEMENT);
+ assertThat(adv.getDataElements(DATA_TYPE_BLE_ADDRESS)).isEqualTo(dataElements);
+ assertThat(adv.getDataElements(DATA_TYPE_PUBLIC_IDENTITY)).isEmpty();
+ }
+
+ private static byte[] getExtendedAdvertisementByteArray() {
+ ByteBuffer buffer = ByteBuffer.allocate(66);
+ buffer.put((byte) 0b00100000); // Header V1
+ buffer.put((byte) 0b00100000); // Salt header: length 2, type 0
+ // Salt data
+ buffer.put(SALT);
+ // Identity header: length 16, type 1 (private identity)
+ buffer.put(new byte[]{(byte) 0b10010000, (byte) 0b00000001});
+ // Identity data
+ buffer.put(CryptorImpIdentityV1.getInstance().encrypt(IDENTITY, SALT, AUTHENTICITY_KEY));
+
+ ByteBuffer deBuffer = ByteBuffer.allocate(28);
+ // Action1 header: length 1, type 6
+ deBuffer.put(new byte[]{(byte) 0b00010110});
+ // Action1 data
+ deBuffer.put((byte) PRESENCE_ACTION_1);
+ // Action2 header: length 1, type 6
+ deBuffer.put(new byte[]{(byte) 0b00010110});
+ // Action2 data
+ deBuffer.put((byte) PRESENCE_ACTION_2);
+ // Ble address header: length 7, type 102
+ deBuffer.put(new byte[]{(byte) 0b10000111, (byte) 0b01100101});
+ // Ble address data
+ deBuffer.put(BLE_ADDRESS);
+ // model id header: length 13, type 7
+ deBuffer.put(new byte[]{(byte) 0b10001101, (byte) 0b00000111});
+ // model id data
+ deBuffer.put(MODE_ID_DATA);
+
+ byte[] data = deBuffer.array();
+ CryptorImpV1 cryptor = CryptorImpV1.getInstance();
+ buffer.put(cryptor.encrypt(data, SALT, AUTHENTICITY_KEY));
+ buffer.put(cryptor.sign(data, AUTHENTICITY_KEY));
+
+ return buffer.array();
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementUtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementUtilsTest.java
new file mode 100644
index 0000000..c4fccf7
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/ExtendedAdvertisementUtilsTest.java
@@ -0,0 +1,103 @@
+/*
+ * 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.google.common.truth.Truth.assertThat;
+
+import android.nearby.BroadcastRequest;
+import android.nearby.DataElement;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+
+/**
+ * Unit test for {@link ExtendedAdvertisementUtils}.
+ */
+public class ExtendedAdvertisementUtilsTest {
+ private static final byte[] ADVERTISEMENT1 = new byte[]{0b00100000, 12, 34, 78, 10};
+ private static final byte[] ADVERTISEMENT2 = new byte[]{0b00100000, 0b00100011, 34, 78,
+ (byte) 0b10010000, (byte) 0b00000100,
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
+
+ private static final int DATA_TYPE_SALT = 3;
+ private static final int DATA_TYPE_PRIVATE_IDENTITY = 4;
+
+ @Test
+ public void test_constructHeader() {
+ assertThat(ExtendedAdvertisementUtils.constructHeader(1)).isEqualTo(0b100000);
+ assertThat(ExtendedAdvertisementUtils.constructHeader(0)).isEqualTo(0);
+ assertThat(ExtendedAdvertisementUtils.constructHeader(6)).isEqualTo((byte) 0b11000000);
+ }
+
+ @Test
+ public void test_getVersion() {
+ assertThat(ExtendedAdvertisementUtils.getVersion(ADVERTISEMENT1)).isEqualTo(1);
+ byte[] adv = new byte[]{(byte) 0b10111100, 9, 19, 90, 23};
+ assertThat(ExtendedAdvertisementUtils.getVersion(adv)).isEqualTo(5);
+ byte[] adv2 = new byte[]{(byte) 0b10011111, 9, 19, 90, 23};
+ assertThat(ExtendedAdvertisementUtils.getVersion(adv2)).isEqualTo(4);
+ }
+
+ @Test
+ public void test_getDataElementHeader_salt() {
+ byte[] saltHeaderArray = ExtendedAdvertisementUtils.getDataElementHeader(ADVERTISEMENT2, 1);
+ DataElementHeader header = DataElementHeader.fromBytes(
+ BroadcastRequest.PRESENCE_VERSION_V1, saltHeaderArray);
+ assertThat(header.getDataType()).isEqualTo(DATA_TYPE_SALT);
+ assertThat(header.getDataLength()).isEqualTo(ExtendedAdvertisement.SALT_DATA_LENGTH);
+ }
+
+ @Test
+ public void test_getDataElementHeader_identity() {
+ byte[] identityHeaderArray =
+ ExtendedAdvertisementUtils.getDataElementHeader(ADVERTISEMENT2, 4);
+ DataElementHeader header = DataElementHeader.fromBytes(BroadcastRequest.PRESENCE_VERSION_V1,
+ identityHeaderArray);
+ assertThat(header.getDataType()).isEqualTo(DATA_TYPE_PRIVATE_IDENTITY);
+ assertThat(header.getDataLength()).isEqualTo(ExtendedAdvertisement.IDENTITY_DATA_LENGTH);
+ }
+
+ @Test
+ public void test_constructDataElement_salt() {
+ DataElement salt = new DataElement(DATA_TYPE_SALT, new byte[]{13, 14});
+ byte[] saltArray = ExtendedAdvertisementUtils.convertDataElementToBytes(salt);
+ // Data length and salt header length.
+ assertThat(saltArray.length).isEqualTo(ExtendedAdvertisement.SALT_DATA_LENGTH + 1);
+ // Header
+ assertThat(saltArray[0]).isEqualTo((byte) 0b00100011);
+ // Data
+ assertThat(saltArray[1]).isEqualTo((byte) 13);
+ assertThat(saltArray[2]).isEqualTo((byte) 14);
+ }
+
+ @Test
+ public void test_constructDataElement_privateIdentity() {
+ byte[] identityData = new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
+ DataElement identity = new DataElement(DATA_TYPE_PRIVATE_IDENTITY, identityData);
+ byte[] identityArray = ExtendedAdvertisementUtils.convertDataElementToBytes(identity);
+ // Data length and identity header length.
+ assertThat(identityArray.length).isEqualTo(ExtendedAdvertisement.IDENTITY_DATA_LENGTH + 2);
+ // 1st header byte
+ assertThat(identityArray[0]).isEqualTo((byte) 0b10010000);
+ // 2st header byte
+ assertThat(identityArray[1]).isEqualTo((byte) 0b00000100);
+ // Data
+ assertThat(Arrays.copyOfRange(identityArray, 2, identityArray.length))
+ .isEqualTo(identityData);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/FastAdvertisementTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/FastAdvertisementTest.java
index 5e0ccbe..8e3e068 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/presence/FastAdvertisementTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/FastAdvertisementTest.java
@@ -75,6 +75,15 @@
assertThat(originalAdvertisement.getVersion()).isEqualTo(
BroadcastRequest.PRESENCE_VERSION_V0);
assertThat(originalAdvertisement.getSalt()).isEqualTo(SALT);
+ assertThat(originalAdvertisement.getTxPower()).isEqualTo(TX_POWER);
+ assertThat(originalAdvertisement.toString())
+ .isEqualTo("FastAdvertisement:<VERSION: 0, length: 19,"
+ + " ltvFieldCount: 4,"
+ + " identityType: 1,"
+ + " identity: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],"
+ + " salt: [2, 3],"
+ + " actions: [123],"
+ + " txPower: 4");
}
@Test
diff --git a/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceDiscoveryResultTest.java b/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceDiscoveryResultTest.java
index 39cab94..856c1a8 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceDiscoveryResultTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceDiscoveryResultTest.java
@@ -18,6 +18,8 @@
import static com.google.common.truth.Truth.assertThat;
+import android.nearby.DataElement;
+import android.nearby.NearbyDeviceParcelable;
import android.nearby.PresenceCredential;
import android.nearby.PresenceDevice;
import android.nearby.PresenceScanFilter;
@@ -28,12 +30,15 @@
import org.junit.Before;
import org.junit.Test;
-import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.List;
/**
* Unit tests for {@link PresenceDiscoveryResult}.
*/
public class PresenceDiscoveryResultTest {
+ private static final int DATA_TYPE_ACCOUNT_KEY = 9;
+ private static final int DATA_TYPE_INTENT = 6;
private static final int PRESENCE_ACTION = 123;
private static final int TX_POWER = -1;
private static final int RSSI = -41;
@@ -43,6 +48,8 @@
private static final byte[] PUBLIC_KEY = new byte[]{1, 1, 2, 2};
private static final byte[] ENCRYPTED_METADATA = new byte[]{1, 2, 3, 4, 5};
private static final byte[] METADATA_ENCRYPTION_KEY_TAG = new byte[]{1, 1, 3, 4, 5};
+ private static final byte[] META_DATA_ENCRYPTION_KEY =
+ new byte[] {-39, -55, 115, 78, -57, 40, 115, 0, -112, 86, -86, 7, -42, 68, 11, 12};
private PresenceDiscoveryResult.Builder mBuilder;
private PublicCredential mCredential;
@@ -59,18 +66,68 @@
.setSalt(SALT)
.setTxPower(TX_POWER)
.setRssi(RSSI)
+ .setEncryptedIdentityTag(METADATA_ENCRYPTION_KEY_TAG)
.addPresenceAction(PRESENCE_ACTION);
}
@Test
@SdkSuppress(minSdkVersion = 32, codeName = "T")
- public void testToDevice() {
- PresenceDiscoveryResult discoveryResult = mBuilder.build();
- PresenceDevice presenceDevice = discoveryResult.toPresenceDevice();
+ public void testFromDevice() {
+ NearbyDeviceParcelable.Builder builder = new NearbyDeviceParcelable.Builder();
+ builder.setTxPower(TX_POWER)
+ .setRssi(RSSI)
+ .setEncryptionKeyTag(METADATA_ENCRYPTION_KEY_TAG)
+ .setSalt(SALT)
+ .setPublicCredential(mCredential);
- assertThat(presenceDevice.getRssi()).isEqualTo(RSSI);
- assertThat(Arrays.equals(presenceDevice.getSalt(), SALT)).isTrue();
- assertThat(Arrays.equals(presenceDevice.getSecretId(), SECRET_ID)).isTrue();
+ PresenceDiscoveryResult discoveryResult =
+ PresenceDiscoveryResult.fromDevice(builder.build());
+ PresenceScanFilter scanFilter = new PresenceScanFilter.Builder()
+ .setMaxPathLoss(80)
+ .addCredential(mCredential)
+ .build();
+
+ assertThat(discoveryResult.matches(scanFilter)).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testFromDevice_presenceDeviceAvailable() {
+ NearbyDeviceParcelable.Builder builder = new NearbyDeviceParcelable.Builder();
+ PresenceDevice presenceDevice =
+ new PresenceDevice.Builder("123", SALT, SECRET_ID, META_DATA_ENCRYPTION_KEY)
+ .addExtendedProperty(new DataElement(
+ DATA_TYPE_INTENT, new byte[]{(byte) PRESENCE_ACTION}))
+ .build();
+ builder.setTxPower(TX_POWER)
+ .setRssi(RSSI)
+ .setEncryptionKeyTag(METADATA_ENCRYPTION_KEY_TAG)
+ .setPresenceDevice(presenceDevice)
+ .setPublicCredential(mCredential);
+
+ PresenceDiscoveryResult discoveryResult =
+ PresenceDiscoveryResult.fromDevice(builder.build());
+ PresenceScanFilter scanFilter = new PresenceScanFilter.Builder()
+ .setMaxPathLoss(80)
+ .addPresenceAction(PRESENCE_ACTION)
+ .addCredential(mCredential)
+ .build();
+
+ assertThat(discoveryResult.matches(scanFilter)).isTrue();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testAccountMatches() {
+ DataElement accountKey = new DataElement(DATA_TYPE_ACCOUNT_KEY, new byte[]{1, 2, 3, 4});
+ mBuilder.addExtendedProperties(List.of(accountKey));
+ PresenceDiscoveryResult discoveryResult = mBuilder.build();
+
+ List<DataElement> extendedProperties = new ArrayList<>();
+ extendedProperties.add(new DataElement(DATA_TYPE_ACCOUNT_KEY, new byte[]{1, 2, 3, 4}));
+ extendedProperties.add(new DataElement(DATA_TYPE_INTENT,
+ new byte[]{(byte) PRESENCE_ACTION}));
+ assertThat(discoveryResult.accountKeyMatches(extendedProperties)).isTrue();
}
@Test
@@ -86,4 +143,24 @@
assertThat(discoveryResult.matches(scanFilter)).isTrue();
}
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void test_notMatches() {
+ PresenceDiscoveryResult.Builder builder = new PresenceDiscoveryResult.Builder()
+ .setPublicCredential(mCredential)
+ .setSalt(SALT)
+ .setTxPower(TX_POWER)
+ .setRssi(RSSI)
+ .setEncryptedIdentityTag(new byte[]{5, 4, 3, 2, 1})
+ .addPresenceAction(PRESENCE_ACTION);
+
+ PresenceScanFilter scanFilter = new PresenceScanFilter.Builder()
+ .setMaxPathLoss(80)
+ .addPresenceAction(PRESENCE_ACTION)
+ .addCredential(mCredential)
+ .build();
+
+ PresenceDiscoveryResult discoveryResult = builder.build();
+ assertThat(discoveryResult.matches(scanFilter)).isFalse();
+ }
}
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..9deb1eb
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/presence/PresenceManagerTest.java
@@ -0,0 +1,84 @@
+/*
+ * 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.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.nearby.DataElement;
+import android.nearby.PresenceDevice;
+
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.server.nearby.common.locator.LocatorContextWrapper;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+public class PresenceManagerTest {
+ private static final byte[] IDENTITY =
+ new byte[] {1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4};
+ private static final byte[] SALT = {2, 3};
+ private static final byte[] SECRET_ID =
+ new byte[] {-97, 10, 107, -86, 25, 65, -54, -95, -72, 59, 54, 93, 9, 3, -24, -88};
+
+ @Mock private Context mContext;
+ private LocatorContextWrapper mLocatorContextWrapper;
+ private PresenceManager mPresenceManager;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+
+ mLocatorContextWrapper = new LocatorContextWrapper(mContext);
+ mPresenceManager = new PresenceManager(mLocatorContextWrapper);
+ when(mContext.getContentResolver())
+ .thenReturn(InstrumentationRegistry.getInstrumentation()
+ .getContext().getContentResolver());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testInit() {
+ mPresenceManager.initiate();
+
+ verify(mContext, times(1)).registerReceiver(any(), any());
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testDeviceStatusUpdated() {
+ DataElement dataElement1 = new DataElement(1, new byte[] {1, 2});
+ DataElement dataElement2 = new DataElement(2, new byte[] {-1, -2, 3, 4, 5, 6, 7, 8, 9});
+
+ PresenceDevice presenceDevice =
+ new PresenceDevice.Builder(/* deviceId= */ "deviceId", SALT, SECRET_ID, IDENTITY)
+ .addExtendedProperty(dataElement1)
+ .addExtendedProperty(dataElement2)
+ .build();
+
+ mPresenceManager.mScanCallback.onDiscovered(presenceDevice);
+ mPresenceManager.mScanCallback.onUpdated(presenceDevice);
+ mPresenceManager.mScanCallback.onLost(presenceDevice);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
index d06a785..05b556b 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/BleBroadcastProviderTest.java
@@ -16,6 +16,7 @@
package com.android.server.nearby.provider;
+import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -24,12 +25,14 @@
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothManager;
import android.bluetooth.le.AdvertiseSettings;
+import android.bluetooth.le.AdvertisingSetCallback;
import android.content.Context;
+import android.hardware.location.ContextHubManager;
import android.nearby.BroadcastCallback;
+import android.nearby.BroadcastRequest;
import androidx.test.core.app.ApplicationProvider;
-import com.android.server.nearby.injector.ContextHubManagerAdapter;
import com.android.server.nearby.injector.Injector;
import com.google.common.util.concurrent.MoreExecutors;
@@ -59,9 +62,10 @@
}
@Test
- public void testOnStatus_success() {
+ public void testOnStatus_success_fastAdv() {
byte[] advertiseBytes = new byte[]{1, 2, 3, 4};
- mBleBroadcastProvider.start(advertiseBytes, mBroadcastListener);
+ mBleBroadcastProvider.start(BroadcastRequest.PRESENCE_VERSION_V0,
+ advertiseBytes, mBroadcastListener);
AdvertiseSettings settings = new AdvertiseSettings.Builder().build();
mBleBroadcastProvider.onStartSuccess(settings);
@@ -69,15 +73,47 @@
}
@Test
- public void testOnStatus_failure() {
+ public void testOnStatus_success_extendedAdv() {
byte[] advertiseBytes = new byte[]{1, 2, 3, 4};
- mBleBroadcastProvider.start(advertiseBytes, mBroadcastListener);
+ mBleBroadcastProvider.start(BroadcastRequest.PRESENCE_VERSION_V1,
+ advertiseBytes, mBroadcastListener);
+
+ // advertising set can not be mocked, so we will allow nulls
+ mBleBroadcastProvider.mAdvertisingSetCallback.onAdvertisingSetStarted(null, -30,
+ AdvertisingSetCallback.ADVERTISE_SUCCESS);
+ verify(mBroadcastListener).onStatusChanged(eq(BroadcastCallback.STATUS_OK));
+ }
+
+ @Test
+ public void testOnStatus_failure_fastAdv() {
+ byte[] advertiseBytes = new byte[]{1, 2, 3, 4};
+ mBleBroadcastProvider.start(BroadcastRequest.PRESENCE_VERSION_V0,
+ advertiseBytes, mBroadcastListener);
mBleBroadcastProvider.onStartFailure(BroadcastCallback.STATUS_FAILURE);
verify(mBroadcastListener, times(1))
.onStatusChanged(eq(BroadcastCallback.STATUS_FAILURE));
}
+ @Test
+ public void testOnStatus_failure_extendedAdv() {
+ byte[] advertiseBytes = new byte[]{1, 2, 3, 4};
+ mBleBroadcastProvider.start(BroadcastRequest.PRESENCE_VERSION_V1,
+ advertiseBytes, mBroadcastListener);
+
+ // advertising set can not be mocked, so we will allow nulls
+ mBleBroadcastProvider.mAdvertisingSetCallback.onAdvertisingSetStarted(null, -30,
+ AdvertisingSetCallback.ADVERTISE_FAILED_INTERNAL_ERROR);
+ // Can be additional failure if the test device does not support LE Extended Advertising.
+ verify(mBroadcastListener, atLeastOnce())
+ .onStatusChanged(eq(BroadcastCallback.STATUS_FAILURE));
+ }
+
+ @Test
+ public void testStop() {
+ mBleBroadcastProvider.stop();
+ }
+
private static class TestInjector implements Injector {
@Override
@@ -88,7 +124,7 @@
}
@Override
- public ContextHubManagerAdapter getContextHubManagerAdapter() {
+ public ContextHubManager getContextHubManager() {
return null;
}
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 902cc33..ebb897e 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
@@ -18,6 +18,8 @@
import static android.bluetooth.le.ScanSettings.CALLBACK_TYPE_ALL_MATCHES;
+import static com.google.common.truth.Truth.assertThat;
+
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -30,10 +32,13 @@
import android.bluetooth.le.ScanRecord;
import android.bluetooth.le.ScanResult;
import android.content.Context;
+import android.hardware.location.ContextHubManager;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
+import android.nearby.ScanFilter;
import androidx.test.platform.app.InstrumentationRegistry;
-import com.android.server.nearby.injector.ContextHubManagerAdapter;
import com.android.server.nearby.injector.Injector;
import org.junit.Before;
@@ -42,6 +47,8 @@
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
public final class BleDiscoveryProviderTest {
@@ -70,6 +77,7 @@
// Wait for callback to be invoked
Thread.sleep(500);
verify(mListener, times(1)).onNearbyDeviceDiscovered(any());
+ mBleDiscoveryProvider.getScanCallback().onScanFailed(1);
}
@Test
@@ -78,6 +86,22 @@
mBleDiscoveryProvider.onStop();
}
+ @Test
+ public void test_stopScan_filersReset() {
+ List<ScanFilter> filterList = new ArrayList<>();
+ filterList.add(getSanFilter());
+
+ mBleDiscoveryProvider.getController().setProviderScanFilters(filterList);
+ mBleDiscoveryProvider.onStart();
+ mBleDiscoveryProvider.onStop();
+ assertThat(mBleDiscoveryProvider.getFiltersLocked()).isNull();
+ }
+
+ @Test
+ public void testInvalidateScanMode() {
+ mBleDiscoveryProvider.invalidateScanMode();
+ }
+
private class TestInjector implements Injector {
@Override
public BluetoothAdapter getBluetoothAdapter() {
@@ -85,7 +109,7 @@
}
@Override
- public ContextHubManagerAdapter getContextHubManagerAdapter() {
+ public ContextHubManager getContextHubManager() {
return null;
}
@@ -125,4 +149,22 @@
return null;
}
}
+
+ private static PresenceScanFilter getSanFilter() {
+ return new PresenceScanFilter.Builder()
+ .setMaxPathLoss(70)
+ .addCredential(getPublicCredential())
+ .addPresenceAction(124)
+ .build();
+ }
+
+ private static PublicCredential getPublicCredential() {
+ return new PublicCredential.Builder(
+ new byte[]{1, 2},
+ new byte[]{1, 2},
+ new byte[]{1, 2},
+ new byte[]{1, 2},
+ new byte[]{1, 2})
+ .build();
+ }
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/BroadcastProviderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/BroadcastProviderManagerTest.java
index d45d570..0179901 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/BroadcastProviderManagerTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/BroadcastProviderManagerTest.java
@@ -101,8 +101,13 @@
@Test
public void testStartAdvertising() {
mBroadcastProviderManager.startBroadcast(mBroadcastRequest, mBroadcastListener);
- verify(mBleBroadcastProvider).start(any(byte[].class), any(
- BleBroadcastProvider.BroadcastListener.class));
+ verify(mBleBroadcastProvider).start(eq(BroadcastRequest.PRESENCE_VERSION_V0),
+ any(byte[].class), any(BleBroadcastProvider.BroadcastListener.class));
+ }
+
+ @Test
+ public void testStopAdvertising() {
+ mBroadcastProviderManager.stopBroadcast(mBroadcastListener);
}
@Test
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreCommunicationTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreCommunicationTest.java
index 1b29b52..58b8584 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreCommunicationTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreCommunicationTest.java
@@ -16,18 +16,30 @@
package com.android.server.nearby.provider;
+import static android.Manifest.permission.READ_DEVICE_CONFIG;
+import static android.Manifest.permission.WRITE_DEVICE_CONFIG;
+import static android.provider.DeviceConfig.NAMESPACE_TETHERING;
+
+import static com.android.server.nearby.NearbyConfiguration.NEARBY_MAINLINE_NANO_APP_MIN_VERSION;
+
+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.verify;
import static org.mockito.Mockito.when;
+import android.content.Context;
import android.hardware.location.ContextHubClient;
import android.hardware.location.ContextHubInfo;
+import android.hardware.location.ContextHubManager;
import android.hardware.location.ContextHubTransaction;
import android.hardware.location.NanoAppMessage;
import android.hardware.location.NanoAppState;
+import android.provider.DeviceConfig;
-import com.android.server.nearby.injector.ContextHubManagerAdapter;
+import androidx.test.platform.app.InstrumentationRegistry;
+
import com.android.server.nearby.injector.Injector;
import org.junit.Before;
@@ -43,7 +55,8 @@
public class ChreCommunicationTest {
@Mock Injector mInjector;
- @Mock ContextHubManagerAdapter mManager;
+ @Mock Context mContext;
+ @Mock ContextHubManager mManager;
@Mock ContextHubTransaction<List<NanoAppState>> mTransaction;
@Mock ContextHubTransaction.Response<List<NanoAppState>> mTransactionResponse;
@Mock ContextHubClient mClient;
@@ -56,38 +69,63 @@
@Before
public void setUp() {
+ InstrumentationRegistry.getInstrumentation().getUiAutomation()
+ .adoptShellPermissionIdentity(WRITE_DEVICE_CONFIG, READ_DEVICE_CONFIG);
+ DeviceConfig.setProperty(NAMESPACE_TETHERING, NEARBY_MAINLINE_NANO_APP_MIN_VERSION,
+ "1", false);
+
MockitoAnnotations.initMocks(this);
- when(mInjector.getContextHubManagerAdapter()).thenReturn(mManager);
+ when(mInjector.getContextHubManager()).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(mManager.createClient(any(), any(), any(), any())).thenReturn(mClient);
when(mTransactionResponse.getResult()).thenReturn(ContextHubTransaction.RESULT_SUCCESS);
when(mTransactionResponse.getContents())
.thenReturn(
Collections.singletonList(
new NanoAppState(ChreDiscoveryProvider.NANOAPP_ID, 1, true)));
- mChreCommunication = new ChreCommunication(mInjector, new InlineExecutor());
+ mChreCommunication = new ChreCommunication(mInjector, mContext, new InlineExecutor());
+ }
+
+ @Test
+ public void testStart() {
mChreCommunication.start(
mChreCallback, Collections.singleton(ChreDiscoveryProvider.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.start(
+ mChreCallback, Collections.singleton(ChreDiscoveryProvider.NANOAPP_ID));
+
+ verify(mTransaction).setOnCompleteListener(mOnQueryCompleteListenerCaptor.capture(), any());
+ mOnQueryCompleteListenerCaptor.getValue().onComplete(mTransaction, mTransactionResponse);
mChreCommunication.stop();
verify(mClient).close();
}
@Test
+ public void testNotReachMinVersion() {
+ DeviceConfig.setProperty(NAMESPACE_TETHERING, NEARBY_MAINLINE_NANO_APP_MIN_VERSION,
+ "3", false);
+ mChreCommunication.start(
+ mChreCallback, Collections.singleton(ChreDiscoveryProvider.NANOAPP_ID));
+ verify(mTransaction).setOnCompleteListener(mOnQueryCompleteListenerCaptor.capture(), any());
+ mOnQueryCompleteListenerCaptor.getValue().onComplete(mTransaction, mTransactionResponse);
+ verify(mChreCallback).started(false);
+ }
+
+ @Test
public void testSendMessageToNanApp() {
+ mChreCommunication.start(
+ mChreCallback, Collections.singleton(ChreDiscoveryProvider.NANOAPP_ID));
+ verify(mTransaction).setOnCompleteListener(mOnQueryCompleteListenerCaptor.capture(), any());
+ mOnQueryCompleteListenerCaptor.getValue().onComplete(mTransaction, mTransactionResponse);
NanoAppMessage message =
NanoAppMessage.createMessageToNanoApp(
ChreDiscoveryProvider.NANOAPP_ID,
@@ -99,6 +137,8 @@
@Test
public void testOnMessageFromNanoApp() {
+ mChreCommunication.start(
+ mChreCallback, Collections.singleton(ChreDiscoveryProvider.NANOAPP_ID));
NanoAppMessage message =
NanoAppMessage.createMessageToNanoApp(
ChreDiscoveryProvider.NANOAPP_ID,
@@ -109,13 +149,60 @@
}
@Test
+ public void testContextHubTransactionResultToString() {
+ assertThat(
+ mChreCommunication.contextHubTransactionResultToString(
+ ContextHubTransaction.RESULT_SUCCESS))
+ .isEqualTo("RESULT_SUCCESS");
+ assertThat(
+ mChreCommunication.contextHubTransactionResultToString(
+ ContextHubTransaction.RESULT_FAILED_UNKNOWN))
+ .isEqualTo("RESULT_FAILED_UNKNOWN");
+ assertThat(
+ mChreCommunication.contextHubTransactionResultToString(
+ ContextHubTransaction.RESULT_FAILED_BAD_PARAMS))
+ .isEqualTo("RESULT_FAILED_BAD_PARAMS");
+ assertThat(
+ mChreCommunication.contextHubTransactionResultToString(
+ ContextHubTransaction.RESULT_FAILED_UNINITIALIZED))
+ .isEqualTo("RESULT_FAILED_UNINITIALIZED");
+ assertThat(
+ mChreCommunication.contextHubTransactionResultToString(
+ ContextHubTransaction.RESULT_FAILED_BUSY))
+ .isEqualTo("RESULT_FAILED_BUSY");
+ assertThat(
+ mChreCommunication.contextHubTransactionResultToString(
+ ContextHubTransaction.RESULT_FAILED_AT_HUB))
+ .isEqualTo("RESULT_FAILED_AT_HUB");
+ assertThat(
+ mChreCommunication.contextHubTransactionResultToString(
+ ContextHubTransaction.RESULT_FAILED_TIMEOUT))
+ .isEqualTo("RESULT_FAILED_TIMEOUT");
+ assertThat(
+ mChreCommunication.contextHubTransactionResultToString(
+ ContextHubTransaction.RESULT_FAILED_SERVICE_INTERNAL_FAILURE))
+ .isEqualTo("RESULT_FAILED_SERVICE_INTERNAL_FAILURE");
+ assertThat(
+ mChreCommunication.contextHubTransactionResultToString(
+ ContextHubTransaction.RESULT_FAILED_HAL_UNAVAILABLE))
+ .isEqualTo("RESULT_FAILED_HAL_UNAVAILABLE");
+ assertThat(
+ mChreCommunication.contextHubTransactionResultToString(9))
+ .isEqualTo("UNKNOWN_RESULT value=9");
+ }
+
+ @Test
public void testOnHubReset() {
+ mChreCommunication.start(
+ mChreCallback, Collections.singleton(ChreDiscoveryProvider.NANOAPP_ID));
mChreCommunication.onHubReset(mClient);
verify(mChreCallback).onHubReset();
}
@Test
public void testOnNanoAppLoaded() {
+ mChreCommunication.start(
+ mChreCallback, Collections.singleton(ChreDiscoveryProvider.NANOAPP_ID));
mChreCommunication.onNanoAppLoaded(mClient, ChreDiscoveryProvider.NANOAPP_ID);
verify(mChreCallback).onNanoAppRestart(eq(ChreDiscoveryProvider.NANOAPP_ID));
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java
index 7c0dd92..270de52 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/ChreDiscoveryProviderTest.java
@@ -16,16 +16,21 @@
package com.android.server.nearby.provider;
+import static com.google.common.truth.Truth.assertThat;
+
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import android.content.Context;
import android.hardware.location.NanoAppMessage;
-import android.nearby.ScanFilter;
+import android.nearby.DataElement;
+import android.nearby.NearbyDeviceParcelable;
import androidx.test.filters.SdkSuppress;
import androidx.test.platform.app.InstrumentationRegistry;
+import com.android.server.nearby.presence.PresenceDiscoveryResult;
+
import com.google.protobuf.ByteString;
import org.junit.Before;
@@ -35,17 +40,25 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import service.proto.Blefilter;
-
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
+import service.proto.Blefilter;
+
public class ChreDiscoveryProviderTest {
@Mock AbstractDiscoveryProvider.Listener mListener;
@Mock ChreCommunication mChreCommunication;
@Captor ArgumentCaptor<ChreCommunication.ContextHubCommsCallback> mChreCallbackCaptor;
+ @Captor ArgumentCaptor<NearbyDeviceParcelable> mNearbyDevice;
+
+ private static final int DATA_TYPE_CONNECTION_STATUS_KEY = 10;
+ private static final int DATA_TYPE_BATTERY_KEY = 11;
+ private static final int DATA_TYPE_TX_POWER_KEY = 5;
+ private static final int DATA_TYPE_BLUETOOTH_ADDR_KEY = 101;
+ private static final int DATA_TYPE_FP_ACCOUNT_KEY = 9;
+ private static final int DATA_TYPE_BLE_SERVICE_DATA_KEY = 100;
private ChreDiscoveryProvider mChreDiscoveryProvider;
@@ -59,13 +72,10 @@
@Test
@SdkSuppress(minSdkVersion = 32, codeName = "T")
- public void testOnStart() {
- List<ScanFilter> scanFilters = new ArrayList<>();
- mChreDiscoveryProvider.getController().setProviderScanFilters(scanFilters);
- mChreDiscoveryProvider.onStart();
+ public void testInit() {
+ mChreDiscoveryProvider.init();
verify(mChreCommunication).start(mChreCallbackCaptor.capture(), any());
mChreCallbackCaptor.getValue().started(true);
- verify(mChreCommunication).sendMessageToNanoApp(any());
}
@Test
@@ -93,12 +103,92 @@
ChreDiscoveryProvider.NANOAPP_MESSAGE_TYPE_FILTER_RESULT,
results.toByteArray());
mChreDiscoveryProvider.getController().setListener(mListener);
+ mChreDiscoveryProvider.init();
mChreDiscoveryProvider.onStart();
verify(mChreCommunication).start(mChreCallbackCaptor.capture(), any());
mChreCallbackCaptor.getValue().onMessageFromNanoApp(chre_message);
verify(mListener).onNearbyDeviceDiscovered(any());
}
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testOnNearbyDeviceDiscoveredWithDataElements() {
+ final byte [] connectionStatus = new byte[] {1, 2, 3};
+ final byte [] batteryStatus = new byte[] {4, 5, 6};
+ final byte [] txPower = new byte[] {2};
+ final byte [] bluetoothAddr = new byte[] {1, 2, 3, 4, 5, 6};
+ final byte [] fastPairAccountKey = new byte[16];
+ // First byte is length of service data, padding zeros should be thrown away.
+ final byte [] bleServiceData = new byte[] {5, 1, 2, 3, 4, 5, 0, 0, 0, 0};
+
+ final List<DataElement> expectedExtendedProperties = new ArrayList<>();
+ expectedExtendedProperties.add(new DataElement(DATA_TYPE_CONNECTION_STATUS_KEY,
+ connectionStatus));
+ expectedExtendedProperties.add(new DataElement(DATA_TYPE_BATTERY_KEY, batteryStatus));
+ expectedExtendedProperties.add(new DataElement(DATA_TYPE_TX_POWER_KEY, txPower));
+ expectedExtendedProperties.add(
+ new DataElement(DATA_TYPE_BLUETOOTH_ADDR_KEY, bluetoothAddr));
+ expectedExtendedProperties.add(
+ new DataElement(DATA_TYPE_FP_ACCOUNT_KEY, fastPairAccountKey));
+ expectedExtendedProperties.add(
+ new DataElement(DATA_TYPE_BLE_SERVICE_DATA_KEY, new byte[] {1, 2, 3, 4, 5}));
+
+ Blefilter.PublicCredential credential =
+ Blefilter.PublicCredential.newBuilder()
+ .setSecretId(ByteString.copyFrom(new byte[] {1}))
+ .setAuthenticityKey(ByteString.copyFrom(new byte[2]))
+ .setPublicKey(ByteString.copyFrom(new byte[3]))
+ .setEncryptedMetadata(ByteString.copyFrom(new byte[4]))
+ .setEncryptedMetadataTag(ByteString.copyFrom(new byte[5]))
+ .build();
+ Blefilter.BleFilterResult result =
+ Blefilter.BleFilterResult.newBuilder()
+ .setTxPower(2)
+ .setRssi(1)
+ .setBluetoothAddress(ByteString.copyFrom(bluetoothAddr))
+ .setBleServiceData(ByteString.copyFrom(bleServiceData))
+ .setPublicCredential(credential)
+ .addDataElement(Blefilter.DataElement.newBuilder()
+ .setKey(
+ Blefilter.DataElement.ElementType
+ .DE_CONNECTION_STATUS)
+ .setValue(ByteString.copyFrom(connectionStatus))
+ .setValueLength(connectionStatus.length)
+ )
+ .addDataElement(Blefilter.DataElement.newBuilder()
+ .setKey(
+ Blefilter.DataElement.ElementType
+ .DE_BATTERY_STATUS)
+ .setValue(ByteString.copyFrom(batteryStatus))
+ .setValueLength(batteryStatus.length)
+ )
+ .addDataElement(Blefilter.DataElement.newBuilder()
+ .setKey(
+ Blefilter.DataElement.ElementType
+ .DE_FAST_PAIR_ACCOUNT_KEY)
+ .setValue(ByteString.copyFrom(fastPairAccountKey))
+ .setValueLength(fastPairAccountKey.length)
+ )
+ .build();
+ Blefilter.BleFilterResults results =
+ Blefilter.BleFilterResults.newBuilder().addResult(result).build();
+ NanoAppMessage chre_message =
+ NanoAppMessage.createMessageToNanoApp(
+ ChreDiscoveryProvider.NANOAPP_ID,
+ ChreDiscoveryProvider.NANOAPP_MESSAGE_TYPE_FILTER_RESULT,
+ results.toByteArray());
+ mChreDiscoveryProvider.getController().setListener(mListener);
+ mChreDiscoveryProvider.init();
+ mChreDiscoveryProvider.onStart();
+ verify(mChreCommunication).start(mChreCallbackCaptor.capture(), any());
+ mChreCallbackCaptor.getValue().onMessageFromNanoApp(chre_message);
+ verify(mListener).onNearbyDeviceDiscovered(mNearbyDevice.capture());
+
+ List<DataElement> extendedProperties = PresenceDiscoveryResult
+ .fromDevice(mNearbyDevice.getValue()).getExtendedProperties();
+ assertThat(extendedProperties).containsExactlyElementsIn(expectedExtendedProperties);
+ }
+
private static class InLineExecutor implements Executor {
@Override
public void execute(Runnable command) {
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/DiscoveryProviderManagerTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/DiscoveryProviderManagerTest.java
new file mode 100644
index 0000000..91a0b56
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/DiscoveryProviderManagerTest.java
@@ -0,0 +1,352 @@
+/*
+ * 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.provider;
+
+import static android.nearby.PresenceCredential.IDENTITY_TYPE_PRIVATE;
+import static android.nearby.ScanRequest.SCAN_TYPE_NEARBY_PRESENCE;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.nearby.DataElement;
+import android.nearby.IScanListener;
+import android.nearby.NearbyDeviceParcelable;
+import android.nearby.PresenceScanFilter;
+import android.nearby.PublicCredential;
+import android.nearby.ScanRequest;
+import android.os.IBinder;
+
+import com.android.server.nearby.injector.Injector;
+import com.android.server.nearby.util.identity.CallerIdentity;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class DiscoveryProviderManagerTest {
+ private static final int SCAN_MODE_CHRE_ONLY = 3;
+ private static final int DATA_TYPE_SCAN_MODE = 102;
+ private static final int UID = 1234;
+ private static final int PID = 5678;
+ private static final String PACKAGE_NAME = "android.nearby.test";
+
+ @Mock Injector mInjector;
+ @Mock Context mContext;
+ @Mock AppOpsManager mAppOpsManager;
+ @Mock BleDiscoveryProvider mBleDiscoveryProvider;
+ @Mock ChreDiscoveryProvider mChreDiscoveryProvider;
+ @Mock DiscoveryProviderController mBluetoothController;
+ @Mock DiscoveryProviderController mChreController;
+ @Mock IScanListener mScanListener;
+ @Mock CallerIdentity mCallerIdentity;
+ @Mock DiscoveryProviderManager.ScanListenerDeathRecipient mScanListenerDeathRecipient;
+ @Mock IBinder mIBinder;
+
+ private DiscoveryProviderManager mDiscoveryProviderManager;
+ private Map<IBinder, DiscoveryProviderManager.ScanListenerRecord>
+ mScanTypeScanListenerRecordMap;
+
+ private static final int RSSI = -60;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ when(mInjector.getAppOpsManager()).thenReturn(mAppOpsManager);
+ when(mBleDiscoveryProvider.getController()).thenReturn(mBluetoothController);
+ when(mChreDiscoveryProvider.getController()).thenReturn(mChreController);
+
+ mScanTypeScanListenerRecordMap = new HashMap<>();
+ mDiscoveryProviderManager =
+ new DiscoveryProviderManager(mContext, mInjector, mBleDiscoveryProvider,
+ mChreDiscoveryProvider,
+ mScanTypeScanListenerRecordMap);
+ mCallerIdentity = CallerIdentity
+ .forTest(UID, PID, PACKAGE_NAME, /* attributionTag= */ null);
+ }
+
+ @Test
+ public void testOnNearbyDeviceDiscovered() {
+ NearbyDeviceParcelable nearbyDeviceParcelable = new NearbyDeviceParcelable.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .build();
+ mDiscoveryProviderManager.onNearbyDeviceDiscovered(nearbyDeviceParcelable);
+ }
+
+ @Test
+ public void testInvalidateProviderScanMode() {
+ mDiscoveryProviderManager.invalidateProviderScanMode();
+ }
+
+ @Test
+ public void testStartProviders_chreOnlyChreAvailable_bleProviderNotStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(true);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getChreOnlyPresenceScanFilter()).build();
+ DiscoveryProviderManager.ScanListenerRecord record =
+ new DiscoveryProviderManager.ScanListenerRecord(scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+ Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ verify(mBluetoothController, never()).start();
+ assertThat(start).isTrue();
+ }
+
+ @Test
+ public void testStartProviders_chreOnlyChreAvailable_multipleFilters_bleProviderNotStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(true);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getChreOnlyPresenceScanFilter())
+ .addScanFilter(getPresenceScanFilter()).build();
+ DiscoveryProviderManager.ScanListenerRecord record =
+ new DiscoveryProviderManager.ScanListenerRecord(scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+ Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ verify(mBluetoothController, never()).start();
+ assertThat(start).isTrue();
+ }
+
+ @Test
+ public void testStartProviders_chreOnlyChreUnavailable_bleProviderNotStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(false);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getChreOnlyPresenceScanFilter()).build();
+ DiscoveryProviderManager.ScanListenerRecord record =
+ new DiscoveryProviderManager.ScanListenerRecord(scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+ Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ verify(mBluetoothController, never()).start();
+ assertThat(start).isFalse();
+ }
+
+ @Test
+ public void testStartProviders_notChreOnlyChreAvailable_bleProviderNotStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(true);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getPresenceScanFilter()).build();
+ DiscoveryProviderManager.ScanListenerRecord record =
+ new DiscoveryProviderManager.ScanListenerRecord(scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+ Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ verify(mBluetoothController, never()).start();
+ assertThat(start).isTrue();
+ }
+
+ @Test
+ public void testStartProviders_notChreOnlyChreUnavailable_bleProviderStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(false);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getPresenceScanFilter()).build();
+ DiscoveryProviderManager.ScanListenerRecord record =
+ new DiscoveryProviderManager.ScanListenerRecord(scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+ Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ verify(mBluetoothController, atLeastOnce()).start();
+ assertThat(start).isTrue();
+ }
+
+ @Test
+ public void testStartProviders_chreOnlyChreUndetermined_bleProviderNotStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(null);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getChreOnlyPresenceScanFilter()).build();
+ DiscoveryProviderManager.ScanListenerRecord record =
+ new DiscoveryProviderManager.ScanListenerRecord(scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+ Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ verify(mBluetoothController, never()).start();
+ assertThat(start).isNull();
+ }
+
+ @Test
+ public void testStartProviders_notChreOnlyChreUndetermined_bleProviderStarted() {
+ when(mChreDiscoveryProvider.available()).thenReturn(null);
+
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getPresenceScanFilter()).build();
+ DiscoveryProviderManager.ScanListenerRecord record =
+ new DiscoveryProviderManager.ScanListenerRecord(scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+
+ Boolean start = mDiscoveryProviderManager.startProviders(scanRequest);
+ verify(mBluetoothController, atLeastOnce()).start();
+ assertThat(start).isTrue();
+ }
+
+ @Test
+ public void test_stopChreProvider_clearFilters() throws Exception {
+ // Cannot use mocked ChreDiscoveryProvider,
+ // so we cannot use class variable mDiscoveryProviderManager
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ DiscoveryProviderManager manager =
+ new DiscoveryProviderManager(mContext, mInjector, mBleDiscoveryProvider,
+ new ChreDiscoveryProvider(
+ mContext,
+ new ChreCommunication(mInjector, mContext, executor), executor),
+ mScanTypeScanListenerRecordMap);
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getPresenceScanFilter()).build();
+ DiscoveryProviderManager.ScanListenerRecord record =
+ new DiscoveryProviderManager.ScanListenerRecord(scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+ manager.startChreProvider(List.of(getPresenceScanFilter()));
+ // This is an asynchronized process. The filters will be set in executor thread. So we need
+ // to wait for some time to get the correct result.
+ Thread.sleep(200);
+
+ assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+ .isTrue();
+ assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+
+ manager.stopChreProvider();
+ Thread.sleep(200);
+ // The filters should be cleared right after.
+ assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+ .isFalse();
+ assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNull();
+ }
+
+ @Test
+ public void test_restartChreProvider() throws Exception {
+ // Cannot use mocked ChreDiscoveryProvider,
+ // so we cannot use class variable mDiscoveryProviderManager
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ DiscoveryProviderManager manager =
+ new DiscoveryProviderManager(mContext, mInjector, mBleDiscoveryProvider,
+ new ChreDiscoveryProvider(
+ mContext,
+ new ChreCommunication(mInjector, mContext, executor), executor),
+ mScanTypeScanListenerRecordMap);
+ ScanRequest scanRequest = new ScanRequest.Builder()
+ .setScanType(SCAN_TYPE_NEARBY_PRESENCE)
+ .addScanFilter(getPresenceScanFilter()).build();
+ DiscoveryProviderManager.ScanListenerRecord record =
+ new DiscoveryProviderManager.ScanListenerRecord(scanRequest, mScanListener,
+ mCallerIdentity, mScanListenerDeathRecipient);
+ mScanTypeScanListenerRecordMap.put(mIBinder, record);
+ manager.startChreProvider(List.of(getPresenceScanFilter()));
+ // This is an asynchronized process. The filters will be set in executor thread. So we need
+ // to wait for some time to get the correct result.
+ Thread.sleep(200);
+
+ assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+ .isTrue();
+ assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+
+ // We want to make sure quickly restart the provider the filters should
+ // be reset correctly.
+ // See b/255922206, there can be a race condition that filters get cleared because onStop()
+ // get executed after onStart() if they are called from different threads.
+ manager.stopChreProvider();
+ manager.mChreDiscoveryProvider.getController().setProviderScanFilters(
+ List.of(getPresenceScanFilter()));
+ manager.startChreProvider(List.of(getPresenceScanFilter()));
+ Thread.sleep(200);
+ assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+ .isTrue();
+ assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+
+ // Wait for enough time
+ Thread.sleep(1000);
+
+ assertThat(manager.mChreDiscoveryProvider.getController().isStarted())
+ .isTrue();
+ assertThat(manager.mChreDiscoveryProvider.getFiltersLocked()).isNotNull();
+ }
+
+ private static PresenceScanFilter getPresenceScanFilter() {
+ final byte[] secretId = new byte[]{1, 2, 3, 4};
+ final byte[] authenticityKey = new byte[]{0, 1, 1, 1};
+ final byte[] publicKey = new byte[]{1, 1, 2, 2};
+ final byte[] encryptedMetadata = new byte[]{1, 2, 3, 4, 5};
+ final byte[] metadataEncryptionKeyTag = new byte[]{1, 1, 3, 4, 5};
+
+ PublicCredential credential = new PublicCredential.Builder(
+ secretId, authenticityKey, publicKey, encryptedMetadata, metadataEncryptionKeyTag)
+ .setIdentityType(IDENTITY_TYPE_PRIVATE)
+ .build();
+
+ final int action = 123;
+ return new PresenceScanFilter.Builder()
+ .addCredential(credential)
+ .setMaxPathLoss(RSSI)
+ .addPresenceAction(action)
+ .build();
+ }
+
+ private static PresenceScanFilter getChreOnlyPresenceScanFilter() {
+ final byte[] secretId = new byte[]{1, 2, 3, 4};
+ final byte[] authenticityKey = new byte[]{0, 1, 1, 1};
+ final byte[] publicKey = new byte[]{1, 1, 2, 2};
+ final byte[] encryptedMetadata = new byte[]{1, 2, 3, 4, 5};
+ final byte[] metadataEncryptionKeyTag = new byte[]{1, 1, 3, 4, 5};
+
+ PublicCredential credential = new PublicCredential.Builder(
+ secretId, authenticityKey, publicKey, encryptedMetadata, metadataEncryptionKeyTag)
+ .setIdentityType(IDENTITY_TYPE_PRIVATE)
+ .build();
+
+ final int action = 123;
+ DataElement scanModeElement = new DataElement(DATA_TYPE_SCAN_MODE,
+ new byte[]{SCAN_MODE_CHRE_ONLY});
+ return new PresenceScanFilter.Builder()
+ .addCredential(credential)
+ .setMaxPathLoss(RSSI)
+ .addPresenceAction(action)
+ .addExtendedProperty(scanModeElement)
+ .build();
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/FastPairDataProviderTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/FastPairDataProviderTest.java
new file mode 100644
index 0000000..300efbd
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/FastPairDataProviderTest.java
@@ -0,0 +1,376 @@
+/*
+ * 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.provider;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.accounts.Account;
+import android.content.Context;
+import android.nearby.FastPairDataProviderService;
+import android.nearby.aidl.FastPairAccountDevicesMetadataRequestParcel;
+import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataParcel;
+import android.nearby.aidl.FastPairAntispoofKeyDeviceMetadataRequestParcel;
+import android.nearby.aidl.FastPairDeviceMetadataParcel;
+import android.nearby.aidl.FastPairEligibleAccountParcel;
+import android.nearby.aidl.FastPairEligibleAccountsRequestParcel;
+import android.nearby.aidl.FastPairManageAccountDeviceRequestParcel;
+import android.nearby.aidl.FastPairManageAccountRequestParcel;
+
+import androidx.test.filters.SdkSuppress;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.server.nearby.fastpair.footprint.FastPairUploadInfo;
+
+import com.google.common.collect.ImmutableList;
+import com.google.protobuf.ByteString;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import service.proto.Cache;
+import service.proto.FastPairString;
+import service.proto.Rpcs;
+
+public class FastPairDataProviderTest {
+
+ private static final Account ACCOUNT = new Account("abc@google.com", "type1");
+ private static final byte[] MODEL_ID = new byte[]{7, 9};
+ private static final int BLE_TX_POWER = 5;
+ private static final String CONNECT_SUCCESS_COMPANION_APP_INSTALLED =
+ "CONNECT_SUCCESS_COMPANION_APP_INSTALLED";
+ private static final String CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED =
+ "CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED";
+ private static final int DEVICE_TYPE = 1;
+ private static final String DOWNLOAD_COMPANION_APP_DESCRIPTION =
+ "DOWNLOAD_COMPANION_APP_DESCRIPTION";
+ private static final String FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION =
+ "FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION";
+ private static final byte[] IMAGE = new byte[]{7, 9};
+ private static final String IMAGE_URL = "IMAGE_URL";
+ private static final String INITIAL_NOTIFICATION_DESCRIPTION =
+ "INITIAL_NOTIFICATION_DESCRIPTION";
+ private static final String INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT =
+ "INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT";
+ private static final String INITIAL_PAIRING_DESCRIPTION = "INITIAL_PAIRING_DESCRIPTION";
+ private static final String INTENT_URI = "INTENT_URI";
+ private static final String OPEN_COMPANION_APP_DESCRIPTION = "OPEN_COMPANION_APP_DESCRIPTION";
+ private static final String RETRO_ACTIVE_PAIRING_DESCRIPTION =
+ "RETRO_ACTIVE_PAIRING_DESCRIPTION";
+ private static final String SUBSEQUENT_PAIRING_DESCRIPTION = "SUBSEQUENT_PAIRING_DESCRIPTION";
+ private static final float TRIGGER_DISTANCE = 111;
+ private static final String TRUE_WIRELESS_IMAGE_URL_CASE = "TRUE_WIRELESS_IMAGE_URL_CASE";
+ private static final String TRUE_WIRELESS_IMAGE_URL_LEFT_BUD =
+ "TRUE_WIRELESS_IMAGE_URL_LEFT_BUD";
+ private static final String TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD =
+ "TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD";
+ private static final String UNABLE_TO_CONNECT_DESCRIPTION = "UNABLE_TO_CONNECT_DESCRIPTION";
+ private static final String UNABLE_TO_CONNECT_TITLE = "UNABLE_TO_CONNECT_TITLE";
+ private static final String UPDATE_COMPANION_APP_DESCRIPTION =
+ "UPDATE_COMPANION_APP_DESCRIPTION";
+ private static final String WAIT_LAUNCH_COMPANION_APP_DESCRIPTION =
+ "WAIT_LAUNCH_COMPANION_APP_DESCRIPTION";
+ private static final byte[] ACCOUNT_KEY = new byte[]{3};
+ private static final byte[] SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS = new byte[]{2, 8};
+ private static final byte[] ANTI_SPOOFING_KEY = new byte[]{4, 5, 6};
+ private static final String ACTION_URL = "ACTION_URL";
+ private static final String APP_NAME = "APP_NAME";
+ private static final byte[] AUTHENTICATION_PUBLIC_KEY_SEC_P256R1 = new byte[]{5, 7};
+ private static final String DESCRIPTION = "DESCRIPTION";
+ private static final String DEVICE_NAME = "DEVICE_NAME";
+ private static final String DISPLAY_URL = "DISPLAY_URL";
+ private static final long FIRST_OBSERVATION_TIMESTAMP_MILLIS = 8393L;
+ private static final String ICON_FIFE_URL = "ICON_FIFE_URL";
+ private static final byte[] ICON_PNG = new byte[]{2, 5};
+ private static final String ID = "ID";
+ private static final long LAST_OBSERVATION_TIMESTAMP_MILLIS = 934234L;
+ private static final String MAC_ADDRESS = "MAC_ADDRESS";
+ private static final String NAME = "NAME";
+ private static final String PACKAGE_NAME = "PACKAGE_NAME";
+ private static final long PENDING_APP_INSTALL_TIMESTAMP_MILLIS = 832393L;
+ private static final int RSSI = 9;
+ private static final String TITLE = "TITLE";
+ private static final String TRIGGER_ID = "TRIGGER_ID";
+ private static final int TX_POWER = 63;
+
+ @Mock ProxyFastPairDataProvider mProxyFastPairDataProvider;
+
+ FastPairDataProvider mFastPairDataProvider;
+ FastPairEligibleAccountParcel[] mFastPairEligibleAccountParcels =
+ { genHappyPathFastPairEligibleAccountParcel() };
+ FastPairAntispoofKeyDeviceMetadataParcel mFastPairAntispoofKeyDeviceMetadataParcel =
+ genHappyPathFastPairAntispoofKeyDeviceMetadataParcel();
+ FastPairUploadInfo mFastPairUploadInfo = genHappyPathFastPairUploadInfo();
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ Context context = InstrumentationRegistry.getInstrumentation().getContext();
+ mFastPairDataProvider = FastPairDataProvider.init(context);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testFailurePath_throwsException() throws IllegalStateException {
+ mFastPairDataProvider = FastPairDataProvider.getInstance();
+ assertThrows(
+ IllegalStateException.class,
+ () -> {
+ mFastPairDataProvider.loadFastPairEligibleAccounts(); });
+ assertThrows(
+ IllegalStateException.class,
+ () -> {
+ mFastPairDataProvider.loadFastPairAntispoofKeyDeviceMetadata(MODEL_ID); });
+ assertThrows(
+ IllegalStateException.class,
+ () -> {
+ mFastPairDataProvider.loadFastPairDeviceWithAccountKey(ACCOUNT); });
+ assertThrows(
+ IllegalStateException.class,
+ () -> {
+ mFastPairDataProvider.loadFastPairDeviceWithAccountKey(
+ ACCOUNT, ImmutableList.of()); });
+ assertThrows(
+ IllegalStateException.class,
+ () -> {
+ mFastPairDataProvider.optIn(ACCOUNT); });
+ assertThrows(
+ IllegalStateException.class,
+ () -> {
+ mFastPairDataProvider.upload(ACCOUNT, mFastPairUploadInfo); });
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testLoadFastPairAntispoofKeyDeviceMetadata_receivesResponse() {
+ mFastPairDataProvider.setProxyDataProvider(mProxyFastPairDataProvider);
+ when(mProxyFastPairDataProvider.loadFastPairAntispoofKeyDeviceMetadata(any()))
+ .thenReturn(mFastPairAntispoofKeyDeviceMetadataParcel);
+
+ mFastPairDataProvider.loadFastPairAntispoofKeyDeviceMetadata(MODEL_ID);
+ ArgumentCaptor<FastPairAntispoofKeyDeviceMetadataRequestParcel> captor =
+ ArgumentCaptor.forClass(FastPairAntispoofKeyDeviceMetadataRequestParcel.class);
+ verify(mProxyFastPairDataProvider).loadFastPairAntispoofKeyDeviceMetadata(captor.capture());
+ assertThat(captor.getValue().modelId).isSameInstanceAs(MODEL_ID);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testOptIn_finishesSuccessfully() {
+ mFastPairDataProvider.setProxyDataProvider(mProxyFastPairDataProvider);
+ doNothing().when(mProxyFastPairDataProvider).manageFastPairAccount(any());
+ mFastPairDataProvider.optIn(ACCOUNT);
+ ArgumentCaptor<FastPairManageAccountRequestParcel> captor =
+ ArgumentCaptor.forClass(FastPairManageAccountRequestParcel.class);
+ verify(mProxyFastPairDataProvider).manageFastPairAccount(captor.capture());
+ assertThat(captor.getValue().account).isSameInstanceAs(ACCOUNT);
+ assertThat(captor.getValue().requestType).isEqualTo(
+ FastPairDataProviderService.MANAGE_REQUEST_ADD);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testUpload_finishesSuccessfully() {
+ mFastPairDataProvider.setProxyDataProvider(mProxyFastPairDataProvider);
+ doNothing().when(mProxyFastPairDataProvider).manageFastPairAccountDevice(any());
+ mFastPairDataProvider.upload(ACCOUNT, mFastPairUploadInfo);
+ ArgumentCaptor<FastPairManageAccountDeviceRequestParcel> captor =
+ ArgumentCaptor.forClass(FastPairManageAccountDeviceRequestParcel.class);
+ verify(mProxyFastPairDataProvider).manageFastPairAccountDevice(captor.capture());
+ assertThat(captor.getValue().account).isSameInstanceAs(ACCOUNT);
+ assertThat(captor.getValue().requestType).isEqualTo(
+ FastPairDataProviderService.MANAGE_REQUEST_ADD);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testLoadFastPairEligibleAccounts_receivesOneAccount() {
+ mFastPairDataProvider.setProxyDataProvider(mProxyFastPairDataProvider);
+ when(mProxyFastPairDataProvider.loadFastPairEligibleAccounts(any()))
+ .thenReturn(mFastPairEligibleAccountParcels);
+ assertThat(mFastPairDataProvider.loadFastPairEligibleAccounts().size())
+ .isEqualTo(1);
+ ArgumentCaptor<FastPairEligibleAccountsRequestParcel> captor =
+ ArgumentCaptor.forClass(FastPairEligibleAccountsRequestParcel.class);
+ verify(mProxyFastPairDataProvider).loadFastPairEligibleAccounts(captor.capture());
+ assertThat(captor.getValue()).isNotNull();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testLoadFastPairDeviceWithAccountKey_finishesSuccessfully() {
+ mFastPairDataProvider.setProxyDataProvider(mProxyFastPairDataProvider);
+ when(mProxyFastPairDataProvider.loadFastPairAccountDevicesMetadata(any()))
+ .thenReturn(null);
+
+ mFastPairDataProvider.loadFastPairDeviceWithAccountKey(ACCOUNT);
+ ArgumentCaptor<FastPairAccountDevicesMetadataRequestParcel> captor =
+ ArgumentCaptor.forClass(FastPairAccountDevicesMetadataRequestParcel.class);
+ verify(mProxyFastPairDataProvider).loadFastPairAccountDevicesMetadata(captor.capture());
+ assertThat(captor.getValue().account).isSameInstanceAs(ACCOUNT);
+ assertThat(captor.getValue().deviceAccountKeys).isEmpty();
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testLoadFastPairDeviceWithAccountKeyDeviceAccountKeys_finishesSuccessfully() {
+ mFastPairDataProvider.setProxyDataProvider(mProxyFastPairDataProvider);
+ when(mProxyFastPairDataProvider.loadFastPairAccountDevicesMetadata(any()))
+ .thenReturn(null);
+
+ mFastPairDataProvider.loadFastPairDeviceWithAccountKey(
+ ACCOUNT, ImmutableList.of(ACCOUNT_KEY));
+ ArgumentCaptor<FastPairAccountDevicesMetadataRequestParcel> captor =
+ ArgumentCaptor.forClass(FastPairAccountDevicesMetadataRequestParcel.class);
+ verify(mProxyFastPairDataProvider).loadFastPairAccountDevicesMetadata(captor.capture());
+ assertThat(captor.getValue().account).isSameInstanceAs(ACCOUNT);
+ assertThat(captor.getValue().deviceAccountKeys.length).isEqualTo(1);
+ assertThat(captor.getValue().deviceAccountKeys[0].byteArray).isSameInstanceAs(ACCOUNT_KEY);
+ }
+
+ private static FastPairEligibleAccountParcel genHappyPathFastPairEligibleAccountParcel() {
+ FastPairEligibleAccountParcel parcel = new FastPairEligibleAccountParcel();
+ parcel.account = ACCOUNT;
+ parcel.optIn = true;
+
+ return parcel;
+ }
+
+ private static FastPairAntispoofKeyDeviceMetadataParcel
+ genHappyPathFastPairAntispoofKeyDeviceMetadataParcel() {
+ FastPairAntispoofKeyDeviceMetadataParcel parcel =
+ new FastPairAntispoofKeyDeviceMetadataParcel();
+ parcel.antispoofPublicKey = ANTI_SPOOFING_KEY;
+ parcel.deviceMetadata = genHappyPathFastPairDeviceMetadataParcel();
+
+ return parcel;
+ }
+
+ private static FastPairDeviceMetadataParcel genHappyPathFastPairDeviceMetadataParcel() {
+ FastPairDeviceMetadataParcel parcel = new FastPairDeviceMetadataParcel();
+
+ parcel.bleTxPower = BLE_TX_POWER;
+ parcel.connectSuccessCompanionAppInstalled = CONNECT_SUCCESS_COMPANION_APP_INSTALLED;
+ parcel.connectSuccessCompanionAppNotInstalled =
+ CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED;
+ parcel.deviceType = DEVICE_TYPE;
+ parcel.downloadCompanionAppDescription = DOWNLOAD_COMPANION_APP_DESCRIPTION;
+ parcel.failConnectGoToSettingsDescription = FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION;
+ parcel.image = IMAGE;
+ parcel.imageUrl = IMAGE_URL;
+ parcel.initialNotificationDescription = INITIAL_NOTIFICATION_DESCRIPTION;
+ parcel.initialNotificationDescriptionNoAccount =
+ INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT;
+ parcel.initialPairingDescription = INITIAL_PAIRING_DESCRIPTION;
+ parcel.intentUri = INTENT_URI;
+ parcel.name = NAME;
+ parcel.openCompanionAppDescription = OPEN_COMPANION_APP_DESCRIPTION;
+ parcel.retroactivePairingDescription = RETRO_ACTIVE_PAIRING_DESCRIPTION;
+ parcel.subsequentPairingDescription = SUBSEQUENT_PAIRING_DESCRIPTION;
+ parcel.triggerDistance = TRIGGER_DISTANCE;
+ parcel.trueWirelessImageUrlCase = TRUE_WIRELESS_IMAGE_URL_CASE;
+ parcel.trueWirelessImageUrlLeftBud = TRUE_WIRELESS_IMAGE_URL_LEFT_BUD;
+ parcel.trueWirelessImageUrlRightBud = TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD;
+ parcel.unableToConnectDescription = UNABLE_TO_CONNECT_DESCRIPTION;
+ parcel.unableToConnectTitle = UNABLE_TO_CONNECT_TITLE;
+ parcel.updateCompanionAppDescription = UPDATE_COMPANION_APP_DESCRIPTION;
+ parcel.waitLaunchCompanionAppDescription = WAIT_LAUNCH_COMPANION_APP_DESCRIPTION;
+
+ return parcel;
+ }
+
+ private static Cache.StoredDiscoveryItem genHappyPathStoredDiscoveryItem() {
+ Cache.StoredDiscoveryItem.Builder storedDiscoveryItemBuilder =
+ Cache.StoredDiscoveryItem.newBuilder();
+ storedDiscoveryItemBuilder.setActionUrl(ACTION_URL);
+ storedDiscoveryItemBuilder.setActionUrlType(Cache.ResolvedUrlType.WEBPAGE);
+ storedDiscoveryItemBuilder.setAppName(APP_NAME);
+ storedDiscoveryItemBuilder.setAuthenticationPublicKeySecp256R1(
+ ByteString.copyFrom(AUTHENTICATION_PUBLIC_KEY_SEC_P256R1));
+ storedDiscoveryItemBuilder.setDescription(DESCRIPTION);
+ storedDiscoveryItemBuilder.setDeviceName(DEVICE_NAME);
+ storedDiscoveryItemBuilder.setDisplayUrl(DISPLAY_URL);
+ storedDiscoveryItemBuilder.setFirstObservationTimestampMillis(
+ FIRST_OBSERVATION_TIMESTAMP_MILLIS);
+ storedDiscoveryItemBuilder.setIconFifeUrl(ICON_FIFE_URL);
+ storedDiscoveryItemBuilder.setIconPng(ByteString.copyFrom(ICON_PNG));
+ storedDiscoveryItemBuilder.setId(ID);
+ storedDiscoveryItemBuilder.setLastObservationTimestampMillis(
+ LAST_OBSERVATION_TIMESTAMP_MILLIS);
+ storedDiscoveryItemBuilder.setMacAddress(MAC_ADDRESS);
+ storedDiscoveryItemBuilder.setPackageName(PACKAGE_NAME);
+ storedDiscoveryItemBuilder.setPendingAppInstallTimestampMillis(
+ PENDING_APP_INSTALL_TIMESTAMP_MILLIS);
+ storedDiscoveryItemBuilder.setRssi(RSSI);
+ storedDiscoveryItemBuilder.setState(Cache.StoredDiscoveryItem.State.STATE_ENABLED);
+ storedDiscoveryItemBuilder.setTitle(TITLE);
+ storedDiscoveryItemBuilder.setTriggerId(TRIGGER_ID);
+ storedDiscoveryItemBuilder.setTxPower(TX_POWER);
+
+ FastPairString.FastPairStrings.Builder stringsBuilder =
+ FastPairString.FastPairStrings.newBuilder();
+ stringsBuilder.setPairingFinishedCompanionAppInstalled(
+ CONNECT_SUCCESS_COMPANION_APP_INSTALLED);
+ stringsBuilder.setPairingFinishedCompanionAppNotInstalled(
+ CONNECT_SUCCESS_COMPANION_APP_NOT_INSTALLED);
+ stringsBuilder.setPairingFailDescription(
+ FAIL_CONNECT_GOTO_SETTINGS_DESCRIPTION);
+ stringsBuilder.setTapToPairWithAccount(
+ INITIAL_NOTIFICATION_DESCRIPTION);
+ stringsBuilder.setTapToPairWithoutAccount(
+ INITIAL_NOTIFICATION_DESCRIPTION_NO_ACCOUNT);
+ stringsBuilder.setInitialPairingDescription(INITIAL_PAIRING_DESCRIPTION);
+ stringsBuilder.setRetroactivePairingDescription(RETRO_ACTIVE_PAIRING_DESCRIPTION);
+ stringsBuilder.setSubsequentPairingDescription(SUBSEQUENT_PAIRING_DESCRIPTION);
+ stringsBuilder.setWaitAppLaunchDescription(WAIT_LAUNCH_COMPANION_APP_DESCRIPTION);
+ storedDiscoveryItemBuilder.setFastPairStrings(stringsBuilder.build());
+
+ Cache.FastPairInformation.Builder fpInformationBuilder =
+ Cache.FastPairInformation.newBuilder();
+ Rpcs.TrueWirelessHeadsetImages.Builder imagesBuilder =
+ Rpcs.TrueWirelessHeadsetImages.newBuilder();
+ imagesBuilder.setCaseUrl(TRUE_WIRELESS_IMAGE_URL_CASE);
+ imagesBuilder.setLeftBudUrl(TRUE_WIRELESS_IMAGE_URL_LEFT_BUD);
+ imagesBuilder.setRightBudUrl(TRUE_WIRELESS_IMAGE_URL_RIGHT_BUD);
+ fpInformationBuilder.setTrueWirelessImages(imagesBuilder.build());
+ fpInformationBuilder.setDeviceType(Rpcs.DeviceType.HEADPHONES);
+
+ storedDiscoveryItemBuilder.setFastPairInformation(fpInformationBuilder.build());
+ storedDiscoveryItemBuilder.setTxPower(TX_POWER);
+
+ storedDiscoveryItemBuilder.setIconPng(ByteString.copyFrom(ICON_PNG));
+ storedDiscoveryItemBuilder.setIconFifeUrl(ICON_FIFE_URL);
+ storedDiscoveryItemBuilder.setActionUrl(ACTION_URL);
+
+ return storedDiscoveryItemBuilder.build();
+ }
+
+ private static FastPairUploadInfo genHappyPathFastPairUploadInfo() {
+ return new FastPairUploadInfo(
+ genHappyPathStoredDiscoveryItem(),
+ ByteString.copyFrom(ACCOUNT_KEY),
+ ByteString.copyFrom(SHA256_ACCOUNT_KEY_PUBLIC_ADDRESS));
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/provider/UtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/provider/UtilsTest.java
index eeea319..35f87f1 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/provider/UtilsTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/provider/UtilsTest.java
@@ -110,8 +110,6 @@
private static final byte[] ICON_PNG = new byte[]{2, 5};
private static final String ID = "ID";
private static final long LAST_OBSERVATION_TIMESTAMP_MILLIS = 934234L;
- private static final int LAST_USER_EXPERIENCE = 1;
- private static final long LOST_MILLIS = 393284L;
private static final String MAC_ADDRESS = "MAC_ADDRESS";
private static final String NAME = "NAME";
private static final String PACKAGE_NAME = "PACKAGE_NAME";
@@ -121,7 +119,6 @@
private static final String TITLE = "TITLE";
private static final String TRIGGER_ID = "TRIGGER_ID";
private static final int TX_POWER = 63;
- private static final int TYPE = 1;
@Test
@SdkSuppress(minSdkVersion = 32, codeName = "T")
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java
new file mode 100644
index 0000000..0fe28df
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/ArrayUtilsTest.java
@@ -0,0 +1,62 @@
+/*
+ * 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.util;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.filters.SdkSuppress;
+
+import org.junit.Test;
+
+public final class ArrayUtilsTest {
+
+ private static final byte[] BYTES_ONE = new byte[] {7, 9};
+ private static final byte[] BYTES_TWO = new byte[] {8};
+ private static final byte[] BYTES_EMPTY = new byte[] {};
+ private static final byte[] BYTES_ALL = new byte[] {7, 9, 8};
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testConcatByteArraysNoInput() {
+ assertThat(ArrayUtils.concatByteArrays().length).isEqualTo(0);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testConcatByteArraysOneEmptyArray() {
+ assertThat(ArrayUtils.concatByteArrays(BYTES_EMPTY).length).isEqualTo(0);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testConcatByteArraysOneNonEmptyArray() {
+ assertThat(ArrayUtils.concatByteArrays(BYTES_ONE)).isEqualTo(BYTES_ONE);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testConcatByteArraysMultipleNonEmptyArrays() {
+ assertThat(ArrayUtils.concatByteArrays(BYTES_ONE, BYTES_TWO)).isEqualTo(BYTES_ALL);
+ }
+
+ @Test
+ @SdkSuppress(minSdkVersion = 32, codeName = "T")
+ public void testConcatByteArraysMultipleArrays() {
+ assertThat(ArrayUtils.concatByteArrays(BYTES_ONE, BYTES_EMPTY, BYTES_TWO))
+ .isEqualTo(BYTES_ALL);
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/BroadcastPermissionsTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/BroadcastPermissionsTest.java
index 1a22412..71ade2a 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/util/BroadcastPermissionsTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/BroadcastPermissionsTest.java
@@ -93,4 +93,9 @@
assertThat(BroadcastPermissions.getPermissionLevel(mMockContext, UID, PID))
.isEqualTo(PERMISSION_BLUETOOTH_ADVERTISE);
}
+
+ @Test
+ public void test_enforceBroadcastPermission() {
+ BroadcastPermissions.enforceBroadcastPermission(mMockContext, mCallerIdentity);
+ }
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/DataUtilsTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/DataUtilsTest.java
index f098600..a742b3d 100644
--- a/nearby/tests/unit/src/com/android/server/nearby/util/DataUtilsTest.java
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/DataUtilsTest.java
@@ -57,10 +57,6 @@
private static final String MESSAGE_RETROACTIVE_PAIR_DESCRIPTION = "message 7";
private static final String MESSAGE_WAIT_LAUNCH_COMPANION_APP_DESCRIPTION = "message 8";
private static final String MESSAGE_FAIL_CONNECT_DESCRIPTION = "message 9";
- private static final String MESSAGE_FAST_PAIR_TV_CONNECT_DEVICE_NO_ACCOUNT_DESCRIPTION =
- "message 10";
- private static final String MESSAGE_ASSISTANT_HALF_SHEET_DESCRIPTION = "message 11";
- private static final String MESSAGE_ASSISTANT_NOTIFICATION_DESCRIPTION = "message 12";
@Test
public void test_toScanFastPairStoreItem_withAccount() {
@@ -107,6 +103,24 @@
public void test_toString() {
Cache.ScanFastPairStoreItem item = DataUtils.toScanFastPairStoreItem(
createObservedDeviceResponse(), BLUETOOTH_ADDRESS, ACCOUNT);
+
+ assertThat(DataUtils.toString(item))
+ .isEqualTo("ScanFastPairStoreItem=[address:00:11:22:33:FF:EE, "
+ + "actionUrl:intent:#Intent;action=cto_be_set%3AACTION_MAGIC_PAIR;"
+ + "package=to_be_set;component=to_be_set;"
+ + "to_be_set%3AEXTRA_COMPANION_APP=test_package;"
+ + "end, deviceName:My device, "
+ + "iconFifeUrl:device_image_url, "
+ + "fastPairStrings:FastPairStrings[tapToPairWithAccount=message 1, "
+ + "tapToPairWithoutAccount=message 2, "
+ + "initialPairingDescription=message 3 My device, "
+ + "pairingFinishedCompanionAppInstalled=message 4, "
+ + "pairingFinishedCompanionAppNotInstalled=message 5, "
+ + "subsequentPairingDescription=message 6, "
+ + "retroactivePairingDescription=message 7, "
+ + "waitAppLaunchDescription=message 8, "
+ + "pairingFailDescription=message 9]]");
+
FastPairStrings strings = item.getFastPairStrings();
assertThat(DataUtils.toString(strings))
diff --git a/Cronet/tests/cts/src/android/net/http/cts/util/CronetCtsTestServer.kt b/nearby/tests/unit/src/com/android/server/nearby/util/EnvironmentTest.java
similarity index 64%
copy from Cronet/tests/cts/src/android/net/http/cts/util/CronetCtsTestServer.kt
copy to nearby/tests/unit/src/com/android/server/nearby/util/EnvironmentTest.java
index 3ccb571..e167cf4 100644
--- a/Cronet/tests/cts/src/android/net/http/cts/util/CronetCtsTestServer.kt
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/EnvironmentTest.java
@@ -14,13 +14,15 @@
* limitations under the License.
*/
-package android.net.http.cts.util
+package com.android.server.nearby.util;
-import android.content.Context
-import android.webkit.cts.CtsTestServer
+import org.junit.Test;
-/** Extends CtsTestServer to handle POST requests and other cronet specific test requests */
-class CronetCtsTestServer(context: Context) : CtsTestServer(context) {
+public class EnvironmentTest {
- val successUrl: String = getAssetUrl("html/hello_world.html")
+ @Test
+ public void getNearbyDirectory() {
+ Environment.getNearbyDirectory();
+ Environment.getNearbyDirectory(1);
+ }
}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpIdentityV1Test.java b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpIdentityV1Test.java
new file mode 100644
index 0000000..f0294fc
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpIdentityV1Test.java
@@ -0,0 +1,76 @@
+/*
+ * 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.util.encryption;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.util.Log;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+
+public class CryptorImpIdentityV1Test {
+ private static final String TAG = "CryptorImpIdentityV1Test";
+ private static final byte[] SALT = new byte[] {102, 22};
+ private static final byte[] DATA =
+ new byte[] {107, -102, 101, 107, 20, 62, 2, 73, 113, 59, 8, -14, -58, 122};
+ private static final byte[] AUTHENTICITY_KEY =
+ new byte[] {-89, 88, -50, -42, -99, 57, 84, -24, 121, 1, -104, -8, -26, -73, -36, 100};
+
+ @Test
+ public void test_encrypt_decrypt() {
+ Cryptor identityCryptor = CryptorImpIdentityV1.getInstance();
+ byte[] encryptedData = identityCryptor.encrypt(DATA, SALT, AUTHENTICITY_KEY);
+
+ assertThat(identityCryptor.decrypt(encryptedData, SALT, AUTHENTICITY_KEY)).isEqualTo(DATA);
+ }
+
+ @Test
+ public void test_encryption() {
+ Cryptor identityCryptor = CryptorImpIdentityV1.getInstance();
+ byte[] encryptedData = identityCryptor.encrypt(DATA, SALT, AUTHENTICITY_KEY);
+
+ // for debugging
+ Log.d(TAG, "encrypted data is: " + Arrays.toString(encryptedData));
+
+ assertThat(encryptedData).isEqualTo(getEncryptedData());
+ }
+
+ @Test
+ public void test_decryption() {
+ Cryptor identityCryptor = CryptorImpIdentityV1.getInstance();
+ byte[] decryptedData =
+ identityCryptor.decrypt(getEncryptedData(), SALT, AUTHENTICITY_KEY);
+ // for debugging
+ Log.d(TAG, "decrypted data is: " + Arrays.toString(decryptedData));
+
+ assertThat(decryptedData).isEqualTo(DATA);
+ }
+
+ @Test
+ public void generateHmacTag() {
+ CryptorImpIdentityV1 identityCryptor = CryptorImpIdentityV1.getInstance();
+ byte[] generatedTag = identityCryptor.sign(DATA);
+ byte[] expectedTag = new byte[]{50, 116, 95, -87, 63, 123, -79, -43};
+ assertThat(generatedTag).isEqualTo(expectedTag);
+ }
+
+ private static byte[] getEncryptedData() {
+ return new byte[]{6, -31, -32, -123, 43, -92, -47, -110, -65, 126, -15, -51, -19, -43};
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpV1Test.java b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpV1Test.java
new file mode 100644
index 0000000..3ca2575
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorImpV1Test.java
@@ -0,0 +1,114 @@
+/*
+ * 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.util.encryption;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.util.Log;
+
+import org.junit.Test;
+
+import java.util.Arrays;
+
+/**
+ * Unit test for {@link CryptorImpV1}
+ */
+public final class CryptorImpV1Test {
+ private static final String TAG = "CryptorImpV1Test";
+ private static final byte[] SALT = new byte[] {102, 22};
+ private static final byte[] DATA =
+ new byte[] {107, -102, 101, 107, 20, 62, 2, 73, 113, 59, 8, -14, -58, 122};
+ private static final byte[] AUTHENTICITY_KEY =
+ new byte[] {-89, 88, -50, -42, -99, 57, 84, -24, 121, 1, -104, -8, -26, -73, -36, 100};
+
+ @Test
+ public void test_encryption() {
+ Cryptor v1Cryptor = CryptorImpV1.getInstance();
+ byte[] encryptedData = v1Cryptor.encrypt(DATA, SALT, AUTHENTICITY_KEY);
+
+ // for debugging
+ Log.d(TAG, "encrypted data is: " + Arrays.toString(encryptedData));
+
+ assertThat(encryptedData).isEqualTo(getEncryptedData());
+ }
+
+ @Test
+ public void test_encryption_invalidInput() {
+ Cryptor v1Cryptor = CryptorImpV1.getInstance();
+ assertThat(v1Cryptor.encrypt(DATA, SALT, new byte[]{1, 2, 3, 4, 6})).isNull();
+ }
+
+ @Test
+ public void test_decryption() {
+ Cryptor v1Cryptor = CryptorImpV1.getInstance();
+ byte[] decryptedData =
+ v1Cryptor.decrypt(getEncryptedData(), SALT, AUTHENTICITY_KEY);
+ // for debugging
+ Log.d(TAG, "decrypted data is: " + Arrays.toString(decryptedData));
+
+ assertThat(decryptedData).isEqualTo(DATA);
+ }
+
+ @Test
+ public void test_decryption_invalidInput() {
+ Cryptor v1Cryptor = CryptorImpV1.getInstance();
+ assertThat(v1Cryptor.decrypt(getEncryptedData(), SALT, new byte[]{1, 2, 3, 4, 6})).isNull();
+ }
+
+ @Test
+ public void generateSign() {
+ CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
+ byte[] generatedTag = v1Cryptor.sign(DATA, AUTHENTICITY_KEY);
+ byte[] expectedTag = new byte[]{
+ 100, 88, -104, 80, -66, 107, -38, 95, 34, 40, -56, -23, -90, 90, -87, 12};
+ assertThat(generatedTag).isEqualTo(expectedTag);
+ }
+
+ @Test
+ public void test_verify() {
+ CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
+ byte[] expectedTag = new byte[]{
+ 100, 88, -104, 80, -66, 107, -38, 95, 34, 40, -56, -23, -90, 90, -87, 12};
+
+ assertThat(v1Cryptor.verify(DATA, AUTHENTICITY_KEY, expectedTag)).isTrue();
+ assertThat(v1Cryptor.verify(DATA, AUTHENTICITY_KEY, DATA)).isFalse();
+ }
+
+ @Test
+ public void test_generateHmacTag_sameResult() {
+ CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
+ byte[] res1 = v1Cryptor.generateHmacTag(DATA, AUTHENTICITY_KEY);
+ assertThat(res1)
+ .isEqualTo(v1Cryptor.generateHmacTag(DATA, AUTHENTICITY_KEY));
+ }
+
+ @Test
+ public void test_generateHmacTag_nullData() {
+ CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
+ assertThat(v1Cryptor.generateHmacTag(/* data= */ null, AUTHENTICITY_KEY)).isNull();
+ }
+
+ @Test
+ public void test_generateHmacTag_nullKey() {
+ CryptorImpV1 v1Cryptor = CryptorImpV1.getInstance();
+ assertThat(v1Cryptor.generateHmacTag(DATA, /* authenticityKey= */ null)).isNull();
+ }
+
+ private static byte[] getEncryptedData() {
+ return new byte[]{-92, 94, -99, -97, 81, -48, -7, 119, -64, -22, 45, -49, -50, 92};
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorTest.java
new file mode 100644
index 0000000..ca612e3
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/encryption/CryptorTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.util.encryption;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+/**
+ * Unit test for {@link Cryptor}
+ */
+public final class CryptorTest {
+
+ private static final byte[] DATA =
+ new byte[] {107, -102, 101, 107, 20, 62, 2, 73, 113, 59, 8, -14, -58, 122};
+ private static final byte[] AUTHENTICITY_KEY =
+ new byte[] {-89, 88, -50, -42, -99, 57, 84, -24, 121, 1, -104, -8, -26, -73, -36, 100};
+
+ @Test
+ public void test_computeHkdf() {
+ int outputSize = 16;
+ byte[] res1 = Cryptor.computeHkdf(DATA, AUTHENTICITY_KEY, outputSize);
+ byte[] res2 = Cryptor.computeHkdf(DATA,
+ new byte[] {-89, 88, -50, -42, -99, 57, 84, -24, 121, 1, -104, -8, -26},
+ outputSize);
+
+ assertThat(res1).hasLength(outputSize);
+ assertThat(res2).hasLength(outputSize);
+ assertThat(res1).isNotEqualTo(res2);
+ assertThat(res1)
+ .isEqualTo(CryptorImpV1.computeHkdf(DATA, AUTHENTICITY_KEY, outputSize));
+ }
+
+ @Test
+ public void test_computeHkdf_invalidInput() {
+ assertThat(Cryptor.computeHkdf(DATA, AUTHENTICITY_KEY, /* size= */ 256000))
+ .isNull();
+ assertThat(Cryptor.computeHkdf(DATA, new byte[0], /* size= */ 255))
+ .isNull();
+ }
+}
diff --git a/nearby/tests/unit/src/com/android/server/nearby/util/identity/CallerIdentityTest.java b/nearby/tests/unit/src/com/android/server/nearby/util/identity/CallerIdentityTest.java
new file mode 100644
index 0000000..c29cb92
--- /dev/null
+++ b/nearby/tests/unit/src/com/android/server/nearby/util/identity/CallerIdentityTest.java
@@ -0,0 +1,45 @@
+/*
+ * 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.util.identity;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import org.junit.Test;
+
+public class CallerIdentityTest {
+ private static final int UID = 100;
+ private static final int PID = 10002;
+ private static final String PACKAGE_NAME = "package_name";
+ private static final String ATTRIBUTION_TAG = "attribution_tag";
+
+ @Test
+ public void testToString() {
+ CallerIdentity callerIdentity =
+ CallerIdentity.forTest(UID, PID, PACKAGE_NAME, ATTRIBUTION_TAG);
+ assertThat(callerIdentity.toString()).isEqualTo("100/package_name[attribution_tag]");
+ assertThat(callerIdentity.isSystemServer()).isFalse();
+ }
+
+ @Test
+ public void testHashCode() {
+ CallerIdentity callerIdentity =
+ CallerIdentity.forTest(UID, PID, PACKAGE_NAME, ATTRIBUTION_TAG);
+ CallerIdentity callerIdentity1 =
+ CallerIdentity.forTest(UID, PID, PACKAGE_NAME, ATTRIBUTION_TAG);
+ assertThat(callerIdentity.hashCode()).isEqualTo(callerIdentity1.hashCode());
+ }
+}
diff --git a/service-t/jni/com_android_server_net_NetworkStatsService.cpp b/service-t/jni/com_android_server_net_NetworkStatsService.cpp
index 39cbaf7..af0b8d8 100644
--- a/service-t/jni/com_android_server_net_NetworkStatsService.cpp
+++ b/service-t/jni/com_android_server_net_NetworkStatsService.cpp
@@ -30,9 +30,11 @@
#include "bpf/BpfUtils.h"
#include "netdbpf/BpfNetworkStats.h"
+#include "netdbpf/NetworkTraceHandler.h"
using android::bpf::bpfGetUidStats;
using android::bpf::bpfGetIfaceStats;
+using android::bpf::NetworkTraceHandler;
namespace android {
@@ -67,7 +69,7 @@
}
}
-static jlong getTotalStat(JNIEnv* env, jclass clazz, jint type) {
+static jlong nativeGetTotalStat(JNIEnv* env, jclass clazz, jint type) {
Stats stats = {};
if (bpfGetIfaceStats(NULL, &stats) == 0) {
@@ -77,7 +79,7 @@
}
}
-static jlong getIfaceStat(JNIEnv* env, jclass clazz, jstring iface, jint type) {
+static jlong nativeGetIfaceStat(JNIEnv* env, jclass clazz, jstring iface, jint type) {
ScopedUtfChars iface8(env, iface);
if (iface8.c_str() == NULL) {
return UNKNOWN;
@@ -92,7 +94,7 @@
}
}
-static jlong getUidStat(JNIEnv* env, jclass clazz, jint uid, jint type) {
+static jlong nativeGetUidStat(JNIEnv* env, jclass clazz, jint uid, jint type) {
Stats stats = {};
if (bpfGetUidStats(uid, &stats) == 0) {
@@ -102,10 +104,15 @@
}
}
+static void nativeInitNetworkTracing(JNIEnv* env, jclass clazz) {
+ NetworkTraceHandler::InitPerfettoTracing();
+}
+
static const JNINativeMethod gMethods[] = {
- {"nativeGetTotalStat", "(I)J", (void*)getTotalStat},
- {"nativeGetIfaceStat", "(Ljava/lang/String;I)J", (void*)getIfaceStat},
- {"nativeGetUidStat", "(II)J", (void*)getUidStat},
+ {"nativeGetTotalStat", "(I)J", (void*)nativeGetTotalStat},
+ {"nativeGetIfaceStat", "(Ljava/lang/String;I)J", (void*)nativeGetIfaceStat},
+ {"nativeGetUidStat", "(II)J", (void*)nativeGetUidStat},
+ {"nativeInitNetworkTracing", "()V", (void*)nativeInitNetworkTracing},
};
int register_android_server_net_NetworkStatsService(JNIEnv* env) {
diff --git a/service-t/native/libs/libnetworkstats/Android.bp b/service-t/native/libs/libnetworkstats/Android.bp
index 5b3d314..aa1ee41 100644
--- a/service-t/native/libs/libnetworkstats/Android.bp
+++ b/service-t/native/libs/libnetworkstats/Android.bp
@@ -24,12 +24,19 @@
host_supported: false,
header_libs: ["bpf_connectivity_headers"],
srcs: [
- "BpfNetworkStats.cpp"
+ "BpfNetworkStats.cpp",
+ "NetworkTraceHandler.cpp",
],
shared_libs: [
"libbase",
"liblog",
],
+ static_libs: [
+ "libperfetto_client_experimental",
+ ],
+ export_static_lib_headers: [
+ "libperfetto_client_experimental",
+ ],
export_include_dirs: ["include"],
cflags: [
"-Wall",
@@ -54,6 +61,7 @@
header_libs: ["bpf_connectivity_headers"],
srcs: [
"BpfNetworkStatsTest.cpp",
+ "NetworkTraceHandlerTest.cpp",
],
cflags: [
"-Wall",
@@ -64,10 +72,12 @@
static_libs: [
"libgmock",
"libnetworkstats",
+ "libperfetto_client_experimental",
],
shared_libs: [
"libbase",
"liblog",
+ "libandroid_net",
],
compile_multilib: "both",
multilib: {
diff --git a/service-t/native/libs/libnetworkstats/NetworkTraceHandler.cpp b/service-t/native/libs/libnetworkstats/NetworkTraceHandler.cpp
new file mode 100644
index 0000000..4c37b8d
--- /dev/null
+++ b/service-t/native/libs/libnetworkstats/NetworkTraceHandler.cpp
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#define LOG_TAG "NetworkTrace"
+
+#include "netdbpf/NetworkTraceHandler.h"
+
+#include <arpa/inet.h>
+#include <bpf/BpfUtils.h>
+#include <log/log.h>
+#include <perfetto/config/android/network_trace_config.pbzero.h>
+#include <perfetto/trace/android/network_trace.pbzero.h>
+#include <perfetto/trace/profiling/profile_packet.pbzero.h>
+#include <perfetto/tracing/platform.h>
+#include <perfetto/tracing/tracing.h>
+
+// Note: this is initializing state for a templated Perfetto type that resides
+// in the `perfetto` namespace. This must be defined in the global scope.
+PERFETTO_DEFINE_DATA_SOURCE_STATIC_MEMBERS(android::bpf::NetworkTraceHandler);
+
+namespace android {
+namespace bpf {
+using ::perfetto::protos::pbzero::NetworkPacketEvent;
+using ::perfetto::protos::pbzero::NetworkPacketTraceConfig;
+using ::perfetto::protos::pbzero::TracePacket;
+using ::perfetto::protos::pbzero::TrafficDirection;
+
+// static
+void NetworkTraceHandler::RegisterDataSource() {
+ ALOGD("Registering Perfetto data source");
+ perfetto::DataSourceDescriptor dsd;
+ dsd.set_name("android.network_packets");
+ NetworkTraceHandler::Register(dsd);
+}
+
+// static
+void NetworkTraceHandler::InitPerfettoTracing() {
+ perfetto::TracingInitArgs args = {};
+ args.backends |= perfetto::kSystemBackend;
+ perfetto::Tracing::Initialize(args);
+ NetworkTraceHandler::RegisterDataSource();
+}
+
+NetworkTraceHandler::NetworkTraceHandler()
+ : NetworkTraceHandler([this](const PacketTrace& pkt) {
+ NetworkTraceHandler::Trace(
+ [this, pkt](NetworkTraceHandler::TraceContext ctx) {
+ Fill(pkt, *ctx.NewTracePacket());
+ });
+ }) {}
+
+void NetworkTraceHandler::OnSetup(const SetupArgs& args) {
+ const std::string& raw = args.config->network_packet_trace_config_raw();
+ NetworkPacketTraceConfig::Decoder config(raw);
+
+ mPollMs = config.poll_ms();
+ if (mPollMs < 100) {
+ ALOGI("poll_ms is missing or below the 100ms minimum. Increasing to 100ms");
+ mPollMs = 100;
+ }
+}
+
+void NetworkTraceHandler::OnStart(const StartArgs&) {
+ if (!Start()) return;
+ mTaskRunner = perfetto::Platform::GetDefaultPlatform()->CreateTaskRunner({});
+ Loop();
+}
+
+void NetworkTraceHandler::OnStop(const StopArgs&) {
+ Stop();
+ mTaskRunner.reset();
+}
+
+void NetworkTraceHandler::Loop() {
+ mTaskRunner->PostDelayedTask([this]() { Loop(); }, mPollMs);
+ ConsumeAll();
+}
+
+void NetworkTraceHandler::Fill(const PacketTrace& src, TracePacket& dst) {
+ dst.set_timestamp(src.timestampNs);
+ auto* event = dst.set_network_packet();
+ event->set_direction(src.egress ? TrafficDirection::DIR_EGRESS
+ : TrafficDirection::DIR_INGRESS);
+ event->set_length(src.length);
+ event->set_uid(src.uid);
+ event->set_tag(src.tag);
+
+ event->set_local_port(src.egress ? ntohs(src.sport) : ntohs(src.dport));
+ event->set_remote_port(src.egress ? ntohs(src.dport) : ntohs(src.sport));
+
+ event->set_ip_proto(src.ipProto);
+ event->set_tcp_flags(src.tcpFlags);
+
+ char ifname[IF_NAMESIZE] = {};
+ if (if_indextoname(src.ifindex, ifname) == ifname) {
+ event->set_interface(std::string(ifname));
+ } else {
+ event->set_interface("error");
+ }
+}
+
+bool NetworkTraceHandler::Start() {
+ ALOGD("Starting datasource");
+
+ auto status = mConfigurationMap.init(PACKET_TRACE_ENABLED_MAP_PATH);
+ if (!status.ok()) {
+ ALOGW("Failed to bind config map: %s", status.error().message().c_str());
+ return false;
+ }
+
+ auto rb = BpfRingbuf<PacketTrace>::Create(PACKET_TRACE_RINGBUF_PATH);
+ if (!rb.ok()) {
+ ALOGW("Failed to create ringbuf: %s", rb.error().message().c_str());
+ return false;
+ }
+
+ mRingBuffer = std::move(*rb);
+
+ auto res = mConfigurationMap.writeValue(0, true, BPF_ANY);
+ if (!res.ok()) {
+ ALOGW("Failed to enable tracing: %s", res.error().message().c_str());
+ return false;
+ }
+
+ return true;
+}
+
+bool NetworkTraceHandler::Stop() {
+ ALOGD("Stopping datasource");
+
+ auto res = mConfigurationMap.writeValue(0, false, BPF_ANY);
+ if (!res.ok()) {
+ ALOGW("Failed to disable tracing: %s", res.error().message().c_str());
+ return false;
+ }
+
+ mRingBuffer.reset();
+
+ return true;
+}
+
+bool NetworkTraceHandler::ConsumeAll() {
+ if (mRingBuffer == nullptr) {
+ ALOGW("Tracing is not active");
+ return false;
+ }
+
+ base::Result<int> ret = mRingBuffer->ConsumeAll(mCallback);
+ if (!ret.ok()) {
+ ALOGW("Failed to poll ringbuf: %s", ret.error().message().c_str());
+ return false;
+ }
+
+ return true;
+}
+
+} // namespace bpf
+} // namespace android
diff --git a/service-t/native/libs/libnetworkstats/NetworkTraceHandlerTest.cpp b/service-t/native/libs/libnetworkstats/NetworkTraceHandlerTest.cpp
new file mode 100644
index 0000000..760ae91
--- /dev/null
+++ b/service-t/native/libs/libnetworkstats/NetworkTraceHandlerTest.cpp
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <android-base/unique_fd.h>
+#include <android/multinetwork.h>
+#include <arpa/inet.h>
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+#include <inttypes.h>
+#include <net/if.h>
+#include <netinet/tcp.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <vector>
+
+#include "netdbpf/NetworkTraceHandler.h"
+
+using ::testing::AllOf;
+using ::testing::AnyOf;
+using ::testing::Each;
+using ::testing::Eq;
+using ::testing::Field;
+using ::testing::Test;
+
+namespace android {
+namespace bpf {
+
+__be16 bindAndListen(int s) {
+ sockaddr_in sin = {.sin_family = AF_INET};
+ socklen_t len = sizeof(sin);
+ if (bind(s, (sockaddr*)&sin, sizeof(sin))) return 0;
+ if (listen(s, 1)) return 0;
+ if (getsockname(s, (sockaddr*)&sin, &len)) return 0;
+ return sin.sin_port;
+}
+
+// This takes tcp flag constants from the standard library and makes them usable
+// with the flags we get from BPF. The standard library flags are big endian
+// whereas the BPF flags are reported in host byte order. BPF also trims the
+// flags down to the 8 single-bit flag bits (fin, syn, rst, etc).
+constexpr inline uint8_t FlagToHost(__be32 be_unix_flags) {
+ return ntohl(be_unix_flags) >> 16;
+}
+
+// Pretty prints all fields for a list of packets (useful for debugging).
+struct PacketPrinter {
+ const std::vector<PacketTrace>& data;
+ static constexpr char kTcpFlagNames[] = "FSRPAUEC";
+
+ friend std::ostream& operator<<(std::ostream& os, const PacketPrinter& d) {
+ os << "Packet count: " << d.data.size();
+ for (const PacketTrace& info : d.data) {
+ os << "\nifidx=" << info.ifindex;
+ os << ", len=" << info.length;
+ os << ", uid=" << info.uid;
+ os << ", tag=" << info.tag;
+ os << ", sport=" << info.sport;
+ os << ", dport=" << info.dport;
+ os << ", direction=" << (info.egress ? "egress" : "ingress");
+ os << ", proto=" << static_cast<int>(info.ipProto);
+ os << ", ip=" << static_cast<int>(info.ipVersion);
+ os << ", flags=";
+ for (int i = 0; i < 8; i++) {
+ os << ((info.tcpFlags & (1 << i)) ? kTcpFlagNames[i] : '.');
+ }
+ }
+ return os;
+ }
+};
+
+class NetworkTraceHandlerTest : public testing::Test {
+ protected:
+ void SetUp() {
+ if (access(PACKET_TRACE_RINGBUF_PATH, R_OK)) {
+ GTEST_SKIP() << "Network tracing is not enabled/loaded on this build";
+ }
+ }
+};
+
+TEST_F(NetworkTraceHandlerTest, PollWhileInactive) {
+ NetworkTraceHandler handler([&](const PacketTrace& pkt) {});
+
+ // One succeed after start and before stop.
+ EXPECT_FALSE(handler.ConsumeAll());
+ ASSERT_TRUE(handler.Start());
+ EXPECT_TRUE(handler.ConsumeAll());
+ ASSERT_TRUE(handler.Stop());
+ EXPECT_FALSE(handler.ConsumeAll());
+}
+
+TEST_F(NetworkTraceHandlerTest, TraceTcpSession) {
+ __be16 server_port = 0;
+ std::vector<PacketTrace> packets;
+
+ // Record all packets with the bound address and current uid. This callback is
+ // involked only within ConsumeAll, at which point the port should have
+ // already been filled in and all packets have been processed.
+ NetworkTraceHandler handler([&](const PacketTrace& pkt) {
+ if (pkt.sport != server_port && pkt.dport != server_port) return;
+ if (pkt.uid != getuid()) return;
+ packets.push_back(pkt);
+ });
+
+ ASSERT_TRUE(handler.Start());
+ const uint32_t kClientTag = 2468;
+ const uint32_t kServerTag = 1357;
+
+ // Go through a typical connection sequence between two v4 sockets using tcp.
+ // This covers connection handshake, shutdown, and one data packet.
+ {
+ android::base::unique_fd clientsocket(socket(AF_INET, SOCK_STREAM, 0));
+ ASSERT_NE(-1, clientsocket) << "Failed to open client socket";
+ ASSERT_EQ(android_tag_socket(clientsocket, kClientTag), 0);
+
+ android::base::unique_fd serversocket(socket(AF_INET, SOCK_STREAM, 0));
+ ASSERT_NE(-1, serversocket) << "Failed to open server socket";
+ ASSERT_EQ(android_tag_socket(serversocket, kServerTag), 0);
+
+ server_port = bindAndListen(serversocket);
+ ASSERT_NE(0, server_port) << "Can't bind to server port";
+
+ sockaddr_in addr = {.sin_family = AF_INET, .sin_port = server_port};
+ ASSERT_EQ(0, connect(clientsocket, (sockaddr*)&addr, sizeof(addr)))
+ << "connect to loopback failed: " << strerror(errno);
+
+ int accepted = accept(serversocket, nullptr, nullptr);
+ ASSERT_NE(-1, accepted) << "accept connection failed: " << strerror(errno);
+
+ const char data[] = "abcdefghijklmnopqrstuvwxyz";
+ EXPECT_EQ(send(clientsocket, data, sizeof(data), 0), sizeof(data))
+ << "failed to send message: " << strerror(errno);
+
+ char buff[100] = {};
+ EXPECT_EQ(recv(accepted, buff, sizeof(buff), 0), sizeof(data))
+ << "failed to receive message: " << strerror(errno);
+
+ EXPECT_EQ(std::string(data), std::string(buff));
+ }
+
+ ASSERT_TRUE(handler.ConsumeAll());
+ ASSERT_TRUE(handler.Stop());
+
+ // There are 12 packets in total (6 messages: each seen by client & server):
+ // 1. Client connects to server with syn
+ // 2. Server responds with syn ack
+ // 3. Client responds with ack
+ // 4. Client sends data with psh ack
+ // 5. Server acks the data packet
+ // 6. Client closes connection with fin ack
+ ASSERT_EQ(packets.size(), 12) << PacketPrinter{packets};
+
+ // All packets should be TCP packets.
+ EXPECT_THAT(packets, Each(Field(&PacketTrace::ipProto, Eq(IPPROTO_TCP))));
+
+ // Packet 1: client requests connection with server.
+ EXPECT_EQ(packets[0].egress, 1) << PacketPrinter{packets};
+ EXPECT_EQ(packets[0].dport, server_port) << PacketPrinter{packets};
+ EXPECT_EQ(packets[0].tag, kClientTag) << PacketPrinter{packets};
+ EXPECT_EQ(packets[0].tcpFlags, FlagToHost(TCP_FLAG_SYN))
+ << PacketPrinter{packets};
+
+ // Packet 2: server receives request from client.
+ EXPECT_EQ(packets[1].egress, 0) << PacketPrinter{packets};
+ EXPECT_EQ(packets[1].dport, server_port) << PacketPrinter{packets};
+ EXPECT_EQ(packets[1].tag, kServerTag) << PacketPrinter{packets};
+ EXPECT_EQ(packets[1].tcpFlags, FlagToHost(TCP_FLAG_SYN))
+ << PacketPrinter{packets};
+
+ // Packet 3: server replies back with syn ack.
+ EXPECT_EQ(packets[2].egress, 1) << PacketPrinter{packets};
+ EXPECT_EQ(packets[2].sport, server_port) << PacketPrinter{packets};
+ EXPECT_EQ(packets[2].tcpFlags, FlagToHost(TCP_FLAG_SYN | TCP_FLAG_ACK))
+ << PacketPrinter{packets};
+
+ // Packet 4: client receives the server's syn ack.
+ EXPECT_EQ(packets[3].egress, 0) << PacketPrinter{packets};
+ EXPECT_EQ(packets[3].sport, server_port) << PacketPrinter{packets};
+ EXPECT_EQ(packets[3].tcpFlags, FlagToHost(TCP_FLAG_SYN | TCP_FLAG_ACK))
+ << PacketPrinter{packets};
+}
+
+} // namespace bpf
+} // namespace android
diff --git a/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTraceHandler.h b/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTraceHandler.h
new file mode 100644
index 0000000..c257aa0
--- /dev/null
+++ b/service-t/native/libs/libnetworkstats/include/netdbpf/NetworkTraceHandler.h
@@ -0,0 +1,84 @@
+/**
+ * Copyright (c) 2023, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <perfetto/base/task_runner.h>
+#include <perfetto/tracing.h>
+
+#include <string>
+#include <unordered_map>
+
+#include "bpf/BpfMap.h"
+#include "bpf/BpfRingbuf.h"
+
+// For PacketTrace struct definition
+#include "netd.h"
+
+namespace android {
+namespace bpf {
+
+class NetworkTraceHandler : public perfetto::DataSource<NetworkTraceHandler> {
+ public:
+ // Registers this DataSource.
+ static void RegisterDataSource();
+
+ // Connects to the system Perfetto daemon and registers the trace handler.
+ static void InitPerfettoTracing();
+
+ // Initialize with the default Perfetto callback.
+ NetworkTraceHandler();
+
+ // Testonly: initialize with a callback capable of intercepting data.
+ NetworkTraceHandler(std::function<void(const PacketTrace&)> callback)
+ : mCallback(std::move(callback)) {}
+
+ // Testonly: standalone functions without perfetto dependency.
+ bool Start();
+ bool Stop();
+ bool ConsumeAll();
+
+ // perfetto::DataSource overrides:
+ void OnSetup(const SetupArgs&) override;
+ void OnStart(const StartArgs&) override;
+ void OnStop(const StopArgs&) override;
+
+ // Convert a PacketTrace into a Perfetto trace packet.
+ void Fill(const PacketTrace& src,
+ ::perfetto::protos::pbzero::TracePacket& dst);
+
+ private:
+ void Loop();
+
+ // How often to poll the ring buffer, defined by the trace config.
+ uint32_t mPollMs;
+
+ // The function to process PacketTrace, typically a Perfetto sink.
+ std::function<void(const PacketTrace&)> mCallback;
+
+ // The BPF ring buffer handle.
+ std::unique_ptr<BpfRingbuf<PacketTrace>> mRingBuffer;
+
+ // The packet tracing config map (really a 1-element array).
+ BpfMap<uint32_t, bool> mConfigurationMap;
+
+ // This must be the last member, causing it to be the first deleted. If it is
+ // not, members required for callbacks can be deleted before it's stopped.
+ std::unique_ptr<perfetto::base::TaskRunner> mTaskRunner;
+};
+
+} // namespace bpf
+} // namespace android
diff --git a/service-t/src/com/android/server/NetworkStatsServiceInitializer.java b/service-t/src/com/android/server/NetworkStatsServiceInitializer.java
index 0ea126a..82a4fbd 100644
--- a/service-t/src/com/android/server/NetworkStatsServiceInitializer.java
+++ b/service-t/src/com/android/server/NetworkStatsServiceInitializer.java
@@ -18,6 +18,7 @@
import android.content.Context;
import android.net.TrafficStats;
+import android.os.Build;
import android.util.Log;
import com.android.modules.utils.build.SdkLevel;
@@ -46,6 +47,15 @@
/* allowIsolated= */ false);
TrafficStats.init(getContext());
}
+
+ // The following code registers the Perfetto Network Trace Handler on non-user builds.
+ // The enhanced tracing is intended to be used for debugging and diagnosing issues. This
+ // is conditional on the build type rather than `isDebuggable` to match the system_server
+ // selinux rules which only allow the Perfetto connection under the same circumstances.
+ if (SdkLevel.isAtLeastU() && !Build.TYPE.equals("user")) {
+ Log.i(TAG, "Initializing network tracing hooks");
+ NetworkStatsService.nativeInitNetworkTracing();
+ }
}
@Override
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index 84b9f12..5dcf860 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -60,6 +60,7 @@
import com.android.net.module.util.DeviceConfigUtils;
import com.android.net.module.util.PermissionUtils;
import com.android.server.connectivity.mdns.ExecutorProvider;
+import com.android.server.connectivity.mdns.MdnsAdvertiser;
import com.android.server.connectivity.mdns.MdnsDiscoveryManager;
import com.android.server.connectivity.mdns.MdnsMultinetworkSocketClient;
import com.android.server.connectivity.mdns.MdnsSearchOptions;
@@ -74,9 +75,15 @@
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -89,10 +96,22 @@
public class NsdService extends INsdManager.Stub {
private static final String TAG = "NsdService";
private static final String MDNS_TAG = "mDnsConnector";
+ /**
+ * Enable discovery using the Java DiscoveryManager, instead of the legacy mdnsresponder
+ * implementation.
+ */
private static final String MDNS_DISCOVERY_MANAGER_VERSION = "mdns_discovery_manager_version";
private static final String LOCAL_DOMAIN_NAME = "local";
+ // Max label length as per RFC 1034/1035
+ private static final int MAX_LABEL_LENGTH = 63;
- private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
+ /**
+ * Enable advertising using the Java MdnsAdvertiser, instead of the legacy mdnsresponder
+ * implementation.
+ */
+ private static final String MDNS_ADVERTISER_VERSION = "mdns_advertiser_version";
+
+ public static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
private static final long CLEANUP_DELAY_MS = 10000;
private static final int IFACE_IDX_ANY = 0;
@@ -106,6 +125,8 @@
private final MdnsDiscoveryManager mMdnsDiscoveryManager;
@Nullable
private final MdnsSocketProvider mMdnsSocketProvider;
+ @Nullable
+ private final MdnsAdvertiser mAdvertiser;
// WARNING : Accessing these values in any thread is not safe, it must only be changed in the
// state machine thread. If change this outside state machine, it will need to introduce
// synchronization.
@@ -345,7 +366,7 @@
mLegacyClientCount -= 1;
}
}
- if (mMdnsDiscoveryManager != null) {
+ if (mMdnsDiscoveryManager != null || mAdvertiser != null) {
maybeStopMonitoringSocketsIfNoActiveRequest();
}
maybeScheduleStop();
@@ -385,6 +406,20 @@
clientId, NsdManager.FAILURE_INTERNAL_ERROR);
}
break;
+ case NsdManager.STOP_RESOLUTION:
+ cInfo = getClientInfoForReply(msg);
+ if (cInfo != null) {
+ cInfo.onStopResolutionFailed(
+ clientId, NsdManager.FAILURE_OPERATION_NOT_RUNNING);
+ }
+ break;
+ case NsdManager.REGISTER_SERVICE_CALLBACK:
+ cInfo = getClientInfoForReply(msg);
+ if (cInfo != null) {
+ cInfo.onServiceInfoCallbackRegistrationFailed(
+ clientId, NsdManager.FAILURE_BAD_PARAMETERS);
+ }
+ break;
case NsdManager.DAEMON_CLEANUP:
maybeStopDaemon();
break;
@@ -446,6 +481,7 @@
clientInfo.mClientRequests.delete(clientId);
mIdToClientInfoMap.remove(globalId);
maybeScheduleStop();
+ maybeStopMonitoringSocketsIfNoActiveRequest();
}
private void storeListenerMap(int clientId, int transactionId, MdnsListener listener,
@@ -462,6 +498,11 @@
maybeStopMonitoringSocketsIfNoActiveRequest();
}
+ private void clearRegisteredServiceInfo(ClientInfo clientInfo) {
+ clientInfo.mRegisteredService = null;
+ clientInfo.mClientIdForServiceUpdates = 0;
+ }
+
/**
* Check the given service type is valid and construct it to a service type
* which can use for discovery / resolution service.
@@ -484,8 +525,31 @@
final Matcher matcher = serviceTypePattern.matcher(serviceType);
if (!matcher.matches()) return null;
return matcher.group(1) == null
- ? serviceType + ".local"
- : matcher.group(1) + "_sub." + matcher.group(2) + ".local";
+ ? serviceType
+ : matcher.group(1) + "_sub." + matcher.group(2);
+ }
+
+ /**
+ * Truncate a service name to up to 63 UTF-8 bytes.
+ *
+ * See RFC6763 4.1.1: service instance names are UTF-8 and up to 63 bytes. Truncating
+ * names used in registerService follows historical behavior (see mdnsresponder
+ * handle_regservice_request).
+ */
+ @NonNull
+ private String truncateServiceName(@NonNull String originalName) {
+ // UTF-8 is at most 4 bytes per character; return early in the common case where
+ // the name can't possibly be over the limit given its string length.
+ if (originalName.length() <= MAX_LABEL_LENGTH / 4) return originalName;
+
+ final Charset utf8 = StandardCharsets.UTF_8;
+ final CharsetEncoder encoder = utf8.newEncoder();
+ final ByteBuffer out = ByteBuffer.allocate(MAX_LABEL_LENGTH);
+ // encode will write as many characters as possible to the out buffer, and just
+ // return an overflow code if there were too many characters (no need to check the
+ // return code here, this method truncates the name on purpose).
+ encoder.encode(CharBuffer.wrap(originalName), out, true /* endOfInput */);
+ return new String(out.array(), 0, out.position(), utf8);
}
@Override
@@ -523,14 +587,16 @@
break;
}
+ final String listenServiceType = serviceType + ".local";
maybeStartMonitoringSockets();
final MdnsListener listener =
- new DiscoveryListener(clientId, id, info, serviceType);
+ new DiscoveryListener(clientId, id, info, listenServiceType);
final MdnsSearchOptions options = MdnsSearchOptions.newBuilder()
.setNetwork(info.getNetwork())
.setIsPassiveMode(true)
.build();
- mMdnsDiscoveryManager.registerListener(serviceType, listener, options);
+ mMdnsDiscoveryManager.registerListener(
+ listenServiceType, listener, options);
storeListenerMap(clientId, id, listener, clientInfo);
clientInfo.onDiscoverServicesStarted(clientId, info);
} else {
@@ -608,16 +674,36 @@
break;
}
- maybeStartDaemon();
id = getUniqueId();
- if (registerService(id, args.serviceInfo)) {
- if (DBG) Log.d(TAG, "Register " + clientId + " " + id);
+ if (mAdvertiser != null) {
+ final NsdServiceInfo serviceInfo = args.serviceInfo;
+ final String serviceType = serviceInfo.getServiceType();
+ final String registerServiceType = constructServiceType(serviceType);
+ if (registerServiceType == null) {
+ Log.e(TAG, "Invalid service type: " + serviceType);
+ clientInfo.onRegisterServiceFailed(clientId,
+ NsdManager.FAILURE_INTERNAL_ERROR);
+ break;
+ }
+ serviceInfo.setServiceType(registerServiceType);
+ serviceInfo.setServiceName(truncateServiceName(
+ serviceInfo.getServiceName()));
+
+ maybeStartMonitoringSockets();
+ mAdvertiser.addService(id, serviceInfo);
storeRequestMap(clientId, id, clientInfo, msg.what);
- // Return success after mDns reports success
} else {
- unregisterService(id);
- clientInfo.onRegisterServiceFailed(
- clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+ maybeStartDaemon();
+ if (registerService(id, args.serviceInfo)) {
+ if (DBG) Log.d(TAG, "Register " + clientId + " " + id);
+ storeRequestMap(clientId, id, clientInfo, msg.what);
+ // Return success after mDns reports success
+ } else {
+ unregisterService(id);
+ clientInfo.onRegisterServiceFailed(
+ clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+ }
+
}
break;
case NsdManager.UNREGISTER_SERVICE:
@@ -633,11 +719,17 @@
}
id = clientInfo.mClientIds.get(clientId);
removeRequestMap(clientId, id, clientInfo);
- if (unregisterService(id)) {
+
+ if (mAdvertiser != null) {
+ mAdvertiser.removeService(id);
clientInfo.onUnregisterServiceSucceeded(clientId);
} else {
- clientInfo.onUnregisterServiceFailed(
- clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+ if (unregisterService(id)) {
+ clientInfo.onUnregisterServiceSucceeded(clientId);
+ } else {
+ clientInfo.onUnregisterServiceFailed(
+ clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+ }
}
break;
case NsdManager.RESOLVE_SERVICE: {
@@ -661,15 +753,17 @@
NsdManager.FAILURE_INTERNAL_ERROR);
break;
}
+ final String resolveServiceType = serviceType + ".local";
maybeStartMonitoringSockets();
final MdnsListener listener =
- new ResolutionListener(clientId, id, info, serviceType);
+ new ResolutionListener(clientId, id, info, resolveServiceType);
final MdnsSearchOptions options = MdnsSearchOptions.newBuilder()
.setNetwork(info.getNetwork())
.setIsPassiveMode(true)
.build();
- mMdnsDiscoveryManager.registerListener(serviceType, listener, options);
+ mMdnsDiscoveryManager.registerListener(
+ resolveServiceType, listener, options);
storeListenerMap(clientId, id, listener, clientInfo);
} else {
if (clientInfo.mResolvedService != null) {
@@ -689,6 +783,79 @@
}
break;
}
+ case NsdManager.STOP_RESOLUTION:
+ if (DBG) Log.d(TAG, "Stop service resolution");
+ args = (ListenerArgs) msg.obj;
+ clientInfo = mClients.get(args.connector);
+ // If the binder death notification for a INsdManagerCallback was received
+ // before any calls are received by NsdService, the clientInfo would be
+ // cleared and cause NPE. Add a null check here to prevent this corner case.
+ if (clientInfo == null) {
+ Log.e(TAG, "Unknown connector in stop resolution");
+ break;
+ }
+
+ id = clientInfo.mClientIds.get(clientId);
+ removeRequestMap(clientId, id, clientInfo);
+ if (stopResolveService(id)) {
+ clientInfo.onStopResolutionSucceeded(clientId);
+ } else {
+ clientInfo.onStopResolutionFailed(
+ clientId, NsdManager.FAILURE_OPERATION_NOT_RUNNING);
+ }
+ clientInfo.mResolvedService = null;
+ // TODO: Implement the stop resolution with MdnsDiscoveryManager.
+ break;
+ case NsdManager.REGISTER_SERVICE_CALLBACK:
+ if (DBG) Log.d(TAG, "Register a service callback");
+ args = (ListenerArgs) msg.obj;
+ clientInfo = mClients.get(args.connector);
+ // If the binder death notification for a INsdManagerCallback was received
+ // before any calls are received by NsdService, the clientInfo would be
+ // cleared and cause NPE. Add a null check here to prevent this corner case.
+ if (clientInfo == null) {
+ Log.e(TAG, "Unknown connector in callback registration");
+ break;
+ }
+
+ if (clientInfo.mRegisteredService != null) {
+ clientInfo.onServiceInfoCallbackRegistrationFailed(
+ clientId, NsdManager.FAILURE_ALREADY_ACTIVE);
+ break;
+ }
+
+ maybeStartDaemon();
+ id = getUniqueId();
+ if (resolveService(id, args.serviceInfo)) {
+ clientInfo.mRegisteredService = new NsdServiceInfo();
+ clientInfo.mClientIdForServiceUpdates = clientId;
+ storeRequestMap(clientId, id, clientInfo, msg.what);
+ } else {
+ clientInfo.onServiceInfoCallbackRegistrationFailed(
+ clientId, NsdManager.FAILURE_BAD_PARAMETERS);
+ }
+ break;
+ case NsdManager.UNREGISTER_SERVICE_CALLBACK:
+ if (DBG) Log.d(TAG, "Unregister a service callback");
+ args = (ListenerArgs) msg.obj;
+ clientInfo = mClients.get(args.connector);
+ // If the binder death notification for a INsdManagerCallback was received
+ // before any calls are received by NsdService, the clientInfo would be
+ // cleared and cause NPE. Add a null check here to prevent this corner case.
+ if (clientInfo == null) {
+ Log.e(TAG, "Unknown connector in callback unregistration");
+ break;
+ }
+
+ id = clientInfo.mClientIds.get(clientId);
+ removeRequestMap(clientId, id, clientInfo);
+ if (stopResolveService(id)) {
+ clientInfo.onServiceInfoCallbackUnregistered(clientId);
+ } else {
+ Log.e(TAG, "Failed to unregister service info callback");
+ }
+ clearRegisteredServiceInfo(clientInfo);
+ break;
case MDNS_SERVICE_EVENT:
if (!handleMDnsServiceEvent(msg.arg1, msg.arg2, msg.obj)) {
return NOT_HANDLED;
@@ -705,6 +872,19 @@
return HANDLED;
}
+ private void notifyResolveFailedResult(boolean isListenedToUpdates, int clientId,
+ ClientInfo clientInfo, int error) {
+ if (isListenedToUpdates) {
+ clientInfo.onServiceInfoCallbackRegistrationFailed(clientId, error);
+ clearRegisteredServiceInfo(clientInfo);
+ } else {
+ // The resolve API always returned FAILURE_INTERNAL_ERROR on error; keep it
+ // for backwards compatibility.
+ clientInfo.onResolveServiceFailed(clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+ clientInfo.mResolvedService = null;
+ }
+ }
+
private boolean handleMDnsServiceEvent(int code, int id, Object obj) {
NsdServiceInfo servInfo;
ClientInfo clientInfo = mIdToClientInfoMap.get(id);
@@ -739,6 +919,12 @@
// interfaces that do not have an associated Network.
break;
}
+ if (foundNetId == INetd.DUMMY_NET_ID) {
+ // Ignore services on the dummy0 interface: they are only seen when
+ // discovering locally advertised services, and are not reachable
+ // through that interface.
+ break;
+ }
setServiceNetworkForCallback(servInfo, info.netId, info.interfaceIdx);
clientInfo.onServiceFound(clientId, servInfo);
break;
@@ -755,6 +941,8 @@
// found services on the same interface index and their network at the time
setServiceNetworkForCallback(servInfo, lostNetId, info.interfaceIdx);
clientInfo.onServiceLost(clientId, servInfo);
+ // TODO: also support registered service lost when not discovering
+ clientInfo.maybeNotifyRegisteredServiceLost(servInfo);
break;
}
case IMDnsEventListener.SERVICE_DISCOVERY_FAILED:
@@ -791,10 +979,15 @@
String rest = fullName.substring(index);
String type = rest.replace(".local.", "");
- clientInfo.mResolvedService.setServiceName(name);
- clientInfo.mResolvedService.setServiceType(type);
- clientInfo.mResolvedService.setPort(info.port);
- clientInfo.mResolvedService.setTxtRecords(info.txtRecord);
+ final boolean isListenedToUpdates =
+ clientId == clientInfo.mClientIdForServiceUpdates;
+ final NsdServiceInfo serviceInfo = isListenedToUpdates
+ ? clientInfo.mRegisteredService : clientInfo.mResolvedService;
+
+ serviceInfo.setServiceName(name);
+ serviceInfo.setServiceType(type);
+ serviceInfo.setPort(info.port);
+ serviceInfo.setTxtRecords(info.txtRecord);
// Network will be added after SERVICE_GET_ADDR_SUCCESS
stopResolveService(id);
@@ -804,9 +997,8 @@
if (getAddrInfo(id2, info.hostname, info.interfaceIdx)) {
storeRequestMap(clientId, id2, clientInfo, NsdManager.RESOLVE_SERVICE);
} else {
- clientInfo.onResolveServiceFailed(
- clientId, NsdManager.FAILURE_INTERNAL_ERROR);
- clientInfo.mResolvedService = null;
+ notifyResolveFailedResult(isListenedToUpdates, clientId, clientInfo,
+ NsdManager.FAILURE_BAD_PARAMETERS);
}
break;
}
@@ -814,17 +1006,17 @@
/* NNN resolveId errorCode */
stopResolveService(id);
removeRequestMap(clientId, id, clientInfo);
- clientInfo.mResolvedService = null;
- clientInfo.onResolveServiceFailed(
- clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+ notifyResolveFailedResult(
+ clientId == clientInfo.mClientIdForServiceUpdates,
+ clientId, clientInfo, NsdManager.FAILURE_BAD_PARAMETERS);
break;
case IMDnsEventListener.SERVICE_GET_ADDR_FAILED:
/* NNN resolveId errorCode */
stopGetAddrInfo(id);
removeRequestMap(clientId, id, clientInfo);
- clientInfo.mResolvedService = null;
- clientInfo.onResolveServiceFailed(
- clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+ notifyResolveFailedResult(
+ clientId == clientInfo.mClientIdForServiceUpdates,
+ clientId, clientInfo, NsdManager.FAILURE_BAD_PARAMETERS);
break;
case IMDnsEventListener.SERVICE_GET_ADDR_SUCCESS: {
/* NNN resolveId hostname ttl addr interfaceIdx netId */
@@ -841,19 +1033,38 @@
// If the resolved service is on an interface without a network, consider it
// as a failure: it would not be usable by apps as they would need
// privileged permissions.
- if (netId != NETID_UNSET && serviceHost != null) {
- clientInfo.mResolvedService.setHost(serviceHost);
- setServiceNetworkForCallback(clientInfo.mResolvedService,
- netId, info.interfaceIdx);
- clientInfo.onResolveServiceSucceeded(
- clientId, clientInfo.mResolvedService);
+ if (clientId == clientInfo.mClientIdForServiceUpdates) {
+ if (netId != NETID_UNSET && serviceHost != null) {
+ setServiceNetworkForCallback(clientInfo.mRegisteredService,
+ netId, info.interfaceIdx);
+ final List<InetAddress> addresses =
+ clientInfo.mRegisteredService.getHostAddresses();
+ addresses.add(serviceHost);
+ clientInfo.mRegisteredService.setHostAddresses(addresses);
+ clientInfo.onServiceUpdated(
+ clientId, clientInfo.mRegisteredService);
+ } else {
+ stopGetAddrInfo(id);
+ removeRequestMap(clientId, id, clientInfo);
+ clearRegisteredServiceInfo(clientInfo);
+ clientInfo.onServiceInfoCallbackRegistrationFailed(
+ clientId, NsdManager.FAILURE_BAD_PARAMETERS);
+ }
} else {
- clientInfo.onResolveServiceFailed(
- clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+ if (netId != NETID_UNSET && serviceHost != null) {
+ clientInfo.mResolvedService.setHost(serviceHost);
+ setServiceNetworkForCallback(clientInfo.mResolvedService,
+ netId, info.interfaceIdx);
+ clientInfo.onResolveServiceSucceeded(
+ clientId, clientInfo.mResolvedService);
+ } else {
+ clientInfo.onResolveServiceFailed(
+ clientId, NsdManager.FAILURE_INTERNAL_ERROR);
+ }
+ stopGetAddrInfo(id);
+ removeRequestMap(clientId, id, clientInfo);
+ clientInfo.mResolvedService = null;
}
- stopGetAddrInfo(id);
- removeRequestMap(clientId, id, clientInfo);
- clientInfo.mResolvedService = null;
break;
}
default:
@@ -1005,18 +1216,32 @@
mNsdStateMachine.start();
mMDnsManager = ctx.getSystemService(MDnsManager.class);
mMDnsEventCallback = new MDnsEventCallback(mNsdStateMachine);
- if (deps.isMdnsDiscoveryManagerEnabled(ctx)) {
+
+ final boolean discoveryManagerEnabled = deps.isMdnsDiscoveryManagerEnabled(ctx);
+ final boolean advertiserEnabled = deps.isMdnsAdvertiserEnabled(ctx);
+ if (discoveryManagerEnabled || advertiserEnabled) {
mMdnsSocketProvider = deps.makeMdnsSocketProvider(ctx, handler.getLooper());
+ } else {
+ mMdnsSocketProvider = null;
+ }
+
+ if (discoveryManagerEnabled) {
mMdnsSocketClient =
new MdnsMultinetworkSocketClient(handler.getLooper(), mMdnsSocketProvider);
mMdnsDiscoveryManager =
deps.makeMdnsDiscoveryManager(new ExecutorProvider(), mMdnsSocketClient);
handler.post(() -> mMdnsSocketClient.setCallback(mMdnsDiscoveryManager));
} else {
- mMdnsSocketProvider = null;
mMdnsSocketClient = null;
mMdnsDiscoveryManager = null;
}
+
+ if (advertiserEnabled) {
+ mAdvertiser = deps.makeMdnsAdvertiser(handler.getLooper(), mMdnsSocketProvider,
+ new AdvertiserCallback());
+ } else {
+ mAdvertiser = null;
+ }
}
/**
@@ -1025,10 +1250,10 @@
@VisibleForTesting
public static class Dependencies {
/**
- * Check whether or not MdnsDiscoveryManager feature is enabled.
+ * Check whether the MdnsDiscoveryManager feature is enabled.
*
* @param context The global context information about an app environment.
- * @return true if MdnsDiscoveryManager feature is enabled.
+ * @return true if the MdnsDiscoveryManager feature is enabled.
*/
public boolean isMdnsDiscoveryManagerEnabled(Context context) {
return DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_CONNECTIVITY,
@@ -1036,6 +1261,17 @@
}
/**
+ * Check whether the MdnsAdvertiser feature is enabled.
+ *
+ * @param context The global context information about an app environment.
+ * @return true if the MdnsAdvertiser feature is enabled.
+ */
+ public boolean isMdnsAdvertiserEnabled(Context context) {
+ return DeviceConfigUtils.isFeatureEnabled(context, NAMESPACE_CONNECTIVITY,
+ MDNS_ADVERTISER_VERSION, false /* defaultEnabled */);
+ }
+
+ /**
* @see MdnsDiscoveryManager
*/
public MdnsDiscoveryManager makeMdnsDiscoveryManager(
@@ -1044,6 +1280,15 @@
}
/**
+ * @see MdnsAdvertiser
+ */
+ public MdnsAdvertiser makeMdnsAdvertiser(
+ @NonNull Looper looper, @NonNull MdnsSocketProvider socketProvider,
+ @NonNull MdnsAdvertiser.AdvertiserCallback cb) {
+ return new MdnsAdvertiser(looper, socketProvider, cb);
+ }
+
+ /**
* @see MdnsSocketProvider
*/
public MdnsSocketProvider makeMdnsSocketProvider(Context context, Looper looper) {
@@ -1101,6 +1346,49 @@
}
}
+ private class AdvertiserCallback implements MdnsAdvertiser.AdvertiserCallback {
+ @Override
+ public void onRegisterServiceSucceeded(int serviceId, NsdServiceInfo registeredInfo) {
+ final ClientInfo clientInfo = getClientInfoOrLog(serviceId);
+ if (clientInfo == null) return;
+
+ final int clientId = getClientIdOrLog(clientInfo, serviceId);
+ if (clientId < 0) return;
+
+ // onRegisterServiceSucceeded only has the service name in its info. This aligns with
+ // historical behavior.
+ final NsdServiceInfo cbInfo = new NsdServiceInfo(registeredInfo.getServiceName(), null);
+ clientInfo.onRegisterServiceSucceeded(clientId, cbInfo);
+ }
+
+ @Override
+ public void onRegisterServiceFailed(int serviceId, int errorCode) {
+ final ClientInfo clientInfo = getClientInfoOrLog(serviceId);
+ if (clientInfo == null) return;
+
+ final int clientId = getClientIdOrLog(clientInfo, serviceId);
+ if (clientId < 0) return;
+
+ clientInfo.onRegisterServiceFailed(clientId, errorCode);
+ }
+
+ private ClientInfo getClientInfoOrLog(int serviceId) {
+ final ClientInfo clientInfo = mIdToClientInfoMap.get(serviceId);
+ if (clientInfo == null) {
+ Log.e(TAG, String.format("Callback for service %d has no client", serviceId));
+ }
+ return clientInfo;
+ }
+
+ private int getClientIdOrLog(@NonNull ClientInfo info, int serviceId) {
+ final int clientId = info.getClientId(serviceId);
+ if (clientId < 0) {
+ Log.e(TAG, String.format("Client ID not found for service %d", serviceId));
+ }
+ return clientId;
+ }
+ }
+
@Override
public INsdServiceConnector connect(INsdManagerCallback cb) {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.INTERNET, "NsdService");
@@ -1156,6 +1444,26 @@
}
@Override
+ public void stopResolution(int listenerKey) {
+ mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
+ NsdManager.STOP_RESOLUTION, 0, listenerKey, new ListenerArgs(this, null)));
+ }
+
+ @Override
+ public void registerServiceInfoCallback(int listenerKey, NsdServiceInfo serviceInfo) {
+ mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
+ NsdManager.REGISTER_SERVICE_CALLBACK, 0, listenerKey,
+ new ListenerArgs(this, serviceInfo)));
+ }
+
+ @Override
+ public void unregisterServiceInfoCallback(int listenerKey) {
+ mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
+ NsdManager.UNREGISTER_SERVICE_CALLBACK, 0, listenerKey,
+ new ListenerArgs(this, null)));
+ }
+
+ @Override
public void startDaemon() {
mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage(
NsdManager.DAEMON_STARTUP, new ListenerArgs(this, null)));
@@ -1316,6 +1624,11 @@
// The target SDK of this client < Build.VERSION_CODES.S
private boolean mIsLegacy = false;
+ /*** The service that is registered to listen to its updates */
+ private NsdServiceInfo mRegisteredService;
+ /*** The client id that listen to updates */
+ private int mClientIdForServiceUpdates;
+
private ClientInfo(INsdManagerCallback cb) {
mCb = cb;
if (DBG) Log.d(TAG, "New client");
@@ -1364,7 +1677,11 @@
stopResolveService(globalId);
break;
case NsdManager.REGISTER_SERVICE:
- unregisterService(globalId);
+ if (mAdvertiser != null) {
+ mAdvertiser.removeService(globalId);
+ } else {
+ unregisterService(globalId);
+ }
break;
default:
break;
@@ -1393,6 +1710,18 @@
return mClientIds.keyAt(idx);
}
+ private void maybeNotifyRegisteredServiceLost(@NonNull NsdServiceInfo info) {
+ if (mRegisteredService == null) return;
+ if (!Objects.equals(mRegisteredService.getServiceName(), info.getServiceName())) return;
+ // Resolved services have a leading dot appended at the beginning of their type, but in
+ // discovered info it's at the end
+ if (!Objects.equals(
+ mRegisteredService.getServiceType() + ".", "." + info.getServiceType())) {
+ return;
+ }
+ onServiceUpdatedLost(mClientIdForServiceUpdates);
+ }
+
void onDiscoverServicesStarted(int listenerKey, NsdServiceInfo info) {
try {
mCb.onDiscoverServicesStarted(listenerKey, info);
@@ -1488,5 +1817,53 @@
Log.e(TAG, "Error calling onResolveServiceSucceeded", e);
}
}
+
+ void onStopResolutionFailed(int listenerKey, int error) {
+ try {
+ mCb.onStopResolutionFailed(listenerKey, error);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling onStopResolutionFailed", e);
+ }
+ }
+
+ void onStopResolutionSucceeded(int listenerKey) {
+ try {
+ mCb.onStopResolutionSucceeded(listenerKey);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling onStopResolutionSucceeded", e);
+ }
+ }
+
+ void onServiceInfoCallbackRegistrationFailed(int listenerKey, int error) {
+ try {
+ mCb.onServiceInfoCallbackRegistrationFailed(listenerKey, error);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling onServiceInfoCallbackRegistrationFailed", e);
+ }
+ }
+
+ void onServiceUpdated(int listenerKey, NsdServiceInfo info) {
+ try {
+ mCb.onServiceUpdated(listenerKey, info);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling onServiceUpdated", e);
+ }
+ }
+
+ void onServiceUpdatedLost(int listenerKey) {
+ try {
+ mCb.onServiceUpdatedLost(listenerKey);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling onServiceUpdatedLost", e);
+ }
+ }
+
+ void onServiceInfoCallbackUnregistered(int listenerKey) {
+ try {
+ mCb.onServiceInfoCallbackUnregistered(listenerKey);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling onServiceInfoCallbackUnregistered", e);
+ }
+ }
}
}
diff --git a/service-t/src/com/android/server/mdns/MdnsInterfaceAdvertiser.java b/service-t/src/com/android/server/mdns/MdnsInterfaceAdvertiser.java
index a14b5ad..c616e01 100644
--- a/service-t/src/com/android/server/mdns/MdnsInterfaceAdvertiser.java
+++ b/service-t/src/com/android/server/mdns/MdnsInterfaceAdvertiser.java
@@ -278,14 +278,23 @@
* Reset a service to the probing state due to a conflict found on the network.
*/
public void restartProbingForConflict(int serviceId) {
- // TODO: implement
+ final MdnsProber.ProbingInfo probingInfo = mRecordRepository.setServiceProbing(serviceId);
+ if (probingInfo == null) return;
+
+ mProber.restartForConflict(probingInfo);
}
/**
* Rename a service following a conflict found on the network, and restart probing.
+ *
+ * If the service was not registered on this {@link MdnsInterfaceAdvertiser}, this is a no-op.
*/
public void renameServiceForConflict(int serviceId, NsdServiceInfo newInfo) {
- // TODO: implement
+ final MdnsProber.ProbingInfo probingInfo = mRecordRepository.renameServiceForConflict(
+ serviceId, newInfo);
+ if (probingInfo == null) return;
+
+ mProber.restartForConflict(probingInfo);
}
/**
@@ -319,8 +328,15 @@
+ packet.additionalRecords.size() + " additional from " + src);
}
- final MdnsRecordRepository.ReplyInfo answers =
- mRecordRepository.getReply(packet, src);
+ for (int conflictServiceId : mRecordRepository.getConflictingServices(packet)) {
+ mCbHandler.post(() -> mCb.onServiceConflict(this, conflictServiceId));
+ }
+
+ // Even in case of conflict, add replies for other services. But in general conflicts would
+ // happen when the incoming packet has answer records (not a question), so there will be no
+ // answer. One exception is simultaneous probe tiebreaking (rfc6762 8.2), in which case the
+ // conflicting service is still probing and won't reply either.
+ final MdnsRecordRepository.ReplyInfo answers = mRecordRepository.getReply(packet, src);
if (answers == null) return;
mReplySender.queueReply(answers);
diff --git a/service-t/src/com/android/server/mdns/MdnsProber.java b/service-t/src/com/android/server/mdns/MdnsProber.java
index 2cd9148..669b323 100644
--- a/service-t/src/com/android/server/mdns/MdnsProber.java
+++ b/service-t/src/com/android/server/mdns/MdnsProber.java
@@ -33,13 +33,13 @@
* TODO: implement receiving replies and handling conflicts.
*/
public class MdnsProber extends MdnsPacketRepeater<MdnsProber.ProbingInfo> {
+ private static final long CONFLICT_RETRY_DELAY_MS = 5_000L;
@NonNull
private final String mLogTag;
public MdnsProber(@NonNull String interfaceTag, @NonNull Looper looper,
@NonNull MdnsReplySender replySender,
@NonNull PacketRepeaterCallback<ProbingInfo> cb) {
- // 3 packets as per https://datatracker.ietf.org/doc/html/rfc6762#section-8.1
super(looper, replySender, cb);
mLogTag = MdnsProber.class.getSimpleName() + "/" + interfaceTag;
}
@@ -140,4 +140,18 @@
private void startProbing(@NonNull ProbingInfo info, long delay) {
startSending(info.getServiceId(), info, delay);
}
+
+ /**
+ * Restart probing with new service info as a conflict was found.
+ */
+ public void restartForConflict(@NonNull ProbingInfo newInfo) {
+ stop(newInfo.getServiceId());
+
+ /* RFC 6762 8.1: "If fifteen conflicts occur within any ten-second period, then the host
+ MUST wait at least five seconds before each successive additional probe attempt. [...]
+ For very simple devices, a valid way to comply with this requirement is to always wait
+ five seconds after any failed probe attempt before trying again. */
+ // TODO: count 15 conflicts in 10s instead of waiting for 5s every time
+ startProbing(newInfo, CONFLICT_RETRY_DELAY_MS);
+ }
}
diff --git a/service-t/src/com/android/server/mdns/MdnsRecordRepository.java b/service-t/src/com/android/server/mdns/MdnsRecordRepository.java
index 4b2f553..e975ab4 100644
--- a/service-t/src/com/android/server/mdns/MdnsRecordRepository.java
+++ b/service-t/src/com/android/server/mdns/MdnsRecordRepository.java
@@ -43,6 +43,7 @@
import java.util.Iterator;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.TreeMap;
@@ -721,6 +722,55 @@
}
/**
+ * Get the service IDs of services conflicting with a received packet.
+ */
+ public Set<Integer> getConflictingServices(MdnsPacket packet) {
+ // Avoid allocating a new set for each incoming packet: use an empty set by default.
+ Set<Integer> conflicting = Collections.emptySet();
+ for (MdnsRecord record : packet.answers) {
+ for (int i = 0; i < mServices.size(); i++) {
+ final ServiceRegistration registration = mServices.valueAt(i);
+ if (registration.exiting) continue;
+
+ // Only look for conflicts in service name, as a different service name can be used
+ // if there is a conflict, but there is nothing actionable if any other conflict
+ // happens. In fact probing is only done for the service name in the SRV record.
+ // This means only SRV and TXT records need to be checked.
+ final RecordInfo<MdnsServiceRecord> srvRecord = registration.srvRecord;
+ if (!Arrays.equals(record.getName(), srvRecord.record.getName())) continue;
+
+ // As per RFC6762 9., it's fine if the "conflict" is an identical record with same
+ // data.
+ if (record instanceof MdnsServiceRecord) {
+ final MdnsServiceRecord local = srvRecord.record;
+ final MdnsServiceRecord other = (MdnsServiceRecord) record;
+ // Note "equals" does not consider TTL or receipt time, as intended here
+ if (Objects.equals(local, other)) {
+ continue;
+ }
+ }
+
+ if (record instanceof MdnsTextRecord) {
+ final MdnsTextRecord local = registration.txtRecord.record;
+ final MdnsTextRecord other = (MdnsTextRecord) record;
+ if (Objects.equals(local, other)) {
+ continue;
+ }
+ }
+
+ if (conflicting.size() == 0) {
+ // Conflict was found: use a mutable set
+ conflicting = new ArraySet<>();
+ }
+ final int serviceId = mServices.keyAt(i);
+ conflicting.add(serviceId);
+ }
+ }
+
+ return conflicting;
+ }
+
+ /**
* (Re)set a service to the probing state.
* @return The {@link MdnsProber.ProbingInfo} to send for probing.
*/
@@ -754,6 +804,21 @@
}
/**
+ * Rename a service to the newly provided info, following a conflict.
+ *
+ * If the specified service does not exist, this returns null.
+ */
+ @Nullable
+ public MdnsProber.ProbingInfo renameServiceForConflict(int serviceId, NsdServiceInfo newInfo) {
+ if (!mServices.contains(serviceId)) return null;
+
+ final ServiceRegistration newService = new ServiceRegistration(
+ mDeviceHostname, newInfo);
+ mServices.put(serviceId, newService);
+ return makeProbingInfo(serviceId, newService.srvRecord.record);
+ }
+
+ /**
* Called when {@link MdnsAdvertiser} sent an advertisement for the given service.
*/
public void onAdvertisementSent(int serviceId) {
diff --git a/service-t/src/com/android/server/net/NetworkStatsService.java b/service-t/src/com/android/server/net/NetworkStatsService.java
index 5852a30..4eeaf6b 100644
--- a/service-t/src/com/android/server/net/NetworkStatsService.java
+++ b/service-t/src/com/android/server/net/NetworkStatsService.java
@@ -3269,4 +3269,7 @@
private static native long nativeGetTotalStat(int type);
private static native long nativeGetIfaceStat(String iface, int type);
private static native long nativeGetUidStat(int uid, int type);
+
+ /** Initializes and registers the Perfetto Network Trace data source */
+ public static native void nativeInitNetworkTracing();
}
diff --git a/service/ServiceConnectivityResources/res/values-eu/strings.xml b/service/ServiceConnectivityResources/res/values-eu/strings.xml
index 9b39fd3..13f9eb4 100644
--- a/service/ServiceConnectivityResources/res/values-eu/strings.xml
+++ b/service/ServiceConnectivityResources/res/values-eu/strings.xml
@@ -36,7 +36,7 @@
<item msgid="3004933964374161223">"datu-konexioa"</item>
<item msgid="5624324321165953608">"Wifia"</item>
<item msgid="5667906231066981731">"Bluetootha"</item>
- <item msgid="346574747471703768">"Ethernet-a"</item>
+ <item msgid="346574747471703768">"Etherneta"</item>
<item msgid="5734728378097476003">"VPNa"</item>
</string-array>
<string name="network_switch_type_name_unknown" msgid="5116448402191972082">"sare mota ezezaguna"</string>
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index a7e6a2e..f5c6fb7 100755
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -269,6 +269,7 @@
import com.android.networkstack.apishim.common.BroadcastOptionsShim;
import com.android.networkstack.apishim.common.UnsupportedApiLevelException;
import com.android.server.connectivity.AutodestructReference;
+import com.android.server.connectivity.AutomaticOnOffKeepaliveTracker;
import com.android.server.connectivity.CarrierPrivilegeAuthenticator;
import com.android.server.connectivity.ClatCoordinator;
import com.android.server.connectivity.ConnectivityFlags;
@@ -843,7 +844,7 @@
private final LocationPermissionChecker mLocationPermissionChecker;
- private final KeepaliveTracker mKeepaliveTracker;
+ private final AutomaticOnOffKeepaliveTracker mKeepaliveTracker;
private final QosCallbackTracker mQosCallbackTracker;
private final NetworkNotificationManager mNotifier;
private final LingerMonitor mLingerMonitor;
@@ -1565,7 +1566,7 @@
mSettingsObserver = new SettingsObserver(mContext, mHandler);
registerSettingsCallbacks();
- mKeepaliveTracker = new KeepaliveTracker(mContext, mHandler);
+ mKeepaliveTracker = new AutomaticOnOffKeepaliveTracker(mContext, mHandler);
mNotifier = new NetworkNotificationManager(mContext, mTelephonyManager);
mQosCallbackTracker = new QosCallbackTracker(mHandler, mNetworkRequestCounter);
@@ -5544,6 +5545,33 @@
mKeepaliveTracker.handleStartKeepalive(msg);
break;
}
+ case NetworkAgent.CMD_MONITOR_AUTOMATIC_KEEPALIVE: {
+ final Network network = (Network) msg.obj;
+ final int slot = msg.arg1;
+
+ boolean networkFound = false;
+ final ArrayList<NetworkAgentInfo> vpnsRunningOnThisNetwork = new ArrayList<>();
+ for (NetworkAgentInfo n : mNetworkAgentInfos) {
+ if (n.network.equals(network)) networkFound = true;
+ if (n.isVPN() && n.everConnected() && hasUnderlyingNetwork(n, network)) {
+ vpnsRunningOnThisNetwork.add(n);
+ }
+ }
+
+ // If the network no longer exists, then the keepalive should have been
+ // cleaned up already. There is no point trying to resume keepalives.
+ if (!networkFound) return;
+
+ if (!vpnsRunningOnThisNetwork.isEmpty()) {
+ mKeepaliveTracker.handleMonitorAutomaticKeepalive(network, slot,
+ // TODO: check all the VPNs running on top of this network
+ vpnsRunningOnThisNetwork.get(0).network.netId);
+ } else {
+ // If no VPN, then make sure the keepalive is running.
+ mKeepaliveTracker.handleMaybeResumeKeepalive(network, slot);
+ }
+ break;
+ }
// Sent by KeepaliveTracker to process an app request on the state machine thread.
case NetworkAgent.CMD_STOP_SOCKET_KEEPALIVE: {
NetworkAgentInfo nai = getNetworkAgentInfoForNetwork((Network) msg.obj);
@@ -6217,9 +6245,7 @@
if (mOemNetworkPreferences.getNetworkPreferences().size() > 0) {
handleSetOemNetworkPreference(mOemNetworkPreferences, null);
}
- if (!mProfileNetworkPreferences.isEmpty()) {
- updateProfileAllowedNetworks();
- }
+ updateProfileAllowedNetworks();
}
private void onUserRemoved(@NonNull final UserHandle user) {
@@ -9788,20 +9814,23 @@
enforceKeepalivePermission();
mKeepaliveTracker.startNattKeepalive(
getNetworkAgentInfoForNetwork(network), null /* fd */,
- intervalSeconds, cb,
- srcAddr, srcPort, dstAddr, NattSocketKeepalive.NATT_PORT);
+ intervalSeconds, cb, srcAddr, srcPort, dstAddr, NattSocketKeepalive.NATT_PORT,
+ // Keep behavior of the deprecated method as it is. Set automaticOnOffKeepalives to
+ // false because there is no way and no plan to configure automaticOnOffKeepalives
+ // in this deprecated method.
+ false /* automaticOnOffKeepalives */);
}
@Override
public void startNattKeepaliveWithFd(Network network, ParcelFileDescriptor pfd, int resourceId,
int intervalSeconds, ISocketKeepaliveCallback cb, String srcAddr,
- String dstAddr) {
+ String dstAddr, boolean automaticOnOffKeepalives) {
try {
final FileDescriptor fd = pfd.getFileDescriptor();
mKeepaliveTracker.startNattKeepalive(
getNetworkAgentInfoForNetwork(network), fd, resourceId,
intervalSeconds, cb,
- srcAddr, dstAddr, NattSocketKeepalive.NATT_PORT);
+ srcAddr, dstAddr, NattSocketKeepalive.NATT_PORT, automaticOnOffKeepalives);
} finally {
// FileDescriptors coming from AIDL calls must be manually closed to prevent leaks.
// startNattKeepalive calls Os.dup(fd) before returning, so we can close immediately.
diff --git a/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
new file mode 100644
index 0000000..27be545
--- /dev/null
+++ b/service/src/com/android/server/connectivity/AutomaticOnOffKeepaliveTracker.java
@@ -0,0 +1,691 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import static android.net.NetworkAgent.CMD_START_SOCKET_KEEPALIVE;
+import static android.net.SocketKeepalive.ERROR_INVALID_SOCKET;
+import static android.net.SocketKeepalive.SUCCESS;
+import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
+import static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.SOL_SOCKET;
+import static android.system.OsConstants.SO_SNDTIMEO;
+
+import static com.android.net.module.util.netlink.NetlinkConstants.NLMSG_DONE;
+import static com.android.net.module.util.netlink.NetlinkConstants.SOCKDIAG_MSG_HEADER_SIZE;
+import static com.android.net.module.util.netlink.NetlinkConstants.SOCK_DIAG_BY_FAMILY;
+import static com.android.net.module.util.netlink.NetlinkUtils.IO_TIMEOUT_MS;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.INetd;
+import android.net.ISocketKeepaliveCallback;
+import android.net.MarkMaskParcel;
+import android.net.Network;
+import android.net.NetworkAgent;
+import android.net.SocketKeepalive;
+import android.net.SocketKeepalive.InvalidSocketException;
+import android.os.FileUtils;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.StructTimeval;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.CollectionUtils;
+import com.android.net.module.util.DeviceConfigUtils;
+import com.android.net.module.util.HexDump;
+import com.android.net.module.util.SocketUtils;
+import com.android.net.module.util.netlink.InetDiagMessage;
+import com.android.net.module.util.netlink.NetlinkUtils;
+import com.android.net.module.util.netlink.StructNlAttr;
+
+import java.io.FileDescriptor;
+import java.io.InterruptedIOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.SocketException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Manages automatic on/off socket keepalive requests.
+ *
+ * Provides methods to stop and start automatic keepalive requests, and keeps track of keepalives
+ * across all networks. For non-automatic on/off keepalive request, this class just forwards the
+ * requests to KeepaliveTracker. This class is tightly coupled to ConnectivityService. It is not
+ * thread-safe and its handle* methods must be called only from the ConnectivityService handler
+ * thread.
+ */
+public class AutomaticOnOffKeepaliveTracker {
+ private static final String TAG = "AutomaticOnOffKeepaliveTracker";
+ private static final int[] ADDRESS_FAMILIES = new int[] {AF_INET6, AF_INET};
+ private static final String ACTION_TCP_POLLING_ALARM =
+ "com.android.server.connectivity.KeepaliveTracker.TCP_POLLING_ALARM";
+ private static final String EXTRA_NETWORK = "network_id";
+ private static final String EXTRA_SLOT = "slot";
+ private static final long DEFAULT_TCP_POLLING_INTERVAL_MS = 120_000L;
+ private static final String AUTOMATIC_ON_OFF_KEEPALIVE_VERSION =
+ "automatic_on_off_keepalive_version";
+ /**
+ * States for {@code #AutomaticOnOffKeepalive}.
+ *
+ * A new AutomaticOnOffKeepalive starts with STATE_ENABLED. The system will monitor
+ * the TCP sockets on VPN networks running on top of the specified network, and turn off
+ * keepalive if there is no TCP socket any of the VPN networks. Conversely, it will turn
+ * keepalive back on if any TCP socket is open on any of the VPN networks.
+ *
+ * When there is no TCP socket on any of the VPN networks, the state becomes STATE_SUSPENDED.
+ * The {@link KeepaliveTracker.KeepaliveInfo} object is kept to remember the parameters so it
+ * is possible to resume keepalive later with the same parameters.
+ *
+ * When the system detects some TCP socket is open on one of the VPNs while in STATE_SUSPENDED,
+ * this AutomaticOnOffKeepalive goes to STATE_ENABLED again.
+ *
+ * When finishing keepalive, this object is deleted.
+ */
+ private static final int STATE_ENABLED = 0;
+ private static final int STATE_SUSPENDED = 1;
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "STATE_" }, value = {
+ STATE_ENABLED,
+ STATE_SUSPENDED
+ })
+ private @interface AutomaticOnOffState {}
+
+ @NonNull
+ private final Handler mConnectivityServiceHandler;
+ @NonNull
+ private final KeepaliveTracker mKeepaliveTracker;
+ @NonNull
+ private final Context mContext;
+ @NonNull
+ private final AlarmManager mAlarmManager;
+
+ /**
+ * The {@code inetDiagReqV2} messages for different IP family.
+ *
+ * Key: Ip family type.
+ * Value: Bytes array represent the {@code inetDiagReqV2}.
+ *
+ * This should only be accessed in the connectivity service handler thread.
+ */
+ private final SparseArray<byte[]> mSockDiagMsg = new SparseArray<>();
+ private final Dependencies mDependencies;
+ private final INetd mNetd;
+ /**
+ * Keeps track of automatic on/off keepalive requests.
+ * This should be only updated in ConnectivityService handler thread.
+ */
+ private final ArrayList<AutomaticOnOffKeepalive> mAutomaticOnOffKeepalives = new ArrayList<>();
+
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (ACTION_TCP_POLLING_ALARM.equals(intent.getAction())) {
+ Log.d(TAG, "Received TCP polling intent");
+ final Network network = intent.getParcelableExtra(EXTRA_NETWORK);
+ final int slot = intent.getIntExtra(EXTRA_SLOT, -1);
+ mConnectivityServiceHandler.obtainMessage(
+ NetworkAgent.CMD_MONITOR_AUTOMATIC_KEEPALIVE,
+ slot, 0 , network).sendToTarget();
+ }
+ }
+ };
+
+ private static class AutomaticOnOffKeepalive {
+ @NonNull
+ private final KeepaliveTracker.KeepaliveInfo mKi;
+ @NonNull
+ private final FileDescriptor mFd;
+ @NonNull
+ private final PendingIntent mTcpPollingAlarm;
+ private final int mSlot;
+ @AutomaticOnOffState
+ private int mAutomaticOnOffState = STATE_ENABLED;
+
+ AutomaticOnOffKeepalive(@NonNull KeepaliveTracker.KeepaliveInfo ki,
+ @NonNull Context context) throws InvalidSocketException {
+ this.mKi = Objects.requireNonNull(ki);
+ // A null fd is acceptable in KeepaliveInfo for backward compatibility of
+ // PacketKeepalive API, but it should not happen here because legacy API cannot setup
+ // automatic keepalive.
+ Objects.requireNonNull(ki.mFd);
+
+ // Get the slot from keepalive because the slot information may be missing when the
+ // keepalive is stopped.
+ this.mSlot = ki.getSlot();
+ try {
+ this.mFd = Os.dup(ki.mFd);
+ } catch (ErrnoException e) {
+ Log.e(TAG, "Cannot dup fd: ", e);
+ throw new InvalidSocketException(ERROR_INVALID_SOCKET, e);
+ }
+ mTcpPollingAlarm = createTcpPollingAlarmIntent(
+ context, ki.getNai().network(), ki.getSlot());
+ }
+
+ public boolean match(Network network, int slot) {
+ return this.mKi.getNai().network().equals(network) && this.mSlot == slot;
+ }
+
+ private static PendingIntent createTcpPollingAlarmIntent(@NonNull Context context,
+ @NonNull Network network, int slot) {
+ final Intent intent = new Intent(ACTION_TCP_POLLING_ALARM);
+ intent.putExtra(EXTRA_NETWORK, network);
+ intent.putExtra(EXTRA_SLOT, slot);
+ return PendingIntent.getBroadcast(
+ context, 0 /* requestCode */, intent, PendingIntent.FLAG_IMMUTABLE);
+ }
+ }
+
+ public AutomaticOnOffKeepaliveTracker(@NonNull Context context, @NonNull Handler handler) {
+ this(context, handler, new Dependencies(context));
+ }
+
+ @VisibleForTesting
+ public AutomaticOnOffKeepaliveTracker(@NonNull Context context, @NonNull Handler handler,
+ @NonNull Dependencies dependencies) {
+ mContext = Objects.requireNonNull(context);
+ mDependencies = Objects.requireNonNull(dependencies);
+ mConnectivityServiceHandler = Objects.requireNonNull(handler);
+ mNetd = mDependencies.getNetd();
+ mKeepaliveTracker = mDependencies.newKeepaliveTracker(
+ mContext, mConnectivityServiceHandler);
+
+ if (SdkLevel.isAtLeastU()) {
+ mContext.registerReceiver(mReceiver, new IntentFilter(ACTION_TCP_POLLING_ALARM),
+ null, handler);
+ }
+ mAlarmManager = mContext.getSystemService(AlarmManager.class);
+ }
+
+ private void startTcpPollingAlarm(@NonNull PendingIntent alarm) {
+ final long triggerAtMillis =
+ SystemClock.elapsedRealtime() + DEFAULT_TCP_POLLING_INTERVAL_MS;
+ // Setup a non-wake up alarm.
+ mAlarmManager.setExact(AlarmManager.ELAPSED_REALTIME, triggerAtMillis, alarm);
+ }
+
+ /**
+ * Determine if any state transition is needed for the specific automatic keepalive.
+ */
+ public void handleMonitorAutomaticKeepalive(@NonNull Network network, int slot, int vpnNetId) {
+ final AutomaticOnOffKeepalive autoKi = findAutomaticOnOffKeepalive(network, slot);
+ // This may happen if the keepalive is removed by the app, and the alarm is fired at the
+ // same time.
+ if (autoKi == null) return;
+
+ handleMonitorTcpConnections(autoKi, vpnNetId);
+ }
+
+ /**
+ * Determine if disable or re-enable keepalive is needed or not based on TCP sockets status.
+ */
+ private void handleMonitorTcpConnections(@NonNull AutomaticOnOffKeepalive ki, int vpnNetId) {
+ if (!isAnyTcpSocketConnected(vpnNetId)) {
+ // No TCP socket exists. Stop keepalive if ENABLED, and remain SUSPENDED if currently
+ // SUSPENDED.
+ if (ki.mAutomaticOnOffState == STATE_ENABLED) {
+ ki.mAutomaticOnOffState = STATE_SUSPENDED;
+ handleSuspendKeepalive(ki.mKi.mNai, ki.mSlot, SUCCESS);
+ }
+ } else {
+ handleMaybeResumeKeepalive(ki);
+ }
+ // TODO: listen to socket status instead of periodically check.
+ startTcpPollingAlarm(ki.mTcpPollingAlarm);
+ }
+
+ /**
+ * Resume keepalive for this slot on this network, if it wasn't already resumed.
+ */
+ public void handleMaybeResumeKeepalive(@NonNull final Network network, final int slot) {
+ final AutomaticOnOffKeepalive autoKi = findAutomaticOnOffKeepalive(network, slot);
+ // This may happen if the keepalive is removed by the app, and the alarm is fired at
+ // the same time.
+ if (autoKi == null) return;
+ handleMaybeResumeKeepalive(autoKi);
+ }
+
+ private void handleMaybeResumeKeepalive(@NonNull AutomaticOnOffKeepalive autoKi) {
+ if (autoKi.mAutomaticOnOffState == STATE_ENABLED) return;
+ KeepaliveTracker.KeepaliveInfo newKi;
+ try {
+ // Get fd from AutomaticOnOffKeepalive since the fd in the original
+ // KeepaliveInfo should be closed.
+ newKi = autoKi.mKi.withFd(autoKi.mFd);
+ } catch (InvalidSocketException | IllegalArgumentException | SecurityException e) {
+ Log.e(TAG, "Fail to construct keepalive", e);
+ mKeepaliveTracker.notifyErrorCallback(autoKi.mKi.mCallback, ERROR_INVALID_SOCKET);
+ return;
+ }
+ autoKi.mAutomaticOnOffState = STATE_ENABLED;
+ handleResumeKeepalive(mConnectivityServiceHandler.obtainMessage(
+ NetworkAgent.CMD_START_SOCKET_KEEPALIVE,
+ autoKi.mAutomaticOnOffState, 0, newKi));
+ }
+
+ private int findAutomaticOnOffKeepaliveIndex(@NonNull Network network, int slot) {
+ ensureRunningOnHandlerThread();
+
+ int index = 0;
+ for (AutomaticOnOffKeepalive ki : mAutomaticOnOffKeepalives) {
+ if (ki.match(network, slot)) {
+ return index;
+ }
+ index++;
+ }
+ return -1;
+ }
+
+ @Nullable
+ private AutomaticOnOffKeepalive findAutomaticOnOffKeepalive(@NonNull Network network,
+ int slot) {
+ ensureRunningOnHandlerThread();
+
+ final int index = findAutomaticOnOffKeepaliveIndex(network, slot);
+ return (index >= 0) ? mAutomaticOnOffKeepalives.get(index) : null;
+ }
+
+ /**
+ * Handle keepalive events from lower layer.
+ *
+ * Forward to KeepaliveTracker.
+ */
+ public void handleEventSocketKeepalive(@NonNull NetworkAgentInfo nai, int slot, int reason) {
+ mKeepaliveTracker.handleEventSocketKeepalive(nai, slot, reason);
+ }
+
+ /**
+ * Handle stop all keepalives on the specific network.
+ */
+ public void handleStopAllKeepalives(NetworkAgentInfo nai, int reason) {
+ mKeepaliveTracker.handleStopAllKeepalives(nai, reason);
+ final List<AutomaticOnOffKeepalive> matches =
+ CollectionUtils.filter(mAutomaticOnOffKeepalives, it -> it.mKi.getNai() == nai);
+ for (final AutomaticOnOffKeepalive ki : matches) {
+ cleanupAutoOnOffKeepalive(ki);
+ }
+ }
+
+ /**
+ * Handle start keepalive contained within a message.
+ *
+ * The message is expected to contain a KeepaliveTracker.KeepaliveInfo.
+ */
+ public void handleStartKeepalive(Message message) {
+ mKeepaliveTracker.handleStartKeepalive(message);
+
+ // Add automatic on/off request into list to track its life cycle.
+ final boolean automaticOnOff = message.arg1 != 0
+ && mDependencies.isFeatureEnabled(AUTOMATIC_ON_OFF_KEEPALIVE_VERSION);
+ if (automaticOnOff) {
+ final KeepaliveTracker.KeepaliveInfo ki = (KeepaliveTracker.KeepaliveInfo) message.obj;
+ AutomaticOnOffKeepalive autoKi;
+ try {
+ // CAREFUL : mKeepaliveTracker.handleStartKeepalive will assign |ki.mSlot| after
+ // pulling |ki| from the message. The constructor below will read this member
+ // (through ki.getSlot()) and therefore actively relies on handleStartKeepalive
+ // having assigned this member before this is called.
+ // TODO : clean this up by assigning the slot at the start of this method instead
+ // and ideally removing the mSlot member from KeepaliveInfo.
+ autoKi = new AutomaticOnOffKeepalive(ki, mContext);
+ } catch (SocketKeepalive.InvalidSocketException | IllegalArgumentException e) {
+ Log.e(TAG, "Fail to construct keepalive", e);
+ mKeepaliveTracker.notifyErrorCallback(ki.mCallback, ERROR_INVALID_SOCKET);
+ return;
+ }
+ mAutomaticOnOffKeepalives.add(autoKi);
+ startTcpPollingAlarm(autoKi.mTcpPollingAlarm);
+ }
+ }
+
+ private void handleResumeKeepalive(Message message) {
+ mKeepaliveTracker.handleStartKeepalive(message);
+ }
+
+ private void handleSuspendKeepalive(NetworkAgentInfo nai, int slot, int reason) {
+ mKeepaliveTracker.handleStopKeepalive(nai, slot, reason);
+ }
+
+ /**
+ * Handle stop keepalives on the specific network with given slot.
+ */
+ public void handleStopKeepalive(NetworkAgentInfo nai, int slot, int reason) {
+ final AutomaticOnOffKeepalive autoKi = findAutomaticOnOffKeepalive(nai.network, slot);
+
+ // Let the original keepalive do the stop first, and then clean up the keepalive if it's an
+ // automatic keepalive.
+ if (autoKi == null || autoKi.mAutomaticOnOffState == STATE_ENABLED) {
+ mKeepaliveTracker.handleStopKeepalive(nai, slot, reason);
+ }
+
+ // Not an AutomaticOnOffKeepalive.
+ if (autoKi == null) return;
+
+ cleanupAutoOnOffKeepalive(autoKi);
+ }
+
+ private void cleanupAutoOnOffKeepalive(@NonNull final AutomaticOnOffKeepalive autoKi) {
+ ensureRunningOnHandlerThread();
+ mAlarmManager.cancel(autoKi.mTcpPollingAlarm);
+ // Close the duplicated fd that maintains the lifecycle of socket.
+ FileUtils.closeQuietly(autoKi.mFd);
+ mAutomaticOnOffKeepalives.remove(autoKi);
+ }
+
+ /**
+ * Called when requesting that keepalives be started on a IPsec NAT-T socket. See
+ * {@link android.net.SocketKeepalive}.
+ *
+ * Forward to KeepaliveTracker.
+ **/
+ public void startNattKeepalive(@Nullable NetworkAgentInfo nai,
+ @Nullable FileDescriptor fd,
+ int intervalSeconds,
+ @NonNull ISocketKeepaliveCallback cb,
+ @NonNull String srcAddrString,
+ int srcPort,
+ @NonNull String dstAddrString,
+ int dstPort, boolean automaticOnOffKeepalives) {
+ final KeepaliveTracker.KeepaliveInfo ki = mKeepaliveTracker.makeNattKeepaliveInfo(nai, fd,
+ intervalSeconds, cb, srcAddrString, srcPort, dstAddrString, dstPort);
+ if (null != ki) {
+ mConnectivityServiceHandler.obtainMessage(NetworkAgent.CMD_START_SOCKET_KEEPALIVE,
+ // TODO : move ConnectivityService#encodeBool to a static lib.
+ automaticOnOffKeepalives ? 1 : 0, 0, ki).sendToTarget();
+ }
+ }
+
+ /**
+ * Called when requesting that keepalives be started on a IPsec NAT-T socket. See
+ * {@link android.net.SocketKeepalive}.
+ *
+ * Forward to KeepaliveTracker.
+ **/
+ public void startNattKeepalive(@Nullable NetworkAgentInfo nai,
+ @Nullable FileDescriptor fd,
+ int resourceId,
+ int intervalSeconds,
+ @NonNull ISocketKeepaliveCallback cb,
+ @NonNull String srcAddrString,
+ @NonNull String dstAddrString,
+ int dstPort,
+ boolean automaticOnOffKeepalives) {
+ final KeepaliveTracker.KeepaliveInfo ki = mKeepaliveTracker.makeNattKeepaliveInfo(nai, fd,
+ resourceId, intervalSeconds, cb, srcAddrString, dstAddrString, dstPort);
+ if (null != ki) {
+ mConnectivityServiceHandler.obtainMessage(NetworkAgent.CMD_START_SOCKET_KEEPALIVE,
+ // TODO : move ConnectivityService#encodeBool to a static lib.
+ automaticOnOffKeepalives ? 1 : 0, 0, ki).sendToTarget();
+ }
+ }
+
+ /**
+ * Called by ConnectivityService to start TCP keepalive on a file descriptor.
+ *
+ * In order to offload keepalive for application correctly, sequence number, ack number and
+ * other fields are needed to form the keepalive packet. Thus, this function synchronously
+ * puts the socket into repair mode to get the necessary information. After the socket has been
+ * put into repair mode, the application cannot access the socket until reverted to normal.
+ * See {@link android.net.SocketKeepalive}.
+ *
+ * Forward to KeepaliveTracker.
+ **/
+ public void startTcpKeepalive(@Nullable NetworkAgentInfo nai,
+ @NonNull FileDescriptor fd,
+ int intervalSeconds,
+ @NonNull ISocketKeepaliveCallback cb) {
+ final KeepaliveTracker.KeepaliveInfo ki = mKeepaliveTracker.makeTcpKeepaliveInfo(nai, fd,
+ intervalSeconds, cb);
+ if (null != ki) {
+ mConnectivityServiceHandler.obtainMessage(CMD_START_SOCKET_KEEPALIVE, ki)
+ .sendToTarget();
+ }
+ }
+
+ /**
+ * Dump AutomaticOnOffKeepaliveTracker state.
+ */
+ public void dump(IndentingPrintWriter pw) {
+ // TODO: Dump the necessary information for automatic on/off keepalive.
+ mKeepaliveTracker.dump(pw);
+ }
+
+ /**
+ * Check all keepalives on the network are still valid.
+ *
+ * Forward to KeepaliveTracker.
+ */
+ public void handleCheckKeepalivesStillValid(NetworkAgentInfo nai) {
+ mKeepaliveTracker.handleCheckKeepalivesStillValid(nai);
+ }
+
+ @VisibleForTesting
+ boolean isAnyTcpSocketConnected(int netId) {
+ FileDescriptor fd = null;
+
+ try {
+ fd = mDependencies.createConnectedNetlinkSocket();
+
+ // Get network mask
+ final MarkMaskParcel parcel = mNetd.getFwmarkForNetwork(netId);
+ final int networkMark = (parcel != null) ? parcel.mark : NetlinkUtils.UNKNOWN_MARK;
+ final int networkMask = (parcel != null) ? parcel.mask : NetlinkUtils.NULL_MASK;
+
+ // Send request for each IP family
+ for (final int family : ADDRESS_FAMILIES) {
+ if (isAnyTcpSocketConnectedForFamily(fd, family, networkMark, networkMask)) {
+ return true;
+ }
+ }
+ } catch (ErrnoException | SocketException | InterruptedIOException | RemoteException e) {
+ Log.e(TAG, "Fail to get socket info via netlink.", e);
+ } finally {
+ SocketUtils.closeSocketQuietly(fd);
+ }
+
+ return false;
+ }
+
+ private boolean isAnyTcpSocketConnectedForFamily(FileDescriptor fd, int family, int networkMark,
+ int networkMask) throws ErrnoException, InterruptedIOException {
+ ensureRunningOnHandlerThread();
+ // Build SocketDiag messages and cache it.
+ if (mSockDiagMsg.get(family) == null) {
+ mSockDiagMsg.put(family, InetDiagMessage.buildInetDiagReqForAliveTcpSockets(family));
+ }
+ mDependencies.sendRequest(fd, mSockDiagMsg.get(family));
+
+ // Iteration limitation as a protection to avoid possible infinite loops.
+ // DEFAULT_RECV_BUFSIZE could read more than 20 sockets per time. Max iteration
+ // should be enough to go through reasonable TCP sockets in the device.
+ final int maxIteration = 100;
+ int parsingIteration = 0;
+ while (parsingIteration < maxIteration) {
+ final ByteBuffer bytes = mDependencies.recvSockDiagResponse(fd);
+
+ try {
+ while (NetlinkUtils.enoughBytesRemainForValidNlMsg(bytes)) {
+ final int startPos = bytes.position();
+
+ final int nlmsgLen = bytes.getInt();
+ final int nlmsgType = bytes.getShort();
+ if (isEndOfMessageOrError(nlmsgType)) return false;
+ // TODO: Parse InetDiagMessage to get uid and dst address information to filter
+ // socket via NetlinkMessage.parse.
+
+ // Skip the header to move to data part.
+ bytes.position(startPos + SOCKDIAG_MSG_HEADER_SIZE);
+
+ if (isTargetTcpSocket(bytes, nlmsgLen, networkMark, networkMask)) {
+ return true;
+ }
+ }
+ } catch (BufferUnderflowException e) {
+ // The exception happens in random place in either header position or any data
+ // position. Partial bytes from the middle of the byte buffer may not be enough to
+ // clarify, so print out the content before the error to possibly prevent printing
+ // the whole 8K buffer.
+ final int exceptionPos = bytes.position();
+ final String hex = HexDump.dumpHexString(bytes.array(), 0, exceptionPos);
+ Log.e(TAG, "Unexpected socket info parsing: " + hex, e);
+ }
+
+ parsingIteration++;
+ }
+ return false;
+ }
+
+ private boolean isEndOfMessageOrError(int nlmsgType) {
+ return nlmsgType == NLMSG_DONE || nlmsgType != SOCK_DIAG_BY_FAMILY;
+ }
+
+ private boolean isTargetTcpSocket(@NonNull ByteBuffer bytes, int nlmsgLen, int networkMark,
+ int networkMask) {
+ final int mark = readSocketDataAndReturnMark(bytes, nlmsgLen);
+ return (mark & networkMask) == networkMark;
+ }
+
+ private int readSocketDataAndReturnMark(@NonNull ByteBuffer bytes, int nlmsgLen) {
+ final int nextMsgOffset = bytes.position() + nlmsgLen - SOCKDIAG_MSG_HEADER_SIZE;
+ int mark = NetlinkUtils.INIT_MARK_VALUE;
+ // Get socket mark
+ // TODO: Add a parsing method in NetlinkMessage.parse to support this to skip the remaining
+ // data.
+ while (bytes.position() < nextMsgOffset) {
+ final StructNlAttr nlattr = StructNlAttr.parse(bytes);
+ if (nlattr != null && nlattr.nla_type == NetlinkUtils.INET_DIAG_MARK) {
+ mark = nlattr.getValueAsInteger();
+ }
+ }
+ return mark;
+ }
+
+ private void ensureRunningOnHandlerThread() {
+ if (mConnectivityServiceHandler.getLooper().getThread() != Thread.currentThread()) {
+ throw new IllegalStateException(
+ "Not running on handler thread: " + Thread.currentThread().getName());
+ }
+ }
+
+ /**
+ * Dependencies class for testing.
+ */
+ @VisibleForTesting
+ public static class Dependencies {
+ private final Context mContext;
+
+ public Dependencies(final Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Create a netlink socket connected to the kernel.
+ *
+ * @return fd the fileDescriptor of the socket.
+ */
+ public FileDescriptor createConnectedNetlinkSocket()
+ throws ErrnoException, SocketException {
+ final FileDescriptor fd = NetlinkUtils.createNetLinkInetDiagSocket();
+ NetlinkUtils.connectSocketToNetlink(fd);
+ Os.setsockoptTimeval(fd, SOL_SOCKET, SO_SNDTIMEO,
+ StructTimeval.fromMillis(IO_TIMEOUT_MS));
+ return fd;
+ }
+
+ /**
+ * Send composed message request to kernel.
+ *
+ * The given FileDescriptor is expected to be created by
+ * {@link #createConnectedNetlinkSocket} or equivalent way.
+ *
+ * @param fd a netlink socket {@code FileDescriptor} connected to the kernel.
+ * @param msg the byte array representing the request message to write to kernel.
+ */
+ public void sendRequest(@NonNull final FileDescriptor fd,
+ @NonNull final byte[] msg)
+ throws ErrnoException, InterruptedIOException {
+ Os.write(fd, msg, 0 /* byteOffset */, msg.length);
+ }
+
+ /**
+ * Get an INetd connector.
+ */
+ public INetd getNetd() {
+ return INetd.Stub.asInterface(
+ (IBinder) mContext.getSystemService(Context.NETD_SERVICE));
+ }
+
+ /**
+ * Receive the response message from kernel via given {@code FileDescriptor}.
+ * The usage should follow the {@code #sendRequest} call with the same
+ * FileDescriptor.
+ *
+ * The overall response may be large but the individual messages should not be
+ * excessively large(8-16kB) because trying to get the kernel to return
+ * everything in one big buffer is inefficient as it forces the kernel to allocate
+ * large chunks of linearly physically contiguous memory. The usage should iterate the
+ * call of this method until the end of the overall message.
+ *
+ * The default receiving buffer size should be small enough that it is always
+ * processed within the {@link NetlinkUtils#IO_TIMEOUT_MS} timeout.
+ */
+ public ByteBuffer recvSockDiagResponse(@NonNull final FileDescriptor fd)
+ throws ErrnoException, InterruptedIOException {
+ return NetlinkUtils.recvMessage(
+ fd, NetlinkUtils.DEFAULT_RECV_BUFSIZE, NetlinkUtils.IO_TIMEOUT_MS);
+ }
+
+ /**
+ * Construct a new KeepaliveTracker.
+ */
+ public KeepaliveTracker newKeepaliveTracker(@NonNull Context context,
+ @NonNull Handler connectivityserviceHander) {
+ return new KeepaliveTracker(mContext, connectivityserviceHander);
+ }
+
+ /**
+ * Find out if a feature is enabled from DeviceConfig.
+ *
+ * @param name The name of the property to look up.
+ * @return whether the feature is enabled
+ */
+ public boolean isFeatureEnabled(@NonNull final String name) {
+ return DeviceConfigUtils.isFeatureEnabled(mContext, NAMESPACE_CONNECTIVITY, name);
+ }
+ }
+}
diff --git a/service/src/com/android/server/connectivity/KeepaliveTracker.java b/service/src/com/android/server/connectivity/KeepaliveTracker.java
index 9c36760..03f8f3e 100644
--- a/service/src/com/android/server/connectivity/KeepaliveTracker.java
+++ b/service/src/com/android/server/connectivity/KeepaliveTracker.java
@@ -18,7 +18,6 @@
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.net.NattSocketKeepalive.NATT_PORT;
-import static android.net.NetworkAgent.CMD_START_SOCKET_KEEPALIVE;
import static android.net.SocketKeepalive.BINDER_DIED;
import static android.net.SocketKeepalive.DATA_RECEIVED;
import static android.net.SocketKeepalive.ERROR_INSUFFICIENT_RESOURCES;
@@ -33,27 +32,15 @@
import static android.net.SocketKeepalive.MIN_INTERVAL_SEC;
import static android.net.SocketKeepalive.NO_KEEPALIVE;
import static android.net.SocketKeepalive.SUCCESS;
-import static android.system.OsConstants.AF_INET;
-import static android.system.OsConstants.AF_INET6;
-import static android.system.OsConstants.SOL_SOCKET;
-import static android.system.OsConstants.SO_SNDTIMEO;
-
-import static com.android.net.module.util.netlink.NetlinkConstants.NLMSG_DONE;
-import static com.android.net.module.util.netlink.NetlinkConstants.SOCKDIAG_MSG_HEADER_SIZE;
-import static com.android.net.module.util.netlink.NetlinkConstants.SOCK_DIAG_BY_FAMILY;
-import static com.android.net.module.util.netlink.NetlinkUtils.IO_TIMEOUT_MS;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
-import android.content.res.Resources;
import android.net.ConnectivityResources;
-import android.net.INetd;
import android.net.ISocketKeepaliveCallback;
import android.net.InetAddresses;
import android.net.InvalidPacketException;
import android.net.KeepalivePacketData;
-import android.net.MarkMaskParcel;
import android.net.NattKeepalivePacketData;
import android.net.NetworkAgent;
import android.net.SocketKeepalive.InvalidSocketException;
@@ -67,29 +54,18 @@
import android.os.RemoteException;
import android.system.ErrnoException;
import android.system.Os;
-import android.system.StructTimeval;
import android.util.Log;
import android.util.Pair;
-import android.util.SparseArray;
import com.android.connectivity.resources.R;
-import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import com.android.net.module.util.HexDump;
import com.android.net.module.util.IpUtils;
-import com.android.net.module.util.SocketUtils;
-import com.android.net.module.util.netlink.InetDiagMessage;
-import com.android.net.module.util.netlink.NetlinkUtils;
-import com.android.net.module.util.netlink.StructNlAttr;
import java.io.FileDescriptor;
-import java.io.InterruptedIOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
-import java.net.SocketException;
-import java.nio.BufferUnderflowException;
-import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@@ -107,12 +83,10 @@
private static final boolean DBG = false;
public static final String PERMISSION = android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD;
- private static final int[] ADDRESS_FAMILIES = new int[] {AF_INET6, AF_INET};
/** Keeps track of keepalive requests. */
private final HashMap <NetworkAgentInfo, HashMap<Integer, KeepaliveInfo>> mKeepalives =
new HashMap<> ();
- private final Handler mConnectivityServiceHandler;
@NonNull
private final TcpKeepaliveController mTcpController;
@NonNull
@@ -131,35 +105,17 @@
// Allowed unprivileged keepalive slots per uid. Caller's permission will be enforced if
// the number of remaining keepalive slots is less than or equal to the threshold.
private final int mAllowedUnprivilegedSlotsForUid;
- /**
- * The {@code inetDiagReqV2} messages for different IP family.
- *
- * Key: Ip family type.
- * Value: Bytes array represent the {@code inetDiagReqV2}.
- *
- * This should only be accessed in the connectivity service handler thread.
- */
- private final SparseArray<byte[]> mSockDiagMsg = new SparseArray<>();
- private final Dependencies mDependencies;
- private final INetd mNetd;
public KeepaliveTracker(Context context, Handler handler) {
- this(context, handler, new Dependencies(context));
- }
-
- @VisibleForTesting
- public KeepaliveTracker(Context context, Handler handler, Dependencies dependencies) {
- mConnectivityServiceHandler = handler;
mTcpController = new TcpKeepaliveController(handler);
mContext = context;
- mDependencies = dependencies;
- mSupportedKeepalives = mDependencies.getSupportedKeepalives();
- mNetd = mDependencies.getNetd();
- final Resources res = mDependencies.newConnectivityResources();
- mReservedPrivilegedSlots = res.getInteger(
+ mSupportedKeepalives = KeepaliveUtils.getSupportedKeepalives(mContext);
+
+ final ConnectivityResources res = new ConnectivityResources(mContext);
+ mReservedPrivilegedSlots = res.get().getInteger(
R.integer.config_reservedPrivilegedKeepaliveSlots);
- mAllowedUnprivilegedSlotsForUid = res.getInteger(
+ mAllowedUnprivilegedSlotsForUid = res.get().getInteger(
R.integer.config_allowedUnprivilegedKeepalivePerUid);
}
@@ -171,13 +127,13 @@
*/
class KeepaliveInfo implements IBinder.DeathRecipient {
// Bookkeeping data.
- private final ISocketKeepaliveCallback mCallback;
+ public final ISocketKeepaliveCallback mCallback;
private final int mUid;
private final int mPid;
private final boolean mPrivileged;
- private final NetworkAgentInfo mNai;
+ public final NetworkAgentInfo mNai;
private final int mType;
- private final FileDescriptor mFd;
+ public final FileDescriptor mFd;
public static final int TYPE_NATT = 1;
public static final int TYPE_TCP = 2;
@@ -285,6 +241,10 @@
}
}
+ public int getSlot() {
+ return mSlot;
+ }
+
private int checkNetworkConnected() {
if (!mNai.networkInfo.isConnectedOrConnecting()) {
return ERROR_INVALID_NETWORK;
@@ -457,6 +417,13 @@
void onFileDescriptorInitiatedStop(final int socketKeepaliveReason) {
handleStopKeepalive(mNai, mSlot, socketKeepaliveReason);
}
+
+ /**
+ * Construct a new KeepaliveInfo from existing KeepaliveInfo with a new fd.
+ */
+ public KeepaliveInfo withFd(@NonNull FileDescriptor fd) throws InvalidSocketException {
+ return new KeepaliveInfo(mCallback, mNai, mPacket, mInterval, mType, fd);
+ }
}
void notifyErrorCallback(ISocketKeepaliveCallback cb, int error) {
@@ -486,6 +453,9 @@
return slot;
}
+ /**
+ * Handle start keepalives with the message.
+ */
public void handleStartKeepalive(Message message) {
KeepaliveInfo ki = (KeepaliveInfo) message.obj;
NetworkAgentInfo nai = ki.getNai();
@@ -646,7 +616,8 @@
* Called when requesting that keepalives be started on a IPsec NAT-T socket. See
* {@link android.net.SocketKeepalive}.
**/
- public void startNattKeepalive(@Nullable NetworkAgentInfo nai,
+ @Nullable
+ public KeepaliveInfo makeNattKeepaliveInfo(@Nullable NetworkAgentInfo nai,
@Nullable FileDescriptor fd,
int intervalSeconds,
@NonNull ISocketKeepaliveCallback cb,
@@ -656,7 +627,7 @@
int dstPort) {
if (nai == null) {
notifyErrorCallback(cb, ERROR_INVALID_NETWORK);
- return;
+ return null;
}
InetAddress srcAddress, dstAddress;
@@ -665,7 +636,7 @@
dstAddress = InetAddresses.parseNumericAddress(dstAddrString);
} catch (IllegalArgumentException e) {
notifyErrorCallback(cb, ERROR_INVALID_IP_ADDRESS);
- return;
+ return null;
}
KeepalivePacketData packet;
@@ -674,7 +645,7 @@
srcAddress, srcPort, dstAddress, NATT_PORT);
} catch (InvalidPacketException e) {
notifyErrorCallback(cb, e.getError());
- return;
+ return null;
}
KeepaliveInfo ki = null;
try {
@@ -683,15 +654,14 @@
} catch (InvalidSocketException | IllegalArgumentException | SecurityException e) {
Log.e(TAG, "Fail to construct keepalive", e);
notifyErrorCallback(cb, ERROR_INVALID_SOCKET);
- return;
+ return null;
}
- Log.d(TAG, "Created keepalive: " + ki.toString());
- mConnectivityServiceHandler.obtainMessage(
- NetworkAgent.CMD_START_SOCKET_KEEPALIVE, ki).sendToTarget();
+ Log.d(TAG, "Created keepalive: " + ki);
+ return ki;
}
/**
- * Called by ConnectivityService to start TCP keepalive on a file descriptor.
+ * Make a KeepaliveInfo for a TCP socket.
*
* In order to offload keepalive for application correctly, sequence number, ack number and
* other fields are needed to form the keepalive packet. Thus, this function synchronously
@@ -700,13 +670,14 @@
*
* See {@link android.net.SocketKeepalive}.
**/
- public void startTcpKeepalive(@Nullable NetworkAgentInfo nai,
+ @Nullable
+ public KeepaliveInfo makeTcpKeepaliveInfo(@Nullable NetworkAgentInfo nai,
@NonNull FileDescriptor fd,
int intervalSeconds,
@NonNull ISocketKeepaliveCallback cb) {
if (nai == null) {
notifyErrorCallback(cb, ERROR_INVALID_NETWORK);
- return;
+ return null;
}
final TcpKeepalivePacketData packet;
@@ -714,10 +685,10 @@
packet = TcpKeepaliveController.getTcpKeepalivePacket(fd);
} catch (InvalidSocketException e) {
notifyErrorCallback(cb, e.error);
- return;
+ return null;
} catch (InvalidPacketException e) {
notifyErrorCallback(cb, e.getError());
- return;
+ return null;
}
KeepaliveInfo ki = null;
try {
@@ -726,20 +697,22 @@
} catch (InvalidSocketException | IllegalArgumentException | SecurityException e) {
Log.e(TAG, "Fail to construct keepalive e=" + e);
notifyErrorCallback(cb, ERROR_INVALID_SOCKET);
- return;
+ return null;
}
Log.d(TAG, "Created keepalive: " + ki.toString());
- mConnectivityServiceHandler.obtainMessage(CMD_START_SOCKET_KEEPALIVE, ki).sendToTarget();
+ return ki;
}
- /**
- * Called when requesting that keepalives be started on a IPsec NAT-T socket. This function is
- * identical to {@link #startNattKeepalive}, but also takes a {@code resourceId}, which is the
- * resource index bound to the {@link UdpEncapsulationSocket} when creating by
- * {@link com.android.server.IpSecService} to verify whether the given
- * {@link UdpEncapsulationSocket} is legitimate.
- **/
- public void startNattKeepalive(@Nullable NetworkAgentInfo nai,
+ /**
+ * Make a KeepaliveInfo for an IPSec NAT-T socket.
+ *
+ * This function is identical to {@link #makeNattKeepaliveInfo}, but also takes a
+ * {@code resourceId}, which is the resource index bound to the {@link UdpEncapsulationSocket}
+ * when creating by {@link com.android.server.IpSecService} to verify whether the given
+ * {@link UdpEncapsulationSocket} is legitimate.
+ **/
+ @Nullable
+ public KeepaliveInfo makeNattKeepaliveInfo(@Nullable NetworkAgentInfo nai,
@Nullable FileDescriptor fd,
int resourceId,
int intervalSeconds,
@@ -750,6 +723,7 @@
// Ensure that the socket is created by IpSecService.
if (!isNattKeepaliveSocketValid(fd, resourceId)) {
notifyErrorCallback(cb, ERROR_INVALID_SOCKET);
+ return null;
}
// Get src port to adopt old API.
@@ -759,10 +733,11 @@
srcPort = ((InetSocketAddress) srcSockAddr).getPort();
} catch (ErrnoException e) {
notifyErrorCallback(cb, ERROR_INVALID_SOCKET);
+ return null;
}
// Forward request to old API.
- startNattKeepalive(nai, fd, intervalSeconds, cb, srcAddrString, srcPort,
+ return makeNattKeepaliveInfo(nai, fd, intervalSeconds, cb, srcAddrString, srcPort,
dstAddrString, dstPort);
}
@@ -801,196 +776,4 @@
}
pw.decreaseIndent();
}
-
- /**
- * Dependencies class for testing.
- */
- @VisibleForTesting
- public static class Dependencies {
- private final Context mContext;
-
- public Dependencies(final Context context) {
- mContext = context;
- }
-
- /**
- * Create a netlink socket connected to the kernel.
- *
- * @return fd the fileDescriptor of the socket.
- */
- public FileDescriptor createConnectedNetlinkSocket()
- throws ErrnoException, SocketException {
- final FileDescriptor fd = NetlinkUtils.createNetLinkInetDiagSocket();
- NetlinkUtils.connectSocketToNetlink(fd);
- Os.setsockoptTimeval(fd, SOL_SOCKET, SO_SNDTIMEO,
- StructTimeval.fromMillis(IO_TIMEOUT_MS));
- return fd;
- }
-
- /**
- * Send composed message request to kernel.
- *
- * The given FileDescriptor is expected to be created by
- * {@link #createConnectedNetlinkSocket} or equivalent way.
- *
- * @param fd a netlink socket {@code FileDescriptor} connected to the kernel.
- * @param msg the byte array representing the request message to write to kernel.
- */
- public void sendRequest(@NonNull final FileDescriptor fd,
- @NonNull final byte[] msg)
- throws ErrnoException, InterruptedIOException {
- Os.write(fd, msg, 0 /* byteOffset */, msg.length);
- }
-
- /**
- * Get an INetd connector.
- */
- public INetd getNetd() {
- return INetd.Stub.asInterface(
- (IBinder) mContext.getSystemService(Context.NETD_SERVICE));
- }
-
- /**
- * Receive the response message from kernel via given {@code FileDescriptor}.
- * The usage should follow the {@code #sendRequest} call with the same
- * FileDescriptor.
- *
- * The overall response may be large but the individual messages should not be
- * excessively large(8-16kB) because trying to get the kernel to return
- * everything in one big buffer is inefficient as it forces the kernel to allocate
- * large chunks of linearly physically contiguous memory. The usage should iterate the
- * call of this method until the end of the overall message.
- *
- * The default receiving buffer size should be small enough that it is always
- * processed within the {@link NetlinkUtils#IO_TIMEOUT_MS} timeout.
- */
- public ByteBuffer recvSockDiagResponse(@NonNull final FileDescriptor fd)
- throws ErrnoException, InterruptedIOException {
- return NetlinkUtils.recvMessage(
- fd, NetlinkUtils.DEFAULT_RECV_BUFSIZE, NetlinkUtils.IO_TIMEOUT_MS);
- }
-
- /**
- * Read supported keepalive count for each transport type from overlay resource.
- */
- public int[] getSupportedKeepalives() {
- return KeepaliveUtils.getSupportedKeepalives(mContext);
- }
-
- /**
- * Construct a new Resource from a new ConnectivityResources.
- */
- public Resources newConnectivityResources() {
- final ConnectivityResources resources = new ConnectivityResources(mContext);
- return resources.get();
- }
- }
-
- private void ensureRunningOnHandlerThread() {
- if (mConnectivityServiceHandler.getLooper().getThread() != Thread.currentThread()) {
- throw new IllegalStateException(
- "Not running on handler thread: " + Thread.currentThread().getName());
- }
- }
-
- @VisibleForTesting
- boolean isAnyTcpSocketConnected(int netId) {
- FileDescriptor fd = null;
-
- try {
- fd = mDependencies.createConnectedNetlinkSocket();
-
- // Get network mask
- final MarkMaskParcel parcel = mNetd.getFwmarkForNetwork(netId);
- final int networkMark = (parcel != null) ? parcel.mark : NetlinkUtils.UNKNOWN_MARK;
- final int networkMask = (parcel != null) ? parcel.mask : NetlinkUtils.NULL_MASK;
-
- // Send request for each IP family
- for (final int family : ADDRESS_FAMILIES) {
- if (isAnyTcpSocketConnectedForFamily(fd, family, networkMark, networkMask)) {
- return true;
- }
- }
- } catch (ErrnoException | SocketException | InterruptedIOException | RemoteException e) {
- Log.e(TAG, "Fail to get socket info via netlink.", e);
- } finally {
- SocketUtils.closeSocketQuietly(fd);
- }
-
- return false;
- }
-
- private boolean isAnyTcpSocketConnectedForFamily(FileDescriptor fd, int family, int networkMark,
- int networkMask) throws ErrnoException, InterruptedIOException {
- ensureRunningOnHandlerThread();
- // Build SocketDiag messages and cache it.
- if (mSockDiagMsg.get(family) == null) {
- mSockDiagMsg.put(family, InetDiagMessage.buildInetDiagReqForAliveTcpSockets(family));
- }
- mDependencies.sendRequest(fd, mSockDiagMsg.get(family));
-
- // Iteration limitation as a protection to avoid possible infinite loops.
- // DEFAULT_RECV_BUFSIZE could read more than 20 sockets per time. Max iteration
- // should be enough to go through reasonable TCP sockets in the device.
- final int maxIteration = 100;
- int parsingIteration = 0;
- while (parsingIteration < maxIteration) {
- final ByteBuffer bytes = mDependencies.recvSockDiagResponse(fd);
-
- try {
- while (NetlinkUtils.enoughBytesRemainForValidNlMsg(bytes)) {
- final int startPos = bytes.position();
-
- final int nlmsgLen = bytes.getInt();
- final int nlmsgType = bytes.getShort();
- if (isEndOfMessageOrError(nlmsgType)) return false;
- // TODO: Parse InetDiagMessage to get uid and dst address information to filter
- // socket via NetlinkMessage.parse.
-
- // Skip the header to move to data part.
- bytes.position(startPos + SOCKDIAG_MSG_HEADER_SIZE);
-
- if (isTargetTcpSocket(bytes, nlmsgLen, networkMark, networkMask)) {
- return true;
- }
- }
- } catch (BufferUnderflowException e) {
- // The exception happens in random place in either header position or any data
- // position. Partial bytes from the middle of the byte buffer may not be enough to
- // clarify, so print out the content before the error to possibly prevent printing
- // the whole 8K buffer.
- final int exceptionPos = bytes.position();
- final String hex = HexDump.dumpHexString(bytes.array(), 0, exceptionPos);
- Log.e(TAG, "Unexpected socket info parsing: " + hex, e);
- }
-
- parsingIteration++;
- }
- return false;
- }
-
- private boolean isEndOfMessageOrError(int nlmsgType) {
- return nlmsgType == NLMSG_DONE || nlmsgType != SOCK_DIAG_BY_FAMILY;
- }
-
- private boolean isTargetTcpSocket(@NonNull ByteBuffer bytes, int nlmsgLen, int networkMark,
- int networkMask) {
- final int mark = readSocketDataAndReturnMark(bytes, nlmsgLen);
- return (mark & networkMask) == networkMark;
- }
-
- private int readSocketDataAndReturnMark(@NonNull ByteBuffer bytes, int nlmsgLen) {
- final int nextMsgOffset = bytes.position() + nlmsgLen - SOCKDIAG_MSG_HEADER_SIZE;
- int mark = NetlinkUtils.INIT_MARK_VALUE;
- // Get socket mark
- // TODO: Add a parsing method in NetlinkMessage.parse to support this to skip the remaining
- // data.
- while (bytes.position() < nextMsgOffset) {
- final StructNlAttr nlattr = StructNlAttr.parse(bytes);
- if (nlattr != null && nlattr.nla_type == NetlinkUtils.INET_DIAG_MARK) {
- mark = nlattr.getValueAsInteger();
- }
- }
- return mark;
- }
}
diff --git a/tests/cts/hostside/Android.bp b/tests/cts/hostside/Android.bp
index 47ea53e..5474d08 100644
--- a/tests/cts/hostside/Android.bp
+++ b/tests/cts/hostside/Android.bp
@@ -37,7 +37,6 @@
data: [
":CtsHostsideNetworkTestsApp",
":CtsHostsideNetworkTestsApp2",
- ":CtsHostsideNetworkTestsAppNext",
],
per_testcase_directory: true,
}
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 61b597a..f596b79 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -96,6 +96,7 @@
import static com.android.net.module.util.NetworkStackConstants.TEST_CAPTIVE_PORTAL_HTTP_URL;
import static com.android.networkstack.apishim.ConstantsShim.BLOCKED_REASON_LOCKDOWN_VPN;
import static com.android.networkstack.apishim.ConstantsShim.BLOCKED_REASON_NONE;
+import static com.android.networkstack.apishim.ConstantsShim.RECEIVER_EXPORTED;
import static com.android.testutils.Cleanup.testAndCleanup;
import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
import static com.android.testutils.MiscAsserts.assertThrows;
@@ -1131,7 +1132,8 @@
final ConnectivityActionReceiver receiver = new ConnectivityActionReceiver(
mCm, ConnectivityManager.TYPE_WIFI, NetworkInfo.State.CONNECTED);
- mContext.registerReceiver(receiver, filter);
+ final int flags = SdkLevel.isAtLeastT() ? RECEIVER_EXPORTED : 0;
+ mContext.registerReceiver(receiver, filter, flags);
// Create a broadcast PendingIntent for NETWORK_CALLBACK_ACTION.
final Intent intent = new Intent(NETWORK_CALLBACK_ACTION)
@@ -1225,7 +1227,8 @@
networkFuture.complete(intent.getParcelableExtra(EXTRA_NETWORK));
}
};
- mContext.registerReceiver(receiver, filter);
+ final int flags = SdkLevel.isAtLeastT() ? RECEIVER_EXPORTED : 0;
+ mContext.registerReceiver(receiver, filter, flags);
final Network wifiNetwork = mCtsNetUtils.ensureWifiConnected();
try {
diff --git a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
index 2b5c305..b7eb009 100644
--- a/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
+++ b/tests/cts/net/src/android/net/cts/NsdManagerTest.kt
@@ -41,7 +41,9 @@
import android.net.cts.NsdManagerTest.NsdRegistrationRecord.RegistrationEvent.ServiceUnregistered
import android.net.cts.NsdManagerTest.NsdRegistrationRecord.RegistrationEvent.UnregistrationFailed
import android.net.cts.NsdManagerTest.NsdResolveRecord.ResolveEvent.ResolveFailed
+import android.net.cts.NsdManagerTest.NsdResolveRecord.ResolveEvent.ResolveStopped
import android.net.cts.NsdManagerTest.NsdResolveRecord.ResolveEvent.ServiceResolved
+import android.net.cts.NsdManagerTest.NsdResolveRecord.ResolveEvent.StopResolutionFailed
import android.net.nsd.NsdManager
import android.net.nsd.NsdManager.DiscoveryListener
import android.net.nsd.NsdManager.RegistrationListener
@@ -66,15 +68,6 @@
import com.android.testutils.runAsShell
import com.android.testutils.tryTest
import com.android.testutils.waitForIdle
-import org.junit.After
-import org.junit.Assert.assertArrayEquals
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertTrue
-import org.junit.Assume.assumeTrue
-import org.junit.Before
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
import java.io.File
import java.net.ServerSocket
import java.nio.charset.StandardCharsets
@@ -86,6 +79,15 @@
import kotlin.test.assertNull
import kotlin.test.assertTrue
import kotlin.test.fail
+import org.junit.After
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
private const val TAG = "NsdManagerTest"
private const val TIMEOUT_MS = 2000L
@@ -182,10 +184,10 @@
val errorCode: Int
) : RegistrationEvent()
- data class ServiceRegistered(override val serviceInfo: NsdServiceInfo)
- : RegistrationEvent()
- data class ServiceUnregistered(override val serviceInfo: NsdServiceInfo)
- : RegistrationEvent()
+ data class ServiceRegistered(override val serviceInfo: NsdServiceInfo) :
+ RegistrationEvent()
+ data class ServiceUnregistered(override val serviceInfo: NsdServiceInfo) :
+ RegistrationEvent()
}
override fun onRegistrationFailed(si: NsdServiceInfo, err: Int) {
@@ -208,11 +210,11 @@
private class NsdDiscoveryRecord(expectedThreadId: Int? = null) :
DiscoveryListener, NsdRecord<NsdDiscoveryRecord.DiscoveryEvent>(expectedThreadId) {
sealed class DiscoveryEvent : NsdEvent {
- data class StartDiscoveryFailed(val serviceType: String, val errorCode: Int)
- : DiscoveryEvent()
+ data class StartDiscoveryFailed(val serviceType: String, val errorCode: Int) :
+ DiscoveryEvent()
- data class StopDiscoveryFailed(val serviceType: String, val errorCode: Int)
- : DiscoveryEvent()
+ data class StopDiscoveryFailed(val serviceType: String, val errorCode: Int) :
+ DiscoveryEvent()
data class DiscoveryStarted(val serviceType: String) : DiscoveryEvent()
data class DiscoveryStopped(val serviceType: String) : DiscoveryEvent()
@@ -259,10 +261,13 @@
private class NsdResolveRecord : ResolveListener,
NsdRecord<NsdResolveRecord.ResolveEvent>() {
sealed class ResolveEvent : NsdEvent {
- data class ResolveFailed(val serviceInfo: NsdServiceInfo, val errorCode: Int)
- : ResolveEvent()
+ data class ResolveFailed(val serviceInfo: NsdServiceInfo, val errorCode: Int) :
+ ResolveEvent()
data class ServiceResolved(val serviceInfo: NsdServiceInfo) : ResolveEvent()
+ data class ResolveStopped(val serviceInfo: NsdServiceInfo) : ResolveEvent()
+ data class StopResolutionFailed(val serviceInfo: NsdServiceInfo, val errorCode: Int) :
+ ResolveEvent()
}
override fun onResolveFailed(si: NsdServiceInfo, err: Int) {
@@ -272,6 +277,14 @@
override fun onServiceResolved(si: NsdServiceInfo) {
add(ServiceResolved(si))
}
+
+ override fun onResolveStopped(si: NsdServiceInfo) {
+ add(ResolveStopped(si))
+ }
+
+ override fun onStopResolutionFailed(si: NsdServiceInfo, err: Int) {
+ add(StopResolutionFailed(si, err))
+ }
}
@Before
@@ -739,6 +752,26 @@
NsdManager.RUN_NATIVE_NSD_ONLY_IF_LEGACY_APPS_T_AND_LATER))
}
+ @Test
+ fun testStopServiceResolution() {
+ // This test requires shims supporting U+ APIs (NsdManager.stopServiceResolution)
+ assumeTrue(TestUtils.shouldTestUApis())
+
+ val si = NsdServiceInfo()
+ si.serviceType = this@NsdManagerTest.serviceType
+ si.serviceName = this@NsdManagerTest.serviceName
+ si.port = 12345 // Test won't try to connect so port does not matter
+
+ val resolveRecord = NsdResolveRecord()
+ // Try to resolve an unknown service then stop it immediately.
+ // Expected ResolveStopped callback.
+ nsdShim.resolveService(nsdManager, si, { it.run() }, resolveRecord)
+ nsdShim.stopServiceResolution(nsdManager, resolveRecord)
+ val stoppedCb = resolveRecord.expectCallback<ResolveStopped>()
+ assertEquals(si.serviceName, stoppedCb.serviceInfo.serviceName)
+ assertEquals(si.serviceType, stoppedCb.serviceInfo.serviceType)
+ }
+
/**
* Register a service and return its registration record.
*/
diff --git a/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java b/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java
index 64355ed..9ce0693 100644
--- a/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java
+++ b/tests/unit/java/android/net/nsd/NsdServiceInfoTest.java
@@ -21,6 +21,7 @@
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
+import android.net.InetAddresses;
import android.net.Network;
import android.os.Build;
import android.os.Bundle;
@@ -38,6 +39,7 @@
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
+import java.util.List;
import java.util.Map;
@RunWith(DevSdkIgnoreRunner.class)
@@ -45,6 +47,8 @@
@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.S_V2)
public class NsdServiceInfoTest {
+ private static final InetAddress IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1");
+ private static final InetAddress IPV6_ADDRESS = InetAddresses.parseNumericAddress("2001:db8::");
public final static InetAddress LOCALHOST;
static {
// Because test.
@@ -124,6 +128,7 @@
fullInfo.setServiceType("_kitten._tcp");
fullInfo.setPort(4242);
fullInfo.setHost(LOCALHOST);
+ fullInfo.setHostAddresses(List.of(IPV4_ADDRESS));
fullInfo.setNetwork(new Network(123));
fullInfo.setInterfaceIndex(456);
checkParcelable(fullInfo);
@@ -139,6 +144,7 @@
attributedInfo.setServiceType("_kitten._tcp");
attributedInfo.setPort(4242);
attributedInfo.setHost(LOCALHOST);
+ fullInfo.setHostAddresses(List.of(IPV6_ADDRESS, IPV4_ADDRESS));
attributedInfo.setAttribute("color", "pink");
attributedInfo.setAttribute("sound", (new String("にゃあ")).getBytes("UTF-8"));
attributedInfo.setAttribute("adorable", (String) null);
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index 17e769c..c9783ba 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -15760,6 +15760,39 @@
}
@Test
+ public void testProfileNetworkPreferenceBlocking_addUser() throws Exception {
+ final InOrder inOrder = inOrder(mMockNetd);
+ doReturn(asList(PRIMARY_USER_HANDLE)).when(mUserManager).getUserHandles(anyBoolean());
+
+ // Only one network
+ mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
+ mCellAgent.connect(true);
+
+ // Verify uid ranges 0~99999 are allowed
+ final ArraySet<UidRange> allowedRanges = new ArraySet<>();
+ allowedRanges.add(PRIMARY_UIDRANGE);
+ final NativeUidRangeConfig config1User = new NativeUidRangeConfig(
+ mCellAgent.getNetwork().netId,
+ toUidRangeStableParcels(allowedRanges),
+ 0 /* subPriority */);
+ inOrder.verify(mMockNetd).setNetworkAllowlist(new NativeUidRangeConfig[] { config1User });
+
+ doReturn(asList(PRIMARY_USER_HANDLE, SECONDARY_USER_HANDLE))
+ .when(mUserManager).getUserHandles(anyBoolean());
+ final Intent addedIntent = new Intent(ACTION_USER_ADDED);
+ addedIntent.putExtra(Intent.EXTRA_USER, UserHandle.of(SECONDARY_USER));
+ processBroadcast(addedIntent);
+
+ // Make sure the allow list has been updated.
+ allowedRanges.add(UidRange.createForUser(SECONDARY_USER_HANDLE));
+ final NativeUidRangeConfig config2Users = new NativeUidRangeConfig(
+ mCellAgent.getNetwork().netId,
+ toUidRangeStableParcels(allowedRanges),
+ 0 /* subPriority */);
+ inOrder.verify(mMockNetd).setNetworkAllowlist(new NativeUidRangeConfig[] { config2Users });
+ }
+
+ @Test
public void testProfileNetworkPreferenceBlocking_changePreference() throws Exception {
final InOrder inOrder = inOrder(mMockNetd);
final UserHandle testHandle = setupEnterpriseNetwork();
diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java
index 1bd49a5..98a8ed2 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -16,7 +16,10 @@
package com.android.server;
+import static android.net.InetAddresses.parseNumericAddress;
+import static android.net.nsd.NsdManager.FAILURE_BAD_PARAMETERS;
import static android.net.nsd.NsdManager.FAILURE_INTERNAL_ERROR;
+import static android.net.nsd.NsdManager.FAILURE_OPERATION_NOT_RUNNING;
import static com.android.testutils.ContextUtils.mockService;
@@ -27,6 +30,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
@@ -47,7 +51,6 @@
import android.content.ContentResolver;
import android.content.Context;
import android.net.INetd;
-import android.net.InetAddresses;
import android.net.Network;
import android.net.mdns.aidl.DiscoveryInfo;
import android.net.mdns.aidl.GetAddressInfo;
@@ -69,11 +72,13 @@
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
+import android.os.RemoteException;
import androidx.annotation.NonNull;
import androidx.test.filters.SmallTest;
import com.android.server.NsdService.Dependencies;
+import com.android.server.connectivity.mdns.MdnsAdvertiser;
import com.android.server.connectivity.mdns.MdnsDiscoveryManager;
import com.android.server.connectivity.mdns.MdnsServiceBrowserListener;
import com.android.server.connectivity.mdns.MdnsServiceInfo;
@@ -96,6 +101,7 @@
import java.net.UnknownHostException;
import java.util.LinkedList;
import java.util.List;
+import java.util.Objects;
import java.util.Queue;
// TODOs:
@@ -129,6 +135,7 @@
@Mock MDnsManager mMockMDnsM;
@Mock Dependencies mDeps;
@Mock MdnsDiscoveryManager mDiscoveryManager;
+ @Mock MdnsAdvertiser mAdvertiser;
@Mock MdnsSocketProvider mSocketProvider;
HandlerThread mThread;
TestHandler mHandler;
@@ -399,13 +406,42 @@
final NsdServiceInfo resolvedService = resInfoCaptor.getValue();
assertEquals(SERVICE_NAME, resolvedService.getServiceName());
assertEquals("." + SERVICE_TYPE, resolvedService.getServiceType());
- assertEquals(InetAddresses.parseNumericAddress(serviceAddress), resolvedService.getHost());
+ assertEquals(parseNumericAddress(serviceAddress), resolvedService.getHost());
assertEquals(servicePort, resolvedService.getPort());
assertNull(resolvedService.getNetwork());
assertEquals(interfaceIdx, resolvedService.getInterfaceIndex());
}
@Test
+ public void testDiscoverOnBlackholeNetwork() throws Exception {
+ final NsdManager client = connectClient(mService);
+ final DiscoveryListener discListener = mock(DiscoveryListener.class);
+ client.discoverServices(SERVICE_TYPE, PROTOCOL, discListener);
+ waitForIdle();
+
+ final IMDnsEventListener eventListener = getEventListener();
+ final ArgumentCaptor<Integer> discIdCaptor = ArgumentCaptor.forClass(Integer.class);
+ verify(mMockMDnsM).discover(discIdCaptor.capture(), eq(SERVICE_TYPE),
+ eq(0) /* interfaceIdx */);
+ // NsdManager uses a separate HandlerThread to dispatch callbacks (on ServiceHandler), so
+ // this needs to use a timeout
+ verify(discListener, timeout(TIMEOUT_MS)).onDiscoveryStarted(SERVICE_TYPE);
+
+ final DiscoveryInfo discoveryInfo = new DiscoveryInfo(
+ discIdCaptor.getValue(),
+ IMDnsEventListener.SERVICE_FOUND,
+ SERVICE_NAME,
+ SERVICE_TYPE,
+ DOMAIN_NAME,
+ 123 /* interfaceIdx */,
+ INetd.DUMMY_NET_ID); // netId of the blackhole network
+ eventListener.onServiceDiscoveryStatus(discoveryInfo);
+ waitForIdle();
+
+ verify(discListener, never()).onServiceFound(any());
+ }
+
+ @Test
public void testServiceRegistrationSuccessfulAndFailed() throws Exception {
final NsdManager client = connectClient(mService);
final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
@@ -572,6 +608,222 @@
anyInt()/* interfaceIdx */);
}
+ @Test
+ public void testStopServiceResolution() {
+ final NsdManager client = connectClient(mService);
+ final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
+ final ResolveListener resolveListener = mock(ResolveListener.class);
+ client.resolveService(request, resolveListener);
+ waitForIdle();
+
+ final ArgumentCaptor<Integer> resolvIdCaptor = ArgumentCaptor.forClass(Integer.class);
+ verify(mMockMDnsM).resolve(resolvIdCaptor.capture(), eq(SERVICE_NAME), eq(SERVICE_TYPE),
+ eq("local.") /* domain */, eq(IFACE_IDX_ANY));
+
+ final int resolveId = resolvIdCaptor.getValue();
+ client.stopServiceResolution(resolveListener);
+ waitForIdle();
+
+ verify(mMockMDnsM).stopOperation(resolveId);
+ verify(resolveListener, timeout(TIMEOUT_MS)).onResolveStopped(argThat(ns ->
+ request.getServiceName().equals(ns.getServiceName())
+ && request.getServiceType().equals(ns.getServiceType())));
+ }
+
+ @Test
+ public void testStopResolutionFailed() {
+ final NsdManager client = connectClient(mService);
+ final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
+ final ResolveListener resolveListener = mock(ResolveListener.class);
+ client.resolveService(request, resolveListener);
+ waitForIdle();
+
+ final ArgumentCaptor<Integer> resolvIdCaptor = ArgumentCaptor.forClass(Integer.class);
+ verify(mMockMDnsM).resolve(resolvIdCaptor.capture(), eq(SERVICE_NAME), eq(SERVICE_TYPE),
+ eq("local.") /* domain */, eq(IFACE_IDX_ANY));
+
+ final int resolveId = resolvIdCaptor.getValue();
+ doReturn(false).when(mMockMDnsM).stopOperation(anyInt());
+ client.stopServiceResolution(resolveListener);
+ waitForIdle();
+
+ verify(mMockMDnsM).stopOperation(resolveId);
+ verify(resolveListener, timeout(TIMEOUT_MS)).onStopResolutionFailed(argThat(ns ->
+ request.getServiceName().equals(ns.getServiceName())
+ && request.getServiceType().equals(ns.getServiceType())),
+ eq(FAILURE_OPERATION_NOT_RUNNING));
+ }
+
+ @Test @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+ public void testStopResolutionDuringGettingAddress() throws RemoteException {
+ final NsdManager client = connectClient(mService);
+ final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
+ final ResolveListener resolveListener = mock(ResolveListener.class);
+ client.resolveService(request, resolveListener);
+ waitForIdle();
+
+ final IMDnsEventListener eventListener = getEventListener();
+ final ArgumentCaptor<Integer> resolvIdCaptor = ArgumentCaptor.forClass(Integer.class);
+ verify(mMockMDnsM).resolve(resolvIdCaptor.capture(), eq(SERVICE_NAME), eq(SERVICE_TYPE),
+ eq("local.") /* domain */, eq(IFACE_IDX_ANY));
+
+ // Resolve service successfully.
+ final ResolutionInfo resolutionInfo = new ResolutionInfo(
+ resolvIdCaptor.getValue(),
+ IMDnsEventListener.SERVICE_RESOLVED,
+ null /* serviceName */,
+ null /* serviceType */,
+ null /* domain */,
+ SERVICE_FULL_NAME,
+ DOMAIN_NAME,
+ PORT,
+ new byte[0] /* txtRecord */,
+ IFACE_IDX_ANY);
+ doReturn(true).when(mMockMDnsM).getServiceAddress(anyInt(), any(), anyInt());
+ eventListener.onServiceResolutionStatus(resolutionInfo);
+ waitForIdle();
+
+ final ArgumentCaptor<Integer> getAddrIdCaptor = ArgumentCaptor.forClass(Integer.class);
+ verify(mMockMDnsM).getServiceAddress(getAddrIdCaptor.capture(), eq(DOMAIN_NAME),
+ eq(IFACE_IDX_ANY));
+
+ final int getAddrId = getAddrIdCaptor.getValue();
+ client.stopServiceResolution(resolveListener);
+ waitForIdle();
+
+ verify(mMockMDnsM).stopOperation(getAddrId);
+ verify(resolveListener, timeout(TIMEOUT_MS)).onResolveStopped(argThat(ns ->
+ request.getServiceName().equals(ns.getServiceName())
+ && request.getServiceType().equals(ns.getServiceType())));
+ }
+
+ private void verifyUpdatedServiceInfo(NsdServiceInfo info, String serviceName,
+ String serviceType, String address, int port, int interfaceIndex, Network network) {
+ assertEquals(serviceName, info.getServiceName());
+ assertEquals(serviceType, info.getServiceType());
+ assertTrue(info.getHostAddresses().contains(parseNumericAddress(address)));
+ assertEquals(port, info.getPort());
+ assertEquals(network, info.getNetwork());
+ assertEquals(interfaceIndex, info.getInterfaceIndex());
+ }
+
+ @Test
+ public void testRegisterAndUnregisterServiceInfoCallback() throws RemoteException {
+ final NsdManager client = connectClient(mService);
+ final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
+ final NsdManager.ServiceInfoCallback serviceInfoCallback = mock(
+ NsdManager.ServiceInfoCallback.class);
+ client.registerServiceInfoCallback(request, Runnable::run, serviceInfoCallback);
+ waitForIdle();
+
+ final IMDnsEventListener eventListener = getEventListener();
+ final ArgumentCaptor<Integer> resolvIdCaptor = ArgumentCaptor.forClass(Integer.class);
+ verify(mMockMDnsM).resolve(resolvIdCaptor.capture(), eq(SERVICE_NAME), eq(SERVICE_TYPE),
+ eq("local.") /* domain */, eq(IFACE_IDX_ANY));
+
+ // Resolve service successfully.
+ final ResolutionInfo resolutionInfo = new ResolutionInfo(
+ resolvIdCaptor.getValue(),
+ IMDnsEventListener.SERVICE_RESOLVED,
+ null /* serviceName */,
+ null /* serviceType */,
+ null /* domain */,
+ SERVICE_FULL_NAME,
+ DOMAIN_NAME,
+ PORT,
+ new byte[0] /* txtRecord */,
+ IFACE_IDX_ANY);
+ doReturn(true).when(mMockMDnsM).getServiceAddress(anyInt(), any(), anyInt());
+ eventListener.onServiceResolutionStatus(resolutionInfo);
+ waitForIdle();
+
+ final ArgumentCaptor<Integer> getAddrIdCaptor = ArgumentCaptor.forClass(Integer.class);
+ verify(mMockMDnsM).getServiceAddress(getAddrIdCaptor.capture(), eq(DOMAIN_NAME),
+ eq(IFACE_IDX_ANY));
+
+ // First address info
+ final String v4Address = "192.0.2.1";
+ final String v6Address = "2001:db8::";
+ final GetAddressInfo addressInfo1 = new GetAddressInfo(
+ getAddrIdCaptor.getValue(),
+ IMDnsEventListener.SERVICE_GET_ADDR_SUCCESS,
+ SERVICE_FULL_NAME,
+ v4Address,
+ IFACE_IDX_ANY,
+ 999 /* netId */);
+ eventListener.onGettingServiceAddressStatus(addressInfo1);
+ waitForIdle();
+
+ final ArgumentCaptor<NsdServiceInfo> updateInfoCaptor =
+ ArgumentCaptor.forClass(NsdServiceInfo.class);
+ verify(serviceInfoCallback, timeout(TIMEOUT_MS).times(1))
+ .onServiceUpdated(updateInfoCaptor.capture());
+ verifyUpdatedServiceInfo(updateInfoCaptor.getAllValues().get(0) /* info */, SERVICE_NAME,
+ "." + SERVICE_TYPE, v4Address, PORT, IFACE_IDX_ANY, new Network(999));
+
+ // Second address info
+ final GetAddressInfo addressInfo2 = new GetAddressInfo(
+ getAddrIdCaptor.getValue(),
+ IMDnsEventListener.SERVICE_GET_ADDR_SUCCESS,
+ SERVICE_FULL_NAME,
+ v6Address,
+ IFACE_IDX_ANY,
+ 999 /* netId */);
+ eventListener.onGettingServiceAddressStatus(addressInfo2);
+ waitForIdle();
+
+ verify(serviceInfoCallback, timeout(TIMEOUT_MS).times(2))
+ .onServiceUpdated(updateInfoCaptor.capture());
+ verifyUpdatedServiceInfo(updateInfoCaptor.getAllValues().get(1) /* info */, SERVICE_NAME,
+ "." + SERVICE_TYPE, v6Address, PORT, IFACE_IDX_ANY, new Network(999));
+
+ client.unregisterServiceInfoCallback(serviceInfoCallback);
+ waitForIdle();
+
+ verify(serviceInfoCallback, timeout(TIMEOUT_MS)).onServiceInfoCallbackUnregistered();
+ }
+
+ @Test
+ public void testRegisterServiceCallbackFailed() throws Exception {
+ final NsdManager client = connectClient(mService);
+ final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
+ final NsdManager.ServiceInfoCallback subscribeListener = mock(
+ NsdManager.ServiceInfoCallback.class);
+ client.registerServiceInfoCallback(request, Runnable::run, subscribeListener);
+ waitForIdle();
+
+ final IMDnsEventListener eventListener = getEventListener();
+ final ArgumentCaptor<Integer> resolvIdCaptor = ArgumentCaptor.forClass(Integer.class);
+ verify(mMockMDnsM).resolve(resolvIdCaptor.capture(), eq(SERVICE_NAME), eq(SERVICE_TYPE),
+ eq("local.") /* domain */, eq(IFACE_IDX_ANY));
+
+ // Fail to resolve service.
+ final ResolutionInfo resolutionFailedInfo = new ResolutionInfo(
+ resolvIdCaptor.getValue(),
+ IMDnsEventListener.SERVICE_RESOLUTION_FAILED,
+ null /* serviceName */,
+ null /* serviceType */,
+ null /* domain */,
+ null /* serviceFullName */,
+ null /* domainName */,
+ 0 /* port */,
+ new byte[0] /* txtRecord */,
+ IFACE_IDX_ANY);
+ eventListener.onServiceResolutionStatus(resolutionFailedInfo);
+ verify(subscribeListener, timeout(TIMEOUT_MS))
+ .onServiceInfoCallbackRegistrationFailed(eq(FAILURE_BAD_PARAMETERS));
+ }
+
+ @Test
+ public void testUnregisterNotRegisteredCallback() {
+ final NsdManager client = connectClient(mService);
+ final NsdManager.ServiceInfoCallback serviceInfoCallback = mock(
+ NsdManager.ServiceInfoCallback.class);
+
+ assertThrows(IllegalArgumentException.class, () ->
+ client.unregisterServiceInfoCallback(serviceInfoCallback));
+ }
+
private void makeServiceWithMdnsDiscoveryManagerEnabled() {
doReturn(true).when(mDeps).isMdnsDiscoveryManagerEnabled(any(Context.class));
doReturn(mDiscoveryManager).when(mDeps).makeMdnsDiscoveryManager(any(), any());
@@ -582,6 +834,16 @@
verify(mDeps).makeMdnsSocketProvider(any(), any());
}
+ private void makeServiceWithMdnsAdvertiserEnabled() {
+ doReturn(true).when(mDeps).isMdnsAdvertiserEnabled(any(Context.class));
+ doReturn(mAdvertiser).when(mDeps).makeMdnsAdvertiser(any(), any(), any());
+ doReturn(mSocketProvider).when(mDeps).makeMdnsSocketProvider(any(), any());
+
+ mService = makeService();
+ verify(mDeps).makeMdnsAdvertiser(any(), any(), any());
+ verify(mDeps).makeMdnsSocketProvider(any(), any());
+ }
+
@Test
public void testMdnsDiscoveryManagerFeature() {
// Create NsdService w/o feature enabled.
@@ -733,7 +995,7 @@
assertTrue(info.getAttributes().containsKey("key"));
assertEquals(1, info.getAttributes().size());
assertArrayEquals(new byte[]{(byte) 0xFF, (byte) 0xFE}, info.getAttributes().get("key"));
- assertEquals(InetAddresses.parseNumericAddress(IPV4_ADDRESS), info.getHost());
+ assertEquals(parseNumericAddress(IPV4_ADDRESS), info.getHost());
assertEquals(network, info.getNetwork());
// Verify the listener has been unregistered.
@@ -742,6 +1004,102 @@
verify(mSocketProvider, timeout(CLEANUP_DELAY_MS + TIMEOUT_MS)).stopMonitoringSockets();
}
+ @Test
+ public void testAdvertiseWithMdnsAdvertiser() {
+ makeServiceWithMdnsAdvertiserEnabled();
+
+ final NsdManager client = connectClient(mService);
+ final RegistrationListener regListener = mock(RegistrationListener.class);
+ // final String serviceTypeWithLocalDomain = SERVICE_TYPE + ".local";
+ final ArgumentCaptor<MdnsAdvertiser.AdvertiserCallback> cbCaptor =
+ ArgumentCaptor.forClass(MdnsAdvertiser.AdvertiserCallback.class);
+ verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture());
+
+ final NsdServiceInfo regInfo = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE);
+ regInfo.setHost(parseNumericAddress("192.0.2.123"));
+ regInfo.setPort(12345);
+ regInfo.setAttribute("testattr", "testvalue");
+ regInfo.setNetwork(new Network(999));
+
+ client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
+ waitForIdle();
+ verify(mSocketProvider).startMonitoringSockets();
+ final ArgumentCaptor<Integer> idCaptor = ArgumentCaptor.forClass(Integer.class);
+ verify(mAdvertiser).addService(idCaptor.capture(), argThat(info ->
+ matches(info, regInfo)));
+
+ // Verify onServiceRegistered callback
+ final MdnsAdvertiser.AdvertiserCallback cb = cbCaptor.getValue();
+ cb.onRegisterServiceSucceeded(idCaptor.getValue(), regInfo);
+
+ verify(regListener, timeout(TIMEOUT_MS)).onServiceRegistered(argThat(info -> matches(info,
+ new NsdServiceInfo(regInfo.getServiceName(), null))));
+
+ client.unregisterService(regListener);
+ waitForIdle();
+ verify(mAdvertiser).removeService(idCaptor.getValue());
+ verify(regListener, timeout(TIMEOUT_MS)).onServiceUnregistered(
+ argThat(info -> matches(info, regInfo)));
+ verify(mSocketProvider, timeout(TIMEOUT_MS)).stopMonitoringSockets();
+ }
+
+ @Test
+ public void testAdvertiseWithMdnsAdvertiser_FailedWithInvalidServiceType() {
+ makeServiceWithMdnsAdvertiserEnabled();
+
+ final NsdManager client = connectClient(mService);
+ final RegistrationListener regListener = mock(RegistrationListener.class);
+ // final String serviceTypeWithLocalDomain = SERVICE_TYPE + ".local";
+ final ArgumentCaptor<MdnsAdvertiser.AdvertiserCallback> cbCaptor =
+ ArgumentCaptor.forClass(MdnsAdvertiser.AdvertiserCallback.class);
+ verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture());
+
+ final NsdServiceInfo regInfo = new NsdServiceInfo(SERVICE_NAME, "invalid_type");
+ regInfo.setHost(parseNumericAddress("192.0.2.123"));
+ regInfo.setPort(12345);
+ regInfo.setAttribute("testattr", "testvalue");
+ regInfo.setNetwork(new Network(999));
+
+ client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
+ waitForIdle();
+ verify(mAdvertiser, never()).addService(anyInt(), any());
+
+ verify(regListener, timeout(TIMEOUT_MS)).onRegistrationFailed(
+ argThat(info -> matches(info, regInfo)), eq(FAILURE_INTERNAL_ERROR));
+ }
+
+ @Test
+ public void testAdvertiseWithMdnsAdvertiser_LongServiceName() {
+ makeServiceWithMdnsAdvertiserEnabled();
+
+ final NsdManager client = connectClient(mService);
+ final RegistrationListener regListener = mock(RegistrationListener.class);
+ // final String serviceTypeWithLocalDomain = SERVICE_TYPE + ".local";
+ final ArgumentCaptor<MdnsAdvertiser.AdvertiserCallback> cbCaptor =
+ ArgumentCaptor.forClass(MdnsAdvertiser.AdvertiserCallback.class);
+ verify(mDeps).makeMdnsAdvertiser(any(), any(), cbCaptor.capture());
+
+ final NsdServiceInfo regInfo = new NsdServiceInfo("a".repeat(70), SERVICE_TYPE);
+ regInfo.setHost(parseNumericAddress("192.0.2.123"));
+ regInfo.setPort(12345);
+ regInfo.setAttribute("testattr", "testvalue");
+ regInfo.setNetwork(new Network(999));
+
+ client.registerService(regInfo, NsdManager.PROTOCOL_DNS_SD, Runnable::run, regListener);
+ waitForIdle();
+ final ArgumentCaptor<Integer> idCaptor = ArgumentCaptor.forClass(Integer.class);
+ // Service name is truncated to 63 characters
+ verify(mAdvertiser).addService(idCaptor.capture(),
+ argThat(info -> info.getServiceName().equals("a".repeat(63))));
+
+ // Verify onServiceRegistered callback
+ final MdnsAdvertiser.AdvertiserCallback cb = cbCaptor.getValue();
+ cb.onRegisterServiceSucceeded(idCaptor.getValue(), regInfo);
+
+ verify(regListener, timeout(TIMEOUT_MS)).onServiceRegistered(
+ argThat(info -> matches(info, new NsdServiceInfo(regInfo.getServiceName(), null))));
+ }
+
private void waitForIdle() {
HandlerUtils.waitForIdle(mHandler, TIMEOUT_MS);
}
@@ -786,6 +1144,19 @@
verify(mMockMDnsM, timeout(cleanupDelayMs + TIMEOUT_MS)).stopDaemon();
}
+ /**
+ * Return true if two service info are the same.
+ *
+ * Useful for argument matchers as {@link NsdServiceInfo} does not implement equals.
+ */
+ private boolean matches(NsdServiceInfo a, NsdServiceInfo b) {
+ return Objects.equals(a.getServiceName(), b.getServiceName())
+ && Objects.equals(a.getServiceType(), b.getServiceType())
+ && Objects.equals(a.getHost(), b.getHost())
+ && Objects.equals(a.getNetwork(), b.getNetwork())
+ && Objects.equals(a.getAttributes(), b.getAttributes());
+ }
+
public static class TestHandler extends Handler {
public Message lastMessage;
diff --git a/tests/unit/java/com/android/server/VpnManagerServiceTest.java b/tests/unit/java/com/android/server/VpnManagerServiceTest.java
index c8a93a6..deb56ef 100644
--- a/tests/unit/java/com/android/server/VpnManagerServiceTest.java
+++ b/tests/unit/java/com/android/server/VpnManagerServiceTest.java
@@ -131,6 +131,11 @@
Vpn vpn, VpnProfile profile) {
return mLockdownVpnTracker;
}
+
+ @Override
+ public @UserIdInt int getMainUserId() {
+ return UserHandle.USER_SYSTEM;
+ }
}
@Before
diff --git a/tests/unit/java/com/android/server/connectivity/KeepaliveTrackerTest.java b/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
similarity index 86%
rename from tests/unit/java/com/android/server/connectivity/KeepaliveTrackerTest.java
rename to tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
index b55ee67..6c29d6e 100644
--- a/tests/unit/java/com/android/server/connectivity/KeepaliveTrackerTest.java
+++ b/tests/unit/java/com/android/server/connectivity/AutomaticOnOffKeepaliveTrackerTest.java
@@ -24,14 +24,12 @@
import static org.mockito.Mockito.doReturn;
import android.content.Context;
-import android.content.res.Resources;
import android.net.INetd;
import android.net.MarkMaskParcel;
import android.os.Build;
import android.os.HandlerThread;
import android.test.suitebuilder.annotation.SmallTest;
-import com.android.connectivity.resources.R;
import com.android.testutils.DevSdkIgnoreRule;
import com.android.testutils.DevSdkIgnoreRunner;
@@ -48,21 +46,19 @@
@RunWith(DevSdkIgnoreRunner.class)
@SmallTest
-@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
-public class KeepaliveTrackerTest {
- private static final int[] TEST_SUPPORTED_KEEPALIVES = {1, 3, 0, 0, 0, 0, 0, 0, 0};
+@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU)
+public class AutomaticOnOffKeepaliveTrackerTest {
private static final int TEST_NETID = 0xA85;
private static final int TEST_NETID_FWMARK = 0x0A85;
private static final int OTHER_NETID = 0x1A85;
private static final int NETID_MASK = 0xffff;
- private static final int SUPPORTED_SLOT_COUNT = 2;
- private KeepaliveTracker mKeepaliveTracker;
+ private AutomaticOnOffKeepaliveTracker mAOOKeepaliveTracker;
private HandlerThread mHandlerThread;
@Mock INetd mNetd;
- @Mock KeepaliveTracker.Dependencies mDependencies;
+ @Mock AutomaticOnOffKeepaliveTracker.Dependencies mDependencies;
@Mock Context mCtx;
- @Mock Resources mResources;
+ @Mock KeepaliveTracker mKeepaliveTracker;
// Hexadecimal representation of a SOCK_DIAG response with tcp info.
private static final String SOCK_DIAG_TCP_INET_HEX =
@@ -169,51 +165,43 @@
doReturn(makeMarkMaskParcel(NETID_MASK, TEST_NETID_FWMARK)).when(mNetd)
.getFwmarkForNetwork(TEST_NETID);
- doReturn(TEST_SUPPORTED_KEEPALIVES).when(mDependencies).getSupportedKeepalives();
- doReturn(mResources).when(mDependencies).newConnectivityResources();
- mockResource();
doNothing().when(mDependencies).sendRequest(any(), any());
mHandlerThread = new HandlerThread("KeepaliveTrackerTest");
mHandlerThread.start();
-
- mKeepaliveTracker = new KeepaliveTracker(mCtx, mHandlerThread.getThreadHandler(),
- mDependencies);
- }
-
- private void mockResource() {
- doReturn(SUPPORTED_SLOT_COUNT).when(mResources).getInteger(
- R.integer.config_reservedPrivilegedKeepaliveSlots);
- doReturn(SUPPORTED_SLOT_COUNT).when(mResources).getInteger(
- R.integer.config_allowedUnprivilegedKeepalivePerUid);
+ doReturn(mKeepaliveTracker).when(mDependencies).newKeepaliveTracker(
+ mCtx, mHandlerThread.getThreadHandler());
+ doReturn(true).when(mDependencies).isFeatureEnabled(any());
+ mAOOKeepaliveTracker = new AutomaticOnOffKeepaliveTracker(
+ mCtx, mHandlerThread.getThreadHandler(), mDependencies);
}
@Test
public void testIsAnyTcpSocketConnected_runOnNonHandlerThread() throws Exception {
setupResponseWithSocketExisting();
assertThrows(IllegalStateException.class,
- () -> mKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID));
+ () -> mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID));
}
@Test
public void testIsAnyTcpSocketConnected_withTargetNetId() throws Exception {
setupResponseWithSocketExisting();
mHandlerThread.getThreadHandler().post(
- () -> assertTrue(mKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID)));
+ () -> assertTrue(mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID)));
}
@Test
public void testIsAnyTcpSocketConnected_withIncorrectNetId() throws Exception {
setupResponseWithSocketExisting();
mHandlerThread.getThreadHandler().post(
- () -> assertFalse(mKeepaliveTracker.isAnyTcpSocketConnected(OTHER_NETID)));
+ () -> assertFalse(mAOOKeepaliveTracker.isAnyTcpSocketConnected(OTHER_NETID)));
}
@Test
public void testIsAnyTcpSocketConnected_noSocketExists() throws Exception {
setupResponseWithoutSocketExisting();
mHandlerThread.getThreadHandler().post(
- () -> assertFalse(mKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID)));
+ () -> assertFalse(mAOOKeepaliveTracker.isAnyTcpSocketConnected(TEST_NETID)));
}
private void setupResponseWithSocketExisting() throws Exception {
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
index 02b3976..4a806b1 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsInterfaceAdvertiserTest.kt
@@ -203,6 +203,59 @@
verify(replySender).queueReply(mockReply)
}
+ @Test
+ fun testConflict() {
+ addServiceAndFinishProbing(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+ doReturn(setOf(TEST_SERVICE_ID_1)).`when`(repository).getConflictingServices(any())
+
+ // Reply obtained with:
+ // scapy.raw(scapy.DNS(
+ // qd = None,
+ // an = scapy.DNSRR(type='TXT', rrname='_testservice._tcp.local'))
+ // ).hex().upper()
+ val query = HexDump.hexStringToByteArray("0000010000000001000000000C5F7465737473657276696" +
+ "365045F746370056C6F63616C0000100001000000000000")
+ val src = InetSocketAddress(parseNumericAddress("2001:db8::456"), MdnsConstants.MDNS_PORT)
+ packetHandler.handlePacket(query, query.size, src)
+
+ val packetCaptor = ArgumentCaptor.forClass(MdnsPacket::class.java)
+ verify(repository).getConflictingServices(packetCaptor.capture())
+
+ packetCaptor.value.let {
+ assertEquals(0, it.questions.size)
+ assertEquals(1, it.answers.size)
+ assertEquals(0, it.authorityRecords.size)
+ assertEquals(0, it.additionalRecords.size)
+
+ assertTrue(it.answers[0] is MdnsTextRecord)
+ assertContentEquals(arrayOf("_testservice", "_tcp", "local"), it.answers[0].name)
+ }
+
+ thread.waitForIdle(TIMEOUT_MS)
+ verify(cb).onServiceConflict(advertiser, TEST_SERVICE_ID_1)
+ }
+
+ @Test
+ fun testRestartProbingForConflict() {
+ val mockProbingInfo = mock(ProbingInfo::class.java)
+ doReturn(mockProbingInfo).`when`(repository).setServiceProbing(TEST_SERVICE_ID_1)
+
+ advertiser.restartProbingForConflict(TEST_SERVICE_ID_1)
+
+ verify(prober).restartForConflict(mockProbingInfo)
+ }
+
+ @Test
+ fun testRenameServiceForConflict() {
+ val mockProbingInfo = mock(ProbingInfo::class.java)
+ doReturn(mockProbingInfo).`when`(repository).renameServiceForConflict(
+ TEST_SERVICE_ID_1, TEST_SERVICE_1)
+
+ advertiser.renameServiceForConflict(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+
+ verify(prober).restartForConflict(mockProbingInfo)
+ }
+
private fun addServiceAndFinishProbing(serviceId: Int, serviceInfo: NsdServiceInfo):
AnnouncementInfo {
val testProbingInfo = mock(ProbingInfo::class.java)
diff --git a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
index 597663c..ecc11ec 100644
--- a/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
+++ b/tests/unit/java/com/android/server/connectivity/mdns/MdnsRecordRepositoryTest.kt
@@ -24,6 +24,7 @@
import com.android.server.connectivity.mdns.MdnsAnnouncer.AnnouncementInfo
import com.android.server.connectivity.mdns.MdnsRecordRepository.Dependencies
import com.android.server.connectivity.mdns.MdnsRecordRepository.getReverseDnsAddress
+import com.android.server.connectivity.mdns.MdnsServiceInfo.TextEntry
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DevSdkIgnoreRunner
import java.net.InetSocketAddress
@@ -400,6 +401,63 @@
intArrayOf(MdnsRecord.TYPE_A, MdnsRecord.TYPE_AAAA)),
), reply.additionalAnswers)
}
+
+ @Test
+ fun testGetConflictingServices() {
+ val repository = MdnsRecordRepository(thread.looper, deps)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
+
+ val packet = MdnsPacket(
+ 0 /* flags */,
+ emptyList() /* questions */,
+ listOf(
+ MdnsServiceRecord(
+ arrayOf("MyTestService", "_testservice", "_tcp", "local"),
+ 0L /* receiptTimeMillis */, true /* cacheFlush */, 0L /* ttlMillis */,
+ 0 /* servicePriority */, 0 /* serviceWeight */,
+ TEST_SERVICE_1.port + 1,
+ TEST_HOSTNAME),
+ MdnsTextRecord(
+ arrayOf("MyOtherTestService", "_testservice", "_tcp", "local"),
+ 0L /* receiptTimeMillis */, true /* cacheFlush */, 0L /* ttlMillis */,
+ listOf(TextEntry.fromString("somedifferent=entry"))),
+ ) /* answers */,
+ emptyList() /* authorityRecords */,
+ emptyList() /* additionalRecords */)
+
+ assertEquals(setOf(TEST_SERVICE_ID_1, TEST_SERVICE_ID_2),
+ repository.getConflictingServices(packet))
+ }
+
+ @Test
+ fun testGetConflictingServices_IdenticalService() {
+ val repository = MdnsRecordRepository(thread.looper, deps)
+ repository.addService(TEST_SERVICE_ID_1, TEST_SERVICE_1)
+ repository.addService(TEST_SERVICE_ID_2, TEST_SERVICE_2)
+
+ val otherTtlMillis = 1234L
+ val packet = MdnsPacket(
+ 0 /* flags */,
+ emptyList() /* questions */,
+ listOf(
+ MdnsServiceRecord(
+ arrayOf("MyTestService", "_testservice", "_tcp", "local"),
+ 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ otherTtlMillis, 0 /* servicePriority */, 0 /* serviceWeight */,
+ TEST_SERVICE_1.port,
+ TEST_HOSTNAME),
+ MdnsTextRecord(
+ arrayOf("MyOtherTestService", "_testservice", "_tcp", "local"),
+ 0L /* receiptTimeMillis */, true /* cacheFlush */,
+ otherTtlMillis, emptyList()),
+ ) /* answers */,
+ emptyList() /* authorityRecords */,
+ emptyList() /* additionalRecords */)
+
+ // Above records are identical to the actual registrations: no conflict
+ assertEquals(emptySet(), repository.getConflictingServices(packet))
+ }
}
private fun MdnsRecordRepository.initWithService(serviceId: Int, serviceInfo: NsdServiceInfo):
diff --git a/tools/gn2bp/Android.bp.swp b/tools/gn2bp/Android.bp.swp
index ebf1a9b..c881767 100644
--- a/tools/gn2bp/Android.bp.swp
+++ b/tools/gn2bp/Android.bp.swp
@@ -14,6 +14,13 @@
//
// This file is automatically generated by gen_android_bp. Do not edit.
+// GN: PACKAGE
+package {
+ default_applicable_licenses: [
+ "external_cronet_license",
+ ],
+}
+
// GN: //components/cronet/android:cronet_api_java
java_library {
name: "cronet_aml_api_java",
@@ -22,11 +29,13 @@
],
libs: [
"androidx.annotation_annotation",
+ "framework-annotations-lib",
],
sdk_version: "module_current",
}
// GN: //components/cronet/android:cronet_api_java
+// TODO(danstahr): add the API helpers separately after the main API is checked in and thoroughly reviewed
filegroup {
name: "cronet_aml_api_sources",
srcs: [
@@ -52,18 +61,6 @@
"components/cronet/android/api/src/android/net/http/UploadDataSink.java",
"components/cronet/android/api/src/android/net/http/UrlRequest.java",
"components/cronet/android/api/src/android/net/http/UrlResponseInfo.java",
- "components/cronet/android/api/src/android/net/http/apihelpers/ByteArrayCallback.java",
- "components/cronet/android/api/src/android/net/http/apihelpers/ContentTypeParametersParser.java",
- "components/cronet/android/api/src/android/net/http/apihelpers/HttpResponse.java",
- "components/cronet/android/api/src/android/net/http/apihelpers/ImplicitFlowControlCallback.java",
- "components/cronet/android/api/src/android/net/http/apihelpers/InMemoryTransformCallback.java",
- "components/cronet/android/api/src/android/net/http/apihelpers/JsonCallback.java",
- "components/cronet/android/api/src/android/net/http/apihelpers/RedirectHandler.java",
- "components/cronet/android/api/src/android/net/http/apihelpers/RedirectHandlers.java",
- "components/cronet/android/api/src/android/net/http/apihelpers/RequestCompletionListener.java",
- "components/cronet/android/api/src/android/net/http/apihelpers/StringCallback.java",
- "components/cronet/android/api/src/android/net/http/apihelpers/UploadDataProviders.java",
- "components/cronet/android/api/src/android/net/http/apihelpers/UrlRequestCallbacks.java",
],
}
@@ -2462,8 +2459,8 @@
"third_party/protobuf/src/",
],
cpp_std: "c++17",
- linker_scripts: [
- "base/android/library_loader/anchor_functions.lds",
+ ldflags: [
+ "-Wl,--script,external/cronet/base/android/library_loader/anchor_functions.lds",
],
stem: "libcronet.108.0.5359.128",
target: {
@@ -10645,3 +10642,55 @@
],
}
+// GN: LICENSE
+license {
+ name: "external_cronet_license",
+ license_kinds: [
+ "SPDX-license-identifier-AFL-2.0",
+ "SPDX-license-identifier-Apache-2.0",
+ "SPDX-license-identifier-BSD",
+ "SPDX-license-identifier-BSL-1.0",
+ "SPDX-license-identifier-GPL",
+ "SPDX-license-identifier-GPL-2.0",
+ "SPDX-license-identifier-GPL-3.0",
+ "SPDX-license-identifier-ICU",
+ "SPDX-license-identifier-ISC",
+ "SPDX-license-identifier-LGPL",
+ "SPDX-license-identifier-LGPL-2.1",
+ "SPDX-license-identifier-MIT",
+ "SPDX-license-identifier-MPL",
+ "SPDX-license-identifier-MPL-2.0",
+ "SPDX-license-identifier-NCSA",
+ "SPDX-license-identifier-OpenSSL",
+ "SPDX-license-identifier-Unicode-DFS",
+ "legacy_unencumbered",
+ ],
+ license_text: [
+ "LICENSE",
+ "base/third_party/double_conversion/LICENSE",
+ "base/third_party/dynamic_annotations/LICENSE",
+ "base/third_party/icu/LICENSE",
+ "base/third_party/nspr/LICENSE",
+ "base/third_party/superfasthash/LICENSE",
+ "base/third_party/symbolize/LICENSE",
+ "base/third_party/valgrind/LICENSE",
+ "base/third_party/xdg_user_dirs/LICENSE",
+ "net/third_party/quiche/src/LICENSE",
+ "net/third_party/uri_template/LICENSE",
+ "third_party/abseil-cpp/LICENSE",
+ "third_party/ashmem/LICENSE",
+ "third_party/boringssl/src/LICENSE",
+ "third_party/boringssl/src/third_party/fiat/LICENSE",
+ "third_party/boringssl/src/third_party/googletest/LICENSE",
+ "third_party/boringssl/src/third_party/wycheproof_testvectors/LICENSE",
+ "third_party/brotli/LICENSE",
+ "third_party/icu/LICENSE",
+ "third_party/icu/scripts/LICENSE",
+ "third_party/libevent/LICENSE",
+ "third_party/metrics_proto/LICENSE",
+ "third_party/modp_b64/LICENSE",
+ "third_party/protobuf/LICENSE",
+ "third_party/protobuf/third_party/utf8_range/LICENSE",
+ ],
+}
+
diff --git a/tools/gn2bp/gen_android_bp b/tools/gn2bp/gen_android_bp
index e10c415..6ae3609 100755
--- a/tools/gn2bp/gen_android_bp
+++ b/tools/gn2bp/gen_android_bp
@@ -128,12 +128,17 @@
"-msse4.2",
]
+def get_linker_script_ldflag(script_path):
+ return f'-Wl,--script,{tree_path}/{script_path}'
+
# Additional arguments to apply to Android.bp rules.
additional_args = {
# TODO: remove if not needed.
'cronet_aml_components_cronet_android_cronet': [
- ('linker_scripts', {
- 'base/android/library_loader/anchor_functions.lds',
+ # linker_scripts property is not available in tm-mainline-prod.
+ # So use ldflags to specify linker script.
+ ('ldflags',{
+ get_linker_script_ldflag('base/android/library_loader/anchor_functions.lds'),
}),
],
'cronet_aml_net_net': [
@@ -370,6 +375,7 @@
self.min_sdk_version = None
self.proto = dict()
self.linker_scripts = set()
+ self.ldflags = set()
# The genrule_XXX below are properties that must to be propagated back
# on the module(s) that depend on the genrule.
self.genrule_headers = set()
@@ -391,6 +397,9 @@
self.processor_class = None
self.sdk_version = None
self.javacflags = set()
+ self.license_kinds = set()
+ self.license_text = set()
+ self.default_applicable_licenses = set()
def to_string(self, output):
if self.comment:
@@ -437,6 +446,7 @@
self._output_field(output, 'stubs')
self._output_field(output, 'proto')
self._output_field(output, 'linker_scripts')
+ self._output_field(output, 'ldflags')
self._output_field(output, 'cppflags')
self._output_field(output, 'libs')
self._output_field(output, 'stem')
@@ -446,6 +456,9 @@
self._output_field(output, 'processor_class')
self._output_field(output, 'sdk_version')
self._output_field(output, 'javacflags')
+ self._output_field(output, 'license_kinds')
+ self._output_field(output, 'license_text')
+ self._output_field(output, 'default_applicable_licenses')
if self.rtti:
self._output_field(output, 'rtti')
@@ -1551,8 +1564,12 @@
def create_java_api_module(blueprint, gn):
source_module = Module('filegroup', module_prefix + 'api_sources', java_api_target_name)
+ # TODO add the API helpers separately after the main API is checked in and thoroughly reviewed
source_module.srcs.update([gn_utils.label_to_path(source)
- for source in get_api_java_sources(gn)])
+ for source in get_api_java_sources(gn)
+ if "apihelpers" not in source])
+ source_module.comment += "\n// TODO(danstahr): add the API helpers separately after the main" \
+ " API is checked in and thoroughly reviewed"
source_module.srcs.update([
':' + create_action_module(blueprint, gn.get_target(dep), 'java_genrule').name
for dep in get_api_java_actions(gn)])
@@ -1563,6 +1580,7 @@
java_module.sdk_version = "module_current"
java_module.libs = {
"androidx.annotation_annotation",
+ "framework-annotations-lib",
}
blueprint.add_module(java_module)
return java_module
@@ -1647,6 +1665,59 @@
return blueprint
+def create_license_module(blueprint):
+ module = Module("license", "external_cronet_license", "LICENSE")
+ module.license_kinds.update({
+ 'SPDX-license-identifier-LGPL-2.1',
+ 'SPDX-license-identifier-GPL-2.0',
+ 'SPDX-license-identifier-MPL',
+ 'SPDX-license-identifier-ISC',
+ 'SPDX-license-identifier-GPL',
+ 'SPDX-license-identifier-AFL-2.0',
+ 'SPDX-license-identifier-MPL-2.0',
+ 'SPDX-license-identifier-BSD',
+ 'SPDX-license-identifier-Apache-2.0',
+ 'SPDX-license-identifier-BSL-1.0',
+ 'SPDX-license-identifier-LGPL',
+ 'SPDX-license-identifier-GPL-3.0',
+ 'SPDX-license-identifier-Unicode-DFS',
+ 'SPDX-license-identifier-NCSA',
+ 'SPDX-license-identifier-OpenSSL',
+ 'SPDX-license-identifier-MIT',
+ "SPDX-license-identifier-ICU",
+ 'legacy_unencumbered', # public domain
+ })
+ module.license_text.update({
+ "LICENSE",
+ "net/third_party/uri_template/LICENSE",
+ "net/third_party/quiche/src/LICENSE",
+ "base/third_party/symbolize/LICENSE",
+ "base/third_party/superfasthash/LICENSE",
+ "base/third_party/xdg_user_dirs/LICENSE",
+ "base/third_party/double_conversion/LICENSE",
+ "base/third_party/nspr/LICENSE",
+ "base/third_party/dynamic_annotations/LICENSE",
+ "base/third_party/icu/LICENSE",
+ "base/third_party/valgrind/LICENSE",
+ "third_party/brotli/LICENSE",
+ "third_party/protobuf/LICENSE",
+ "third_party/protobuf/third_party/utf8_range/LICENSE",
+ "third_party/metrics_proto/LICENSE",
+ "third_party/boringssl/src/LICENSE",
+ "third_party/boringssl/src/third_party/googletest/LICENSE",
+ "third_party/boringssl/src/third_party/wycheproof_testvectors/LICENSE",
+ "third_party/boringssl/src/third_party/fiat/LICENSE",
+ "third_party/libevent/LICENSE",
+ "third_party/ashmem/LICENSE",
+ "third_party/icu/LICENSE",
+ "third_party/icu/scripts/LICENSE",
+ "third_party/abseil-cpp/LICENSE",
+ "third_party/modp_b64/LICENSE",
+ })
+ default_license = Module("package", "", "PACKAGE")
+ default_license.default_applicable_licenses.add(module.name)
+ blueprint.add_module(module)
+ blueprint.add_module(default_license)
def main():
parser = argparse.ArgumentParser(
@@ -1698,7 +1769,7 @@
# Add any proto groups to the blueprint.
for l_name, t_names in proto_groups.items():
create_proto_group_modules(blueprint, gn, l_name, t_names)
-
+ create_license_module(blueprint)
output = [
"""// Copyright (C) 2022 The Android Open Source Project
//