• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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.server.telecom.tests;
18 
19 import static org.junit.Assert.assertEquals;
20 import static org.mockito.ArgumentMatchers.anyBoolean;
21 import static org.mockito.ArgumentMatchers.anyString;
22 import static org.mockito.ArgumentMatchers.eq;
23 import static org.mockito.ArgumentMatchers.nullable;
24 import static org.mockito.Mockito.never;
25 import static org.mockito.Mockito.reset;
26 import static org.mockito.Mockito.times;
27 import static org.mockito.Mockito.verify;
28 import static org.mockito.Mockito.when;
29 
30 import android.bluetooth.BluetoothAdapter;
31 import android.bluetooth.BluetoothDevice;
32 import android.bluetooth.BluetoothHeadset;
33 import android.bluetooth.BluetoothHearingAid;
34 import android.bluetooth.BluetoothLeAudio;
35 import android.bluetooth.BluetoothProfile;
36 import android.bluetooth.BluetoothStatusCodes;
37 import android.content.ContentResolver;
38 import android.media.AudioDeviceInfo;
39 import android.os.Parcel;
40 import android.telecom.Log;
41 
42 import androidx.test.filters.SmallTest;
43 
44 import com.android.internal.os.SomeArgs;
45 import com.android.server.telecom.CallAudioCommunicationDeviceTracker;
46 import com.android.server.telecom.TelecomSystem;
47 import com.android.server.telecom.Timeouts;
48 import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
49 import com.android.server.telecom.bluetooth.BluetoothRouteManager;
50 
51 import org.junit.After;
52 import org.junit.Before;
53 import org.junit.Test;
54 import org.junit.runner.RunWith;
55 import org.junit.runners.JUnit4;
56 import org.mockito.Mock;
57 
58 import java.util.Arrays;
59 import java.util.List;
60 import java.util.stream.Collectors;
61 import java.util.stream.Stream;
62 
63 @RunWith(JUnit4.class)
64 public class BluetoothRouteManagerTest extends TelecomTestCase {
65     private static final int TEST_TIMEOUT = 1000;
66     static final BluetoothDevice DEVICE1 = makeBluetoothDevice("00:00:00:00:00:01");
67     static final BluetoothDevice DEVICE2 = makeBluetoothDevice("00:00:00:00:00:02");
68     static final BluetoothDevice DEVICE3 = makeBluetoothDevice("00:00:00:00:00:03");
69     static final BluetoothDevice HEARING_AID_DEVICE_LEFT = makeBluetoothDevice("CA:FE:DE:CA:00:01");
70     static final BluetoothDevice HEARING_AID_DEVICE_RIGHT =
71       makeBluetoothDevice("CA:FE:DE:CA:00:02");
72     // See HearingAidService#getActiveDevices
73     // Note: It is really important that the left HA is the first one. The left HA is always
74     // in the first index (0) and the right one in the second index (1).
75     static final BluetoothDevice[] HEARING_AIDS =
76       new BluetoothDevice[]{HEARING_AID_DEVICE_LEFT, HEARING_AID_DEVICE_RIGHT};
77 
78     @Mock private BluetoothAdapter mBluetoothAdapter;
79     @Mock private BluetoothDeviceManager mDeviceManager;
80     @Mock private BluetoothHeadset mBluetoothHeadset;
81     @Mock private BluetoothHearingAid mBluetoothHearingAid;
82     @Mock private BluetoothLeAudio mBluetoothLeAudio;
83     @Mock private Timeouts.Adapter mTimeoutsAdapter;
84     @Mock private BluetoothRouteManager.BluetoothStateListener mListener;
85     @Mock private CallAudioCommunicationDeviceTracker mCommunicationDeviceTracker;
86 
87     @Override
88     @Before
setUp()89     public void setUp() throws Exception {
90         super.setUp();
91     }
92 
93     @Override
94     @After
tearDown()95     public void tearDown() throws Exception {
96         super.tearDown();
97     }
98 
99     @SmallTest
100     @Test
testConnectLeftHearingAidWhenLeftIsActive()101     public void testConnectLeftHearingAidWhenLeftIsActive() {
102         BluetoothRouteManager sm = setupStateMachine(
103                 BluetoothRouteManager.AUDIO_OFF_STATE_NAME, HEARING_AID_DEVICE_LEFT);
104         sm.onActiveDeviceChanged(HEARING_AID_DEVICE_LEFT,
105             BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID);
106         when(mDeviceManager.connectAudio(anyString(), anyBoolean())).thenReturn(true);
107         when(mDeviceManager.isHearingAidSetAsCommunicationDevice()).thenReturn(true);
108         when(mCommunicationDeviceTracker.isAudioDeviceSetForType(
109                 eq(AudioDeviceInfo.TYPE_HEARING_AID))).thenReturn(true);
110 
111         setupConnectedDevices(null, HEARING_AIDS, null, null, HEARING_AIDS, null);
112         when(mBluetoothHeadset.getAudioState(nullable(BluetoothDevice.class)))
113           .thenReturn(BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
114 
115         executeRoutingAction(sm,
116             BluetoothRouteManager.NEW_DEVICE_CONNECTED, HEARING_AID_DEVICE_LEFT.getAddress());
117 
118         executeRoutingAction(sm,
119             BluetoothRouteManager.CONNECT_BT, HEARING_AID_DEVICE_LEFT.getAddress());
120 
121         assertEquals(BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX
122             + ":" + HEARING_AID_DEVICE_LEFT.getAddress(), sm.getCurrentState().getName());
123 
124         sm.quitNow();
125     }
126 
127     @SmallTest
128     @Test
testConnectRightHearingAidWhenLeftIsActive()129     public void testConnectRightHearingAidWhenLeftIsActive() {
130         BluetoothRouteManager sm = setupStateMachine(
131                 BluetoothRouteManager.AUDIO_OFF_STATE_NAME, HEARING_AID_DEVICE_RIGHT);
132         sm.onActiveDeviceChanged(HEARING_AID_DEVICE_LEFT,
133             BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID);
134         when(mDeviceManager.connectAudio(anyString(), anyBoolean())).thenReturn(true);
135         when(mDeviceManager.isHearingAidSetAsCommunicationDevice()).thenReturn(true);
136         when(mCommunicationDeviceTracker.isAudioDeviceSetForType(
137                 eq(AudioDeviceInfo.TYPE_HEARING_AID))).thenReturn(true);
138 
139         setupConnectedDevices(null, HEARING_AIDS, null, null, HEARING_AIDS, null);
140         when(mBluetoothHeadset.getAudioState(nullable(BluetoothDevice.class)))
141           .thenReturn(BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
142 
143         executeRoutingAction(sm,
144             BluetoothRouteManager.NEW_DEVICE_CONNECTED, HEARING_AID_DEVICE_LEFT.getAddress());
145 
146         executeRoutingAction(sm,
147             BluetoothRouteManager.CONNECT_BT, HEARING_AID_DEVICE_LEFT.getAddress());
148 
149         assertEquals(BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX
150             + ":" + HEARING_AID_DEVICE_LEFT.getAddress(), sm.getCurrentState().getName());
151 
152         sm.quitNow();
153     }
154 
155     @SmallTest
156     @Test
testConnectBtRetryWhileNotConnected()157     public void testConnectBtRetryWhileNotConnected() {
158         BluetoothRouteManager sm = setupStateMachine(
159                 BluetoothRouteManager.AUDIO_OFF_STATE_NAME, null);
160         setupConnectedDevices(new BluetoothDevice[]{DEVICE1}, null, null, null, null, null);
161         when(mTimeoutsAdapter.getRetryBluetoothConnectAudioBackoffMillis(
162                 nullable(ContentResolver.class))).thenReturn(0L);
163         when(mBluetoothHeadset.connectAudio()).thenReturn(BluetoothStatusCodes.ERROR_UNKNOWN);
164         executeRoutingAction(sm, BluetoothRouteManager.CONNECT_BT, DEVICE1.getAddress());
165         // Wait 3 times: for the first connection attempt, the retry attempt,
166         // the second retry, and once more to make sure there are only three attempts.
167         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
168         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
169         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
170         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
171         verifyConnectionAttempt(DEVICE1, 3);
172         assertEquals(BluetoothRouteManager.AUDIO_OFF_STATE_NAME, sm.getCurrentState().getName());
173         sm.getHandler().removeMessages(BluetoothRouteManager.CONNECTION_TIMEOUT);
174         sm.quitNow();
175     }
176 
177     @SmallTest
178     @Test
testAmbiguousActiveDevice()179     public void testAmbiguousActiveDevice() {
180         BluetoothRouteManager sm = setupStateMachine(
181                 BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX, DEVICE1);
182         setupConnectedDevices(new BluetoothDevice[]{DEVICE1},
183                 HEARING_AIDS, new BluetoothDevice[]{DEVICE2},
184                 DEVICE1,  HEARING_AIDS, DEVICE2);
185         sm.onActiveDeviceChanged(DEVICE1, BluetoothDeviceManager.DEVICE_TYPE_HEADSET);
186         sm.onActiveDeviceChanged(DEVICE2, BluetoothDeviceManager.DEVICE_TYPE_LE_AUDIO);
187         sm.onActiveDeviceChanged(HEARING_AID_DEVICE_LEFT,
188                 BluetoothDeviceManager.DEVICE_TYPE_HEARING_AID);
189         executeRoutingAction(sm, BluetoothRouteManager.BT_AUDIO_LOST, DEVICE1.getAddress());
190 
191         verifyConnectionAttempt(HEARING_AID_DEVICE_LEFT, 0);
192         verifyConnectionAttempt(DEVICE1, 0);
193         verifyConnectionAttempt(DEVICE2, 0);
194         assertEquals(BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX
195                         + ":" + DEVICE1.getAddress(),
196                 sm.getCurrentState().getName());
197         sm.quitNow();
198     }
199 
200     @SmallTest
201     @Test
testAudioOnDeviceWithScoOffActiveDevice()202     public void testAudioOnDeviceWithScoOffActiveDevice() {
203         BluetoothRouteManager sm = setupStateMachine(
204                 BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX, DEVICE1);
205         setupConnectedDevices(new BluetoothDevice[]{DEVICE1}, null, null, DEVICE1, null, null);
206         when(mBluetoothHeadset.getAudioState(DEVICE1))
207                 .thenReturn(BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
208         executeRoutingAction(sm, BluetoothRouteManager.BT_AUDIO_LOST, DEVICE1.getAddress());
209 
210         verifyConnectionAttempt(DEVICE1, 0);
211         assertEquals(BluetoothRouteManager.AUDIO_OFF_STATE_NAME,
212                 sm.getCurrentState().getName());
213         sm.quitNow();
214     }
215 
216     @SmallTest
217     @Test
testConnectBtRetryWhileConnectedToAnotherDevice()218     public void testConnectBtRetryWhileConnectedToAnotherDevice() {
219         BluetoothRouteManager sm = setupStateMachine(
220                 BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX, DEVICE1);
221         setupConnectedDevices(new BluetoothDevice[]{DEVICE1, DEVICE2}, null, null, null, null,
222                               null);
223         when(mTimeoutsAdapter.getRetryBluetoothConnectAudioBackoffMillis(
224                 nullable(ContentResolver.class))).thenReturn(0L);
225         when(mBluetoothHeadset.connectAudio()).thenReturn(BluetoothStatusCodes.ERROR_UNKNOWN);
226         executeRoutingAction(sm, BluetoothRouteManager.CONNECT_BT, DEVICE2.getAddress());
227         // Wait 3 times: the first connection attempt is accounted for in executeRoutingAction,
228         // so wait twice for the retry attempt, again to make sure there are only three attempts,
229         // and once more for good luck.
230         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
231         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
232         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
233         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
234         verifyConnectionAttempt(DEVICE2, 3);
235         assertEquals(BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX
236                         + ":" + DEVICE1.getAddress(),
237                 sm.getCurrentState().getName());
238         sm.getHandler().removeMessages(BluetoothRouteManager.CONNECTION_TIMEOUT);
239         sm.quitNow();
240     }
241 
242     @SmallTest
243     @Test
testSkipInactiveBtDeviceWhenEvaluateActualState()244     public void testSkipInactiveBtDeviceWhenEvaluateActualState() {
245         BluetoothRouteManager sm = setupStateMachine(
246                 BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX, HEARING_AID_DEVICE_LEFT);
247         setupConnectedDevices(null, HEARING_AIDS,
248                 null, null, HEARING_AIDS, null);
249         executeRoutingAction(sm, BluetoothRouteManager.BT_AUDIO_LOST,
250                 HEARING_AID_DEVICE_LEFT.getAddress());
251         assertEquals(BluetoothRouteManager.AUDIO_OFF_STATE_NAME, sm.getCurrentState().getName());
252         sm.quitNow();
253     }
254 
255     @SmallTest
256     @Test
testConnectBtWithoutAddress_SwitchingBtDeviceFlag()257     public void testConnectBtWithoutAddress_SwitchingBtDeviceFlag() {
258         when(mFeatureFlags.resolveSwitchingBtDevicesComputation()).thenReturn(true);
259         verifyConnectBtWithoutAddress();
260     }
261 
262     @SmallTest
263     @Test
testConnectBtWithoutAddress_SwitchingBtDeviceFlagDisabled()264     public void testConnectBtWithoutAddress_SwitchingBtDeviceFlagDisabled() {
265         verifyConnectBtWithoutAddress();
266     }
267 
verifyConnectBtWithoutAddress()268     private void verifyConnectBtWithoutAddress() {
269         when(mFeatureFlags.useActualAddressToEnterConnectingState()).thenReturn(true);
270         BluetoothRouteManager sm = setupStateMachine(
271                 BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX, DEVICE1);
272         setupConnectedDevices(new BluetoothDevice[]{DEVICE1, DEVICE2}, null, null, null, null,
273                 null);
274         when(mTimeoutsAdapter.getRetryBluetoothConnectAudioBackoffMillis(
275                 nullable(ContentResolver.class))).thenReturn(0L);
276         when(mBluetoothHeadset.connectAudio()).thenReturn(BluetoothStatusCodes.ERROR_UNKNOWN);
277         executeRoutingAction(sm, BluetoothRouteManager.CONNECT_BT, null);
278         // Wait 3 times: the first connection attempt is accounted for in executeRoutingAction,
279         // so wait twice for the retry attempt, again to make sure there are only three attempts,
280         // and once more for good luck.
281         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
282         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
283         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
284         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
285         // We should not expect explicit connection attempt (BluetoothDeviceManager#connectAudio)
286         // as the device is already "connected" as per how the state machine was initialized.
287         if (mFeatureFlags.resolveSwitchingBtDevicesComputation()) {
288             verify(mDeviceManager, never()).disconnectAudio();
289         } else {
290             // Legacy behavior
291             verifyConnectionAttempt(DEVICE1, 1);
292             verify(mDeviceManager, times(1)).disconnectAudio();
293         }
294         assertEquals(BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX
295                         + ":" + DEVICE1.getAddress(),
296                 sm.getCurrentState().getName());
297         sm.getHandler().removeMessages(BluetoothRouteManager.CONNECTION_TIMEOUT);
298         sm.quitNow();
299     }
300 
setupStateMachine(String initialState, BluetoothDevice initialDevice)301     private BluetoothRouteManager setupStateMachine(String initialState,
302             BluetoothDevice initialDevice) {
303         resetMocks();
304         BluetoothRouteManager sm = new BluetoothRouteManager(mContext,
305                 new TelecomSystem.SyncRoot() { }, mDeviceManager,
306                 mTimeoutsAdapter, mCommunicationDeviceTracker, mFeatureFlags,
307                 mContext.getMainLooper());
308         sm.setListener(mListener);
309         sm.setInitialStateForTesting(initialState, initialDevice);
310         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
311         resetMocks();
312         return sm;
313     }
314 
setupConnectedDevices(BluetoothDevice[] hfpDevices, BluetoothDevice[] hearingAidDevices, BluetoothDevice[] leAudioDevices, BluetoothDevice hfpActiveDevice, BluetoothDevice[] hearingAidActiveDevices, BluetoothDevice leAudioDevice)315     private void setupConnectedDevices(BluetoothDevice[] hfpDevices,
316             BluetoothDevice[] hearingAidDevices, BluetoothDevice[] leAudioDevices,
317             BluetoothDevice hfpActiveDevice, BluetoothDevice[] hearingAidActiveDevices,
318             BluetoothDevice leAudioDevice) {
319         if (hfpDevices == null) hfpDevices = new BluetoothDevice[]{};
320         if (hearingAidDevices == null) hearingAidDevices = new BluetoothDevice[]{};
321         if (hearingAidActiveDevices == null) hearingAidActiveDevices = new BluetoothDevice[]{};
322         if (leAudioDevice == null) leAudioDevices = new BluetoothDevice[]{};
323 
324         when(mDeviceManager.getNumConnectedDevices()).thenReturn(
325                 hfpDevices.length + hearingAidDevices.length + leAudioDevices.length);
326         List<BluetoothDevice> allDevices = Stream.of(
327                 Arrays.stream(hfpDevices), Arrays.stream(hearingAidDevices),
328                 Arrays.stream(leAudioDevices)).flatMap(i -> i).collect(Collectors.toList());
329 
330         when(mDeviceManager.getConnectedDevices()).thenReturn(allDevices);
331         when(mBluetoothHeadset.getConnectedDevices()).thenReturn(Arrays.asList(hfpDevices));
332         when(mBluetoothAdapter.getActiveDevices(eq(BluetoothProfile.HEADSET)))
333                 .thenReturn(Arrays.asList(hfpActiveDevice));
334         when(mBluetoothHeadset.getAudioState(hfpActiveDevice))
335                 .thenReturn(BluetoothHeadset.STATE_AUDIO_CONNECTED);
336 
337         when(mBluetoothHearingAid.getConnectedDevices())
338                 .thenReturn(Arrays.asList(hearingAidDevices));
339         when(mBluetoothAdapter.getActiveDevices(eq(BluetoothProfile.HEARING_AID)))
340                 .thenReturn(Arrays.asList(hearingAidActiveDevices));
341         when(mBluetoothAdapter.getActiveDevices(eq(BluetoothProfile.LE_AUDIO)))
342                 .thenReturn(Arrays.asList(leAudioDevice, null));
343     }
344 
executeRoutingAction(BluetoothRouteManager brm, int message, String device)345     static void executeRoutingAction(BluetoothRouteManager brm, int message, String
346             device) {
347         SomeArgs args = SomeArgs.obtain();
348         args.arg1 = Log.createSubsession();
349         args.arg2 = device;
350         brm.sendMessage(message, args);
351         waitForHandlerAction(brm.getHandler(), TEST_TIMEOUT);
352     }
353 
makeBluetoothDevice(String address)354     public static BluetoothDevice makeBluetoothDevice(String address) {
355         Parcel p1 = Parcel.obtain();
356         p1.writeString(address);
357         p1.setDataPosition(0);
358         BluetoothDevice device = BluetoothDevice.CREATOR.createFromParcel(p1);
359         p1.recycle();
360         return device;
361     }
362 
resetMocks()363     private void resetMocks() {
364         reset(mDeviceManager, mListener, mBluetoothHeadset, mTimeoutsAdapter);
365         when(mDeviceManager.getBluetoothHeadset()).thenReturn(mBluetoothHeadset);
366         when(mDeviceManager.getBluetoothHearingAid()).thenReturn(mBluetoothHearingAid);
367         when(mDeviceManager.getBluetoothAdapter()).thenReturn(mBluetoothAdapter);
368         when(mDeviceManager.getLeAudioService()).thenReturn(mBluetoothLeAudio);
369         when(mBluetoothHeadset.connectAudio()).thenReturn(BluetoothStatusCodes.SUCCESS);
370         when(mBluetoothAdapter.setActiveDevice(nullable(BluetoothDevice.class),
371                 eq(BluetoothAdapter.ACTIVE_DEVICE_ALL))).thenReturn(true);
372         when(mTimeoutsAdapter.getRetryBluetoothConnectAudioBackoffMillis(
373                 nullable(ContentResolver.class))).thenReturn(100000L);
374         when(mTimeoutsAdapter.getBluetoothPendingTimeoutMillis(
375                 nullable(ContentResolver.class))).thenReturn(100000L);
376     }
377 
verifyConnectionAttempt(BluetoothDevice device, int numTimes)378     private void verifyConnectionAttempt(BluetoothDevice device, int numTimes) {
379         verify(mDeviceManager, times(numTimes)).connectAudio(eq(device.getAddress()),
380             anyBoolean());
381     }
382 }
383