blob: 2ed393f5be800f2c577af2b4422203c2655c2ef5 [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.bluetooth.cts;
import static android.Manifest.permission.BLUETOOTH_CONNECT;
import static org.junit.Assert.assertThrows;
import android.app.UiAutomation;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothHearingAid;
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.content.pm.PackageManager;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.MediumTest;
import android.util.Log;
import androidx.test.InstrumentationRegistry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/**
* Unit test cases for {@link BluetoothHearingAid}.
* <p>
* To run the test, use adb shell am instrument -e class 'android.bluetooth.HearingAidProfileTest'
* -w 'com.android.bluetooth.tests/android.bluetooth.BluetoothTestRunner'
*/
public class HearingAidProfileTest extends AndroidTestCase {
private static final String TAG = "HearingAidProfileTest";
private static final int WAIT_FOR_INTENT_TIMEOUT_MS = 10000; // ms to wait for intent callback
private static final int PROXY_CONNECTION_TIMEOUT_MS = 500; // ms timeout for Proxy Connect
// ADAPTER_ENABLE_TIMEOUT_MS = AdapterState.BLE_START_TIMEOUT_DELAY +
// AdapterState.BREDR_START_TIMEOUT_DELAY
private static final int ADAPTER_ENABLE_TIMEOUT_MS = 8000;
// ADAPTER_DISABLE_TIMEOUT_MS = AdapterState.BLE_STOP_TIMEOUT_DELAY +
// AdapterState.BREDR_STOP_TIMEOUT_DELAY
private static final int ADAPTER_DISABLE_TIMEOUT_MS = 5000;
private boolean mIsHearingAidSupported;
private boolean mIsBleSupported;
private BluetoothHearingAid mService;
private BluetoothAdapter mBluetoothAdapter;
private BroadcastReceiver mIntentReceiver;
private UiAutomation mUiAutomation;;
private Condition mConditionProfileIsConnected;
private ReentrantLock mProfileConnectedlock;
private boolean mIsProfileReady;
private static List<Integer> mValidConnectionStates = new ArrayList<Integer>(
Arrays.asList(BluetoothProfile.STATE_CONNECTING, BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_DISCONNECTING));
private List<BluetoothDevice> mIntentCallbackDeviceList;
public void setUp() throws Exception {
if (!isBleSupported()) return;
mIsBleSupported = true;
mIsHearingAidSupported = TestUtils.isProfileEnabled(BluetoothProfile.HEARING_AID);
if (!mIsHearingAidSupported) return;
mUiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
mUiAutomation.adoptShellPermissionIdentity(BLUETOOTH_CONNECT);
BluetoothManager manager = (BluetoothManager) mContext.getSystemService(
Context.BLUETOOTH_SERVICE);
mBluetoothAdapter = manager.getAdapter();
assertTrue(BTAdapterUtils.enableAdapter(mBluetoothAdapter, mContext));
mProfileConnectedlock = new ReentrantLock();
mConditionProfileIsConnected = mProfileConnectedlock.newCondition();
mIsProfileReady = false;
mService = null;
mBluetoothAdapter.getProfileProxy(getContext(), new HearingAidsServiceListener(),
BluetoothProfile.HEARING_AID);
}
@Override
public void tearDown() {
if (!(mIsBleSupported && mIsHearingAidSupported)) {
return;
}
if (mBluetoothAdapter != null) {
assertTrue(BTAdapterUtils.disableAdapter(mBluetoothAdapter, mContext));
}
mUiAutomation.dropShellPermissionIdentity();
}
/**
* Basic test case to make sure that Hearing Aid Profile Proxy can connect.
*/
@MediumTest
public void test_getProxyServiceConnect() {
if (!(mIsBleSupported && mIsHearingAidSupported)) return;
waitForProfileConnect();
assertTrue(mIsProfileReady);
assertNotNull(mService);
}
/**
* Basic test case to make sure that a fictional device is disconnected.
*/
@MediumTest
public void test_getConnectionState() {
if (!(mIsBleSupported && mIsHearingAidSupported)) {
return;
}
waitForProfileConnect();
assertTrue(mIsProfileReady);
assertNotNull(mService);
// Create a fake device
BluetoothDevice device = mBluetoothAdapter.getRemoteDevice("00:11:22:AA:BB:CC");
assertNotNull(device);
int connectionState = mService.getConnectionState(device);
// Fake device should be disconnected
assertEquals(connectionState, BluetoothProfile.STATE_DISCONNECTED);
}
/**
* Basic test case to make sure that a fictional device throw a SecurityException when setting
* volume.
*/
@MediumTest
public void test_setVolume() {
if (!(mIsBleSupported && mIsHearingAidSupported)) {
return;
}
waitForProfileConnect();
assertTrue(mIsProfileReady);
assertNotNull(mService);
// This should throw a SecurityException because no BLUETOOTH_PRIVILEGED permission
assertThrows(SecurityException.class, () -> mService.setVolume(42));
}
/**
* Basic test case to get the list of connected Hearing Aid devices.
*/
@MediumTest
public void test_getConnectedDevices() {
if (!(mIsBleSupported && mIsHearingAidSupported)) {
return;
}
waitForProfileConnect();
assertTrue(mIsProfileReady);
assertNotNull(mService);
List<BluetoothDevice> deviceList;
deviceList = mService.getConnectedDevices();
Log.d(TAG, "getConnectedDevices(): size=" + deviceList.size());
for (BluetoothDevice device : deviceList) {
int connectionState = mService.getConnectionState(device);
checkValidConnectionState(connectionState);
}
}
/**
* Basic test case to get the list of matching Hearing Aid devices for each of the 4 connection
* states.
*/
@MediumTest
public void test_getDevicesMatchingConnectionStates() {
if (!(mIsBleSupported && mIsHearingAidSupported)) {
return;
}
waitForProfileConnect();
assertTrue(mIsProfileReady);
assertNotNull(mService);
for (int connectionState : mValidConnectionStates) {
List<BluetoothDevice> deviceList;
deviceList = mService.getDevicesMatchingConnectionStates(new int[]{connectionState});
assertNotNull(deviceList);
Log.d(TAG, "getDevicesMatchingConnectionStates(" + connectionState + "): size="
+ deviceList.size());
checkDeviceListAndStates(deviceList, connectionState);
}
}
/**
* Test case to make sure that if the connection changed intent is called, the parameters and
* device are correct.
*/
@MediumTest
public void test_getConnectionStateChangedIntent() {
if (!(mIsBleSupported && mIsHearingAidSupported)) {
return;
}
waitForProfileConnect();
assertTrue(mIsProfileReady);
assertNotNull(mService);
// Find out how many Hearing Aid bonded devices
List<BluetoothDevice> bondedDeviceList = new ArrayList();
int numDevices = 0;
for (int connectionState : mValidConnectionStates) {
List<BluetoothDevice> deviceList;
deviceList = mService.getDevicesMatchingConnectionStates(new int[]{connectionState});
bondedDeviceList.addAll(deviceList);
numDevices += deviceList.size();
}
if (numDevices <= 0) return;
Log.d(TAG, "Number Hearing Aids devices bonded=" + numDevices);
mIntentCallbackDeviceList = new ArrayList();
// Set up the Connection State Changed receiver
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED);
mIntentReceiver = new HearingAidIntentReceiver();
mContext.registerReceiver(mIntentReceiver, filter);
Log.d(TAG, "test_getConnectionStateChangedIntent: disable adapter and wait");
assertTrue(BTAdapterUtils.disableAdapter(mBluetoothAdapter, mContext));
Log.d(TAG, "test_getConnectionStateChangedIntent: enable adapter and wait");
assertTrue(BTAdapterUtils.enableAdapter(mBluetoothAdapter, mContext));
int sanityCount = WAIT_FOR_INTENT_TIMEOUT_MS;
while ((numDevices != mIntentCallbackDeviceList.size()) && (sanityCount > 0)) {
final int SLEEP_QUANTUM_MS = 100;
sleep(SLEEP_QUANTUM_MS);
sanityCount -= SLEEP_QUANTUM_MS;
}
// Tear down
mContext.unregisterReceiver(mIntentReceiver);
Log.d(TAG, "test_getConnectionStateChangedIntent: number of bonded device="
+ numDevices + ", mIntentCallbackDeviceList.size()="
+ mIntentCallbackDeviceList.size());
for (BluetoothDevice device : mIntentCallbackDeviceList) {
assertTrue(bondedDeviceList.contains(device));
}
}
private boolean waitForProfileConnect() {
mProfileConnectedlock.lock();
try {
// Wait for the Adapter to be disabled
while (!mIsProfileReady) {
if (!mConditionProfileIsConnected.await(
PROXY_CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
// Timeout
Log.e(TAG, "Timeout while waiting for Profile Connect");
break;
} // else spurious wakeups
}
} catch(InterruptedException e) {
Log.e(TAG, "waitForProfileConnect: interrrupted");
} finally {
mProfileConnectedlock.unlock();
}
return mIsProfileReady;
}
private final class HearingAidsServiceListener
implements BluetoothProfile.ServiceListener {
public void onServiceConnected(int profile, BluetoothProfile proxy) {
mProfileConnectedlock.lock();
mService = (BluetoothHearingAid) proxy;
mIsProfileReady = true;
try {
mConditionProfileIsConnected.signal();
} finally {
mProfileConnectedlock.unlock();
}
}
public void onServiceDisconnected(int profile) {
mProfileConnectedlock.lock();
mIsProfileReady = false;
mService = null;
mProfileConnectedlock.unlock();
}
}
private class HearingAidIntentReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) {
int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
int previousState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1);
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
Log.d(TAG,"HearingAidIntentReceiver.onReceive: device=" + device
+ ", state=" + state + ", previousState=" + previousState);
checkValidConnectionState(state);
checkValidConnectionState(previousState);
mIntentCallbackDeviceList.add(device);
}
}
}
private void checkDeviceListAndStates(List<BluetoothDevice> deviceList, int connectionState) {
Log.d(TAG, "checkDeviceListAndStates(): size=" + deviceList.size()
+ ", connectionState=" + connectionState);
for (BluetoothDevice device : deviceList) {
int deviceConnectionState = mService.getConnectionState(device);
assertEquals("Mismatched connection state for " + device,
connectionState, deviceConnectionState);
}
}
private void checkValidConnectionState(int connectionState) {
assertTrue(mValidConnectionStates.contains(connectionState));
}
// Returns whether offloaded scan batching is supported.
private boolean isBleBatchScanSupported() {
return mBluetoothAdapter.isOffloadedScanBatchingSupported();
}
// Check if Bluetooth LE feature is supported on DUT.
private boolean isBleSupported() {
return getContext().getPackageManager()
.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE);
}
private static void sleep(long t) {
try {
Thread.sleep(t);
} catch (InterruptedException e) {}
}
}