3756 lines
149 KiB
Java
3756 lines
149 KiB
Java
![]() |
/*
|
||
|
* Copyright 2019 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 android.media;
|
||
|
|
||
|
import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;
|
||
|
import static com.android.media.flags.Flags.FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES;
|
||
|
import static com.android.media.flags.Flags.FLAG_ENABLE_GET_TRANSFERABLE_ROUTES;
|
||
|
import static com.android.media.flags.Flags.FLAG_ENABLE_PRIVILEGED_ROUTING_FOR_MEDIA_ROUTING_CONTROL;
|
||
|
import static com.android.media.flags.Flags.FLAG_ENABLE_RLP_CALLBACKS_IN_MEDIA_ROUTER2;
|
||
|
import static com.android.media.flags.Flags.FLAG_ENABLE_SCREEN_OFF_SCANNING;
|
||
|
|
||
|
import android.Manifest;
|
||
|
import android.annotation.CallbackExecutor;
|
||
|
import android.annotation.FlaggedApi;
|
||
|
import android.annotation.IntDef;
|
||
|
import android.annotation.NonNull;
|
||
|
import android.annotation.Nullable;
|
||
|
import android.annotation.RequiresPermission;
|
||
|
import android.annotation.SystemApi;
|
||
|
import android.annotation.TestApi;
|
||
|
import android.app.AppOpsManager;
|
||
|
import android.content.Context;
|
||
|
import android.content.pm.PackageManager;
|
||
|
import android.os.Bundle;
|
||
|
import android.os.Handler;
|
||
|
import android.os.Looper;
|
||
|
import android.os.Message;
|
||
|
import android.os.Process;
|
||
|
import android.os.RemoteException;
|
||
|
import android.os.ServiceManager;
|
||
|
import android.os.UserHandle;
|
||
|
import android.text.TextUtils;
|
||
|
import android.util.ArrayMap;
|
||
|
import android.util.ArraySet;
|
||
|
import android.util.Log;
|
||
|
import android.util.SparseArray;
|
||
|
|
||
|
import com.android.internal.annotations.GuardedBy;
|
||
|
import com.android.media.flags.Flags;
|
||
|
|
||
|
import java.lang.annotation.Retention;
|
||
|
import java.lang.annotation.RetentionPolicy;
|
||
|
import java.util.ArrayList;
|
||
|
import java.util.Collections;
|
||
|
import java.util.Comparator;
|
||
|
import java.util.HashMap;
|
||
|
import java.util.List;
|
||
|
import java.util.Map;
|
||
|
import java.util.Objects;
|
||
|
import java.util.Set;
|
||
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||
|
import java.util.concurrent.Executor;
|
||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||
|
import java.util.function.Consumer;
|
||
|
import java.util.stream.Collectors;
|
||
|
|
||
|
/**
|
||
|
* This API is not generally intended for third party application developers. Use the
|
||
|
* <a href="{@docRoot}jetpack/androidx.html">AndroidX</a>
|
||
|
* <a href="{@docRoot}reference/androidx/mediarouter/media/package-summary.html">Media Router
|
||
|
* Library</a> for consistent behavior across all devices.
|
||
|
*
|
||
|
* <p>MediaRouter2 allows applications to control the routing of media channels and streams from
|
||
|
* the current device to remote speakers and devices.
|
||
|
*/
|
||
|
// TODO(b/157873330): Add method names at the beginning of log messages. (e.g. selectRoute)
|
||
|
// Not only MediaRouter2, but also to service / manager / provider.
|
||
|
// TODO: ensure thread-safe and document it
|
||
|
public final class MediaRouter2 {
|
||
|
|
||
|
/**
|
||
|
* The state of a router not requesting route scanning.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
public static final int SCANNING_STATE_NOT_SCANNING = 0;
|
||
|
|
||
|
/**
|
||
|
* The state of a router requesting scanning only while the user interacts with its owner app.
|
||
|
*
|
||
|
* <p>The device's screen must be on and the app must be in the foreground to trigger scanning
|
||
|
* under this state.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
public static final int SCANNING_STATE_WHILE_INTERACTIVE = 1;
|
||
|
|
||
|
/**
|
||
|
* The state of a router requesting unrestricted scanning.
|
||
|
*
|
||
|
* <p>This state triggers scanning regardless of the restrictions required for {@link
|
||
|
* #SCANNING_STATE_WHILE_INTERACTIVE}.
|
||
|
*
|
||
|
* <p>Routers requesting unrestricted scanning must hold {@link
|
||
|
* Manifest.permission#MEDIA_ROUTING_CONTROL}.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
public static final int SCANNING_STATE_SCANNING_FULL = 2;
|
||
|
|
||
|
/** @hide */
|
||
|
@IntDef(
|
||
|
prefix = "SCANNING_STATE",
|
||
|
value = {
|
||
|
SCANNING_STATE_NOT_SCANNING,
|
||
|
SCANNING_STATE_WHILE_INTERACTIVE,
|
||
|
SCANNING_STATE_SCANNING_FULL
|
||
|
})
|
||
|
@Retention(RetentionPolicy.SOURCE)
|
||
|
public @interface ScanningState {}
|
||
|
|
||
|
private static final String TAG = "MR2";
|
||
|
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
|
||
|
private static final Object sSystemRouterLock = new Object();
|
||
|
private static final Object sRouterLock = new Object();
|
||
|
|
||
|
// The maximum time for the old routing controller available after transfer.
|
||
|
private static final int TRANSFER_TIMEOUT_MS = 30_000;
|
||
|
// The manager request ID representing that no manager is involved.
|
||
|
private static final long MANAGER_REQUEST_ID_NONE = MediaRoute2ProviderService.REQUEST_ID_NONE;
|
||
|
|
||
|
private record PackageNameUserHandlePair(String packageName, UserHandle user) {}
|
||
|
|
||
|
private record InstanceInvalidatedCallbackRecord(Executor executor, Runnable runnable) {}
|
||
|
|
||
|
@GuardedBy("sSystemRouterLock")
|
||
|
private static final Map<PackageNameUserHandlePair, MediaRouter2> sAppToProxyRouterMap =
|
||
|
new ArrayMap<>();
|
||
|
|
||
|
@GuardedBy("sRouterLock")
|
||
|
private static MediaRouter2 sInstance;
|
||
|
|
||
|
private final Context mContext;
|
||
|
private final IMediaRouterService mMediaRouterService;
|
||
|
private final Object mLock = new Object();
|
||
|
private final MediaRouter2Impl mImpl;
|
||
|
|
||
|
private final CopyOnWriteArrayList<RouteCallbackRecord> mRouteCallbackRecords =
|
||
|
new CopyOnWriteArrayList<>();
|
||
|
private final CopyOnWriteArrayList<RouteListingPreferenceCallbackRecord>
|
||
|
mListingPreferenceCallbackRecords = new CopyOnWriteArrayList<>();
|
||
|
private final CopyOnWriteArrayList<TransferCallbackRecord> mTransferCallbackRecords =
|
||
|
new CopyOnWriteArrayList<>();
|
||
|
private final CopyOnWriteArrayList<ControllerCallbackRecord> mControllerCallbackRecords =
|
||
|
new CopyOnWriteArrayList<>();
|
||
|
|
||
|
private final CopyOnWriteArrayList<ControllerCreationRequest> mControllerCreationRequests =
|
||
|
new CopyOnWriteArrayList<>();
|
||
|
|
||
|
/**
|
||
|
* Stores the latest copy of all routes received from the system server, without any filtering,
|
||
|
* sorting, or deduplication.
|
||
|
*
|
||
|
* <p>Uses {@link MediaRoute2Info#getId()} to set each entry's key.
|
||
|
*/
|
||
|
@GuardedBy("mLock")
|
||
|
private final Map<String, MediaRoute2Info> mRoutes = new ArrayMap<>();
|
||
|
|
||
|
private final RoutingController mSystemController;
|
||
|
|
||
|
@GuardedBy("mLock")
|
||
|
private final Map<String, RoutingController> mNonSystemRoutingControllers = new ArrayMap<>();
|
||
|
|
||
|
@GuardedBy("mLock")
|
||
|
private int mScreenOffScanRequestCount = 0;
|
||
|
|
||
|
@GuardedBy("mLock")
|
||
|
private int mScreenOnScanRequestCount = 0;
|
||
|
|
||
|
private final SparseArray<ScanRequest> mScanRequestsMap = new SparseArray<>();
|
||
|
private final AtomicInteger mNextRequestId = new AtomicInteger(1);
|
||
|
private final Handler mHandler;
|
||
|
|
||
|
@GuardedBy("mLock")
|
||
|
private RouteDiscoveryPreference mDiscoveryPreference = RouteDiscoveryPreference.EMPTY;
|
||
|
|
||
|
// TODO: Make MediaRouter2 is always connected to the MediaRouterService.
|
||
|
@GuardedBy("mLock")
|
||
|
private MediaRouter2Stub mStub;
|
||
|
|
||
|
@GuardedBy("mLock")
|
||
|
@Nullable
|
||
|
private RouteListingPreference mRouteListingPreference;
|
||
|
|
||
|
/**
|
||
|
* Stores an auxiliary copy of {@link #mFilteredRoutes} at the time of the last route callback
|
||
|
* dispatch. This is only used to determine what callback a route should be assigned to (added,
|
||
|
* removed, changed) in {@link #dispatchFilteredRoutesUpdatedOnHandler(List)}.
|
||
|
*/
|
||
|
private volatile ArrayMap<String, MediaRoute2Info> mPreviousFilteredRoutes = new ArrayMap<>();
|
||
|
|
||
|
private final Map<String, MediaRoute2Info> mPreviousUnfilteredRoutes = new ArrayMap<>();
|
||
|
|
||
|
/**
|
||
|
* Stores the latest copy of exposed routes after filtering, sorting, and deduplication. Can be
|
||
|
* accessed through {@link #getRoutes()}.
|
||
|
*
|
||
|
* <p>This list is a copy of {@link #mRoutes} which has undergone filtering, sorting, and
|
||
|
* deduplication using criteria in {@link #mDiscoveryPreference}.
|
||
|
*
|
||
|
* @see #filterRoutesWithCompositePreferenceLocked(List)
|
||
|
*/
|
||
|
private volatile List<MediaRoute2Info> mFilteredRoutes = Collections.emptyList();
|
||
|
private volatile OnGetControllerHintsListener mOnGetControllerHintsListener;
|
||
|
|
||
|
/** Gets an instance of the media router associated with the context. */
|
||
|
@NonNull
|
||
|
public static MediaRouter2 getInstance(@NonNull Context context) {
|
||
|
Objects.requireNonNull(context, "context must not be null");
|
||
|
synchronized (sRouterLock) {
|
||
|
if (sInstance == null) {
|
||
|
sInstance = new MediaRouter2(context.getApplicationContext());
|
||
|
}
|
||
|
return sInstance;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns a proxy MediaRouter2 instance that allows you to control the routing of an app
|
||
|
* specified by {@code clientPackageName}. Returns {@code null} if the specified package name
|
||
|
* does not exist.
|
||
|
*
|
||
|
* <p>Proxy MediaRouter2 instances operate differently than regular MediaRouter2 instances:
|
||
|
*
|
||
|
* <ul>
|
||
|
* <li>
|
||
|
* <p>{@link #registerRouteCallback} ignores any {@link RouteDiscoveryPreference discovery
|
||
|
* preference} passed by a proxy router. Use {@link RouteDiscoveryPreference#EMPTY} when
|
||
|
* setting a route callback.
|
||
|
* <li>
|
||
|
* <p>Methods returning non-system {@link RoutingController controllers} always return new
|
||
|
* instances with the latest data. Do not attempt to compare or store them. Instead, use
|
||
|
* {@link #getController(String)} or {@link #getControllers()} to query the most
|
||
|
* up-to-date state.
|
||
|
* <li>
|
||
|
* <p>Calls to {@link #setOnGetControllerHintsListener} are ignored.
|
||
|
* </ul>
|
||
|
*
|
||
|
* <p>Callers that only hold the revocable form of {@link
|
||
|
* Manifest.permission#MEDIA_ROUTING_CONTROL} must use {@link #getInstance(Context, String,
|
||
|
* Executor, Runnable)} instead of this method.
|
||
|
*
|
||
|
* @param clientPackageName the package name of the app to control
|
||
|
* @return a proxy MediaRouter2 instance if {@code clientPackageName} exists or {@code null}.
|
||
|
* @throws IllegalStateException if the caller only holds a revocable version of {@link
|
||
|
* Manifest.permission#MEDIA_ROUTING_CONTROL}.
|
||
|
* @hide
|
||
|
*/
|
||
|
@SuppressWarnings("RequiresPermission")
|
||
|
@RequiresPermission(
|
||
|
anyOf = {
|
||
|
Manifest.permission.MEDIA_CONTENT_CONTROL,
|
||
|
Manifest.permission.MEDIA_ROUTING_CONTROL
|
||
|
})
|
||
|
@SystemApi
|
||
|
@Nullable
|
||
|
public static MediaRouter2 getInstance(
|
||
|
@NonNull Context context, @NonNull String clientPackageName) {
|
||
|
// Capturing the IAE here to not break nullability.
|
||
|
try {
|
||
|
return findOrCreateProxyInstanceForCallingUser(
|
||
|
context,
|
||
|
clientPackageName,
|
||
|
context.getUser(),
|
||
|
/* executor */ null,
|
||
|
/* onInstanceInvalidatedListener */ null);
|
||
|
} catch (IllegalArgumentException ex) {
|
||
|
Log.e(TAG, "Package " + clientPackageName + " not found. Ignoring.");
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns a proxy MediaRouter2 instance that allows you to control the routing of an app
|
||
|
* specified by {@code clientPackageName}. Returns {@code null} if the specified package name
|
||
|
* does not exist.
|
||
|
*
|
||
|
* <p>Proxy MediaRouter2 instances operate differently than regular MediaRouter2 instances:
|
||
|
*
|
||
|
* <ul>
|
||
|
* <li>
|
||
|
* <p>{@link #registerRouteCallback} ignores any {@link RouteDiscoveryPreference discovery
|
||
|
* preference} passed by a proxy router. Use {@link RouteDiscoveryPreference#EMPTY} when
|
||
|
* setting a route callback.
|
||
|
* <li>
|
||
|
* <p>Methods returning non-system {@link RoutingController controllers} always return new
|
||
|
* instances with the latest data. Do not attempt to compare or store them. Instead, use
|
||
|
* {@link #getController(String)} or {@link #getControllers()} to query the most
|
||
|
* up-to-date state.
|
||
|
* <li>
|
||
|
* <p>Calls to {@link #setOnGetControllerHintsListener} are ignored.
|
||
|
* </ul>
|
||
|
*
|
||
|
* <p>Use this method when you only hold a revocable version of {@link
|
||
|
* Manifest.permission#MEDIA_ROUTING_CONTROL} (e.g. acquired via the {@link AppOpsManager}).
|
||
|
* Otherwise, use {@link #getInstance(Context, String)}.
|
||
|
*
|
||
|
* <p>{@code onInstanceInvalidatedListener} is called when the instance is invalidated because
|
||
|
* the calling app has lost {@link Manifest.permission#MEDIA_ROUTING_CONTROL} and does not hold
|
||
|
* {@link Manifest.permission#MEDIA_CONTENT_CONTROL}. Do not use the invalidated instance after
|
||
|
* receiving this callback, as the system will ignore all operations. Call {@link
|
||
|
* #getInstance(Context, String, Executor, Runnable)} again after reacquiring the relevant
|
||
|
* permissions.
|
||
|
*
|
||
|
* @param context The {@link Context} of the caller.
|
||
|
* @param clientPackageName The package name of the app you want to control the routing of.
|
||
|
* @param executor The {@link Executor} on which to invoke {@code
|
||
|
* onInstanceInvalidatedListener}.
|
||
|
* @param onInstanceInvalidatedListener Callback for when the {@link MediaRouter2} instance is
|
||
|
* invalidated due to lost permissions.
|
||
|
* @throws IllegalArgumentException if {@code clientPackageName} does not exist in the calling
|
||
|
* user.
|
||
|
*/
|
||
|
@SuppressWarnings("RequiresPermission")
|
||
|
@FlaggedApi(FLAG_ENABLE_PRIVILEGED_ROUTING_FOR_MEDIA_ROUTING_CONTROL)
|
||
|
@RequiresPermission(
|
||
|
anyOf = {
|
||
|
Manifest.permission.MEDIA_CONTENT_CONTROL,
|
||
|
Manifest.permission.MEDIA_ROUTING_CONTROL
|
||
|
})
|
||
|
@NonNull
|
||
|
public static MediaRouter2 getInstance(
|
||
|
@NonNull Context context,
|
||
|
@NonNull String clientPackageName,
|
||
|
@NonNull Executor executor,
|
||
|
@NonNull Runnable onInstanceInvalidatedListener) {
|
||
|
Objects.requireNonNull(executor, "Executor must not be null");
|
||
|
Objects.requireNonNull(
|
||
|
onInstanceInvalidatedListener, "onInstanceInvalidatedListener must not be null.");
|
||
|
|
||
|
return findOrCreateProxyInstanceForCallingUser(
|
||
|
context,
|
||
|
clientPackageName,
|
||
|
context.getUser(),
|
||
|
executor,
|
||
|
onInstanceInvalidatedListener);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns a proxy MediaRouter2 instance that allows you to control the routing of an app
|
||
|
* specified by {@code clientPackageName} and {@code user}.
|
||
|
*
|
||
|
* <p>Proxy MediaRouter2 instances operate differently than regular MediaRouter2 instances:
|
||
|
*
|
||
|
* <ul>
|
||
|
* <li>
|
||
|
* <p>{@link #registerRouteCallback} ignores any {@link RouteDiscoveryPreference discovery
|
||
|
* preference} passed by a proxy router. Use a {@link RouteDiscoveryPreference} with empty
|
||
|
* {@link RouteDiscoveryPreference.Builder#setPreferredFeatures(List) preferred features}
|
||
|
* when setting a route callback.
|
||
|
* <li>
|
||
|
* <p>Methods returning non-system {@link RoutingController controllers} always return new
|
||
|
* instances with the latest data. Do not attempt to compare or store them. Instead, use
|
||
|
* {@link #getController(String)} or {@link #getControllers()} to query the most
|
||
|
* up-to-date state.
|
||
|
* <li>
|
||
|
* <p>Calls to {@link #setOnGetControllerHintsListener} are ignored.
|
||
|
* </ul>
|
||
|
*
|
||
|
* @param context The {@link Context} of the caller.
|
||
|
* @param clientPackageName The package name of the app you want to control the routing of.
|
||
|
* @param user The {@link UserHandle} of the user running the app for which to get the proxy
|
||
|
* router instance. Must match {@link Process#myUserHandle()} if the caller doesn't hold
|
||
|
* {@code Manifest.permission#INTERACT_ACROSS_USERS_FULL}.
|
||
|
* @throws SecurityException if {@code user} does not match {@link Process#myUserHandle()} and
|
||
|
* the caller does not hold {@code Manifest.permission#INTERACT_ACROSS_USERS_FULL}.
|
||
|
* @throws IllegalArgumentException if {@code clientPackageName} does not exist in {@code user}.
|
||
|
* @hide
|
||
|
*/
|
||
|
@RequiresPermission(
|
||
|
anyOf = {
|
||
|
Manifest.permission.MEDIA_CONTENT_CONTROL,
|
||
|
Manifest.permission.MEDIA_ROUTING_CONTROL
|
||
|
})
|
||
|
@NonNull
|
||
|
public static MediaRouter2 getInstance(
|
||
|
@NonNull Context context, @NonNull String clientPackageName, @NonNull UserHandle user) {
|
||
|
return findOrCreateProxyInstanceForCallingUser(
|
||
|
context,
|
||
|
clientPackageName,
|
||
|
user,
|
||
|
/* executor */ null,
|
||
|
/* onInstanceInvalidatedListener */ null);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the per-process singleton proxy router instance for the {@code clientPackageName} and
|
||
|
* {@code user} if it exists, or otherwise it creates the appropriate instance.
|
||
|
*
|
||
|
* <p>If no instance has been created previously, the method will create an instance via {@link
|
||
|
* #MediaRouter2(Context, Looper, String, UserHandle)}.
|
||
|
*/
|
||
|
@NonNull
|
||
|
private static MediaRouter2 findOrCreateProxyInstanceForCallingUser(
|
||
|
Context context,
|
||
|
String clientPackageName,
|
||
|
UserHandle user,
|
||
|
@Nullable Executor executor,
|
||
|
@Nullable Runnable onInstanceInvalidatedListener) {
|
||
|
Objects.requireNonNull(context, "context must not be null");
|
||
|
Objects.requireNonNull(user, "user must not be null");
|
||
|
|
||
|
if (TextUtils.isEmpty(clientPackageName)) {
|
||
|
throw new IllegalArgumentException("clientPackageName must not be null or empty");
|
||
|
}
|
||
|
|
||
|
if (executor == null || onInstanceInvalidatedListener == null) {
|
||
|
if (checkCallerHasOnlyRevocablePermissions(context)) {
|
||
|
throw new IllegalStateException(
|
||
|
"Use getInstance(Context, String, Executor, Runnable) to obtain a proxy"
|
||
|
+ " MediaRouter2 instance.");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
PackageNameUserHandlePair key = new PackageNameUserHandlePair(clientPackageName, user);
|
||
|
|
||
|
synchronized (sSystemRouterLock) {
|
||
|
MediaRouter2 instance = sAppToProxyRouterMap.get(key);
|
||
|
if (instance == null) {
|
||
|
instance =
|
||
|
new MediaRouter2(context, Looper.getMainLooper(), clientPackageName, user);
|
||
|
// Register proxy router after instantiation to avoid race condition.
|
||
|
((ProxyMediaRouter2Impl) instance.mImpl).registerProxyRouter();
|
||
|
sAppToProxyRouterMap.put(key, instance);
|
||
|
}
|
||
|
((ProxyMediaRouter2Impl) instance.mImpl)
|
||
|
.registerInstanceInvalidatedCallback(executor, onInstanceInvalidatedListener);
|
||
|
return instance;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static boolean checkCallerHasOnlyRevocablePermissions(@NonNull Context context) {
|
||
|
boolean hasMediaContentControl =
|
||
|
context.checkSelfPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
|
||
|
== PackageManager.PERMISSION_GRANTED;
|
||
|
boolean hasRegularMediaRoutingControl =
|
||
|
context.checkSelfPermission(Manifest.permission.MEDIA_ROUTING_CONTROL)
|
||
|
== PackageManager.PERMISSION_GRANTED;
|
||
|
AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class);
|
||
|
boolean hasAppOpMediaRoutingControl =
|
||
|
appOpsManager.unsafeCheckOp(
|
||
|
AppOpsManager.OPSTR_MEDIA_ROUTING_CONTROL,
|
||
|
context.getApplicationInfo().uid,
|
||
|
context.getOpPackageName())
|
||
|
== AppOpsManager.MODE_ALLOWED;
|
||
|
|
||
|
return !hasMediaContentControl
|
||
|
&& !hasRegularMediaRoutingControl
|
||
|
&& hasAppOpMediaRoutingControl;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Starts scanning remote routes.
|
||
|
*
|
||
|
* <p>Route discovery can happen even when the {@link #startScan()} is not called. This is
|
||
|
* because the scanning could be started before by other apps. Therefore, calling this method
|
||
|
* after calling {@link #stopScan()} does not necessarily mean that the routes found before are
|
||
|
* removed and added again.
|
||
|
*
|
||
|
* <p>Use {@link RouteCallback} to get the route related events.
|
||
|
*
|
||
|
* <p>Note that calling start/stopScan is applied to all system routers in the same process.
|
||
|
*
|
||
|
* <p>This will be no-op for non-system media routers.
|
||
|
*
|
||
|
* @see #stopScan()
|
||
|
* @see #getInstance(Context, String)
|
||
|
* @hide
|
||
|
*/
|
||
|
@SystemApi
|
||
|
@RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
|
||
|
public void startScan() {
|
||
|
mImpl.startScan();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Stops scanning remote routes to reduce resource consumption.
|
||
|
*
|
||
|
* <p>Route discovery can be continued even after this method is called. This is because the
|
||
|
* scanning is only turned off when all the apps stop scanning. Therefore, calling this method
|
||
|
* does not necessarily mean the routes are removed. Also, for the same reason it does not mean
|
||
|
* that {@link RouteCallback#onRoutesAdded(List)} is not called afterwards.
|
||
|
*
|
||
|
* <p>Use {@link RouteCallback} to get the route related events.
|
||
|
*
|
||
|
* <p>Note that calling start/stopScan is applied to all system routers in the same process.
|
||
|
*
|
||
|
* <p>This will be no-op for non-system media routers.
|
||
|
*
|
||
|
* @see #startScan()
|
||
|
* @see #getInstance(Context, String)
|
||
|
* @hide
|
||
|
*/
|
||
|
@SystemApi
|
||
|
@RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
|
||
|
public void stopScan() {
|
||
|
mImpl.stopScan();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Requests the system to actively scan for routes based on the router's {@link
|
||
|
* RouteDiscoveryPreference route discovery preference}.
|
||
|
*
|
||
|
* <p>You must call {@link #cancelScanRequest(ScanToken)} promptly to preserve system resources
|
||
|
* like battery. Avoid scanning unless there is clear intention from the user to start routing
|
||
|
* their media.
|
||
|
*
|
||
|
* <p>{@code scanRequest} specifies relevant scanning options, like whether the system should
|
||
|
* scan with the screen off. Screen off scanning requires {@link
|
||
|
* Manifest.permission#MEDIA_ROUTING_CONTROL}
|
||
|
*
|
||
|
* <p>Proxy routers use the registered {@link RouteDiscoveryPreference} of their target routers.
|
||
|
*
|
||
|
* @return A unique {@link ScanToken} that identifies the scan request.
|
||
|
*/
|
||
|
@FlaggedApi(FLAG_ENABLE_SCREEN_OFF_SCANNING)
|
||
|
@NonNull
|
||
|
public ScanToken requestScan(@NonNull ScanRequest scanRequest) {
|
||
|
Objects.requireNonNull(scanRequest, "scanRequest must not be null.");
|
||
|
ScanToken token = new ScanToken(mNextRequestId.getAndIncrement());
|
||
|
|
||
|
synchronized (mLock) {
|
||
|
boolean shouldUpdate =
|
||
|
mScreenOffScanRequestCount == 0
|
||
|
&& (scanRequest.isScreenOffScan() || mScreenOnScanRequestCount == 0);
|
||
|
|
||
|
if (shouldUpdate) {
|
||
|
try {
|
||
|
mImpl.updateScanningState(
|
||
|
scanRequest.isScreenOffScan()
|
||
|
? SCANNING_STATE_SCANNING_FULL
|
||
|
: SCANNING_STATE_WHILE_INTERACTIVE);
|
||
|
|
||
|
} catch (RemoteException ex) {
|
||
|
throw ex.rethrowFromSystemServer();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (scanRequest.isScreenOffScan()) {
|
||
|
mScreenOffScanRequestCount++;
|
||
|
} else {
|
||
|
mScreenOnScanRequestCount++;
|
||
|
}
|
||
|
|
||
|
mScanRequestsMap.put(token.mId, scanRequest);
|
||
|
return token;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Releases the active scan request linked to the provided {@link ScanToken}.
|
||
|
*
|
||
|
* @see #requestScan(ScanRequest)
|
||
|
* @param token {@link ScanToken} of the {@link ScanRequest} to release.
|
||
|
* @throws IllegalArgumentException if the token does not match any active scan request.
|
||
|
*/
|
||
|
@FlaggedApi(FLAG_ENABLE_SCREEN_OFF_SCANNING)
|
||
|
public void cancelScanRequest(@NonNull ScanToken token) {
|
||
|
Objects.requireNonNull(token, "token must not be null");
|
||
|
|
||
|
synchronized (mLock) {
|
||
|
ScanRequest request = mScanRequestsMap.get(token.mId);
|
||
|
|
||
|
if (request == null) {
|
||
|
throw new IllegalArgumentException(
|
||
|
"The token does not match any active scan request");
|
||
|
}
|
||
|
|
||
|
boolean shouldUpdate =
|
||
|
request.isScreenOffScan()
|
||
|
? mScreenOffScanRequestCount == 1
|
||
|
: mScreenOnScanRequestCount == 1 && mScreenOffScanRequestCount == 0;
|
||
|
|
||
|
if (shouldUpdate) {
|
||
|
try {
|
||
|
if (!request.isScreenOffScan() || mScreenOnScanRequestCount == 0) {
|
||
|
mImpl.updateScanningState(SCANNING_STATE_NOT_SCANNING);
|
||
|
} else {
|
||
|
mImpl.updateScanningState(SCANNING_STATE_WHILE_INTERACTIVE);
|
||
|
}
|
||
|
|
||
|
} catch (RemoteException ex) {
|
||
|
ex.rethrowFromSystemServer();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (request.isScreenOffScan()) {
|
||
|
mScreenOffScanRequestCount--;
|
||
|
} else {
|
||
|
mScreenOnScanRequestCount--;
|
||
|
}
|
||
|
|
||
|
mScanRequestsMap.remove(token.mId);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private MediaRouter2(Context appContext) {
|
||
|
mContext = appContext;
|
||
|
mMediaRouterService =
|
||
|
IMediaRouterService.Stub.asInterface(
|
||
|
ServiceManager.getService(Context.MEDIA_ROUTER_SERVICE));
|
||
|
mImpl = new LocalMediaRouter2Impl(mContext.getPackageName());
|
||
|
mHandler = new Handler(Looper.getMainLooper());
|
||
|
|
||
|
loadSystemRoutes(/* isProxyRouter */ false);
|
||
|
|
||
|
RoutingSessionInfo currentSystemSessionInfo = mImpl.getSystemSessionInfo();
|
||
|
if (currentSystemSessionInfo == null) {
|
||
|
throw new RuntimeException("Null currentSystemSessionInfo. Something is wrong.");
|
||
|
}
|
||
|
|
||
|
mSystemController = new SystemRoutingController(currentSystemSessionInfo);
|
||
|
}
|
||
|
|
||
|
private MediaRouter2(
|
||
|
Context context, Looper looper, String clientPackageName, UserHandle user) {
|
||
|
mContext = context;
|
||
|
mHandler = new Handler(looper);
|
||
|
mMediaRouterService =
|
||
|
IMediaRouterService.Stub.asInterface(
|
||
|
ServiceManager.getService(Context.MEDIA_ROUTER_SERVICE));
|
||
|
|
||
|
loadSystemRoutes(/* isProxyRouter */ true);
|
||
|
|
||
|
mSystemController =
|
||
|
new SystemRoutingController(
|
||
|
ProxyMediaRouter2Impl.getSystemSessionInfoImpl(
|
||
|
mMediaRouterService, mContext.getPackageName(), clientPackageName));
|
||
|
|
||
|
mImpl = new ProxyMediaRouter2Impl(context, clientPackageName, user);
|
||
|
}
|
||
|
|
||
|
@GuardedBy("mLock")
|
||
|
private void loadSystemRoutes(boolean isProxyRouter) {
|
||
|
List<MediaRoute2Info> currentSystemRoutes = null;
|
||
|
try {
|
||
|
currentSystemRoutes = mMediaRouterService.getSystemRoutes(mContext.getPackageName(),
|
||
|
isProxyRouter);
|
||
|
} catch (RemoteException ex) {
|
||
|
ex.rethrowFromSystemServer();
|
||
|
}
|
||
|
|
||
|
if (currentSystemRoutes == null || currentSystemRoutes.isEmpty()) {
|
||
|
throw new RuntimeException("Null or empty currentSystemRoutes. Something is wrong.");
|
||
|
}
|
||
|
|
||
|
for (MediaRoute2Info route : currentSystemRoutes) {
|
||
|
mRoutes.put(route.getId(), route);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets the client package name of the app which this media router controls.
|
||
|
*
|
||
|
* <p>This will return null for non-system media routers.
|
||
|
*
|
||
|
* @see #getInstance(Context, String)
|
||
|
* @hide
|
||
|
*/
|
||
|
@SystemApi
|
||
|
@Nullable
|
||
|
public String getClientPackageName() {
|
||
|
return mImpl.getClientPackageName();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Registers a callback to discover routes and to receive events when they change.
|
||
|
*
|
||
|
* <p>If the specified callback is already registered, its registration will be updated for the
|
||
|
* given {@link Executor executor} and {@link RouteDiscoveryPreference discovery preference}.
|
||
|
*/
|
||
|
public void registerRouteCallback(
|
||
|
@NonNull @CallbackExecutor Executor executor,
|
||
|
@NonNull RouteCallback routeCallback,
|
||
|
@NonNull RouteDiscoveryPreference preference) {
|
||
|
Objects.requireNonNull(executor, "executor must not be null");
|
||
|
Objects.requireNonNull(routeCallback, "callback must not be null");
|
||
|
Objects.requireNonNull(preference, "preference must not be null");
|
||
|
|
||
|
RouteCallbackRecord record =
|
||
|
mImpl.createRouteCallbackRecord(executor, routeCallback, preference);
|
||
|
|
||
|
mRouteCallbackRecords.remove(record);
|
||
|
// It can fail to add the callback record if another registration with the same callback
|
||
|
// is happening but it's okay because either this or the other registration should be done.
|
||
|
mRouteCallbackRecords.addIfAbsent(record);
|
||
|
|
||
|
mImpl.registerRouteCallback();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Unregisters the given callback. The callback will no longer receive events. If the callback
|
||
|
* has not been added or been removed already, it is ignored.
|
||
|
*
|
||
|
* @param routeCallback the callback to unregister
|
||
|
* @see #registerRouteCallback
|
||
|
*/
|
||
|
public void unregisterRouteCallback(@NonNull RouteCallback routeCallback) {
|
||
|
Objects.requireNonNull(routeCallback, "callback must not be null");
|
||
|
|
||
|
if (!mRouteCallbackRecords.remove(new RouteCallbackRecord(null, routeCallback, null))) {
|
||
|
Log.w(TAG, "unregisterRouteCallback: Ignoring unknown callback");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
mImpl.unregisterRouteCallback();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Registers the given callback to be invoked when the {@link RouteListingPreference} of the
|
||
|
* target router changes.
|
||
|
*
|
||
|
* <p>Calls using a previously registered callback will overwrite the previous executor.
|
||
|
*
|
||
|
* @see #setRouteListingPreference(RouteListingPreference)
|
||
|
*/
|
||
|
@FlaggedApi(FLAG_ENABLE_RLP_CALLBACKS_IN_MEDIA_ROUTER2)
|
||
|
public void registerRouteListingPreferenceUpdatedCallback(
|
||
|
@NonNull @CallbackExecutor Executor executor,
|
||
|
@NonNull Consumer<RouteListingPreference> routeListingPreferenceCallback) {
|
||
|
Objects.requireNonNull(executor, "executor must not be null");
|
||
|
Objects.requireNonNull(routeListingPreferenceCallback, "callback must not be null");
|
||
|
|
||
|
RouteListingPreferenceCallbackRecord record =
|
||
|
new RouteListingPreferenceCallbackRecord(executor, routeListingPreferenceCallback);
|
||
|
|
||
|
mListingPreferenceCallbackRecords.remove(record);
|
||
|
mListingPreferenceCallbackRecords.add(record);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Unregisters the given callback to not receive {@link RouteListingPreference} change events.
|
||
|
*
|
||
|
* @see #registerRouteListingPreferenceUpdatedCallback(Executor, Consumer)
|
||
|
*/
|
||
|
@FlaggedApi(FLAG_ENABLE_RLP_CALLBACKS_IN_MEDIA_ROUTER2)
|
||
|
public void unregisterRouteListingPreferenceUpdatedCallback(
|
||
|
@NonNull Consumer<RouteListingPreference> callback) {
|
||
|
Objects.requireNonNull(callback, "callback must not be null");
|
||
|
|
||
|
if (!mListingPreferenceCallbackRecords.remove(
|
||
|
new RouteListingPreferenceCallbackRecord(/* executor */ null, callback))) {
|
||
|
Log.w(
|
||
|
TAG,
|
||
|
"unregisterRouteListingPreferenceUpdatedCallback: Ignoring an unknown"
|
||
|
+ " callback");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Shows the system output switcher dialog.
|
||
|
*
|
||
|
* <p>Should only be called when the context of MediaRouter2 is in the foreground and visible on
|
||
|
* the screen.
|
||
|
*
|
||
|
* <p>The appearance and precise behaviour of the system output switcher dialog may vary across
|
||
|
* different devices, OS versions, and form factors, but the basic functionality stays the same.
|
||
|
*
|
||
|
* <p>See <a
|
||
|
* href="https://developer.android.com/guide/topics/media/media-routing#output-switcher">Output
|
||
|
* Switcher documentation</a> for more details.
|
||
|
*
|
||
|
* @return {@code true} if the output switcher dialog is being shown, or {@code false} if the
|
||
|
* call is ignored because the app is in the background.
|
||
|
*/
|
||
|
public boolean showSystemOutputSwitcher() {
|
||
|
return mImpl.showSystemOutputSwitcher();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets the {@link RouteListingPreference} of the app associated to this media router.
|
||
|
*
|
||
|
* <p>Use this method to inform the system UI of the routes that you would like to list for
|
||
|
* media routing, via the Output Switcher.
|
||
|
*
|
||
|
* <p>You should call this method before {@link #registerRouteCallback registering any route
|
||
|
* callbacks} and immediately after receiving any {@link RouteCallback#onRoutesUpdated route
|
||
|
* updates} in order to keep the system UI in a consistent state. You can also call this method
|
||
|
* at any other point to update the listing preference dynamically.
|
||
|
*
|
||
|
* <p>Any calls to this method from a privileged router will throw an {@link
|
||
|
* UnsupportedOperationException}.
|
||
|
*
|
||
|
* <p>Notes:
|
||
|
*
|
||
|
* <ol>
|
||
|
* <li>You should not include the ids of two or more routes with a match in their {@link
|
||
|
* MediaRoute2Info#getDeduplicationIds() deduplication ids}. If you do, the system will
|
||
|
* deduplicate them using its own criteria.
|
||
|
* <li>You can use this method to rank routes in the output switcher, placing the more
|
||
|
* important routes first. The system might override the proposed ranking.
|
||
|
* <li>You can use this method to avoid listing routes using dynamic criteria. For example,
|
||
|
* you can limit access to a specific type of device according to runtime criteria.
|
||
|
* </ol>
|
||
|
*
|
||
|
* @param routeListingPreference The {@link RouteListingPreference} for the system to use for
|
||
|
* route listing. When null, the system uses its default listing criteria.
|
||
|
*/
|
||
|
public void setRouteListingPreference(@Nullable RouteListingPreference routeListingPreference) {
|
||
|
mImpl.setRouteListingPreference(routeListingPreference);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the current {@link RouteListingPreference} of the target router.
|
||
|
*
|
||
|
* <p>If this instance was created using {@code #getInstance(Context, String)}, then it returns
|
||
|
* the last {@link RouteListingPreference} set by the process this router was created for.
|
||
|
*
|
||
|
* @see #setRouteListingPreference(RouteListingPreference)
|
||
|
*/
|
||
|
@FlaggedApi(FLAG_ENABLE_RLP_CALLBACKS_IN_MEDIA_ROUTER2)
|
||
|
@Nullable
|
||
|
public RouteListingPreference getRouteListingPreference() {
|
||
|
synchronized (mLock) {
|
||
|
return mRouteListingPreference;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@GuardedBy("mLock")
|
||
|
private boolean updateDiscoveryPreferenceIfNeededLocked() {
|
||
|
RouteDiscoveryPreference newDiscoveryPreference = new RouteDiscoveryPreference.Builder(
|
||
|
mRouteCallbackRecords.stream().map(record -> record.mPreference).collect(
|
||
|
Collectors.toList())).build();
|
||
|
|
||
|
if (Objects.equals(mDiscoveryPreference, newDiscoveryPreference)) {
|
||
|
return false;
|
||
|
}
|
||
|
mDiscoveryPreference = newDiscoveryPreference;
|
||
|
updateFilteredRoutesLocked();
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets the list of all discovered routes. This list includes the routes that are not related to
|
||
|
* the client app.
|
||
|
*
|
||
|
* <p>This will return an empty list for non-system media routers.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
@SystemApi
|
||
|
@NonNull
|
||
|
public List<MediaRoute2Info> getAllRoutes() {
|
||
|
return mImpl.getAllRoutes();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets the unmodifiable list of {@link MediaRoute2Info routes} currently known to the media
|
||
|
* router.
|
||
|
*
|
||
|
* <p>Please note that the list can be changed before callbacks are invoked.
|
||
|
*
|
||
|
* @return the list of routes that contains at least one of the route features in discovery
|
||
|
* preferences registered by the application
|
||
|
*/
|
||
|
@NonNull
|
||
|
public List<MediaRoute2Info> getRoutes() {
|
||
|
synchronized (mLock) {
|
||
|
return mFilteredRoutes;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Registers a callback to get the result of {@link #transferTo(MediaRoute2Info)}.
|
||
|
* If you register the same callback twice or more, it will be ignored.
|
||
|
*
|
||
|
* @param executor the executor to execute the callback on
|
||
|
* @param callback the callback to register
|
||
|
* @see #unregisterTransferCallback
|
||
|
*/
|
||
|
public void registerTransferCallback(
|
||
|
@NonNull @CallbackExecutor Executor executor, @NonNull TransferCallback callback) {
|
||
|
Objects.requireNonNull(executor, "executor must not be null");
|
||
|
Objects.requireNonNull(callback, "callback must not be null");
|
||
|
|
||
|
TransferCallbackRecord record = new TransferCallbackRecord(executor, callback);
|
||
|
if (!mTransferCallbackRecords.addIfAbsent(record)) {
|
||
|
Log.w(TAG, "registerTransferCallback: Ignoring the same callback");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Unregisters the given callback. The callback will no longer receive events.
|
||
|
* If the callback has not been added or been removed already, it is ignored.
|
||
|
*
|
||
|
* @param callback the callback to unregister
|
||
|
* @see #registerTransferCallback
|
||
|
*/
|
||
|
public void unregisterTransferCallback(@NonNull TransferCallback callback) {
|
||
|
Objects.requireNonNull(callback, "callback must not be null");
|
||
|
|
||
|
if (!mTransferCallbackRecords.remove(new TransferCallbackRecord(null, callback))) {
|
||
|
Log.w(TAG, "unregisterTransferCallback: Ignoring an unknown callback");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Registers a {@link ControllerCallback}. If you register the same callback twice or more, it
|
||
|
* will be ignored.
|
||
|
*
|
||
|
* @see #unregisterControllerCallback(ControllerCallback)
|
||
|
*/
|
||
|
public void registerControllerCallback(
|
||
|
@NonNull @CallbackExecutor Executor executor, @NonNull ControllerCallback callback) {
|
||
|
Objects.requireNonNull(executor, "executor must not be null");
|
||
|
Objects.requireNonNull(callback, "callback must not be null");
|
||
|
|
||
|
ControllerCallbackRecord record = new ControllerCallbackRecord(executor, callback);
|
||
|
if (!mControllerCallbackRecords.addIfAbsent(record)) {
|
||
|
Log.w(TAG, "registerControllerCallback: Ignoring the same callback");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Unregisters a {@link ControllerCallback}. The callback will no longer receive events.
|
||
|
* If the callback has not been added or been removed already, it is ignored.
|
||
|
*
|
||
|
* @see #registerControllerCallback(Executor, ControllerCallback)
|
||
|
*/
|
||
|
public void unregisterControllerCallback(@NonNull ControllerCallback callback) {
|
||
|
Objects.requireNonNull(callback, "callback must not be null");
|
||
|
|
||
|
if (!mControllerCallbackRecords.remove(new ControllerCallbackRecord(null, callback))) {
|
||
|
Log.w(TAG, "unregisterControllerCallback: Ignoring an unknown callback");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets an {@link OnGetControllerHintsListener} to send hints when creating a
|
||
|
* {@link RoutingController}. To send the hints, listener should be set <em>BEFORE</em> calling
|
||
|
* {@link #transferTo(MediaRoute2Info)}.
|
||
|
*
|
||
|
* @param listener A listener to send optional app-specific hints when creating a controller.
|
||
|
* {@code null} for unset.
|
||
|
*/
|
||
|
public void setOnGetControllerHintsListener(@Nullable OnGetControllerHintsListener listener) {
|
||
|
mImpl.setOnGetControllerHintsListener(listener);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Transfers the current media to the given route. If it's necessary a new
|
||
|
* {@link RoutingController} is created or it is handled within the current routing controller.
|
||
|
*
|
||
|
* @param route the route you want to transfer the current media to. Pass {@code null} to
|
||
|
* stop routing of the current media.
|
||
|
* @see TransferCallback#onTransfer
|
||
|
* @see TransferCallback#onTransferFailure
|
||
|
*/
|
||
|
public void transferTo(@NonNull MediaRoute2Info route) {
|
||
|
mImpl.transferTo(route);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Stops the current media routing. If the {@link #getSystemController() system controller}
|
||
|
* controls the media routing, this method is a no-op.
|
||
|
*/
|
||
|
public void stop() {
|
||
|
mImpl.stop();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Transfers the media of a routing controller to the given route.
|
||
|
*
|
||
|
* <p>This will be no-op for non-system media routers.
|
||
|
*
|
||
|
* @param controller a routing controller controlling media routing.
|
||
|
* @param route the route you want to transfer the media to.
|
||
|
* @hide
|
||
|
*/
|
||
|
@SystemApi
|
||
|
@RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL)
|
||
|
public void transfer(@NonNull RoutingController controller, @NonNull MediaRoute2Info route) {
|
||
|
mImpl.transfer(controller.getRoutingSessionInfo(), route);
|
||
|
}
|
||
|
|
||
|
void requestCreateController(
|
||
|
@NonNull RoutingController controller,
|
||
|
@NonNull MediaRoute2Info route,
|
||
|
long managerRequestId,
|
||
|
@NonNull UserHandle transferInitiatorUserHandle,
|
||
|
@NonNull String transferInitiatorPackageName) {
|
||
|
|
||
|
final int requestId = mNextRequestId.getAndIncrement();
|
||
|
|
||
|
ControllerCreationRequest request =
|
||
|
new ControllerCreationRequest(requestId, managerRequestId, route, controller);
|
||
|
mControllerCreationRequests.add(request);
|
||
|
|
||
|
OnGetControllerHintsListener listener = mOnGetControllerHintsListener;
|
||
|
Bundle controllerHints = null;
|
||
|
if (listener != null) {
|
||
|
controllerHints = listener.onGetControllerHints(route);
|
||
|
if (controllerHints != null) {
|
||
|
controllerHints = new Bundle(controllerHints);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
MediaRouter2Stub stub;
|
||
|
synchronized (mLock) {
|
||
|
stub = mStub;
|
||
|
}
|
||
|
if (stub != null) {
|
||
|
try {
|
||
|
mMediaRouterService.requestCreateSessionWithRouter2(
|
||
|
stub,
|
||
|
requestId,
|
||
|
managerRequestId,
|
||
|
controller.getRoutingSessionInfo(),
|
||
|
route,
|
||
|
controllerHints,
|
||
|
transferInitiatorUserHandle,
|
||
|
transferInitiatorPackageName);
|
||
|
} catch (RemoteException ex) {
|
||
|
Log.e(TAG, "createControllerForTransfer: "
|
||
|
+ "Failed to request for creating a controller.", ex);
|
||
|
mControllerCreationRequests.remove(request);
|
||
|
if (managerRequestId == MANAGER_REQUEST_ID_NONE) {
|
||
|
notifyTransferFailure(route);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@NonNull
|
||
|
private RoutingController getCurrentController() {
|
||
|
List<RoutingController> controllers = getControllers();
|
||
|
return controllers.get(controllers.size() - 1);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets a {@link RoutingController} which can control the routes provided by system.
|
||
|
* e.g. Phone speaker, wired headset, Bluetooth, etc.
|
||
|
*
|
||
|
* <p>Note: The system controller can't be released. Calling {@link RoutingController#release()}
|
||
|
* will be ignored.
|
||
|
*
|
||
|
* <p>This method always returns the same instance.
|
||
|
*/
|
||
|
@NonNull
|
||
|
public RoutingController getSystemController() {
|
||
|
return mSystemController;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets a {@link RoutingController} whose ID is equal to the given ID.
|
||
|
* Returns {@code null} if there is no matching controller.
|
||
|
*/
|
||
|
@Nullable
|
||
|
public RoutingController getController(@NonNull String id) {
|
||
|
Objects.requireNonNull(id, "id must not be null");
|
||
|
for (RoutingController controller : getControllers()) {
|
||
|
if (TextUtils.equals(id, controller.getId())) {
|
||
|
return controller;
|
||
|
}
|
||
|
}
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets the list of currently active {@link RoutingController routing controllers} on which
|
||
|
* media can be played.
|
||
|
*
|
||
|
* <p>Note: The list returned here will never be empty. The first element in the list is
|
||
|
* always the {@link #getSystemController() system controller}.
|
||
|
*/
|
||
|
@NonNull
|
||
|
public List<RoutingController> getControllers() {
|
||
|
return mImpl.getControllers();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets the volume for a specific route.
|
||
|
*
|
||
|
* <p>The call may have no effect if the route is currently not selected.
|
||
|
*
|
||
|
* <p>This method is only supported by {@link #getInstance(Context, String) proxy MediaRouter2
|
||
|
* instances}. Use {@link RoutingController#setVolume(int) RoutingController#setVolume(int)}
|
||
|
* instead for {@link #getInstance(Context) local MediaRouter2 instances}.</p>
|
||
|
*
|
||
|
* @param volume The new volume value between 0 and {@link MediaRoute2Info#getVolumeMax}.
|
||
|
* @throws UnsupportedOperationException If called on a {@link #getInstance(Context) local
|
||
|
* router instance}.
|
||
|
*/
|
||
|
@FlaggedApi(FLAG_ENABLE_PRIVILEGED_ROUTING_FOR_MEDIA_ROUTING_CONTROL)
|
||
|
@RequiresPermission(
|
||
|
anyOf = {
|
||
|
Manifest.permission.MEDIA_CONTENT_CONTROL,
|
||
|
Manifest.permission.MEDIA_ROUTING_CONTROL
|
||
|
})
|
||
|
public void setRouteVolume(@NonNull MediaRoute2Info route, int volume) {
|
||
|
Objects.requireNonNull(route, "route must not be null");
|
||
|
|
||
|
mImpl.setRouteVolume(route, volume);
|
||
|
}
|
||
|
|
||
|
void syncRoutesOnHandler(
|
||
|
List<MediaRoute2Info> currentRoutes, RoutingSessionInfo currentSystemSessionInfo) {
|
||
|
if (currentRoutes == null || currentRoutes.isEmpty() || currentSystemSessionInfo == null) {
|
||
|
Log.e(TAG, "syncRoutesOnHandler: Received wrong data. currentRoutes=" + currentRoutes
|
||
|
+ ", currentSystemSessionInfo=" + currentSystemSessionInfo);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
updateRoutesOnHandler(currentRoutes);
|
||
|
|
||
|
RoutingSessionInfo oldInfo = mSystemController.getRoutingSessionInfo();
|
||
|
mSystemController.setRoutingSessionInfo(ensureClientPackageNameForSystemSession(
|
||
|
currentSystemSessionInfo, mContext.getPackageName()));
|
||
|
if (!oldInfo.equals(currentSystemSessionInfo)) {
|
||
|
notifyControllerUpdated(mSystemController);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void dispatchFilteredRoutesUpdatedOnHandler(List<MediaRoute2Info> newRoutes) {
|
||
|
List<MediaRoute2Info> addedRoutes = new ArrayList<>();
|
||
|
List<MediaRoute2Info> removedRoutes = new ArrayList<>();
|
||
|
List<MediaRoute2Info> changedRoutes = new ArrayList<>();
|
||
|
|
||
|
Set<String> newRouteIds =
|
||
|
newRoutes.stream().map(MediaRoute2Info::getId).collect(Collectors.toSet());
|
||
|
|
||
|
for (MediaRoute2Info route : newRoutes) {
|
||
|
MediaRoute2Info prevRoute = mPreviousFilteredRoutes.get(route.getId());
|
||
|
if (prevRoute == null) {
|
||
|
addedRoutes.add(route);
|
||
|
} else if (!prevRoute.equals(route)) {
|
||
|
changedRoutes.add(route);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for (int i = 0; i < mPreviousFilteredRoutes.size(); i++) {
|
||
|
if (!newRouteIds.contains(mPreviousFilteredRoutes.keyAt(i))) {
|
||
|
removedRoutes.add(mPreviousFilteredRoutes.valueAt(i));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// update previous routes
|
||
|
for (MediaRoute2Info route : removedRoutes) {
|
||
|
mPreviousFilteredRoutes.remove(route.getId());
|
||
|
}
|
||
|
for (MediaRoute2Info route : addedRoutes) {
|
||
|
mPreviousFilteredRoutes.put(route.getId(), route);
|
||
|
}
|
||
|
for (MediaRoute2Info route : changedRoutes) {
|
||
|
mPreviousFilteredRoutes.put(route.getId(), route);
|
||
|
}
|
||
|
|
||
|
if (!addedRoutes.isEmpty()) {
|
||
|
notifyRoutesAdded(addedRoutes);
|
||
|
}
|
||
|
if (!removedRoutes.isEmpty()) {
|
||
|
notifyRoutesRemoved(removedRoutes);
|
||
|
}
|
||
|
if (!changedRoutes.isEmpty()) {
|
||
|
notifyRoutesChanged(changedRoutes);
|
||
|
}
|
||
|
|
||
|
// Note: We don't notify clients of changes in route ordering.
|
||
|
if (!addedRoutes.isEmpty() || !removedRoutes.isEmpty() || !changedRoutes.isEmpty()) {
|
||
|
notifyRoutesUpdated(newRoutes);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void dispatchControllerUpdatedIfNeededOnHandler(Map<String, MediaRoute2Info> routesMap) {
|
||
|
List<RoutingController> controllers = getControllers();
|
||
|
for (RoutingController controller : controllers) {
|
||
|
|
||
|
for (String selectedRoute : controller.getRoutingSessionInfo().getSelectedRoutes()) {
|
||
|
if (routesMap.containsKey(selectedRoute)
|
||
|
&& mPreviousUnfilteredRoutes.containsKey(selectedRoute)) {
|
||
|
MediaRoute2Info currentRoute = routesMap.get(selectedRoute);
|
||
|
MediaRoute2Info oldRoute = mPreviousUnfilteredRoutes.get(selectedRoute);
|
||
|
if (!currentRoute.equals(oldRoute)) {
|
||
|
notifyControllerUpdated(controller);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
mPreviousUnfilteredRoutes.clear();
|
||
|
mPreviousUnfilteredRoutes.putAll(routesMap);
|
||
|
}
|
||
|
|
||
|
void updateRoutesOnHandler(List<MediaRoute2Info> newRoutes) {
|
||
|
synchronized (mLock) {
|
||
|
mRoutes.clear();
|
||
|
for (MediaRoute2Info route : newRoutes) {
|
||
|
mRoutes.put(route.getId(), route);
|
||
|
}
|
||
|
updateFilteredRoutesLocked();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** Updates filtered routes and dispatch callbacks */
|
||
|
@GuardedBy("mLock")
|
||
|
void updateFilteredRoutesLocked() {
|
||
|
mFilteredRoutes =
|
||
|
Collections.unmodifiableList(
|
||
|
filterRoutesWithCompositePreferenceLocked(List.copyOf(mRoutes.values())));
|
||
|
mHandler.sendMessage(
|
||
|
obtainMessage(
|
||
|
MediaRouter2::dispatchFilteredRoutesUpdatedOnHandler,
|
||
|
this,
|
||
|
mFilteredRoutes));
|
||
|
mHandler.sendMessage(
|
||
|
obtainMessage(
|
||
|
MediaRouter2::dispatchControllerUpdatedIfNeededOnHandler,
|
||
|
this,
|
||
|
new HashMap<>(mRoutes)));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Creates a controller and calls the {@link TransferCallback#onTransfer}. If the controller
|
||
|
* creation has failed, then it calls {@link TransferCallback#onTransferFailure}.
|
||
|
*
|
||
|
* <p>Pass {@code null} to sessionInfo for the failure case.
|
||
|
*/
|
||
|
void createControllerOnHandler(int requestId, @Nullable RoutingSessionInfo sessionInfo) {
|
||
|
ControllerCreationRequest matchingRequest = null;
|
||
|
for (ControllerCreationRequest request : mControllerCreationRequests) {
|
||
|
if (request.mRequestId == requestId) {
|
||
|
matchingRequest = request;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (matchingRequest == null) {
|
||
|
Log.w(TAG, "createControllerOnHandler: Ignoring an unknown request.");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
mControllerCreationRequests.remove(matchingRequest);
|
||
|
MediaRoute2Info requestedRoute = matchingRequest.mRoute;
|
||
|
|
||
|
// TODO: Notify the reason for failure.
|
||
|
if (sessionInfo == null) {
|
||
|
notifyTransferFailure(requestedRoute);
|
||
|
return;
|
||
|
} else if (!TextUtils.equals(requestedRoute.getProviderId(), sessionInfo.getProviderId())) {
|
||
|
Log.w(
|
||
|
TAG,
|
||
|
"The session's provider ID does not match the requested route's. "
|
||
|
+ "(requested route's providerId="
|
||
|
+ requestedRoute.getProviderId()
|
||
|
+ ", actual providerId="
|
||
|
+ sessionInfo.getProviderId()
|
||
|
+ ")");
|
||
|
notifyTransferFailure(requestedRoute);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
RoutingController oldController = matchingRequest.mOldController;
|
||
|
// When the old controller is released before transferred, treat it as a failure.
|
||
|
// This could also happen when transfer is requested twice or more.
|
||
|
if (!oldController.scheduleRelease()) {
|
||
|
Log.w(
|
||
|
TAG,
|
||
|
"createControllerOnHandler: "
|
||
|
+ "Ignoring controller creation for released old controller. "
|
||
|
+ "oldController="
|
||
|
+ oldController);
|
||
|
if (!sessionInfo.isSystemSession()) {
|
||
|
new RoutingController(sessionInfo).release();
|
||
|
}
|
||
|
notifyTransferFailure(requestedRoute);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
RoutingController newController = addRoutingController(sessionInfo);
|
||
|
notifyTransfer(oldController, newController);
|
||
|
}
|
||
|
|
||
|
@NonNull
|
||
|
private RoutingController addRoutingController(@NonNull RoutingSessionInfo session) {
|
||
|
RoutingController controller;
|
||
|
if (session.isSystemSession()) {
|
||
|
// mSystemController is never released, so we only need to update its status.
|
||
|
mSystemController.setRoutingSessionInfo(session);
|
||
|
controller = mSystemController;
|
||
|
} else {
|
||
|
controller = new RoutingController(session);
|
||
|
synchronized (mLock) {
|
||
|
mNonSystemRoutingControllers.put(controller.getId(), controller);
|
||
|
}
|
||
|
}
|
||
|
return controller;
|
||
|
}
|
||
|
|
||
|
void updateControllerOnHandler(RoutingSessionInfo sessionInfo) {
|
||
|
if (sessionInfo == null) {
|
||
|
Log.w(TAG, "updateControllerOnHandler: Ignoring null sessionInfo.");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
RoutingController controller =
|
||
|
getMatchingController(sessionInfo, /* logPrefix */ "updateControllerOnHandler");
|
||
|
if (controller != null) {
|
||
|
controller.setRoutingSessionInfo(sessionInfo);
|
||
|
notifyControllerUpdated(controller);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void releaseControllerOnHandler(RoutingSessionInfo sessionInfo) {
|
||
|
if (sessionInfo == null) {
|
||
|
Log.w(TAG, "releaseControllerOnHandler: Ignoring null sessionInfo.");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
RoutingController matchingController =
|
||
|
getMatchingController(sessionInfo, /* logPrefix */ "releaseControllerOnHandler");
|
||
|
|
||
|
if (matchingController != null) {
|
||
|
matchingController.releaseInternal(/* shouldReleaseSession= */ false);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Nullable
|
||
|
private RoutingController getMatchingController(
|
||
|
RoutingSessionInfo sessionInfo, String logPrefix) {
|
||
|
if (sessionInfo.isSystemSession()) {
|
||
|
return getSystemController();
|
||
|
} else {
|
||
|
RoutingController controller;
|
||
|
synchronized (mLock) {
|
||
|
controller = mNonSystemRoutingControllers.get(sessionInfo.getId());
|
||
|
}
|
||
|
|
||
|
if (controller == null) {
|
||
|
Log.w(
|
||
|
TAG,
|
||
|
logPrefix
|
||
|
+ ": Matching controller not found. uniqueSessionId="
|
||
|
+ sessionInfo.getId());
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
RoutingSessionInfo oldInfo = controller.getRoutingSessionInfo();
|
||
|
if (!TextUtils.equals(oldInfo.getProviderId(), sessionInfo.getProviderId())) {
|
||
|
Log.w(
|
||
|
TAG,
|
||
|
logPrefix
|
||
|
+ ": Provider IDs are not matched. old="
|
||
|
+ oldInfo.getProviderId()
|
||
|
+ ", new="
|
||
|
+ sessionInfo.getProviderId());
|
||
|
return null;
|
||
|
}
|
||
|
return controller;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void onRequestCreateControllerByManagerOnHandler(
|
||
|
RoutingSessionInfo oldSession,
|
||
|
MediaRoute2Info route,
|
||
|
long managerRequestId,
|
||
|
@NonNull UserHandle transferInitiatorUserHandle,
|
||
|
@NonNull String transferInitiatorPackageName) {
|
||
|
Log.i(
|
||
|
TAG,
|
||
|
TextUtils.formatSimple(
|
||
|
"requestCreateSessionByManager | requestId: %d, oldSession: %s, route: %s",
|
||
|
managerRequestId, oldSession, route));
|
||
|
RoutingController controller;
|
||
|
if (oldSession.isSystemSession()) {
|
||
|
controller = getSystemController();
|
||
|
} else {
|
||
|
synchronized (mLock) {
|
||
|
controller = mNonSystemRoutingControllers.get(oldSession.getId());
|
||
|
}
|
||
|
}
|
||
|
if (controller == null) {
|
||
|
return;
|
||
|
}
|
||
|
requestCreateController(controller, route, managerRequestId, transferInitiatorUserHandle,
|
||
|
transferInitiatorPackageName);
|
||
|
}
|
||
|
|
||
|
private List<MediaRoute2Info> getSortedRoutes(
|
||
|
List<MediaRoute2Info> routes, List<String> packageOrder) {
|
||
|
if (packageOrder.isEmpty()) {
|
||
|
return routes;
|
||
|
}
|
||
|
Map<String, Integer> packagePriority = new ArrayMap<>();
|
||
|
int count = packageOrder.size();
|
||
|
for (int i = 0; i < count; i++) {
|
||
|
// the last package will have 1 as the priority
|
||
|
packagePriority.put(packageOrder.get(i), count - i);
|
||
|
}
|
||
|
ArrayList<MediaRoute2Info> sortedRoutes = new ArrayList<>(routes);
|
||
|
// take the negative for descending order
|
||
|
sortedRoutes.sort(
|
||
|
Comparator.comparingInt(r -> -packagePriority.getOrDefault(r.getPackageName(), 0)));
|
||
|
return sortedRoutes;
|
||
|
}
|
||
|
|
||
|
@GuardedBy("mLock")
|
||
|
private List<MediaRoute2Info> filterRoutesWithCompositePreferenceLocked(
|
||
|
List<MediaRoute2Info> routes) {
|
||
|
|
||
|
Set<String> deduplicationIdSet = new ArraySet<>();
|
||
|
|
||
|
List<MediaRoute2Info> filteredRoutes = new ArrayList<>();
|
||
|
for (MediaRoute2Info route :
|
||
|
getSortedRoutes(routes, mDiscoveryPreference.getDeduplicationPackageOrder())) {
|
||
|
if (!route.hasAnyFeatures(mDiscoveryPreference.getPreferredFeatures())) {
|
||
|
continue;
|
||
|
}
|
||
|
if (!mDiscoveryPreference.getAllowedPackages().isEmpty()
|
||
|
&& (route.getPackageName() == null
|
||
|
|| !mDiscoveryPreference
|
||
|
.getAllowedPackages()
|
||
|
.contains(route.getPackageName()))) {
|
||
|
continue;
|
||
|
}
|
||
|
if (mDiscoveryPreference.shouldRemoveDuplicates()) {
|
||
|
if (!Collections.disjoint(deduplicationIdSet, route.getDeduplicationIds())) {
|
||
|
continue;
|
||
|
}
|
||
|
deduplicationIdSet.addAll(route.getDeduplicationIds());
|
||
|
}
|
||
|
filteredRoutes.add(route);
|
||
|
}
|
||
|
return filteredRoutes;
|
||
|
}
|
||
|
|
||
|
@NonNull
|
||
|
private List<MediaRoute2Info> getRoutesWithIds(@NonNull List<String> routeIds) {
|
||
|
synchronized (mLock) {
|
||
|
return routeIds.stream()
|
||
|
.map(mRoutes::get)
|
||
|
.filter(Objects::nonNull)
|
||
|
.collect(Collectors.toList());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void notifyRoutesAdded(List<MediaRoute2Info> routes) {
|
||
|
for (RouteCallbackRecord record : mRouteCallbackRecords) {
|
||
|
List<MediaRoute2Info> filteredRoutes =
|
||
|
mImpl.filterRoutesWithIndividualPreference(routes, record.mPreference);
|
||
|
if (!filteredRoutes.isEmpty()) {
|
||
|
record.mExecutor.execute(() -> record.mRouteCallback.onRoutesAdded(filteredRoutes));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void notifyRoutesRemoved(List<MediaRoute2Info> routes) {
|
||
|
for (RouteCallbackRecord record : mRouteCallbackRecords) {
|
||
|
List<MediaRoute2Info> filteredRoutes =
|
||
|
mImpl.filterRoutesWithIndividualPreference(routes, record.mPreference);
|
||
|
if (!filteredRoutes.isEmpty()) {
|
||
|
record.mExecutor.execute(
|
||
|
() -> record.mRouteCallback.onRoutesRemoved(filteredRoutes));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void notifyRoutesChanged(List<MediaRoute2Info> routes) {
|
||
|
for (RouteCallbackRecord record : mRouteCallbackRecords) {
|
||
|
List<MediaRoute2Info> filteredRoutes =
|
||
|
mImpl.filterRoutesWithIndividualPreference(routes, record.mPreference);
|
||
|
if (!filteredRoutes.isEmpty()) {
|
||
|
record.mExecutor.execute(
|
||
|
() -> record.mRouteCallback.onRoutesChanged(filteredRoutes));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void notifyRoutesUpdated(List<MediaRoute2Info> routes) {
|
||
|
for (RouteCallbackRecord record : mRouteCallbackRecords) {
|
||
|
List<MediaRoute2Info> filteredRoutes =
|
||
|
mImpl.filterRoutesWithIndividualPreference(routes, record.mPreference);
|
||
|
record.mExecutor.execute(() -> record.mRouteCallback.onRoutesUpdated(filteredRoutes));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void notifyPreferredFeaturesChanged(List<String> features) {
|
||
|
for (RouteCallbackRecord record : mRouteCallbackRecords) {
|
||
|
record.mExecutor.execute(
|
||
|
() -> record.mRouteCallback.onPreferredFeaturesChanged(features));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void notifyRouteListingPreferenceUpdated(@Nullable RouteListingPreference preference) {
|
||
|
for (RouteListingPreferenceCallbackRecord record : mListingPreferenceCallbackRecords) {
|
||
|
record.mExecutor.execute(
|
||
|
() -> record.mRouteListingPreferenceCallback.accept(preference));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void notifyTransfer(RoutingController oldController, RoutingController newController) {
|
||
|
for (TransferCallbackRecord record : mTransferCallbackRecords) {
|
||
|
record.mExecutor.execute(
|
||
|
() -> record.mTransferCallback.onTransfer(oldController, newController));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void notifyTransferFailure(MediaRoute2Info route) {
|
||
|
for (TransferCallbackRecord record : mTransferCallbackRecords) {
|
||
|
record.mExecutor.execute(() -> record.mTransferCallback.onTransferFailure(route));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void notifyRequestFailed(int reason) {
|
||
|
for (TransferCallbackRecord record : mTransferCallbackRecords) {
|
||
|
record.mExecutor.execute(() -> record.mTransferCallback.onRequestFailed(reason));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void notifyStop(RoutingController controller) {
|
||
|
for (TransferCallbackRecord record : mTransferCallbackRecords) {
|
||
|
record.mExecutor.execute(() -> record.mTransferCallback.onStop(controller));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void notifyControllerUpdated(RoutingController controller) {
|
||
|
for (ControllerCallbackRecord record : mControllerCallbackRecords) {
|
||
|
record.mExecutor.execute(() -> record.mCallback.onControllerUpdated(controller));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Sets the routing session's {@linkplain RoutingSessionInfo#getClientPackageName() client
|
||
|
* package name} to {@code packageName} if empty and returns the session.
|
||
|
*
|
||
|
* <p>This method must only be used for {@linkplain RoutingSessionInfo#isSystemSession()
|
||
|
* system routing sessions}.
|
||
|
*/
|
||
|
private static RoutingSessionInfo ensureClientPackageNameForSystemSession(
|
||
|
@NonNull RoutingSessionInfo sessionInfo, @NonNull String packageName) {
|
||
|
if (!sessionInfo.isSystemSession()
|
||
|
|| !TextUtils.isEmpty(sessionInfo.getClientPackageName())) {
|
||
|
return sessionInfo;
|
||
|
}
|
||
|
|
||
|
return new RoutingSessionInfo.Builder(sessionInfo)
|
||
|
.setClientPackageName(packageName)
|
||
|
.build();
|
||
|
}
|
||
|
|
||
|
/** Callback for receiving events about media route discovery. */
|
||
|
public abstract static class RouteCallback {
|
||
|
/**
|
||
|
* Called when routes are added. Whenever you register a callback, this will be invoked with
|
||
|
* known routes.
|
||
|
*
|
||
|
* @param routes the list of routes that have been added. It's never empty.
|
||
|
* @deprecated Use {@link #onRoutesUpdated(List)} instead.
|
||
|
*/
|
||
|
@Deprecated
|
||
|
public void onRoutesAdded(@NonNull List<MediaRoute2Info> routes) {}
|
||
|
|
||
|
/**
|
||
|
* Called when routes are removed.
|
||
|
*
|
||
|
* @param routes the list of routes that have been removed. It's never empty.
|
||
|
* @deprecated Use {@link #onRoutesUpdated(List)} instead.
|
||
|
*/
|
||
|
@Deprecated
|
||
|
public void onRoutesRemoved(@NonNull List<MediaRoute2Info> routes) {}
|
||
|
|
||
|
/**
|
||
|
* Called when the properties of one or more existing routes are changed. For example, it is
|
||
|
* called when a route's name or volume have changed.
|
||
|
*
|
||
|
* @param routes the list of routes that have been changed. It's never empty.
|
||
|
* @deprecated Use {@link #onRoutesUpdated(List)} instead.
|
||
|
*/
|
||
|
@Deprecated
|
||
|
public void onRoutesChanged(@NonNull List<MediaRoute2Info> routes) {}
|
||
|
|
||
|
/**
|
||
|
* Called when the route list is updated, which can happen when routes are added, removed,
|
||
|
* or modified. It will also be called when a route callback is registered.
|
||
|
*
|
||
|
* @param routes the updated list of routes filtered by the callback's individual discovery
|
||
|
* preferences.
|
||
|
*/
|
||
|
public void onRoutesUpdated(@NonNull List<MediaRoute2Info> routes) {}
|
||
|
|
||
|
/**
|
||
|
* Called when the client app's preferred features are changed. When this is called, it is
|
||
|
* recommended to {@link #getRoutes()} to get the routes that are currently available to the
|
||
|
* app.
|
||
|
*
|
||
|
* @param preferredFeatures the new preferred features set by the application
|
||
|
* @hide
|
||
|
*/
|
||
|
@SystemApi
|
||
|
public void onPreferredFeaturesChanged(@NonNull List<String> preferredFeatures) {}
|
||
|
}
|
||
|
|
||
|
/** Callback for receiving events on media transfer. */
|
||
|
public abstract static class TransferCallback {
|
||
|
/**
|
||
|
* Called when a media is transferred between two different routing controllers. This can
|
||
|
* happen by calling {@link #transferTo(MediaRoute2Info)}.
|
||
|
*
|
||
|
* <p>Override this to start playback with {@code newController}. You may want to get the
|
||
|
* status of the media that is being played with {@code oldController} and resume it
|
||
|
* continuously with {@code newController}. After this is called, any callbacks with {@code
|
||
|
* oldController} will not be invoked unless {@code oldController} is the {@link
|
||
|
* #getSystemController() system controller}. You need to {@link RoutingController#release()
|
||
|
* release} {@code oldController} before playing the media with {@code newController}.
|
||
|
*
|
||
|
* @param oldController the previous controller that controlled routing
|
||
|
* @param newController the new controller to control routing
|
||
|
* @see #transferTo(MediaRoute2Info)
|
||
|
*/
|
||
|
public void onTransfer(
|
||
|
@NonNull RoutingController oldController,
|
||
|
@NonNull RoutingController newController) {}
|
||
|
|
||
|
/**
|
||
|
* Called when {@link #transferTo(MediaRoute2Info)} failed.
|
||
|
*
|
||
|
* @param requestedRoute the route info which was used for the transfer
|
||
|
*/
|
||
|
public void onTransferFailure(@NonNull MediaRoute2Info requestedRoute) {}
|
||
|
|
||
|
/**
|
||
|
* Called when a media routing stops. It can be stopped by a user or a provider. App should
|
||
|
* not continue playing media locally when this method is called. The {@code controller} is
|
||
|
* released before this method is called.
|
||
|
*
|
||
|
* @param controller the controller that controlled the stopped media routing
|
||
|
*/
|
||
|
public void onStop(@NonNull RoutingController controller) {}
|
||
|
|
||
|
/**
|
||
|
* Called when a routing request fails.
|
||
|
*
|
||
|
* @param reason Reason for failure as per {@link
|
||
|
* android.media.MediaRoute2ProviderService.Reason}
|
||
|
* @hide
|
||
|
*/
|
||
|
public void onRequestFailed(int reason) {}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* A listener interface to send optional app-specific hints when creating a {@link
|
||
|
* RoutingController}.
|
||
|
*/
|
||
|
public interface OnGetControllerHintsListener {
|
||
|
/**
|
||
|
* Called when the {@link MediaRouter2} or the system is about to request a media route
|
||
|
* provider service to create a controller with the given route. The {@link Bundle} returned
|
||
|
* here will be sent to media route provider service as a hint.
|
||
|
*
|
||
|
* <p>Since controller creation can be requested by the {@link MediaRouter2} and the system,
|
||
|
* set the listener as soon as possible after acquiring {@link MediaRouter2} instance. The
|
||
|
* method will be called on the same thread that calls {@link #transferTo(MediaRoute2Info)}
|
||
|
* or the main thread if it is requested by the system.
|
||
|
*
|
||
|
* @param route the route to create a controller with
|
||
|
* @return An optional bundle of app-specific arguments to send to the provider, or {@code
|
||
|
* null} if none. The contents of this bundle may affect the result of controller
|
||
|
* creation.
|
||
|
* @see MediaRoute2ProviderService#onCreateSession(long, String, String, Bundle)
|
||
|
*/
|
||
|
@Nullable
|
||
|
Bundle onGetControllerHints(@NonNull MediaRoute2Info route);
|
||
|
}
|
||
|
|
||
|
/** Callback for receiving {@link RoutingController} updates. */
|
||
|
public abstract static class ControllerCallback {
|
||
|
/**
|
||
|
* Called when a controller is updated. (e.g., when the selected routes of the controller is
|
||
|
* changed or when the volume of the controller is changed.)
|
||
|
*
|
||
|
* @param controller the updated controller. It may be the {@link #getSystemController()
|
||
|
* system controller}.
|
||
|
* @see #getSystemController()
|
||
|
*/
|
||
|
public void onControllerUpdated(@NonNull RoutingController controller) {}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Represents an active scan request registered in the system.
|
||
|
*
|
||
|
* <p>See {@link #requestScan(ScanRequest)} for more information.
|
||
|
*/
|
||
|
@FlaggedApi(FLAG_ENABLE_SCREEN_OFF_SCANNING)
|
||
|
public static final class ScanToken {
|
||
|
private final int mId;
|
||
|
|
||
|
private ScanToken(int id) {
|
||
|
mId = id;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Represents a set of parameters for scanning requests.
|
||
|
*
|
||
|
* <p>See {@link #requestScan(ScanRequest)} for more details.
|
||
|
*/
|
||
|
@FlaggedApi(FLAG_ENABLE_SCREEN_OFF_SCANNING)
|
||
|
public static final class ScanRequest {
|
||
|
private final boolean mIsScreenOffScan;
|
||
|
|
||
|
private ScanRequest(boolean isScreenOffScan) {
|
||
|
mIsScreenOffScan = isScreenOffScan;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns whether the scan request corresponds to a screen-off scan.
|
||
|
*
|
||
|
* @see #requestScan(ScanRequest)
|
||
|
*/
|
||
|
public boolean isScreenOffScan() {
|
||
|
return mIsScreenOffScan;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Builder class for {@link ScanRequest}.
|
||
|
*
|
||
|
* @see #requestScan(ScanRequest)
|
||
|
*/
|
||
|
public static final class Builder {
|
||
|
boolean mIsScreenOffScan;
|
||
|
|
||
|
/**
|
||
|
* Creates a builder for a {@link ScanRequest} instance.
|
||
|
*
|
||
|
* @see #requestScan(ScanRequest)
|
||
|
*/
|
||
|
public Builder() {}
|
||
|
|
||
|
/**
|
||
|
* Sets whether the app is requesting to scan even while the screen is off, bypassing
|
||
|
* default scanning restrictions. Only companion apps holding {@link
|
||
|
* Manifest.permission#MEDIA_ROUTING_CONTROL} should set this to {@code true}.
|
||
|
*
|
||
|
* @see #requestScan(ScanRequest)
|
||
|
*/
|
||
|
@NonNull
|
||
|
public Builder setScreenOffScan(boolean isScreenOffScan) {
|
||
|
mIsScreenOffScan = isScreenOffScan;
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
/** Returns a new {@link ScanRequest} instance. */
|
||
|
@NonNull
|
||
|
public ScanRequest build() {
|
||
|
return new ScanRequest(mIsScreenOffScan);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* A class to control media routing session in media route provider. For example,
|
||
|
* selecting/deselecting/transferring to routes of a session can be done through this. Instances
|
||
|
* are created when {@link TransferCallback#onTransfer(RoutingController, RoutingController)} is
|
||
|
* called, which is invoked after {@link #transferTo(MediaRoute2Info)} is called.
|
||
|
*/
|
||
|
public class RoutingController {
|
||
|
private final Object mControllerLock = new Object();
|
||
|
|
||
|
private static final int CONTROLLER_STATE_UNKNOWN = 0;
|
||
|
private static final int CONTROLLER_STATE_ACTIVE = 1;
|
||
|
private static final int CONTROLLER_STATE_RELEASING = 2;
|
||
|
private static final int CONTROLLER_STATE_RELEASED = 3;
|
||
|
|
||
|
@GuardedBy("mControllerLock")
|
||
|
private RoutingSessionInfo mSessionInfo;
|
||
|
|
||
|
@GuardedBy("mControllerLock")
|
||
|
private int mState;
|
||
|
|
||
|
RoutingController(@NonNull RoutingSessionInfo sessionInfo) {
|
||
|
mSessionInfo = sessionInfo;
|
||
|
mState = CONTROLLER_STATE_ACTIVE;
|
||
|
}
|
||
|
|
||
|
RoutingController(@NonNull RoutingSessionInfo sessionInfo, int state) {
|
||
|
mSessionInfo = sessionInfo;
|
||
|
mState = state;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return the ID of the controller. It is globally unique.
|
||
|
*/
|
||
|
@NonNull
|
||
|
public String getId() {
|
||
|
synchronized (mControllerLock) {
|
||
|
return mSessionInfo.getId();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets the original session ID set by {@link RoutingSessionInfo.Builder#Builder(String,
|
||
|
* String)}.
|
||
|
*
|
||
|
* @hide
|
||
|
*/
|
||
|
@NonNull
|
||
|
@TestApi
|
||
|
public String getOriginalId() {
|
||
|
synchronized (mControllerLock) {
|
||
|
return mSessionInfo.getOriginalId();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets the control hints used to control routing session if available. It is set by the
|
||
|
* media route provider.
|
||
|
*/
|
||
|
@Nullable
|
||
|
public Bundle getControlHints() {
|
||
|
synchronized (mControllerLock) {
|
||
|
return mSessionInfo.getControlHints();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return the unmodifiable list of currently selected routes
|
||
|
*/
|
||
|
@NonNull
|
||
|
public List<MediaRoute2Info> getSelectedRoutes() {
|
||
|
List<String> selectedRouteIds;
|
||
|
synchronized (mControllerLock) {
|
||
|
selectedRouteIds = mSessionInfo.getSelectedRoutes();
|
||
|
}
|
||
|
return getRoutesWithIds(selectedRouteIds);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return the unmodifiable list of selectable routes for the session.
|
||
|
*/
|
||
|
@NonNull
|
||
|
public List<MediaRoute2Info> getSelectableRoutes() {
|
||
|
List<String> selectableRouteIds;
|
||
|
synchronized (mControllerLock) {
|
||
|
selectableRouteIds = mSessionInfo.getSelectableRoutes();
|
||
|
}
|
||
|
return getRoutesWithIds(selectableRouteIds);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return the unmodifiable list of deselectable routes for the session.
|
||
|
*/
|
||
|
@NonNull
|
||
|
public List<MediaRoute2Info> getDeselectableRoutes() {
|
||
|
List<String> deselectableRouteIds;
|
||
|
synchronized (mControllerLock) {
|
||
|
deselectableRouteIds = mSessionInfo.getDeselectableRoutes();
|
||
|
}
|
||
|
return getRoutesWithIds(deselectableRouteIds);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the unmodifiable list of transferable routes for the session.
|
||
|
*
|
||
|
* @see RoutingSessionInfo#getTransferableRoutes()
|
||
|
*/
|
||
|
@FlaggedApi(FLAG_ENABLE_GET_TRANSFERABLE_ROUTES)
|
||
|
@NonNull
|
||
|
public List<MediaRoute2Info> getTransferableRoutes() {
|
||
|
List<String> transferableRoutes;
|
||
|
synchronized (mControllerLock) {
|
||
|
transferableRoutes = mSessionInfo.getTransferableRoutes();
|
||
|
}
|
||
|
return getRoutesWithIds(transferableRoutes);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns whether the transfer was initiated by the calling app (as determined by comparing
|
||
|
* {@link UserHandle} and package name).
|
||
|
*/
|
||
|
@FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES)
|
||
|
public boolean wasTransferInitiatedBySelf() {
|
||
|
return mImpl.wasTransferredBySelf(getRoutingSessionInfo());
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the current {@link RoutingSessionInfo} associated to this controller.
|
||
|
*/
|
||
|
@NonNull
|
||
|
public RoutingSessionInfo getRoutingSessionInfo() {
|
||
|
synchronized (mControllerLock) {
|
||
|
return mSessionInfo;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets the information about how volume is handled on the session.
|
||
|
*
|
||
|
* <p>Please note that you may not control the volume of the session even when you can
|
||
|
* control the volume of each selected route in the session.
|
||
|
*
|
||
|
* @return {@link MediaRoute2Info#PLAYBACK_VOLUME_FIXED} or {@link
|
||
|
* MediaRoute2Info#PLAYBACK_VOLUME_VARIABLE}
|
||
|
*/
|
||
|
@MediaRoute2Info.PlaybackVolume
|
||
|
public int getVolumeHandling() {
|
||
|
synchronized (mControllerLock) {
|
||
|
return mSessionInfo.getVolumeHandling();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** Gets the maximum volume of the session. */
|
||
|
public int getVolumeMax() {
|
||
|
synchronized (mControllerLock) {
|
||
|
return mSessionInfo.getVolumeMax();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Gets the current volume of the session.
|
||
|
*
|
||
|
* <p>When it's available, it represents the volume of routing session, which is a group of
|
||
|
* selected routes. Use {@link MediaRoute2Info#getVolume()} to get the volume of a route,
|
||
|
*
|
||
|
* @see MediaRoute2Info#getVolume()
|
||
|
*/
|
||
|
public int getVolume() {
|
||
|
synchronized (mControllerLock) {
|
||
|
return mSessionInfo.getVolume();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns true if this controller is released, false otherwise. If it is released, then all
|
||
|
* other getters from this instance may return invalid values. Also, any operations to this
|
||
|
* instance will be ignored once released.
|
||
|
*
|
||
|
* @see #release
|
||
|
*/
|
||
|
public boolean isReleased() {
|
||
|
synchronized (mControllerLock) {
|
||
|
return mState == CONTROLLER_STATE_RELEASED;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Selects a route for the remote session. After a route is selected, the media is expected
|
||
|
* to be played to the all the selected routes. This is different from {@link
|
||
|
* MediaRouter2#transferTo(MediaRoute2Info) transferring to a route}, where the media is
|
||
|
* expected to 'move' from one route to another.
|
||
|
*
|
||
|
* <p>The given route must satisfy all of the following conditions:
|
||
|
*
|
||
|
* <ul>
|
||
|
* <li>It should not be included in {@link #getSelectedRoutes()}
|
||
|
* <li>It should be included in {@link #getSelectableRoutes()}
|
||
|
* </ul>
|
||
|
*
|
||
|
* If the route doesn't meet any of above conditions, it will be ignored.
|
||
|
*
|
||
|
* @see #deselectRoute(MediaRoute2Info)
|
||
|
* @see #getSelectedRoutes()
|
||
|
* @see #getSelectableRoutes()
|
||
|
* @see ControllerCallback#onControllerUpdated
|
||
|
*/
|
||
|
public void selectRoute(@NonNull MediaRoute2Info route) {
|
||
|
Objects.requireNonNull(route, "route must not be null");
|
||
|
if (isReleased()) {
|
||
|
Log.w(TAG, "selectRoute: Called on released controller. Ignoring.");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
List<MediaRoute2Info> selectedRoutes = getSelectedRoutes();
|
||
|
if (containsRouteInfoWithId(selectedRoutes, route.getId())) {
|
||
|
Log.w(TAG, "Ignoring selecting a route that is already selected. route=" + route);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
List<MediaRoute2Info> selectableRoutes = getSelectableRoutes();
|
||
|
if (!containsRouteInfoWithId(selectableRoutes, route.getId())) {
|
||
|
Log.w(TAG, "Ignoring selecting a non-selectable route=" + route);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
mImpl.selectRoute(route, getRoutingSessionInfo());
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Deselects a route from the remote session. After a route is deselected, the media is
|
||
|
* expected to be stopped on the deselected route.
|
||
|
*
|
||
|
* <p>The given route must satisfy all of the following conditions:
|
||
|
*
|
||
|
* <ul>
|
||
|
* <li>It should be included in {@link #getSelectedRoutes()}
|
||
|
* <li>It should be included in {@link #getDeselectableRoutes()}
|
||
|
* </ul>
|
||
|
*
|
||
|
* If the route doesn't meet any of above conditions, it will be ignored.
|
||
|
*
|
||
|
* @see #getSelectedRoutes()
|
||
|
* @see #getDeselectableRoutes()
|
||
|
* @see ControllerCallback#onControllerUpdated
|
||
|
*/
|
||
|
public void deselectRoute(@NonNull MediaRoute2Info route) {
|
||
|
Objects.requireNonNull(route, "route must not be null");
|
||
|
if (isReleased()) {
|
||
|
Log.w(TAG, "deselectRoute: called on released controller. Ignoring.");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
List<MediaRoute2Info> selectedRoutes = getSelectedRoutes();
|
||
|
if (!containsRouteInfoWithId(selectedRoutes, route.getId())) {
|
||
|
Log.w(TAG, "Ignoring deselecting a route that is not selected. route=" + route);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
List<MediaRoute2Info> deselectableRoutes = getDeselectableRoutes();
|
||
|
if (!containsRouteInfoWithId(deselectableRoutes, route.getId())) {
|
||
|
Log.w(TAG, "Ignoring deselecting a non-deselectable route=" + route);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
mImpl.deselectRoute(route, getRoutingSessionInfo());
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Attempts a transfer to a {@link RoutingSessionInfo#getTransferableRoutes() transferable
|
||
|
* route}.
|
||
|
*
|
||
|
* <p>Transferring to a transferable route does not require the app to transfer the playback
|
||
|
* state from one route to the other. The route provider completely manages the transfer. An
|
||
|
* example of provider-managed transfers are the switches between the system's routes, like
|
||
|
* the built-in speakers and a BT headset.
|
||
|
*
|
||
|
* @return True if the transfer is handled by this controller, or false if a new controller
|
||
|
* should be created instead.
|
||
|
* @see RoutingSessionInfo#getSelectedRoutes()
|
||
|
* @see RoutingSessionInfo#getTransferableRoutes()
|
||
|
* @see ControllerCallback#onControllerUpdated
|
||
|
*/
|
||
|
boolean tryTransferWithinProvider(@NonNull MediaRoute2Info route) {
|
||
|
Objects.requireNonNull(route, "route must not be null");
|
||
|
synchronized (mControllerLock) {
|
||
|
if (isReleased()) {
|
||
|
Log.w(
|
||
|
TAG,
|
||
|
"tryTransferWithinProvider: Called on released controller. Ignoring.");
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
// If this call is trying to transfer to a selected system route, we let them
|
||
|
// through as a provider driven transfer in order to update the transfer reason and
|
||
|
// initiator data.
|
||
|
boolean isSystemRouteReselection =
|
||
|
Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()
|
||
|
&& mSessionInfo.isSystemSession()
|
||
|
&& route.isSystemRoute()
|
||
|
&& mSessionInfo.getSelectedRoutes().contains(route.getId());
|
||
|
if (!isSystemRouteReselection
|
||
|
&& !mSessionInfo.getTransferableRoutes().contains(route.getId())) {
|
||
|
Log.i(
|
||
|
TAG,
|
||
|
"Transferring to a non-transferable route="
|
||
|
+ route
|
||
|
+ " session= "
|
||
|
+ mSessionInfo.getId());
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
MediaRouter2Stub stub;
|
||
|
synchronized (mLock) {
|
||
|
stub = mStub;
|
||
|
}
|
||
|
if (stub != null) {
|
||
|
try {
|
||
|
mMediaRouterService.transferToRouteWithRouter2(stub, getId(), route);
|
||
|
} catch (RemoteException ex) {
|
||
|
Log.e(TAG, "Unable to transfer to route for session.", ex);
|
||
|
}
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Requests a volume change for the remote session asynchronously.
|
||
|
*
|
||
|
* @param volume The new volume value between 0 and {@link RoutingController#getVolumeMax}
|
||
|
* (inclusive).
|
||
|
* @see #getVolume()
|
||
|
*/
|
||
|
public void setVolume(int volume) {
|
||
|
if (getVolumeHandling() == MediaRoute2Info.PLAYBACK_VOLUME_FIXED) {
|
||
|
Log.w(TAG, "setVolume: The routing session has fixed volume. Ignoring.");
|
||
|
return;
|
||
|
}
|
||
|
if (volume < 0 || volume > getVolumeMax()) {
|
||
|
Log.w(TAG, "setVolume: The target volume is out of range. Ignoring");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (isReleased()) {
|
||
|
Log.w(TAG, "setVolume: Called on released controller. Ignoring.");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
mImpl.setSessionVolume(volume, getRoutingSessionInfo());
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Releases this controller and the corresponding session. Any operations on this controller
|
||
|
* after calling this method will be ignored. The devices that are playing media will stop
|
||
|
* playing it.
|
||
|
*/
|
||
|
public void release() {
|
||
|
releaseInternal(/* shouldReleaseSession= */ true);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Schedules release of the controller.
|
||
|
*
|
||
|
* @return {@code true} if it's successfully scheduled, {@code false} if it's already
|
||
|
* scheduled to be released or released.
|
||
|
*/
|
||
|
boolean scheduleRelease() {
|
||
|
synchronized (mControllerLock) {
|
||
|
if (mState != CONTROLLER_STATE_ACTIVE) {
|
||
|
return false;
|
||
|
}
|
||
|
mState = CONTROLLER_STATE_RELEASING;
|
||
|
}
|
||
|
|
||
|
synchronized (mLock) {
|
||
|
// It could happen if the controller is released by the another thread
|
||
|
// in between two locks
|
||
|
if (!mNonSystemRoutingControllers.remove(getId(), this)) {
|
||
|
// In that case, onStop isn't called so we return true to call onTransfer.
|
||
|
// It's also consistent with that the another thread acquires the lock later.
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
mHandler.postDelayed(this::release, TRANSFER_TIMEOUT_MS);
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
void releaseInternal(boolean shouldReleaseSession) {
|
||
|
boolean shouldNotifyStop;
|
||
|
|
||
|
synchronized (mControllerLock) {
|
||
|
if (mState == CONTROLLER_STATE_RELEASED) {
|
||
|
if (DEBUG) {
|
||
|
Log.d(TAG, "releaseInternal: Called on released controller. Ignoring.");
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
shouldNotifyStop = (mState == CONTROLLER_STATE_ACTIVE);
|
||
|
mState = CONTROLLER_STATE_RELEASED;
|
||
|
}
|
||
|
|
||
|
mImpl.releaseSession(shouldReleaseSession, shouldNotifyStop, this);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public String toString() {
|
||
|
// To prevent logging spam, we only print the ID of each route.
|
||
|
List<String> selectedRoutes =
|
||
|
getSelectedRoutes().stream()
|
||
|
.map(MediaRoute2Info::getId)
|
||
|
.collect(Collectors.toList());
|
||
|
List<String> selectableRoutes =
|
||
|
getSelectableRoutes().stream()
|
||
|
.map(MediaRoute2Info::getId)
|
||
|
.collect(Collectors.toList());
|
||
|
List<String> deselectableRoutes =
|
||
|
getDeselectableRoutes().stream()
|
||
|
.map(MediaRoute2Info::getId)
|
||
|
.collect(Collectors.toList());
|
||
|
|
||
|
StringBuilder result =
|
||
|
new StringBuilder()
|
||
|
.append("RoutingController{ ")
|
||
|
.append("id=")
|
||
|
.append(getId())
|
||
|
.append(", selectedRoutes={")
|
||
|
.append(selectedRoutes)
|
||
|
.append("}")
|
||
|
.append(", selectableRoutes={")
|
||
|
.append(selectableRoutes)
|
||
|
.append("}")
|
||
|
.append(", deselectableRoutes={")
|
||
|
.append(deselectableRoutes)
|
||
|
.append("}")
|
||
|
.append(" }");
|
||
|
return result.toString();
|
||
|
}
|
||
|
|
||
|
void setRoutingSessionInfo(@NonNull RoutingSessionInfo info) {
|
||
|
synchronized (mControllerLock) {
|
||
|
mSessionInfo = info;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** Returns whether any route in {@code routeList} has a same unique ID with given route. */
|
||
|
private static boolean containsRouteInfoWithId(
|
||
|
@NonNull List<MediaRoute2Info> routeList, @NonNull String routeId) {
|
||
|
for (MediaRoute2Info info : routeList) {
|
||
|
if (TextUtils.equals(routeId, info.getId())) {
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class SystemRoutingController extends RoutingController {
|
||
|
SystemRoutingController(@NonNull RoutingSessionInfo sessionInfo) {
|
||
|
super(sessionInfo);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public boolean isReleased() {
|
||
|
// SystemRoutingController will never be released
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
boolean scheduleRelease() {
|
||
|
// SystemRoutingController can be always transferred
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
void releaseInternal(boolean shouldReleaseSession) {
|
||
|
// Do nothing. SystemRoutingController will never be released
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static final class RouteCallbackRecord {
|
||
|
public final Executor mExecutor;
|
||
|
public final RouteCallback mRouteCallback;
|
||
|
public final RouteDiscoveryPreference mPreference;
|
||
|
|
||
|
RouteCallbackRecord(
|
||
|
@Nullable Executor executor,
|
||
|
@NonNull RouteCallback routeCallback,
|
||
|
@Nullable RouteDiscoveryPreference preference) {
|
||
|
mRouteCallback = routeCallback;
|
||
|
mExecutor = executor;
|
||
|
mPreference = preference;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public boolean equals(Object obj) {
|
||
|
if (this == obj) {
|
||
|
return true;
|
||
|
}
|
||
|
if (!(obj instanceof RouteCallbackRecord)) {
|
||
|
return false;
|
||
|
}
|
||
|
return mRouteCallback == ((RouteCallbackRecord) obj).mRouteCallback;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int hashCode() {
|
||
|
return mRouteCallback.hashCode();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static final class RouteListingPreferenceCallbackRecord {
|
||
|
public final Executor mExecutor;
|
||
|
public final Consumer<RouteListingPreference> mRouteListingPreferenceCallback;
|
||
|
|
||
|
/* package */ RouteListingPreferenceCallbackRecord(
|
||
|
@NonNull Executor executor,
|
||
|
@NonNull Consumer<RouteListingPreference> routeListingPreferenceCallback) {
|
||
|
mExecutor = executor;
|
||
|
mRouteListingPreferenceCallback = routeListingPreferenceCallback;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public boolean equals(Object obj) {
|
||
|
if (this == obj) {
|
||
|
return true;
|
||
|
}
|
||
|
if (!(obj instanceof RouteListingPreferenceCallbackRecord)) {
|
||
|
return false;
|
||
|
}
|
||
|
return mRouteListingPreferenceCallback
|
||
|
== ((RouteListingPreferenceCallbackRecord) obj).mRouteListingPreferenceCallback;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int hashCode() {
|
||
|
return mRouteListingPreferenceCallback.hashCode();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static final class TransferCallbackRecord {
|
||
|
public final Executor mExecutor;
|
||
|
public final TransferCallback mTransferCallback;
|
||
|
|
||
|
TransferCallbackRecord(
|
||
|
@NonNull Executor executor, @NonNull TransferCallback transferCallback) {
|
||
|
mTransferCallback = transferCallback;
|
||
|
mExecutor = executor;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public boolean equals(Object obj) {
|
||
|
if (this == obj) {
|
||
|
return true;
|
||
|
}
|
||
|
if (!(obj instanceof TransferCallbackRecord)) {
|
||
|
return false;
|
||
|
}
|
||
|
return mTransferCallback == ((TransferCallbackRecord) obj).mTransferCallback;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int hashCode() {
|
||
|
return mTransferCallback.hashCode();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static final class ControllerCallbackRecord {
|
||
|
public final Executor mExecutor;
|
||
|
public final ControllerCallback mCallback;
|
||
|
|
||
|
ControllerCallbackRecord(
|
||
|
@Nullable Executor executor, @NonNull ControllerCallback callback) {
|
||
|
mCallback = callback;
|
||
|
mExecutor = executor;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public boolean equals(Object obj) {
|
||
|
if (this == obj) {
|
||
|
return true;
|
||
|
}
|
||
|
if (!(obj instanceof ControllerCallbackRecord)) {
|
||
|
return false;
|
||
|
}
|
||
|
return mCallback == ((ControllerCallbackRecord) obj).mCallback;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public int hashCode() {
|
||
|
return mCallback.hashCode();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static final class ControllerCreationRequest {
|
||
|
public final int mRequestId;
|
||
|
public final long mManagerRequestId;
|
||
|
public final MediaRoute2Info mRoute;
|
||
|
public final RoutingController mOldController;
|
||
|
|
||
|
ControllerCreationRequest(
|
||
|
int requestId,
|
||
|
long managerRequestId,
|
||
|
@NonNull MediaRoute2Info route,
|
||
|
@NonNull RoutingController oldController) {
|
||
|
mRequestId = requestId;
|
||
|
mManagerRequestId = managerRequestId;
|
||
|
mRoute = Objects.requireNonNull(route, "route must not be null");
|
||
|
mOldController =
|
||
|
Objects.requireNonNull(oldController, "oldController must not be null");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class MediaRouter2Stub extends IMediaRouter2.Stub {
|
||
|
@Override
|
||
|
public void notifyRouterRegistered(
|
||
|
List<MediaRoute2Info> currentRoutes, RoutingSessionInfo currentSystemSessionInfo) {
|
||
|
mHandler.sendMessage(
|
||
|
obtainMessage(
|
||
|
MediaRouter2::syncRoutesOnHandler,
|
||
|
MediaRouter2.this,
|
||
|
currentRoutes,
|
||
|
currentSystemSessionInfo));
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void notifyRoutesUpdated(List<MediaRoute2Info> routes) {
|
||
|
mHandler.sendMessage(
|
||
|
obtainMessage(MediaRouter2::updateRoutesOnHandler, MediaRouter2.this, routes));
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void notifySessionCreated(int requestId, @Nullable RoutingSessionInfo sessionInfo) {
|
||
|
mHandler.sendMessage(
|
||
|
obtainMessage(
|
||
|
MediaRouter2::createControllerOnHandler,
|
||
|
MediaRouter2.this,
|
||
|
requestId,
|
||
|
sessionInfo));
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void notifySessionInfoChanged(@Nullable RoutingSessionInfo sessionInfo) {
|
||
|
mHandler.sendMessage(
|
||
|
obtainMessage(
|
||
|
MediaRouter2::updateControllerOnHandler,
|
||
|
MediaRouter2.this,
|
||
|
sessionInfo));
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void notifySessionReleased(RoutingSessionInfo sessionInfo) {
|
||
|
mHandler.sendMessage(
|
||
|
obtainMessage(
|
||
|
MediaRouter2::releaseControllerOnHandler,
|
||
|
MediaRouter2.this,
|
||
|
sessionInfo));
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void requestCreateSessionByManager(
|
||
|
long managerRequestId,
|
||
|
RoutingSessionInfo oldSession,
|
||
|
MediaRoute2Info route,
|
||
|
UserHandle transferInitiatorUserHandle,
|
||
|
String transferInitiatorPackageName) {
|
||
|
mHandler.sendMessage(
|
||
|
obtainMessage(
|
||
|
MediaRouter2::onRequestCreateControllerByManagerOnHandler,
|
||
|
MediaRouter2.this,
|
||
|
oldSession,
|
||
|
route,
|
||
|
managerRequestId,
|
||
|
transferInitiatorUserHandle,
|
||
|
transferInitiatorPackageName));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Provides a common interface for separating {@link LocalMediaRouter2Impl local} and {@link
|
||
|
* ProxyMediaRouter2Impl proxy} {@link MediaRouter2} instances.
|
||
|
*/
|
||
|
private interface MediaRouter2Impl {
|
||
|
|
||
|
void updateScanningState(@ScanningState int scanningState) throws RemoteException;
|
||
|
|
||
|
void startScan();
|
||
|
|
||
|
void stopScan();
|
||
|
|
||
|
String getClientPackageName();
|
||
|
|
||
|
String getPackageName();
|
||
|
|
||
|
RoutingSessionInfo getSystemSessionInfo();
|
||
|
|
||
|
RouteCallbackRecord createRouteCallbackRecord(
|
||
|
@NonNull @CallbackExecutor Executor executor,
|
||
|
@NonNull RouteCallback routeCallback,
|
||
|
@NonNull RouteDiscoveryPreference preference);
|
||
|
|
||
|
void registerRouteCallback();
|
||
|
|
||
|
void unregisterRouteCallback();
|
||
|
|
||
|
void setRouteListingPreference(@Nullable RouteListingPreference preference);
|
||
|
|
||
|
boolean showSystemOutputSwitcher();
|
||
|
|
||
|
List<MediaRoute2Info> getAllRoutes();
|
||
|
|
||
|
void setOnGetControllerHintsListener(OnGetControllerHintsListener listener);
|
||
|
|
||
|
void transferTo(MediaRoute2Info route);
|
||
|
|
||
|
void stop();
|
||
|
|
||
|
void transfer(@NonNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route);
|
||
|
|
||
|
List<RoutingController> getControllers();
|
||
|
|
||
|
void setRouteVolume(MediaRoute2Info route, int volume);
|
||
|
|
||
|
List<MediaRoute2Info> filterRoutesWithIndividualPreference(
|
||
|
List<MediaRoute2Info> routes, RouteDiscoveryPreference discoveryPreference);
|
||
|
|
||
|
// RoutingController methods.
|
||
|
void setSessionVolume(int volume, RoutingSessionInfo sessionInfo);
|
||
|
|
||
|
void selectRoute(MediaRoute2Info route, RoutingSessionInfo sessionInfo);
|
||
|
|
||
|
void deselectRoute(MediaRoute2Info route, RoutingSessionInfo sessionInfo);
|
||
|
|
||
|
void releaseSession(
|
||
|
boolean shouldReleaseSession,
|
||
|
boolean shouldNotifyStop,
|
||
|
RoutingController controller);
|
||
|
|
||
|
/**
|
||
|
* Returns the value of {@link RoutingController#wasTransferInitiatedBySelf()} for the app
|
||
|
* associated with this router.
|
||
|
*/
|
||
|
boolean wasTransferredBySelf(RoutingSessionInfo sessionInfo);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Implements logic specific to proxy {@link MediaRouter2} instances.
|
||
|
*
|
||
|
* <p>A proxy {@link MediaRouter2} instance controls the routing of a different package and can
|
||
|
* be obtained by calling {@link #getInstance(Context, String)}. This requires {@link
|
||
|
* Manifest.permission#MEDIA_CONTENT_CONTROL MEDIA_CONTENT_CONTROL} permission.
|
||
|
*
|
||
|
* <p>Proxy routers behave differently than local routers. See {@link #getInstance(Context,
|
||
|
* String)} for more details.
|
||
|
*/
|
||
|
private class ProxyMediaRouter2Impl implements MediaRouter2Impl {
|
||
|
// Fields originating from MediaRouter2Manager.
|
||
|
private final IMediaRouter2Manager.Stub mClient;
|
||
|
private final CopyOnWriteArrayList<MediaRouter2Manager.TransferRequest>
|
||
|
mTransferRequests = new CopyOnWriteArrayList<>();
|
||
|
private final AtomicInteger mScanRequestCount = new AtomicInteger(/* initialValue= */ 0);
|
||
|
|
||
|
// Fields originating from MediaRouter2.
|
||
|
@NonNull private final String mClientPackageName;
|
||
|
@NonNull private final UserHandle mClientUser;
|
||
|
private final AtomicBoolean mIsScanning = new AtomicBoolean(/* initialValue= */ false);
|
||
|
|
||
|
@GuardedBy("mLock")
|
||
|
private final List<InstanceInvalidatedCallbackRecord> mInstanceInvalidatedCallbackRecords =
|
||
|
new ArrayList<>();
|
||
|
|
||
|
ProxyMediaRouter2Impl(
|
||
|
@NonNull Context context,
|
||
|
@NonNull String clientPackageName,
|
||
|
@NonNull UserHandle user) {
|
||
|
mClientUser = user;
|
||
|
mClientPackageName = clientPackageName;
|
||
|
mClient = new Client();
|
||
|
mDiscoveryPreference = RouteDiscoveryPreference.EMPTY;
|
||
|
}
|
||
|
|
||
|
public void registerProxyRouter() {
|
||
|
try {
|
||
|
mMediaRouterService.registerProxyRouter(
|
||
|
mClient,
|
||
|
mContext.getApplicationContext().getPackageName(),
|
||
|
mClientPackageName,
|
||
|
mClientUser);
|
||
|
} catch (RemoteException ex) {
|
||
|
throw ex.rethrowFromSystemServer();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public void registerInstanceInvalidatedCallback(
|
||
|
@Nullable Executor executor, @Nullable Runnable onInstanceInvalidatedListener) {
|
||
|
if (executor == null || onInstanceInvalidatedListener == null) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
InstanceInvalidatedCallbackRecord record =
|
||
|
new InstanceInvalidatedCallbackRecord(executor, onInstanceInvalidatedListener);
|
||
|
synchronized (mLock) {
|
||
|
if (!mInstanceInvalidatedCallbackRecords.contains(record)) {
|
||
|
mInstanceInvalidatedCallbackRecords.add(record);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void updateScanningState(int scanningState) throws RemoteException {
|
||
|
mMediaRouterService.updateScanningState(mClient, scanningState);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void startScan() {
|
||
|
if (!mIsScanning.getAndSet(true)) {
|
||
|
if (mScanRequestCount.getAndIncrement() == 0) {
|
||
|
try {
|
||
|
mMediaRouterService.updateScanningState(
|
||
|
mClient, SCANNING_STATE_WHILE_INTERACTIVE);
|
||
|
} catch (RemoteException ex) {
|
||
|
throw ex.rethrowFromSystemServer();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void stopScan() {
|
||
|
if (mIsScanning.getAndSet(false)) {
|
||
|
if (mScanRequestCount.updateAndGet(
|
||
|
count -> {
|
||
|
if (count == 0) {
|
||
|
throw new IllegalStateException(
|
||
|
"No active scan requests to unregister.");
|
||
|
} else {
|
||
|
return --count;
|
||
|
}
|
||
|
})
|
||
|
== 0) {
|
||
|
try {
|
||
|
mMediaRouterService.updateScanningState(
|
||
|
mClient, SCANNING_STATE_NOT_SCANNING);
|
||
|
} catch (RemoteException ex) {
|
||
|
throw ex.rethrowFromSystemServer();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public String getClientPackageName() {
|
||
|
return mClientPackageName;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns {@code null}. This refers to the package name of the caller app, which is only
|
||
|
* relevant for local routers.
|
||
|
*/
|
||
|
@Override
|
||
|
public String getPackageName() {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public RoutingSessionInfo getSystemSessionInfo() {
|
||
|
return getSystemSessionInfoImpl(
|
||
|
mMediaRouterService, mContext.getPackageName(), mClientPackageName);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* {@link RouteDiscoveryPreference Discovery preferences} are ignored for proxy routers, as
|
||
|
* their callbacks should receive events related to the media app's preferences. This is
|
||
|
* equivalent to setting {@link RouteDiscoveryPreference#EMPTY empty preferences}.
|
||
|
*/
|
||
|
@Override
|
||
|
public RouteCallbackRecord createRouteCallbackRecord(
|
||
|
Executor executor,
|
||
|
RouteCallback routeCallback,
|
||
|
RouteDiscoveryPreference preference) {
|
||
|
return new RouteCallbackRecord(executor, routeCallback, RouteDiscoveryPreference.EMPTY);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* No-op. Only local routers communicate directly with {@link
|
||
|
* com.android.server.media.MediaRouter2ServiceImpl MediaRouter2ServiceImpl} and modify
|
||
|
* {@link RouteDiscoveryPreference}. Proxy routers receive callbacks from {@link
|
||
|
* MediaRouter2Manager}.
|
||
|
*/
|
||
|
@Override
|
||
|
public void registerRouteCallback() {
|
||
|
// Do nothing.
|
||
|
}
|
||
|
|
||
|
/** No-op. See {@link ProxyMediaRouter2Impl#registerRouteCallback()}. */
|
||
|
@Override
|
||
|
public void unregisterRouteCallback() {
|
||
|
// Do nothing.
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void setRouteListingPreference(@Nullable RouteListingPreference preference) {
|
||
|
throw new UnsupportedOperationException(
|
||
|
"RouteListingPreference cannot be set by a privileged MediaRouter2 instance.");
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public boolean showSystemOutputSwitcher() {
|
||
|
throw new UnsupportedOperationException(
|
||
|
"Cannot show system output switcher from a privileged router.");
|
||
|
}
|
||
|
|
||
|
/** Gets the list of all discovered routes. */
|
||
|
@Override
|
||
|
public List<MediaRoute2Info> getAllRoutes() {
|
||
|
synchronized (mLock) {
|
||
|
return new ArrayList<>(mRoutes.values());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** No-op. Controller hints can only be provided by the media app through a local router. */
|
||
|
@Override
|
||
|
public void setOnGetControllerHintsListener(OnGetControllerHintsListener listener) {
|
||
|
// Do nothing.
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Transfers the current {@link RoutingSessionInfo routing session} associated with the
|
||
|
* router's {@link #mClientPackageName client package name} to a specified {@link
|
||
|
* MediaRoute2Info route}.
|
||
|
*
|
||
|
* <p>This method is equivalent to {@link #transfer(RoutingSessionInfo, MediaRoute2Info)},
|
||
|
* except that the {@link RoutingSessionInfo routing session} is resolved based on the
|
||
|
* router's {@link #mClientPackageName client package name}.
|
||
|
*
|
||
|
* @param route The route to transfer to.
|
||
|
*/
|
||
|
@Override
|
||
|
public void transferTo(MediaRoute2Info route) {
|
||
|
Objects.requireNonNull(route, "route must not be null");
|
||
|
|
||
|
List<RoutingSessionInfo> sessionInfos = getRoutingSessions();
|
||
|
RoutingSessionInfo targetSession = sessionInfos.get(sessionInfos.size() - 1);
|
||
|
transfer(targetSession, route);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void stop() {
|
||
|
List<RoutingSessionInfo> sessionInfos = getRoutingSessions();
|
||
|
RoutingSessionInfo sessionToRelease = sessionInfos.get(sessionInfos.size() - 1);
|
||
|
releaseSession(sessionToRelease);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Transfers a {@link RoutingSessionInfo routing session} to a {@link MediaRoute2Info
|
||
|
* route}.
|
||
|
*
|
||
|
* <p>{@link #onTransferred} is called on success or {@link #onTransferFailed} is called if
|
||
|
* the request fails.
|
||
|
*
|
||
|
* <p>This method will default for in-session transfer if the {@link MediaRoute2Info route}
|
||
|
* is a {@link RoutingSessionInfo#getTransferableRoutes() transferable route}. Otherwise, it
|
||
|
* will attempt an out-of-session transfer.
|
||
|
*
|
||
|
* @param sessionInfo The {@link RoutingSessionInfo routing session} to transfer.
|
||
|
* @param route The {@link MediaRoute2Info route} to transfer to.
|
||
|
* @see #transferToRoute(RoutingSessionInfo, MediaRoute2Info, UserHandle, String)
|
||
|
* @see #requestCreateSession(RoutingSessionInfo, MediaRoute2Info)
|
||
|
*/
|
||
|
@Override
|
||
|
@SuppressWarnings("AndroidFrameworkRequiresPermission")
|
||
|
public void transfer(
|
||
|
@NonNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route) {
|
||
|
Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
|
||
|
Objects.requireNonNull(route, "route must not be null");
|
||
|
|
||
|
Log.v(
|
||
|
TAG,
|
||
|
"Transferring routing session. session= " + sessionInfo + ", route=" + route);
|
||
|
|
||
|
boolean isUnknownRoute;
|
||
|
synchronized (mLock) {
|
||
|
isUnknownRoute = !mRoutes.containsKey(route.getId());
|
||
|
}
|
||
|
|
||
|
if (isUnknownRoute) {
|
||
|
Log.w(TAG, "transfer: Ignoring an unknown route id=" + route.getId());
|
||
|
this.onTransferFailed(sessionInfo, route);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// If this call is trying to transfer to a selected system route, we let them
|
||
|
// through as a provider driven transfer in order to update the transfer reason and
|
||
|
// initiator data.
|
||
|
boolean isSystemRouteReselection =
|
||
|
Flags.enableBuiltInSpeakerRouteSuitabilityStatuses()
|
||
|
&& sessionInfo.isSystemSession()
|
||
|
&& route.isSystemRoute()
|
||
|
&& sessionInfo.getSelectedRoutes().contains(route.getId());
|
||
|
if (sessionInfo.getTransferableRoutes().contains(route.getId())
|
||
|
|| isSystemRouteReselection) {
|
||
|
transferToRoute(sessionInfo, route, mClientUser, mClientPackageName);
|
||
|
} else {
|
||
|
requestCreateSession(sessionInfo, route, mClientUser, mClientPackageName);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Requests an in-session transfer of a {@link RoutingSessionInfo routing session} to a
|
||
|
* {@link MediaRoute2Info route}.
|
||
|
*
|
||
|
* <p>The provided {@link MediaRoute2Info route} must be listed in the {@link
|
||
|
* RoutingSessionInfo routing session's} {@link RoutingSessionInfo#getTransferableRoutes()
|
||
|
* transferable routes list}. Otherwise, the request will fail.
|
||
|
*
|
||
|
* <p>Use {@link #requestCreateSession(RoutingSessionInfo, MediaRoute2Info)} to request an
|
||
|
* out-of-session transfer.
|
||
|
*
|
||
|
* @param session The {@link RoutingSessionInfo routing session} to transfer.
|
||
|
* @param route The {@link MediaRoute2Info route} to transfer to. Must be one of the {@link
|
||
|
* RoutingSessionInfo routing session's} {@link
|
||
|
* RoutingSessionInfo#getTransferableRoutes() transferable routes}.
|
||
|
*/
|
||
|
@RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
|
||
|
private void transferToRoute(
|
||
|
@NonNull RoutingSessionInfo session,
|
||
|
@NonNull MediaRoute2Info route,
|
||
|
@NonNull UserHandle transferInitiatorUserHandle,
|
||
|
@NonNull String transferInitiatorPackageName) {
|
||
|
int requestId = createTransferRequest(session, route);
|
||
|
|
||
|
try {
|
||
|
mMediaRouterService.transferToRouteWithManager(
|
||
|
mClient,
|
||
|
requestId,
|
||
|
session.getId(),
|
||
|
route,
|
||
|
transferInitiatorUserHandle,
|
||
|
transferInitiatorPackageName);
|
||
|
} catch (RemoteException ex) {
|
||
|
throw ex.rethrowFromSystemServer();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Requests an out-of-session transfer of a {@link RoutingSessionInfo routing session} to a
|
||
|
* {@link MediaRoute2Info route}.
|
||
|
*
|
||
|
* <p>This request creates a new {@link RoutingSessionInfo routing session} regardless of
|
||
|
* whether the {@link MediaRoute2Info route} is one of the {@link RoutingSessionInfo current
|
||
|
* session's} {@link RoutingSessionInfo#getTransferableRoutes() transferable routes}.
|
||
|
*
|
||
|
* <p>Use {@link #transferToRoute(RoutingSessionInfo, MediaRoute2Info)} to request an
|
||
|
* in-session transfer.
|
||
|
*
|
||
|
* @param oldSession The {@link RoutingSessionInfo routing session} to transfer.
|
||
|
* @param route The {@link MediaRoute2Info route} to transfer to.
|
||
|
*/
|
||
|
private void requestCreateSession(
|
||
|
@NonNull RoutingSessionInfo oldSession,
|
||
|
@NonNull MediaRoute2Info route,
|
||
|
@NonNull UserHandle transferInitiatorUserHandle,
|
||
|
@NonNull String transferInitiatorPackageName) {
|
||
|
if (TextUtils.isEmpty(oldSession.getClientPackageName())) {
|
||
|
Log.w(TAG, "requestCreateSession: Can't create a session without package name.");
|
||
|
this.onTransferFailed(oldSession, route);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
int requestId = createTransferRequest(oldSession, route);
|
||
|
|
||
|
try {
|
||
|
mMediaRouterService.requestCreateSessionWithManager(
|
||
|
mClient,
|
||
|
requestId,
|
||
|
oldSession,
|
||
|
route,
|
||
|
transferInitiatorUserHandle,
|
||
|
transferInitiatorPackageName);
|
||
|
} catch (RemoteException ex) {
|
||
|
throw ex.rethrowFromSystemServer();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public List<RoutingController> getControllers() {
|
||
|
List<RoutingController> result = new ArrayList<>();
|
||
|
|
||
|
/* Unlike local MediaRouter2 instances, controller instances cannot be kept because
|
||
|
transfer events initiated from other apps will not come through manager.*/
|
||
|
List<RoutingSessionInfo> sessions = getRoutingSessions();
|
||
|
for (RoutingSessionInfo session : sessions) {
|
||
|
RoutingController controller;
|
||
|
if (session.isSystemSession()) {
|
||
|
mSystemController.setRoutingSessionInfo(session);
|
||
|
controller = mSystemController;
|
||
|
} else {
|
||
|
controller = new RoutingController(session);
|
||
|
}
|
||
|
result.add(controller);
|
||
|
}
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Requests a volume change for a {@link MediaRoute2Info route}.
|
||
|
*
|
||
|
* <p>It may have no effect if the {@link MediaRoute2Info route} is not currently selected.
|
||
|
*
|
||
|
* @param volume The desired volume value between 0 and {@link
|
||
|
* MediaRoute2Info#getVolumeMax()} (inclusive).
|
||
|
*/
|
||
|
@Override
|
||
|
public void setRouteVolume(@NonNull MediaRoute2Info route, int volume) {
|
||
|
if (route.getVolumeHandling() == MediaRoute2Info.PLAYBACK_VOLUME_FIXED) {
|
||
|
Log.w(TAG, "setRouteVolume: the route has fixed volume. Ignoring.");
|
||
|
return;
|
||
|
}
|
||
|
if (volume < 0 || volume > route.getVolumeMax()) {
|
||
|
Log.w(TAG, "setRouteVolume: the target volume is out of range. Ignoring");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
int requestId = mNextRequestId.getAndIncrement();
|
||
|
mMediaRouterService.setRouteVolumeWithManager(mClient, requestId, route, volume);
|
||
|
} catch (RemoteException ex) {
|
||
|
throw ex.rethrowFromSystemServer();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Requests a volume change for a {@link RoutingSessionInfo routing session}.
|
||
|
*
|
||
|
* @param volume The desired volume value between 0 and {@link
|
||
|
* RoutingSessionInfo#getVolumeMax()} (inclusive).
|
||
|
*/
|
||
|
@Override
|
||
|
public void setSessionVolume(int volume, RoutingSessionInfo sessionInfo) {
|
||
|
Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
|
||
|
|
||
|
if (sessionInfo.getVolumeHandling() == MediaRoute2Info.PLAYBACK_VOLUME_FIXED) {
|
||
|
Log.w(TAG, "setSessionVolume: the route has fixed volume. Ignoring.");
|
||
|
return;
|
||
|
}
|
||
|
if (volume < 0 || volume > sessionInfo.getVolumeMax()) {
|
||
|
Log.w(TAG, "setSessionVolume: the target volume is out of range. Ignoring");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
int requestId = mNextRequestId.getAndIncrement();
|
||
|
mMediaRouterService.setSessionVolumeWithManager(
|
||
|
mClient, requestId, sessionInfo.getId(), volume);
|
||
|
} catch (RemoteException ex) {
|
||
|
throw ex.rethrowFromSystemServer();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns an exact copy of the routes. Individual {@link RouteDiscoveryPreference
|
||
|
* preferences} do not apply to proxy routers.
|
||
|
*/
|
||
|
@Override
|
||
|
public List<MediaRoute2Info> filterRoutesWithIndividualPreference(
|
||
|
List<MediaRoute2Info> routes, RouteDiscoveryPreference discoveryPreference) {
|
||
|
// Individual discovery preferences do not apply for the system router.
|
||
|
return new ArrayList<>(routes);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Adds a {@linkplain MediaRoute2Info route} to the routing session's {@linkplain
|
||
|
* RoutingSessionInfo#getSelectedRoutes() selected route list}.
|
||
|
*
|
||
|
* <p>Upon success, {@link #onSessionUpdated(RoutingSessionInfo)} is invoked. Failed
|
||
|
* requests are silently ignored.
|
||
|
*
|
||
|
* <p>The {@linkplain RoutingSessionInfo#getSelectedRoutes() selected routes list} of a
|
||
|
* routing session contains the group of devices playing media for that {@linkplain
|
||
|
* RoutingSessionInfo session}.
|
||
|
*
|
||
|
* <p>The given route must not be already selected and must be listed in the session's
|
||
|
* {@linkplain RoutingSessionInfo#getSelectableRoutes() selectable routes}. Otherwise, the
|
||
|
* request will be ignored.
|
||
|
*
|
||
|
* <p>This method should not be confused with {@link #transfer(RoutingSessionInfo,
|
||
|
* MediaRoute2Info)}.
|
||
|
*
|
||
|
* @see RoutingSessionInfo#getSelectedRoutes()
|
||
|
* @see RoutingSessionInfo#getSelectableRoutes()
|
||
|
*/
|
||
|
@Override
|
||
|
public void selectRoute(MediaRoute2Info route, RoutingSessionInfo sessionInfo) {
|
||
|
Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
|
||
|
Objects.requireNonNull(route, "route must not be null");
|
||
|
|
||
|
if (sessionInfo.getSelectedRoutes().contains(route.getId())) {
|
||
|
Log.w(TAG, "Ignoring selecting a route that is already selected. route=" + route);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (!sessionInfo.getSelectableRoutes().contains(route.getId())) {
|
||
|
Log.w(TAG, "Ignoring selecting a non-selectable route=" + route);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
int requestId = mNextRequestId.getAndIncrement();
|
||
|
mMediaRouterService.selectRouteWithManager(
|
||
|
mClient, requestId, sessionInfo.getId(), route);
|
||
|
} catch (RemoteException ex) {
|
||
|
throw ex.rethrowFromSystemServer();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Removes a route from a session's {@linkplain RoutingSessionInfo#getSelectedRoutes()
|
||
|
* selected routes list}. Calls {@link #onSessionUpdated(RoutingSessionInfo)} on success.
|
||
|
*
|
||
|
* <p>The given route must be selected and must be listed in the session's {@linkplain
|
||
|
* RoutingSessionInfo#getDeselectableRoutes() deselectable route list}. Otherwise, the
|
||
|
* request will be ignored.
|
||
|
*
|
||
|
* @see RoutingSessionInfo#getSelectedRoutes()
|
||
|
* @see RoutingSessionInfo#getDeselectableRoutes()
|
||
|
*/
|
||
|
@Override
|
||
|
public void deselectRoute(MediaRoute2Info route, RoutingSessionInfo sessionInfo) {
|
||
|
Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
|
||
|
Objects.requireNonNull(route, "route must not be null");
|
||
|
|
||
|
if (!sessionInfo.getSelectedRoutes().contains(route.getId())) {
|
||
|
Log.w(TAG, "Ignoring deselecting a route that is not selected. route=" + route);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (!sessionInfo.getDeselectableRoutes().contains(route.getId())) {
|
||
|
Log.w(TAG, "Ignoring deselecting a non-deselectable route=" + route);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
int requestId = mNextRequestId.getAndIncrement();
|
||
|
mMediaRouterService.deselectRouteWithManager(
|
||
|
mClient, requestId, sessionInfo.getId(), route);
|
||
|
} catch (RemoteException ex) {
|
||
|
throw ex.rethrowFromSystemServer();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void releaseSession(
|
||
|
boolean shouldReleaseSession,
|
||
|
boolean shouldNotifyStop,
|
||
|
RoutingController controller) {
|
||
|
releaseSession(controller.getRoutingSessionInfo());
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public boolean wasTransferredBySelf(RoutingSessionInfo sessionInfo) {
|
||
|
UserHandle transferInitiatorUserHandle = sessionInfo.getTransferInitiatorUserHandle();
|
||
|
String transferInitiatorPackageName = sessionInfo.getTransferInitiatorPackageName();
|
||
|
return Objects.equals(mClientUser, transferInitiatorUserHandle)
|
||
|
&& Objects.equals(mClientPackageName, transferInitiatorPackageName);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Retrieves the system session info for the given package.
|
||
|
*
|
||
|
* <p>The returned routing session is guaranteed to have a non-null {@link
|
||
|
* RoutingSessionInfo#getClientPackageName() client package name}.
|
||
|
*
|
||
|
* <p>Extracted into a static method to allow calling this from the constructor.
|
||
|
*/
|
||
|
/* package */ static RoutingSessionInfo getSystemSessionInfoImpl(
|
||
|
@NonNull IMediaRouterService service,
|
||
|
@NonNull String callerPackageName,
|
||
|
@NonNull String clientPackageName) {
|
||
|
try {
|
||
|
return service.getSystemSessionInfoForPackage(callerPackageName, clientPackageName);
|
||
|
} catch (RemoteException ex) {
|
||
|
throw ex.rethrowFromSystemServer();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Requests the release of a {@linkplain RoutingSessionInfo routing session}. Calls {@link
|
||
|
* #onSessionReleasedOnHandler(RoutingSessionInfo)} on success.
|
||
|
*
|
||
|
* <p>Once released, a routing session ignores incoming requests.
|
||
|
*/
|
||
|
private void releaseSession(@NonNull RoutingSessionInfo sessionInfo) {
|
||
|
Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
|
||
|
|
||
|
try {
|
||
|
int requestId = mNextRequestId.getAndIncrement();
|
||
|
mMediaRouterService.releaseSessionWithManager(
|
||
|
mClient, requestId, sessionInfo.getId());
|
||
|
} catch (RemoteException ex) {
|
||
|
throw ex.rethrowFromSystemServer();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private int createTransferRequest(
|
||
|
@NonNull RoutingSessionInfo session, @NonNull MediaRoute2Info route) {
|
||
|
int requestId = mNextRequestId.getAndIncrement();
|
||
|
MediaRouter2Manager.TransferRequest transferRequest =
|
||
|
new MediaRouter2Manager.TransferRequest(requestId, session, route);
|
||
|
mTransferRequests.add(transferRequest);
|
||
|
|
||
|
Message timeoutMessage =
|
||
|
obtainMessage(
|
||
|
ProxyMediaRouter2Impl::handleTransferTimeout, this, transferRequest);
|
||
|
mHandler.sendMessageDelayed(timeoutMessage, TRANSFER_TIMEOUT_MS);
|
||
|
return requestId;
|
||
|
}
|
||
|
|
||
|
private void handleTransferTimeout(MediaRouter2Manager.TransferRequest request) {
|
||
|
boolean removed = mTransferRequests.remove(request);
|
||
|
if (removed) {
|
||
|
this.onTransferFailed(request.mOldSessionInfo, request.mTargetRoute);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns the {@linkplain RoutingSessionInfo routing sessions} associated with {@link
|
||
|
* #mClientPackageName}. The first element of the returned list is the {@linkplain
|
||
|
* #getSystemSessionInfo() system routing session}.
|
||
|
*
|
||
|
* @see #getSystemSessionInfo()
|
||
|
*/
|
||
|
@NonNull
|
||
|
private List<RoutingSessionInfo> getRoutingSessions() {
|
||
|
List<RoutingSessionInfo> sessions = new ArrayList<>();
|
||
|
sessions.add(getSystemSessionInfo());
|
||
|
|
||
|
List<RoutingSessionInfo> remoteSessions;
|
||
|
try {
|
||
|
remoteSessions = mMediaRouterService.getRemoteSessions(mClient);
|
||
|
} catch (RemoteException ex) {
|
||
|
throw ex.rethrowFromSystemServer();
|
||
|
}
|
||
|
|
||
|
for (RoutingSessionInfo sessionInfo : remoteSessions) {
|
||
|
if (TextUtils.equals(sessionInfo.getClientPackageName(), mClientPackageName)) {
|
||
|
sessions.add(sessionInfo);
|
||
|
}
|
||
|
}
|
||
|
return sessions;
|
||
|
}
|
||
|
|
||
|
private void onTransferred(
|
||
|
@NonNull RoutingSessionInfo oldSession, @NonNull RoutingSessionInfo newSession) {
|
||
|
if (!isSessionRelatedToTargetPackageName(oldSession)
|
||
|
|| !isSessionRelatedToTargetPackageName(newSession)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
RoutingController oldController;
|
||
|
if (oldSession.isSystemSession()) {
|
||
|
mSystemController.setRoutingSessionInfo(
|
||
|
ensureClientPackageNameForSystemSession(oldSession, mClientPackageName));
|
||
|
oldController = mSystemController;
|
||
|
} else {
|
||
|
oldController = new RoutingController(oldSession);
|
||
|
}
|
||
|
|
||
|
RoutingController newController;
|
||
|
if (newSession.isSystemSession()) {
|
||
|
mSystemController.setRoutingSessionInfo(
|
||
|
ensureClientPackageNameForSystemSession(newSession, mClientPackageName));
|
||
|
newController = mSystemController;
|
||
|
} else {
|
||
|
newController = new RoutingController(newSession);
|
||
|
}
|
||
|
|
||
|
notifyTransfer(oldController, newController);
|
||
|
}
|
||
|
|
||
|
private void onTransferFailed(
|
||
|
@NonNull RoutingSessionInfo session, @NonNull MediaRoute2Info route) {
|
||
|
if (!isSessionRelatedToTargetPackageName(session)) {
|
||
|
return;
|
||
|
}
|
||
|
notifyTransferFailure(route);
|
||
|
}
|
||
|
|
||
|
private void onSessionUpdated(@NonNull RoutingSessionInfo session) {
|
||
|
if (!isSessionRelatedToTargetPackageName(session)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
RoutingController controller;
|
||
|
if (session.isSystemSession()) {
|
||
|
mSystemController.setRoutingSessionInfo(
|
||
|
ensureClientPackageNameForSystemSession(session, mClientPackageName));
|
||
|
controller = mSystemController;
|
||
|
} else {
|
||
|
controller = new RoutingController(session);
|
||
|
}
|
||
|
notifyControllerUpdated(controller);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns {@code true} if the session is a system session or if its client package name
|
||
|
* matches the proxy router's target package name.
|
||
|
*/
|
||
|
private boolean isSessionRelatedToTargetPackageName(@NonNull RoutingSessionInfo session) {
|
||
|
return session.isSystemSession()
|
||
|
|| TextUtils.equals(getClientPackageName(), session.getClientPackageName());
|
||
|
}
|
||
|
|
||
|
private void onSessionCreatedOnHandler(
|
||
|
int requestId, @NonNull RoutingSessionInfo sessionInfo) {
|
||
|
MediaRouter2Manager.TransferRequest matchingRequest = null;
|
||
|
for (MediaRouter2Manager.TransferRequest request : mTransferRequests) {
|
||
|
if (request.mRequestId == requestId) {
|
||
|
matchingRequest = request;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (matchingRequest == null) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
mTransferRequests.remove(matchingRequest);
|
||
|
|
||
|
MediaRoute2Info requestedRoute = matchingRequest.mTargetRoute;
|
||
|
|
||
|
if (!sessionInfo.getSelectedRoutes().contains(requestedRoute.getId())) {
|
||
|
Log.w(
|
||
|
TAG,
|
||
|
"The session does not contain the requested route. "
|
||
|
+ "(requestedRouteId="
|
||
|
+ requestedRoute.getId()
|
||
|
+ ", actualRoutes="
|
||
|
+ sessionInfo.getSelectedRoutes()
|
||
|
+ ")");
|
||
|
this.onTransferFailed(matchingRequest.mOldSessionInfo, requestedRoute);
|
||
|
} else if (!TextUtils.equals(
|
||
|
requestedRoute.getProviderId(), sessionInfo.getProviderId())) {
|
||
|
Log.w(
|
||
|
TAG,
|
||
|
"The session's provider ID does not match the requested route's. "
|
||
|
+ "(requested route's providerId="
|
||
|
+ requestedRoute.getProviderId()
|
||
|
+ ", actual providerId="
|
||
|
+ sessionInfo.getProviderId()
|
||
|
+ ")");
|
||
|
this.onTransferFailed(matchingRequest.mOldSessionInfo, requestedRoute);
|
||
|
} else {
|
||
|
this.onTransferred(matchingRequest.mOldSessionInfo, sessionInfo);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void onSessionUpdatedOnHandler(@NonNull RoutingSessionInfo updatedSession) {
|
||
|
for (MediaRouter2Manager.TransferRequest request : mTransferRequests) {
|
||
|
String sessionId = request.mOldSessionInfo.getId();
|
||
|
if (!TextUtils.equals(sessionId, updatedSession.getId())) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if (updatedSession.getSelectedRoutes().contains(request.mTargetRoute.getId())) {
|
||
|
mTransferRequests.remove(request);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
this.onSessionUpdated(updatedSession);
|
||
|
}
|
||
|
|
||
|
private void onSessionReleasedOnHandler(@NonNull RoutingSessionInfo session) {
|
||
|
if (session.isSystemSession()) {
|
||
|
Log.e(TAG, "onSessionReleasedOnHandler: Called on system session. Ignoring.");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (!TextUtils.equals(getClientPackageName(), session.getClientPackageName())) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
notifyStop(new RoutingController(session, RoutingController.CONTROLLER_STATE_RELEASED));
|
||
|
}
|
||
|
|
||
|
private void onDiscoveryPreferenceChangedOnHandler(
|
||
|
@NonNull String packageName, @Nullable RouteDiscoveryPreference preference) {
|
||
|
if (!TextUtils.equals(getClientPackageName(), packageName)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (preference == null) {
|
||
|
return;
|
||
|
}
|
||
|
synchronized (mLock) {
|
||
|
if (Objects.equals(preference, mDiscoveryPreference)) {
|
||
|
return;
|
||
|
}
|
||
|
mDiscoveryPreference = preference;
|
||
|
updateFilteredRoutesLocked();
|
||
|
}
|
||
|
notifyPreferredFeaturesChanged(preference.getPreferredFeatures());
|
||
|
}
|
||
|
|
||
|
private void onRouteListingPreferenceChangedOnHandler(
|
||
|
@NonNull String packageName,
|
||
|
@Nullable RouteListingPreference routeListingPreference) {
|
||
|
if (!TextUtils.equals(getClientPackageName(), packageName)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
synchronized (mLock) {
|
||
|
if (Objects.equals(mRouteListingPreference, routeListingPreference)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
mRouteListingPreference = routeListingPreference;
|
||
|
}
|
||
|
|
||
|
notifyRouteListingPreferenceUpdated(routeListingPreference);
|
||
|
}
|
||
|
|
||
|
private void onRequestFailedOnHandler(int requestId, int reason) {
|
||
|
MediaRouter2Manager.TransferRequest matchingRequest = null;
|
||
|
for (MediaRouter2Manager.TransferRequest request : mTransferRequests) {
|
||
|
if (request.mRequestId == requestId) {
|
||
|
matchingRequest = request;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (matchingRequest != null) {
|
||
|
mTransferRequests.remove(matchingRequest);
|
||
|
onTransferFailed(matchingRequest.mOldSessionInfo, matchingRequest.mTargetRoute);
|
||
|
} else {
|
||
|
notifyRequestFailed(reason);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private void onInvalidateInstanceOnHandler() {
|
||
|
Log.w(
|
||
|
TAG,
|
||
|
"MEDIA_ROUTING_CONTROL has been revoked for this package. Invalidating"
|
||
|
+ " instance.");
|
||
|
// After this block, all following getInstance() calls should throw a SecurityException,
|
||
|
// so no new onInstanceInvalidatedListeners can be registered to this instance.
|
||
|
synchronized (sSystemRouterLock) {
|
||
|
PackageNameUserHandlePair key =
|
||
|
new PackageNameUserHandlePair(mClientPackageName, mClientUser);
|
||
|
sAppToProxyRouterMap.remove(key);
|
||
|
}
|
||
|
|
||
|
synchronized (mLock) {
|
||
|
for (InstanceInvalidatedCallbackRecord record :
|
||
|
mInstanceInvalidatedCallbackRecords) {
|
||
|
record.executor.execute(record.runnable);
|
||
|
}
|
||
|
}
|
||
|
mRouteCallbackRecords.clear();
|
||
|
mControllerCallbackRecords.clear();
|
||
|
mTransferCallbackRecords.clear();
|
||
|
}
|
||
|
|
||
|
private class Client extends IMediaRouter2Manager.Stub {
|
||
|
|
||
|
@Override
|
||
|
public void notifySessionCreated(int requestId, RoutingSessionInfo routingSessionInfo) {
|
||
|
mHandler.sendMessage(
|
||
|
obtainMessage(
|
||
|
ProxyMediaRouter2Impl::onSessionCreatedOnHandler,
|
||
|
ProxyMediaRouter2Impl.this,
|
||
|
requestId,
|
||
|
routingSessionInfo));
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void notifySessionUpdated(RoutingSessionInfo routingSessionInfo) {
|
||
|
mHandler.sendMessage(
|
||
|
obtainMessage(
|
||
|
ProxyMediaRouter2Impl::onSessionUpdatedOnHandler,
|
||
|
ProxyMediaRouter2Impl.this,
|
||
|
routingSessionInfo));
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void notifySessionReleased(RoutingSessionInfo routingSessionInfo) {
|
||
|
mHandler.sendMessage(
|
||
|
obtainMessage(
|
||
|
ProxyMediaRouter2Impl::onSessionReleasedOnHandler,
|
||
|
ProxyMediaRouter2Impl.this,
|
||
|
routingSessionInfo));
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void notifyDiscoveryPreferenceChanged(
|
||
|
String packageName, RouteDiscoveryPreference routeDiscoveryPreference) {
|
||
|
mHandler.sendMessage(
|
||
|
obtainMessage(
|
||
|
ProxyMediaRouter2Impl::onDiscoveryPreferenceChangedOnHandler,
|
||
|
ProxyMediaRouter2Impl.this,
|
||
|
packageName,
|
||
|
routeDiscoveryPreference));
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void notifyRouteListingPreferenceChange(
|
||
|
String packageName, RouteListingPreference routeListingPreference) {
|
||
|
mHandler.sendMessage(
|
||
|
obtainMessage(
|
||
|
ProxyMediaRouter2Impl::onRouteListingPreferenceChangedOnHandler,
|
||
|
ProxyMediaRouter2Impl.this,
|
||
|
packageName,
|
||
|
routeListingPreference));
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void notifyRoutesUpdated(List<MediaRoute2Info> routes) {
|
||
|
mHandler.sendMessage(
|
||
|
obtainMessage(
|
||
|
MediaRouter2::updateRoutesOnHandler, MediaRouter2.this, routes));
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void notifyRequestFailed(int requestId, int reason) {
|
||
|
mHandler.sendMessage(
|
||
|
obtainMessage(
|
||
|
ProxyMediaRouter2Impl::onRequestFailedOnHandler,
|
||
|
ProxyMediaRouter2Impl.this,
|
||
|
requestId,
|
||
|
reason));
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void invalidateInstance() {
|
||
|
mHandler.sendMessage(
|
||
|
obtainMessage(
|
||
|
ProxyMediaRouter2Impl::onInvalidateInstanceOnHandler,
|
||
|
ProxyMediaRouter2Impl.this));
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Implements logic specific to local {@link MediaRouter2} instances.
|
||
|
*
|
||
|
* <p>Local routers allow an app to control its own routing without any special permissions.
|
||
|
* Apps can obtain an instance by calling {@link #getInstance(Context)}.
|
||
|
*/
|
||
|
private class LocalMediaRouter2Impl implements MediaRouter2Impl {
|
||
|
private final String mPackageName;
|
||
|
|
||
|
LocalMediaRouter2Impl(@NonNull String packageName) {
|
||
|
mPackageName = packageName;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* No-op. Local routers cannot explicitly control route scanning.
|
||
|
*
|
||
|
* <p>Local routers can control scanning indirectly through {@link
|
||
|
* #registerRouteCallback(Executor, RouteCallback, RouteDiscoveryPreference)}.
|
||
|
*/
|
||
|
@Override
|
||
|
public void startScan() {
|
||
|
// Do nothing.
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* No-op. Local routers cannot explicitly control route scanning.
|
||
|
*
|
||
|
* <p>Local routers can control scanning indirectly through {@link
|
||
|
* #registerRouteCallback(Executor, RouteCallback, RouteDiscoveryPreference)}.
|
||
|
*/
|
||
|
@Override
|
||
|
public void stopScan() {
|
||
|
// Do nothing.
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
@GuardedBy("mLock")
|
||
|
public void updateScanningState(int scanningState) throws RemoteException {
|
||
|
if (scanningState != SCANNING_STATE_NOT_SCANNING) {
|
||
|
registerRouterStubIfNeededLocked();
|
||
|
}
|
||
|
mMediaRouterService.updateScanningStateWithRouter2(mStub, scanningState);
|
||
|
if (scanningState == SCANNING_STATE_NOT_SCANNING) {
|
||
|
unregisterRouterStubIfNeededLocked(/* isScanningStopping */ true);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns {@code null}. The client package name is only associated to proxy {@link
|
||
|
* MediaRouter2} instances.
|
||
|
*/
|
||
|
@Override
|
||
|
public String getClientPackageName() {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public String getPackageName() {
|
||
|
return mPackageName;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public RoutingSessionInfo getSystemSessionInfo() {
|
||
|
RoutingSessionInfo currentSystemSessionInfo = null;
|
||
|
try {
|
||
|
currentSystemSessionInfo = ensureClientPackageNameForSystemSession(
|
||
|
mMediaRouterService.getSystemSessionInfo(), mContext.getPackageName());
|
||
|
} catch (RemoteException ex) {
|
||
|
ex.rethrowFromSystemServer();
|
||
|
}
|
||
|
return currentSystemSessionInfo;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public RouteCallbackRecord createRouteCallbackRecord(
|
||
|
Executor executor,
|
||
|
RouteCallback routeCallback,
|
||
|
RouteDiscoveryPreference preference) {
|
||
|
return new RouteCallbackRecord(executor, routeCallback, preference);
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void registerRouteCallback() {
|
||
|
synchronized (mLock) {
|
||
|
try {
|
||
|
registerRouterStubIfNeededLocked();
|
||
|
|
||
|
if (updateDiscoveryPreferenceIfNeededLocked()) {
|
||
|
mMediaRouterService.setDiscoveryRequestWithRouter2(
|
||
|
mStub, mDiscoveryPreference);
|
||
|
}
|
||
|
} catch (RemoteException ex) {
|
||
|
ex.rethrowFromSystemServer();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void unregisterRouteCallback() {
|
||
|
synchronized (mLock) {
|
||
|
if (mStub == null) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
if (updateDiscoveryPreferenceIfNeededLocked()) {
|
||
|
mMediaRouterService.setDiscoveryRequestWithRouter2(
|
||
|
mStub, mDiscoveryPreference);
|
||
|
}
|
||
|
|
||
|
unregisterRouterStubIfNeededLocked(/* isScanningStopping */ false);
|
||
|
|
||
|
} catch (RemoteException ex) {
|
||
|
Log.e(TAG, "unregisterRouteCallback: Unable to set discovery request.", ex);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void setRouteListingPreference(@Nullable RouteListingPreference preference) {
|
||
|
synchronized (mLock) {
|
||
|
if (Objects.equals(mRouteListingPreference, preference)) {
|
||
|
// Nothing changed. We return early to save a call to the system server.
|
||
|
return;
|
||
|
}
|
||
|
mRouteListingPreference = preference;
|
||
|
try {
|
||
|
registerRouterStubIfNeededLocked();
|
||
|
mMediaRouterService.setRouteListingPreference(mStub, mRouteListingPreference);
|
||
|
} catch (RemoteException ex) {
|
||
|
ex.rethrowFromSystemServer();
|
||
|
}
|
||
|
notifyRouteListingPreferenceUpdated(preference);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public boolean showSystemOutputSwitcher() {
|
||
|
synchronized (mLock) {
|
||
|
try {
|
||
|
return mMediaRouterService.showMediaOutputSwitcher(mImpl.getPackageName());
|
||
|
} catch (RemoteException ex) {
|
||
|
ex.rethrowFromSystemServer();
|
||
|
}
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns {@link Collections#emptyList()}. Local routes can only access routes related to
|
||
|
* their {@link RouteDiscoveryPreference} through {@link #getRoutes()}.
|
||
|
*/
|
||
|
@Override
|
||
|
public List<MediaRoute2Info> getAllRoutes() {
|
||
|
return Collections.emptyList();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void setOnGetControllerHintsListener(OnGetControllerHintsListener listener) {
|
||
|
mOnGetControllerHintsListener = listener;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void transferTo(MediaRoute2Info route) {
|
||
|
Log.v(TAG, "Transferring to route: " + route);
|
||
|
|
||
|
boolean routeFound;
|
||
|
synchronized (mLock) {
|
||
|
// TODO: Check thread-safety
|
||
|
routeFound = mRoutes.containsKey(route.getId());
|
||
|
}
|
||
|
if (!routeFound) {
|
||
|
notifyTransferFailure(route);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
RoutingController controller = getCurrentController();
|
||
|
if (!controller.tryTransferWithinProvider(route)) {
|
||
|
requestCreateController(
|
||
|
controller,
|
||
|
route,
|
||
|
MANAGER_REQUEST_ID_NONE,
|
||
|
Process.myUserHandle(),
|
||
|
mContext.getPackageName());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void stop() {
|
||
|
getCurrentController().release();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* No-op. Local routers cannot request transfers of specific {@link RoutingSessionInfo}.
|
||
|
* This operation is only available to proxy routers.
|
||
|
*
|
||
|
* <p>Local routers can only transfer the current {@link RoutingSessionInfo} using {@link
|
||
|
* #transferTo(MediaRoute2Info)}.
|
||
|
*/
|
||
|
@Override
|
||
|
public void transfer(
|
||
|
@NonNull RoutingSessionInfo sessionInfo, @NonNull MediaRoute2Info route) {
|
||
|
// Do nothing.
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public List<RoutingController> getControllers() {
|
||
|
List<RoutingController> result = new ArrayList<>();
|
||
|
|
||
|
result.add(0, mSystemController);
|
||
|
synchronized (mLock) {
|
||
|
result.addAll(mNonSystemRoutingControllers.values());
|
||
|
}
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
/** Local routers cannot modify the volume of specific routes. */
|
||
|
@Override
|
||
|
public void setRouteVolume(MediaRoute2Info route, int volume) {
|
||
|
throw new UnsupportedOperationException(
|
||
|
"setRouteVolume is only supported by proxy routers. See javadoc.");
|
||
|
// If this API needs to be public, use IMediaRouterService#setRouteVolumeWithRouter2()
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void setSessionVolume(int volume, RoutingSessionInfo sessionInfo) {
|
||
|
MediaRouter2Stub stub;
|
||
|
synchronized (mLock) {
|
||
|
stub = mStub;
|
||
|
}
|
||
|
if (stub != null) {
|
||
|
try {
|
||
|
mMediaRouterService.setSessionVolumeWithRouter2(
|
||
|
stub, sessionInfo.getId(), volume);
|
||
|
} catch (RemoteException ex) {
|
||
|
Log.e(TAG, "setVolume: Failed to deliver request.", ex);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public List<MediaRoute2Info> filterRoutesWithIndividualPreference(
|
||
|
List<MediaRoute2Info> routes, RouteDiscoveryPreference discoveryPreference) {
|
||
|
List<MediaRoute2Info> filteredRoutes = new ArrayList<>();
|
||
|
for (MediaRoute2Info route : routes) {
|
||
|
if (!route.hasAnyFeatures(discoveryPreference.getPreferredFeatures())) {
|
||
|
continue;
|
||
|
}
|
||
|
if (!discoveryPreference.getAllowedPackages().isEmpty()
|
||
|
&& (route.getPackageName() == null
|
||
|
|| !discoveryPreference
|
||
|
.getAllowedPackages()
|
||
|
.contains(route.getPackageName()))) {
|
||
|
continue;
|
||
|
}
|
||
|
filteredRoutes.add(route);
|
||
|
}
|
||
|
return filteredRoutes;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void selectRoute(MediaRoute2Info route, RoutingSessionInfo sessionInfo) {
|
||
|
MediaRouter2Stub stub;
|
||
|
synchronized (mLock) {
|
||
|
stub = mStub;
|
||
|
}
|
||
|
if (stub != null) {
|
||
|
try {
|
||
|
mMediaRouterService.selectRouteWithRouter2(stub, sessionInfo.getId(), route);
|
||
|
} catch (RemoteException ex) {
|
||
|
Log.e(TAG, "Unable to select route for session.", ex);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void deselectRoute(MediaRoute2Info route, RoutingSessionInfo sessionInfo) {
|
||
|
MediaRouter2Stub stub;
|
||
|
synchronized (mLock) {
|
||
|
stub = mStub;
|
||
|
}
|
||
|
if (stub != null) {
|
||
|
try {
|
||
|
mMediaRouterService.deselectRouteWithRouter2(stub, sessionInfo.getId(), route);
|
||
|
} catch (RemoteException ex) {
|
||
|
Log.e(TAG, "Unable to deselect route from session.", ex);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void releaseSession(
|
||
|
boolean shouldReleaseSession,
|
||
|
boolean shouldNotifyStop,
|
||
|
RoutingController controller) {
|
||
|
synchronized (mLock) {
|
||
|
mNonSystemRoutingControllers.remove(controller.getId(), controller);
|
||
|
|
||
|
if (shouldReleaseSession && mStub != null) {
|
||
|
try {
|
||
|
mMediaRouterService.releaseSessionWithRouter2(mStub, controller.getId());
|
||
|
} catch (RemoteException ex) {
|
||
|
ex.rethrowFromSystemServer();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (shouldNotifyStop) {
|
||
|
mHandler.sendMessage(
|
||
|
obtainMessage(MediaRouter2::notifyStop, MediaRouter2.this, controller));
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
unregisterRouterStubIfNeededLocked(/* isScanningStopping */ false);
|
||
|
} catch (RemoteException ex) {
|
||
|
ex.rethrowFromSystemServer();
|
||
|
}
|
||
|
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public boolean wasTransferredBySelf(RoutingSessionInfo sessionInfo) {
|
||
|
UserHandle transferInitiatorUserHandle = sessionInfo.getTransferInitiatorUserHandle();
|
||
|
String transferInitiatorPackageName = sessionInfo.getTransferInitiatorPackageName();
|
||
|
return Objects.equals(Process.myUserHandle(), transferInitiatorUserHandle)
|
||
|
&& Objects.equals(mContext.getPackageName(), transferInitiatorPackageName);
|
||
|
}
|
||
|
|
||
|
@GuardedBy("mLock")
|
||
|
private void registerRouterStubIfNeededLocked() throws RemoteException {
|
||
|
if (mStub == null) {
|
||
|
MediaRouter2Stub stub = new MediaRouter2Stub();
|
||
|
mMediaRouterService.registerRouter2(stub, mPackageName);
|
||
|
mStub = stub;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
@GuardedBy("mLock")
|
||
|
private void unregisterRouterStubIfNeededLocked(boolean isScanningStopping)
|
||
|
throws RemoteException {
|
||
|
if (mStub != null
|
||
|
&& mRouteCallbackRecords.isEmpty()
|
||
|
&& mNonSystemRoutingControllers.isEmpty()
|
||
|
&& (mScanRequestsMap.size() == 0 || isScanningStopping)) {
|
||
|
mMediaRouterService.unregisterRouter2(mStub);
|
||
|
mStub = null;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|