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
+        }
+    }
+}