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