/*
* Copyright (C) 2013 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 androidx.mediarouter.media;
import android.annotation.SuppressLint;
import android.content.ComponentName;
import android.content.Context;
import android.content.IntentFilter;
import android.content.res.Resources;
import android.os.Build;
import android.util.Log;
import android.view.Display;
import androidx.annotation.DoNotInline;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.mediarouter.R;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/**
* Provides routes for built-in system destinations such as the local display and speaker. it
* queries the framework {@link android.media.MediaRouter} for framework-provided routes and
* registers non-framework-provided routes as user routes.
*/
abstract class PlatformMediaRouter1RouteProvider extends MediaRouteProvider {
public static final String TAG = "AxSysMediaRouteProvider";
public static final String PACKAGE_NAME = "android";
public static final String DEFAULT_ROUTE_ID = "DEFAULT_ROUTE";
public static final int ROUTE_TYPE_LIVE_AUDIO = 0x1;
public static final int ROUTE_TYPE_LIVE_VIDEO = 0x2;
public static final int ROUTE_TYPE_USER = 0x00800000;
public static final int ALL_ROUTE_TYPES =
ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO | ROUTE_TYPE_USER;
protected PlatformMediaRouter1RouteProvider(Context context) {
super(context, new ProviderMetadata(new ComponentName(PACKAGE_NAME,
PlatformMediaRouter1RouteProvider.class.getName())));
}
public static PlatformMediaRouter1RouteProvider obtain(
Context context, SyncCallback syncCallback) {
if (Build.VERSION.SDK_INT >= 24) {
return new Api24Impl(context, syncCallback);
}
return new JellybeanMr2Impl(context, syncCallback);
}
/**
* Called by the media router when a route is added to synchronize state with
* the framework media router.
*/
public void onSyncRouteAdded(MediaRouter.RouteInfo route) {
}
/**
* Called by the media router when a route is removed to synchronize state with
* the framework media router.
*/
public void onSyncRouteRemoved(MediaRouter.RouteInfo route) {
}
/**
* Called by the media router when a route is changed to synchronize state with
* the framework media router.
*/
public void onSyncRouteChanged(MediaRouter.RouteInfo route) {
}
/**
* Called by the media router when a route is selected to synchronize state with
* the framework media router.
*/
public void onSyncRouteSelected(MediaRouter.RouteInfo route) {
}
/**
* Callbacks into the media router to synchronize state with the framework media router.
*/
public interface SyncCallback {
void onPlatformRouteSelectedByDescriptorId(@NonNull String id);
}
/** Jellybean MR2 implementation. */
private static class JellybeanMr2Impl extends PlatformMediaRouter1RouteProvider
implements MediaRouterUtils.Callback, MediaRouterUtils.VolumeCallback {
private static final ArrayList<IntentFilter> LIVE_AUDIO_CONTROL_FILTERS;
static {
IntentFilter f = new IntentFilter();
f.addCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO);
LIVE_AUDIO_CONTROL_FILTERS = new ArrayList<>();
LIVE_AUDIO_CONTROL_FILTERS.add(f);
}
private static final ArrayList<IntentFilter> LIVE_VIDEO_CONTROL_FILTERS;
static {
IntentFilter f = new IntentFilter();
f.addCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
LIVE_VIDEO_CONTROL_FILTERS = new ArrayList<>();
LIVE_VIDEO_CONTROL_FILTERS.add(f);
}
private final SyncCallback mSyncCallback;
protected final android.media.MediaRouter mRouter;
protected final android.media.MediaRouter.Callback mCallback;
protected final android.media.MediaRouter.VolumeCallback mVolumeCallback;
protected final android.media.MediaRouter.RouteCategory mUserRouteCategory;
protected int mRouteTypes;
protected boolean mActiveScan;
protected boolean mCallbackRegistered;
// Maintains an association from framework routes to support library routes.
// Note that we cannot use the tag field for this because an application may
// have published its own user routes to the framework media router and already
// used the tag for its own purposes.
protected final ArrayList<SystemRouteRecord> mSystemRouteRecords =
new ArrayList<>();
// Maintains an association from support library routes to framework routes.
protected final ArrayList<UserRouteRecord> mUserRouteRecords =
new ArrayList<>();
/* package */ JellybeanMr2Impl(Context context, SyncCallback syncCallback) {
super(context);
mSyncCallback = syncCallback;
mRouter =
(android.media.MediaRouter)
context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
mCallback = MediaRouterUtils.createCallback(this);
mVolumeCallback = MediaRouterUtils.createVolumeCallback(this);
Resources r = context.getResources();
mUserRouteCategory =
mRouter.createRouteCategory(
r.getString(R.string.mr_user_route_category_name),
/* isGroupable= */ false);
updateSystemRoutes();
}
@Override
public RouteController onCreateRouteController(@NonNull String routeId) {
int index = findSystemRouteRecordByDescriptorId(routeId);
if (index >= 0) {
SystemRouteRecord record = mSystemRouteRecords.get(index);
return new SystemRouteController(record.mRoute);
}
return null;
}
@Override
public void onDiscoveryRequestChanged(MediaRouteDiscoveryRequest request) {
int newRouteTypes = 0;
boolean newActiveScan = false;
if (request != null) {
final MediaRouteSelector selector = request.getSelector();
final List<String> categories = selector.getControlCategories();
final int count = categories.size();
for (int i = 0; i < count; i++) {
String category = categories.get(i);
if (category.equals(MediaControlIntent.CATEGORY_LIVE_AUDIO)) {
newRouteTypes |= ROUTE_TYPE_LIVE_AUDIO;
} else if (category.equals(MediaControlIntent.CATEGORY_LIVE_VIDEO)) {
newRouteTypes |= ROUTE_TYPE_LIVE_VIDEO;
} else {
newRouteTypes |= ROUTE_TYPE_USER;
}
}
newActiveScan = request.isActiveScan();
}
if (mRouteTypes != newRouteTypes || mActiveScan != newActiveScan) {
mRouteTypes = newRouteTypes;
mActiveScan = newActiveScan;
updateSystemRoutes();
}
}
@Override
public void onRouteAdded(@NonNull android.media.MediaRouter.RouteInfo route) {
if (addSystemRouteNoPublish(route)) {
publishRoutes();
}
}
private void updateSystemRoutes() {
updateCallback();
boolean changed = false;
for (android.media.MediaRouter.RouteInfo route : getRoutes()) {
changed |= addSystemRouteNoPublish(route);
}
if (changed) {
publishRoutes();
}
}
private List<android.media.MediaRouter.RouteInfo> getRoutes() {
final int count = mRouter.getRouteCount();
List<android.media.MediaRouter.RouteInfo> out = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
out.add(mRouter.getRouteAt(i));
}
return out;
}
private boolean addSystemRouteNoPublish(android.media.MediaRouter.RouteInfo route) {
if (getUserRouteRecord(route) == null && findSystemRouteRecord(route) < 0) {
String id = assignRouteId(route);
SystemRouteRecord record = new SystemRouteRecord(route, id);
updateSystemRouteDescriptor(record);
mSystemRouteRecords.add(record);
return true;
}
return false;
}
private String assignRouteId(android.media.MediaRouter.RouteInfo route) {
// TODO: The framework media router should supply a unique route id that
// we can use here. For now we use a hash of the route name and take care
// to dedupe it.
boolean isDefault = (getDefaultRoute() == route);
String id = isDefault ? DEFAULT_ROUTE_ID :
String.format(Locale.US, "ROUTE_%08x", getRouteName(route).hashCode());
if (findSystemRouteRecordByDescriptorId(id) < 0) {
return id;
}
for (int i = 2; ; i++) {
String newId = String.format(Locale.US, "%s_%d", id, i);
if (findSystemRouteRecordByDescriptorId(newId) < 0) {
return newId;
}
}
}
@Override
public void onRouteRemoved(@NonNull android.media.MediaRouter.RouteInfo route) {
if (getUserRouteRecord(route) == null) {
int index = findSystemRouteRecord(route);
if (index >= 0) {
mSystemRouteRecords.remove(index);
publishRoutes();
}
}
}
@Override
public void onRouteChanged(@NonNull android.media.MediaRouter.RouteInfo route) {
if (getUserRouteRecord(route) == null) {
int index = findSystemRouteRecord(route);
if (index >= 0) {
SystemRouteRecord record = mSystemRouteRecords.get(index);
updateSystemRouteDescriptor(record);
publishRoutes();
}
}
}
@Override
public void onRouteVolumeChanged(@NonNull android.media.MediaRouter.RouteInfo route) {
if (getUserRouteRecord(route) == null) {
int index = findSystemRouteRecord(route);
if (index >= 0) {
SystemRouteRecord record = mSystemRouteRecords.get(index);
int newVolume = route.getVolume();
if (newVolume != record.mRouteDescriptor.getVolume()) {
record.mRouteDescriptor =
new MediaRouteDescriptor.Builder(record.mRouteDescriptor)
.setVolume(newVolume)
.build();
publishRoutes();
}
}
}
}
@Override
public void onRouteSelected(int type,
@NonNull android.media.MediaRouter.RouteInfo route) {
if (route != mRouter.getSelectedRoute(ALL_ROUTE_TYPES)) {
// The currently selected route has already changed so this callback
// is stale. Drop it to prevent getting into sync loops.
return;
}
UserRouteRecord userRouteRecord = getUserRouteRecord(route);
if (userRouteRecord != null) {
userRouteRecord.mRoute.select();
} else {
// Select the route if it already exists in the compat media router.
// If not, we will select it instead when the route is added.
int index = findSystemRouteRecord(route);
if (index >= 0) {
SystemRouteRecord record = mSystemRouteRecords.get(index);
mSyncCallback.onPlatformRouteSelectedByDescriptorId(record.mRouteDescriptorId);
}
}
}
@Override
public void onRouteUnselected(int type,
@NonNull android.media.MediaRouter.RouteInfo route) {
// Nothing to do when a route is unselected.
// We only need to handle when a route is selected.
}
@Override
public void onRouteGrouped(@NonNull android.media.MediaRouter.RouteInfo route,
@NonNull android.media.MediaRouter.RouteGroup group, int index) {
// Route grouping is deprecated and no longer supported.
}
@Override
public void onRouteUngrouped(@NonNull android.media.MediaRouter.RouteInfo route,
@NonNull android.media.MediaRouter.RouteGroup group) {
// Route grouping is deprecated and no longer supported.
}
@Override
public void onVolumeSetRequest(@NonNull android.media.MediaRouter.RouteInfo route,
int volume) {
UserRouteRecord record = getUserRouteRecord(route);
if (record != null) {
record.mRoute.requestSetVolume(volume);
}
}
@Override
public void onVolumeUpdateRequest(@NonNull android.media.MediaRouter.RouteInfo route,
int direction) {
UserRouteRecord record = getUserRouteRecord(route);
if (record != null) {
record.mRoute.requestUpdateVolume(direction);
}
}
@Override
public void onSyncRouteAdded(MediaRouter.RouteInfo route) {
if (route.getProviderInstance() != this) {
android.media.MediaRouter.UserRouteInfo userRoute =
mRouter.createUserRoute(mUserRouteCategory);
UserRouteRecord record = new UserRouteRecord(route, userRoute);
userRoute.setTag(record);
userRoute.setVolumeCallback(mVolumeCallback);
updateUserRouteProperties(record);
mUserRouteRecords.add(record);
mRouter.addUserRoute(userRoute);
} else {
// If the newly added route is the counterpart of the currently selected
// route in the framework media router then ensure it is selected in
// the compat media router.
android.media.MediaRouter.RouteInfo routeObj =
mRouter.getSelectedRoute(ALL_ROUTE_TYPES);
int index = findSystemRouteRecord(routeObj);
if (index >= 0) {
SystemRouteRecord record = mSystemRouteRecords.get(index);
if (record.mRouteDescriptorId.equals(route.getDescriptorId())) {
route.select();
}
}
}
}
@Override
public void onSyncRouteRemoved(MediaRouter.RouteInfo route) {
if (route.getProviderInstance() != this) {
int index = findUserRouteRecord(route);
if (index >= 0) {
UserRouteRecord record = mUserRouteRecords.remove(index);
record.mUserRoute.setTag(null);
record.mUserRoute.setVolumeCallback(null);
try {
mRouter.removeUserRoute(record.mUserRoute);
} catch (IllegalArgumentException e) {
// Work around for https://issuetracker.google.com/issues/202931542.
Log.w(TAG, "Failed to remove user route", e);
}
}
}
}
@Override
public void onSyncRouteChanged(MediaRouter.RouteInfo route) {
if (route.getProviderInstance() != this) {
int index = findUserRouteRecord(route);
if (index >= 0) {
UserRouteRecord record = mUserRouteRecords.get(index);
updateUserRouteProperties(record);
}
}
}
@Override
public void onSyncRouteSelected(MediaRouter.RouteInfo route) {
if (!route.isSelected()) {
// The currently selected route has already changed so this callback
// is stale. Drop it to prevent getting into sync loops.
return;
}
if (route.getProviderInstance() != this) {
int index = findUserRouteRecord(route);
if (index >= 0) {
UserRouteRecord record = mUserRouteRecords.get(index);
selectRoute(record.mUserRoute);
}
} else {
int index = findSystemRouteRecordByDescriptorId(route.getDescriptorId());
if (index >= 0) {
SystemRouteRecord record = mSystemRouteRecords.get(index);
selectRoute(record.mRoute);
}
}
}
protected void publishRoutes() {
MediaRouteProviderDescriptor.Builder builder =
new MediaRouteProviderDescriptor.Builder();
int count = mSystemRouteRecords.size();
for (int i = 0; i < count; i++) {
builder.addRoute(mSystemRouteRecords.get(i).mRouteDescriptor);
}
setDescriptor(builder.build());
}
protected int findSystemRouteRecord(android.media.MediaRouter.RouteInfo route) {
final int count = mSystemRouteRecords.size();
for (int i = 0; i < count; i++) {
if (mSystemRouteRecords.get(i).mRoute == route) {
return i;
}
}
return -1;
}
protected int findSystemRouteRecordByDescriptorId(String id) {
final int count = mSystemRouteRecords.size();
for (int i = 0; i < count; i++) {
if (mSystemRouteRecords.get(i).mRouteDescriptorId.equals(id)) {
return i;
}
}
return -1;
}
protected int findUserRouteRecord(MediaRouter.RouteInfo route) {
final int count = mUserRouteRecords.size();
for (int i = 0; i < count; i++) {
if (mUserRouteRecords.get(i).mRoute == route) {
return i;
}
}
return -1;
}
protected UserRouteRecord getUserRouteRecord(android.media.MediaRouter.RouteInfo route) {
Object tag = route.getTag();
return tag instanceof UserRouteRecord ? (UserRouteRecord) tag : null;
}
protected void updateSystemRouteDescriptor(SystemRouteRecord record) {
// We must always recreate the route descriptor when making any changes
// because they are intended to be immutable once published.
MediaRouteDescriptor.Builder builder = new MediaRouteDescriptor.Builder(
record.mRouteDescriptorId, getRouteName(record.mRoute));
onBuildSystemRouteDescriptor(record, builder);
record.mRouteDescriptor = builder.build();
}
protected String getRouteName(android.media.MediaRouter.RouteInfo route) {
// Routes should not have null names but it may happen for badly configured
// user routes. We tolerate this by using an empty name string here but
// such unnamed routes will be discarded by the media router upstream
// (with a log message so we can track down the problem).
CharSequence name = route.getName(getContext());
return name != null ? name.toString() : "";
}
@DoNotInline
protected void onBuildSystemRouteDescriptor(SystemRouteRecord record,
MediaRouteDescriptor.Builder builder) {
int supportedTypes = record.mRoute.getSupportedTypes();
if ((supportedTypes & ROUTE_TYPE_LIVE_AUDIO) != 0) {
builder.addControlFilters(LIVE_AUDIO_CONTROL_FILTERS);
}
if ((supportedTypes & ROUTE_TYPE_LIVE_VIDEO) != 0) {
builder.addControlFilters(LIVE_VIDEO_CONTROL_FILTERS);
}
builder.setPlaybackType(record.mRoute.getPlaybackType());
builder.setPlaybackStream(record.mRoute.getPlaybackStream());
builder.setVolume(record.mRoute.getVolume());
builder.setVolumeMax(record.mRoute.getVolumeMax());
builder.setVolumeHandling(record.mRoute.getVolumeHandling());
builder.setIsSystemRoute((supportedTypes & ROUTE_TYPE_USER) == 0);
if (!record.mRoute.isEnabled()) {
builder.setEnabled(false);
}
if (isConnecting(record)) {
builder.setConnectionState(MediaRouter.RouteInfo.CONNECTION_STATE_CONNECTING);
}
Display presentationDisplay = record.mRoute.getPresentationDisplay();
if (presentationDisplay != null) {
builder.setPresentationDisplayId(presentationDisplay.getDisplayId());
}
CharSequence description = record.mRoute.getDescription();
if (description != null) {
builder.setDescription(description.toString());
}
}
@Override
public void onRoutePresentationDisplayChanged(
@NonNull android.media.MediaRouter.RouteInfo route) {
int index = findSystemRouteRecord(route);
if (index >= 0) {
SystemRouteRecord record = mSystemRouteRecords.get(index);
Display newPresentationDisplay = route.getPresentationDisplay();
int newPresentationDisplayId = (newPresentationDisplay != null
? newPresentationDisplay.getDisplayId() : -1);
if (newPresentationDisplayId
!= record.mRouteDescriptor.getPresentationDisplayId()) {
record.mRouteDescriptor =
new MediaRouteDescriptor.Builder(record.mRouteDescriptor)
.setPresentationDisplayId(newPresentationDisplayId)
.build();
publishRoutes();
}
}
}
@DoNotInline
protected void selectRoute(android.media.MediaRouter.RouteInfo route) {
mRouter.selectRoute(ALL_ROUTE_TYPES, route);
}
@DoNotInline
protected android.media.MediaRouter.RouteInfo getDefaultRoute() {
return mRouter.getDefaultRoute();
}
@SuppressLint("WrongConstant") // False positive. See b/310913043.
@DoNotInline
protected void updateUserRouteProperties(UserRouteRecord record) {
android.media.MediaRouter.UserRouteInfo userRoute = record.mUserRoute;
MediaRouter.RouteInfo routeInfo = record.mRoute;
userRoute.setName(routeInfo.getName());
userRoute.setPlaybackType(routeInfo.getPlaybackType());
userRoute.setPlaybackStream(routeInfo.getPlaybackStream());
userRoute.setVolume(routeInfo.getVolume());
userRoute.setVolumeMax(routeInfo.getVolumeMax());
userRoute.setVolumeHandling(routeInfo.getVolumeHandling());
userRoute.setDescription(routeInfo.getDescription());
}
@DoNotInline
protected void updateCallback() {
if (mCallbackRegistered) {
mRouter.removeCallback(mCallback);
}
mCallbackRegistered = true;
int flags = MediaRouter.CALLBACK_FLAG_UNFILTERED_EVENTS
| (mActiveScan ? MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN : 0);
mRouter.addCallback(mRouteTypes, mCallback, flags);
}
@DoNotInline
protected boolean isConnecting(SystemRouteRecord record) {
return record.mRoute.isConnecting();
}
/**
* Represents a route that is provided by the framework media router
* and published by this route provider to the support library media router.
*/
protected static final class SystemRouteRecord {
public final android.media.MediaRouter.RouteInfo mRoute;
public final String mRouteDescriptorId;
public MediaRouteDescriptor mRouteDescriptor; // assigned immediately after creation
public SystemRouteRecord(android.media.MediaRouter.RouteInfo route, String id) {
mRoute = route;
mRouteDescriptorId = id;
}
}
/**
* Represents a route that is provided by the support library media router
* and published by this route provider to the framework media router.
*/
protected static final class UserRouteRecord {
public final MediaRouter.RouteInfo mRoute;
public final android.media.MediaRouter.UserRouteInfo mUserRoute;
public UserRouteRecord(MediaRouter.RouteInfo route,
android.media.MediaRouter.UserRouteInfo userRoute) {
mRoute = route;
mUserRoute = userRoute;
}
}
protected static final class SystemRouteController extends RouteController {
private final android.media.MediaRouter.RouteInfo mRoute;
public SystemRouteController(android.media.MediaRouter.RouteInfo route) {
mRoute = route;
}
@Override
public void onSetVolume(int volume) {
mRoute.requestSetVolume(volume);
}
@Override
public void onUpdateVolume(int delta) {
mRoute.requestUpdateVolume(delta);
}
}
}
/**
* Api24 implementation.
*/
@RequiresApi(24)
private static class Api24Impl extends JellybeanMr2Impl {
/* package */ Api24Impl(Context context, SyncCallback syncCallback) {
super(context, syncCallback);
}
@SuppressLint("WrongConstant") // False positive. See b/283059575.
@Override
@DoNotInline
protected void onBuildSystemRouteDescriptor(SystemRouteRecord record,
MediaRouteDescriptor.Builder builder) {
super.onBuildSystemRouteDescriptor(record, builder);
builder.setDeviceType(record.mRoute.getDeviceType());
}
}
}