BluetoothRouteManagerTest.java revision 2afe9027824dbe6d88fb953045e12496b0da4cbc
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
17package com.android.server.telecom.tests;
18
19import android.bluetooth.BluetoothDevice;
20import android.content.ContentResolver;
21import android.os.Parcel;
22import android.telecom.Log;
23import android.test.suitebuilder.annotation.SmallTest;
24
25import com.android.internal.os.SomeArgs;
26import com.android.server.telecom.BluetoothHeadsetProxy;
27import com.android.server.telecom.TelecomSystem;
28import com.android.server.telecom.Timeouts;
29import com.android.server.telecom.bluetooth.BluetoothDeviceManager;
30import com.android.server.telecom.bluetooth.BluetoothRouteManager;
31
32import org.junit.Before;
33import org.junit.Test;
34import org.junit.runner.RunWith;
35import org.junit.runners.JUnit4;
36import org.mockito.Mock;
37
38import java.util.Arrays;
39import java.util.Objects;
40
41import static org.junit.Assert.assertEquals;
42import static org.mockito.ArgumentMatchers.nullable;
43import static org.mockito.Matchers.eq;
44import static org.mockito.Mockito.doAnswer;
45import static org.mockito.Mockito.reset;
46import static org.mockito.Mockito.times;
47import static org.mockito.Mockito.verify;
48import static org.mockito.Mockito.when;
49
50@RunWith(JUnit4.class)
51public class BluetoothRouteManagerTest extends TelecomTestCase {
52    private static final int TEST_TIMEOUT = 1000;
53    static final BluetoothDevice DEVICE1 = makeBluetoothDevice("00:00:00:00:00:01");
54    static final BluetoothDevice DEVICE2 = makeBluetoothDevice("00:00:00:00:00:02");
55    static final BluetoothDevice DEVICE3 = makeBluetoothDevice("00:00:00:00:00:03");
56
57    @Mock private BluetoothDeviceManager mDeviceManager;
58    @Mock private BluetoothHeadsetProxy mHeadsetProxy;
59    @Mock private Timeouts.Adapter mTimeoutsAdapter;
60    @Mock private BluetoothRouteManager.BluetoothStateListener mListener;
61
62    @Override
63    @Before
64    public void setUp() throws Exception {
65        super.setUp();
66    }
67
68    @SmallTest
69    @Test
70    public void testConnectHfpRetryWhileNotConnected() {
71        BluetoothRouteManager sm = setupStateMachine(
72                BluetoothRouteManager.AUDIO_OFF_STATE_NAME, null);
73        setupConnectedDevices(new BluetoothDevice[]{DEVICE1}, null);
74        when(mTimeoutsAdapter.getRetryBluetoothConnectAudioBackoffMillis(
75                nullable(ContentResolver.class))).thenReturn(0L);
76        when(mHeadsetProxy.connectAudio(nullable(String.class))).thenReturn(false);
77        executeRoutingAction(sm, BluetoothRouteManager.CONNECT_HFP, null);
78        // Wait 3 times: for the first connection attempt, the retry attempt,
79        // the second retry, and once more to make sure there are only three attempts.
80        waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
81        waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
82        waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
83        waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
84        verify(mHeadsetProxy, times(3)).connectAudio(DEVICE1.getAddress());
85        assertEquals(BluetoothRouteManager.AUDIO_OFF_STATE_NAME, sm.getCurrentState().getName());
86        sm.getHandler().removeMessages(BluetoothRouteManager.CONNECTION_TIMEOUT);
87        sm.quitNow();
88    }
89
90    @SmallTest
91    @Test
92    public void testConnectHfpRetryWhileConnectedToAnotherDevice() {
93        BluetoothRouteManager sm = setupStateMachine(
94                BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX, DEVICE1);
95        setupConnectedDevices(new BluetoothDevice[]{DEVICE1, DEVICE2}, null);
96        when(mTimeoutsAdapter.getRetryBluetoothConnectAudioBackoffMillis(
97                nullable(ContentResolver.class))).thenReturn(0L);
98        when(mHeadsetProxy.connectAudio(nullable(String.class))).thenReturn(false);
99        executeRoutingAction(sm, BluetoothRouteManager.CONNECT_HFP, DEVICE2.getAddress());
100        // Wait 3 times: the first connection attempt is accounted for in executeRoutingAction,
101        // so wait twice for the retry attempt, again to make sure there are only three attempts,
102        // and once more for good luck.
103        waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
104        waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
105        waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
106        waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
107        verify(mHeadsetProxy, times(3)).connectAudio(DEVICE2.getAddress());
108        assertEquals(BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX
109                        + ":" + DEVICE1.getAddress(),
110                sm.getCurrentState().getName());
111        sm.getHandler().removeMessages(BluetoothRouteManager.CONNECTION_TIMEOUT);
112        sm.quitNow();
113    }
114
115    @SmallTest
116    @Test
117    public void testProperFallbackOrder1() {
118        // Device 1, 2, 3 are connected in that order. Device 1 is activated, then device 2.
119        // Disconnect device 2, verify fallback to device 1. Disconnect device 1, fallback to
120        // device 3.
121        BluetoothRouteManager sm = setupStateMachine(
122                BluetoothRouteManager.AUDIO_OFF_STATE_NAME, null);
123        setupConnectedDevices(new BluetoothDevice[]{DEVICE3, DEVICE2, DEVICE1}, null);
124        executeRoutingAction(sm, BluetoothRouteManager.CONNECT_HFP, DEVICE1.getAddress());
125        verify(mHeadsetProxy, times(1)).connectAudio(DEVICE1.getAddress());
126
127        setupConnectedDevices(new BluetoothDevice[]{DEVICE3, DEVICE2, DEVICE1}, DEVICE1);
128        executeRoutingAction(sm, BluetoothRouteManager.HFP_IS_ON, DEVICE1.getAddress());
129
130        executeRoutingAction(sm, BluetoothRouteManager.CONNECT_HFP, DEVICE2.getAddress());
131        verify(mHeadsetProxy, times(1)).connectAudio(DEVICE2.getAddress());
132
133        setupConnectedDevices(new BluetoothDevice[]{DEVICE3, DEVICE2, DEVICE1}, DEVICE2);
134        executeRoutingAction(sm, BluetoothRouteManager.HFP_IS_ON, DEVICE2.getAddress());
135        // Disconnect device 2
136        setupConnectedDevices(new BluetoothDevice[]{DEVICE3, DEVICE1}, null);
137        executeRoutingAction(sm, BluetoothRouteManager.LOST_DEVICE, DEVICE2.getAddress());
138        // Verify that we've fallen back to device 1
139        verify(mHeadsetProxy, times(2)).connectAudio(DEVICE1.getAddress());
140        assertEquals(BluetoothRouteManager.AUDIO_CONNECTING_STATE_NAME_PREFIX
141                        + ":" + DEVICE1.getAddress(),
142                sm.getCurrentState().getName());
143        setupConnectedDevices(new BluetoothDevice[]{DEVICE3, DEVICE1}, DEVICE1);
144        executeRoutingAction(sm, BluetoothRouteManager.HFP_IS_ON, DEVICE1.getAddress());
145        assertEquals(BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX
146                        + ":" + DEVICE1.getAddress(),
147                sm.getCurrentState().getName());
148
149        // Disconnect device 1
150        setupConnectedDevices(new BluetoothDevice[]{DEVICE3}, null);
151        executeRoutingAction(sm, BluetoothRouteManager.LOST_DEVICE, DEVICE1.getAddress());
152        // Verify that we've fallen back to device 3
153        verify(mHeadsetProxy, times(1)).connectAudio(DEVICE3.getAddress());
154        assertEquals(BluetoothRouteManager.AUDIO_CONNECTING_STATE_NAME_PREFIX
155                        + ":" + DEVICE3.getAddress(),
156                sm.getCurrentState().getName());
157        setupConnectedDevices(new BluetoothDevice[]{DEVICE3}, DEVICE3);
158        executeRoutingAction(sm, BluetoothRouteManager.HFP_IS_ON, DEVICE3.getAddress());
159        assertEquals(BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX
160                        + ":" + DEVICE3.getAddress(),
161                sm.getCurrentState().getName());
162
163        sm.getHandler().removeMessages(BluetoothRouteManager.CONNECTION_TIMEOUT);
164        sm.quitNow();
165    }
166
167    @SmallTest
168    @Test
169    public void testProperFallbackOrder2() {
170        // Device 1, 2, 3 are connected in that order. Device 3 is activated.
171        // Disconnect device 3, verify fallback to device 2. Disconnect device 2, fallback to
172        // device 1.
173        BluetoothRouteManager sm = setupStateMachine(
174                BluetoothRouteManager.AUDIO_OFF_STATE_NAME, null);
175        setupConnectedDevices(new BluetoothDevice[]{DEVICE3, DEVICE2, DEVICE1}, null);
176        executeRoutingAction(sm, BluetoothRouteManager.CONNECT_HFP, DEVICE3.getAddress());
177        verify(mHeadsetProxy, times(1)).connectAudio(DEVICE3.getAddress());
178
179        setupConnectedDevices(new BluetoothDevice[]{DEVICE3, DEVICE2, DEVICE1}, DEVICE3);
180        executeRoutingAction(sm, BluetoothRouteManager.HFP_IS_ON, DEVICE3.getAddress());
181
182        // Disconnect device 2
183        setupConnectedDevices(new BluetoothDevice[]{DEVICE2, DEVICE1}, null);
184        executeRoutingAction(sm, BluetoothRouteManager.LOST_DEVICE, DEVICE3.getAddress());
185        // Verify that we've fallen back to device 2
186        verify(mHeadsetProxy, times(1)).connectAudio(DEVICE2.getAddress());
187        assertEquals(BluetoothRouteManager.AUDIO_CONNECTING_STATE_NAME_PREFIX
188                        + ":" + DEVICE2.getAddress(),
189                sm.getCurrentState().getName());
190        setupConnectedDevices(new BluetoothDevice[]{DEVICE2, DEVICE1}, DEVICE2);
191        executeRoutingAction(sm, BluetoothRouteManager.HFP_IS_ON, DEVICE2.getAddress());
192        assertEquals(BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX
193                        + ":" + DEVICE2.getAddress(),
194                sm.getCurrentState().getName());
195
196        // Disconnect device 2
197        setupConnectedDevices(new BluetoothDevice[]{DEVICE1}, null);
198        executeRoutingAction(sm, BluetoothRouteManager.LOST_DEVICE, DEVICE2.getAddress());
199        // Verify that we've fallen back to device 1
200        verify(mHeadsetProxy, times(1)).connectAudio(DEVICE1.getAddress());
201        assertEquals(BluetoothRouteManager.AUDIO_CONNECTING_STATE_NAME_PREFIX
202                        + ":" + DEVICE1.getAddress(),
203                sm.getCurrentState().getName());
204        setupConnectedDevices(new BluetoothDevice[]{DEVICE1}, DEVICE1);
205        executeRoutingAction(sm, BluetoothRouteManager.HFP_IS_ON, DEVICE1.getAddress());
206        assertEquals(BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX
207                        + ":" + DEVICE1.getAddress(),
208                sm.getCurrentState().getName());
209
210        sm.getHandler().removeMessages(BluetoothRouteManager.CONNECTION_TIMEOUT);
211        sm.quitNow();
212    }
213
214    private BluetoothRouteManager setupStateMachine(String initialState,
215            BluetoothDevice initialDevice) {
216        resetMocks();
217        BluetoothRouteManager sm = new BluetoothRouteManager(mContext,
218                new TelecomSystem.SyncRoot() { }, mDeviceManager, mTimeoutsAdapter);
219        sm.setListener(mListener);
220        sm.setInitialStateForTesting(initialState, initialDevice);
221        waitForHandlerAction(sm.getHandler(), TEST_TIMEOUT);
222        resetMocks();
223        return sm;
224    }
225
226    private void setupConnectedDevices(BluetoothDevice[] devices, BluetoothDevice activeDevice) {
227        when(mDeviceManager.getNumConnectedDevices()).thenReturn(devices.length);
228        when(mDeviceManager.getConnectedDevices()).thenReturn(Arrays.asList(devices));
229        when(mHeadsetProxy.getConnectedDevices()).thenReturn(Arrays.asList(devices));
230        if (activeDevice != null) {
231            when(mHeadsetProxy.isAudioConnected(eq(activeDevice))).thenReturn(true);
232        }
233        doAnswer(invocation -> {
234            BluetoothDevice first = getFirstExcluding(devices,
235                    (String) invocation.getArguments()[0]);
236            return first == null ? null : first.getAddress();
237        }).when(mDeviceManager).getMostRecentlyConnectedDevice(nullable(String.class));
238    }
239
240    static void executeRoutingAction(BluetoothRouteManager brm, int message, String
241            device) {
242        SomeArgs args = SomeArgs.obtain();
243        args.arg1 = Log.createSubsession();
244        args.arg2 = device;
245        brm.sendMessage(message, args);
246        waitForHandlerAction(brm.getHandler(), TEST_TIMEOUT);
247    }
248
249    public static BluetoothDevice makeBluetoothDevice(String address) {
250        Parcel p1 = Parcel.obtain();
251        p1.writeString(address);
252        p1.setDataPosition(0);
253        BluetoothDevice device = BluetoothDevice.CREATOR.createFromParcel(p1);
254        p1.recycle();
255        return device;
256    }
257
258    private void resetMocks() {
259        reset(mDeviceManager, mListener, mHeadsetProxy, mTimeoutsAdapter);
260        when(mDeviceManager.getHeadsetService()).thenReturn(mHeadsetProxy);
261        when(mHeadsetProxy.connectAudio(nullable(String.class))).thenReturn(true);
262        when(mTimeoutsAdapter.getRetryBluetoothConnectAudioBackoffMillis(
263                nullable(ContentResolver.class))).thenReturn(100000L);
264        when(mTimeoutsAdapter.getBluetoothPendingTimeoutMillis(
265                nullable(ContentResolver.class))).thenReturn(100000L);
266    }
267
268    private static BluetoothDevice getFirstExcluding(
269            BluetoothDevice[] devices, String excludeAddress) {
270        for (BluetoothDevice x : devices) {
271            if (!Objects.equals(excludeAddress, x.getAddress())) {
272                return x;
273            }
274        }
275        return null;
276    }
277}
278