1 /*
2  * Copyright 2019 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 package com.android.server.audio;
17 
18 import static org.mockito.Mockito.any;
19 import static org.mockito.Mockito.anyInt;
20 import static org.mockito.Mockito.doNothing;
21 import static org.mockito.Mockito.mock;
22 import static org.mockito.Mockito.spy;
23 import static org.mockito.Mockito.times;
24 import static org.mockito.Mockito.verify;
25 import static org.mockito.Mockito.when;
26 
27 import android.bluetooth.BluetoothAdapter;
28 import android.bluetooth.BluetoothDevice;
29 import android.content.Context;
30 import android.content.Intent;
31 import android.media.AudioDeviceAttributes;
32 import android.media.AudioDeviceInfo;
33 import android.media.AudioManager;
34 import android.media.AudioSystem;
35 import android.media.BluetoothProfileConnectionInfo;
36 import android.util.Log;
37 
38 import androidx.test.filters.MediumTest;
39 import androidx.test.platform.app.InstrumentationRegistry;
40 import androidx.test.runner.AndroidJUnit4;
41 
42 import org.junit.After;
43 import org.junit.Assert;
44 import org.junit.Before;
45 import org.junit.Test;
46 import org.junit.runner.RunWith;
47 import org.mockito.ArgumentMatchers;
48 import org.mockito.Mock;
49 import org.mockito.Spy;
50 
51 @MediumTest
52 @RunWith(AndroidJUnit4.class)
53 public class AudioDeviceBrokerTest {
54 
55     private static final String TAG = "AudioDeviceBrokerTest";
56     private static final int MAX_MESSAGE_HANDLING_DELAY_MS = 100;
57 
58     // the actual class under test
59     private AudioDeviceBroker mAudioDeviceBroker;
60 
61     @Mock private AudioService mMockAudioService;
62     @Spy private AudioDeviceInventory mSpyDevInventory;
63     @Spy private AudioSystemAdapter mSpyAudioSystem;
64     @Spy private SystemServerAdapter mSpySystemServer;
65 
66     private BluetoothDevice mFakeBtDevice;
67 
68     @Before
setUp()69     public void setUp() throws Exception {
70         Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
71 
72         mMockAudioService = mock(AudioService.class);
73         mSpyAudioSystem = spy(new NoOpAudioSystemAdapter());
74         mSpyDevInventory = spy(new AudioDeviceInventory(mSpyAudioSystem));
75         mSpySystemServer = spy(new NoOpSystemServerAdapter());
76         mAudioDeviceBroker = new AudioDeviceBroker(context, mMockAudioService, mSpyDevInventory,
77                 mSpySystemServer, mSpyAudioSystem);
78         mSpyDevInventory.setDeviceBroker(mAudioDeviceBroker);
79 
80         BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
81         mFakeBtDevice = adapter.getRemoteDevice("00:01:02:03:04:05");
82         Assert.assertNotNull("invalid null BT device", mFakeBtDevice);
83     }
84 
85     @After
tearDown()86     public void tearDown() throws Exception { }
87 
88 //    @Test
89 //    public void testSetUpAndTearDown() { }
90 
91     /**
92      * postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent() for connection:
93      * - verify it calls into AudioDeviceInventory with the right params
94      * - verify it calls into AudioSystem and stays connected (no 2nd call to disconnect)
95      * @throws Exception
96      */
97     @Test
testPostA2dpDeviceConnectionChange()98     public void testPostA2dpDeviceConnectionChange() throws Exception {
99         Log.i(TAG, "starting testPostA2dpDeviceConnectionChange");
100         Assert.assertNotNull("invalid null BT device", mFakeBtDevice);
101 
102         mAudioDeviceBroker.queueOnBluetoothActiveDeviceChanged(
103                 new AudioDeviceBroker.BtDeviceChangedData(mFakeBtDevice, null,
104                     BluetoothProfileConnectionInfo.createA2dpInfo(true, 1), "testSource"));
105         Thread.sleep(2 * MAX_MESSAGE_HANDLING_DELAY_MS);
106         verify(mSpyDevInventory, times(1)).setBluetoothActiveDevice(
107                 any(AudioDeviceBroker.BtDeviceInfo.class)
108         );
109 
110         // verify the connection was reported to AudioSystem
111         checkSingleSystemConnection(mFakeBtDevice);
112     }
113 
114     /**
115      * Verify call to postBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent() for
116      *    connection > pause > disconnection > connection
117      * keeps the device connected
118      * @throws Exception
119      */
120     @Test
testA2dpDeviceConnectionDisconnectionConnectionChange()121     public void testA2dpDeviceConnectionDisconnectionConnectionChange() throws Exception {
122         Log.i(TAG, "starting testA2dpDeviceConnectionDisconnectionConnectionChange");
123 
124         doTestConnectionDisconnectionReconnection(0, false,
125                 // cannot guarantee single connection since commands are posted in separate thread
126                 // than they are processed
127                 false);
128     }
129 
130     /**
131      * Verify device disconnection and reconnection within the BECOMING_NOISY window
132      * in the absence of media playback
133      * @throws Exception
134      */
135     @Test
testA2dpDeviceReconnectionWithinBecomingNoisyDelay()136     public void testA2dpDeviceReconnectionWithinBecomingNoisyDelay() throws Exception {
137         Log.i(TAG, "starting testA2dpDeviceReconnectionWithinBecomingNoisyDelay");
138 
139         doTestConnectionDisconnectionReconnection(AudioService.BECOMING_NOISY_DELAY_MS / 2,
140                 false,
141                 // do not check single connection since the connection command will come much
142                 // after the disconnection command
143                 false);
144     }
145 
146     /**
147      * Same as testA2dpDeviceConnectionDisconnectionConnectionChange() but with mock media playback
148      * @throws Exception
149      */
150     @Test
testA2dpConnectionDisconnectionConnectionChange_MediaPlayback()151     public void testA2dpConnectionDisconnectionConnectionChange_MediaPlayback() throws Exception {
152         Log.i(TAG, "starting testA2dpConnectionDisconnectionConnectionChange_MediaPlayback");
153 
154         doTestConnectionDisconnectionReconnection(0, true,
155                 // guarantee single connection since because of media playback the disconnection
156                 // is supposed to be delayed, and thus cancelled because of the connection
157                 true);
158     }
159 
160     /**
161      * Same as testA2dpDeviceReconnectionWithinBecomingNoisyDelay() but with mock media playback
162      * @throws Exception
163      */
164     @Test
testA2dpReconnectionWithinBecomingNoisyDelay_MediaPlayback()165     public void testA2dpReconnectionWithinBecomingNoisyDelay_MediaPlayback() throws Exception {
166         Log.i(TAG, "starting testA2dpReconnectionWithinBecomingNoisyDelay_MediaPlayback");
167 
168         doTestConnectionDisconnectionReconnection(AudioService.BECOMING_NOISY_DELAY_MS / 2,
169                 true,
170                 // guarantee single connection since because of media playback the disconnection
171                 // is supposed to be delayed, and thus cancelled because of the connection
172                 true);
173     }
174 
175     /**
176      * Test that device wired state intents are broadcasted on connection state change
177      * @throws Exception
178      */
179     @Test
testSetWiredDeviceConnectionState()180     public void testSetWiredDeviceConnectionState() throws Exception {
181         Log.i(TAG, "starting postSetWiredDeviceConnectionState");
182 
183         final String address = "testAddress";
184         final String name = "testName";
185         final String caller = "testCaller";
186 
187         doNothing().when(mSpySystemServer).broadcastStickyIntentToCurrentProfileGroup(
188                 any(Intent.class));
189 
190         mSpyDevInventory.setWiredDeviceConnectionState(new AudioDeviceAttributes(
191                         AudioSystem.DEVICE_OUT_WIRED_HEADSET, address, name),
192                 AudioService.CONNECTION_STATE_CONNECTED, caller);
193         Thread.sleep(MAX_MESSAGE_HANDLING_DELAY_MS);
194 
195         // Verify that the sticky intent is broadcasted
196         verify(mSpySystemServer, times(1)).broadcastStickyIntentToCurrentProfileGroup(
197                 any(Intent.class));
198     }
199 
200     /**
201      * Test that constructing an AdiDeviceState instance requires a non-null address for a
202      * wireless type, but can take null for a non-wireless type;
203      * @throws Exception
204      */
205     @Test
testAdiDeviceStateNullAddressCtor()206     public void testAdiDeviceStateNullAddressCtor() throws Exception {
207         try {
208             new AdiDeviceState(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER,
209                     AudioManager.DEVICE_OUT_SPEAKER, null);
210             new AdiDeviceState(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP,
211                     AudioManager.DEVICE_OUT_BLUETOOTH_A2DP, null);
212             Assert.fail();
213         } catch (NullPointerException e) { }
214     }
215 
216     @Test
testAdiDeviceStateStringSerialization()217     public void testAdiDeviceStateStringSerialization() throws Exception {
218         Log.i(TAG, "starting testAdiDeviceStateStringSerialization");
219         final AdiDeviceState devState = new AdiDeviceState(
220                 AudioDeviceInfo.TYPE_BUILTIN_SPEAKER, AudioManager.DEVICE_OUT_SPEAKER, "bla");
221         devState.setHasHeadTracker(false);
222         devState.setHeadTrackerEnabled(false);
223         devState.setSAEnabled(true);
224         final String persistString = devState.toPersistableString();
225         final AdiDeviceState result = AdiDeviceState.fromPersistedString(persistString);
226         Log.i(TAG, "original:" + devState);
227         Log.i(TAG, "result  :" + result);
228         Assert.assertEquals(devState, result);
229     }
230 
doTestConnectionDisconnectionReconnection(int delayAfterDisconnection, boolean mockMediaPlayback, boolean guaranteeSingleConnection)231     private void doTestConnectionDisconnectionReconnection(int delayAfterDisconnection,
232             boolean mockMediaPlayback, boolean guaranteeSingleConnection) throws Exception {
233         when(mMockAudioService.getDeviceForStream(AudioManager.STREAM_MUSIC))
234                 .thenReturn(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP);
235         when(mMockAudioService.isInCommunication()).thenReturn(false);
236         when(mMockAudioService.hasMediaDynamicPolicy()).thenReturn(false);
237         when(mMockAudioService.hasAudioFocusUsers()).thenReturn(false);
238 
239         ((NoOpAudioSystemAdapter) mSpyAudioSystem).configureIsStreamActive(mockMediaPlayback);
240 
241         // first connection: ensure the device is connected as a starting condition for the test
242         mAudioDeviceBroker.queueOnBluetoothActiveDeviceChanged(
243                 new AudioDeviceBroker.BtDeviceChangedData(mFakeBtDevice, null,
244                     BluetoothProfileConnectionInfo.createA2dpInfo(true, 1), "testSource"));
245         Thread.sleep(MAX_MESSAGE_HANDLING_DELAY_MS);
246 
247         // disconnection
248         mAudioDeviceBroker.queueOnBluetoothActiveDeviceChanged(
249                 new AudioDeviceBroker.BtDeviceChangedData(null, mFakeBtDevice,
250                     BluetoothProfileConnectionInfo.createA2dpInfo(false, -1), "testSource"));
251         if (delayAfterDisconnection > 0) {
252             Thread.sleep(delayAfterDisconnection);
253         }
254 
255         // reconnection
256         mAudioDeviceBroker.queueOnBluetoothActiveDeviceChanged(
257                 new AudioDeviceBroker.BtDeviceChangedData(mFakeBtDevice, null,
258                     BluetoothProfileConnectionInfo.createA2dpInfo(true, 2), "testSource"));
259         Thread.sleep(AudioService.BECOMING_NOISY_DELAY_MS + MAX_MESSAGE_HANDLING_DELAY_MS);
260 
261         // Verify disconnection has been cancelled and we're seeing two connections attempts,
262         // with the device connected at the end of the test
263         verify(mSpyDevInventory, times(2)).onSetBtActiveDevice(
264                 any(AudioDeviceBroker.BtDeviceInfo.class), anyInt() /*codec*/,
265                 anyInt() /*streamType*/);
266         Assert.assertTrue("Mock device not connected",
267                 mSpyDevInventory.isA2dpDeviceConnected(mFakeBtDevice));
268 
269         if (guaranteeSingleConnection) {
270             // when the disconnection was expected to be cancelled, there should have been a single
271             //  call to AudioSystem to declare the device connected (available)
272             checkSingleSystemConnection(mFakeBtDevice);
273         }
274     }
275 
276     /**
277      * Verifies the given device was reported to AudioSystem exactly once as available
278      * @param btDevice
279      * @throws Exception
280      */
checkSingleSystemConnection(BluetoothDevice btDevice)281     private void checkSingleSystemConnection(BluetoothDevice btDevice) throws Exception {
282         final String expectedName = btDevice.getName() == null ? "" : btDevice.getName();
283         AudioDeviceAttributes expected = new AudioDeviceAttributes(
284                 AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, btDevice.getAddress(), expectedName);
285         verify(mSpyAudioSystem, times(1)).setDeviceConnectionState(
286                 ArgumentMatchers.argThat(x -> x.equalTypeAddress(expected)),
287                 ArgumentMatchers.eq(AudioSystem.DEVICE_STATE_AVAILABLE),
288                 anyInt() /*codec*/);
289     }
290 }
291