Merge "Implement selectRoute in AudioPoliciesDeviceRouteController" into udc-dev

This commit is contained in:
Alex Dadukin
2023-03-06 16:01:22 +00:00
committed by Android (Google) Code Review
2 changed files with 324 additions and 12 deletions

View File

@@ -62,7 +62,11 @@ import java.util.Objects;
private final AudioRoutesObserver mAudioRoutesObserver = new AudioRoutesObserver(); private final AudioRoutesObserver mAudioRoutesObserver = new AudioRoutesObserver();
private int mDeviceVolume; private int mDeviceVolume;
@NonNull
private MediaRoute2Info mDeviceRoute; private MediaRoute2Info mDeviceRoute;
@Nullable
private MediaRoute2Info mSelectedRoute;
@VisibleForTesting @VisibleForTesting
/* package */ AudioPoliciesDeviceRouteController(@NonNull Context context, /* package */ AudioPoliciesDeviceRouteController(@NonNull Context context,
@@ -91,14 +95,26 @@ import java.util.Objects;
} }
@Override @Override
public boolean selectRoute(@Nullable Integer type) { public synchronized boolean selectRoute(@Nullable Integer type) {
// No-op as the controller does not support selection from the outside of the class. if (type == null) {
mSelectedRoute = null;
return true;
}
if (!isDeviceRouteType(type)) {
return false; return false;
} }
mSelectedRoute = createRouteFromAudioInfo(type);
return true;
}
@Override @Override
@NonNull @NonNull
public synchronized MediaRoute2Info getDeviceRoute() { public synchronized MediaRoute2Info getDeviceRoute() {
if (mSelectedRoute != null) {
return mSelectedRoute;
}
return mDeviceRoute; return mDeviceRoute;
} }
@@ -109,6 +125,13 @@ import java.util.Objects;
} }
mDeviceVolume = volume; mDeviceVolume = volume;
if (mSelectedRoute != null) {
mSelectedRoute = new MediaRoute2Info.Builder(mSelectedRoute)
.setVolume(volume)
.build();
}
mDeviceRoute = new MediaRoute2Info.Builder(mDeviceRoute) mDeviceRoute = new MediaRoute2Info.Builder(mDeviceRoute)
.setVolume(volume) .setVolume(volume)
.build(); .build();
@@ -116,29 +139,47 @@ import java.util.Objects;
return true; return true;
} }
@NonNull
private MediaRoute2Info createRouteFromAudioInfo(@Nullable AudioRoutesInfo newRoutes) { private MediaRoute2Info createRouteFromAudioInfo(@Nullable AudioRoutesInfo newRoutes) {
int name = R.string.default_audio_route_name;
int type = TYPE_BUILTIN_SPEAKER; int type = TYPE_BUILTIN_SPEAKER;
if (newRoutes != null) { if (newRoutes != null) {
if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADPHONES) != 0) { if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADPHONES) != 0) {
type = TYPE_WIRED_HEADPHONES; type = TYPE_WIRED_HEADPHONES;
name = R.string.default_audio_route_name_headphones;
} else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADSET) != 0) { } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADSET) != 0) {
type = TYPE_WIRED_HEADSET; type = TYPE_WIRED_HEADSET;
name = R.string.default_audio_route_name_headphones;
} else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) { } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) {
type = TYPE_DOCK; type = TYPE_DOCK;
name = R.string.default_audio_route_name_dock_speakers;
} else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HDMI) != 0) { } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HDMI) != 0) {
type = TYPE_HDMI; type = TYPE_HDMI;
name = R.string.default_audio_route_name_external_device;
} else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_USB) != 0) { } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_USB) != 0) {
type = TYPE_USB_DEVICE; 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) { synchronized (this) {
return new MediaRoute2Info.Builder( return new MediaRoute2Info.Builder(
DEVICE_ROUTE_ID, mContext.getResources().getText(name).toString()) 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 { private class AudioRoutesObserver extends IAudioRoutesObserver.Stub {
@Override @Override
public void dispatchAudioRoutesChanged(AudioRoutesInfo newAudioRoutes) { public void dispatchAudioRoutesChanged(AudioRoutesInfo newAudioRoutes) {
boolean isDeviceRouteChanged;
MediaRoute2Info deviceRoute = createRouteFromAudioInfo(newAudioRoutes); MediaRoute2Info deviceRoute = createRouteFromAudioInfo(newAudioRoutes);
synchronized (AudioPoliciesDeviceRouteController.this) { synchronized (AudioPoliciesDeviceRouteController.this) {
mDeviceRoute = deviceRoute; mDeviceRoute = deviceRoute;
isDeviceRouteChanged = mSelectedRoute == null;
}
if (isDeviceRouteChanged) {
mOnDeviceRouteChangedListener.onDeviceRouteChanged(deviceRoute);
} }
notifyDeviceRouteUpdate(deviceRoute);
} }
} }

View File

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