Merge "Implement selectRoute in AudioPoliciesDeviceRouteController" into udc-dev am: c0fca28778 am: a503c92de7
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/21673440 Change-Id: Ia5990d6d26f287ca7b77c59fbd26f50677560806 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
This commit is contained in:
@@ -62,7 +62,11 @@ import java.util.Objects;
|
||||
private final AudioRoutesObserver mAudioRoutesObserver = new AudioRoutesObserver();
|
||||
|
||||
private int mDeviceVolume;
|
||||
|
||||
@NonNull
|
||||
private MediaRoute2Info mDeviceRoute;
|
||||
@Nullable
|
||||
private MediaRoute2Info mSelectedRoute;
|
||||
|
||||
@VisibleForTesting
|
||||
/* package */ AudioPoliciesDeviceRouteController(@NonNull Context context,
|
||||
@@ -91,14 +95,26 @@ import java.util.Objects;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean selectRoute(@Nullable Integer type) {
|
||||
// No-op as the controller does not support selection from the outside of the class.
|
||||
return false;
|
||||
public synchronized boolean selectRoute(@Nullable Integer type) {
|
||||
if (type == null) {
|
||||
mSelectedRoute = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!isDeviceRouteType(type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
mSelectedRoute = createRouteFromAudioInfo(type);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public synchronized MediaRoute2Info getDeviceRoute() {
|
||||
if (mSelectedRoute != null) {
|
||||
return mSelectedRoute;
|
||||
}
|
||||
return mDeviceRoute;
|
||||
}
|
||||
|
||||
@@ -109,6 +125,13 @@ import java.util.Objects;
|
||||
}
|
||||
|
||||
mDeviceVolume = volume;
|
||||
|
||||
if (mSelectedRoute != null) {
|
||||
mSelectedRoute = new MediaRoute2Info.Builder(mSelectedRoute)
|
||||
.setVolume(volume)
|
||||
.build();
|
||||
}
|
||||
|
||||
mDeviceRoute = new MediaRoute2Info.Builder(mDeviceRoute)
|
||||
.setVolume(volume)
|
||||
.build();
|
||||
@@ -116,29 +139,47 @@ import java.util.Objects;
|
||||
return true;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
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 = R.string.default_audio_route_name_headphones;
|
||||
} else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADSET) != 0) {
|
||||
type = TYPE_WIRED_HEADSET;
|
||||
name = R.string.default_audio_route_name_headphones;
|
||||
} else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) {
|
||||
type = TYPE_DOCK;
|
||||
name = R.string.default_audio_route_name_dock_speakers;
|
||||
} else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HDMI) != 0) {
|
||||
type = TYPE_HDMI;
|
||||
name = R.string.default_audio_route_name_external_device;
|
||||
} else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_USB) != 0) {
|
||||
type = TYPE_USB_DEVICE;
|
||||
name = R.string.default_audio_route_name_usb;
|
||||
}
|
||||
}
|
||||
|
||||
return createRouteFromAudioInfo(type);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private MediaRoute2Info createRouteFromAudioInfo(@MediaRoute2Info.Type int type) {
|
||||
int name = R.string.default_audio_route_name;
|
||||
|
||||
switch (type) {
|
||||
case TYPE_WIRED_HEADPHONES:
|
||||
case TYPE_WIRED_HEADSET:
|
||||
name = R.string.default_audio_route_name_headphones;
|
||||
break;
|
||||
case TYPE_DOCK:
|
||||
name = R.string.default_audio_route_name_dock_speakers;
|
||||
break;
|
||||
case TYPE_HDMI:
|
||||
name = R.string.default_audio_route_name_external_device;
|
||||
break;
|
||||
case TYPE_USB_DEVICE:
|
||||
name = R.string.default_audio_route_name_usb;
|
||||
break;
|
||||
}
|
||||
|
||||
synchronized (this) {
|
||||
return new MediaRoute2Info.Builder(
|
||||
DEVICE_ROUTE_ID, mContext.getResources().getText(name).toString())
|
||||
@@ -156,19 +197,43 @@ import java.util.Objects;
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyDeviceRouteUpdate(@NonNull MediaRoute2Info deviceRoute) {
|
||||
mOnDeviceRouteChangedListener.onDeviceRouteChanged(deviceRoute);
|
||||
/**
|
||||
* Checks if the given type is a device route.
|
||||
*
|
||||
* <p>Device route means a route which is either built-in or wired to the current device.
|
||||
*
|
||||
* @param type specifies the type of the device.
|
||||
* @return {@code true} if the device is wired or built-in and {@code false} otherwise.
|
||||
*/
|
||||
private boolean isDeviceRouteType(@MediaRoute2Info.Type int type) {
|
||||
switch (type) {
|
||||
case TYPE_BUILTIN_SPEAKER:
|
||||
case TYPE_WIRED_HEADPHONES:
|
||||
case TYPE_WIRED_HEADSET:
|
||||
case TYPE_DOCK:
|
||||
case TYPE_HDMI:
|
||||
case TYPE_USB_DEVICE:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private class AudioRoutesObserver extends IAudioRoutesObserver.Stub {
|
||||
|
||||
@Override
|
||||
public void dispatchAudioRoutesChanged(AudioRoutesInfo newAudioRoutes) {
|
||||
boolean isDeviceRouteChanged;
|
||||
MediaRoute2Info deviceRoute = createRouteFromAudioInfo(newAudioRoutes);
|
||||
|
||||
synchronized (AudioPoliciesDeviceRouteController.this) {
|
||||
mDeviceRoute = deviceRoute;
|
||||
isDeviceRouteChanged = mSelectedRoute == null;
|
||||
}
|
||||
|
||||
if (isDeviceRouteChanged) {
|
||||
mOnDeviceRouteChangedListener.onDeviceRouteChanged(deviceRoute);
|
||||
}
|
||||
notifyDeviceRouteUpdate(deviceRoute);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
/*
|
||||
* 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.Mockito.anyInt;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
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 com.android.internal.R;
|
||||
import com.android.server.audio.AudioService;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.JUnit4;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
@RunWith(JUnit4.class)
|
||||
public class AudioPoliciesDeviceRouteControllerTest {
|
||||
|
||||
private static final String ROUTE_NAME_DEFAULT = "default";
|
||||
private static final String ROUTE_NAME_DOCK = "dock";
|
||||
private static final String ROUTE_NAME_HEADPHONES = "headphones";
|
||||
|
||||
private static final int VOLUME_SAMPLE_1 = 25;
|
||||
|
||||
@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 AudioPoliciesDeviceRouteController mController;
|
||||
|
||||
private IAudioRoutesObserver.Stub mAudioRoutesObserver;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
|
||||
when(mContext.getResources()).thenReturn(mResources);
|
||||
when(mResources.getText(anyInt())).thenReturn(ROUTE_NAME_DEFAULT);
|
||||
|
||||
// Setting built-in speaker as default speaker.
|
||||
AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo();
|
||||
audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_SPEAKER;
|
||||
when(mAudioService.startWatchingRoutes(mAudioRoutesObserverCaptor.capture()))
|
||||
.thenReturn(audioRoutesInfo);
|
||||
|
||||
mController = new AudioPoliciesDeviceRouteController(
|
||||
mContext, mAudioManager, mAudioService, mOnDeviceRouteChangedListener);
|
||||
|
||||
mAudioRoutesObserver = mAudioRoutesObserverCaptor.getValue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getDeviceRoute_noSelectedRoutes_returnsDefaultDevice() {
|
||||
MediaRoute2Info route2Info = mController.getDeviceRoute();
|
||||
|
||||
assertThat(route2Info.getName()).isEqualTo(ROUTE_NAME_DEFAULT);
|
||||
assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_BUILTIN_SPEAKER);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getDeviceRoute_audioRouteHasChanged_returnsRouteFromAudioService() {
|
||||
when(mResources.getText(R.string.default_audio_route_name_headphones))
|
||||
.thenReturn(ROUTE_NAME_HEADPHONES);
|
||||
|
||||
AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo();
|
||||
audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_HEADPHONES;
|
||||
callAudioRoutesObserver(audioRoutesInfo);
|
||||
|
||||
MediaRoute2Info route2Info = mController.getDeviceRoute();
|
||||
assertThat(route2Info.getName()).isEqualTo(ROUTE_NAME_HEADPHONES);
|
||||
assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_WIRED_HEADPHONES);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getDeviceRoute_selectDevice_returnsSelectedRoute() {
|
||||
when(mResources.getText(R.string.default_audio_route_name_dock_speakers))
|
||||
.thenReturn(ROUTE_NAME_DOCK);
|
||||
|
||||
mController.selectRoute(MediaRoute2Info.TYPE_DOCK);
|
||||
|
||||
MediaRoute2Info route2Info = mController.getDeviceRoute();
|
||||
assertThat(route2Info.getName()).isEqualTo(ROUTE_NAME_DOCK);
|
||||
assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_DOCK);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getDeviceRoute_hasSelectedAndAudioServiceRoutes_returnsSelectedRoute() {
|
||||
when(mResources.getText(R.string.default_audio_route_name_headphones))
|
||||
.thenReturn(ROUTE_NAME_HEADPHONES);
|
||||
when(mResources.getText(R.string.default_audio_route_name_dock_speakers))
|
||||
.thenReturn(ROUTE_NAME_DOCK);
|
||||
|
||||
AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo();
|
||||
audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_HEADPHONES;
|
||||
callAudioRoutesObserver(audioRoutesInfo);
|
||||
|
||||
mController.selectRoute(MediaRoute2Info.TYPE_DOCK);
|
||||
|
||||
MediaRoute2Info route2Info = mController.getDeviceRoute();
|
||||
assertThat(route2Info.getName()).isEqualTo(ROUTE_NAME_DOCK);
|
||||
assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_DOCK);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getDeviceRoute_unselectRoute_returnsAudioServiceRoute() {
|
||||
when(mResources.getText(R.string.default_audio_route_name_headphones))
|
||||
.thenReturn(ROUTE_NAME_HEADPHONES);
|
||||
when(mResources.getText(R.string.default_audio_route_name_dock_speakers))
|
||||
.thenReturn(ROUTE_NAME_DOCK);
|
||||
|
||||
mController.selectRoute(MediaRoute2Info.TYPE_DOCK);
|
||||
|
||||
AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo();
|
||||
audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_HEADPHONES;
|
||||
callAudioRoutesObserver(audioRoutesInfo);
|
||||
|
||||
mController.selectRoute(null);
|
||||
|
||||
MediaRoute2Info route2Info = mController.getDeviceRoute();
|
||||
assertThat(route2Info.getName()).isEqualTo(ROUTE_NAME_HEADPHONES);
|
||||
assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_WIRED_HEADPHONES);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getDeviceRoute_selectRouteFails_returnsAudioServiceRoute() {
|
||||
when(mResources.getText(R.string.default_audio_route_name_headphones))
|
||||
.thenReturn(ROUTE_NAME_HEADPHONES);
|
||||
|
||||
AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo();
|
||||
audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_HEADPHONES;
|
||||
callAudioRoutesObserver(audioRoutesInfo);
|
||||
|
||||
mController.selectRoute(MediaRoute2Info.TYPE_BLUETOOTH_A2DP);
|
||||
|
||||
MediaRoute2Info route2Info = mController.getDeviceRoute();
|
||||
assertThat(route2Info.getName()).isEqualTo(ROUTE_NAME_HEADPHONES);
|
||||
assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_WIRED_HEADPHONES);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void selectRoute_selectWiredRoute_returnsTrue() {
|
||||
assertThat(mController.selectRoute(MediaRoute2Info.TYPE_HDMI)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void selectRoute_selectBluetoothRoute_returnsFalse() {
|
||||
assertThat(mController.selectRoute(MediaRoute2Info.TYPE_BLUETOOTH_A2DP)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void selectRoute_unselectRoute_returnsTrue() {
|
||||
assertThat(mController.selectRoute(null)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateVolume_noSelectedRoute_deviceRouteVolumeChanged() {
|
||||
when(mResources.getText(R.string.default_audio_route_name_headphones))
|
||||
.thenReturn(ROUTE_NAME_HEADPHONES);
|
||||
|
||||
AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo();
|
||||
audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_HEADPHONES;
|
||||
callAudioRoutesObserver(audioRoutesInfo);
|
||||
|
||||
mController.updateVolume(VOLUME_SAMPLE_1);
|
||||
|
||||
MediaRoute2Info route2Info = mController.getDeviceRoute();
|
||||
assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_WIRED_HEADPHONES);
|
||||
assertThat(route2Info.getVolume()).isEqualTo(VOLUME_SAMPLE_1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateVolume_connectSelectedRouteLater_selectedRouteVolumeChanged() {
|
||||
when(mResources.getText(R.string.default_audio_route_name_headphones))
|
||||
.thenReturn(ROUTE_NAME_HEADPHONES);
|
||||
when(mResources.getText(R.string.default_audio_route_name_dock_speakers))
|
||||
.thenReturn(ROUTE_NAME_DOCK);
|
||||
|
||||
AudioRoutesInfo audioRoutesInfo = new AudioRoutesInfo();
|
||||
audioRoutesInfo.mainType = AudioRoutesInfo.MAIN_HEADPHONES;
|
||||
callAudioRoutesObserver(audioRoutesInfo);
|
||||
|
||||
mController.updateVolume(VOLUME_SAMPLE_1);
|
||||
|
||||
mController.selectRoute(MediaRoute2Info.TYPE_DOCK);
|
||||
|
||||
MediaRoute2Info route2Info = mController.getDeviceRoute();
|
||||
assertThat(route2Info.getType()).isEqualTo(MediaRoute2Info.TYPE_DOCK);
|
||||
assertThat(route2Info.getVolume()).isEqualTo(VOLUME_SAMPLE_1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user