From 6f4ccd59e824e79b7c91ae47cfa5a72aec460c1c Mon Sep 17 00:00:00 2001 From: Alex Dadukin Date: Wed, 11 Jan 2023 17:59:25 +0000 Subject: [PATCH] Extract wired routes logic into a separate class. Bug: b/255495104 Test: atest SystemMediaRouter2Test DeviceRouteControllerTest Change-Id: I6c59e4ef61ca060141c326cc515b822272be0a3c --- .../server/media/DeviceRouteController.java | 193 +++++++++ .../media/SystemMediaRoute2Provider.java | 120 ++---- .../media/DeviceRouteControllerTest.java | 371 ++++++++++++++++++ 3 files changed, 593 insertions(+), 91 deletions(-) create mode 100644 services/core/java/com/android/server/media/DeviceRouteController.java create mode 100644 services/tests/servicestests/src/com/android/server/media/DeviceRouteControllerTest.java diff --git a/services/core/java/com/android/server/media/DeviceRouteController.java b/services/core/java/com/android/server/media/DeviceRouteController.java new file mode 100644 index 0000000000000..8bd6416a6ddb6 --- /dev/null +++ b/services/core/java/com/android/server/media/DeviceRouteController.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.media; + +import static android.media.MediaRoute2Info.FEATURE_LIVE_AUDIO; +import static android.media.MediaRoute2Info.FEATURE_LIVE_VIDEO; +import static android.media.MediaRoute2Info.FEATURE_LOCAL_PLAYBACK; +import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER; +import static android.media.MediaRoute2Info.TYPE_DOCK; +import static android.media.MediaRoute2Info.TYPE_HDMI; +import static android.media.MediaRoute2Info.TYPE_USB_DEVICE; +import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES; +import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.media.AudioManager; +import android.media.AudioRoutesInfo; +import android.media.IAudioRoutesObserver; +import android.media.IAudioService; +import android.media.MediaRoute2Info; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.util.Slog; + +import com.android.internal.R; +import com.android.internal.annotations.VisibleForTesting; + +import java.util.Objects; + +/** + * Controls device routes. + * + *

A device route is a system wired route, for example, built-in speaker, wired + * headsets and headphones, dock, hdmi, or usb devices. + * + *

Thread safe. + * + * @see SystemMediaRoute2Provider + */ +/* package */ final class DeviceRouteController { + + private static final String TAG = "WiredRoutesController"; + + private static final String DEVICE_ROUTE_ID = "DEVICE_ROUTE"; + + @NonNull + private final Context mContext; + @NonNull + private final AudioManager mAudioManager; + @NonNull + private final IAudioService mAudioService; + + @NonNull + private final OnDeviceRouteChangedListener mOnDeviceRouteChangedListener; + @NonNull + private final AudioRoutesObserver mAudioRoutesObserver = new AudioRoutesObserver(); + + private int mDeviceVolume; + private MediaRoute2Info mDeviceRoute; + + /* package */ static DeviceRouteController createInstance(@NonNull Context context, + @NonNull OnDeviceRouteChangedListener onDeviceRouteChangedListener) { + AudioManager audioManager = context.getSystemService(AudioManager.class); + IAudioService audioService = IAudioService.Stub.asInterface( + ServiceManager.getService(Context.AUDIO_SERVICE)); + + return new DeviceRouteController(context, + audioManager, + audioService, + onDeviceRouteChangedListener); + } + + @VisibleForTesting + /* package */ DeviceRouteController(@NonNull Context context, + @NonNull AudioManager audioManager, + @NonNull IAudioService audioService, + @NonNull OnDeviceRouteChangedListener onDeviceRouteChangedListener) { + Objects.requireNonNull(context); + Objects.requireNonNull(audioManager); + Objects.requireNonNull(audioService); + Objects.requireNonNull(onDeviceRouteChangedListener); + + mContext = context; + mOnDeviceRouteChangedListener = onDeviceRouteChangedListener; + + mAudioManager = audioManager; + mAudioService = audioService; + + AudioRoutesInfo newAudioRoutes = null; + try { + newAudioRoutes = mAudioService.startWatchingRoutes(mAudioRoutesObserver); + } catch (RemoteException e) { + Slog.w(TAG, "Cannot connect to audio service to start listen to routes", e); + } + + mDeviceRoute = createRouteFromAudioInfo(newAudioRoutes); + } + + @NonNull + /* package */ synchronized MediaRoute2Info getDeviceRoute() { + return mDeviceRoute; + } + + /* package */ synchronized boolean updateVolume(int volume) { + if (mDeviceVolume == volume) { + return false; + } + + mDeviceVolume = volume; + mDeviceRoute = new MediaRoute2Info.Builder(mDeviceRoute) + .setVolume(volume) + .build(); + + return true; + } + + private MediaRoute2Info createRouteFromAudioInfo(@Nullable AudioRoutesInfo newRoutes) { + int name = R.string.default_audio_route_name; + int type = TYPE_BUILTIN_SPEAKER; + + if (newRoutes != null) { + if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADPHONES) != 0) { + type = TYPE_WIRED_HEADPHONES; + name = com.android.internal.R.string.default_audio_route_name_headphones; + } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADSET) != 0) { + type = TYPE_WIRED_HEADSET; + name = com.android.internal.R.string.default_audio_route_name_headphones; + } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) { + type = TYPE_DOCK; + name = com.android.internal.R.string.default_audio_route_name_dock_speakers; + } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HDMI) != 0) { + type = TYPE_HDMI; + name = com.android.internal.R.string.default_audio_route_name_external_device; + } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_USB) != 0) { + type = TYPE_USB_DEVICE; + name = com.android.internal.R.string.default_audio_route_name_usb; + } + } + + synchronized (this) { + return new MediaRoute2Info.Builder( + DEVICE_ROUTE_ID, mContext.getResources().getText(name).toString()) + .setVolumeHandling(mAudioManager.isVolumeFixed() + ? MediaRoute2Info.PLAYBACK_VOLUME_FIXED + : MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE) + .setVolume(mDeviceVolume) + .setVolumeMax(mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)) + .setType(type) + .addFeature(FEATURE_LIVE_AUDIO) + .addFeature(FEATURE_LIVE_VIDEO) + .addFeature(FEATURE_LOCAL_PLAYBACK) + .setConnectionState(MediaRoute2Info.CONNECTION_STATE_CONNECTED) + .build(); + } + } + + private void notifyDeviceRouteUpdate(@NonNull MediaRoute2Info deviceRoute) { + mOnDeviceRouteChangedListener.onDeviceRouteChanged(deviceRoute); + } + + /* package */ interface OnDeviceRouteChangedListener { + void onDeviceRouteChanged(@NonNull MediaRoute2Info deviceRoute); + } + + private class AudioRoutesObserver extends IAudioRoutesObserver.Stub { + + @Override + public void dispatchAudioRoutesChanged(AudioRoutesInfo newAudioRoutes) { + MediaRoute2Info deviceRoute = createRouteFromAudioInfo(newAudioRoutes); + synchronized (DeviceRouteController.this) { + mDeviceRoute = deviceRoute; + } + notifyDeviceRouteUpdate(deviceRoute); + } + } + +} diff --git a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java index 76ff19fcb3229..638e81a5df36c 100644 --- a/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java +++ b/services/core/java/com/android/server/media/SystemMediaRoute2Provider.java @@ -16,25 +16,12 @@ package com.android.server.media; -import static android.media.MediaRoute2Info.FEATURE_LIVE_AUDIO; -import static android.media.MediaRoute2Info.FEATURE_LIVE_VIDEO; -import static android.media.MediaRoute2Info.FEATURE_LOCAL_PLAYBACK; -import static android.media.MediaRoute2Info.TYPE_BUILTIN_SPEAKER; -import static android.media.MediaRoute2Info.TYPE_DOCK; -import static android.media.MediaRoute2Info.TYPE_HDMI; -import static android.media.MediaRoute2Info.TYPE_USB_DEVICE; -import static android.media.MediaRoute2Info.TYPE_WIRED_HEADPHONES; -import static android.media.MediaRoute2Info.TYPE_WIRED_HEADSET; - import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.media.AudioManager; -import android.media.AudioRoutesInfo; -import android.media.IAudioRoutesObserver; -import android.media.IAudioService; import android.media.MediaRoute2Info; import android.media.MediaRoute2ProviderInfo; import android.media.MediaRoute2ProviderService; @@ -43,14 +30,11 @@ import android.media.RoutingSessionInfo; import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import android.os.RemoteException; -import android.os.ServiceManager; import android.os.UserHandle; import android.text.TextUtils; import android.util.Log; import android.util.Slog; -import com.android.internal.R; import com.android.internal.annotations.GuardedBy; import java.util.Objects; @@ -68,23 +52,21 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { SystemMediaRoute2Provider.class.getName()); static final String DEFAULT_ROUTE_ID = "DEFAULT_ROUTE"; - static final String DEVICE_ROUTE_ID = "DEVICE_ROUTE"; static final String SYSTEM_SESSION_ID = "SYSTEM_SESSION"; private final AudioManager mAudioManager; - private final IAudioService mAudioService; private final Handler mHandler; private final Context mContext; private final UserHandle mUser; + + private final DeviceRouteController mDeviceRouteController; private final BluetoothRouteController mBtRouteProvider; private String mSelectedRouteId; // For apps without MODIFYING_AUDIO_ROUTING permission. // This should be the currently selected route. MediaRoute2Info mDefaultRoute; - MediaRoute2Info mDeviceRoute; RoutingSessionInfo mDefaultSessionInfo; - int mDeviceVolume; private final AudioManagerBroadcastReceiver mAudioReceiver = new AudioManagerBroadcastReceiver(); @@ -93,19 +75,6 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { @GuardedBy("mRequestLock") private volatile SessionCreationRequest mPendingSessionCreationRequest; - final IAudioRoutesObserver.Stub mAudioRoutesObserver = new IAudioRoutesObserver.Stub() { - @Override - public void dispatchAudioRoutesChanged(final AudioRoutesInfo newRoutes) { - mHandler.post(() -> { - updateDeviceRoute(newRoutes); - notifyProviderState(); - if (updateSessionInfosIfNeeded()) { - notifySessionInfoUpdated(); - } - }); - } - }; - SystemMediaRoute2Provider(Context context, UserHandle user) { super(COMPONENT_NAME); mIsSystemRouteProvider = true; @@ -114,8 +83,6 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { mHandler = new Handler(Looper.getMainLooper()); mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - mAudioService = IAudioService.Stub.asInterface( - ServiceManager.getService(Context.AUDIO_SERVICE)); mBtRouteProvider = BluetoothRouteController.createInstance(context, (routes) -> { publishProviderState(); @@ -124,15 +91,18 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { } }); - AudioRoutesInfo newAudioRoutes = null; - try { - newAudioRoutes = mAudioService.startWatchingRoutes(mAudioRoutesObserver); - } catch (RemoteException e) { - } + mDeviceRouteController = DeviceRouteController.createInstance(context, (deviceRoute) -> { + mHandler.post(() -> { + publishProviderState(); + if (updateSessionInfosIfNeeded()) { + notifySessionInfoUpdated(); + } + }); + }); - // The methods below should be called after all fields are initialized, as they + // These methods below should be called after all fields are initialized, as they // access the fields inside. - updateDeviceRoute(newAudioRoutes); + updateProviderState(); updateSessionInfosIfNeeded(); } @@ -216,7 +186,9 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { // The currently selected route is the default route. return; } - if (TextUtils.equals(routeId, mDeviceRoute.getId())) { + + MediaRoute2Info deviceRoute = mDeviceRouteController.getDeviceRoute(); + if (TextUtils.equals(routeId, deviceRoute.getId())) { mBtRouteProvider.transferTo(null); } else { mBtRouteProvider.transferTo(routeId); @@ -254,9 +226,12 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { if (mSessionInfos.isEmpty()) { return null; } + + MediaRoute2Info deviceRoute = mDeviceRouteController.getDeviceRoute(); + RoutingSessionInfo.Builder builder = new RoutingSessionInfo.Builder( SYSTEM_SESSION_ID, packageName).setSystemSession(true); - builder.addSelectedRoute(mDeviceRoute.getId()); + builder.addSelectedRoute(deviceRoute.getId()); for (MediaRoute2Info route : mBtRouteProvider.getAllBluetoothRoutes()) { builder.addTransferableRoute(route.getId()); } @@ -264,47 +239,12 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { } } - private void updateDeviceRoute(AudioRoutesInfo newRoutes) { - int name = R.string.default_audio_route_name; - int type = TYPE_BUILTIN_SPEAKER; - if (newRoutes != null) { - if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADPHONES) != 0) { - type = TYPE_WIRED_HEADPHONES; - name = com.android.internal.R.string.default_audio_route_name_headphones; - } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADSET) != 0) { - type = TYPE_WIRED_HEADSET; - name = com.android.internal.R.string.default_audio_route_name_headphones; - } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) { - type = TYPE_DOCK; - name = com.android.internal.R.string.default_audio_route_name_dock_speakers; - } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HDMI) != 0) { - type = TYPE_HDMI; - name = com.android.internal.R.string.default_audio_route_name_external_device; - } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_USB) != 0) { - type = TYPE_USB_DEVICE; - name = com.android.internal.R.string.default_audio_route_name_usb; - } - } - - mDeviceRoute = new MediaRoute2Info.Builder( - DEVICE_ROUTE_ID, mContext.getResources().getText(name).toString()) - .setVolumeHandling(mAudioManager.isVolumeFixed() - ? MediaRoute2Info.PLAYBACK_VOLUME_FIXED - : MediaRoute2Info.PLAYBACK_VOLUME_VARIABLE) - .setVolume(mDeviceVolume) - .setVolumeMax(mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)) - .setType(type) - .addFeature(FEATURE_LIVE_AUDIO) - .addFeature(FEATURE_LIVE_VIDEO) - .addFeature(FEATURE_LOCAL_PLAYBACK) - .setConnectionState(MediaRoute2Info.CONNECTION_STATE_CONNECTED) - .build(); - updateProviderState(); - } - private void updateProviderState() { MediaRoute2ProviderInfo.Builder builder = new MediaRoute2ProviderInfo.Builder(); - builder.addRoute(mDeviceRoute); + + // We must have a device route in the provider info. + builder.addRoute(mDeviceRouteController.getDeviceRoute()); + for (MediaRoute2Info route : mBtRouteProvider.getAllBluetoothRoutes()) { builder.addRoute(route); } @@ -327,11 +267,12 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { SYSTEM_SESSION_ID, "" /* clientPackageName */) .setSystemSession(true); - MediaRoute2Info selectedRoute = mDeviceRoute; + MediaRoute2Info deviceRoute = mDeviceRouteController.getDeviceRoute(); + MediaRoute2Info selectedRoute = deviceRoute; MediaRoute2Info selectedBtRoute = mBtRouteProvider.getSelectedRoute(); if (selectedBtRoute != null) { selectedRoute = selectedBtRoute; - builder.addTransferableRoute(mDeviceRoute.getId()); + builder.addTransferableRoute(deviceRoute.getId()); } mSelectedRouteId = selectedRoute.getId(); mDefaultRoute = new MediaRoute2Info.Builder(DEFAULT_ROUTE_ID, selectedRoute) @@ -423,12 +364,9 @@ class SystemMediaRoute2Provider extends MediaRoute2Provider { if (mBtRouteProvider.updateVolumeForDevices(devices, volume)) { return; } - if (mDeviceVolume != volume) { - mDeviceVolume = volume; - mDeviceRoute = new MediaRoute2Info.Builder(mDeviceRoute) - .setVolume(volume) - .build(); - } + + mDeviceRouteController.updateVolume(volume); + publishProviderState(); } diff --git a/services/tests/servicestests/src/com/android/server/media/DeviceRouteControllerTest.java b/services/tests/servicestests/src/com/android/server/media/DeviceRouteControllerTest.java new file mode 100644 index 0000000000000..24ed42cab63ad --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/media/DeviceRouteControllerTest.java @@ -0,0 +1,371 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.media; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import android.annotation.IdRes; +import android.content.Context; +import android.content.res.Resources; +import android.media.AudioManager; +import android.media.AudioRoutesInfo; +import android.media.IAudioRoutesObserver; +import android.media.MediaRoute2Info; +import android.os.RemoteException; +import android.text.TextUtils; + +import com.android.internal.R; +import com.android.server.audio.AudioService; + +import org.junit.Before; +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.junit.runners.Parameterized; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Arrays; +import java.util.Collection; + +@RunWith(Enclosed.class) +public class DeviceRouteControllerTest { + + private static final String DEFAULT_ROUTE_NAME = "default_route"; + private static final String DEFAULT_HEADPHONES_NAME = "headphone"; + private static final String DEFAULT_HEADSET_NAME = "headset"; + private static final String DEFAULT_DOCK_NAME = "dock"; + private static final String DEFAULT_HDMI_NAME = "hdmi"; + private static final String DEFAULT_USB_NAME = "usb"; + private static final int VOLUME_DEFAULT_VALUE = 0; + private static final int VOLUME_VALUE_SAMPLE_1 = 10; + + private static AudioRoutesInfo createFakeBluetoothAudioRoute() { + AudioRoutesInfo btRouteInfo = new AudioRoutesInfo(); + btRouteInfo.mainType = AudioRoutesInfo.MAIN_SPEAKER; + btRouteInfo.bluetoothName = "bt_device"; + return btRouteInfo; + } + + @RunWith(JUnit4.class) + public static class DefaultDeviceRouteValueTest { + @Mock + private Context mContext; + @Mock + private Resources mResources; + @Mock + private AudioManager mAudioManager; + @Mock + private AudioService mAudioService; + @Mock + private DeviceRouteController.OnDeviceRouteChangedListener mOnDeviceRouteChangedListener; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + when(mContext.getResources()).thenReturn(mResources); + } + + @Test + public void initialize_noRoutesInfo_defaultRouteIsNotNull() { + // Mocking default_audio_route_name. + when(mResources.getText(R.string.default_audio_route_name)) + .thenReturn(DEFAULT_ROUTE_NAME); + + // Default route should be initialized even when AudioService returns null. + when(mAudioService.startWatchingRoutes(any())).thenReturn(null); + + DeviceRouteController deviceRouteController = new DeviceRouteController( + mContext, + mAudioManager, + mAudioService, + mOnDeviceRouteChangedListener + ); + + MediaRoute2Info actualMediaRoute = deviceRouteController.getDeviceRoute(); + + assertThat(actualMediaRoute.getType()).isEqualTo(MediaRoute2Info.TYPE_BUILTIN_SPEAKER); + assertThat(TextUtils.equals(actualMediaRoute.getName(), DEFAULT_ROUTE_NAME)) + .isTrue(); + assertThat(actualMediaRoute.getVolume()).isEqualTo(VOLUME_DEFAULT_VALUE); + } + + @Test + public void initialize_bluetoothRouteAvailable_deviceRouteReturnsDefaultRoute() { + // Mocking default_audio_route_name. + when(mResources.getText(R.string.default_audio_route_name)) + .thenReturn(DEFAULT_ROUTE_NAME); + + // This route should be ignored. + AudioRoutesInfo fakeBluetoothAudioRoute = createFakeBluetoothAudioRoute(); + when(mAudioService.startWatchingRoutes(any())).thenReturn(fakeBluetoothAudioRoute); + + DeviceRouteController deviceRouteController = new DeviceRouteController( + mContext, + mAudioManager, + mAudioService, + mOnDeviceRouteChangedListener + ); + + MediaRoute2Info actualMediaRoute = deviceRouteController.getDeviceRoute(); + + assertThat(actualMediaRoute.getType()).isEqualTo(MediaRoute2Info.TYPE_BUILTIN_SPEAKER); + assertThat(TextUtils.equals(actualMediaRoute.getName(), DEFAULT_ROUTE_NAME)) + .isTrue(); + assertThat(actualMediaRoute.getVolume()).isEqualTo(VOLUME_DEFAULT_VALUE); + } + } + + @RunWith(Parameterized.class) + public static class DeviceRouteInitializationTest { + + @Parameterized.Parameters + public static Collection data() { + return Arrays.asList(new Object[][] { + { /* expected res */ + com.android.internal.R.string.default_audio_route_name_headphones, + /* expected name */ + DEFAULT_HEADPHONES_NAME, + /* expected type */ + MediaRoute2Info.TYPE_WIRED_HEADPHONES, + /* actual audio route type */ + AudioRoutesInfo.MAIN_HEADPHONES }, + { /* expected res */ + com.android.internal.R.string.default_audio_route_name_headphones, + /* expected name */ + DEFAULT_HEADSET_NAME, + /* expected type */ + MediaRoute2Info.TYPE_WIRED_HEADSET, + /* actual audio route type */ + AudioRoutesInfo.MAIN_HEADSET }, + { /* expected res */ + R.string.default_audio_route_name_dock_speakers, + /* expected name */ + DEFAULT_DOCK_NAME, + /* expected type */ + MediaRoute2Info.TYPE_DOCK, + /* actual audio route type */ + AudioRoutesInfo.MAIN_DOCK_SPEAKERS }, + { /* expected res */ + R.string.default_audio_route_name_external_device, + /* expected name */ + DEFAULT_HDMI_NAME, + /* expected type */ + MediaRoute2Info.TYPE_HDMI, + /* actual audio route type */ + AudioRoutesInfo.MAIN_HDMI }, + { /* expected res */ + R.string.default_audio_route_name_usb, + /* expected name */ + DEFAULT_USB_NAME, + /* expected type */ + MediaRoute2Info.TYPE_USB_DEVICE, + /* actual audio route type */ + AudioRoutesInfo.MAIN_USB } + }); + } + + @Mock + private Context mContext; + @Mock + private Resources mResources; + @Mock + private AudioManager mAudioManager; + @Mock + private AudioService mAudioService; + @Mock + private DeviceRouteController.OnDeviceRouteChangedListener mOnDeviceRouteChangedListener; + + @IdRes + private final int mExpectedRouteNameResource; + private final String mExpectedRouteNameValue; + private final int mExpectedRouteType; + private final int mActualAudioRouteType; + + public DeviceRouteInitializationTest(int expectedRouteNameResource, + String expectedRouteNameValue, + int expectedMediaRouteType, + int actualAudioRouteType) { + this.mExpectedRouteNameResource = expectedRouteNameResource; + this.mExpectedRouteNameValue = expectedRouteNameValue; + this.mExpectedRouteType = expectedMediaRouteType; + this.mActualAudioRouteType = actualAudioRouteType; + } + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + when(mContext.getResources()).thenReturn(mResources); + } + + @Test + public void initialize_wiredRouteAvailable_deviceRouteReturnsWiredRoute() { + // Mocking default_audio_route_name. + when(mResources.getText(R.string.default_audio_route_name)) + .thenReturn(DEFAULT_ROUTE_NAME); + + // At first, WiredRouteController should initialize device + // route based on AudioService response. + AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo(); + audioRoutesInfo.mainType = mActualAudioRouteType; + when(mAudioService.startWatchingRoutes(any())).thenReturn(audioRoutesInfo); + + when(mResources.getText(mExpectedRouteNameResource)) + .thenReturn(mExpectedRouteNameValue); + + DeviceRouteController deviceRouteController = new DeviceRouteController( + mContext, + mAudioManager, + mAudioService, + mOnDeviceRouteChangedListener + ); + + MediaRoute2Info actualMediaRoute = deviceRouteController.getDeviceRoute(); + + assertThat(actualMediaRoute.getType()).isEqualTo(mExpectedRouteType); + assertThat(TextUtils.equals(actualMediaRoute.getName(), mExpectedRouteNameValue)) + .isTrue(); + // Volume did not change, so it should be set to default value (0). + assertThat(actualMediaRoute.getVolume()).isEqualTo(VOLUME_DEFAULT_VALUE); + } + } + + @RunWith(JUnit4.class) + public static class VolumeAndDeviceRoutesChangesTest { + @Mock + private Context mContext; + @Mock + private Resources mResources; + @Mock + private AudioManager mAudioManager; + @Mock + private AudioService mAudioService; + @Mock + private DeviceRouteController.OnDeviceRouteChangedListener mOnDeviceRouteChangedListener; + + @Captor + private ArgumentCaptor mAudioRoutesObserverCaptor; + + private DeviceRouteController mDeviceRouteController; + private IAudioRoutesObserver.Stub mAudioRoutesObserver; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + when(mContext.getResources()).thenReturn(mResources); + + when(mResources.getText(R.string.default_audio_route_name)) + .thenReturn(DEFAULT_ROUTE_NAME); + + // Setting built-in speaker as default speaker. + AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo(); + audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_SPEAKER; + when(mAudioService.startWatchingRoutes(mAudioRoutesObserverCaptor.capture())) + .thenReturn(audioRoutesInfo); + + mDeviceRouteController = new DeviceRouteController( + mContext, + mAudioManager, + mAudioService, + mOnDeviceRouteChangedListener + ); + + mAudioRoutesObserver = mAudioRoutesObserverCaptor.getValue(); + } + + @Test + public void newDeviceConnects_wiredDevice_deviceRouteReturnsWiredDevice() { + // Connecting wired headset + AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo(); + audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_HEADPHONES; + + when(mResources.getText( + com.android.internal.R.string.default_audio_route_name_headphones)) + .thenReturn(DEFAULT_HEADPHONES_NAME); + + // Simulating wired device being connected. + callAudioRoutesObserver(audioRoutesInfo); + + MediaRoute2Info actualMediaRoute = mDeviceRouteController.getDeviceRoute(); + + assertThat(actualMediaRoute.getType()).isEqualTo(MediaRoute2Info.TYPE_WIRED_HEADPHONES); + assertThat(TextUtils.equals(actualMediaRoute.getName(), DEFAULT_HEADPHONES_NAME)) + .isTrue(); + assertThat(actualMediaRoute.getVolume()).isEqualTo(VOLUME_DEFAULT_VALUE); + } + + @Test + public void newDeviceConnects_bluetoothDevice_deviceRouteReturnsBluetoothDevice() { + // Simulating bluetooth speaker being connected. + AudioRoutesInfo fakeBluetoothAudioRoute = createFakeBluetoothAudioRoute(); + callAudioRoutesObserver(fakeBluetoothAudioRoute); + + MediaRoute2Info actualMediaRoute = mDeviceRouteController.getDeviceRoute(); + + assertThat(actualMediaRoute.getType()).isEqualTo(MediaRoute2Info.TYPE_BUILTIN_SPEAKER); + assertThat(TextUtils.equals(actualMediaRoute.getName(), DEFAULT_ROUTE_NAME)) + .isTrue(); + assertThat(actualMediaRoute.getVolume()).isEqualTo(VOLUME_DEFAULT_VALUE); + } + + @Test + public void updateVolume_differentValue_updatesDeviceRouteVolume() { + MediaRoute2Info actualMediaRoute = mDeviceRouteController.getDeviceRoute(); + assertThat(actualMediaRoute.getVolume()).isEqualTo(VOLUME_DEFAULT_VALUE); + + assertThat(mDeviceRouteController.updateVolume(VOLUME_VALUE_SAMPLE_1)).isTrue(); + + actualMediaRoute = mDeviceRouteController.getDeviceRoute(); + assertThat(actualMediaRoute.getVolume()).isEqualTo(VOLUME_VALUE_SAMPLE_1); + } + + @Test + public void updateVolume_sameValue_returnsFalse() { + assertThat(mDeviceRouteController.updateVolume(VOLUME_VALUE_SAMPLE_1)).isTrue(); + assertThat(mDeviceRouteController.updateVolume(VOLUME_VALUE_SAMPLE_1)).isFalse(); + } + + /** + * Simulates {@link IAudioRoutesObserver.Stub#dispatchAudioRoutesChanged(AudioRoutesInfo)} + * from {@link AudioService}. This happens when there is a wired route change, + * like a wired headset being connected. + * + * @param audioRoutesInfo updated state of connected wired device + */ + private void callAudioRoutesObserver(AudioRoutesInfo audioRoutesInfo) { + try { + // this is a captured observer implementation + // from WiredRoutesController's AudioService#startWatchingRoutes call + mAudioRoutesObserver.dispatchAudioRoutesChanged(audioRoutesInfo); + } catch (RemoteException exception) { + // Should not happen since the object is mocked. + assertWithMessage("An unexpected RemoteException happened.").fail(); + } + } + } + +}