Merge "BluetoothCddTest: Verify CDD requirements in CTS"
diff --git a/tests/tests/bluetooth/bluetoothTestUtilLib/Android.bp b/tests/tests/bluetooth/bluetoothTestUtilLib/Android.bp
index 0bf1be1..21c4f7f 100644
--- a/tests/tests/bluetooth/bluetoothTestUtilLib/Android.bp
+++ b/tests/tests/bluetooth/bluetoothTestUtilLib/Android.bp
@@ -18,13 +18,12 @@
java_library {
name: "bluetooth-test-util-lib",
-
+ defaults: ["cts_defaults"],
static_libs: [
"junit",
"compatibility-device-util-axt",
+ "PlatformProperties",
],
-
srcs: ["src/**/*.java"],
-
sdk_version: "test_current",
}
diff --git a/tests/tests/bluetooth/src/android/bluetooth/cts/TestUtils.java b/tests/tests/bluetooth/bluetoothTestUtilLib/src/android/bluetooth/cts/TestUtils.java
similarity index 61%
rename from tests/tests/bluetooth/src/android/bluetooth/cts/TestUtils.java
rename to tests/tests/bluetooth/bluetoothTestUtilLib/src/android/bluetooth/cts/TestUtils.java
index 54aefc8..03de0d5 100644
--- a/tests/tests/bluetooth/src/android/bluetooth/cts/TestUtils.java
+++ b/tests/tests/bluetooth/bluetoothTestUtilLib/src/android/bluetooth/cts/TestUtils.java
@@ -17,9 +17,8 @@
package android.bluetooth.cts;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
@@ -30,23 +29,26 @@
import android.sysprop.BluetoothProperties;
import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.test.platform.app.InstrumentationRegistry;
-import junit.framework.Assert;
-
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.ReentrantLock;
/**
* Utility class for Bluetooth CTS test.
*/
-class TestUtils {
+public class TestUtils {
/**
* Checks whether this device has Bluetooth feature
* @return true if this device has Bluetooth feature
*/
- static boolean hasBluetooth() {
+ public static boolean hasBluetooth() {
Context context = InstrumentationRegistry.getInstrumentation().getContext();
return context.getPackageManager().hasSystemFeature(
PackageManager.FEATURE_BLUETOOTH);
@@ -55,7 +57,7 @@
/**
* Get the current enabled status of a given profile
*/
- static boolean isProfileEnabled(int profile) {
+ public static boolean isProfileEnabled(int profile) {
switch (profile) {
case BluetoothProfile.A2DP:
return BluetoothProperties.isProfileA2dpSourceEnabled().orElse(false);
@@ -120,7 +122,7 @@
* Adopt shell UID's permission via {@link android.app.UiAutomation}
* @param permission permission to adopt
*/
- static void adoptPermissionAsShellUid(@Nullable String... permission) {
+ public static void adoptPermissionAsShellUid(@Nullable String... permission) {
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.adoptShellPermissionIdentity(permission);
}
@@ -128,7 +130,7 @@
/**
* Drop all permissions adopted as shell UID
*/
- static void dropPermissionAsShellUid() {
+ public static void dropPermissionAsShellUid() {
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
}
@@ -138,7 +140,7 @@
* Fail the test if {@link BluetoothAdapter} is null
* @return instance of {@link BluetoothAdapter}
*/
- @NonNull static BluetoothAdapter getBluetoothAdapterOrDie() {
+ @NonNull public static BluetoothAdapter getBluetoothAdapterOrDie() {
Context context = InstrumentationRegistry.getInstrumentation().getContext();
BluetoothManager manager = context.getSystemService(BluetoothManager.class);
assertNotNull(manager);
@@ -150,7 +152,7 @@
/**
* Utility method to call hidden ScanRecord.parseFromBytes method.
*/
- static ScanRecord parseScanRecord(byte[] bytes) {
+ public static ScanRecord parseScanRecord(byte[] bytes) {
Class<?> scanRecordClass = ScanRecord.class;
try {
Method method = scanRecordClass.getDeclaredMethod("parseFromBytes", byte[].class);
@@ -164,17 +166,17 @@
/**
* Assert two byte arrays are equal.
*/
- static void assertArrayEquals(byte[] expected, byte[] actual) {
+ public static void assertArrayEquals(byte[] expected, byte[] actual) {
if (!Arrays.equals(expected, actual)) {
- Assert.fail("expected:<" + Arrays.toString(expected) +
- "> but was:<" + Arrays.toString(actual) + ">");
+ fail("expected:<" + Arrays.toString(expected)
+ + "> but was:<" + Arrays.toString(actual) + ">");
}
}
/**
* Get current location mode settings.
*/
- static int getLocationMode(Context context) {
+ public static int getLocationMode(Context context) {
return Settings.Secure.getInt(context.getContentResolver(),
Settings.Secure.LOCATION_MODE, Settings.Secure.LOCATION_MODE_OFF);
}
@@ -182,7 +184,7 @@
/**
* Set location settings mode.
*/
- static void setLocationMode(Context context, int mode) {
+ public static void setLocationMode(Context context, int mode) {
Settings.Secure.putInt(context.getContentResolver(), Settings.Secure.LOCATION_MODE,
mode);
}
@@ -190,21 +192,21 @@
/**
* Return true if location is on.
*/
- static boolean isLocationOn(Context context) {
+ public static boolean isLocationOn(Context context) {
return getLocationMode(context) != Settings.Secure.LOCATION_MODE_OFF;
}
/**
* Enable location and set the mode to GPS only.
*/
- static void enableLocation(Context context) {
+ public static void enableLocation(Context context) {
setLocationMode(context, Settings.Secure.LOCATION_MODE_SENSORS_ONLY);
}
/**
* Disable location.
*/
- static void disableLocation(Context context) {
+ public static void disableLocation(Context context) {
setLocationMode(context, Settings.Secure.LOCATION_MODE_OFF);
}
@@ -213,7 +215,7 @@
* @param context current device context
* @return true if BLE is supported, false otherwise
*/
- static boolean isBleSupported(Context context) {
+ public static boolean isBleSupported(Context context) {
return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE);
}
@@ -221,11 +223,113 @@
* Put the current thread to sleep.
* @param sleepMillis number of milliseconds to sleep for
*/
- static void sleep(int sleepMillis) {
+ public static void sleep(int sleepMillis) {
try {
Thread.sleep(sleepMillis);
} catch (InterruptedException e) {
Log.e(TestUtils.class.getSimpleName(), "interrupted", e);
}
}
+
+ /**
+ * Boilerplate class for profile listener
+ */
+ public static class BluetoothCtsServiceConnector {
+ private static final int PROXY_CONNECTION_TIMEOUT_MS = 500; // ms timeout for Proxy Connect
+ private BluetoothProfile mProfileProxy = null;
+ private boolean mIsProfileReady = false;
+ private boolean mIsProfileConnecting = false;
+ private final Condition mConditionProfileConnection;
+ private final ReentrantLock mProfileConnectionLock;
+ private final String mLogTag;
+ private final int mProfileId;
+ private final BluetoothAdapter mAdapter;
+ private final Context mContext;
+ BluetoothCtsServiceConnector(String logTag, int profileId, BluetoothAdapter adapter,
+ Context context) {
+ mLogTag = logTag;
+ mProfileId = profileId;
+ mAdapter = adapter;
+ mContext = context;
+ mProfileConnectionLock = new ReentrantLock();
+ mConditionProfileConnection = mProfileConnectionLock.newCondition();
+ assertNotNull(mLogTag);
+ assertNotNull(mAdapter);
+ assertNotNull(mContext);
+ }
+
+ public BluetoothProfile getProfileProxy() {
+ return mProfileProxy;
+ }
+
+ public void closeProfileProxy() {
+ if (mProfileProxy != null) {
+ mAdapter.closeProfileProxy(mProfileId, mProfileProxy);
+ mProfileProxy = null;
+ mIsProfileReady = false;
+ }
+ }
+
+ public boolean openProfileProxyAsync() {
+ mIsProfileConnecting = mAdapter.getProfileProxy(mContext, mServiceListener, mProfileId);
+ return mIsProfileConnecting;
+ }
+
+ public boolean waitForProfileConnect() {
+ return waitForProfileConnect(PROXY_CONNECTION_TIMEOUT_MS);
+ }
+
+ public boolean waitForProfileConnect(int timeoutMs) {
+ if (!mIsProfileConnecting) {
+ mIsProfileConnecting =
+ mAdapter.getProfileProxy(mContext, mServiceListener, mProfileId);
+ }
+ if (!mIsProfileConnecting) {
+ return false;
+ }
+ mProfileConnectionLock.lock();
+ try {
+ // Wait for the Adapter to be disabled
+ while (!mIsProfileReady) {
+ if (!mConditionProfileConnection.await(timeoutMs, TimeUnit.MILLISECONDS)) {
+ // Timeout
+ Log.e(mLogTag, "Timeout while waiting for Profile Connect");
+ break;
+ } // else spurious wake-ups
+ }
+ } catch (InterruptedException e) {
+ Log.e(mLogTag, "waitForProfileConnect: interrupted");
+ } finally {
+ mProfileConnectionLock.unlock();
+ }
+ mIsProfileConnecting = false;
+ return mIsProfileReady;
+ }
+
+ private final BluetoothProfile.ServiceListener mServiceListener =
+ new BluetoothProfile.ServiceListener() {
+ @Override
+ public void onServiceConnected(int profile, BluetoothProfile proxy) {
+ mProfileConnectionLock.lock();
+ mProfileProxy = proxy;
+ mIsProfileReady = true;
+ try {
+ mConditionProfileConnection.signal();
+ } finally {
+ mProfileConnectionLock.unlock();
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(int profile) {
+ mProfileConnectionLock.lock();
+ mIsProfileReady = false;
+ try {
+ mConditionProfileConnection.signal();
+ } finally {
+ mProfileConnectionLock.unlock();
+ }
+ }
+ };
+ }
}
diff --git a/tests/tests/bluetooth/src/android/bluetooth/cts/BluetoothCddTest.java b/tests/tests/bluetooth/src/android/bluetooth/cts/BluetoothCddTest.java
new file mode 100644
index 0000000..c838307
--- /dev/null
+++ b/tests/tests/bluetooth/src/android/bluetooth/cts/BluetoothCddTest.java
@@ -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.
+ */
+
+package android.bluetooth.cts;
+
+import static android.Manifest.permission.BLUETOOTH_CONNECT;
+import static android.Manifest.permission.BLUETOOTH_PRIVILEGED;
+import static android.Manifest.permission.BLUETOOTH_SCAN;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.BluetoothStatusCodes;
+import android.content.Context;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class BluetoothCddTest {
+ private static final String TAG = BluetoothCddTest.class.getSimpleName();
+ private static final int PROFILE_MCP_SERVER = 24;
+ private static final int PROFILE_LE_CALL_CONTROL = 27;
+ // Some devices need some extra time after entering STATE_OFF
+ private static final int BLUETOOTH_TOGGLE_DELAY_MS = 2000;
+ private Context mContext;
+ private boolean mHasBluetooth;
+ private BluetoothAdapter mAdapter;
+
+ @Before
+ public void setUp() {
+ mContext = InstrumentationRegistry.getInstrumentation().getContext();
+ mHasBluetooth = TestUtils.hasBluetooth();
+ Assume.assumeTrue(mHasBluetooth);
+ TestUtils.adoptPermissionAsShellUid(BLUETOOTH_CONNECT, BLUETOOTH_PRIVILEGED,
+ BLUETOOTH_SCAN);
+ mAdapter = TestUtils.getBluetoothAdapterOrDie();
+ if (mAdapter.isEnabled()) {
+ assertThat(BTAdapterUtils.disableAdapter(mAdapter, mContext)).isTrue();
+ try {
+ Thread.sleep(BLUETOOTH_TOGGLE_DELAY_MS);
+ } catch (InterruptedException ignored) { }
+ }
+ }
+
+ @After
+ public void tearDown() {
+ if (!mHasBluetooth) {
+ return;
+ }
+ if (mAdapter != null && mAdapter.getState() != BluetoothAdapter.STATE_OFF) {
+ if (mAdapter.getState() == BluetoothAdapter.STATE_ON) {
+ assertThat(BTAdapterUtils.disableAdapter(mAdapter, mContext)).isTrue();
+ }
+ try {
+ Thread.sleep(BLUETOOTH_TOGGLE_DELAY_MS);
+ } catch (InterruptedException ignored) { }
+ }
+ mAdapter = null;
+ mContext = null;
+ TestUtils.dropPermissionAsShellUid();
+ }
+
+ @Test
+ public void test_C_3_BleRequirements() {
+ Assume.assumeTrue(mHasBluetooth);
+ Assume.assumeTrue(TestUtils.isBleSupported(mContext));
+ assertThat(BTAdapterUtils.enableAdapter(mAdapter, mContext)).isTrue();
+ assertThat(mAdapter.getSupportedProfiles()).containsAtLeast(
+ BluetoothProfile.GATT,
+ BluetoothProfile.HEARING_AID);
+ }
+
+ @Test
+ public void test_C_5_1_AshaRequirements() {
+ Assume.assumeTrue(mHasBluetooth);
+ Assume.assumeTrue(TestUtils.isBleSupported(mContext));
+ assertThat(BTAdapterUtils.enableAdapter(mAdapter, mContext)).isTrue();
+ assertThat(mAdapter.getSupportedProfiles()).contains(BluetoothProfile.HEARING_AID);
+ TestUtils.BluetoothCtsServiceConnector connector =
+ new TestUtils.BluetoothCtsServiceConnector(TAG,
+ BluetoothProfile.HEARING_AID, mAdapter, mContext);
+ try {
+ assertThat(connector.openProfileProxyAsync()).isTrue();
+ assertThat(connector.waitForProfileConnect()).isTrue();
+ assertThat(connector.getProfileProxy()).isNotNull();
+ } finally {
+ connector.closeProfileProxy();
+ }
+ }
+
+ @Test
+ public void test_C_7_LeAudioUnicastRequirements() {
+ Assume.assumeTrue(mHasBluetooth);
+ assertThat(BTAdapterUtils.enableAdapter(mAdapter, mContext)).isTrue();
+ // Assert that BluetoothAdapter#isLeAudioSupported() and
+ // BluetoothAdapter#getSupportedProfiles() return the same information
+ if (mAdapter.isLeAudioSupported() != BluetoothStatusCodes.FEATURE_SUPPORTED) {
+ assertThat(mAdapter.getSupportedProfiles()).doesNotContain(BluetoothProfile.LE_AUDIO);
+ return;
+ }
+ assertThat(mAdapter.getSupportedProfiles()).containsAtLeast(
+ BluetoothProfile.LE_AUDIO,
+ BluetoothProfile.CSIP_SET_COORDINATOR,
+ PROFILE_MCP_SERVER,
+ BluetoothProfile.VOLUME_CONTROL,
+ PROFILE_LE_CALL_CONTROL);
+ assertThat(mAdapter.isLe2MPhySupported()).isTrue();
+ assertThat(mAdapter.isLeExtendedAdvertisingSupported()).isTrue();
+ }
+
+ @Test
+ public void test_C_8_LeAudioBroadcastSourceRequirements() {
+ Assume.assumeTrue(mHasBluetooth);
+ assertThat(BTAdapterUtils.enableAdapter(mAdapter, mContext)).isTrue();
+ // Assert that BluetoothAdapter#isLeAudioBroadcastSourceSupported() and
+ // BluetoothAdapter#getSupportedProfiles() return the same information
+ if (mAdapter.isLeAudioBroadcastSourceSupported()
+ != BluetoothStatusCodes.FEATURE_SUPPORTED) {
+ assertThat(mAdapter.getSupportedProfiles()).doesNotContain(
+ BluetoothProfile.LE_AUDIO_BROADCAST);
+ } else {
+ assertThat(mAdapter.getSupportedProfiles()).containsAtLeast(
+ BluetoothProfile.LE_AUDIO_BROADCAST,
+ BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
+ assertThat(mAdapter.isLePeriodicAdvertisingSupported()).isTrue();
+ // TODO: Enforce Periodic Advertising support
+ }
+
+ }
+
+ @Test
+ public void test_C_9_LeAudioBroadcastAssistantRequirements() {
+ Assume.assumeTrue(mHasBluetooth);
+ assertThat(BTAdapterUtils.enableAdapter(mAdapter, mContext)).isTrue();
+ // Assert that BluetoothAdapter#isLeAudioBroadcastAssistantSupported() and
+ // BluetoothAdapter#getSupportedProfiles() return the same information
+ if (mAdapter.isLeAudioBroadcastAssistantSupported()
+ != BluetoothStatusCodes.FEATURE_SUPPORTED) {
+ assertThat(mAdapter.getSupportedProfiles()).doesNotContain(
+ BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
+ } else {
+ assertThat(mAdapter.getSupportedProfiles()).contains(
+ BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
+ assertThat(mAdapter.isLePeriodicAdvertisingSupported()).isTrue();
+ // TODO: Enforce Periodic Advertising support
+ }
+ }
+}