1 /*
2  * Copyright 2017 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.a2dp;
18 
19 import static org.mockito.Mockito.*;
20 
21 import android.bluetooth.BluetoothA2dp;
22 import android.bluetooth.BluetoothAdapter;
23 import android.bluetooth.BluetoothCodecConfig;
24 import android.bluetooth.BluetoothCodecStatus;
25 import android.bluetooth.BluetoothDevice;
26 import android.bluetooth.BluetoothProfile;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.os.Bundle;
30 import android.os.HandlerThread;
31 
32 import androidx.test.InstrumentationRegistry;
33 import androidx.test.filters.MediumTest;
34 import androidx.test.runner.AndroidJUnit4;
35 
36 import com.android.bluetooth.R;
37 import com.android.bluetooth.TestUtils;
38 import com.android.bluetooth.btservice.AdapterService;
39 
40 import org.hamcrest.core.IsInstanceOf;
41 import org.junit.After;
42 import org.junit.Assert;
43 import org.junit.Assume;
44 import org.junit.Before;
45 import org.junit.Test;
46 import org.junit.runner.RunWith;
47 import org.mockito.ArgumentCaptor;
48 import org.mockito.Mock;
49 import org.mockito.MockitoAnnotations;
50 
51 @MediumTest
52 @RunWith(AndroidJUnit4.class)
53 public class A2dpStateMachineTest {
54     private BluetoothAdapter mAdapter;
55     private Context mTargetContext;
56     private HandlerThread mHandlerThread;
57     private A2dpStateMachine mA2dpStateMachine;
58     private BluetoothDevice mTestDevice;
59     private static final int TIMEOUT_MS = 1000;    // 1s
60 
61     private BluetoothCodecConfig mCodecConfigSbc;
62     private BluetoothCodecConfig mCodecConfigAac;
63 
64     @Mock private AdapterService mAdapterService;
65     @Mock private A2dpService mA2dpService;
66     @Mock private A2dpNativeInterface mA2dpNativeInterface;
67 
68     @Before
setUp()69     public void setUp() throws Exception {
70         mTargetContext = InstrumentationRegistry.getTargetContext();
71         Assume.assumeTrue("Ignore test when A2dpService is not enabled",
72                 mTargetContext.getResources().getBoolean(R.bool.profile_supported_a2dp));
73         // Set up mocks and test assets
74         MockitoAnnotations.initMocks(this);
75         TestUtils.setAdapterService(mAdapterService);
76 
77         mAdapter = BluetoothAdapter.getDefaultAdapter();
78 
79         // Get a device for testing
80         mTestDevice = mAdapter.getRemoteDevice("00:01:02:03:04:05");
81 
82         // Set up sample codec config
83         mCodecConfigSbc = new BluetoothCodecConfig(
84             BluetoothCodecConfig.SOURCE_CODEC_TYPE_SBC,
85             BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
86             BluetoothCodecConfig.SAMPLE_RATE_44100,
87             BluetoothCodecConfig.BITS_PER_SAMPLE_16,
88             BluetoothCodecConfig.CHANNEL_MODE_STEREO,
89             0, 0, 0, 0);       // Codec-specific fields
90         mCodecConfigAac = new BluetoothCodecConfig(
91             BluetoothCodecConfig.SOURCE_CODEC_TYPE_AAC,
92             BluetoothCodecConfig.CODEC_PRIORITY_DEFAULT,
93             BluetoothCodecConfig.SAMPLE_RATE_48000,
94             BluetoothCodecConfig.BITS_PER_SAMPLE_16,
95             BluetoothCodecConfig.CHANNEL_MODE_STEREO,
96             0, 0, 0, 0);       // Codec-specific fields
97 
98         // Set up thread and looper
99         mHandlerThread = new HandlerThread("A2dpStateMachineTestHandlerThread");
100         mHandlerThread.start();
101         mA2dpStateMachine = new A2dpStateMachine(mTestDevice, mA2dpService,
102                                                  mA2dpNativeInterface, mHandlerThread.getLooper());
103         // Override the timeout value to speed up the test
104         A2dpStateMachine.sConnectTimeoutMs = 1000;     // 1s
105         mA2dpStateMachine.start();
106     }
107 
108     @After
tearDown()109     public void tearDown() throws Exception {
110         if (!mTargetContext.getResources().getBoolean(R.bool.profile_supported_a2dp)) {
111             return;
112         }
113         mA2dpStateMachine.doQuit();
114         mHandlerThread.quit();
115         TestUtils.clearAdapterService(mAdapterService);
116     }
117 
118     /**
119      * Test that default state is disconnected
120      */
121     @Test
testDefaultDisconnectedState()122     public void testDefaultDisconnectedState() {
123         Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED,
124                 mA2dpStateMachine.getConnectionState());
125     }
126 
127     /**
128      * Allow/disallow connection to any device.
129      *
130      * @param allow if true, connection is allowed
131      */
allowConnection(boolean allow)132     private void allowConnection(boolean allow) {
133         doReturn(allow).when(mA2dpService).okToConnect(any(BluetoothDevice.class),
134                                                        anyBoolean());
135     }
136 
137     /**
138      * Test that an incoming connection with low priority is rejected
139      */
140     @Test
testIncomingPriorityReject()141     public void testIncomingPriorityReject() {
142         allowConnection(false);
143 
144         // Inject an event for when incoming connection is requested
145         A2dpStackEvent connStCh =
146                 new A2dpStackEvent(A2dpStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
147         connStCh.device = mTestDevice;
148         connStCh.valueInt = A2dpStackEvent.CONNECTION_STATE_CONNECTED;
149         mA2dpStateMachine.sendMessage(A2dpStateMachine.STACK_EVENT, connStCh);
150 
151         // Verify that no connection state broadcast is executed
152         verify(mA2dpService, after(TIMEOUT_MS).never()).sendBroadcast(any(Intent.class),
153                 anyString(), any(Bundle.class));
154         // Check that we are in Disconnected state
155         Assert.assertThat(mA2dpStateMachine.getCurrentState(),
156                           IsInstanceOf.instanceOf(A2dpStateMachine.Disconnected.class));
157     }
158 
159     /**
160      * Test that an incoming connection with high priority is accepted
161      */
162     @Test
testIncomingPriorityAccept()163     public void testIncomingPriorityAccept() {
164         allowConnection(true);
165 
166         // Inject an event for when incoming connection is requested
167         A2dpStackEvent connStCh =
168                 new A2dpStackEvent(A2dpStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
169         connStCh.device = mTestDevice;
170         connStCh.valueInt = A2dpStackEvent.CONNECTION_STATE_CONNECTING;
171         mA2dpStateMachine.sendMessage(A2dpStateMachine.STACK_EVENT, connStCh);
172 
173         // Verify that one connection state broadcast is executed
174         ArgumentCaptor<Intent> intentArgument1 = ArgumentCaptor.forClass(Intent.class);
175         verify(mA2dpService, timeout(TIMEOUT_MS).times(1)).sendBroadcast(intentArgument1.capture(),
176                 anyString(), any(Bundle.class));
177         Assert.assertEquals(BluetoothProfile.STATE_CONNECTING,
178                 intentArgument1.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1));
179 
180         // Check that we are in Connecting state
181         Assert.assertThat(mA2dpStateMachine.getCurrentState(),
182                           IsInstanceOf.instanceOf(A2dpStateMachine.Connecting.class));
183 
184         // Send a message to trigger connection completed
185         A2dpStackEvent connCompletedEvent =
186                 new A2dpStackEvent(A2dpStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
187         connCompletedEvent.device = mTestDevice;
188         connCompletedEvent.valueInt = A2dpStackEvent.CONNECTION_STATE_CONNECTED;
189         mA2dpStateMachine.sendMessage(A2dpStateMachine.STACK_EVENT, connCompletedEvent);
190 
191         // Verify that the expected number of broadcasts are executed:
192         // - two calls to broadcastConnectionState(): Disconnected -> Conecting -> Connected
193         // - one call to broadcastAudioState() when entering Connected state
194         ArgumentCaptor<Intent> intentArgument2 = ArgumentCaptor.forClass(Intent.class);
195         verify(mA2dpService, timeout(TIMEOUT_MS).times(3)).sendBroadcast(intentArgument2.capture(),
196                 anyString(), any(Bundle.class));
197         // Verify that the last broadcast was to change the A2DP playing state
198         // to STATE_NOT_PLAYING
199         Assert.assertEquals(BluetoothA2dp.ACTION_PLAYING_STATE_CHANGED,
200                 intentArgument2.getValue().getAction());
201         Assert.assertEquals(BluetoothA2dp.STATE_NOT_PLAYING,
202                 intentArgument2.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1));
203         // Check that we are in Connected state
204         Assert.assertThat(mA2dpStateMachine.getCurrentState(),
205                           IsInstanceOf.instanceOf(A2dpStateMachine.Connected.class));
206     }
207 
208     /**
209      * Test that an outgoing connection times out
210      */
211     @Test
testOutgoingTimeout()212     public void testOutgoingTimeout() {
213         allowConnection(true);
214         doReturn(true).when(mA2dpNativeInterface).connectA2dp(any(BluetoothDevice.class));
215         doReturn(true).when(mA2dpNativeInterface).disconnectA2dp(any(BluetoothDevice.class));
216 
217         // Send a connect request
218         mA2dpStateMachine.sendMessage(A2dpStateMachine.CONNECT, mTestDevice);
219 
220         // Verify that one connection state broadcast is executed
221         ArgumentCaptor<Intent> intentArgument1 = ArgumentCaptor.forClass(Intent.class);
222         verify(mA2dpService, timeout(TIMEOUT_MS).times(1)).sendBroadcast(intentArgument1.capture(),
223                 anyString(), any(Bundle.class));
224         Assert.assertEquals(BluetoothProfile.STATE_CONNECTING,
225                 intentArgument1.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1));
226 
227         // Check that we are in Connecting state
228         Assert.assertThat(mA2dpStateMachine.getCurrentState(),
229                 IsInstanceOf.instanceOf(A2dpStateMachine.Connecting.class));
230 
231         // Verify that one connection state broadcast is executed
232         ArgumentCaptor<Intent> intentArgument2 = ArgumentCaptor.forClass(Intent.class);
233         verify(mA2dpService, timeout(A2dpStateMachine.sConnectTimeoutMs * 2).times(
234                 2)).sendBroadcast(intentArgument2.capture(), anyString(),
235                 any(Bundle.class));
236         Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED,
237                 intentArgument2.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1));
238 
239         // Check that we are in Disconnected state
240         Assert.assertThat(mA2dpStateMachine.getCurrentState(),
241                           IsInstanceOf.instanceOf(A2dpStateMachine.Disconnected.class));
242     }
243 
244     /**
245      * Test that an incoming connection times out
246      */
247     @Test
testIncomingTimeout()248     public void testIncomingTimeout() {
249         allowConnection(true);
250         doReturn(true).when(mA2dpNativeInterface).connectA2dp(any(BluetoothDevice.class));
251         doReturn(true).when(mA2dpNativeInterface).disconnectA2dp(any(BluetoothDevice.class));
252 
253         // Inject an event for when incoming connection is requested
254         A2dpStackEvent connStCh =
255                 new A2dpStackEvent(A2dpStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
256         connStCh.device = mTestDevice;
257         connStCh.valueInt = A2dpStackEvent.CONNECTION_STATE_CONNECTING;
258         mA2dpStateMachine.sendMessage(A2dpStateMachine.STACK_EVENT, connStCh);
259 
260         // Verify that one connection state broadcast is executed
261         ArgumentCaptor<Intent> intentArgument1 = ArgumentCaptor.forClass(Intent.class);
262         verify(mA2dpService, timeout(TIMEOUT_MS).times(1)).sendBroadcast(intentArgument1.capture(),
263                 anyString(), any(Bundle.class));
264         Assert.assertEquals(BluetoothProfile.STATE_CONNECTING,
265                 intentArgument1.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1));
266 
267         // Check that we are in Connecting state
268         Assert.assertThat(mA2dpStateMachine.getCurrentState(),
269                 IsInstanceOf.instanceOf(A2dpStateMachine.Connecting.class));
270 
271         // Verify that one connection state broadcast is executed
272         ArgumentCaptor<Intent> intentArgument2 = ArgumentCaptor.forClass(Intent.class);
273         verify(mA2dpService, timeout(A2dpStateMachine.sConnectTimeoutMs * 2).times(
274                 2)).sendBroadcast(intentArgument2.capture(), anyString(),
275                 any(Bundle.class));
276         Assert.assertEquals(BluetoothProfile.STATE_DISCONNECTED,
277                 intentArgument2.getValue().getIntExtra(BluetoothProfile.EXTRA_STATE, -1));
278 
279         // Check that we are in Disconnected state
280         Assert.assertThat(mA2dpStateMachine.getCurrentState(),
281                 IsInstanceOf.instanceOf(A2dpStateMachine.Disconnected.class));
282     }
283 
284     /**
285      * Test that codec config change been reported to A2dpService properly.
286      */
287     @Test
testProcessCodecConfigEvent()288     public void testProcessCodecConfigEvent() {
289         testProcessCodecConfigEventCase(false);
290     }
291 
292     /**
293      * Test that codec config change been reported to A2dpService properly when
294      * A2DP hardware offloading is enabled.
295      */
296     @Test
testProcessCodecConfigEvent_OffloadEnabled()297     public void testProcessCodecConfigEvent_OffloadEnabled() {
298         testProcessCodecConfigEventCase(true);
299     }
300 
301     /**
302      * Helper methold to test processCodecConfigEvent()
303      */
testProcessCodecConfigEventCase(boolean offloadEnabled)304     public void testProcessCodecConfigEventCase(boolean offloadEnabled) {
305         if (offloadEnabled) {
306             mA2dpStateMachine.mA2dpOffloadEnabled = true;
307         }
308 
309         doNothing().when(mA2dpService).codecConfigUpdated(any(BluetoothDevice.class),
310                 any(BluetoothCodecStatus.class), anyBoolean());
311         doNothing().when(mA2dpService).updateOptionalCodecsSupport(any(BluetoothDevice.class));
312         allowConnection(true);
313 
314         BluetoothCodecConfig[] codecsSelectableSbc;
315         codecsSelectableSbc = new BluetoothCodecConfig[1];
316         codecsSelectableSbc[0] = mCodecConfigSbc;
317 
318         BluetoothCodecConfig[] codecsSelectableSbcAac;
319         codecsSelectableSbcAac = new BluetoothCodecConfig[2];
320         codecsSelectableSbcAac[0] = mCodecConfigSbc;
321         codecsSelectableSbcAac[1] = mCodecConfigAac;
322 
323         BluetoothCodecStatus codecStatusSbcAndSbc = new BluetoothCodecStatus(mCodecConfigSbc,
324                 codecsSelectableSbcAac, codecsSelectableSbc);
325         BluetoothCodecStatus codecStatusSbcAndSbcAac = new BluetoothCodecStatus(mCodecConfigSbc,
326                 codecsSelectableSbcAac, codecsSelectableSbcAac);
327         BluetoothCodecStatus codecStatusAacAndSbcAac = new BluetoothCodecStatus(mCodecConfigAac,
328                 codecsSelectableSbcAac, codecsSelectableSbcAac);
329 
330         // Set default codec status when device disconnected
331         // Selected codec = SBC, selectable codec = SBC
332         mA2dpStateMachine.processCodecConfigEvent(codecStatusSbcAndSbc);
333         verify(mA2dpService).codecConfigUpdated(mTestDevice, codecStatusSbcAndSbc, false);
334 
335         // Inject an event to change state machine to connected state
336         A2dpStackEvent connStCh =
337                 new A2dpStackEvent(A2dpStackEvent.EVENT_TYPE_CONNECTION_STATE_CHANGED);
338         connStCh.device = mTestDevice;
339         connStCh.valueInt = A2dpStackEvent.CONNECTION_STATE_CONNECTED;
340         mA2dpStateMachine.sendMessage(A2dpStateMachine.STACK_EVENT, connStCh);
341 
342         // Verify that the expected number of broadcasts are executed:
343         // - two calls to broadcastConnectionState(): Disconnected -> Conecting -> Connected
344         // - one call to broadcastAudioState() when entering Connected state
345         ArgumentCaptor<Intent> intentArgument2 = ArgumentCaptor.forClass(Intent.class);
346         verify(mA2dpService, timeout(TIMEOUT_MS).times(2)).sendBroadcast(intentArgument2.capture(),
347                 anyString(), any(Bundle.class));
348 
349         // Verify that state machine update optional codec when enter connected state
350         verify(mA2dpService, times(1)).updateOptionalCodecsSupport(mTestDevice);
351 
352         // Change codec status when device connected.
353         // Selected codec = SBC, selectable codec = SBC+AAC
354         mA2dpStateMachine.processCodecConfigEvent(codecStatusSbcAndSbcAac);
355         if (!offloadEnabled) {
356             verify(mA2dpService).codecConfigUpdated(mTestDevice, codecStatusSbcAndSbcAac, true);
357         }
358         verify(mA2dpService, times(2)).updateOptionalCodecsSupport(mTestDevice);
359 
360         // Update selected codec with selectable codec unchanged.
361         // Selected codec = AAC, selectable codec = SBC+AAC
362         mA2dpStateMachine.processCodecConfigEvent(codecStatusAacAndSbcAac);
363         verify(mA2dpService).codecConfigUpdated(mTestDevice, codecStatusAacAndSbcAac, false);
364         verify(mA2dpService, times(2)).updateOptionalCodecsSupport(mTestDevice);
365     }
366 }
367