| /* |
| * Copyright 2023 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package androidx.mediarouter.media; |
| |
| import static androidx.mediarouter.media.MediaRouter.AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE; |
| import static androidx.mediarouter.media.MediaRouter.AVAILABILITY_FLAG_REQUIRE_MATCH; |
| import static androidx.mediarouter.media.MediaRouter.CALLBACK_FLAG_FORCE_DISCOVERY; |
| import static androidx.mediarouter.media.MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN; |
| import static androidx.mediarouter.media.MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY; |
| import static androidx.mediarouter.media.MediaRouter.UNSELECT_REASON_ROUTE_CHANGED; |
| import static androidx.mediarouter.media.MediaRouter.UNSELECT_REASON_STOPPED; |
| import static androidx.mediarouter.media.MediaRouter.UNSELECT_REASON_UNKNOWN; |
| |
| import android.annotation.SuppressLint; |
| import android.app.ActivityManager; |
| import android.content.ContentResolver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.os.Build; |
| import android.os.Handler; |
| import android.os.Message; |
| import android.support.v4.media.session.MediaSessionCompat; |
| import android.text.TextUtils; |
| import android.util.Log; |
| import android.view.Display; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.annotation.RestrictTo; |
| import androidx.annotation.VisibleForTesting; |
| import androidx.core.app.ActivityManagerCompat; |
| import androidx.core.content.ContextCompat; |
| import androidx.core.hardware.display.DisplayManagerCompat; |
| import androidx.core.util.Pair; |
| import androidx.media.VolumeProviderCompat; |
| |
| import com.google.common.util.concurrent.ListenableFuture; |
| |
| import java.lang.ref.WeakReference; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Set; |
| |
| /** |
| * Global state for the media router. |
| * |
| * <p>Media routes and media route providers are global to the process; their state and the bulk of |
| * the media router implementation lives here. |
| */ |
| /* package */ final class GlobalMediaRouter |
| implements SystemMediaRouteProvider.SyncCallback, |
| RegisteredMediaRouteProviderWatcher.Callback { |
| |
| static final String TAG = "GlobalMediaRouter"; |
| static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); |
| |
| final CallbackHandler mCallbackHandler = new CallbackHandler(); |
| // A map from unique route ID to RouteController for the member routes in the currently |
| // selected route group. |
| final Map<String, MediaRouteProvider.RouteController> mRouteControllerMap = new HashMap<>(); |
| |
| @VisibleForTesting |
| RegisteredMediaRouteProviderWatcher mRegisteredProviderWatcher; |
| MediaRouter.RouteInfo mSelectedRoute; |
| MediaRouteProvider.RouteController mSelectedRouteController; |
| MediaRouter.OnPrepareTransferListener mOnPrepareTransferListener; |
| MediaRouter.PrepareTransferNotifier mTransferNotifier; |
| |
| private final Context mApplicationContext; |
| private final ArrayList<WeakReference<MediaRouter>> mRouters = new ArrayList<>(); |
| private final ArrayList<MediaRouter.RouteInfo> mRoutes = new ArrayList<>(); |
| private final Map<Pair<String, String>, String> mUniqueIdMap = new HashMap<>(); |
| private final ArrayList<MediaRouter.ProviderInfo> mProviders = new ArrayList<>(); |
| private final ArrayList<RemoteControlClientRecord> mRemoteControlClients = new ArrayList<>(); |
| private final RemoteControlClientCompat.PlaybackInfo mPlaybackInfo = |
| new RemoteControlClientCompat.PlaybackInfo(); |
| private final ProviderCallback mProviderCallback = new ProviderCallback(); |
| private final boolean mLowRam; |
| private final MediaSessionCompat.OnActiveChangeListener mSessionActiveListener = |
| new MediaSessionCompat.OnActiveChangeListener() { |
| @Override |
| public void onActiveChanged() { |
| if (mRccMediaSession != null) { |
| android.media.RemoteControlClient remoteControlClient = |
| (android.media.RemoteControlClient) |
| mRccMediaSession.getRemoteControlClient(); |
| if (mRccMediaSession.isActive()) { |
| addRemoteControlClient(remoteControlClient); |
| } else { |
| removeRemoteControlClient(remoteControlClient); |
| } |
| } |
| } |
| }; |
| |
| private boolean mTransferReceiverDeclared; |
| private boolean mUseMediaRouter2ForSystemRouting; |
| private MediaRoute2Provider mMr2Provider; |
| private SystemMediaRouteProvider mSystemProvider; |
| private DisplayManagerCompat mDisplayManager; |
| private MediaRouterActiveScanThrottlingHelper mActiveScanThrottlingHelper; |
| private MediaRouterParams mRouterParams; |
| private MediaRouter.RouteInfo mDefaultRoute; |
| private MediaRouter.RouteInfo mBluetoothRoute; |
| // Represents a route that are requested to be selected asynchronously. |
| private MediaRouter.RouteInfo mRequestedRoute; |
| private MediaRouteProvider.RouteController mRequestedRouteController; |
| private MediaRouteDiscoveryRequest mDiscoveryRequest; |
| private MediaRouteDiscoveryRequest mDiscoveryRequestForMr2Provider; |
| private int mCallbackCount; |
| private MediaSessionRecord mMediaSession; |
| private MediaSessionCompat mRccMediaSession; |
| private MediaSessionCompat mCompatSession; |
| |
| /* package */ GlobalMediaRouter(Context applicationContext) { |
| mApplicationContext = applicationContext; |
| mLowRam = |
| ActivityManagerCompat.isLowRamDevice( |
| (ActivityManager) |
| applicationContext.getSystemService(Context.ACTIVITY_SERVICE)); |
| |
| mTransferReceiverDeclared = |
| Build.VERSION.SDK_INT >= Build.VERSION_CODES.R |
| && MediaTransferReceiver.isDeclared(mApplicationContext); |
| mUseMediaRouter2ForSystemRouting = |
| SystemRoutingUsingMediaRouter2Receiver.isDeclared(mApplicationContext); |
| |
| if (DEBUG && mUseMediaRouter2ForSystemRouting) { |
| // This is only added to skip the presubmit check for UnusedVariable |
| // TODO: Remove it once mUseMediaRouter2ForSystemRouting is actually used |
| Log.d(TAG, "Using MediaRouter2 for system routing"); |
| } |
| |
| mMr2Provider = |
| Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && mTransferReceiverDeclared |
| ? new MediaRoute2Provider(mApplicationContext, new Mr2ProviderCallback()) |
| : null; |
| |
| // Add the system media route provider for interoperating with |
| // the framework media router. This one is special and receives |
| // synchronization messages from the media router. |
| mSystemProvider = SystemMediaRouteProvider.obtain(mApplicationContext, this); |
| start(); |
| } |
| |
| private void start() { |
| mActiveScanThrottlingHelper = |
| new MediaRouterActiveScanThrottlingHelper(this::updateDiscoveryRequest); |
| addProvider(mSystemProvider, /* treatRouteDescriptorIdsAsUnique= */ true); |
| if (mMr2Provider != null) { |
| addProvider(mMr2Provider, /* treatRouteDescriptorIdsAsUnique= */ true); |
| } |
| |
| // Start watching for routes published by registered media route |
| // provider services. |
| mRegisteredProviderWatcher = |
| new RegisteredMediaRouteProviderWatcher(mApplicationContext, this); |
| mRegisteredProviderWatcher.start(); |
| } |
| |
| /* package */ void reset() { |
| mActiveScanThrottlingHelper.reset(); |
| |
| setRouteListingPreference(null); |
| setMediaSessionCompat(null); |
| |
| mRegisteredProviderWatcher.stop(); |
| |
| for (RemoteControlClientRecord record : mRemoteControlClients) { |
| record.disconnect(); |
| } |
| |
| List<MediaRouter.ProviderInfo> providers = new ArrayList<>(mProviders); |
| for (MediaRouter.ProviderInfo providerInfo : providers) { |
| removeProvider(providerInfo.mProviderInstance); |
| } |
| mCallbackHandler.removeCallbacksAndMessages(null); |
| } |
| |
| /* package */ MediaRouter getRouter(Context context) { |
| MediaRouter router; |
| for (int i = mRouters.size(); --i >= 0; ) { |
| router = mRouters.get(i).get(); |
| if (router == null) { |
| mRouters.remove(i); |
| } else if (router.mContext == context) { |
| return router; |
| } |
| } |
| router = new MediaRouter(context); |
| mRouters.add(new WeakReference<>(router)); |
| return router; |
| } |
| |
| /* package */ ContentResolver getContentResolver() { |
| return mApplicationContext.getContentResolver(); |
| } |
| |
| /* package */ Display getDisplay(int displayId) { |
| if (mDisplayManager == null) { |
| mDisplayManager = DisplayManagerCompat.getInstance(mApplicationContext); |
| } |
| return mDisplayManager.getDisplay(displayId); |
| } |
| |
| /* package */ void sendControlRequest( |
| MediaRouter.RouteInfo route, |
| Intent intent, |
| MediaRouter.ControlRequestCallback callback) { |
| if (route == mSelectedRoute && mSelectedRouteController != null) { |
| if (mSelectedRouteController.onControlRequest(intent, callback)) { |
| return; |
| } |
| } |
| if (mTransferNotifier != null |
| && route == mTransferNotifier.mToRoute |
| && mTransferNotifier.mToRouteController != null) { |
| if (mTransferNotifier.mToRouteController.onControlRequest(intent, callback)) { |
| return; |
| } |
| } |
| if (callback != null) { |
| callback.onError(null, null); |
| } |
| } |
| |
| /* package */ void requestSetVolume(MediaRouter.RouteInfo route, int volume) { |
| if (route == mSelectedRoute && mSelectedRouteController != null) { |
| mSelectedRouteController.onSetVolume(volume); |
| } else if (!mRouteControllerMap.isEmpty()) { |
| MediaRouteProvider.RouteController controller = |
| mRouteControllerMap.get(route.mUniqueId); |
| if (controller != null) { |
| controller.onSetVolume(volume); |
| } |
| } |
| } |
| |
| /* package */ void requestUpdateVolume(MediaRouter.RouteInfo route, int delta) { |
| if (route == mSelectedRoute && mSelectedRouteController != null) { |
| mSelectedRouteController.onUpdateVolume(delta); |
| } else if (!mRouteControllerMap.isEmpty()) { |
| MediaRouteProvider.RouteController controller = |
| mRouteControllerMap.get(route.mUniqueId); |
| if (controller != null) { |
| controller.onUpdateVolume(delta); |
| } |
| } |
| } |
| |
| /* package */ MediaRouter.RouteInfo getRoute(String uniqueId) { |
| for (MediaRouter.RouteInfo info : mRoutes) { |
| if (info.mUniqueId.equals(uniqueId)) { |
| return info; |
| } |
| } |
| return null; |
| } |
| |
| /* package */ List<MediaRouter.RouteInfo> getRoutes() { |
| return mRoutes; |
| } |
| |
| @Nullable |
| /* package */ MediaRouterParams getRouterParams() { |
| return mRouterParams; |
| } |
| |
| // isMediaTransferEnabled() is true only on R+ device. |
| @SuppressLint("NewApi") |
| /* package */ void setRouterParams(@Nullable MediaRouterParams params) { |
| MediaRouterParams oldParams = mRouterParams; |
| mRouterParams = params; |
| |
| if (isMediaTransferEnabled()) { |
| if (mMr2Provider == null) { |
| mMr2Provider = |
| new MediaRoute2Provider(mApplicationContext, new Mr2ProviderCallback()); |
| addProvider(mMr2Provider, /* treatRouteDescriptorIdsAsUnique= */ true); |
| // Make sure mDiscoveryRequestForMr2Provider is updated |
| updateDiscoveryRequest(); |
| mRegisteredProviderWatcher.rescan(); |
| } |
| |
| boolean oldTransferToLocalEnabled = |
| oldParams != null && oldParams.isTransferToLocalEnabled(); |
| boolean newTransferToLocalEnabled = params != null && params.isTransferToLocalEnabled(); |
| |
| if (oldTransferToLocalEnabled != newTransferToLocalEnabled) { |
| // Since the discovery request itself is not changed, |
| // call setDiscoveryRequestInternal to avoid the equality check. |
| mMr2Provider.setDiscoveryRequestInternal(mDiscoveryRequestForMr2Provider); |
| } |
| } else { |
| if (mMr2Provider != null) { |
| removeProvider(mMr2Provider); |
| mMr2Provider = null; |
| mRegisteredProviderWatcher.rescan(); |
| } |
| } |
| mCallbackHandler.post(CallbackHandler.MSG_ROUTER_PARAMS_CHANGED, params); |
| } |
| |
| /* package */ void setRouteListingPreference( |
| @Nullable RouteListingPreference routeListingPreference) { |
| if (mMr2Provider != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { |
| mMr2Provider.setRouteListingPreference(routeListingPreference); |
| } |
| } |
| |
| @NonNull |
| /* package */ List<MediaRouter.ProviderInfo> getProviders() { |
| return mProviders; |
| } |
| |
| @NonNull |
| /* package */ MediaRouter.RouteInfo getDefaultRoute() { |
| if (mDefaultRoute == null) { |
| // This should never happen once the media router has been fully |
| // initialized but it is good to check for the error in case there |
| // is a bug in provider initialization. |
| throw new IllegalStateException( |
| "There is no default route. " |
| + "The media router has not yet been fully initialized."); |
| } |
| return mDefaultRoute; |
| } |
| |
| /* package */ MediaRouter.RouteInfo getBluetoothRoute() { |
| return mBluetoothRoute; |
| } |
| |
| @NonNull |
| /* package */ MediaRouter.RouteInfo getSelectedRoute() { |
| if (mSelectedRoute == null) { |
| // This should never happen once the media router has been fully |
| // initialized but it is good to check for the error in case there |
| // is a bug in provider initialization. |
| throw new IllegalStateException( |
| "There is no currently selected route. " |
| + "The media router has not yet been fully initialized."); |
| } |
| return mSelectedRoute; |
| } |
| |
| @Nullable |
| /* package */ MediaRouter.RouteInfo.DynamicGroupState getDynamicGroupState( |
| MediaRouter.RouteInfo route) { |
| return mSelectedRoute.getDynamicGroupState(route); |
| } |
| |
| /* package */ void addMemberToDynamicGroup(@NonNull MediaRouter.RouteInfo route) { |
| if (!(mSelectedRouteController instanceof MediaRouteProvider.DynamicGroupRouteController)) { |
| throw new IllegalStateException("There is no currently selected dynamic group route."); |
| } |
| MediaRouter.RouteInfo.DynamicGroupState state = getDynamicGroupState(route); |
| if (mSelectedRoute.getMemberRoutes().contains(route) |
| || state == null |
| || !state.isGroupable()) { |
| Log.w(TAG, "Ignoring attempt to add a non-groupable route to dynamic group : " + route); |
| return; |
| } |
| ((MediaRouteProvider.DynamicGroupRouteController) mSelectedRouteController) |
| .onAddMemberRoute(route.getDescriptorId()); |
| } |
| |
| /* package */ void removeMemberFromDynamicGroup(@NonNull MediaRouter.RouteInfo route) { |
| if (!(mSelectedRouteController instanceof MediaRouteProvider.DynamicGroupRouteController)) { |
| throw new IllegalStateException("There is no currently selected dynamic group route."); |
| } |
| MediaRouter.RouteInfo.DynamicGroupState state = getDynamicGroupState(route); |
| if (!mSelectedRoute.getMemberRoutes().contains(route) |
| || state == null |
| || !state.isUnselectable()) { |
| Log.w(TAG, "Ignoring attempt to remove a non-unselectable member route : " + route); |
| return; |
| } |
| if (mSelectedRoute.getMemberRoutes().size() <= 1) { |
| Log.w(TAG, "Ignoring attempt to remove the last member route."); |
| return; |
| } |
| ((MediaRouteProvider.DynamicGroupRouteController) mSelectedRouteController) |
| .onRemoveMemberRoute(route.getDescriptorId()); |
| } |
| |
| /* package */ void transferToRoute(@NonNull MediaRouter.RouteInfo route) { |
| if (!(mSelectedRouteController instanceof MediaRouteProvider.DynamicGroupRouteController)) { |
| throw new IllegalStateException("There is no currently selected dynamic group route."); |
| } |
| MediaRouter.RouteInfo.DynamicGroupState state = getDynamicGroupState(route); |
| if (state == null || !state.isTransferable()) { |
| Log.w(TAG, "Ignoring attempt to transfer to a non-transferable route."); |
| return; |
| } |
| ((MediaRouteProvider.DynamicGroupRouteController) mSelectedRouteController) |
| .onUpdateMemberRoutes(Collections.singletonList(route.getDescriptorId())); |
| } |
| |
| /* package */ void selectRoute( |
| @NonNull MediaRouter.RouteInfo route, @MediaRouter.UnselectReason int unselectReason) { |
| if (!mRoutes.contains(route)) { |
| Log.w(TAG, "Ignoring attempt to select removed route: " + route); |
| return; |
| } |
| if (!route.mEnabled) { |
| Log.w(TAG, "Ignoring attempt to select disabled route: " + route); |
| return; |
| } |
| |
| // Check whether the route comes from MediaRouter2. The SDK check is required to avoid a |
| // lint error but is not needed. |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R |
| && route.getProviderInstance() == mMr2Provider |
| && mSelectedRoute != route) { |
| mMr2Provider.transferTo(route.getDescriptorId()); |
| } else { |
| selectRouteInternal(route, unselectReason); |
| } |
| } |
| |
| /* package */ boolean isRouteAvailable(MediaRouteSelector selector, int flags) { |
| if (selector.isEmpty()) { |
| return false; |
| } |
| |
| // On low-RAM devices, do not rely on actual discovery results unless asked to. |
| if ((flags & AVAILABILITY_FLAG_REQUIRE_MATCH) == 0 && mLowRam) { |
| return true; |
| } |
| |
| boolean useOutputSwitcher = |
| mRouterParams != null |
| && mRouterParams.isOutputSwitcherEnabled() |
| && isMediaTransferEnabled(); |
| // Check whether any existing routes match the selector. |
| final int routeCount = mRoutes.size(); |
| for (int i = 0; i < routeCount; i++) { |
| MediaRouter.RouteInfo route = mRoutes.get(i); |
| if ((flags & AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE) != 0 |
| && route.isDefaultOrBluetooth()) { |
| continue; |
| } |
| // When using the output switcher, we only care about MR2 routes and system routes. |
| if (useOutputSwitcher |
| && !route.isDefaultOrBluetooth() |
| && route.getProviderInstance() != mMr2Provider) { |
| continue; |
| } |
| if (route.matchesSelector(selector)) { |
| return true; |
| } |
| } |
| |
| // It doesn't look like we can find a matching route right now. |
| return false; |
| } |
| |
| /* package */ void updateDiscoveryRequest() { |
| // Combine all of the callback selectors and active scan flags. |
| boolean discover = false; |
| MediaRouteSelector.Builder builder = new MediaRouteSelector.Builder(); |
| mActiveScanThrottlingHelper.reset(); |
| |
| int callbackCount = 0; |
| for (int i = mRouters.size(); --i >= 0; ) { |
| MediaRouter router = mRouters.get(i).get(); |
| if (router == null) { |
| mRouters.remove(i); |
| } else { |
| final int count = router.mCallbackRecords.size(); |
| callbackCount += count; |
| for (int j = 0; j < count; j++) { |
| MediaRouter.CallbackRecord callback = router.mCallbackRecords.get(j); |
| builder.addSelector(callback.mSelector); |
| boolean callbackRequestingActiveScan = |
| (callback.mFlags & CALLBACK_FLAG_PERFORM_ACTIVE_SCAN) != 0; |
| mActiveScanThrottlingHelper.requestActiveScan( |
| callbackRequestingActiveScan, callback.mTimestamp); |
| if (callbackRequestingActiveScan) { |
| discover = true; // perform active scan implies request discovery |
| } |
| if ((callback.mFlags & CALLBACK_FLAG_REQUEST_DISCOVERY) != 0) { |
| if (!mLowRam) { |
| discover = true; |
| } |
| } |
| if ((callback.mFlags & CALLBACK_FLAG_FORCE_DISCOVERY) != 0) { |
| discover = true; |
| } |
| } |
| } |
| } |
| |
| boolean activeScan = |
| mActiveScanThrottlingHelper |
| .finalizeActiveScanAndScheduleSuppressActiveScanRunnable(); |
| |
| mCallbackCount = callbackCount; |
| MediaRouteSelector selector = discover ? builder.build() : MediaRouteSelector.EMPTY; |
| |
| // MediaRoute2Provider should keep registering discovery preference |
| // even when the callback flag is zero. |
| updateMr2ProviderDiscoveryRequest(builder.build(), activeScan); |
| |
| // Create a new discovery request. |
| if (mDiscoveryRequest != null |
| && mDiscoveryRequest.getSelector().equals(selector) |
| && mDiscoveryRequest.isActiveScan() == activeScan) { |
| return; // no change |
| } |
| if (selector.isEmpty() && !activeScan) { |
| // Discovery is not needed. |
| if (mDiscoveryRequest == null) { |
| return; // no change |
| } |
| mDiscoveryRequest = null; |
| } else { |
| // Discovery is needed. |
| mDiscoveryRequest = new MediaRouteDiscoveryRequest(selector, activeScan); |
| } |
| if (DEBUG) { |
| Log.d(TAG, "Updated discovery request: " + mDiscoveryRequest); |
| } |
| if (discover && !activeScan && mLowRam) { |
| Log.i( |
| TAG, |
| "Forcing passive route discovery on a low-RAM device, " |
| + "system performance may be affected. Please consider using " |
| + "CALLBACK_FLAG_REQUEST_DISCOVERY instead of " |
| + "CALLBACK_FLAG_FORCE_DISCOVERY."); |
| } |
| |
| // Notify providers. |
| for (MediaRouter.ProviderInfo providerInfo : mProviders) { |
| MediaRouteProvider provider = providerInfo.mProviderInstance; |
| if (provider == mMr2Provider) { |
| // MediaRoute2Provider is handled by updateMr2ProviderDiscoveryRequest(). |
| continue; |
| } |
| provider.setDiscoveryRequest(mDiscoveryRequest); |
| } |
| } |
| |
| private void updateMr2ProviderDiscoveryRequest( |
| @NonNull MediaRouteSelector selector, boolean activeScan) { |
| if (!isMediaTransferEnabled()) { |
| return; |
| } |
| |
| if (mDiscoveryRequestForMr2Provider != null |
| && mDiscoveryRequestForMr2Provider.getSelector().equals(selector) |
| && mDiscoveryRequestForMr2Provider.isActiveScan() == activeScan) { |
| return; // no change |
| } |
| if (selector.isEmpty() && !activeScan) { |
| // Discovery is not needed. |
| if (mDiscoveryRequestForMr2Provider == null) { |
| return; // no change |
| } |
| mDiscoveryRequestForMr2Provider = null; |
| } else { |
| // Discovery is needed. |
| mDiscoveryRequestForMr2Provider = new MediaRouteDiscoveryRequest(selector, activeScan); |
| } |
| if (DEBUG) { |
| Log.d( |
| TAG, |
| "Updated MediaRoute2Provider's discovery request: " |
| + mDiscoveryRequestForMr2Provider); |
| } |
| |
| mMr2Provider.setDiscoveryRequest(mDiscoveryRequestForMr2Provider); |
| } |
| |
| /* package */ int getCallbackCount() { |
| return mCallbackCount; |
| } |
| |
| /* package */ boolean isMediaTransferEnabled() { |
| // The default value for isMediaTransferReceiverEnabled() is {@code true}. |
| return mTransferReceiverDeclared |
| && (mRouterParams == null || mRouterParams.isMediaTransferReceiverEnabled()); |
| } |
| |
| /* package */ boolean isTransferToLocalEnabled() { |
| if (mRouterParams == null) { |
| return false; |
| } |
| return mRouterParams.isTransferToLocalEnabled(); |
| } |
| |
| /** */ |
| @RestrictTo(RestrictTo.Scope.LIBRARY) |
| /* package */ boolean isGroupVolumeUxEnabled() { |
| return mRouterParams == null |
| || mRouterParams.mExtras == null |
| || mRouterParams.mExtras.getBoolean(MediaRouterParams.ENABLE_GROUP_VOLUME_UX, true); |
| } |
| |
| @Override |
| public void addProvider(@NonNull MediaRouteProvider providerInstance) { |
| addProvider(providerInstance, /* treatRouteDescriptorIdsAsUnique= */ false); |
| } |
| |
| private void addProvider( |
| @NonNull MediaRouteProvider providerInstance, boolean treatRouteDescriptorIdsAsUnique) { |
| if (findProviderInfo(providerInstance) == null) { |
| // 1. Add the provider to the list. |
| MediaRouter.ProviderInfo provider = |
| new MediaRouter.ProviderInfo(providerInstance, treatRouteDescriptorIdsAsUnique); |
| mProviders.add(provider); |
| if (DEBUG) { |
| Log.d(TAG, "Provider added: " + provider); |
| } |
| mCallbackHandler.post(CallbackHandler.MSG_PROVIDER_ADDED, provider); |
| // 2. Create the provider's contents. |
| updateProviderContents(provider, providerInstance.getDescriptor()); |
| // 3. Register the provider callback. |
| providerInstance.setCallback(mProviderCallback); |
| // 4. Set the discovery request. |
| providerInstance.setDiscoveryRequest(mDiscoveryRequest); |
| } |
| } |
| |
| @Override |
| public void removeProvider(@NonNull MediaRouteProvider providerInstance) { |
| MediaRouter.ProviderInfo provider = findProviderInfo(providerInstance); |
| if (provider != null) { |
| // 1. Unregister the provider callback. |
| providerInstance.setCallback(null); |
| // 2. Clear the discovery request. |
| providerInstance.setDiscoveryRequest(null); |
| // 3. Delete the provider's contents. |
| updateProviderContents(provider, null); |
| // 4. Remove the provider from the list. |
| if (DEBUG) { |
| Log.d(TAG, "Provider removed: " + provider); |
| } |
| mCallbackHandler.post(CallbackHandler.MSG_PROVIDER_REMOVED, provider); |
| mProviders.remove(provider); |
| } |
| } |
| |
| @Override |
| public void releaseProviderController( |
| @NonNull RegisteredMediaRouteProvider provider, |
| @NonNull MediaRouteProvider.RouteController controller) { |
| if (mSelectedRouteController == controller) { |
| selectRoute(chooseFallbackRoute(), UNSELECT_REASON_STOPPED); |
| } |
| // TODO: Maybe release a member route controller if the given controller is a member of |
| // the selected route. |
| } |
| |
| /* package */ void updateProviderDescriptor( |
| MediaRouteProvider providerInstance, MediaRouteProviderDescriptor descriptor) { |
| MediaRouter.ProviderInfo provider = findProviderInfo(providerInstance); |
| if (provider != null) { |
| // Update the provider's contents. |
| updateProviderContents(provider, descriptor); |
| } |
| } |
| |
| private MediaRouter.ProviderInfo findProviderInfo(MediaRouteProvider providerInstance) { |
| for (MediaRouter.ProviderInfo providerInfo : mProviders) { |
| if (providerInfo.mProviderInstance == providerInstance) { |
| return providerInfo; |
| } |
| } |
| return null; |
| } |
| |
| private void updateProviderContents( |
| MediaRouter.ProviderInfo provider, MediaRouteProviderDescriptor providerDescriptor) { |
| if (!provider.updateDescriptor(providerDescriptor)) { |
| // Nothing to update. |
| return; |
| } |
| // Update all existing routes and reorder them to match |
| // the order of their descriptors. |
| int targetIndex = 0; |
| boolean selectedRouteDescriptorChanged = false; |
| if (providerDescriptor != null |
| && (providerDescriptor.isValid() |
| || providerDescriptor == mSystemProvider.getDescriptor())) { |
| final List<MediaRouteDescriptor> routeDescriptors = providerDescriptor.getRoutes(); |
| // Updating route group's contents requires all member routes' information. |
| // Add the groups to the lists and update them later. |
| List<Pair<MediaRouter.RouteInfo, MediaRouteDescriptor>> addedGroups = new ArrayList<>(); |
| List<Pair<MediaRouter.RouteInfo, MediaRouteDescriptor>> updatedGroups = |
| new ArrayList<>(); |
| for (MediaRouteDescriptor routeDescriptor : routeDescriptors) { |
| // SystemMediaRouteProvider may have invalid routes |
| if (routeDescriptor == null || !routeDescriptor.isValid()) { |
| Log.w(TAG, "Ignoring invalid system route descriptor: " + routeDescriptor); |
| continue; |
| } |
| final String id = routeDescriptor.getId(); |
| final int sourceIndex = provider.findRouteIndexByDescriptorId(id); |
| |
| if (sourceIndex < 0) { |
| // 1. Add the route to the list. |
| String uniqueId = assignRouteUniqueId(provider, id); |
| MediaRouter.RouteInfo route = |
| new MediaRouter.RouteInfo( |
| provider, id, uniqueId, routeDescriptor.isSystemRoute()); |
| |
| provider.mRoutes.add(targetIndex++, route); |
| mRoutes.add(route); |
| // 2. Create the route's contents. |
| if (routeDescriptor.getGroupMemberIds().size() > 0) { |
| addedGroups.add(new Pair<>(route, routeDescriptor)); |
| } else { |
| route.maybeUpdateDescriptor(routeDescriptor); |
| // 3. Notify clients about addition. |
| if (DEBUG) { |
| Log.d(TAG, "Route added: " + route); |
| } |
| mCallbackHandler.post(CallbackHandler.MSG_ROUTE_ADDED, route); |
| } |
| } else if (sourceIndex < targetIndex) { |
| Log.w(TAG, "Ignoring route descriptor with duplicate id: " + routeDescriptor); |
| } else { |
| MediaRouter.RouteInfo route = provider.mRoutes.get(sourceIndex); |
| // 1. Reorder the route within the list. |
| Collections.swap(provider.mRoutes, sourceIndex, targetIndex++); |
| // 2. Update the route's contents. |
| if (routeDescriptor.getGroupMemberIds().size() > 0) { |
| updatedGroups.add(new Pair<>(route, routeDescriptor)); |
| } else { |
| // 3. Notify clients about changes. |
| if (updateRouteDescriptorAndNotify(route, routeDescriptor) != 0) { |
| if (route == mSelectedRoute) { |
| selectedRouteDescriptorChanged = true; |
| } |
| } |
| } |
| } |
| } |
| // Update the new and/or existing groups. |
| for (Pair<MediaRouter.RouteInfo, MediaRouteDescriptor> pair : addedGroups) { |
| MediaRouter.RouteInfo route = pair.first; |
| route.maybeUpdateDescriptor(pair.second); |
| if (DEBUG) { |
| Log.d(TAG, "Route added: " + route); |
| } |
| mCallbackHandler.post(CallbackHandler.MSG_ROUTE_ADDED, route); |
| } |
| for (Pair<MediaRouter.RouteInfo, MediaRouteDescriptor> pair : updatedGroups) { |
| MediaRouter.RouteInfo route = pair.first; |
| if (updateRouteDescriptorAndNotify(route, pair.second) != 0) { |
| if (route == mSelectedRoute) { |
| selectedRouteDescriptorChanged = true; |
| } |
| } |
| } |
| } else { |
| Log.w(TAG, "Ignoring invalid provider descriptor: " + providerDescriptor); |
| } |
| |
| // Dispose all remaining routes that do not have matching descriptors. |
| for (int i = provider.mRoutes.size() - 1; i >= targetIndex; i--) { |
| // 1. Delete the route's contents. |
| MediaRouter.RouteInfo route = provider.mRoutes.get(i); |
| route.maybeUpdateDescriptor(null); |
| // 2. Remove the route from the list. |
| mRoutes.remove(route); |
| } |
| |
| // Update the selected route if needed. |
| updateSelectedRouteIfNeeded(selectedRouteDescriptorChanged); |
| |
| // Now notify clients about routes that were removed. |
| // We do this after updating the selected route to ensure |
| // that the framework media router observes the new route |
| // selection before the removal since removing the currently |
| // selected route may have side-effects. |
| for (int i = provider.mRoutes.size() - 1; i >= targetIndex; i--) { |
| MediaRouter.RouteInfo route = provider.mRoutes.remove(i); |
| if (DEBUG) { |
| Log.d(TAG, "Route removed: " + route); |
| } |
| mCallbackHandler.post(CallbackHandler.MSG_ROUTE_REMOVED, route); |
| } |
| |
| // Notify provider changed. |
| if (DEBUG) { |
| Log.d(TAG, "Provider changed: " + provider); |
| } |
| mCallbackHandler.post(CallbackHandler.MSG_PROVIDER_CHANGED, provider); |
| } |
| |
| /* package */ int updateRouteDescriptorAndNotify( |
| MediaRouter.RouteInfo route, MediaRouteDescriptor routeDescriptor) { |
| int changes = route.maybeUpdateDescriptor(routeDescriptor); |
| if (changes != 0) { |
| if ((changes & MediaRouter.RouteInfo.CHANGE_GENERAL) != 0) { |
| if (DEBUG) { |
| Log.d(TAG, "Route changed: " + route); |
| } |
| mCallbackHandler.post(CallbackHandler.MSG_ROUTE_CHANGED, route); |
| } |
| if ((changes & MediaRouter.RouteInfo.CHANGE_VOLUME) != 0) { |
| if (DEBUG) { |
| Log.d(TAG, "Route volume changed: " + route); |
| } |
| mCallbackHandler.post(CallbackHandler.MSG_ROUTE_VOLUME_CHANGED, route); |
| } |
| if ((changes & MediaRouter.RouteInfo.CHANGE_PRESENTATION_DISPLAY) != 0) { |
| if (DEBUG) { |
| Log.d(TAG, "Route presentation display changed: " + route); |
| } |
| mCallbackHandler.post( |
| CallbackHandler.MSG_ROUTE_PRESENTATION_DISPLAY_CHANGED, route); |
| } |
| } |
| return changes; |
| } |
| |
| /* package */ String assignRouteUniqueId( |
| MediaRouter.ProviderInfo provider, String routeDescriptorId) { |
| // Although route descriptor ids are unique within a provider, it's |
| // possible for there to be two providers with the same package name. |
| // Therefore we must dedupe the composite id. |
| String componentName = provider.getComponentName().flattenToShortString(); |
| String uniqueId = |
| provider.mTreatRouteDescriptorIdsAsUnique |
| ? routeDescriptorId |
| : (componentName + ":" + routeDescriptorId); |
| if (provider.mTreatRouteDescriptorIdsAsUnique || findRouteByUniqueId(uniqueId) < 0) { |
| mUniqueIdMap.put(new Pair<>(componentName, routeDescriptorId), uniqueId); |
| return uniqueId; |
| } |
| Log.w( |
| TAG, |
| "Either " |
| + routeDescriptorId |
| + " isn't unique in " |
| + componentName |
| + " or we're trying to assign a unique ID for an already added route"); |
| int i = 2; |
| while (true) { |
| String newUniqueId = String.format(Locale.US, "%s_%d", uniqueId, i); |
| if (findRouteByUniqueId(newUniqueId) < 0) { |
| mUniqueIdMap.put(new Pair<>(componentName, routeDescriptorId), newUniqueId); |
| return newUniqueId; |
| } |
| i++; |
| } |
| } |
| |
| private int findRouteByUniqueId(String uniqueId) { |
| final int count = mRoutes.size(); |
| for (int i = 0; i < count; i++) { |
| if (mRoutes.get(i).mUniqueId.equals(uniqueId)) { |
| return i; |
| } |
| } |
| return -1; |
| } |
| |
| /* package */ String getUniqueId(MediaRouter.ProviderInfo provider, String routeDescriptorId) { |
| String componentName = provider.getComponentName().flattenToShortString(); |
| return mUniqueIdMap.get(new Pair<>(componentName, routeDescriptorId)); |
| } |
| |
| /* package */ void updateSelectedRouteIfNeeded(boolean selectedRouteDescriptorChanged) { |
| // Update default route. |
| if (mDefaultRoute != null && !mDefaultRoute.isSelectable()) { |
| Log.i( |
| TAG, |
| "Clearing the default route because it " |
| + "is no longer selectable: " |
| + mDefaultRoute); |
| mDefaultRoute = null; |
| } |
| if (mDefaultRoute == null) { |
| for (MediaRouter.RouteInfo route : mRoutes) { |
| if (isSystemDefaultRoute(route) && route.isSelectable()) { |
| mDefaultRoute = route; |
| Log.i(TAG, "Found default route: " + mDefaultRoute); |
| break; |
| } |
| } |
| } |
| |
| // Update bluetooth route. |
| if (mBluetoothRoute != null && !mBluetoothRoute.isSelectable()) { |
| Log.i( |
| TAG, |
| "Clearing the bluetooth route because it " |
| + "is no longer selectable: " |
| + mBluetoothRoute); |
| mBluetoothRoute = null; |
| } |
| if (mBluetoothRoute == null) { |
| for (MediaRouter.RouteInfo route : mRoutes) { |
| if (isSystemLiveAudioOnlyRoute(route) && route.isSelectable()) { |
| mBluetoothRoute = route; |
| Log.i(TAG, "Found bluetooth route: " + mBluetoothRoute); |
| break; |
| } |
| } |
| } |
| |
| // Update selected route. |
| if (mSelectedRoute == null || !mSelectedRoute.isEnabled()) { |
| Log.i( |
| TAG, |
| "Unselecting the current route because it " |
| + "is no longer selectable: " |
| + mSelectedRoute); |
| selectRouteInternal(chooseFallbackRoute(), UNSELECT_REASON_UNKNOWN); |
| } else if (selectedRouteDescriptorChanged) { |
| // In case the selected route is a route group, select/unselect route controllers |
| // for the added/removed route members. |
| maybeUpdateMemberRouteControllers(); |
| updatePlaybackInfoFromSelectedRoute(); |
| } |
| } |
| |
| /* package */ MediaRouter.RouteInfo chooseFallbackRoute() { |
| // When the current route is removed or no longer selectable, |
| // we want to revert to a live audio route if there is |
| // one (usually Bluetooth A2DP). Failing that, use |
| // the default route. |
| for (MediaRouter.RouteInfo route : mRoutes) { |
| if (route != mDefaultRoute |
| && isSystemLiveAudioOnlyRoute(route) |
| && route.isSelectable()) { |
| return route; |
| } |
| } |
| return mDefaultRoute; |
| } |
| |
| private boolean isSystemLiveAudioOnlyRoute(MediaRouter.RouteInfo route) { |
| return route.getProviderInstance() == mSystemProvider |
| && route.supportsControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO) |
| && !route.supportsControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO); |
| } |
| |
| private boolean isSystemDefaultRoute(MediaRouter.RouteInfo route) { |
| return route.getProviderInstance() == mSystemProvider |
| && route.mDescriptorId.equals(SystemMediaRouteProvider.DEFAULT_ROUTE_ID); |
| } |
| |
| /* package */ void selectRouteInternal( |
| @NonNull MediaRouter.RouteInfo route, @MediaRouter.UnselectReason int unselectReason) { |
| if (mSelectedRoute == route) { |
| return; |
| } |
| |
| // Cancel the previous asynchronous select if exists. |
| if (mRequestedRoute != null) { |
| mRequestedRoute = null; |
| if (mRequestedRouteController != null) { |
| mRequestedRouteController.onUnselect(UNSELECT_REASON_ROUTE_CHANGED); |
| mRequestedRouteController.onRelease(); |
| mRequestedRouteController = null; |
| } |
| } |
| |
| // TODO: determine how to enable dynamic grouping on pre-R devices. |
| if (isMediaTransferEnabled() && route.getProvider().supportsDynamicGroup()) { |
| MediaRouteProvider.DynamicGroupRouteController dynamicGroupRouteController = |
| route.getProviderInstance() |
| .onCreateDynamicGroupRouteController(route.mDescriptorId); |
| // Select route asynchronously. |
| if (dynamicGroupRouteController != null) { |
| dynamicGroupRouteController.setOnDynamicRoutesChangedListener( |
| ContextCompat.getMainExecutor(mApplicationContext), mDynamicRoutesListener); |
| mRequestedRoute = route; |
| mRequestedRouteController = dynamicGroupRouteController; |
| mRequestedRouteController.onSelect(); |
| return; |
| } else { |
| Log.w( |
| TAG, |
| "setSelectedRouteInternal: Failed to create dynamic group route " |
| + "controller. route=" |
| + route); |
| } |
| } |
| |
| MediaRouteProvider.RouteController routeController = |
| route.getProviderInstance().onCreateRouteController(route.mDescriptorId); |
| if (routeController != null) { |
| routeController.onSelect(); |
| } |
| |
| if (DEBUG) { |
| Log.d(TAG, "Route selected: " + route); |
| } |
| |
| // Don't notify during the initialization. |
| if (mSelectedRoute == null) { |
| mSelectedRoute = route; |
| mSelectedRouteController = routeController; |
| mCallbackHandler.post( |
| GlobalMediaRouter.CallbackHandler.MSG_ROUTE_SELECTED, |
| new Pair<>(null, route), |
| unselectReason); |
| } else { |
| notifyTransfer( |
| this, |
| route, |
| routeController, |
| unselectReason, |
| /* requestedRoute= */ null, |
| /* memberRoutes= */ null); |
| } |
| } |
| |
| /* package */ void maybeUpdateMemberRouteControllers() { |
| if (!mSelectedRoute.isGroup()) { |
| return; |
| } |
| List<MediaRouter.RouteInfo> routes = mSelectedRoute.getMemberRoutes(); |
| // Build a set of descriptor IDs for the new route group. |
| Set<String> idSet = new HashSet<>(); |
| for (MediaRouter.RouteInfo route : routes) { |
| idSet.add(route.mUniqueId); |
| } |
| // Unselect route controllers for the removed routes. |
| Iterator<Map.Entry<String, MediaRouteProvider.RouteController>> iter = |
| mRouteControllerMap.entrySet().iterator(); |
| while (iter.hasNext()) { |
| Map.Entry<String, MediaRouteProvider.RouteController> entry = iter.next(); |
| if (!idSet.contains(entry.getKey())) { |
| MediaRouteProvider.RouteController controller = entry.getValue(); |
| controller.onUnselect(UNSELECT_REASON_UNKNOWN); |
| controller.onRelease(); |
| iter.remove(); |
| } |
| } |
| // Select route controllers for the added routes. |
| for (MediaRouter.RouteInfo route : routes) { |
| if (!mRouteControllerMap.containsKey(route.mUniqueId)) { |
| MediaRouteProvider.RouteController controller = |
| route.getProviderInstance() |
| .onCreateRouteController( |
| route.mDescriptorId, mSelectedRoute.mDescriptorId); |
| controller.onSelect(); |
| mRouteControllerMap.put(route.mUniqueId, controller); |
| } |
| } |
| } |
| |
| /* package */ void notifyTransfer( |
| GlobalMediaRouter router, |
| MediaRouter.RouteInfo route, |
| @Nullable MediaRouteProvider.RouteController routeController, |
| @MediaRouter.UnselectReason int reason, |
| @Nullable MediaRouter.RouteInfo requestedRoute, |
| @Nullable |
| Collection< |
| MediaRouteProvider.DynamicGroupRouteController |
| .DynamicRouteDescriptor> |
| memberRoutes) { |
| if (mTransferNotifier != null) { |
| mTransferNotifier.cancel(); |
| mTransferNotifier = null; |
| } |
| mTransferNotifier = |
| new MediaRouter.PrepareTransferNotifier( |
| router, route, routeController, reason, requestedRoute, memberRoutes); |
| |
| if (mTransferNotifier.mReason != UNSELECT_REASON_ROUTE_CHANGED |
| || mOnPrepareTransferListener == null) { |
| mTransferNotifier.finishTransfer(); |
| } else { |
| ListenableFuture<Void> future = |
| mOnPrepareTransferListener.onPrepareTransfer( |
| mSelectedRoute, mTransferNotifier.mToRoute); |
| if (future == null) { |
| mTransferNotifier.finishTransfer(); |
| } else { |
| mTransferNotifier.setFuture(future); |
| } |
| } |
| } |
| |
| /* package */ MediaRouteProvider.DynamicGroupRouteController.OnDynamicRoutesChangedListener |
| mDynamicRoutesListener = |
| new MediaRouteProvider.DynamicGroupRouteController |
| .OnDynamicRoutesChangedListener() { |
| @Override |
| public void onRoutesChanged( |
| @NonNull MediaRouteProvider.DynamicGroupRouteController controller, |
| @Nullable MediaRouteDescriptor groupRouteDescriptor, |
| @NonNull |
| Collection< |
| MediaRouteProvider |
| .DynamicGroupRouteController |
| .DynamicRouteDescriptor> |
| routes) { |
| if (controller == mRequestedRouteController |
| && groupRouteDescriptor != null) { |
| MediaRouter.ProviderInfo provider = mRequestedRoute.getProvider(); |
| String groupId = groupRouteDescriptor.getId(); |
| |
| String uniqueId = assignRouteUniqueId(provider, groupId); |
| MediaRouter.RouteInfo route = |
| new MediaRouter.RouteInfo(provider, groupId, uniqueId); |
| route.maybeUpdateDescriptor(groupRouteDescriptor); |
| |
| if (mSelectedRoute == route) { |
| return; |
| } |
| |
| notifyTransfer( |
| GlobalMediaRouter.this, |
| route, |
| mRequestedRouteController, |
| UNSELECT_REASON_ROUTE_CHANGED, |
| mRequestedRoute, |
| routes); |
| |
| mRequestedRoute = null; |
| mRequestedRouteController = null; |
| } else if (controller == mSelectedRouteController) { |
| if (groupRouteDescriptor != null) { |
| updateRouteDescriptorAndNotify( |
| mSelectedRoute, groupRouteDescriptor); |
| } |
| mSelectedRoute.updateDynamicDescriptors(routes); |
| } |
| } |
| }; |
| |
| @Override |
| public void onSystemRouteSelectedByDescriptorId(@NonNull String id) { |
| // System route is selected, do not sync the route we selected before. |
| mCallbackHandler.removeMessages(CallbackHandler.MSG_ROUTE_SELECTED); |
| MediaRouter.ProviderInfo provider = findProviderInfo(mSystemProvider); |
| if (provider != null) { |
| MediaRouter.RouteInfo route = provider.findRouteByDescriptorId(id); |
| if (route != null) { |
| route.select(); |
| } |
| } |
| } |
| |
| /* package */ void addRemoteControlClient(android.media.RemoteControlClient rcc) { |
| int index = findRemoteControlClientRecord(rcc); |
| if (index < 0) { |
| RemoteControlClientRecord record = new RemoteControlClientRecord(rcc); |
| mRemoteControlClients.add(record); |
| } |
| } |
| |
| /* package */ void removeRemoteControlClient(android.media.RemoteControlClient rcc) { |
| int index = findRemoteControlClientRecord(rcc); |
| if (index >= 0) { |
| RemoteControlClientRecord record = mRemoteControlClients.remove(index); |
| record.disconnect(); |
| } |
| } |
| |
| /* package */ void setMediaSession(Object session) { |
| setMediaSessionRecord(session != null ? new MediaSessionRecord(session) : null); |
| } |
| |
| /* package */ void setMediaSessionCompat(final MediaSessionCompat session) { |
| mCompatSession = session; |
| if (Build.VERSION.SDK_INT >= 21) { |
| setMediaSessionRecord(session != null ? new MediaSessionRecord(session) : null); |
| } else { |
| if (mRccMediaSession != null) { |
| removeRemoteControlClient( |
| (android.media.RemoteControlClient) |
| mRccMediaSession.getRemoteControlClient()); |
| mRccMediaSession.removeOnActiveChangeListener(mSessionActiveListener); |
| } |
| mRccMediaSession = session; |
| if (session != null) { |
| session.addOnActiveChangeListener(mSessionActiveListener); |
| if (session.isActive()) { |
| addRemoteControlClient( |
| (android.media.RemoteControlClient) session.getRemoteControlClient()); |
| } |
| } |
| } |
| } |
| |
| private void setMediaSessionRecord(MediaSessionRecord mediaSessionRecord) { |
| if (mMediaSession != null) { |
| mMediaSession.clearVolumeHandling(); |
| } |
| mMediaSession = mediaSessionRecord; |
| if (mediaSessionRecord != null) { |
| updatePlaybackInfoFromSelectedRoute(); |
| } |
| } |
| |
| /* package */ MediaSessionCompat.Token getMediaSessionToken() { |
| if (mMediaSession != null) { |
| return mMediaSession.getToken(); |
| } else if (mCompatSession != null) { |
| return mCompatSession.getSessionToken(); |
| } |
| return null; |
| } |
| |
| private int findRemoteControlClientRecord(android.media.RemoteControlClient rcc) { |
| final int count = mRemoteControlClients.size(); |
| for (int i = 0; i < count; i++) { |
| RemoteControlClientRecord record = mRemoteControlClients.get(i); |
| if (record.getRemoteControlClient() == rcc) { |
| return i; |
| } |
| } |
| return -1; |
| } |
| |
| @SuppressLint("NewApi") |
| void updatePlaybackInfoFromSelectedRoute() { |
| if (mSelectedRoute != null) { |
| mPlaybackInfo.volume = mSelectedRoute.getVolume(); |
| mPlaybackInfo.volumeMax = mSelectedRoute.getVolumeMax(); |
| mPlaybackInfo.volumeHandling = mSelectedRoute.getVolumeHandling(); |
| mPlaybackInfo.playbackStream = mSelectedRoute.getPlaybackStream(); |
| mPlaybackInfo.playbackType = mSelectedRoute.getPlaybackType(); |
| if (isMediaTransferEnabled() && mSelectedRoute.getProviderInstance() == mMr2Provider) { |
| mPlaybackInfo.volumeControlId = |
| MediaRoute2Provider.getSessionIdForRouteController( |
| mSelectedRouteController); |
| } else { |
| mPlaybackInfo.volumeControlId = null; |
| } |
| |
| for (RemoteControlClientRecord remoteControlClientRecord : mRemoteControlClients) { |
| remoteControlClientRecord.updatePlaybackInfo(); |
| } |
| if (mMediaSession != null) { |
| if (mSelectedRoute == getDefaultRoute() || mSelectedRoute == getBluetoothRoute()) { |
| // Local route |
| mMediaSession.clearVolumeHandling(); |
| } else { |
| @VolumeProviderCompat.ControlType |
| int controlType = VolumeProviderCompat.VOLUME_CONTROL_FIXED; |
| if (mPlaybackInfo.volumeHandling |
| == MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE) { |
| controlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE; |
| } |
| mMediaSession.configureVolume( |
| controlType, |
| mPlaybackInfo.volumeMax, |
| mPlaybackInfo.volume, |
| mPlaybackInfo.volumeControlId); |
| } |
| } |
| } else { |
| if (mMediaSession != null) { |
| mMediaSession.clearVolumeHandling(); |
| } |
| } |
| } |
| |
| private final class ProviderCallback extends MediaRouteProvider.Callback { |
| ProviderCallback() { |
| } |
| |
| @Override |
| public void onDescriptorChanged( |
| @NonNull MediaRouteProvider provider, MediaRouteProviderDescriptor descriptor) { |
| updateProviderDescriptor(provider, descriptor); |
| } |
| } |
| |
| /* package */ final class Mr2ProviderCallback extends MediaRoute2Provider.Callback { |
| @Override |
| public void onSelectRoute( |
| @NonNull String routeDescriptorId, @MediaRouter.UnselectReason int reason) { |
| MediaRouter.RouteInfo routeToSelect = null; |
| for (MediaRouter.RouteInfo routeInfo : getRoutes()) { |
| if (routeInfo.getProviderInstance() != mMr2Provider) { |
| continue; |
| } |
| if (TextUtils.equals(routeDescriptorId, routeInfo.getDescriptorId())) { |
| routeToSelect = routeInfo; |
| break; |
| } |
| } |
| |
| if (routeToSelect == null) { |
| Log.w( |
| TAG, |
| "onSelectRoute: The target RouteInfo is not found for descriptorId=" |
| + routeDescriptorId); |
| return; |
| } |
| |
| selectRouteInternal(routeToSelect, reason); |
| } |
| |
| @Override |
| public void onSelectFallbackRoute(@MediaRouter.UnselectReason int reason) { |
| selectRouteToFallbackRoute(reason); |
| } |
| |
| @Override |
| public void onReleaseController(@NonNull MediaRouteProvider.RouteController controller) { |
| if (controller == mSelectedRouteController) { |
| // Stop casting |
| selectRouteToFallbackRoute(UNSELECT_REASON_STOPPED); |
| } else if (DEBUG) { |
| // 'Cast -> Phone' / 'Cast -> Cast(old)' cases triggered by selectRoute(). |
| // Nothing to do. |
| Log.d( |
| TAG, |
| "A RouteController unrelated to the selected route is released." |
| + " controller=" |
| + controller); |
| } |
| } |
| |
| /* package */ void selectRouteToFallbackRoute(@MediaRouter.UnselectReason int reason) { |
| MediaRouter.RouteInfo fallbackRoute = chooseFallbackRoute(); |
| if (getSelectedRoute() != fallbackRoute) { |
| selectRouteInternal(fallbackRoute, reason); |
| } |
| // Does nothing when the selected route is same with fallback route. |
| // This is the difference between this and unselect(). |
| } |
| } |
| |
| private final class MediaSessionRecord { |
| private final MediaSessionCompat mMsCompat; |
| |
| private @VolumeProviderCompat.ControlType int mControlType; |
| private int mMaxVolume; |
| private VolumeProviderCompat mVpCompat; |
| |
| MediaSessionRecord(Object mediaSession) { |
| this(MediaSessionCompat.fromMediaSession(mApplicationContext, mediaSession)); |
| } |
| |
| MediaSessionRecord(MediaSessionCompat mediaSessionCompat) { |
| mMsCompat = mediaSessionCompat; |
| } |
| |
| /* package */ void configureVolume( |
| @VolumeProviderCompat.ControlType int controlType, |
| int max, |
| int current, |
| @Nullable String volumeControlId) { |
| if (mMsCompat != null) { |
| if (mVpCompat != null && controlType == mControlType && max == mMaxVolume) { |
| // If we haven't changed control type or max just set the |
| // new current volume |
| mVpCompat.setCurrentVolume(current); |
| } else { |
| // Otherwise create a new provider and update |
| mVpCompat = |
| new VolumeProviderCompat(controlType, max, current, volumeControlId) { |
| @Override |
| public void onSetVolumeTo(final int volume) { |
| mCallbackHandler.post( |
| () -> { |
| if (mSelectedRoute != null) { |
| mSelectedRoute.requestSetVolume(volume); |
| } |
| }); |
| } |
| |
| @Override |
| public void onAdjustVolume(final int direction) { |
| mCallbackHandler.post( |
| () -> { |
| if (mSelectedRoute != null) { |
| mSelectedRoute.requestUpdateVolume(direction); |
| } |
| }); |
| } |
| }; |
| mMsCompat.setPlaybackToRemote(mVpCompat); |
| } |
| } |
| } |
| |
| /* package */ void clearVolumeHandling() { |
| if (mMsCompat != null) { |
| mMsCompat.setPlaybackToLocal(mPlaybackInfo.playbackStream); |
| mVpCompat = null; |
| } |
| } |
| |
| /* package */ MediaSessionCompat.Token getToken() { |
| if (mMsCompat != null) { |
| return mMsCompat.getSessionToken(); |
| } |
| return null; |
| } |
| } |
| |
| private final class RemoteControlClientRecord |
| implements RemoteControlClientCompat.VolumeCallback { |
| private final RemoteControlClientCompat mRccCompat; |
| private boolean mDisconnected; |
| |
| RemoteControlClientRecord(android.media.RemoteControlClient rcc) { |
| mRccCompat = RemoteControlClientCompat.obtain(mApplicationContext, rcc); |
| mRccCompat.setVolumeCallback(this); |
| updatePlaybackInfo(); |
| } |
| |
| /* package */ android.media.RemoteControlClient getRemoteControlClient() { |
| return mRccCompat.getRemoteControlClient(); |
| } |
| |
| /* package */ void disconnect() { |
| mDisconnected = true; |
| mRccCompat.setVolumeCallback(null); |
| } |
| |
| /* package */ void updatePlaybackInfo() { |
| mRccCompat.setPlaybackInfo(mPlaybackInfo); |
| } |
| |
| @Override |
| public void onVolumeSetRequest(int volume) { |
| if (!mDisconnected && mSelectedRoute != null) { |
| mSelectedRoute.requestSetVolume(volume); |
| } |
| } |
| |
| @Override |
| public void onVolumeUpdateRequest(int direction) { |
| if (!mDisconnected && mSelectedRoute != null) { |
| mSelectedRoute.requestUpdateVolume(direction); |
| } |
| } |
| } |
| |
| @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) |
| /* package */ final class CallbackHandler extends Handler { |
| private final ArrayList<MediaRouter.CallbackRecord> mTempCallbackRecords = |
| new ArrayList<>(); |
| private final List<MediaRouter.RouteInfo> mDynamicGroupRoutes = new ArrayList<>(); |
| |
| private static final int MSG_TYPE_MASK = 0xff00; |
| private static final int MSG_TYPE_ROUTE = 0x0100; |
| private static final int MSG_TYPE_PROVIDER = 0x0200; |
| private static final int MSG_TYPE_ROUTER = 0x0300; |
| |
| public static final int MSG_ROUTE_ADDED = MSG_TYPE_ROUTE | 1; |
| public static final int MSG_ROUTE_REMOVED = MSG_TYPE_ROUTE | 2; |
| public static final int MSG_ROUTE_CHANGED = MSG_TYPE_ROUTE | 3; |
| public static final int MSG_ROUTE_VOLUME_CHANGED = MSG_TYPE_ROUTE | 4; |
| public static final int MSG_ROUTE_PRESENTATION_DISPLAY_CHANGED = MSG_TYPE_ROUTE | 5; |
| public static final int MSG_ROUTE_SELECTED = MSG_TYPE_ROUTE | 6; |
| public static final int MSG_ROUTE_UNSELECTED = MSG_TYPE_ROUTE | 7; |
| public static final int MSG_ROUTE_ANOTHER_SELECTED = MSG_TYPE_ROUTE | 8; |
| |
| public static final int MSG_PROVIDER_ADDED = MSG_TYPE_PROVIDER | 1; |
| public static final int MSG_PROVIDER_REMOVED = MSG_TYPE_PROVIDER | 2; |
| public static final int MSG_PROVIDER_CHANGED = MSG_TYPE_PROVIDER | 3; |
| |
| public static final int MSG_ROUTER_PARAMS_CHANGED = MSG_TYPE_ROUTER | 1; |
| |
| CallbackHandler() { |
| } |
| |
| /* package */ void post(int msg, Object obj) { |
| obtainMessage(msg, obj).sendToTarget(); |
| } |
| |
| /* package */ void post(int msg, Object obj, int arg) { |
| Message message = obtainMessage(msg, obj); |
| message.arg1 = arg; |
| message.sendToTarget(); |
| } |
| |
| @Override |
| public void handleMessage(Message msg) { |
| final int what = msg.what; |
| final Object obj = msg.obj; |
| final int arg = msg.arg1; |
| |
| if (what == MSG_ROUTE_CHANGED |
| && getSelectedRoute().getId().equals(((MediaRouter.RouteInfo) obj).getId())) { |
| updateSelectedRouteIfNeeded(true); |
| } |
| |
| // Synchronize state with the system media router. |
| syncWithSystemProvider(what, obj); |
| |
| // Invoke all registered callbacks. |
| // Build a list of callbacks before invoking them in case callbacks |
| // are added or removed during dispatch. |
| try { |
| for (int i = mRouters.size(); --i >= 0; ) { |
| MediaRouter router = mRouters.get(i).get(); |
| if (router == null) { |
| mRouters.remove(i); |
| } else { |
| mTempCallbackRecords.addAll(router.mCallbackRecords); |
| } |
| } |
| |
| for (MediaRouter.CallbackRecord tempCallbackRecord : mTempCallbackRecords) { |
| invokeCallback(tempCallbackRecord, what, obj, arg); |
| } |
| } finally { |
| mTempCallbackRecords.clear(); |
| } |
| } |
| |
| // Using Pair<RouteInfo, RouteInfo> |
| @SuppressWarnings({"unchecked"}) |
| private void syncWithSystemProvider(int what, Object obj) { |
| switch (what) { |
| case MSG_ROUTE_ADDED: |
| mSystemProvider.onSyncRouteAdded((MediaRouter.RouteInfo) obj); |
| break; |
| case MSG_ROUTE_REMOVED: |
| mSystemProvider.onSyncRouteRemoved((MediaRouter.RouteInfo) obj); |
| break; |
| case MSG_ROUTE_CHANGED: |
| mSystemProvider.onSyncRouteChanged((MediaRouter.RouteInfo) obj); |
| break; |
| case MSG_ROUTE_SELECTED: { |
| MediaRouter.RouteInfo selectedRoute = |
| ((Pair<MediaRouter.RouteInfo, MediaRouter.RouteInfo>) obj).second; |
| mSystemProvider.onSyncRouteSelected(selectedRoute); |
| // TODO(b/166794092): Remove this nullness check |
| if (mDefaultRoute != null && selectedRoute.isDefaultOrBluetooth()) { |
| for (MediaRouter.RouteInfo prevGroupRoute : mDynamicGroupRoutes) { |
| mSystemProvider.onSyncRouteRemoved(prevGroupRoute); |
| } |
| mDynamicGroupRoutes.clear(); |
| } |
| break; |
| } |
| case MSG_ROUTE_ANOTHER_SELECTED: { |
| MediaRouter.RouteInfo groupRoute = |
| ((Pair<MediaRouter.RouteInfo, MediaRouter.RouteInfo>) obj).second; |
| mDynamicGroupRoutes.add(groupRoute); |
| mSystemProvider.onSyncRouteAdded(groupRoute); |
| mSystemProvider.onSyncRouteSelected(groupRoute); |
| break; |
| } |
| } |
| } |
| |
| @SuppressWarnings("unchecked") // Using Pair<RouteInfo, RouteInfo> |
| private void invokeCallback( |
| MediaRouter.CallbackRecord record, int what, Object obj, int arg) { |
| final MediaRouter router = record.mRouter; |
| final MediaRouter.Callback callback = record.mCallback; |
| switch (what & MSG_TYPE_MASK) { |
| case MSG_TYPE_ROUTE: { |
| final MediaRouter.RouteInfo route = |
| (what == MSG_ROUTE_ANOTHER_SELECTED || what == MSG_ROUTE_SELECTED) |
| ? ((Pair<MediaRouter.RouteInfo, MediaRouter.RouteInfo>) obj) |
| .second |
| : (MediaRouter.RouteInfo) obj; |
| final MediaRouter.RouteInfo optionalRoute = |
| (what == MSG_ROUTE_ANOTHER_SELECTED || what == MSG_ROUTE_SELECTED) |
| ? ((Pair<MediaRouter.RouteInfo, MediaRouter.RouteInfo>) obj) |
| .first |
| : null; |
| if (route == null |
| || !record.filterRouteEvent(route, what, optionalRoute, arg)) { |
| break; |
| } |
| switch (what) { |
| case MSG_ROUTE_ADDED: |
| callback.onRouteAdded(router, route); |
| break; |
| case MSG_ROUTE_REMOVED: |
| callback.onRouteRemoved(router, route); |
| break; |
| case MSG_ROUTE_CHANGED: |
| callback.onRouteChanged(router, route); |
| break; |
| case MSG_ROUTE_VOLUME_CHANGED: |
| callback.onRouteVolumeChanged(router, route); |
| break; |
| case MSG_ROUTE_PRESENTATION_DISPLAY_CHANGED: |
| callback.onRoutePresentationDisplayChanged(router, route); |
| break; |
| case MSG_ROUTE_SELECTED: |
| callback.onRouteSelected(router, route, arg, route); |
| break; |
| case MSG_ROUTE_UNSELECTED: |
| callback.onRouteUnselected(router, route, arg); |
| break; |
| case MSG_ROUTE_ANOTHER_SELECTED: |
| callback.onRouteSelected(router, route, arg, optionalRoute); |
| break; |
| } |
| break; |
| } |
| case MSG_TYPE_PROVIDER: { |
| final MediaRouter.ProviderInfo provider = (MediaRouter.ProviderInfo) obj; |
| switch (what) { |
| case MSG_PROVIDER_ADDED: |
| callback.onProviderAdded(router, provider); |
| break; |
| case MSG_PROVIDER_REMOVED: |
| callback.onProviderRemoved(router, provider); |
| break; |
| case MSG_PROVIDER_CHANGED: |
| callback.onProviderChanged(router, provider); |
| break; |
| } |
| break; |
| } |
| case MSG_TYPE_ROUTER: { |
| switch (what) { |
| case MSG_ROUTER_PARAMS_CHANGED: |
| final MediaRouterParams params = (MediaRouterParams) obj; |
| callback.onRouterParamsChanged(router, params); |
| break; |
| } |
| break; |
| } |
| } |
| } |
| } |
| } |