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