1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.bluetooth.cts;
18 
19 import android.bluetooth.BluetoothAdapter;
20 import android.bluetooth.BluetoothDevice;
21 import android.bluetooth.BluetoothHearingAid;
22 import android.bluetooth.BluetoothManager;
23 import android.bluetooth.BluetoothProfile;
24 import android.content.BroadcastReceiver;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.IntentFilter;
28 import android.content.pm.PackageManager;
29 import android.test.AndroidTestCase;
30 import android.test.suitebuilder.annotation.MediumTest;
31 import android.util.Log;
32 
33 import java.io.IOException;
34 import java.util.List;
35 import java.util.ArrayList;
36 import java.util.Arrays;
37 import java.util.concurrent.locks.Condition;
38 import java.util.concurrent.locks.ReentrantLock;
39 import java.util.concurrent.TimeUnit;
40 
41 /**
42  * Unit test cases for {@link BluetoothHearingAid}.
43  * <p>
44  * To run the test, use adb shell am instrument -e class 'android.bluetooth.HearingAidProfileTest'
45  * -w 'com.android.bluetooth.tests/android.bluetooth.BluetoothTestRunner'
46  */
47 public class HearingAidProfileTest extends AndroidTestCase {
48     private static final String TAG = "HearingAidProfileTest";
49 
50     private static final int WAIT_FOR_INTENT_TIMEOUT_MS = 10000; // ms to wait for intent callback
51     private static final int PROXY_CONNECTION_TIMEOUT_MS = 500;  // ms timeout for Proxy Connect
52     // ADAPTER_ENABLE_TIMEOUT_MS = AdapterState.BLE_START_TIMEOUT_DELAY +
53     //                              AdapterState.BREDR_START_TIMEOUT_DELAY
54     private static final int ADAPTER_ENABLE_TIMEOUT_MS = 8000;
55     // ADAPTER_DISABLE_TIMEOUT_MS = AdapterState.BLE_STOP_TIMEOUT_DELAY +
56     //                                  AdapterState.BREDR_STOP_TIMEOUT_DELAY
57     private static final int ADAPTER_DISABLE_TIMEOUT_MS = 5000;
58 
59     private boolean mIsHearingAidSupported;
60     private boolean mIsBleSupported;
61     private BluetoothHearingAid mService;
62     private BluetoothAdapter mBluetoothAdapter;
63     private BroadcastReceiver mIntentReceiver;
64 
65     private Condition mConditionProfileIsConnected;
66     private ReentrantLock mProfileConnectedlock;
67     private boolean mIsProfileReady;
68 
69     private static List<Integer> mValidConnectionStates = new ArrayList<Integer>(
70         Arrays.asList(BluetoothProfile.STATE_CONNECTING, BluetoothProfile.STATE_CONNECTED,
71                       BluetoothProfile.STATE_DISCONNECTED, BluetoothProfile.STATE_DISCONNECTING));
72 
73     private List<BluetoothDevice> mIntentCallbackDeviceList;
74 
setUp()75     public void setUp() throws Exception {
76         if (!isBleSupported()) return;
77         mIsBleSupported = true;
78 
79         BluetoothManager manager = (BluetoothManager) mContext.getSystemService(
80                 Context.BLUETOOTH_SERVICE);
81         mBluetoothAdapter = manager.getAdapter();
82 
83         if (!BTAdapterUtils.enableAdapter(mBluetoothAdapter, mContext)) {
84             Log.e(TAG, "Unable to enable Bluetooth Adapter!");
85             assertTrue(mBluetoothAdapter.isEnabled());
86         }
87 
88         mProfileConnectedlock = new ReentrantLock();
89         mConditionProfileIsConnected  = mProfileConnectedlock.newCondition();
90         mIsProfileReady = false;
91         mService = null;
92         mIsHearingAidSupported = mBluetoothAdapter.getProfileProxy(getContext(),
93                                                   new HearingAidsServiceListener(),
94                                                   BluetoothProfile.HEARING_AID);
95         if (!mIsHearingAidSupported) return;
96     }
97 
98     @Override
tearDown()99     public void tearDown() {
100         if (!mIsBleSupported) return;
101 
102         if (!BTAdapterUtils.disableAdapter(mBluetoothAdapter, mContext)) {
103             Log.e(TAG, "Unable to disable Bluetooth Adapter!");
104             assertTrue(mBluetoothAdapter.isEnabled());
105         }
106     }
107 
108     /**
109      * Basic test case to make sure that Hearing Aid Profile Proxy can connect.
110      */
111     @MediumTest
test_getProxyServiceConnect()112     public void test_getProxyServiceConnect() {
113         if (!(mIsBleSupported && mIsHearingAidSupported)) return;
114 
115         waitForProfileConnect();
116         assertTrue(mIsProfileReady);
117         assertNotNull(mService);
118     }
119 
120     /**
121      * Basic test case to make sure that a fictional device is disconnected.
122      */
123     @MediumTest
test_getConnectionState()124     public void test_getConnectionState() {
125         if (!(mIsBleSupported && mIsHearingAidSupported)) {
126             return;
127         }
128 
129         waitForProfileConnect();
130         assertTrue(mIsProfileReady);
131         assertNotNull(mService);
132 
133         // Create a dummy device
134         BluetoothDevice device = mBluetoothAdapter.getRemoteDevice("00:11:22:AA:BB:CC");
135         assertNotNull(device);
136 
137         int connectionState = mService.getConnectionState(device);
138         // Dummy device should be disconnected
139         assertEquals(connectionState, BluetoothProfile.STATE_DISCONNECTED);
140     }
141 
142     /**
143      * Basic test case to get the list of connected Hearing Aid devices.
144      */
145     @MediumTest
test_getConnectedDevices()146     public void test_getConnectedDevices() {
147         if (!(mIsBleSupported && mIsHearingAidSupported)) {
148             return;
149         }
150 
151         waitForProfileConnect();
152         assertTrue(mIsProfileReady);
153         assertNotNull(mService);
154 
155         List<BluetoothDevice> deviceList;
156 
157         deviceList = mService.getConnectedDevices();
158         Log.d(TAG, "getConnectedDevices(): size=" + deviceList.size());
159         for (BluetoothDevice device : deviceList) {
160             int connectionState = mService.getConnectionState(device);
161             checkValidConnectionState(connectionState);
162         }
163     }
164 
165     /**
166      * Basic test case to get the list of matching Hearing Aid devices for each of the 4 connection
167      * states.
168      */
169     @MediumTest
test_getDevicesMatchingConnectionStates()170     public void test_getDevicesMatchingConnectionStates() {
171         if (!(mIsBleSupported && mIsHearingAidSupported)) {
172             return;
173         }
174 
175         waitForProfileConnect();
176         assertTrue(mIsProfileReady);
177         assertNotNull(mService);
178 
179         for (int connectionState : mValidConnectionStates) {
180             List<BluetoothDevice> deviceList;
181 
182             deviceList = mService.getDevicesMatchingConnectionStates(new int[]{connectionState});
183             assertNotNull(deviceList);
184             Log.d(TAG, "getDevicesMatchingConnectionStates(" + connectionState + "): size="
185                   + deviceList.size());
186             checkDeviceListAndStates(deviceList, connectionState);
187         }
188     }
189 
190     /**
191      * Test case to make sure that if the connection changed intent is called, the parameters and
192      * device are correct.
193      */
194     @MediumTest
test_getConnectionStateChangedIntent()195     public void test_getConnectionStateChangedIntent() {
196         if (!(mIsBleSupported && mIsHearingAidSupported)) {
197             return;
198         }
199 
200         waitForProfileConnect();
201         assertTrue(mIsProfileReady);
202         assertNotNull(mService);
203 
204         // Find out how many Hearing Aid bonded devices
205         List<BluetoothDevice> bondedDeviceList = new ArrayList();
206         int numDevices = 0;
207         for (int connectionState : mValidConnectionStates) {
208             List<BluetoothDevice> deviceList;
209 
210             deviceList = mService.getDevicesMatchingConnectionStates(new int[]{connectionState});
211             bondedDeviceList.addAll(deviceList);
212             numDevices += deviceList.size();
213         }
214 
215         if (numDevices <= 0) return;
216         Log.d(TAG, "Number Hearing Aids devices bonded=" + numDevices);
217 
218         mIntentCallbackDeviceList = new ArrayList();
219 
220         // Set up the Connection State Changed receiver
221         IntentFilter filter = new IntentFilter();
222         filter.addAction(BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED);
223         mIntentReceiver = new HearingAidIntentReceiver();
224         mContext.registerReceiver(mIntentReceiver, filter);
225 
226         Log.d(TAG, "test_getConnectionStateChangedIntent: disable adapter and wait");
227         assertTrue(BTAdapterUtils.disableAdapter(mBluetoothAdapter, mContext));
228 
229         Log.d(TAG, "test_getConnectionStateChangedIntent: enable adapter and wait");
230         assertTrue(BTAdapterUtils.enableAdapter(mBluetoothAdapter, mContext));
231 
232         int sanityCount = WAIT_FOR_INTENT_TIMEOUT_MS;
233         while ((numDevices != mIntentCallbackDeviceList.size()) && (sanityCount > 0)) {
234             final int SLEEP_QUANTUM_MS = 100;
235             sleep(SLEEP_QUANTUM_MS);
236             sanityCount -= SLEEP_QUANTUM_MS;
237         }
238 
239         // Tear down
240         mContext.unregisterReceiver(mIntentReceiver);
241 
242         Log.d(TAG, "test_getConnectionStateChangedIntent: number of bonded device="
243               + numDevices + ", mIntentCallbackDeviceList.size()="
244               + mIntentCallbackDeviceList.size());
245         for (BluetoothDevice device : mIntentCallbackDeviceList) {
246             assertTrue(bondedDeviceList.contains(device));
247         }
248     }
249 
waitForProfileConnect()250     private boolean waitForProfileConnect() {
251         mProfileConnectedlock.lock();
252         try {
253             // Wait for the Adapter to be disabled
254             while (!mIsProfileReady) {
255                 if (!mConditionProfileIsConnected.await(
256                     PROXY_CONNECTION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
257                     // Timeout
258                     Log.e(TAG, "Timeout while waiting for Profile Connect");
259                     break;
260                 } // else spurious wakeups
261             }
262         } catch(InterruptedException e) {
263             Log.e(TAG, "waitForProfileConnect: interrrupted");
264         } finally {
265             mProfileConnectedlock.unlock();
266         }
267         return mIsProfileReady;
268     }
269 
270     private final class HearingAidsServiceListener
271             implements BluetoothProfile.ServiceListener {
272 
onServiceConnected(int profile, BluetoothProfile proxy)273         public void onServiceConnected(int profile, BluetoothProfile proxy) {
274             mProfileConnectedlock.lock();
275             mService = (BluetoothHearingAid) proxy;
276             mIsProfileReady = true;
277             try {
278                 mConditionProfileIsConnected.signal();
279             } finally {
280                 mProfileConnectedlock.unlock();
281             }
282         }
283 
onServiceDisconnected(int profile)284         public void onServiceDisconnected(int profile) {
285             mProfileConnectedlock.lock();
286             mIsProfileReady = false;
287             mService = null;
288             mProfileConnectedlock.unlock();
289         }
290     }
291 
292     private class HearingAidIntentReceiver extends BroadcastReceiver {
293         @Override
onReceive(Context context, Intent intent)294         public void onReceive(Context context, Intent intent) {
295             if (BluetoothHearingAid.ACTION_CONNECTION_STATE_CHANGED.equals(intent.getAction())) {
296                 int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1);
297                 int previousState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, -1);
298                 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
299 
300                 Log.d(TAG,"HearingAidIntentReceiver.onReceive: device=" + device
301                       + ", state=" + state + ", previousState=" + previousState);
302 
303                 checkValidConnectionState(state);
304                 checkValidConnectionState(previousState);
305 
306                 mIntentCallbackDeviceList.add(device);
307             }
308         }
309     }
310 
checkDeviceListAndStates(List<BluetoothDevice> deviceList, int connectionState)311     private void checkDeviceListAndStates(List<BluetoothDevice> deviceList, int connectionState) {
312         Log.d(TAG, "checkDeviceListAndStates(): size=" + deviceList.size()
313               + ", connectionState=" + connectionState);
314         for (BluetoothDevice device : deviceList) {
315             int deviceConnectionState = mService.getConnectionState(device);
316             assertEquals("Mismatched connection state for " + device,
317                          connectionState, deviceConnectionState);
318         }
319     }
320 
checkValidConnectionState(int connectionState)321     private void checkValidConnectionState(int connectionState) {
322         assertTrue(mValidConnectionStates.contains(connectionState));
323     }
324 
325     // Returns whether offloaded scan batching is supported.
isBleBatchScanSupported()326     private boolean isBleBatchScanSupported() {
327         return mBluetoothAdapter.isOffloadedScanBatchingSupported();
328     }
329 
330     // Check if Bluetooth LE feature is supported on DUT.
isBleSupported()331     private boolean isBleSupported() {
332         return getContext().getPackageManager()
333                 .hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE);
334     }
335 
sleep(long t)336     private static void sleep(long t) {
337         try {
338             Thread.sleep(t);
339         } catch (InterruptedException e) {}
340     }
341 }
342