Merge "Merge "[A-0-2] Android Automotive required Bluetooth profiles CTS test" into pie-cts-dev am: a6834c8362"
diff --git a/tests/tests/car/AndroidManifest.xml b/tests/tests/car/AndroidManifest.xml
index fe1b64f..cce4761 100644
--- a/tests/tests/car/AndroidManifest.xml
+++ b/tests/tests/car/AndroidManifest.xml
@@ -21,6 +21,8 @@
<uses-permission android:name="android.car.permission.CAR_INFO" />
<uses-permission android:name="android.car.permission.CAR_POWERTRAIN" />
<uses-permission android:name="android.car.permission.CAR_SPEED" />
+ <uses-permission android:name="android.permission.BLUETOOTH" />
+ <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<application>
<uses-library android:name="android.test.runner" />
<activity android:name=".drivingstate.DistractionOptimizedActivity">
diff --git a/tests/tests/car/src/android/car/cts/CarBluetoothTest.java b/tests/tests/car/src/android/car/cts/CarBluetoothTest.java
new file mode 100644
index 0000000..2116b14
--- /dev/null
+++ b/tests/tests/car/src/android/car/cts/CarBluetoothTest.java
@@ -0,0 +1,361 @@
+/*
+ * Copyright (C) 2019 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.car.cts;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeTrue;
+
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothManager;
+import android.bluetooth.BluetoothProfile;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.platform.test.annotations.RequiresDevice;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.util.Log;
+import android.util.SparseArray;
+import com.android.compatibility.common.util.CddTest;
+import com.android.compatibility.common.util.FeatureUtil;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.ReentrantLock;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Contains the tests to prove compliance with android automotive specific bluetooth requirements.
+ */
+@SmallTest
+@RequiresDevice
+@RunWith(AndroidJUnit4.class)
+public class CarBluetoothTest {
+ private static final String TAG = "CarBluetoothTest";
+ private static final boolean DBG = false;
+ private Context mContext;
+
+ // Bluetooth Core objects
+ private BluetoothManager mBluetoothManager;
+ private BluetoothAdapter mBluetoothAdapter;
+
+ // Timeout for waiting for an adapter state change
+ private static final int BT_ADAPTER_TIMEOUT_MS = 8000; // ms
+
+ // Objects to block until the adapter has reached a desired state
+ private ReentrantLock mBluetoothAdapterLock;
+ private Condition mConditionAdapterStateReached;
+ private int mDesiredState;
+ private int mOriginalState;
+
+ /**
+ * Handles BluetoothAdapter state changes and signals when we've reached a desired state
+ */
+ private class BluetoothAdapterReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+
+ // Decode the intent
+ String action = intent.getAction();
+
+ // Watch for BluetoothAdapter intents only
+ if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) {
+ int newState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1);
+ if (DBG) {
+ Log.d(TAG, "Bluetooth adapter state changed: " + newState);
+ }
+
+ // Signal if the state is set to the one we're waiting on. If its not and we got a
+ // STATE_OFF event then handle the unexpected off event. Note that we could
+ // proactively turn the adapter back on to continue testing. For now we'll just
+ // log it
+ mBluetoothAdapterLock.lock();
+ try {
+ if (mDesiredState == newState) {
+ mConditionAdapterStateReached.signal();
+ } else if (newState == BluetoothAdapter.STATE_OFF) {
+ Log.w(TAG, "Bluetooth turned off unexpectedly while test was running.");
+ }
+ } finally {
+ mBluetoothAdapterLock.unlock();
+ }
+ }
+ }
+ }
+ private BluetoothAdapterReceiver mBluetoothAdapterReceiver;
+
+ private void waitForAdapterOn() {
+ if (DBG) {
+ Log.d(TAG, "Waiting for adapter to be on...");
+ }
+ waitForAdapterState(BluetoothAdapter.STATE_ON);
+ }
+
+ private void waitForAdapterOff() {
+ if (DBG) {
+ Log.d(TAG, "Waiting for adapter to be off...");
+ }
+ waitForAdapterState(BluetoothAdapter.STATE_OFF);
+ }
+
+ // Wait for the bluetooth adapter to be in a given state
+ private void waitForAdapterState(int desiredState) {
+ if (DBG) {
+ Log.d(TAG, "Waiting for adapter state " + desiredState);
+ }
+ mBluetoothAdapterLock.lock();
+ try {
+ // Update the desired state so that we'll signal when we get there
+ mDesiredState = desiredState;
+ if (desiredState == BluetoothAdapter.STATE_ON) {
+ mBluetoothAdapter.enable();
+ } else {
+ mBluetoothAdapter.disable();
+ }
+
+ // Wait until we're reached that desired state
+ while (desiredState != mBluetoothAdapter.getState()) {
+ if (!mConditionAdapterStateReached.await(
+ BT_ADAPTER_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+ Log.e(TAG, "Timeout while waiting for Bluetooth adapter state " + desiredState);
+ break;
+ }
+ }
+ } catch (InterruptedException e) {
+ Log.w(TAG, "waitForAdapterState(" + desiredState + "): interrupted", e);
+ } finally {
+ mBluetoothAdapterLock.unlock();
+ }
+ }
+
+ // Utility class to hold profile information and state
+ private static class ProfileInfo {
+ final String mName;
+ boolean mConnected;
+
+ public ProfileInfo(String name) {
+ mName = name;
+ mConnected = false;
+ }
+ }
+
+ // Automotive required profiles and meta data. Profile defaults to 'not connected' and name
+ // is used in debug and error messages
+ private static SparseArray<ProfileInfo> sRequiredBluetoothProfiles = new SparseArray();
+ static {
+ sRequiredBluetoothProfiles.put(BluetoothProfile.A2DP_SINK,
+ new ProfileInfo("A2DP Sink")); // 11
+ sRequiredBluetoothProfiles.put(BluetoothProfile.AVRCP_CONTROLLER,
+ new ProfileInfo("AVRCP Controller")); // 12
+ sRequiredBluetoothProfiles.put(BluetoothProfile.HEADSET_CLIENT,
+ new ProfileInfo("HSP Client")); // 16
+ sRequiredBluetoothProfiles.put(BluetoothProfile.PBAP_CLIENT,
+ new ProfileInfo("PBAP Client")); // 17
+ }
+ private static final int MAX_PROFILES_SUPPORTED = sRequiredBluetoothProfiles.size();
+
+ // Configurable timeout for waiting for profile proxies to connect
+ private static final int PROXY_CONNECTIONS_TIMEOUT_MS = 1000; // ms
+
+ // Objects to block until all profile proxy connections have finished, or the timeout occurs
+ private Condition mConditionAllProfilesConnected;
+ private ReentrantLock mProfileConnectedLock;
+ private int mProfilesSupported;
+
+ // Capture profile proxy connection events
+ private final class ProfileServiceListener implements BluetoothProfile.ServiceListener {
+ @Override
+ public void onServiceConnected(int profile, BluetoothProfile proxy) {
+ if (DBG) {
+ Log.d(TAG, "Profile '" + profile + "' has connected");
+ }
+ mProfileConnectedLock.lock();
+ try {
+ sRequiredBluetoothProfiles.get(profile).mConnected = true;
+ mProfilesSupported++;
+ if (mProfilesSupported == MAX_PROFILES_SUPPORTED) {
+ mConditionAllProfilesConnected.signal();
+ }
+ } finally {
+ mProfileConnectedLock.unlock();
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(int profile) {
+ if (DBG) {
+ Log.d(TAG, "Profile '" + profile + "' has disconnected");
+ }
+ mProfileConnectedLock.lock();
+ try {
+ sRequiredBluetoothProfiles.get(profile).mConnected = false;
+ mProfilesSupported--;
+ } finally {
+ mProfileConnectedLock.unlock();
+ }
+ }
+ }
+
+ // Initiate connections to all profiles and wait until we connect to all, or time out
+ private void waitForProfileConnections() {
+ if (DBG) {
+ Log.d(TAG, "Starting profile proxy connections...");
+ }
+ mProfileConnectedLock.lock();
+ try {
+ // Attempt connection to each required profile
+ for (int i = 0; i < sRequiredBluetoothProfiles.size(); i++) {
+ int profile = sRequiredBluetoothProfiles.keyAt(i);
+ mBluetoothAdapter.getProfileProxy(mContext, new ProfileServiceListener(), profile);
+ }
+
+ // Wait for the Adapter to be disabled
+ while (mProfilesSupported != MAX_PROFILES_SUPPORTED) {
+ if (!mConditionAllProfilesConnected.await(
+ PROXY_CONNECTIONS_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
+ Log.e(TAG, "Timeout while waiting for Profile Connections");
+ break;
+ }
+ }
+ } catch (InterruptedException e) {
+ Log.w(TAG, "waitForProfileConnections: interrupted", e);
+ } finally {
+ mProfileConnectedLock.unlock();
+ }
+
+ if (DBG) {
+ Log.d(TAG, "Proxy connection attempts complete. Connected " + mProfilesSupported
+ + "/" + MAX_PROFILES_SUPPORTED + " profiles");
+ }
+ }
+
+ // Check and make sure each profile is connected. If any are not supported then build an
+ // error string to report each missing profile and assert a failure
+ private void checkProfileConnections() {
+ if (DBG) {
+ Log.d(TAG, "Checking for all required profiles");
+ }
+ mProfileConnectedLock.lock();
+ try {
+ if (mProfilesSupported != MAX_PROFILES_SUPPORTED) {
+ if (DBG) {
+ Log.d(TAG, "Some profiles failed to connect");
+ }
+ StringBuilder e = new StringBuilder();
+ for (int i = 0; i < sRequiredBluetoothProfiles.size(); i++) {
+ int profile = sRequiredBluetoothProfiles.keyAt(i);
+ String name = sRequiredBluetoothProfiles.get(profile).mName;
+ if (!sRequiredBluetoothProfiles.get(profile).mConnected) {
+ if (e.length() == 0) {
+ e.append("Missing Profiles: ");
+ } else {
+ e.append(", ");
+ }
+ e.append(name + " (" + profile + ")");
+
+ if (DBG) {
+ Log.d(TAG, name + " failed to connect");
+ }
+ }
+ }
+ fail(e.toString());
+ }
+ } finally {
+ mProfileConnectedLock.unlock();
+ }
+ }
+
+ // Set the connection status for each profile to false
+ private void clearProfileStatuses() {
+ if (DBG) {
+ Log.d(TAG, "Setting all profiles to 'disconnected'");
+ }
+ for (int i = 0; i < sRequiredBluetoothProfiles.size(); i++) {
+ int profile = sRequiredBluetoothProfiles.keyAt(i);
+ sRequiredBluetoothProfiles.get(profile).mConnected = false;
+ }
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ if (DBG) {
+ Log.d(TAG, "Setting up Automotive Bluetooth test. Device is "
+ + (FeatureUtil.isAutomotive() ? "" : "not ") + "automotive");
+ }
+
+ // Automotive only
+ assumeTrue(FeatureUtil.isAutomotive());
+
+ // Get the context
+ mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+
+ // Get bluetooth core objects so we can get proxies/check for profile existence
+ mBluetoothManager = (BluetoothManager) mContext.getSystemService(
+ Context.BLUETOOTH_SERVICE);
+ mBluetoothAdapter = mBluetoothManager.getAdapter();
+
+ // Initialize all the profile connection variables
+ mProfilesSupported = 0;
+ mProfileConnectedLock = new ReentrantLock();
+ mConditionAllProfilesConnected = mProfileConnectedLock.newCondition();
+ clearProfileStatuses();
+
+ // Register the adapter receiver and initialize adapter state wait objects
+ mDesiredState = -1; // Set and checked by waitForAdapterState()
+ mBluetoothAdapterLock = new ReentrantLock();
+ mConditionAdapterStateReached = mBluetoothAdapterLock.newCondition();
+ mBluetoothAdapterReceiver = new BluetoothAdapterReceiver();
+ IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
+ mContext.registerReceiver(mBluetoothAdapterReceiver, filter);
+
+ // Make sure Bluetooth is enabled before the test
+ waitForAdapterOn();
+ assertTrue(mBluetoothAdapter.isEnabled());
+ }
+
+ @After
+ public void tearDown() {
+ waitForAdapterOff();
+ mContext.unregisterReceiver(mBluetoothAdapterReceiver);
+ }
+
+ // [A-0-2] : Android Automotive devices must support the following Bluetooth profiles:
+ // * Hands Free Profile (HFP) [Phone calling]
+ // * Audio Distribution Profile (A2DP) [Media playback]
+ // * Audio/Video Remote Control Profile (AVRCP) [Media playback control]
+ // * Phone Book Access Profile (PBAP) [Contact sharing/receiving]
+ //
+ // This test fires off connections to each required profile (which are asynchronous in nature)
+ // and waits for all of them to connect (proving they are there and implemented), or for the
+ // configured timeout. If all required profiles connect, the test passes.
+ @Test
+ @CddTest(requirement = "7.4.3/A-0-2")
+ public void testRequiredBluetoothProfilesExist() throws Exception {
+ if (DBG) {
+ Log.d(TAG, "Begin testRequiredBluetoothProfilesExist()");
+ }
+ assertNotNull(mBluetoothAdapter);
+ waitForProfileConnections();
+ checkProfileConnections();
+ }
+}