blob: ac12ce45e6a31d439f91aa4e07598ea1bc9db0bb [file] [log] [blame]
/*
* 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.test.suitebuilder.annotation.SmallTest;
import android.util.Log;
import android.util.SparseArray;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import com.android.compatibility.common.util.CddTest;
import com.android.compatibility.common.util.FeatureUtil;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* 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();
}
}