Create DeviceRouteController interface

- Old implementation of DeviceRouteController has been renamed to
  LegacyDeviceRouteController
- Extracted interface from DeviceRouteController to make granular
  changes

Bug: b/264876378
Test: tested manually
Change-Id: I7905e90fec9189a49ec6e58fe79e4cc69bc0cbcb
This commit is contained in:
Alex Dadukin
2023-03-01 17:38:47 +00:00
parent f950c81209
commit 576c04cf57
3 changed files with 211 additions and 146 deletions

View File

@@ -16,32 +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.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.
@@ -49,145 +29,52 @@ import java.util.Objects;
* <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 */ interface DeviceRouteController {
/**
* Returns a new instance of {@link DeviceRouteController}.
*/
/* 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,
return new LegacyDeviceRouteController(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);
}
/**
* Returns currently selected device (built-in or wired) route.
*
* @return non-null device route.
*/
@NonNull
/* package */ synchronized MediaRoute2Info getDeviceRoute() {
return mDeviceRoute;
}
MediaRoute2Info getDeviceRoute();
/* package */ synchronized boolean updateVolume(int volume) {
if (mDeviceVolume == volume) {
return false;
}
/**
* Updates device route volume.
*
* @param volume specifies a volume for the device route or 0 for unknown.
* @return {@code true} if updated successfully and {@code false} otherwise.
*/
boolean updateVolume(int volume);
mDeviceVolume = volume;
mDeviceRoute = new MediaRoute2Info.Builder(mDeviceRoute)
.setVolume(volume)
.build();
/**
* Interface for receiving events when device route has changed.
*/
interface OnDeviceRouteChangedListener {
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 {
/**
* Called when device route has changed.
*
* @param deviceRoute non-null device route.
*/
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

@@ -0,0 +1,178 @@
/*
* 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.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 LegacyDeviceRouteController implements 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;
@VisibleForTesting
/* package */ LegacyDeviceRouteController(@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);
}
@Override
@NonNull
public synchronized MediaRoute2Info getDeviceRoute() {
return mDeviceRoute;
}
@Override
public 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);
}
private class AudioRoutesObserver extends IAudioRoutesObserver.Stub {
@Override
public void dispatchAudioRoutesChanged(AudioRoutesInfo newAudioRoutes) {
MediaRoute2Info deviceRoute = createRouteFromAudioInfo(newAudioRoutes);
synchronized (LegacyDeviceRouteController.this) {
mDeviceRoute = deviceRoute;
}
notifyDeviceRouteUpdate(deviceRoute);
}
}
}

View File

@@ -50,7 +50,7 @@ import java.util.Arrays;
import java.util.Collection;
@RunWith(Enclosed.class)
public class DeviceRouteControllerTest {
public class LegacyDeviceRouteControllerTest {
private static final String DEFAULT_ROUTE_NAME = "default_route";
private static final String DEFAULT_HEADPHONES_NAME = "headphone";
@@ -97,7 +97,7 @@ public class DeviceRouteControllerTest {
// Default route should be initialized even when AudioService returns null.
when(mAudioService.startWatchingRoutes(any())).thenReturn(null);
DeviceRouteController deviceRouteController = new DeviceRouteController(
LegacyDeviceRouteController deviceRouteController = new LegacyDeviceRouteController(
mContext,
mAudioManager,
mAudioService,
@@ -122,7 +122,7 @@ public class DeviceRouteControllerTest {
AudioRoutesInfo fakeBluetoothAudioRoute = createFakeBluetoothAudioRoute();
when(mAudioService.startWatchingRoutes(any())).thenReturn(fakeBluetoothAudioRoute);
DeviceRouteController deviceRouteController = new DeviceRouteController(
LegacyDeviceRouteController deviceRouteController = new LegacyDeviceRouteController(
mContext,
mAudioManager,
mAudioService,
@@ -236,7 +236,7 @@ public class DeviceRouteControllerTest {
when(mResources.getText(mExpectedRouteNameResource))
.thenReturn(mExpectedRouteNameValue);
DeviceRouteController deviceRouteController = new DeviceRouteController(
LegacyDeviceRouteController deviceRouteController = new LegacyDeviceRouteController(
mContext,
mAudioManager,
mAudioService,
@@ -269,7 +269,7 @@ public class DeviceRouteControllerTest {
@Captor
private ArgumentCaptor<IAudioRoutesObserver.Stub> mAudioRoutesObserverCaptor;
private DeviceRouteController mDeviceRouteController;
private LegacyDeviceRouteController mDeviceRouteController;
private IAudioRoutesObserver.Stub mAudioRoutesObserver;
@Before
@@ -287,7 +287,7 @@ public class DeviceRouteControllerTest {
when(mAudioService.startWatchingRoutes(mAudioRoutesObserverCaptor.capture()))
.thenReturn(audioRoutesInfo);
mDeviceRouteController = new DeviceRouteController(
mDeviceRouteController = new LegacyDeviceRouteController(
mContext,
mAudioManager,
mAudioService,