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 android.bluetooth.BluetoothDevice;
20 import android.bluetooth.BluetoothHeadset;
21 import android.bluetooth.BluetoothHearingAid;
22 import android.content.ContentResolver;
23 import android.os.Parcel;
24 import android.telecom.Log;
25 import android.test.suitebuilder.annotation.SmallTest;
26 
27 import com.android.internal.os.SomeArgs;
28 import com.android.server.telecom.BluetoothHeadsetProxy;
29 import com.android.server.telecom.TelecomSystem;
30 import com.android.server.telecom.Timeouts;
31 import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
32 import com.android.server.telecom.bluetooth.BluetoothRouteManager;
33 
34 import org.junit.After;
35 import org.junit.Before;
36 import org.junit.Test;
37 import org.junit.runner.RunWith;
38 import org.junit.runners.JUnit4;
39 import org.mockito.Mock;
40 
41 import java.util.Arrays;
42 import java.util.List;
43 import java.util.stream.Collectors;
44 import java.util.stream.Stream;
45 
46 import static org.junit.Assert.assertEquals;
47 import static org.mockito.ArgumentMatchers.any;
48 import static org.mockito.ArgumentMatchers.nullable;
49 import static org.mockito.Matchers.eq;
50 import static org.mockito.Mockito.reset;
51 import static org.mockito.Mockito.times;
52 import static org.mockito.Mockito.verify;
53 import static org.mockito.Mockito.when;
54 
55 @RunWith(JUnit4.class)
56 public class BluetoothRouteManagerTest extends TelecomTestCase {
57     private static final int TEST_TIMEOUT = 1000;
58     static final BluetoothDevice DEVICE1 = makeBluetoothDevice("00:00:00:00:00:01");
59     static final BluetoothDevice DEVICE2 = makeBluetoothDevice("00:00:00:00:00:02");
60     static final BluetoothDevice DEVICE3 = makeBluetoothDevice("00:00:00:00:00:03");
61     static final BluetoothDevice HEARING_AID_DEVICE = makeBluetoothDevice("00:00:00:00:00:04");
62 
63     @Mock private BluetoothDeviceManager mDeviceManager;
64     @Mock private BluetoothHeadsetProxy mHeadsetProxy;
65     @Mock private BluetoothHearingAid mBluetoothHearingAid;
66     @Mock private Timeouts.Adapter mTimeoutsAdapter;
67     @Mock private BluetoothRouteManager.BluetoothStateListener mListener;
68 
69     @Override
70     @Before
setUp()71     public void setUp() throws Exception {
72         super.setUp();
73     }
74 
75     @Override
76     @After
tearDown()77     public void tearDown() throws Exception {
78         super.tearDown();
79     }
80 
81     @SmallTest
82     @Test
testConnectHfpRetryWhileNotConnected()83     public void testConnectHfpRetryWhileNotConnected() {
84         BluetoothRouteManager sm = setupStateMachine(
85                 BluetoothRouteManager.AUDIO_OFF_STATE_NAME, null);
86         setupConnectedDevices(new BluetoothDevice[]{DEVICE1}, null, null, null);
87         when(mTimeoutsAdapter.getRetryBluetoothConnectAudioBackoffMillis(
88                 nullable(ContentResolver.class))).thenReturn(0L);
89         when(mHeadsetProxy.connectAudio()).thenReturn(false);
90         executeRoutingAction(sm, BluetoothRouteManager.CONNECT_HFP, DEVICE1.getAddress());
91         // Wait 3 times: for the first connection attempt, the retry attempt,
92         // the second retry, and once more to make sure there are only three attempts.
93         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
94         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
95         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
96         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
97         verifyConnectionAttempt(DEVICE1, 3);
98         assertEquals(BluetoothRouteManager.AUDIO_OFF_STATE_NAME, sm.getCurrentState().getName());
99         sm.getHandler().removeMessages(BluetoothRouteManager.CONNECTION_TIMEOUT);
100         sm.quitNow();
101     }
102 
103     @SmallTest
104     @Test
testAmbiguousActiveDevice()105     public void testAmbiguousActiveDevice() {
106         BluetoothRouteManager sm = setupStateMachine(
107                 BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX, DEVICE1);
108         setupConnectedDevices(new BluetoothDevice[]{DEVICE1},
109                 new BluetoothDevice[]{HEARING_AID_DEVICE}, DEVICE1, HEARING_AID_DEVICE);
110         sm.onActiveDeviceChanged(DEVICE1, false);
111         sm.onActiveDeviceChanged(HEARING_AID_DEVICE, true);
112         executeRoutingAction(sm, BluetoothRouteManager.BT_AUDIO_LOST, DEVICE1.getAddress());
113 
114         verifyConnectionAttempt(HEARING_AID_DEVICE, 0);
115         verifyConnectionAttempt(DEVICE1, 0);
116         assertEquals(BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX
117                         + ":" + HEARING_AID_DEVICE.getAddress(),
118                 sm.getCurrentState().getName());
119         sm.quitNow();
120     }
121 
122     @SmallTest
123     @Test
testAudioOnDeviceWithScoOffActiveDevice()124     public void testAudioOnDeviceWithScoOffActiveDevice() {
125         BluetoothRouteManager sm = setupStateMachine(
126                 BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX, DEVICE1);
127         setupConnectedDevices(new BluetoothDevice[]{DEVICE1}, null, DEVICE1, null);
128         when(mHeadsetProxy.getAudioState(DEVICE1))
129                 .thenReturn(BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
130         executeRoutingAction(sm, BluetoothRouteManager.BT_AUDIO_LOST, DEVICE1.getAddress());
131 
132         verifyConnectionAttempt(DEVICE1, 0);
133         assertEquals(BluetoothRouteManager.AUDIO_OFF_STATE_NAME,
134                 sm.getCurrentState().getName());
135         sm.quitNow();
136     }
137 
138     @SmallTest
139     @Test
testConnectHfpRetryWhileConnectedToAnotherDevice()140     public void testConnectHfpRetryWhileConnectedToAnotherDevice() {
141         BluetoothRouteManager sm = setupStateMachine(
142                 BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX, DEVICE1);
143         setupConnectedDevices(new BluetoothDevice[]{DEVICE1, DEVICE2}, null, null, null);
144         when(mTimeoutsAdapter.getRetryBluetoothConnectAudioBackoffMillis(
145                 nullable(ContentResolver.class))).thenReturn(0L);
146         when(mHeadsetProxy.connectAudio()).thenReturn(false);
147         executeRoutingAction(sm, BluetoothRouteManager.CONNECT_HFP, DEVICE2.getAddress());
148         // Wait 3 times: the first connection attempt is accounted for in executeRoutingAction,
149         // so wait twice for the retry attempt, again to make sure there are only three attempts,
150         // and once more for good luck.
151         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
152         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
153         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
154         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
155         verifyConnectionAttempt(DEVICE2, 3);
156         assertEquals(BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX
157                         + ":" + DEVICE1.getAddress(),
158                 sm.getCurrentState().getName());
159         sm.getHandler().removeMessages(BluetoothRouteManager.CONNECTION_TIMEOUT);
160         sm.quitNow();
161     }
162 
setupStateMachine(String initialState, BluetoothDevice initialDevice)163     private BluetoothRouteManager setupStateMachine(String initialState,
164             BluetoothDevice initialDevice) {
165         resetMocks();
166         BluetoothRouteManager sm = new BluetoothRouteManager(mContext,
167                 new TelecomSystem.SyncRoot() { }, mDeviceManager, mTimeoutsAdapter);
168         sm.setListener(mListener);
169         sm.setInitialStateForTesting(initialState, initialDevice);
170         waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
171         resetMocks();
172         return sm;
173     }
174 
setupConnectedDevices(BluetoothDevice[] hfpDevices, BluetoothDevice[] hearingAidDevices, BluetoothDevice hfpActiveDevice, BluetoothDevice hearingAidActiveDevice)175     private void setupConnectedDevices(BluetoothDevice[] hfpDevices,
176             BluetoothDevice[] hearingAidDevices,
177             BluetoothDevice hfpActiveDevice, BluetoothDevice hearingAidActiveDevice) {
178         if (hfpDevices == null) hfpDevices = new BluetoothDevice[]{};
179         if (hearingAidDevices == null) hearingAidDevices = new BluetoothDevice[]{};
180 
181         when(mDeviceManager.getNumConnectedDevices()).thenReturn(
182                 hfpDevices.length + hearingAidDevices.length);
183         List<BluetoothDevice> allDevices = Stream.concat(
184                 Arrays.stream(hfpDevices), Arrays.stream(hearingAidDevices))
185                 .collect(Collectors.toList());
186 
187         when(mDeviceManager.getConnectedDevices()).thenReturn(allDevices);
188         when(mHeadsetProxy.getConnectedDevices()).thenReturn(Arrays.asList(hfpDevices));
189         when(mHeadsetProxy.getActiveDevice()).thenReturn(hfpActiveDevice);
190         when(mHeadsetProxy.getAudioState(hfpActiveDevice))
191                 .thenReturn(BluetoothHeadset.STATE_AUDIO_CONNECTED);
192 
193         when(mBluetoothHearingAid.getConnectedDevices())
194                 .thenReturn(Arrays.asList(hearingAidDevices));
195         when(mBluetoothHearingAid.getActiveDevices())
196                 .thenReturn(Arrays.asList(hearingAidActiveDevice, null));
197     }
198 
executeRoutingAction(BluetoothRouteManager brm, int message, String device)199     static void executeRoutingAction(BluetoothRouteManager brm, int message, String
200             device) {
201         SomeArgs args = SomeArgs.obtain();
202         args.arg1 = Log.createSubsession();
203         args.arg2 = device;
204         brm.sendMessage(message, args);
205         waitForHandlerAction(brm.getHandler(), TEST_TIMEOUT);
206     }
207 
makeBluetoothDevice(String address)208     public static BluetoothDevice makeBluetoothDevice(String address) {
209         Parcel p1 = Parcel.obtain();
210         p1.writeString(address);
211         p1.setDataPosition(0);
212         BluetoothDevice device = BluetoothDevice.CREATOR.createFromParcel(p1);
213         p1.recycle();
214         return device;
215     }
216 
resetMocks()217     private void resetMocks() {
218         reset(mDeviceManager, mListener, mHeadsetProxy, mTimeoutsAdapter);
219         when(mDeviceManager.getHeadsetService()).thenReturn(mHeadsetProxy);
220         when(mDeviceManager.getHearingAidService()).thenReturn(mBluetoothHearingAid);
221         when(mHeadsetProxy.connectAudio()).thenReturn(true);
222         when(mHeadsetProxy.setActiveDevice(nullable(BluetoothDevice.class))).thenReturn(true);
223         when(mTimeoutsAdapter.getRetryBluetoothConnectAudioBackoffMillis(
224                 nullable(ContentResolver.class))).thenReturn(100000L);
225         when(mTimeoutsAdapter.getBluetoothPendingTimeoutMillis(
226                 nullable(ContentResolver.class))).thenReturn(100000L);
227     }
228 
verifyConnectionAttempt(BluetoothDevice device, int numTimes)229     private void verifyConnectionAttempt(BluetoothDevice device, int numTimes) {
230         verify(mDeviceManager, times(numTimes)).connectAudio(device.getAddress());
231     }
232 }
233