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