1 /* 2 * Copyright 2018 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 com.android.bluetooth.hearingaid; 18 19 import static org.mockito.Mockito.*; 20 21 import android.bluetooth.BluetoothAdapter; 22 import android.bluetooth.BluetoothDevice; 23 import android.bluetooth.BluetoothProfile; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.os.HandlerThread; 27 28 import androidx.test.InstrumentationRegistry; 29 import androidx.test.filters.MediumTest; 30 import androidx.test.runner.AndroidJUnit4; 31 32 import com.android.bluetooth.TestUtils; 33 import com.android.bluetooth.btservice.AdapterService; 34 import com.android.internal.R; 35 36 import org.hamcrest.core.IsInstanceOf; 37 import org.junit.After; 38 import org.junit.Assert; 39 import org.junit.Assume; 40 import org.junit.Before; 41 import org.junit.Test; 42 import org.junit.runner.RunWith; 43 import org.mockito.ArgumentCaptor; 44 import org.mockito.Mock; 45 import org.mockito.MockitoAnnotations; 46 47 @MediumTest 48 @RunWith(AndroidJUnit4.class) 49 public class HearingAidStateMachineTest { 50 private BluetoothAdapter mAdapter; 51 private Context mTargetContext; 52 private HandlerThread mHandlerThread; 53 private HearingAidStateMachine mHearingAidStateMachine; 54 private BluetoothDevice mTestDevice; 55 private static final int TIMEOUT_MS = 1000; 56 57 @Mock private AdapterService mAdapterService; 58 @Mock private HearingAidService mHearingAidService; 59 @Mock private HearingAidNativeInterface mHearingAidNativeInterface; 60 61 @Before setUp()62 public void setUp() throws Exception { 63 mTargetContext = InstrumentationRegistry.getTargetContext(); 64 Assume.assumeTrue("Ignore test when HearingAidService is not enabled", 65 mTargetContext.getResources().getBoolean( 66 R.bool.config_hearing_aid_profile_supported)); 67 // Set up mocks and test assets 68 MockitoAnnotations.initMocks(this); 69 TestUtils.setAdapterService(mAdapterService); 70 71 mAdapter = BluetoothAdapter.getDefaultAdapter(); 72 73 // Get a device for testing 74 mTestDevice = mAdapter.getRemoteDevice("00:01:02:03:04:05"); 75 76 // Set up thread and looper 77 mHandlerThread = new HandlerThread("HearingAidStateMachineTestHandlerThread"); 78 mHandlerThread.start(); 79 mHearingAidStateMachine = new HearingAidStateMachine(mTestDevice, mHearingAidService, 80 mHearingAidNativeInterface, mHandlerThread.getLooper()); 81 // Override the timeout value to speed up the test 82 mHearingAidStateMachine.sConnectTimeoutMs = 1000; // 1s 83 mHearingAidStateMachine.start(); 84 } 85 86 @After tearDown()87 public void tearDown() throws Exception { 88 if (!mTargetContext.getResources().getBoolean( 89 R.bool.config_hearing_aid_profile_supported)) { 90 return; 91 } 92 mHearingAidStateMachine.doQuit(); 93 mHandlerThread.quit(); 94 TestUtils.clearAdapterService(mAdapterService); 95 } 96 97 /** 98 * Test that default state is disconnected 99 */ 100 @Test testDefaultDisconnectedState()101 public void testDefaultDisconnectedState() { 102 Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED, 103 mHearingAidStateMachine.getConnectionState()); 104 } 105 106 /** 107 * Allow/disallow connection to any device. 108 * 109 * @param allow if true, connection is allowed 110 */ allowConnection(boolean allow)111 private void allowConnection(boolean allow) { 112 doReturn(allow).when(mHearingAidService).okToConnect(any(BluetoothDevice.class)); 113 } 114 115 /** 116 * Test that an incoming connection with low priority is rejected 117 */ 118 @Test testIncomingPriorityReject()119 public void testIncomingPriorityReject() { 120 allowConnection(false); 121 122 // Inject an event for when incoming connection is requested 123 HearingAidStackEvent connStCh = 124 new HearingAidStackEvent(HearingAidStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED); 125 connStCh.device = mTestDevice; 126 connStCh.valueInt1 = HearingAidStackEvent.CONNECTION_STATE_CONNECTED; 127 mHearingAidStateMachine.sendMessage(HearingAidStateMachine.STACK_EVENT, connStCh); 128 129 // Verify that no connection state broadcast is executed 130 verify(mHearingAidService, after(TIMEOUT_MS).never()).sendBroadcast(any(Intent.class), 131 anyString()); 132 // Check that we are in Disconnected state 133 Assert.assertThat(mHearingAidStateMachine.getCurrentState(), 134 IsInstanceOf.instanceOf(HearingAidStateMachine.Disconnected.class)); 135 } 136 137 /** 138 * Test that an incoming connection with high priority is accepted 139 */ 140 @Test testIncomingPriorityAccept()141 public void testIncomingPriorityAccept() { 142 allowConnection(true); 143 144 // Inject an event for when incoming connection is requested 145 HearingAidStackEvent connStCh = 146 new HearingAidStackEvent(HearingAidStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED); 147 connStCh.device = mTestDevice; 148 connStCh.valueInt1 = HearingAidStackEvent.CONNECTION_STATE_CONNECTING; 149 mHearingAidStateMachine.sendMessage(HearingAidStateMachine.STACK_EVENT, connStCh); 150 151 // Verify that one connection state broadcast is executed 152 ArgumentCaptor<Intent> intentArgument1 = ArgumentCaptor.forClass(Intent.class); 153 verify(mHearingAidService, timeout(TIMEOUT_MS).times(1)).sendBroadcast( 154 intentArgument1.capture(), anyString()); 155 Assert.assertEquals(BluetoothProfile.STATE_CONNECTING, 156 intentArgument1.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1)); 157 158 // Check that we are in Connecting state 159 Assert.assertThat(mHearingAidStateMachine.getCurrentState(), 160 IsInstanceOf.instanceOf(HearingAidStateMachine.Connecting.class)); 161 162 // Send a message to trigger connection completed 163 HearingAidStackEvent connCompletedEvent = 164 new HearingAidStackEvent(HearingAidStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED); 165 connCompletedEvent.device = mTestDevice; 166 connCompletedEvent.valueInt1 = HearingAidStackEvent.CONNECTION_STATE_CONNECTED; 167 mHearingAidStateMachine.sendMessage(HearingAidStateMachine.STACK_EVENT, connCompletedEvent); 168 169 // Verify that the expected number of broadcasts are executed: 170 // - two calls to broadcastConnectionState(): Disconnected -> Conecting -> Connected 171 ArgumentCaptor<Intent> intentArgument2 = ArgumentCaptor.forClass(Intent.class); 172 verify(mHearingAidService, timeout(TIMEOUT_MS).times(2)).sendBroadcast( 173 intentArgument2.capture(), anyString()); 174 // Check that we are in Connected state 175 Assert.assertThat(mHearingAidStateMachine.getCurrentState(), 176 IsInstanceOf.instanceOf(HearingAidStateMachine.Connected.class)); 177 } 178 179 /** 180 * Test that an outgoing connection times out 181 */ 182 @Test testOutgoingTimeout()183 public void testOutgoingTimeout() { 184 allowConnection(true); 185 doReturn(true).when(mHearingAidNativeInterface).connectHearingAid(any( 186 BluetoothDevice.class)); 187 doReturn(true).when(mHearingAidNativeInterface).disconnectHearingAid(any( 188 BluetoothDevice.class)); 189 when(mHearingAidService.isConnectedPeerDevices(mTestDevice)).thenReturn(true); 190 191 // Send a connect request 192 mHearingAidStateMachine.sendMessage(HearingAidStateMachine.CONNECT, mTestDevice); 193 194 // Verify that one connection state broadcast is executed 195 ArgumentCaptor<Intent> intentArgument1 = ArgumentCaptor.forClass(Intent.class); 196 verify(mHearingAidService, timeout(TIMEOUT_MS).times(1)).sendBroadcast( 197 intentArgument1.capture(), 198 anyString()); 199 Assert.assertEquals(BluetoothProfile.STATE_CONNECTING, 200 intentArgument1.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1)); 201 202 // Check that we are in Connecting state 203 Assert.assertThat(mHearingAidStateMachine.getCurrentState(), 204 IsInstanceOf.instanceOf(HearingAidStateMachine.Connecting.class)); 205 206 // Verify that one connection state broadcast is executed 207 ArgumentCaptor<Intent> intentArgument2 = ArgumentCaptor.forClass(Intent.class); 208 verify(mHearingAidService, timeout(HearingAidStateMachine.sConnectTimeoutMs * 2).times( 209 2)).sendBroadcast(intentArgument2.capture(), anyString()); 210 Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED, 211 intentArgument2.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1)); 212 213 // Check that we are in Disconnected state 214 Assert.assertThat(mHearingAidStateMachine.getCurrentState(), 215 IsInstanceOf.instanceOf(HearingAidStateMachine.Disconnected.class)); 216 verify(mHearingAidNativeInterface).addToWhiteList(eq(mTestDevice)); 217 } 218 219 /** 220 * Test that an incoming connection times out 221 */ 222 @Test testIncomingTimeout()223 public void testIncomingTimeout() { 224 allowConnection(true); 225 doReturn(true).when(mHearingAidNativeInterface).connectHearingAid(any( 226 BluetoothDevice.class)); 227 doReturn(true).when(mHearingAidNativeInterface).disconnectHearingAid(any( 228 BluetoothDevice.class)); 229 when(mHearingAidService.isConnectedPeerDevices(mTestDevice)).thenReturn(true); 230 231 // Inject an event for when incoming connection is requested 232 HearingAidStackEvent connStCh = 233 new HearingAidStackEvent(HearingAidStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED); 234 connStCh.device = mTestDevice; 235 connStCh.valueInt1 = HearingAidStackEvent.CONNECTION_STATE_CONNECTING; 236 mHearingAidStateMachine.sendMessage(HearingAidStateMachine.STACK_EVENT, connStCh); 237 238 // Verify that one connection state broadcast is executed 239 ArgumentCaptor<Intent> intentArgument1 = ArgumentCaptor.forClass(Intent.class); 240 verify(mHearingAidService, timeout(TIMEOUT_MS).times(1)).sendBroadcast( 241 intentArgument1.capture(), 242 anyString()); 243 Assert.assertEquals(BluetoothProfile.STATE_CONNECTING, 244 intentArgument1.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1)); 245 246 // Check that we are in Connecting state 247 Assert.assertThat(mHearingAidStateMachine.getCurrentState(), 248 IsInstanceOf.instanceOf(HearingAidStateMachine.Connecting.class)); 249 250 // Verify that one connection state broadcast is executed 251 ArgumentCaptor<Intent> intentArgument2 = ArgumentCaptor.forClass(Intent.class); 252 verify(mHearingAidService, timeout(HearingAidStateMachine.sConnectTimeoutMs * 2).times( 253 2)).sendBroadcast(intentArgument2.capture(), anyString()); 254 Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED, 255 intentArgument2.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1)); 256 257 // Check that we are in Disconnected state 258 Assert.assertThat(mHearingAidStateMachine.getCurrentState(), 259 IsInstanceOf.instanceOf(HearingAidStateMachine.Disconnected.class)); 260 verify(mHearingAidNativeInterface).addToWhiteList(eq(mTestDevice)); 261 } 262 } 263