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();
+    }
+}