Merge "Extract wired routes logic into a separate class." into udc-dev

This commit is contained in:
Alex Dadukin
2023-02-28 11:31:36 +00:00
committed by Android (Google) Code Review
3 changed files with 593 additions and 91 deletions

View File

@@ -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.
*
* <p>A device route is a system wired route, for example, built-in speaker, wired
* headsets and headphones, dock, hdmi, or usb devices.
*
* <p>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);
}
}
}

View File

@@ -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();
}

View File

@@ -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<Object[]> 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<IAudioRoutesObserver.Stub> 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();
}
}
}
}