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