1 /*
2  * Copyright (C) 2023 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.media;
18 
19 import static com.google.common.truth.Truth.assertThat;
20 import static com.google.common.truth.Truth.assertWithMessage;
21 
22 import static org.mockito.ArgumentMatchers.any;
23 import static org.mockito.Mockito.when;
24 
25 import android.annotation.IdRes;
26 import android.content.Context;
27 import android.content.res.Resources;
28 import android.media.AudioManager;
29 import android.media.AudioRoutesInfo;
30 import android.media.IAudioRoutesObserver;
31 import android.media.MediaRoute2Info;
32 import android.os.RemoteException;
33 import android.text.TextUtils;
34 
35 import com.android.internal.R;
36 import com.android.server.audio.AudioService;
37 
38 import org.junit.Before;
39 import org.junit.Test;
40 import org.junit.experimental.runners.Enclosed;
41 import org.junit.runner.RunWith;
42 import org.junit.runners.JUnit4;
43 import org.junit.runners.Parameterized;
44 import org.mockito.ArgumentCaptor;
45 import org.mockito.Captor;
46 import org.mockito.Mock;
47 import org.mockito.MockitoAnnotations;
48 
49 import java.util.Arrays;
50 import java.util.Collection;
51 
52 @RunWith(Enclosed.class)
53 public class LegacyDeviceRouteControllerTest {
54 
55     private static final String DEFAULT_ROUTE_NAME = "default_route";
56     private static final String DEFAULT_HEADPHONES_NAME = "headphone";
57     private static final String DEFAULT_HEADSET_NAME = "headset";
58     private static final String DEFAULT_DOCK_NAME = "dock";
59     private static final String DEFAULT_HDMI_NAME = "hdmi";
60     private static final String DEFAULT_USB_NAME = "usb";
61     private static final int VOLUME_DEFAULT_VALUE = 0;
62     private static final int VOLUME_VALUE_SAMPLE_1 = 10;
63 
createFakeBluetoothAudioRoute()64     private static AudioRoutesInfo createFakeBluetoothAudioRoute() {
65         AudioRoutesInfo btRouteInfo = new AudioRoutesInfo();
66         btRouteInfo.mainType = AudioRoutesInfo.MAIN_SPEAKER;
67         btRouteInfo.bluetoothName = "bt_device";
68         return btRouteInfo;
69     }
70 
71     @RunWith(JUnit4.class)
72     public static class DefaultDeviceRouteValueTest {
73         @Mock
74         private Context mContext;
75         @Mock
76         private Resources mResources;
77         @Mock
78         private AudioManager mAudioManager;
79         @Mock
80         private AudioService mAudioService;
81         @Mock
82         private DeviceRouteController.OnDeviceRouteChangedListener mOnDeviceRouteChangedListener;
83 
84         @Before
setUp()85         public void setUp() {
86             MockitoAnnotations.initMocks(this);
87 
88             when(mContext.getResources()).thenReturn(mResources);
89         }
90 
91         @Test
initialize_noRoutesInfo_defaultRouteIsNotNull()92         public void initialize_noRoutesInfo_defaultRouteIsNotNull() {
93             // Mocking default_audio_route_name.
94             when(mResources.getText(R.string.default_audio_route_name))
95                     .thenReturn(DEFAULT_ROUTE_NAME);
96 
97             // Default route should be initialized even when AudioService returns null.
98             when(mAudioService.startWatchingRoutes(any())).thenReturn(null);
99 
100             LegacyDeviceRouteController deviceRouteController = new LegacyDeviceRouteController(
101                     mContext,
102                     mAudioManager,
103                     mAudioService,
104                     mOnDeviceRouteChangedListener
105             );
106 
107             MediaRoute2Info actualMediaRoute = deviceRouteController.getDeviceRoute();
108 
109             assertThat(actualMediaRoute.getType()).isEqualTo(MediaRoute2Info.TYPE_BUILTIN_SPEAKER);
110             assertThat(TextUtils.equals(actualMediaRoute.getName(), DEFAULT_ROUTE_NAME))
111                     .isTrue();
112             assertThat(actualMediaRoute.getVolume()).isEqualTo(VOLUME_DEFAULT_VALUE);
113         }
114 
115         @Test
initialize_bluetoothRouteAvailable_deviceRouteReturnsDefaultRoute()116         public void initialize_bluetoothRouteAvailable_deviceRouteReturnsDefaultRoute() {
117             // Mocking default_audio_route_name.
118             when(mResources.getText(R.string.default_audio_route_name))
119                     .thenReturn(DEFAULT_ROUTE_NAME);
120 
121             // This route should be ignored.
122             AudioRoutesInfo fakeBluetoothAudioRoute = createFakeBluetoothAudioRoute();
123             when(mAudioService.startWatchingRoutes(any())).thenReturn(fakeBluetoothAudioRoute);
124 
125             LegacyDeviceRouteController deviceRouteController = new LegacyDeviceRouteController(
126                     mContext,
127                     mAudioManager,
128                     mAudioService,
129                     mOnDeviceRouteChangedListener
130             );
131 
132             MediaRoute2Info actualMediaRoute = deviceRouteController.getDeviceRoute();
133 
134             assertThat(actualMediaRoute.getType()).isEqualTo(MediaRoute2Info.TYPE_BUILTIN_SPEAKER);
135             assertThat(TextUtils.equals(actualMediaRoute.getName(), DEFAULT_ROUTE_NAME))
136                     .isTrue();
137             assertThat(actualMediaRoute.getVolume()).isEqualTo(VOLUME_DEFAULT_VALUE);
138         }
139     }
140 
141     @RunWith(Parameterized.class)
142     public static class DeviceRouteInitializationTest {
143 
144         @Parameterized.Parameters
data()145         public static Collection<Object[]> data() {
146             return Arrays.asList(new Object[][] {
147                     {     /* expected res */
148                           com.android.internal.R.string.default_audio_route_name_headphones,
149                           /* expected name */
150                           DEFAULT_HEADPHONES_NAME,
151                           /* expected type */
152                           MediaRoute2Info.TYPE_WIRED_HEADPHONES,
153                           /* actual audio route type */
154                           AudioRoutesInfo.MAIN_HEADPHONES },
155                     {   /* expected res */
156                         com.android.internal.R.string.default_audio_route_name_headphones,
157                         /* expected name */
158                         DEFAULT_HEADSET_NAME,
159                         /* expected type */
160                         MediaRoute2Info.TYPE_WIRED_HEADSET,
161                         /* actual audio route type */
162                         AudioRoutesInfo.MAIN_HEADSET },
163                     {    /* expected res */
164                         R.string.default_audio_route_name_dock_speakers,
165                         /* expected name */
166                         DEFAULT_DOCK_NAME,
167                         /* expected type */
168                         MediaRoute2Info.TYPE_DOCK,
169                         /* actual audio route type */
170                         AudioRoutesInfo.MAIN_DOCK_SPEAKERS },
171                     {   /* expected res */
172                         R.string.default_audio_route_name_external_device,
173                         /* expected name */
174                         DEFAULT_HDMI_NAME,
175                         /* expected type */
176                         MediaRoute2Info.TYPE_HDMI,
177                         /* actual audio route type */
178                         AudioRoutesInfo.MAIN_HDMI },
179                     {   /* expected res */
180                         R.string.default_audio_route_name_usb,
181                         /* expected name */
182                         DEFAULT_USB_NAME,
183                         /* expected type */
184                         MediaRoute2Info.TYPE_USB_DEVICE,
185                         /* actual audio route type */
186                         AudioRoutesInfo.MAIN_USB }
187             });
188         }
189 
190         @Mock
191         private Context mContext;
192         @Mock
193         private Resources mResources;
194         @Mock
195         private AudioManager mAudioManager;
196         @Mock
197         private AudioService mAudioService;
198         @Mock
199         private DeviceRouteController.OnDeviceRouteChangedListener mOnDeviceRouteChangedListener;
200 
201         @IdRes
202         private final int mExpectedRouteNameResource;
203         private final String mExpectedRouteNameValue;
204         private final int mExpectedRouteType;
205         private final int mActualAudioRouteType;
206 
DeviceRouteInitializationTest(int expectedRouteNameResource, String expectedRouteNameValue, int expectedMediaRouteType, int actualAudioRouteType)207         public DeviceRouteInitializationTest(int expectedRouteNameResource,
208                 String expectedRouteNameValue,
209                 int expectedMediaRouteType,
210                 int actualAudioRouteType) {
211             this.mExpectedRouteNameResource = expectedRouteNameResource;
212             this.mExpectedRouteNameValue = expectedRouteNameValue;
213             this.mExpectedRouteType = expectedMediaRouteType;
214             this.mActualAudioRouteType = actualAudioRouteType;
215         }
216 
217         @Before
setUp()218         public void setUp() {
219             MockitoAnnotations.initMocks(this);
220 
221             when(mContext.getResources()).thenReturn(mResources);
222         }
223 
224         @Test
initialize_wiredRouteAvailable_deviceRouteReturnsWiredRoute()225         public void initialize_wiredRouteAvailable_deviceRouteReturnsWiredRoute() {
226             // Mocking default_audio_route_name.
227             when(mResources.getText(R.string.default_audio_route_name))
228                     .thenReturn(DEFAULT_ROUTE_NAME);
229 
230             // At first, WiredRouteController should initialize device
231             // route based on AudioService response.
232             AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo();
233             audioRoutesInfo.mainType = mActualAudioRouteType;
234             when(mAudioService.startWatchingRoutes(any())).thenReturn(audioRoutesInfo);
235 
236             when(mResources.getText(mExpectedRouteNameResource))
237                     .thenReturn(mExpectedRouteNameValue);
238 
239             LegacyDeviceRouteController deviceRouteController = new LegacyDeviceRouteController(
240                     mContext,
241                     mAudioManager,
242                     mAudioService,
243                     mOnDeviceRouteChangedListener
244             );
245 
246             MediaRoute2Info actualMediaRoute = deviceRouteController.getDeviceRoute();
247 
248             assertThat(actualMediaRoute.getType()).isEqualTo(mExpectedRouteType);
249             assertThat(TextUtils.equals(actualMediaRoute.getName(), mExpectedRouteNameValue))
250                     .isTrue();
251             // Volume did not change, so it should be set to default value (0).
252             assertThat(actualMediaRoute.getVolume()).isEqualTo(VOLUME_DEFAULT_VALUE);
253         }
254     }
255 
256     @RunWith(JUnit4.class)
257     public static class VolumeAndDeviceRoutesChangesTest {
258         @Mock
259         private Context mContext;
260         @Mock
261         private Resources mResources;
262         @Mock
263         private AudioManager mAudioManager;
264         @Mock
265         private AudioService mAudioService;
266         @Mock
267         private DeviceRouteController.OnDeviceRouteChangedListener mOnDeviceRouteChangedListener;
268 
269         @Captor
270         private ArgumentCaptor<IAudioRoutesObserver.Stub> mAudioRoutesObserverCaptor;
271 
272         private LegacyDeviceRouteController mDeviceRouteController;
273         private IAudioRoutesObserver.Stub mAudioRoutesObserver;
274 
275         @Before
setUp()276         public void setUp() {
277             MockitoAnnotations.initMocks(this);
278 
279             when(mContext.getResources()).thenReturn(mResources);
280 
281             when(mResources.getText(R.string.default_audio_route_name))
282                     .thenReturn(DEFAULT_ROUTE_NAME);
283 
284             // Setting built-in speaker as default speaker.
285             AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo();
286             audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_SPEAKER;
287             when(mAudioService.startWatchingRoutes(mAudioRoutesObserverCaptor.capture()))
288                     .thenReturn(audioRoutesInfo);
289 
290             mDeviceRouteController = new LegacyDeviceRouteController(
291                     mContext,
292                     mAudioManager,
293                     mAudioService,
294                     mOnDeviceRouteChangedListener
295             );
296 
297             mAudioRoutesObserver = mAudioRoutesObserverCaptor.getValue();
298         }
299 
300         @Test
newDeviceConnects_wiredDevice_deviceRouteReturnsWiredDevice()301         public void newDeviceConnects_wiredDevice_deviceRouteReturnsWiredDevice() {
302             // Connecting wired headset
303             AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo();
304             audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_HEADPHONES;
305 
306             when(mResources.getText(
307                     com.android.internal.R.string.default_audio_route_name_headphones))
308                     .thenReturn(DEFAULT_HEADPHONES_NAME);
309 
310             // Simulating wired device being connected.
311             callAudioRoutesObserver(audioRoutesInfo);
312 
313             MediaRoute2Info actualMediaRoute = mDeviceRouteController.getDeviceRoute();
314 
315             assertThat(actualMediaRoute.getType()).isEqualTo(MediaRoute2Info.TYPE_WIRED_HEADPHONES);
316             assertThat(TextUtils.equals(actualMediaRoute.getName(), DEFAULT_HEADPHONES_NAME))
317                     .isTrue();
318             assertThat(actualMediaRoute.getVolume()).isEqualTo(VOLUME_DEFAULT_VALUE);
319         }
320 
321         @Test
newDeviceConnects_bluetoothDevice_deviceRouteReturnsBluetoothDevice()322         public void newDeviceConnects_bluetoothDevice_deviceRouteReturnsBluetoothDevice() {
323             // Simulating bluetooth speaker being connected.
324             AudioRoutesInfo fakeBluetoothAudioRoute = createFakeBluetoothAudioRoute();
325             callAudioRoutesObserver(fakeBluetoothAudioRoute);
326 
327             MediaRoute2Info actualMediaRoute = mDeviceRouteController.getDeviceRoute();
328 
329             assertThat(actualMediaRoute.getType()).isEqualTo(MediaRoute2Info.TYPE_BUILTIN_SPEAKER);
330             assertThat(TextUtils.equals(actualMediaRoute.getName(), DEFAULT_ROUTE_NAME))
331                     .isTrue();
332             assertThat(actualMediaRoute.getVolume()).isEqualTo(VOLUME_DEFAULT_VALUE);
333         }
334 
335         @Test
updateVolume_differentValue_updatesDeviceRouteVolume()336         public void updateVolume_differentValue_updatesDeviceRouteVolume() {
337             MediaRoute2Info actualMediaRoute = mDeviceRouteController.getDeviceRoute();
338             assertThat(actualMediaRoute.getVolume()).isEqualTo(VOLUME_DEFAULT_VALUE);
339 
340             assertThat(mDeviceRouteController.updateVolume(VOLUME_VALUE_SAMPLE_1)).isTrue();
341 
342             actualMediaRoute = mDeviceRouteController.getDeviceRoute();
343             assertThat(actualMediaRoute.getVolume()).isEqualTo(VOLUME_VALUE_SAMPLE_1);
344         }
345 
346         @Test
updateVolume_sameValue_returnsFalse()347         public void updateVolume_sameValue_returnsFalse() {
348             assertThat(mDeviceRouteController.updateVolume(VOLUME_VALUE_SAMPLE_1)).isTrue();
349             assertThat(mDeviceRouteController.updateVolume(VOLUME_VALUE_SAMPLE_1)).isFalse();
350         }
351 
352         /**
353          * Simulates {@link IAudioRoutesObserver.Stub#dispatchAudioRoutesChanged(AudioRoutesInfo)}
354          * from {@link AudioService}. This happens when there is a wired route change,
355          * like a wired headset being connected.
356          *
357          * @param audioRoutesInfo updated state of connected wired device
358          */
callAudioRoutesObserver(AudioRoutesInfo audioRoutesInfo)359         private void callAudioRoutesObserver(AudioRoutesInfo audioRoutesInfo) {
360             try {
361                 // this is a captured observer implementation
362                 // from WiredRoutesController's AudioService#startWatchingRoutes call
363                 mAudioRoutesObserver.dispatchAudioRoutesChanged(audioRoutesInfo);
364             } catch (RemoteException exception) {
365                 // Should not happen since the object is mocked.
366                 assertWithMessage("An unexpected RemoteException happened.").fail();
367             }
368         }
369     }
370 
371 }
372