| /* |
| * Copyright (C) 2012 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 android.Manifest; |
| import android.annotation.DrawableRes; |
| import android.annotation.IntDef; |
| import android.annotation.NonNull; |
| import android.annotation.SystemService; |
| import android.app.ActivityThread; |
| import android.content.BroadcastReceiver; |
| import android.content.Context; |
| import android.content.Intent; |
| import android.content.IntentFilter; |
| import android.content.pm.PackageManager; |
| import android.content.res.Resources; |
| import android.graphics.drawable.Drawable; |
| import android.hardware.display.DisplayManager; |
| import android.hardware.display.WifiDisplay; |
| import android.hardware.display.WifiDisplayStatus; |
| import android.media.session.MediaSession; |
| import android.os.Handler; |
| import android.os.IBinder; |
| import android.os.Process; |
| import android.os.RemoteException; |
| import android.os.ServiceManager; |
| import android.os.UserHandle; |
| import android.text.TextUtils; |
| import android.util.Log; |
| import android.view.Display; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Objects; |
| import java.util.concurrent.CopyOnWriteArrayList; |
| |
| /** |
| * MediaRouter allows applications to control the routing of media channels |
| * and streams from the current device to external speakers and destination devices. |
| * |
| * <p>A MediaRouter is retrieved through {@link Context#getSystemService(String) |
| * Context.getSystemService()} of a {@link Context#MEDIA_ROUTER_SERVICE |
| * Context.MEDIA_ROUTER_SERVICE}. |
| * |
| * <p>The media router API is not thread-safe; all interactions with it must be |
| * done from the main thread of the process.</p> |
| */ |
| @SystemService(Context.MEDIA_ROUTER_SERVICE) |
| public class MediaRouter { |
| private static final String TAG = "MediaRouter"; |
| private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); |
| |
| static class Static implements DisplayManager.DisplayListener { |
| final String mPackageName; |
| final Resources mResources; |
| final IAudioService mAudioService; |
| final DisplayManager mDisplayService; |
| final IMediaRouterService mMediaRouterService; |
| final Handler mHandler; |
| final CopyOnWriteArrayList<CallbackInfo> mCallbacks = |
| new CopyOnWriteArrayList<CallbackInfo>(); |
| |
| final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>(); |
| final ArrayList<RouteCategory> mCategories = new ArrayList<RouteCategory>(); |
| |
| final RouteCategory mSystemCategory; |
| |
| final AudioRoutesInfo mCurAudioRoutesInfo = new AudioRoutesInfo(); |
| |
| RouteInfo mDefaultAudioVideo; |
| RouteInfo mBluetoothA2dpRoute; |
| |
| RouteInfo mSelectedRoute; |
| |
| final boolean mCanConfigureWifiDisplays; |
| boolean mActivelyScanningWifiDisplays; |
| String mPreviousActiveWifiDisplayAddress; |
| |
| int mDiscoveryRequestRouteTypes; |
| boolean mDiscoverRequestActiveScan; |
| |
| int mCurrentUserId = -1; |
| IMediaRouterClient mClient; |
| MediaRouterClientState mClientState; |
| |
| final IAudioRoutesObserver.Stub mAudioRoutesObserver = new IAudioRoutesObserver.Stub() { |
| @Override |
| public void dispatchAudioRoutesChanged(final AudioRoutesInfo newRoutes) { |
| mHandler.post(new Runnable() { |
| @Override public void run() { |
| updateAudioRoutes(newRoutes); |
| } |
| }); |
| } |
| }; |
| |
| Static(Context appContext) { |
| mPackageName = appContext.getPackageName(); |
| mResources = appContext.getResources(); |
| mHandler = new Handler(appContext.getMainLooper()); |
| |
| IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE); |
| mAudioService = IAudioService.Stub.asInterface(b); |
| |
| mDisplayService = (DisplayManager) appContext.getSystemService(Context.DISPLAY_SERVICE); |
| |
| mMediaRouterService = IMediaRouterService.Stub.asInterface( |
| ServiceManager.getService(Context.MEDIA_ROUTER_SERVICE)); |
| |
| mSystemCategory = new RouteCategory( |
| com.android.internal.R.string.default_audio_route_category_name, |
| ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO, false); |
| mSystemCategory.mIsSystem = true; |
| |
| // Only the system can configure wifi displays. The display manager |
| // enforces this with a permission check. Set a flag here so that we |
| // know whether this process is actually allowed to scan and connect. |
| mCanConfigureWifiDisplays = appContext.checkPermission( |
| Manifest.permission.CONFIGURE_WIFI_DISPLAY, |
| Process.myPid(), Process.myUid()) == PackageManager.PERMISSION_GRANTED; |
| } |
| |
| // Called after sStatic is initialized |
| void startMonitoringRoutes(Context appContext) { |
| mDefaultAudioVideo = new RouteInfo(mSystemCategory); |
| mDefaultAudioVideo.mNameResId = com.android.internal.R.string.default_audio_route_name; |
| mDefaultAudioVideo.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO; |
| mDefaultAudioVideo.updatePresentationDisplay(); |
| if (((AudioManager) appContext.getSystemService(Context.AUDIO_SERVICE)) |
| .isVolumeFixed()) { |
| mDefaultAudioVideo.mVolumeHandling = RouteInfo.PLAYBACK_VOLUME_FIXED; |
| } |
| |
| addRouteStatic(mDefaultAudioVideo); |
| |
| // This will select the active wifi display route if there is one. |
| updateWifiDisplayStatus(mDisplayService.getWifiDisplayStatus()); |
| |
| appContext.registerReceiver(new WifiDisplayStatusChangedReceiver(), |
| new IntentFilter(DisplayManager.ACTION_WIFI_DISPLAY_STATUS_CHANGED)); |
| appContext.registerReceiver(new VolumeChangeReceiver(), |
| new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION)); |
| |
| mDisplayService.registerDisplayListener(this, mHandler); |
| |
| AudioRoutesInfo newAudioRoutes = null; |
| try { |
| newAudioRoutes = mAudioService.startWatchingRoutes(mAudioRoutesObserver); |
| } catch (RemoteException e) { |
| } |
| if (newAudioRoutes != null) { |
| // This will select the active BT route if there is one and the current |
| // selected route is the default system route, or if there is no selected |
| // route yet. |
| updateAudioRoutes(newAudioRoutes); |
| } |
| |
| // Bind to the media router service. |
| rebindAsUser(UserHandle.myUserId()); |
| |
| // Select the default route if the above didn't sync us up |
| // appropriately with relevant system state. |
| if (mSelectedRoute == null) { |
| selectDefaultRouteStatic(); |
| } |
| } |
| |
| void updateAudioRoutes(AudioRoutesInfo newRoutes) { |
| boolean audioRoutesChanged = false; |
| boolean forceUseDefaultRoute = false; |
| |
| if (newRoutes.mainType != mCurAudioRoutesInfo.mainType) { |
| mCurAudioRoutesInfo.mainType = newRoutes.mainType; |
| int name; |
| if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADPHONES) != 0 |
| || (newRoutes.mainType & AudioRoutesInfo.MAIN_HEADSET) != 0) { |
| name = com.android.internal.R.string.default_audio_route_name_headphones; |
| } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) { |
| name = com.android.internal.R.string.default_audio_route_name_dock_speakers; |
| } else if ((newRoutes.mainType&AudioRoutesInfo.MAIN_HDMI) != 0) { |
| name = com.android.internal.R.string.default_audio_route_name_hdmi; |
| } else if ((newRoutes.mainType&AudioRoutesInfo.MAIN_USB) != 0) { |
| name = com.android.internal.R.string.default_audio_route_name_usb; |
| } else { |
| name = com.android.internal.R.string.default_audio_route_name; |
| } |
| mDefaultAudioVideo.mNameResId = name; |
| dispatchRouteChanged(mDefaultAudioVideo); |
| |
| if ((newRoutes.mainType & (AudioRoutesInfo.MAIN_HEADSET |
| | AudioRoutesInfo.MAIN_HEADPHONES | AudioRoutesInfo.MAIN_USB)) != 0) { |
| forceUseDefaultRoute = true; |
| } |
| audioRoutesChanged = true; |
| } |
| |
| if (!TextUtils.equals(newRoutes.bluetoothName, mCurAudioRoutesInfo.bluetoothName)) { |
| forceUseDefaultRoute = false; |
| mCurAudioRoutesInfo.bluetoothName = newRoutes.bluetoothName; |
| if (mCurAudioRoutesInfo.bluetoothName != null) { |
| if (mBluetoothA2dpRoute == null) { |
| // BT connected |
| final RouteInfo info = new RouteInfo(mSystemCategory); |
| info.mName = mCurAudioRoutesInfo.bluetoothName; |
| info.mDescription = mResources.getText( |
| com.android.internal.R.string.bluetooth_a2dp_audio_route_name); |
| info.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO; |
| info.mDeviceType = RouteInfo.DEVICE_TYPE_BLUETOOTH; |
| mBluetoothA2dpRoute = info; |
| addRouteStatic(mBluetoothA2dpRoute); |
| } else { |
| mBluetoothA2dpRoute.mName = mCurAudioRoutesInfo.bluetoothName; |
| dispatchRouteChanged(mBluetoothA2dpRoute); |
| } |
| } else if (mBluetoothA2dpRoute != null) { |
| // BT disconnected |
| removeRouteStatic(mBluetoothA2dpRoute); |
| mBluetoothA2dpRoute = null; |
| } |
| audioRoutesChanged = true; |
| } |
| |
| if (audioRoutesChanged) { |
| Log.v(TAG, "Audio routes updated: " + newRoutes + ", a2dp=" + isBluetoothA2dpOn()); |
| if (mSelectedRoute == null || mSelectedRoute == mDefaultAudioVideo |
| || mSelectedRoute == mBluetoothA2dpRoute) { |
| if (forceUseDefaultRoute || mBluetoothA2dpRoute == null) { |
| selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO, mDefaultAudioVideo, false); |
| } else { |
| selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO, mBluetoothA2dpRoute, false); |
| } |
| } |
| } |
| } |
| |
| boolean isBluetoothA2dpOn() { |
| try { |
| return mBluetoothA2dpRoute != null && mAudioService.isBluetoothA2dpOn(); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Error querying Bluetooth A2DP state", e); |
| return false; |
| } |
| } |
| |
| void updateDiscoveryRequest() { |
| // What are we looking for today? |
| int routeTypes = 0; |
| int passiveRouteTypes = 0; |
| boolean activeScan = false; |
| boolean activeScanWifiDisplay = false; |
| final int count = mCallbacks.size(); |
| for (int i = 0; i < count; i++) { |
| CallbackInfo cbi = mCallbacks.get(i); |
| if ((cbi.flags & (CALLBACK_FLAG_PERFORM_ACTIVE_SCAN |
| | CALLBACK_FLAG_REQUEST_DISCOVERY)) != 0) { |
| // Discovery explicitly requested. |
| routeTypes |= cbi.type; |
| } else if ((cbi.flags & CALLBACK_FLAG_PASSIVE_DISCOVERY) != 0) { |
| // Discovery only passively requested. |
| passiveRouteTypes |= cbi.type; |
| } else { |
| // Legacy case since applications don't specify the discovery flag. |
| // Unfortunately we just have to assume they always need discovery |
| // whenever they have a callback registered. |
| routeTypes |= cbi.type; |
| } |
| if ((cbi.flags & CALLBACK_FLAG_PERFORM_ACTIVE_SCAN) != 0) { |
| activeScan = true; |
| if ((cbi.type & ROUTE_TYPE_REMOTE_DISPLAY) != 0) { |
| activeScanWifiDisplay = true; |
| } |
| } |
| } |
| if (routeTypes != 0 || activeScan) { |
| // If someone else requests discovery then enable the passive listeners. |
| // This is used by the MediaRouteButton and MediaRouteActionProvider since |
| // they don't receive lifecycle callbacks from the Activity. |
| routeTypes |= passiveRouteTypes; |
| } |
| |
| // Update wifi display scanning. |
| // TODO: All of this should be managed by the media router service. |
| if (mCanConfigureWifiDisplays) { |
| if (mSelectedRoute != null |
| && mSelectedRoute.matchesTypes(ROUTE_TYPE_REMOTE_DISPLAY)) { |
| // Don't scan while already connected to a remote display since |
| // it may interfere with the ongoing transmission. |
| activeScanWifiDisplay = false; |
| } |
| if (activeScanWifiDisplay) { |
| if (!mActivelyScanningWifiDisplays) { |
| mActivelyScanningWifiDisplays = true; |
| mDisplayService.startWifiDisplayScan(); |
| } |
| } else { |
| if (mActivelyScanningWifiDisplays) { |
| mActivelyScanningWifiDisplays = false; |
| mDisplayService.stopWifiDisplayScan(); |
| } |
| } |
| } |
| |
| // Tell the media router service all about it. |
| if (routeTypes != mDiscoveryRequestRouteTypes |
| || activeScan != mDiscoverRequestActiveScan) { |
| mDiscoveryRequestRouteTypes = routeTypes; |
| mDiscoverRequestActiveScan = activeScan; |
| publishClientDiscoveryRequest(); |
| } |
| } |
| |
| @Override |
| public void onDisplayAdded(int displayId) { |
| updatePresentationDisplays(displayId); |
| } |
| |
| @Override |
| public void onDisplayChanged(int displayId) { |
| updatePresentationDisplays(displayId); |
| } |
| |
| @Override |
| public void onDisplayRemoved(int displayId) { |
| updatePresentationDisplays(displayId); |
| } |
| |
| public Display[] getAllPresentationDisplays() { |
| return mDisplayService.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION); |
| } |
| |
| private void updatePresentationDisplays(int changedDisplayId) { |
| final int count = mRoutes.size(); |
| for (int i = 0; i < count; i++) { |
| final RouteInfo route = mRoutes.get(i); |
| if (route.updatePresentationDisplay() || (route.mPresentationDisplay != null |
| && route.mPresentationDisplay.getDisplayId() == changedDisplayId)) { |
| dispatchRoutePresentationDisplayChanged(route); |
| } |
| } |
| } |
| |
| void setSelectedRoute(RouteInfo info, boolean explicit) { |
| // Must be non-reentrant. |
| mSelectedRoute = info; |
| publishClientSelectedRoute(explicit); |
| } |
| |
| void rebindAsUser(int userId) { |
| if (mCurrentUserId != userId || userId < 0 || mClient == null) { |
| if (mClient != null) { |
| try { |
| mMediaRouterService.unregisterClient(mClient); |
| } catch (RemoteException ex) { |
| Log.e(TAG, "Unable to unregister media router client.", ex); |
| } |
| mClient = null; |
| } |
| |
| mCurrentUserId = userId; |
| |
| try { |
| Client client = new Client(); |
| mMediaRouterService.registerClientAsUser(client, mPackageName, userId); |
| mClient = client; |
| } catch (RemoteException ex) { |
| Log.e(TAG, "Unable to register media router client.", ex); |
| } |
| |
| publishClientDiscoveryRequest(); |
| publishClientSelectedRoute(false); |
| updateClientState(); |
| } |
| } |
| |
| void publishClientDiscoveryRequest() { |
| if (mClient != null) { |
| try { |
| mMediaRouterService.setDiscoveryRequest(mClient, |
| mDiscoveryRequestRouteTypes, mDiscoverRequestActiveScan); |
| } catch (RemoteException ex) { |
| Log.e(TAG, "Unable to publish media router client discovery request.", ex); |
| } |
| } |
| } |
| |
| void publishClientSelectedRoute(boolean explicit) { |
| if (mClient != null) { |
| try { |
| mMediaRouterService.setSelectedRoute(mClient, |
| mSelectedRoute != null ? mSelectedRoute.mGlobalRouteId : null, |
| explicit); |
| } catch (RemoteException ex) { |
| Log.e(TAG, "Unable to publish media router client selected route.", ex); |
| } |
| } |
| } |
| |
| void updateClientState() { |
| // Update the client state. |
| mClientState = null; |
| if (mClient != null) { |
| try { |
| mClientState = mMediaRouterService.getState(mClient); |
| } catch (RemoteException ex) { |
| Log.e(TAG, "Unable to retrieve media router client state.", ex); |
| } |
| } |
| final ArrayList<MediaRouterClientState.RouteInfo> globalRoutes = |
| mClientState != null ? mClientState.routes : null; |
| |
| // Add or update routes. |
| final int globalRouteCount = globalRoutes != null ? globalRoutes.size() : 0; |
| for (int i = 0; i < globalRouteCount; i++) { |
| final MediaRouterClientState.RouteInfo globalRoute = globalRoutes.get(i); |
| RouteInfo route = findGlobalRoute(globalRoute.id); |
| if (route == null) { |
| route = makeGlobalRoute(globalRoute); |
| addRouteStatic(route); |
| } else { |
| updateGlobalRoute(route, globalRoute); |
| } |
| } |
| |
| // Remove defunct routes. |
| outer: for (int i = mRoutes.size(); i-- > 0; ) { |
| final RouteInfo route = mRoutes.get(i); |
| final String globalRouteId = route.mGlobalRouteId; |
| if (globalRouteId != null) { |
| for (int j = 0; j < globalRouteCount; j++) { |
| MediaRouterClientState.RouteInfo globalRoute = globalRoutes.get(j); |
| if (globalRouteId.equals(globalRoute.id)) { |
| continue outer; // found |
| } |
| } |
| // not found |
| removeRouteStatic(route); |
| } |
| } |
| } |
| |
| void requestSetVolume(RouteInfo route, int volume) { |
| if (route.mGlobalRouteId != null && mClient != null) { |
| try { |
| mMediaRouterService.requestSetVolume(mClient, |
| route.mGlobalRouteId, volume); |
| } catch (RemoteException ex) { |
| Log.w(TAG, "Unable to request volume change.", ex); |
| } |
| } |
| } |
| |
| void requestUpdateVolume(RouteInfo route, int direction) { |
| if (route.mGlobalRouteId != null && mClient != null) { |
| try { |
| mMediaRouterService.requestUpdateVolume(mClient, |
| route.mGlobalRouteId, direction); |
| } catch (RemoteException ex) { |
| Log.w(TAG, "Unable to request volume change.", ex); |
| } |
| } |
| } |
| |
| RouteInfo makeGlobalRoute(MediaRouterClientState.RouteInfo globalRoute) { |
| RouteInfo route = new RouteInfo(mSystemCategory); |
| route.mGlobalRouteId = globalRoute.id; |
| route.mName = globalRoute.name; |
| route.mDescription = globalRoute.description; |
| route.mSupportedTypes = globalRoute.supportedTypes; |
| route.mDeviceType = globalRoute.deviceType; |
| route.mEnabled = globalRoute.enabled; |
| route.setRealStatusCode(globalRoute.statusCode); |
| route.mPlaybackType = globalRoute.playbackType; |
| route.mPlaybackStream = globalRoute.playbackStream; |
| route.mVolume = globalRoute.volume; |
| route.mVolumeMax = globalRoute.volumeMax; |
| route.mVolumeHandling = globalRoute.volumeHandling; |
| route.mPresentationDisplayId = globalRoute.presentationDisplayId; |
| route.updatePresentationDisplay(); |
| return route; |
| } |
| |
| void updateGlobalRoute(RouteInfo route, MediaRouterClientState.RouteInfo globalRoute) { |
| boolean changed = false; |
| boolean volumeChanged = false; |
| boolean presentationDisplayChanged = false; |
| |
| if (!Objects.equals(route.mName, globalRoute.name)) { |
| route.mName = globalRoute.name; |
| changed = true; |
| } |
| if (!Objects.equals(route.mDescription, globalRoute.description)) { |
| route.mDescription = globalRoute.description; |
| changed = true; |
| } |
| final int oldSupportedTypes = route.mSupportedTypes; |
| if (oldSupportedTypes != globalRoute.supportedTypes) { |
| route.mSupportedTypes = globalRoute.supportedTypes; |
| changed = true; |
| } |
| if (route.mEnabled != globalRoute.enabled) { |
| route.mEnabled = globalRoute.enabled; |
| changed = true; |
| } |
| if (route.mRealStatusCode != globalRoute.statusCode) { |
| route.setRealStatusCode(globalRoute.statusCode); |
| changed = true; |
| } |
| if (route.mPlaybackType != globalRoute.playbackType) { |
| route.mPlaybackType = globalRoute.playbackType; |
| changed = true; |
| } |
| if (route.mPlaybackStream != globalRoute.playbackStream) { |
| route.mPlaybackStream = globalRoute.playbackStream; |
| changed = true; |
| } |
| if (route.mVolume != globalRoute.volume) { |
| route.mVolume = globalRoute.volume; |
| changed = true; |
| volumeChanged = true; |
| } |
| if (route.mVolumeMax != globalRoute.volumeMax) { |
| route.mVolumeMax = globalRoute.volumeMax; |
| changed = true; |
| volumeChanged = true; |
| } |
| if (route.mVolumeHandling != globalRoute.volumeHandling) { |
| route.mVolumeHandling = globalRoute.volumeHandling; |
| changed = true; |
| volumeChanged = true; |
| } |
| if (route.mPresentationDisplayId != globalRoute.presentationDisplayId) { |
| route.mPresentationDisplayId = globalRoute.presentationDisplayId; |
| route.updatePresentationDisplay(); |
| changed = true; |
| presentationDisplayChanged = true; |
| } |
| |
| if (changed) { |
| dispatchRouteChanged(route, oldSupportedTypes); |
| } |
| if (volumeChanged) { |
| dispatchRouteVolumeChanged(route); |
| } |
| if (presentationDisplayChanged) { |
| dispatchRoutePresentationDisplayChanged(route); |
| } |
| } |
| |
| RouteInfo findGlobalRoute(String globalRouteId) { |
| final int count = mRoutes.size(); |
| for (int i = 0; i < count; i++) { |
| final RouteInfo route = mRoutes.get(i); |
| if (globalRouteId.equals(route.mGlobalRouteId)) { |
| return route; |
| } |
| } |
| return null; |
| } |
| |
| boolean isPlaybackActive() { |
| if (mClient != null) { |
| try { |
| return mMediaRouterService.isPlaybackActive(mClient); |
| } catch (RemoteException ex) { |
| Log.e(TAG, "Unable to retrieve playback active state.", ex); |
| } |
| } |
| return false; |
| } |
| |
| final class Client extends IMediaRouterClient.Stub { |
| @Override |
| public void onStateChanged() { |
| mHandler.post(new Runnable() { |
| @Override |
| public void run() { |
| if (Client.this == mClient) { |
| updateClientState(); |
| } |
| } |
| }); |
| } |
| |
| @Override |
| public void onRestoreRoute() { |
| // Skip restoring route if the selected route is not a system audio route, or |
| // MediaRouter is initializing. |
| if ((mSelectedRoute != mDefaultAudioVideo && mSelectedRoute != mBluetoothA2dpRoute) |
| || mSelectedRoute == null) { |
| return; |
| } |
| Log.v(TAG, "onRestoreRoute() : a2dp=" + isBluetoothA2dpOn()); |
| mSelectedRoute.select(); |
| } |
| } |
| } |
| |
| static Static sStatic; |
| |
| /** |
| * Route type flag for live audio. |
| * |
| * <p>A device that supports live audio routing will allow the media audio stream |
| * to be routed to supported destinations. This can include internal speakers or |
| * audio jacks on the device itself, A2DP devices, and more.</p> |
| * |
| * <p>Once initiated this routing is transparent to the application. All audio |
| * played on the media stream will be routed to the selected destination.</p> |
| */ |
| public static final int ROUTE_TYPE_LIVE_AUDIO = 1 << 0; |
| |
| /** |
| * Route type flag for live video. |
| * |
| * <p>A device that supports live video routing will allow a mirrored version |
| * of the device's primary display or a customized |
| * {@link android.app.Presentation Presentation} to be routed to supported destinations.</p> |
| * |
| * <p>Once initiated, display mirroring is transparent to the application. |
| * While remote routing is active the application may use a |
| * {@link android.app.Presentation Presentation} to replace the mirrored view |
| * on the external display with different content.</p> |
| * |
| * @see RouteInfo#getPresentationDisplay() |
| * @see android.app.Presentation |
| */ |
| public static final int ROUTE_TYPE_LIVE_VIDEO = 1 << 1; |
| |
| /** |
| * Temporary interop constant to identify remote displays. |
| * @hide To be removed when media router API is updated. |
| */ |
| public static final int ROUTE_TYPE_REMOTE_DISPLAY = 1 << 2; |
| |
| /** |
| * Route type flag for application-specific usage. |
| * |
| * <p>Unlike other media route types, user routes are managed by the application. |
| * The MediaRouter will manage and dispatch events for user routes, but the application |
| * is expected to interpret the meaning of these events and perform the requested |
| * routing tasks.</p> |
| */ |
| public static final int ROUTE_TYPE_USER = 1 << 23; |
| |
| static final int ROUTE_TYPE_ANY = ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO |
| | ROUTE_TYPE_REMOTE_DISPLAY | ROUTE_TYPE_USER; |
| |
| /** |
| * Flag for {@link #addCallback}: Actively scan for routes while this callback |
| * is registered. |
| * <p> |
| * When this flag is specified, the media router will actively scan for new |
| * routes. Certain routes, such as wifi display routes, may not be discoverable |
| * except when actively scanning. This flag is typically used when the route picker |
| * dialog has been opened by the user to ensure that the route information is |
| * up to date. |
| * </p><p> |
| * Active scanning may consume a significant amount of power and may have intrusive |
| * effects on wireless connectivity. Therefore it is important that active scanning |
| * only be requested when it is actually needed to satisfy a user request to |
| * discover and select a new route. |
| * </p> |
| */ |
| public static final int CALLBACK_FLAG_PERFORM_ACTIVE_SCAN = 1 << 0; |
| |
| /** |
| * Flag for {@link #addCallback}: Do not filter route events. |
| * <p> |
| * When this flag is specified, the callback will be invoked for event that affect any |
| * route even if they do not match the callback's filter. |
| * </p> |
| */ |
| public static final int CALLBACK_FLAG_UNFILTERED_EVENTS = 1 << 1; |
| |
| /** |
| * Explicitly requests discovery. |
| * |
| * @hide Future API ported from support library. Revisit this later. |
| */ |
| public static final int CALLBACK_FLAG_REQUEST_DISCOVERY = 1 << 2; |
| |
| /** |
| * Requests that discovery be performed but only if there is some other active |
| * callback already registered. |
| * |
| * @hide Compatibility workaround for the fact that applications do not currently |
| * request discovery explicitly (except when using the support library API). |
| */ |
| public static final int CALLBACK_FLAG_PASSIVE_DISCOVERY = 1 << 3; |
| |
| /** |
| * Flag for {@link #isRouteAvailable}: Ignore the default route. |
| * <p> |
| * This flag is used to determine whether a matching non-default route is available. |
| * This constraint may be used to decide whether to offer the route chooser dialog |
| * to the user. There is no point offering the chooser if there are no |
| * non-default choices. |
| * </p> |
| * |
| * @hide Future API ported from support library. Revisit this later. |
| */ |
| public static final int AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE = 1 << 0; |
| |
| // Maps application contexts |
| static final HashMap<Context, MediaRouter> sRouters = new HashMap<Context, MediaRouter>(); |
| |
| static String typesToString(int types) { |
| final StringBuilder result = new StringBuilder(); |
| if ((types & ROUTE_TYPE_LIVE_AUDIO) != 0) { |
| result.append("ROUTE_TYPE_LIVE_AUDIO "); |
| } |
| if ((types & ROUTE_TYPE_LIVE_VIDEO) != 0) { |
| result.append("ROUTE_TYPE_LIVE_VIDEO "); |
| } |
| if ((types & ROUTE_TYPE_REMOTE_DISPLAY) != 0) { |
| result.append("ROUTE_TYPE_REMOTE_DISPLAY "); |
| } |
| if ((types & ROUTE_TYPE_USER) != 0) { |
| result.append("ROUTE_TYPE_USER "); |
| } |
| return result.toString(); |
| } |
| |
| /** @hide */ |
| public MediaRouter(Context context) { |
| synchronized (Static.class) { |
| if (sStatic == null) { |
| final Context appContext = context.getApplicationContext(); |
| sStatic = new Static(appContext); |
| sStatic.startMonitoringRoutes(appContext); |
| } |
| } |
| } |
| |
| /** |
| * Gets the default route for playing media content on the system. |
| * <p> |
| * The system always provides a default route. |
| * </p> |
| * |
| * @return The default route, which is guaranteed to never be null. |
| */ |
| public RouteInfo getDefaultRoute() { |
| return sStatic.mDefaultAudioVideo; |
| } |
| |
| /** |
| * Returns a Bluetooth route if available, otherwise the default route. |
| * @hide |
| */ |
| public RouteInfo getFallbackRoute() { |
| return (sStatic.mBluetoothA2dpRoute != null) |
| ? sStatic.mBluetoothA2dpRoute : sStatic.mDefaultAudioVideo; |
| } |
| |
| /** |
| * @hide for use by framework routing UI |
| */ |
| public RouteCategory getSystemCategory() { |
| return sStatic.mSystemCategory; |
| } |
| |
| /** @hide */ |
| public RouteInfo getSelectedRoute() { |
| return getSelectedRoute(ROUTE_TYPE_ANY); |
| } |
| |
| /** |
| * Return the currently selected route for any of the given types |
| * |
| * @param type route types |
| * @return the selected route |
| */ |
| public RouteInfo getSelectedRoute(int type) { |
| if (sStatic.mSelectedRoute != null && |
| (sStatic.mSelectedRoute.mSupportedTypes & type) != 0) { |
| // If the selected route supports any of the types supplied, it's still considered |
| // 'selected' for that type. |
| return sStatic.mSelectedRoute; |
| } else if (type == ROUTE_TYPE_USER) { |
| // The caller specifically asked for a user route and the currently selected route |
| // doesn't qualify. |
| return null; |
| } |
| // If the above didn't match and we're not specifically asking for a user route, |
| // consider the default selected. |
| return sStatic.mDefaultAudioVideo; |
| } |
| |
| /** |
| * Returns true if there is a route that matches the specified types. |
| * <p> |
| * This method returns true if there are any available routes that match the types |
| * regardless of whether they are enabled or disabled. If the |
| * {@link #AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE} flag is specified, then |
| * the method will only consider non-default routes. |
| * </p> |
| * |
| * @param types The types to match. |
| * @param flags Flags to control the determination of whether a route may be available. |
| * May be zero or {@link #AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE}. |
| * @return True if a matching route may be available. |
| * |
| * @hide Future API ported from support library. Revisit this later. |
| */ |
| public boolean isRouteAvailable(int types, int flags) { |
| final int count = sStatic.mRoutes.size(); |
| for (int i = 0; i < count; i++) { |
| RouteInfo route = sStatic.mRoutes.get(i); |
| if (route.matchesTypes(types)) { |
| if ((flags & AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE) == 0 |
| || route != sStatic.mDefaultAudioVideo) { |
| return true; |
| } |
| } |
| } |
| |
| // It doesn't look like we can find a matching route right now. |
| return false; |
| } |
| |
| /** |
| * Add a callback to listen to events about specific kinds of media routes. |
| * If the specified callback is already registered, its registration will be updated for any |
| * additional route types specified. |
| * <p> |
| * This is a convenience method that has the same effect as calling |
| * {@link #addCallback(int, Callback, int)} without flags. |
| * </p> |
| * |
| * @param types Types of routes this callback is interested in |
| * @param cb Callback to add |
| */ |
| public void addCallback(int types, Callback cb) { |
| addCallback(types, cb, 0); |
| } |
| |
| /** |
| * Add a callback to listen to events about specific kinds of media routes. |
| * If the specified callback is already registered, its registration will be updated for any |
| * additional route types specified. |
| * <p> |
| * By default, the callback will only be invoked for events that affect routes |
| * that match the specified selector. The filtering may be disabled by specifying |
| * the {@link #CALLBACK_FLAG_UNFILTERED_EVENTS} flag. |
| * </p> |
| * |
| * @param types Types of routes this callback is interested in |
| * @param cb Callback to add |
| * @param flags Flags to control the behavior of the callback. |
| * May be zero or a combination of {@link #CALLBACK_FLAG_PERFORM_ACTIVE_SCAN} and |
| * {@link #CALLBACK_FLAG_UNFILTERED_EVENTS}. |
| */ |
| public void addCallback(int types, Callback cb, int flags) { |
| CallbackInfo info; |
| int index = findCallbackInfo(cb); |
| if (index >= 0) { |
| info = sStatic.mCallbacks.get(index); |
| info.type |= types; |
| info.flags |= flags; |
| } else { |
| info = new CallbackInfo(cb, types, flags, this); |
| sStatic.mCallbacks.add(info); |
| } |
| sStatic.updateDiscoveryRequest(); |
| } |
| |
| /** |
| * Remove the specified callback. It will no longer receive events about media routing. |
| * |
| * @param cb Callback to remove |
| */ |
| public void removeCallback(Callback cb) { |
| int index = findCallbackInfo(cb); |
| if (index >= 0) { |
| sStatic.mCallbacks.remove(index); |
| sStatic.updateDiscoveryRequest(); |
| } else { |
| Log.w(TAG, "removeCallback(" + cb + "): callback not registered"); |
| } |
| } |
| |
| private int findCallbackInfo(Callback cb) { |
| final int count = sStatic.mCallbacks.size(); |
| for (int i = 0; i < count; i++) { |
| final CallbackInfo info = sStatic.mCallbacks.get(i); |
| if (info.cb == cb) { |
| return i; |
| } |
| } |
| return -1; |
| } |
| |
| /** |
| * Select the specified route to use for output of the given media types. |
| * <p class="note"> |
| * As API version 18, this function may be used to select any route. |
| * In prior versions, this function could only be used to select user |
| * routes and would ignore any attempt to select a system route. |
| * </p> |
| * |
| * @param types type flags indicating which types this route should be used for. |
| * The route must support at least a subset. |
| * @param route Route to select |
| * @throws IllegalArgumentException if the given route is {@code null} |
| */ |
| public void selectRoute(int types, @NonNull RouteInfo route) { |
| if (route == null) { |
| throw new IllegalArgumentException("Route cannot be null."); |
| } |
| selectRouteStatic(types, route, true); |
| } |
| |
| /** |
| * @hide internal use |
| */ |
| public void selectRouteInt(int types, RouteInfo route, boolean explicit) { |
| selectRouteStatic(types, route, explicit); |
| } |
| |
| static void selectRouteStatic(int types, @NonNull RouteInfo route, boolean explicit) { |
| Log.v(TAG, "Selecting route: " + route); |
| assert(route != null); |
| final RouteInfo oldRoute = sStatic.mSelectedRoute; |
| final RouteInfo currentSystemRoute = sStatic.isBluetoothA2dpOn() |
| ? sStatic.mBluetoothA2dpRoute : sStatic.mDefaultAudioVideo; |
| boolean wasDefaultOrBluetoothRoute = (oldRoute == sStatic.mDefaultAudioVideo |
| || oldRoute == sStatic.mBluetoothA2dpRoute); |
| if (oldRoute == route |
| && (!wasDefaultOrBluetoothRoute || route == currentSystemRoute)) { |
| return; |
| } |
| if (!route.matchesTypes(types)) { |
| Log.w(TAG, "selectRoute ignored; cannot select route with supported types " + |
| typesToString(route.getSupportedTypes()) + " into route types " + |
| typesToString(types)); |
| return; |
| } |
| |
| final RouteInfo btRoute = sStatic.mBluetoothA2dpRoute; |
| if (sStatic.isPlaybackActive() && btRoute != null && (types & ROUTE_TYPE_LIVE_AUDIO) != 0 |
| && (route == btRoute || route == sStatic.mDefaultAudioVideo)) { |
| try { |
| sStatic.mAudioService.setBluetoothA2dpOn(route == btRoute); |
| // TODO: Remove the following logging when no longer needed. |
| if (route != btRoute) { |
| StackTraceElement[] callStack = Thread.currentThread().getStackTrace(); |
| StringBuffer sb = new StringBuffer(); |
| // callStack[3] is the caller of this method. |
| for (int i = 3; i < callStack.length; i++) { |
| StackTraceElement caller = callStack[i]; |
| sb.append(caller.getClassName() + "." + caller.getMethodName() |
| + ":" + caller.getLineNumber()).append(" "); |
| } |
| Log.w(TAG, "Default route is selected while a BT route is available: pkgName=" |
| + sStatic.mPackageName + ", callers=" + sb.toString()); |
| } |
| } catch (RemoteException e) { |
| Log.e(TAG, "Error changing Bluetooth A2DP state", e); |
| } |
| } |
| |
| final WifiDisplay activeDisplay = |
| sStatic.mDisplayService.getWifiDisplayStatus().getActiveDisplay(); |
| final boolean oldRouteHasAddress = oldRoute != null && oldRoute.mDeviceAddress != null; |
| final boolean newRouteHasAddress = route.mDeviceAddress != null; |
| if (activeDisplay != null || oldRouteHasAddress || newRouteHasAddress) { |
| if (newRouteHasAddress && !matchesDeviceAddress(activeDisplay, route)) { |
| if (sStatic.mCanConfigureWifiDisplays) { |
| sStatic.mDisplayService.connectWifiDisplay(route.mDeviceAddress); |
| } else { |
| Log.e(TAG, "Cannot connect to wifi displays because this process " |
| + "is not allowed to do so."); |
| } |
| } else if (activeDisplay != null && !newRouteHasAddress) { |
| sStatic.mDisplayService.disconnectWifiDisplay(); |
| } |
| } |
| |
| sStatic.setSelectedRoute(route, explicit); |
| |
| if (oldRoute != null) { |
| dispatchRouteUnselected(types & oldRoute.getSupportedTypes(), oldRoute); |
| if (oldRoute.resolveStatusCode()) { |
| dispatchRouteChanged(oldRoute); |
| } |
| } |
| if (route != null) { |
| if (route.resolveStatusCode()) { |
| dispatchRouteChanged(route); |
| } |
| dispatchRouteSelected(types & route.getSupportedTypes(), route); |
| } |
| |
| // The behavior of active scans may depend on the currently selected route. |
| sStatic.updateDiscoveryRequest(); |
| } |
| |
| static void selectDefaultRouteStatic() { |
| // TODO: Be smarter about the route types here; this selects for all valid. |
| if (sStatic.mSelectedRoute != sStatic.mBluetoothA2dpRoute && sStatic.isBluetoothA2dpOn()) { |
| selectRouteStatic(ROUTE_TYPE_ANY, sStatic.mBluetoothA2dpRoute, false); |
| } else { |
| selectRouteStatic(ROUTE_TYPE_ANY, sStatic.mDefaultAudioVideo, false); |
| } |
| } |
| |
| /** |
| * Compare the device address of a display and a route. |
| * Nulls/no device address will match another null/no address. |
| */ |
| static boolean matchesDeviceAddress(WifiDisplay display, RouteInfo info) { |
| final boolean routeHasAddress = info != null && info.mDeviceAddress != null; |
| if (display == null && !routeHasAddress) { |
| return true; |
| } |
| |
| if (display != null && routeHasAddress) { |
| return display.getDeviceAddress().equals(info.mDeviceAddress); |
| } |
| return false; |
| } |
| |
| /** |
| * Add an app-specified route for media to the MediaRouter. |
| * App-specified route definitions are created using {@link #createUserRoute(RouteCategory)} |
| * |
| * @param info Definition of the route to add |
| * @see #createUserRoute(RouteCategory) |
| * @see #removeUserRoute(UserRouteInfo) |
| */ |
| public void addUserRoute(UserRouteInfo info) { |
| addRouteStatic(info); |
| } |
| |
| /** |
| * @hide Framework use only |
| */ |
| public void addRouteInt(RouteInfo info) { |
| addRouteStatic(info); |
| } |
| |
| static void addRouteStatic(RouteInfo info) { |
| Log.v(TAG, "Adding route: " + info); |
| final RouteCategory cat = info.getCategory(); |
| if (!sStatic.mCategories.contains(cat)) { |
| sStatic.mCategories.add(cat); |
| } |
| if (cat.isGroupable() && !(info instanceof RouteGroup)) { |
| // Enforce that any added route in a groupable category must be in a group. |
| final RouteGroup group = new RouteGroup(info.getCategory()); |
| group.mSupportedTypes = info.mSupportedTypes; |
| sStatic.mRoutes.add(group); |
| dispatchRouteAdded(group); |
| group.addRoute(info); |
| |
| info = group; |
| } else { |
| sStatic.mRoutes.add(info); |
| dispatchRouteAdded(info); |
| } |
| } |
| |
| /** |
| * Remove an app-specified route for media from the MediaRouter. |
| * |
| * @param info Definition of the route to remove |
| * @see #addUserRoute(UserRouteInfo) |
| */ |
| public void removeUserRoute(UserRouteInfo info) { |
| removeRouteStatic(info); |
| } |
| |
| /** |
| * Remove all app-specified routes from the MediaRouter. |
| * |
| * @see #removeUserRoute(UserRouteInfo) |
| */ |
| public void clearUserRoutes() { |
| for (int i = 0; i < sStatic.mRoutes.size(); i++) { |
| final RouteInfo info = sStatic.mRoutes.get(i); |
| // TODO Right now, RouteGroups only ever contain user routes. |
| // The code below will need to change if this assumption does. |
| if (info instanceof UserRouteInfo || info instanceof RouteGroup) { |
| removeRouteStatic(info); |
| i--; |
| } |
| } |
| } |
| |
| /** |
| * @hide internal use only |
| */ |
| public void removeRouteInt(RouteInfo info) { |
| removeRouteStatic(info); |
| } |
| |
| static void removeRouteStatic(RouteInfo info) { |
| Log.v(TAG, "Removing route: " + info); |
| if (sStatic.mRoutes.remove(info)) { |
| final RouteCategory removingCat = info.getCategory(); |
| final int count = sStatic.mRoutes.size(); |
| boolean found = false; |
| for (int i = 0; i < count; i++) { |
| final RouteCategory cat = sStatic.mRoutes.get(i).getCategory(); |
| if (removingCat == cat) { |
| found = true; |
| break; |
| } |
| } |
| if (info.isSelected()) { |
| // Removing the currently selected route? Select the default before we remove it. |
| selectDefaultRouteStatic(); |
| } |
| if (!found) { |
| sStatic.mCategories.remove(removingCat); |
| } |
| dispatchRouteRemoved(info); |
| } |
| } |
| |
| /** |
| * Return the number of {@link MediaRouter.RouteCategory categories} currently |
| * represented by routes known to this MediaRouter. |
| * |
| * @return the number of unique categories represented by this MediaRouter's known routes |
| */ |
| public int getCategoryCount() { |
| return sStatic.mCategories.size(); |
| } |
| |
| /** |
| * Return the {@link MediaRouter.RouteCategory category} at the given index. |
| * Valid indices are in the range [0-getCategoryCount). |
| * |
| * @param index which category to return |
| * @return the category at index |
| */ |
| public RouteCategory getCategoryAt(int index) { |
| return sStatic.mCategories.get(index); |
| } |
| |
| /** |
| * Return the number of {@link MediaRouter.RouteInfo routes} currently known |
| * to this MediaRouter. |
| * |
| * @return the number of routes tracked by this router |
| */ |
| public int getRouteCount() { |
| return sStatic.mRoutes.size(); |
| } |
| |
| /** |
| * Return the route at the specified index. |
| * |
| * @param index index of the route to return |
| * @return the route at index |
| */ |
| public RouteInfo getRouteAt(int index) { |
| return sStatic.mRoutes.get(index); |
| } |
| |
| static int getRouteCountStatic() { |
| return sStatic.mRoutes.size(); |
| } |
| |
| static RouteInfo getRouteAtStatic(int index) { |
| return sStatic.mRoutes.get(index); |
| } |
| |
| /** |
| * Create a new user route that may be modified and registered for use by the application. |
| * |
| * @param category The category the new route will belong to |
| * @return A new UserRouteInfo for use by the application |
| * |
| * @see #addUserRoute(UserRouteInfo) |
| * @see #removeUserRoute(UserRouteInfo) |
| * @see #createRouteCategory(CharSequence, boolean) |
| */ |
| public UserRouteInfo createUserRoute(RouteCategory category) { |
| return new UserRouteInfo(category); |
| } |
| |
| /** |
| * Create a new route category. Each route must belong to a category. |
| * |
| * @param name Name of the new category |
| * @param isGroupable true if routes in this category may be grouped with one another |
| * @return the new RouteCategory |
| */ |
| public RouteCategory createRouteCategory(CharSequence name, boolean isGroupable) { |
| return new RouteCategory(name, ROUTE_TYPE_USER, isGroupable); |
| } |
| |
| /** |
| * Create a new route category. Each route must belong to a category. |
| * |
| * @param nameResId Resource ID of the name of the new category |
| * @param isGroupable true if routes in this category may be grouped with one another |
| * @return the new RouteCategory |
| */ |
| public RouteCategory createRouteCategory(int nameResId, boolean isGroupable) { |
| return new RouteCategory(nameResId, ROUTE_TYPE_USER, isGroupable); |
| } |
| |
| /** |
| * Rebinds the media router to handle routes that belong to the specified user. |
| * Requires the interact across users permission to access the routes of another user. |
| * <p> |
| * This method is a complete hack to work around the singleton nature of the |
| * media router when running inside of singleton processes like QuickSettings. |
| * This mechanism should be burned to the ground when MediaRouter is redesigned. |
| * Ideally the current user would be pulled from the Context but we need to break |
| * down MediaRouter.Static before we can get there. |
| * </p> |
| * |
| * @hide |
| */ |
| public void rebindAsUser(int userId) { |
| sStatic.rebindAsUser(userId); |
| } |
| |
| static void updateRoute(final RouteInfo info) { |
| dispatchRouteChanged(info); |
| } |
| |
| static void dispatchRouteSelected(int type, RouteInfo info) { |
| for (CallbackInfo cbi : sStatic.mCallbacks) { |
| if (cbi.filterRouteEvent(info)) { |
| cbi.cb.onRouteSelected(cbi.router, type, info); |
| } |
| } |
| } |
| |
| static void dispatchRouteUnselected(int type, RouteInfo info) { |
| for (CallbackInfo cbi : sStatic.mCallbacks) { |
| if (cbi.filterRouteEvent(info)) { |
| cbi.cb.onRouteUnselected(cbi.router, type, info); |
| } |
| } |
| } |
| |
| static void dispatchRouteChanged(RouteInfo info) { |
| dispatchRouteChanged(info, info.mSupportedTypes); |
| } |
| |
| static void dispatchRouteChanged(RouteInfo info, int oldSupportedTypes) { |
| if (DEBUG) { |
| Log.d(TAG, "Dispatching route change: " + info); |
| } |
| final int newSupportedTypes = info.mSupportedTypes; |
| for (CallbackInfo cbi : sStatic.mCallbacks) { |
| // Reconstruct some of the history for callbacks that may not have observed |
| // all of the events needed to correctly interpret the current state. |
| // FIXME: This is a strong signal that we should deprecate route type filtering |
| // completely in the future because it can lead to inconsistencies in |
| // applications. |
| final boolean oldVisibility = cbi.filterRouteEvent(oldSupportedTypes); |
| final boolean newVisibility = cbi.filterRouteEvent(newSupportedTypes); |
| if (!oldVisibility && newVisibility) { |
| cbi.cb.onRouteAdded(cbi.router, info); |
| if (info.isSelected()) { |
| cbi.cb.onRouteSelected(cbi.router, newSupportedTypes, info); |
| } |
| } |
| if (oldVisibility || newVisibility) { |
| cbi.cb.onRouteChanged(cbi.router, info); |
| } |
| if (oldVisibility && !newVisibility) { |
| if (info.isSelected()) { |
| cbi.cb.onRouteUnselected(cbi.router, oldSupportedTypes, info); |
| } |
| cbi.cb.onRouteRemoved(cbi.router, info); |
| } |
| } |
| } |
| |
| static void dispatchRouteAdded(RouteInfo info) { |
| for (CallbackInfo cbi : sStatic.mCallbacks) { |
| if (cbi.filterRouteEvent(info)) { |
| cbi.cb.onRouteAdded(cbi.router, info); |
| } |
| } |
| } |
| |
| static void dispatchRouteRemoved(RouteInfo info) { |
| for (CallbackInfo cbi : sStatic.mCallbacks) { |
| if (cbi.filterRouteEvent(info)) { |
| cbi.cb.onRouteRemoved(cbi.router, info); |
| } |
| } |
| } |
| |
| static void dispatchRouteGrouped(RouteInfo info, RouteGroup group, int index) { |
| for (CallbackInfo cbi : sStatic.mCallbacks) { |
| if (cbi.filterRouteEvent(group)) { |
| cbi.cb.onRouteGrouped(cbi.router, info, group, index); |
| } |
| } |
| } |
| |
| static void dispatchRouteUngrouped(RouteInfo info, RouteGroup group) { |
| for (CallbackInfo cbi : sStatic.mCallbacks) { |
| if (cbi.filterRouteEvent(group)) { |
| cbi.cb.onRouteUngrouped(cbi.router, info, group); |
| } |
| } |
| } |
| |
| static void dispatchRouteVolumeChanged(RouteInfo info) { |
| for (CallbackInfo cbi : sStatic.mCallbacks) { |
| if (cbi.filterRouteEvent(info)) { |
| cbi.cb.onRouteVolumeChanged(cbi.router, info); |
| } |
| } |
| } |
| |
| static void dispatchRoutePresentationDisplayChanged(RouteInfo info) { |
| for (CallbackInfo cbi : sStatic.mCallbacks) { |
| if (cbi.filterRouteEvent(info)) { |
| cbi.cb.onRoutePresentationDisplayChanged(cbi.router, info); |
| } |
| } |
| } |
| |
| static void systemVolumeChanged(int newValue) { |
| final RouteInfo selectedRoute = sStatic.mSelectedRoute; |
| if (selectedRoute == null) return; |
| |
| if (selectedRoute == sStatic.mBluetoothA2dpRoute || |
| selectedRoute == sStatic.mDefaultAudioVideo) { |
| dispatchRouteVolumeChanged(selectedRoute); |
| } else if (sStatic.mBluetoothA2dpRoute != null) { |
| try { |
| dispatchRouteVolumeChanged(sStatic.mAudioService.isBluetoothA2dpOn() ? |
| sStatic.mBluetoothA2dpRoute : sStatic.mDefaultAudioVideo); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Error checking Bluetooth A2DP state to report volume change", e); |
| } |
| } else { |
| dispatchRouteVolumeChanged(sStatic.mDefaultAudioVideo); |
| } |
| } |
| |
| static void updateWifiDisplayStatus(WifiDisplayStatus status) { |
| WifiDisplay[] displays; |
| WifiDisplay activeDisplay; |
| if (status.getFeatureState() == WifiDisplayStatus.FEATURE_STATE_ON) { |
| displays = status.getDisplays(); |
| activeDisplay = status.getActiveDisplay(); |
| |
| // Only the system is able to connect to wifi display routes. |
| // The display manager will enforce this with a permission check but it |
| // still publishes information about all available displays. |
| // Filter the list down to just the active display. |
| if (!sStatic.mCanConfigureWifiDisplays) { |
| if (activeDisplay != null) { |
| displays = new WifiDisplay[] { activeDisplay }; |
| } else { |
| displays = WifiDisplay.EMPTY_ARRAY; |
| } |
| } |
| } else { |
| displays = WifiDisplay.EMPTY_ARRAY; |
| activeDisplay = null; |
| } |
| String activeDisplayAddress = activeDisplay != null ? |
| activeDisplay.getDeviceAddress() : null; |
| |
| // Add or update routes. |
| for (int i = 0; i < displays.length; i++) { |
| final WifiDisplay d = displays[i]; |
| if (shouldShowWifiDisplay(d, activeDisplay)) { |
| RouteInfo route = findWifiDisplayRoute(d); |
| if (route == null) { |
| route = makeWifiDisplayRoute(d, status); |
| addRouteStatic(route); |
| } else { |
| String address = d.getDeviceAddress(); |
| boolean disconnected = !address.equals(activeDisplayAddress) |
| && address.equals(sStatic.mPreviousActiveWifiDisplayAddress); |
| updateWifiDisplayRoute(route, d, status, disconnected); |
| } |
| if (d.equals(activeDisplay)) { |
| selectRouteStatic(route.getSupportedTypes(), route, false); |
| } |
| } |
| } |
| |
| // Remove stale routes. |
| for (int i = sStatic.mRoutes.size(); i-- > 0; ) { |
| RouteInfo route = sStatic.mRoutes.get(i); |
| if (route.mDeviceAddress != null) { |
| WifiDisplay d = findWifiDisplay(displays, route.mDeviceAddress); |
| if (d == null || !shouldShowWifiDisplay(d, activeDisplay)) { |
| removeRouteStatic(route); |
| } |
| } |
| } |
| |
| // Remember the current active wifi display address so that we can infer disconnections. |
| // TODO: This hack will go away once all of this is moved into the media router service. |
| sStatic.mPreviousActiveWifiDisplayAddress = activeDisplayAddress; |
| } |
| |
| private static boolean shouldShowWifiDisplay(WifiDisplay d, WifiDisplay activeDisplay) { |
| return d.isRemembered() || d.equals(activeDisplay); |
| } |
| |
| static int getWifiDisplayStatusCode(WifiDisplay d, WifiDisplayStatus wfdStatus) { |
| int newStatus; |
| if (wfdStatus.getScanState() == WifiDisplayStatus.SCAN_STATE_SCANNING) { |
| newStatus = RouteInfo.STATUS_SCANNING; |
| } else if (d.isAvailable()) { |
| newStatus = d.canConnect() ? |
| RouteInfo.STATUS_AVAILABLE: RouteInfo.STATUS_IN_USE; |
| } else { |
| newStatus = RouteInfo.STATUS_NOT_AVAILABLE; |
| } |
| |
| if (d.equals(wfdStatus.getActiveDisplay())) { |
| final int activeState = wfdStatus.getActiveDisplayState(); |
| switch (activeState) { |
| case WifiDisplayStatus.DISPLAY_STATE_CONNECTED: |
| newStatus = RouteInfo.STATUS_CONNECTED; |
| break; |
| case WifiDisplayStatus.DISPLAY_STATE_CONNECTING: |
| newStatus = RouteInfo.STATUS_CONNECTING; |
| break; |
| case WifiDisplayStatus.DISPLAY_STATE_NOT_CONNECTED: |
| Log.e(TAG, "Active display is not connected!"); |
| break; |
| } |
| } |
| |
| return newStatus; |
| } |
| |
| static boolean isWifiDisplayEnabled(WifiDisplay d, WifiDisplayStatus wfdStatus) { |
| return d.isAvailable() && (d.canConnect() || d.equals(wfdStatus.getActiveDisplay())); |
| } |
| |
| static RouteInfo makeWifiDisplayRoute(WifiDisplay display, WifiDisplayStatus wfdStatus) { |
| final RouteInfo newRoute = new RouteInfo(sStatic.mSystemCategory); |
| newRoute.mDeviceAddress = display.getDeviceAddress(); |
| newRoute.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO |
| | ROUTE_TYPE_REMOTE_DISPLAY; |
| newRoute.mVolumeHandling = RouteInfo.PLAYBACK_VOLUME_FIXED; |
| newRoute.mPlaybackType = RouteInfo.PLAYBACK_TYPE_REMOTE; |
| |
| newRoute.setRealStatusCode(getWifiDisplayStatusCode(display, wfdStatus)); |
| newRoute.mEnabled = isWifiDisplayEnabled(display, wfdStatus); |
| newRoute.mName = display.getFriendlyDisplayName(); |
| newRoute.mDescription = sStatic.mResources.getText( |
| com.android.internal.R.string.wireless_display_route_description); |
| newRoute.updatePresentationDisplay(); |
| newRoute.mDeviceType = RouteInfo.DEVICE_TYPE_TV; |
| return newRoute; |
| } |
| |
| private static void updateWifiDisplayRoute( |
| RouteInfo route, WifiDisplay display, WifiDisplayStatus wfdStatus, |
| boolean disconnected) { |
| boolean changed = false; |
| final String newName = display.getFriendlyDisplayName(); |
| if (!route.getName().equals(newName)) { |
| route.mName = newName; |
| changed = true; |
| } |
| |
| boolean enabled = isWifiDisplayEnabled(display, wfdStatus); |
| changed |= route.mEnabled != enabled; |
| route.mEnabled = enabled; |
| |
| changed |= route.setRealStatusCode(getWifiDisplayStatusCode(display, wfdStatus)); |
| |
| if (changed) { |
| dispatchRouteChanged(route); |
| } |
| |
| if ((!enabled || disconnected) && route.isSelected()) { |
| // Oops, no longer available. Reselect the default. |
| selectDefaultRouteStatic(); |
| } |
| } |
| |
| private static WifiDisplay findWifiDisplay(WifiDisplay[] displays, String deviceAddress) { |
| for (int i = 0; i < displays.length; i++) { |
| final WifiDisplay d = displays[i]; |
| if (d.getDeviceAddress().equals(deviceAddress)) { |
| return d; |
| } |
| } |
| return null; |
| } |
| |
| private static RouteInfo findWifiDisplayRoute(WifiDisplay d) { |
| final int count = sStatic.mRoutes.size(); |
| for (int i = 0; i < count; i++) { |
| final RouteInfo info = sStatic.mRoutes.get(i); |
| if (d.getDeviceAddress().equals(info.mDeviceAddress)) { |
| return info; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Information about a media route. |
| */ |
| public static class RouteInfo { |
| CharSequence mName; |
| int mNameResId; |
| CharSequence mDescription; |
| private CharSequence mStatus; |
| int mSupportedTypes; |
| int mDeviceType; |
| RouteGroup mGroup; |
| final RouteCategory mCategory; |
| Drawable mIcon; |
| // playback information |
| int mPlaybackType = PLAYBACK_TYPE_LOCAL; |
| int mVolumeMax = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME; |
| int mVolume = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME; |
| int mVolumeHandling = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME_HANDLING; |
| int mPlaybackStream = AudioManager.STREAM_MUSIC; |
| VolumeCallbackInfo mVcb; |
| Display mPresentationDisplay; |
| int mPresentationDisplayId = -1; |
| |
| String mDeviceAddress; |
| boolean mEnabled = true; |
| |
| // An id by which the route is known to the media router service. |
| // Null if this route only exists as an artifact within this process. |
| String mGlobalRouteId; |
| |
| // A predetermined connection status that can override mStatus |
| private int mRealStatusCode; |
| private int mResolvedStatusCode; |
| |
| /** @hide */ public static final int STATUS_NONE = 0; |
| /** @hide */ public static final int STATUS_SCANNING = 1; |
| /** @hide */ public static final int STATUS_CONNECTING = 2; |
| /** @hide */ public static final int STATUS_AVAILABLE = 3; |
| /** @hide */ public static final int STATUS_NOT_AVAILABLE = 4; |
| /** @hide */ public static final int STATUS_IN_USE = 5; |
| /** @hide */ public static final int STATUS_CONNECTED = 6; |
| |
| /** @hide */ |
| @IntDef({DEVICE_TYPE_UNKNOWN, DEVICE_TYPE_TV, DEVICE_TYPE_SPEAKER, DEVICE_TYPE_BLUETOOTH}) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface DeviceType {} |
| |
| /** |
| * The default receiver device type of the route indicating the type is unknown. |
| * |
| * @see #getDeviceType |
| */ |
| public static final int DEVICE_TYPE_UNKNOWN = 0; |
| |
| /** |
| * A receiver device type of the route indicating the presentation of the media is happening |
| * on a TV. |
| * |
| * @see #getDeviceType |
| */ |
| public static final int DEVICE_TYPE_TV = 1; |
| |
| /** |
| * A receiver device type of the route indicating the presentation of the media is happening |
| * on a speaker. |
| * |
| * @see #getDeviceType |
| */ |
| public static final int DEVICE_TYPE_SPEAKER = 2; |
| |
| /** |
| * A receiver device type of the route indicating the presentation of the media is happening |
| * on a bluetooth device such as a bluetooth speaker. |
| * |
| * @see #getDeviceType |
| */ |
| public static final int DEVICE_TYPE_BLUETOOTH = 3; |
| |
| private Object mTag; |
| |
| /** @hide */ |
| @IntDef({PLAYBACK_TYPE_LOCAL, PLAYBACK_TYPE_REMOTE}) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface PlaybackType {} |
| |
| /** |
| * The default playback type, "local", indicating the presentation of the media is happening |
| * on the same device (e.g. a phone, a tablet) as where it is controlled from. |
| * @see #getPlaybackType() |
| */ |
| public final static int PLAYBACK_TYPE_LOCAL = 0; |
| |
| /** |
| * A playback type indicating the presentation of the media is happening on |
| * a different device (i.e. the remote device) than where it is controlled from. |
| * @see #getPlaybackType() |
| */ |
| public final static int PLAYBACK_TYPE_REMOTE = 1; |
| |
| /** @hide */ |
| @IntDef({PLAYBACK_VOLUME_FIXED,PLAYBACK_VOLUME_VARIABLE}) |
| @Retention(RetentionPolicy.SOURCE) |
| private @interface PlaybackVolume {} |
| |
| /** |
| * Playback information indicating the playback volume is fixed, i.e. it cannot be |
| * controlled from this object. An example of fixed playback volume is a remote player, |
| * playing over HDMI where the user prefers to control the volume on the HDMI sink, rather |
| * than attenuate at the source. |
| * @see #getVolumeHandling() |
| */ |
| public final static int PLAYBACK_VOLUME_FIXED = 0; |
| /** |
| * Playback information indicating the playback volume is variable and can be controlled |
| * from this object. |
| * @see #getVolumeHandling() |
| */ |
| public final static int PLAYBACK_VOLUME_VARIABLE = 1; |
| |
| RouteInfo(RouteCategory category) { |
| mCategory = category; |
| mDeviceType = DEVICE_TYPE_UNKNOWN; |
| } |
| |
| /** |
| * Gets the user-visible name of the route. |
| * <p> |
| * The route name identifies the destination represented by the route. |
| * It may be a user-supplied name, an alias, or device serial number. |
| * </p> |
| * |
| * @return The user-visible name of a media route. This is the string presented |
| * to users who may select this as the active route. |
| */ |
| public CharSequence getName() { |
| return getName(sStatic.mResources); |
| } |
| |
| /** |
| * Return the properly localized/resource user-visible name of this route. |
| * <p> |
| * The route name identifies the destination represented by the route. |
| * It may be a user-supplied name, an alias, or device serial number. |
| * </p> |
| * |
| * @param context Context used to resolve the correct configuration to load |
| * @return The user-visible name of a media route. This is the string presented |
| * to users who may select this as the active route. |
| */ |
| public CharSequence getName(Context context) { |
| return getName(context.getResources()); |
| } |
| |
| CharSequence getName(Resources res) { |
| if (mNameResId != 0) { |
| return res.getText(mNameResId); |
| } |
| return mName; |
| } |
| |
| /** |
| * Gets the user-visible description of the route. |
| * <p> |
| * The route description describes the kind of destination represented by the route. |
| * It may be a user-supplied string, a model number or brand of device. |
| * </p> |
| * |
| * @return The description of the route, or null if none. |
| */ |
| public CharSequence getDescription() { |
| return mDescription; |
| } |
| |
| /** |
| * @return The user-visible status for a media route. This may include a description |
| * of the currently playing media, if available. |
| */ |
| public CharSequence getStatus() { |
| return mStatus; |
| } |
| |
| /** |
| * Set this route's status by predetermined status code. If the caller |
| * should dispatch a route changed event this call will return true; |
| */ |
| boolean setRealStatusCode(int statusCode) { |
| if (mRealStatusCode != statusCode) { |
| mRealStatusCode = statusCode; |
| return resolveStatusCode(); |
| } |
| return false; |
| } |
| |
| /** |
| * Resolves the status code whenever the real status code or selection state |
| * changes. |
| */ |
| boolean resolveStatusCode() { |
| int statusCode = mRealStatusCode; |
| if (isSelected()) { |
| switch (statusCode) { |
| // If the route is selected and its status appears to be between states |
| // then report it as connecting even though it has not yet had a chance |
| // to officially move into the CONNECTING state. Note that routes in |
| // the NONE state are assumed to not require an explicit connection |
| // lifecycle whereas those that are AVAILABLE are assumed to have |
| // to eventually proceed to CONNECTED. |
| case STATUS_AVAILABLE: |
| case STATUS_SCANNING: |
| statusCode = STATUS_CONNECTING; |
| break; |
| } |
| } |
| if (mResolvedStatusCode == statusCode) { |
| return false; |
| } |
| |
| mResolvedStatusCode = statusCode; |
| int resId; |
| switch (statusCode) { |
| case STATUS_SCANNING: |
| resId = com.android.internal.R.string.media_route_status_scanning; |
| break; |
| case STATUS_CONNECTING: |
| resId = com.android.internal.R.string.media_route_status_connecting; |
| break; |
| case STATUS_AVAILABLE: |
| resId = com.android.internal.R.string.media_route_status_available; |
| break; |
| case STATUS_NOT_AVAILABLE: |
| resId = com.android.internal.R.string.media_route_status_not_available; |
| break; |
| case STATUS_IN_USE: |
| resId = com.android.internal.R.string.media_route_status_in_use; |
| break; |
| case STATUS_CONNECTED: |
| case STATUS_NONE: |
| default: |
| resId = 0; |
| break; |
| } |
| mStatus = resId != 0 ? sStatic.mResources.getText(resId) : null; |
| return true; |
| } |
| |
| /** |
| * @hide |
| */ |
| public int getStatusCode() { |
| return mResolvedStatusCode; |
| } |
| |
| /** |
| * @return A media type flag set describing which types this route supports. |
| */ |
| public int getSupportedTypes() { |
| return mSupportedTypes; |
| } |
| |
| /** |
| * Gets the type of the receiver device associated with this route. |
| * |
| * @return The type of the receiver device associated with this route: |
| * {@link #DEVICE_TYPE_BLUETOOTH}, {@link #DEVICE_TYPE_TV}, {@link #DEVICE_TYPE_SPEAKER}, |
| * or {@link #DEVICE_TYPE_UNKNOWN}. |
| */ |
| @DeviceType |
| public int getDeviceType() { |
| return mDeviceType; |
| } |
| |
| /** @hide */ |
| public boolean matchesTypes(int types) { |
| return (mSupportedTypes & types) != 0; |
| } |
| |
| /** |
| * @return The group that this route belongs to. |
| */ |
| public RouteGroup getGroup() { |
| return mGroup; |
| } |
| |
| /** |
| * @return the category this route belongs to. |
| */ |
| public RouteCategory getCategory() { |
| return mCategory; |
| } |
| |
| /** |
| * Get the icon representing this route. |
| * This icon will be used in picker UIs if available. |
| * |
| * @return the icon representing this route or null if no icon is available |
| */ |
| public Drawable getIconDrawable() { |
| return mIcon; |
| } |
| |
| /** |
| * Set an application-specific tag object for this route. |
| * The application may use this to store arbitrary data associated with the |
| * route for internal tracking. |
| * |
| * <p>Note that the lifespan of a route may be well past the lifespan of |
| * an Activity or other Context; take care that objects you store here |
| * will not keep more data in memory alive than you intend.</p> |
| * |
| * @param tag Arbitrary, app-specific data for this route to hold for later use |
| */ |
| public void setTag(Object tag) { |
| mTag = tag; |
| routeUpdated(); |
| } |
| |
| /** |
| * @return The tag object previously set by the application |
| * @see #setTag(Object) |
| */ |
| public Object getTag() { |
| return mTag; |
| } |
| |
| /** |
| * @return the type of playback associated with this route |
| * @see UserRouteInfo#setPlaybackType(int) |
| */ |
| @PlaybackType |
| public int getPlaybackType() { |
| return mPlaybackType; |
| } |
| |
| /** |
| * @return the stream over which the playback associated with this route is performed |
| * @see UserRouteInfo#setPlaybackStream(int) |
| */ |
| public int getPlaybackStream() { |
| return mPlaybackStream; |
| } |
| |
| /** |
| * Return the current volume for this route. Depending on the route, this may only |
| * be valid if the route is currently selected. |
| * |
| * @return the volume at which the playback associated with this route is performed |
| * @see UserRouteInfo#setVolume(int) |
| */ |
| public int getVolume() { |
| if (mPlaybackType == PLAYBACK_TYPE_LOCAL) { |
| int vol = 0; |
| try { |
| vol = sStatic.mAudioService.getStreamVolume(mPlaybackStream); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Error getting local stream volume", e); |
| } |
| return vol; |
| } else { |
| return mVolume; |
| } |
| } |
| |
| /** |
| * Request a volume change for this route. |
| * @param volume value between 0 and getVolumeMax |
| */ |
| public void requestSetVolume(int volume) { |
| if (mPlaybackType == PLAYBACK_TYPE_LOCAL) { |
| try { |
| sStatic.mAudioService.setStreamVolume(mPlaybackStream, volume, 0, |
| ActivityThread.currentPackageName()); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Error setting local stream volume", e); |
| } |
| } else { |
| sStatic.requestSetVolume(this, volume); |
| } |
| } |
| |
| /** |
| * Request an incremental volume update for this route. |
| * @param direction Delta to apply to the current volume |
| */ |
| public void requestUpdateVolume(int direction) { |
| if (mPlaybackType == PLAYBACK_TYPE_LOCAL) { |
| try { |
| final int volume = |
| Math.max(0, Math.min(getVolume() + direction, getVolumeMax())); |
| sStatic.mAudioService.setStreamVolume(mPlaybackStream, volume, 0, |
| ActivityThread.currentPackageName()); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Error setting local stream volume", e); |
| } |
| } else { |
| sStatic.requestUpdateVolume(this, direction); |
| } |
| } |
| |
| /** |
| * @return the maximum volume at which the playback associated with this route is performed |
| * @see UserRouteInfo#setVolumeMax(int) |
| */ |
| public int getVolumeMax() { |
| if (mPlaybackType == PLAYBACK_TYPE_LOCAL) { |
| int volMax = 0; |
| try { |
| volMax = sStatic.mAudioService.getStreamMaxVolume(mPlaybackStream); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Error getting local stream volume", e); |
| } |
| return volMax; |
| } else { |
| return mVolumeMax; |
| } |
| } |
| |
| /** |
| * @return how volume is handling on the route |
| * @see UserRouteInfo#setVolumeHandling(int) |
| */ |
| @PlaybackVolume |
| public int getVolumeHandling() { |
| return mVolumeHandling; |
| } |
| |
| /** |
| * Gets the {@link Display} that should be used by the application to show |
| * a {@link android.app.Presentation} on an external display when this route is selected. |
| * Depending on the route, this may only be valid if the route is currently |
| * selected. |
| * <p> |
| * The preferred presentation display may change independently of the route |
| * being selected or unselected. For example, the presentation display |
| * of the default system route may change when an external HDMI display is connected |
| * or disconnected even though the route itself has not changed. |
| * </p><p> |
| * This method may return null if there is no external display associated with |
| * the route or if the display is not ready to show UI yet. |
| * </p><p> |
| * The application should listen for changes to the presentation display |
| * using the {@link Callback#onRoutePresentationDisplayChanged} callback and |
| * show or dismiss its {@link android.app.Presentation} accordingly when the display |
| * becomes available or is removed. |
| * </p><p> |
| * This method only makes sense for {@link #ROUTE_TYPE_LIVE_VIDEO live video} routes. |
| * </p> |
| * |
| * @return The preferred presentation display to use when this route is |
| * selected or null if none. |
| * |
| * @see #ROUTE_TYPE_LIVE_VIDEO |
| * @see android.app.Presentation |
| */ |
| public Display getPresentationDisplay() { |
| return mPresentationDisplay; |
| } |
| |
| boolean updatePresentationDisplay() { |
| Display display = choosePresentationDisplay(); |
| if (mPresentationDisplay != display) { |
| mPresentationDisplay = display; |
| return true; |
| } |
| return false; |
| } |
| |
| private Display choosePresentationDisplay() { |
| if ((mSupportedTypes & ROUTE_TYPE_LIVE_VIDEO) != 0) { |
| Display[] displays = sStatic.getAllPresentationDisplays(); |
| |
| // Ensure that the specified display is valid for presentations. |
| // This check will normally disallow the default display unless it was |
| // configured as a presentation display for some reason. |
| if (mPresentationDisplayId >= 0) { |
| for (Display display : displays) { |
| if (display.getDisplayId() == mPresentationDisplayId) { |
| return display; |
| } |
| } |
| return null; |
| } |
| |
| // Find the indicated Wifi display by its address. |
| if (mDeviceAddress != null) { |
| for (Display display : displays) { |
| if (display.getType() == Display.TYPE_WIFI |
| && mDeviceAddress.equals(display.getAddress())) { |
| return display; |
| } |
| } |
| return null; |
| } |
| |
| // For the default route, choose the first presentation display from the list. |
| if (this == sStatic.mDefaultAudioVideo && displays.length > 0) { |
| return displays[0]; |
| } |
| } |
| return null; |
| } |
| |
| /** @hide */ |
| public String getDeviceAddress() { |
| return mDeviceAddress; |
| } |
| |
| /** |
| * Returns true if this route is enabled and may be selected. |
| * |
| * @return True if this route is enabled. |
| */ |
| public boolean isEnabled() { |
| return mEnabled; |
| } |
| |
| /** |
| * Returns true if the route is in the process of connecting and is not |
| * yet ready for use. |
| * |
| * @return True if this route is in the process of connecting. |
| */ |
| public boolean isConnecting() { |
| return mResolvedStatusCode == STATUS_CONNECTING; |
| } |
| |
| /** @hide */ |
| public boolean isSelected() { |
| return this == sStatic.mSelectedRoute; |
| } |
| |
| /** @hide */ |
| public boolean isDefault() { |
| return this == sStatic.mDefaultAudioVideo; |
| } |
| |
| /** @hide */ |
| public boolean isBluetooth() { |
| return this == sStatic.mBluetoothA2dpRoute; |
| } |
| |
| /** @hide */ |
| public void select() { |
| selectRouteStatic(mSupportedTypes, this, true); |
| } |
| |
| void setStatusInt(CharSequence status) { |
| if (!status.equals(mStatus)) { |
| mStatus = status; |
| if (mGroup != null) { |
| mGroup.memberStatusChanged(this, status); |
| } |
| routeUpdated(); |
| } |
| } |
| |
| final IRemoteVolumeObserver.Stub mRemoteVolObserver = new IRemoteVolumeObserver.Stub() { |
| @Override |
| public void dispatchRemoteVolumeUpdate(final int direction, final int value) { |
| sStatic.mHandler.post(new Runnable() { |
| @Override |
| public void run() { |
| if (mVcb != null) { |
| if (direction != 0) { |
| mVcb.vcb.onVolumeUpdateRequest(mVcb.route, direction); |
| } else { |
| mVcb.vcb.onVolumeSetRequest(mVcb.route, value); |
| } |
| } |
| } |
| }); |
| } |
| }; |
| |
| void routeUpdated() { |
| updateRoute(this); |
| } |
| |
| @Override |
| public String toString() { |
| String supportedTypes = typesToString(getSupportedTypes()); |
| return getClass().getSimpleName() + "{ name=" + getName() + |
| ", description=" + getDescription() + |
| ", status=" + getStatus() + |
| ", category=" + getCategory() + |
| ", supportedTypes=" + supportedTypes + |
| ", presentationDisplay=" + mPresentationDisplay + " }"; |
| } |
| } |
| |
| /** |
| * Information about a route that the application may define and modify. |
| * A user route defaults to {@link RouteInfo#PLAYBACK_TYPE_REMOTE} and |
| * {@link RouteInfo#PLAYBACK_VOLUME_FIXED}. |
| * |
| * @see MediaRouter.RouteInfo |
| */ |
| public static class UserRouteInfo extends RouteInfo { |
| RemoteControlClient mRcc; |
| SessionVolumeProvider mSvp; |
| |
| UserRouteInfo(RouteCategory category) { |
| super(category); |
| mSupportedTypes = ROUTE_TYPE_USER; |
| mPlaybackType = PLAYBACK_TYPE_REMOTE; |
| mVolumeHandling = PLAYBACK_VOLUME_FIXED; |
| } |
| |
| /** |
| * Set the user-visible name of this route. |
| * @param name Name to display to the user to describe this route |
| */ |
| public void setName(CharSequence name) { |
| mNameResId = 0; |
| mName = name; |
| routeUpdated(); |
| } |
| |
| /** |
| * Set the user-visible name of this route. |
| * <p> |
| * The route name identifies the destination represented by the route. |
| * It may be a user-supplied name, an alias, or device serial number. |
| * </p> |
| * |
| * @param resId Resource ID of the name to display to the user to describe this route |
| */ |
| public void setName(int resId) { |
| mNameResId = resId; |
| mName = null; |
| routeUpdated(); |
| } |
| |
| /** |
| * Set the user-visible description of this route. |
| * <p> |
| * The route description describes the kind of destination represented by the route. |
| * It may be a user-supplied string, a model number or brand of device. |
| * </p> |
| * |
| * @param description The description of the route, or null if none. |
| */ |
| public void setDescription(CharSequence description) { |
| mDescription = description; |
| routeUpdated(); |
| } |
| |
| /** |
| * Set the current user-visible status for this route. |
| * @param status Status to display to the user to describe what the endpoint |
| * of this route is currently doing |
| */ |
| public void setStatus(CharSequence status) { |
| setStatusInt(status); |
| } |
| |
| /** |
| * Set the RemoteControlClient responsible for reporting playback info for this |
| * user route. |
| * |
| * <p>If this route manages remote playback, the data exposed by this |
| * RemoteControlClient will be used to reflect and update information |
| * such as route volume info in related UIs.</p> |
| * |
| * <p>The RemoteControlClient must have been previously registered with |
| * {@link AudioManager#registerRemoteControlClient(RemoteControlClient)}.</p> |
| * |
| * @param rcc RemoteControlClient associated with this route |
| */ |
| public void setRemoteControlClient(RemoteControlClient rcc) { |
| mRcc = rcc; |
| updatePlaybackInfoOnRcc(); |
| } |
| |
| /** |
| * Retrieve the RemoteControlClient associated with this route, if one has been set. |
| * |
| * @return the RemoteControlClient associated with this route |
| * @see #setRemoteControlClient(RemoteControlClient) |
| */ |
| public RemoteControlClient getRemoteControlClient() { |
| return mRcc; |
| } |
| |
| /** |
| * Set an icon that will be used to represent this route. |
| * The system may use this icon in picker UIs or similar. |
| * |
| * @param icon icon drawable to use to represent this route |
| */ |
| public void setIconDrawable(Drawable icon) { |
| mIcon = icon; |
| } |
| |
| /** |
| * Set an icon that will be used to represent this route. |
| * The system may use this icon in picker UIs or similar. |
| * |
| * @param resId Resource ID of an icon drawable to use to represent this route |
| */ |
| public void setIconResource(@DrawableRes int resId) { |
| setIconDrawable(sStatic.mResources.getDrawable(resId)); |
| } |
| |
| /** |
| * Set a callback to be notified of volume update requests |
| * @param vcb |
| */ |
| public void setVolumeCallback(VolumeCallback vcb) { |
| mVcb = new VolumeCallbackInfo(vcb, this); |
| } |
| |
| /** |
| * Defines whether playback associated with this route is "local" |
| * ({@link RouteInfo#PLAYBACK_TYPE_LOCAL}) or "remote" |
| * ({@link RouteInfo#PLAYBACK_TYPE_REMOTE}). |
| * @param type |
| */ |
| public void setPlaybackType(@RouteInfo.PlaybackType int type) { |
| if (mPlaybackType != type) { |
| mPlaybackType = type; |
| configureSessionVolume(); |
| } |
| } |
| |
| /** |
| * Defines whether volume for the playback associated with this route is fixed |
| * ({@link RouteInfo#PLAYBACK_VOLUME_FIXED}) or can modified |
| * ({@link RouteInfo#PLAYBACK_VOLUME_VARIABLE}). |
| * @param volumeHandling |
| */ |
| public void setVolumeHandling(@RouteInfo.PlaybackVolume int volumeHandling) { |
| if (mVolumeHandling != volumeHandling) { |
| mVolumeHandling = volumeHandling; |
| configureSessionVolume(); |
| } |
| } |
| |
| /** |
| * Defines at what volume the playback associated with this route is performed (for user |
| * feedback purposes). This information is only used when the playback is not local. |
| * @param volume |
| */ |
| public void setVolume(int volume) { |
| volume = Math.max(0, Math.min(volume, getVolumeMax())); |
| if (mVolume != volume) { |
| mVolume = volume; |
| if (mSvp != null) { |
| mSvp.setCurrentVolume(mVolume); |
| } |
| dispatchRouteVolumeChanged(this); |
| if (mGroup != null) { |
| mGroup.memberVolumeChanged(this); |
| } |
| } |
| } |
| |
| @Override |
| public void requestSetVolume(int volume) { |
| if (mVolumeHandling == PLAYBACK_VOLUME_VARIABLE) { |
| if (mVcb == null) { |
| Log.e(TAG, "Cannot requestSetVolume on user route - no volume callback set"); |
| return; |
| } |
| mVcb.vcb.onVolumeSetRequest(this, volume); |
| } |
| } |
| |
| @Override |
| public void requestUpdateVolume(int direction) { |
| if (mVolumeHandling == PLAYBACK_VOLUME_VARIABLE) { |
| if (mVcb == null) { |
| Log.e(TAG, "Cannot requestChangeVolume on user route - no volumec callback set"); |
| return; |
| } |
| mVcb.vcb.onVolumeUpdateRequest(this, direction); |
| } |
| } |
| |
| /** |
| * Defines the maximum volume at which the playback associated with this route is performed |
| * (for user feedback purposes). This information is only used when the playback is not |
| * local. |
| * @param volumeMax |
| */ |
| public void setVolumeMax(int volumeMax) { |
| if (mVolumeMax != volumeMax) { |
| mVolumeMax = volumeMax; |
| configureSessionVolume(); |
| } |
| } |
| |
| /** |
| * Defines over what stream type the media is presented. |
| * @param stream |
| */ |
| public void setPlaybackStream(int stream) { |
| if (mPlaybackStream != stream) { |
| mPlaybackStream = stream; |
| configureSessionVolume(); |
| } |
| } |
| |
| private void updatePlaybackInfoOnRcc() { |
| configureSessionVolume(); |
| } |
| |
| private void configureSessionVolume() { |
| if (mRcc == null) { |
| if (DEBUG) { |
| Log.d(TAG, "No Rcc to configure volume for route " + getName()); |
| } |
| return; |
| } |
| MediaSession session = mRcc.getMediaSession(); |
| if (session == null) { |
| if (DEBUG) { |
| Log.d(TAG, "Rcc has no session to configure volume"); |
| } |
| return; |
| } |
| if (mPlaybackType == RemoteControlClient.PLAYBACK_TYPE_REMOTE) { |
| @VolumeProvider.ControlType int volumeControl = |
| VolumeProvider.VOLUME_CONTROL_FIXED; |
| switch (mVolumeHandling) { |
| case RemoteControlClient.PLAYBACK_VOLUME_VARIABLE: |
| volumeControl = VolumeProvider.VOLUME_CONTROL_ABSOLUTE; |
| break; |
| case RemoteControlClient.PLAYBACK_VOLUME_FIXED: |
| default: |
| break; |
| } |
| // Only register a new listener if necessary |
| if (mSvp == null || mSvp.getVolumeControl() != volumeControl |
| || mSvp.getMaxVolume() != mVolumeMax) { |
| mSvp = new SessionVolumeProvider(volumeControl, mVolumeMax, mVolume); |
| session.setPlaybackToRemote(mSvp); |
| } |
| } else { |
| // We only know how to handle local and remote, fall back to local if not remote. |
| AudioAttributes.Builder bob = new AudioAttributes.Builder(); |
| bob.setLegacyStreamType(mPlaybackStream); |
| session.setPlaybackToLocal(bob.build()); |
| mSvp = null; |
| } |
| } |
| |
| class SessionVolumeProvider extends VolumeProvider { |
| |
| public SessionVolumeProvider(@VolumeProvider.ControlType int volumeControl, |
| int maxVolume, int currentVolume) { |
| super(volumeControl, maxVolume, currentVolume); |
| } |
| |
| @Override |
| public void onSetVolumeTo(final int volume) { |
| sStatic.mHandler.post(new Runnable() { |
| @Override |
| public void run() { |
| if (mVcb != null) { |
| mVcb.vcb.onVolumeSetRequest(mVcb.route, volume); |
| } |
| } |
| }); |
| } |
| |
| @Override |
| public void onAdjustVolume(final int direction) { |
| sStatic.mHandler.post(new Runnable() { |
| @Override |
| public void run() { |
| if (mVcb != null) { |
| mVcb.vcb.onVolumeUpdateRequest(mVcb.route, direction); |
| } |
| } |
| }); |
| } |
| } |
| } |
| |
| /** |
| * Information about a route that consists of multiple other routes in a group. |
| */ |
| public static class RouteGroup extends RouteInfo { |
| final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>(); |
| private boolean mUpdateName; |
| |
| RouteGroup(RouteCategory category) { |
| super(category); |
| mGroup = this; |
| mVolumeHandling = PLAYBACK_VOLUME_FIXED; |
| } |
| |
| @Override |
| CharSequence getName(Resources res) { |
| if (mUpdateName) updateName(); |
| return super.getName(res); |
| } |
| |
| /** |
| * Add a route to this group. The route must not currently belong to another group. |
| * |
| * @param route route to add to this group |
| */ |
| public void addRoute(RouteInfo route) { |
| if (route.getGroup() != null) { |
| throw new IllegalStateException("Route " + route + " is already part of a group."); |
| } |
| if (route.getCategory() != mCategory) { |
| throw new IllegalArgumentException( |
| "Route cannot be added to a group with a different category. " + |
| "(Route category=" + route.getCategory() + |
| " group category=" + mCategory + ")"); |
| } |
| final int at = mRoutes.size(); |
| mRoutes.add(route); |
| route.mGroup = this; |
| mUpdateName = true; |
| updateVolume(); |
| routeUpdated(); |
| dispatchRouteGrouped(route, this, at); |
| } |
| |
| /** |
| * Add a route to this group before the specified index. |
| * |
| * @param route route to add |
| * @param insertAt insert the new route before this index |
| */ |
| public void addRoute(RouteInfo route, int insertAt) { |
| if (route.getGroup() != null) { |
| throw new IllegalStateException("Route " + route + " is already part of a group."); |
| } |
| if (route.getCategory() != mCategory) { |
| throw new IllegalArgumentException( |
| "Route cannot be added to a group with a different category. " + |
| "(Route category=" + route.getCategory() + |
| " group category=" + mCategory + ")"); |
| } |
| mRoutes.add(insertAt, route); |
| route.mGroup = this; |
| mUpdateName = true; |
| updateVolume(); |
| routeUpdated(); |
| dispatchRouteGrouped(route, this, insertAt); |
| } |
| |
| /** |
| * Remove a route from this group. |
| * |
| * @param route route to remove |
| */ |
| public void removeRoute(RouteInfo route) { |
| if (route.getGroup() != this) { |
| throw new IllegalArgumentException("Route " + route + |
| " is not a member of this group."); |
| } |
| mRoutes.remove(route); |
| route.mGroup = null; |
| mUpdateName = true; |
| updateVolume(); |
| dispatchRouteUngrouped(route, this); |
| routeUpdated(); |
| } |
| |
| /** |
| * Remove the route at the specified index from this group. |
| * |
| * @param index index of the route to remove |
| */ |
| public void removeRoute(int index) { |
| RouteInfo route = mRoutes.remove(index); |
| route.mGroup = null; |
| mUpdateName = true; |
| updateVolume(); |
| dispatchRouteUngrouped(route, this); |
| routeUpdated(); |
| } |
| |
| /** |
| * @return The number of routes in this group |
| */ |
| public int getRouteCount() { |
| return mRoutes.size(); |
| } |
| |
| /** |
| * Return the route in this group at the specified index |
| * |
| * @param index Index to fetch |
| * @return The route at index |
| */ |
| public RouteInfo getRouteAt(int index) { |
| return mRoutes.get(index); |
| } |
| |
| /** |
| * Set an icon that will be used to represent this group. |
| * The system may use this icon in picker UIs or similar. |
| * |
| * @param icon icon drawable to use to represent this group |
| */ |
| public void setIconDrawable(Drawable icon) { |
| mIcon = icon; |
| } |
| |
| /** |
| * Set an icon that will be used to represent this group. |
| * The system may use this icon in picker UIs or similar. |
| * |
| * @param resId Resource ID of an icon drawable to use to represent this group |
| */ |
| public void setIconResource(@DrawableRes int resId) { |
| setIconDrawable(sStatic.mResources.getDrawable(resId)); |
| } |
| |
| @Override |
| public void requestSetVolume(int volume) { |
| final int maxVol = getVolumeMax(); |
| if (maxVol == 0) { |
| return; |
| } |
| |
| final float scaledVolume = (float) volume / maxVol; |
| final int routeCount = getRouteCount(); |
| for (int i = 0; i < routeCount; i++) { |
| final RouteInfo route = getRouteAt(i); |
| final int routeVol = (int) (scaledVolume * route.getVolumeMax()); |
| route.requestSetVolume(routeVol); |
| } |
| if (volume != mVolume) { |
| mVolume = volume; |
| dispatchRouteVolumeChanged(this); |
| } |
| } |
| |
| @Override |
| public void requestUpdateVolume(int direction) { |
| final int maxVol = getVolumeMax(); |
| if (maxVol == 0) { |
| return; |
| } |
| |
| final int routeCount = getRouteCount(); |
| int volume = 0; |
| for (int i = 0; i < routeCount; i++) { |
| final RouteInfo route = getRouteAt(i); |
| route.requestUpdateVolume(direction); |
| final int routeVol = route.getVolume(); |
| if (routeVol > volume) { |
| volume = routeVol; |
| } |
| } |
| if (volume != mVolume) { |
| mVolume = volume; |
| dispatchRouteVolumeChanged(this); |
| } |
| } |
| |
| void memberNameChanged(RouteInfo info, CharSequence name) { |
| mUpdateName = true; |
| routeUpdated(); |
| } |
| |
| void memberStatusChanged(RouteInfo info, CharSequence status) { |
| setStatusInt(status); |
| } |
| |
| void memberVolumeChanged(RouteInfo info) { |
| updateVolume(); |
| } |
| |
| void updateVolume() { |
| // A group always represents the highest component volume value. |
| final int routeCount = getRouteCount(); |
| int volume = 0; |
| for (int i = 0; i < routeCount; i++) { |
| final int routeVol = getRouteAt(i).getVolume(); |
| if (routeVol > volume) { |
| volume = routeVol; |
| } |
| } |
| if (volume != mVolume) { |
| mVolume = volume; |
| dispatchRouteVolumeChanged(this); |
| } |
| } |
| |
| @Override |
| void routeUpdated() { |
| int types = 0; |
| final int count = mRoutes.size(); |
| if (count == 0) { |
| // Don't keep empty groups in the router. |
| MediaRouter.removeRouteStatic(this); |
| return; |
| } |
| |
| int maxVolume = 0; |
| boolean isLocal = true; |
| boolean isFixedVolume = true; |
| for (int i = 0; i < count; i++) { |
| final RouteInfo route = mRoutes.get(i); |
| types |= route.mSupportedTypes; |
| final int routeMaxVolume = route.getVolumeMax(); |
| if (routeMaxVolume > maxVolume) { |
| maxVolume = routeMaxVolume; |
| } |
| isLocal &= route.getPlaybackType() == PLAYBACK_TYPE_LOCAL; |
| isFixedVolume &= route.getVolumeHandling() == PLAYBACK_VOLUME_FIXED; |
| } |
| mPlaybackType = isLocal ? PLAYBACK_TYPE_LOCAL : PLAYBACK_TYPE_REMOTE; |
| mVolumeHandling = isFixedVolume ? PLAYBACK_VOLUME_FIXED : PLAYBACK_VOLUME_VARIABLE; |
| mSupportedTypes = types; |
| mVolumeMax = maxVolume; |
| mIcon = count == 1 ? mRoutes.get(0).getIconDrawable() : null; |
| super.routeUpdated(); |
| } |
| |
| void updateName() { |
| final StringBuilder sb = new StringBuilder(); |
| final int count = mRoutes.size(); |
| for (int i = 0; i < count; i++) { |
| final RouteInfo info = mRoutes.get(i); |
| // TODO: There's probably a much more correct way to localize this. |
| if (i > 0) { |
| sb.append(", "); |
| } |
| sb.append(info.getName()); |
| } |
| mName = sb.toString(); |
| mUpdateName = false; |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder sb = new StringBuilder(super.toString()); |
| sb.append('['); |
| final int count = mRoutes.size(); |
| for (int i = 0; i < count; i++) { |
| if (i > 0) sb.append(", "); |
| sb.append(mRoutes.get(i)); |
| } |
| sb.append(']'); |
| return sb.toString(); |
| } |
| } |
| |
| /** |
| * Definition of a category of routes. All routes belong to a category. |
| */ |
| public static class RouteCategory { |
| CharSequence mName; |
| int mNameResId; |
| int mTypes; |
| final boolean mGroupable; |
| boolean mIsSystem; |
| |
| RouteCategory(CharSequence name, int types, boolean groupable) { |
| mName = name; |
| mTypes = types; |
| mGroupable = groupable; |
| } |
| |
| RouteCategory(int nameResId, int types, boolean groupable) { |
| mNameResId = nameResId; |
| mTypes = types; |
| mGroupable = groupable; |
| } |
| |
| /** |
| * @return the name of this route category |
| */ |
| public CharSequence getName() { |
| return getName(sStatic.mResources); |
| } |
| |
| /** |
| * Return the properly localized/configuration dependent name of this RouteCategory. |
| * |
| * @param context Context to resolve name resources |
| * @return the name of this route category |
| */ |
| public CharSequence getName(Context context) { |
| return getName(context.getResources()); |
| } |
| |
| CharSequence getName(Resources res) { |
| if (mNameResId != 0) { |
| return res.getText(mNameResId); |
| } |
| return mName; |
| } |
| |
| /** |
| * Return the current list of routes in this category that have been added |
| * to the MediaRouter. |
| * |
| * <p>This list will not include routes that are nested within RouteGroups. |
| * A RouteGroup is treated as a single route within its category.</p> |
| * |
| * @param out a List to fill with the routes in this category. If this parameter is |
| * non-null, it will be cleared, filled with the current routes with this |
| * category, and returned. If this parameter is null, a new List will be |
| * allocated to report the category's current routes. |
| * @return A list with the routes in this category that have been added to the MediaRouter. |
| */ |
| public List<RouteInfo> getRoutes(List<RouteInfo> out) { |
| if (out == null) { |
| out = new ArrayList<RouteInfo>(); |
| } else { |
| out.clear(); |
| } |
| |
| final int count = getRouteCountStatic(); |
| for (int i = 0; i < count; i++) { |
| final RouteInfo route = getRouteAtStatic(i); |
| if (route.mCategory == this) { |
| out.add(route); |
| } |
| } |
| return out; |
| } |
| |
| /** |
| * @return Flag set describing the route types supported by this category |
| */ |
| public int getSupportedTypes() { |
| return mTypes; |
| } |
| |
| /** |
| * Return whether or not this category supports grouping. |
| * |
| * <p>If this method returns true, all routes obtained from this category |
| * via calls to {@link #getRouteAt(int)} will be {@link MediaRouter.RouteGroup}s.</p> |
| * |
| * @return true if this category supports |
| */ |
| public boolean isGroupable() { |
| return mGroupable; |
| } |
| |
| /** |
| * @return true if this is the category reserved for system routes. |
| * @hide |
| */ |
| public boolean isSystem() { |
| return mIsSystem; |
| } |
| |
| @Override |
| public String toString() { |
| return "RouteCategory{ name=" + getName() + " types=" + typesToString(mTypes) + |
| " groupable=" + mGroupable + " }"; |
| } |
| } |
| |
| static class CallbackInfo { |
| public int type; |
| public int flags; |
| public final Callback cb; |
| public final MediaRouter router; |
| |
| public CallbackInfo(Callback cb, int type, int flags, MediaRouter router) { |
| this.cb = cb; |
| this.type = type; |
| this.flags = flags; |
| this.router = router; |
| } |
| |
| public boolean filterRouteEvent(RouteInfo route) { |
| return filterRouteEvent(route.mSupportedTypes); |
| } |
| |
| public boolean filterRouteEvent(int supportedTypes) { |
| return (flags & CALLBACK_FLAG_UNFILTERED_EVENTS) != 0 |
| || (type & supportedTypes) != 0; |
| } |
| } |
| |
| /** |
| * Interface for receiving events about media routing changes. |
| * All methods of this interface will be called from the application's main thread. |
| * <p> |
| * A Callback will only receive events relevant to routes that the callback |
| * was registered for unless the {@link MediaRouter#CALLBACK_FLAG_UNFILTERED_EVENTS} |
| * flag was specified in {@link MediaRouter#addCallback(int, Callback, int)}. |
| * </p> |
| * |
| * @see MediaRouter#addCallback(int, Callback, int) |
| * @see MediaRouter#removeCallback(Callback) |
| */ |
| public static abstract class Callback { |
| /** |
| * Called when the supplied route becomes selected as the active route |
| * for the given route type. |
| * |
| * @param router the MediaRouter reporting the event |
| * @param type Type flag set indicating the routes that have been selected |
| * @param info Route that has been selected for the given route types |
| */ |
| public abstract void onRouteSelected(MediaRouter router, int type, RouteInfo info); |
| |
| /** |
| * Called when the supplied route becomes unselected as the active route |
| * for the given route type. |
| * |
| * @param router the MediaRouter reporting the event |
| * @param type Type flag set indicating the routes that have been unselected |
| * @param info Route that has been unselected for the given route types |
| */ |
| public abstract void onRouteUnselected(MediaRouter router, int type, RouteInfo info); |
| |
| /** |
| * Called when a route for the specified type was added. |
| * |
| * @param router the MediaRouter reporting the event |
| * @param info Route that has become available for use |
| */ |
| public abstract void onRouteAdded(MediaRouter router, RouteInfo info); |
| |
| /** |
| * Called when a route for the specified type was removed. |
| * |
| * @param router the MediaRouter reporting the event |
| * @param info Route that has been removed from availability |
| */ |
| public abstract void onRouteRemoved(MediaRouter router, RouteInfo info); |
| |
| /** |
| * Called when an aspect of the indicated route has changed. |
| * |
| * <p>This will not indicate that the types supported by this route have |
| * changed, only that cosmetic info such as name or status have been updated.</p> |
| * |
| * @param router the MediaRouter reporting the event |
| * @param info The route that was changed |
| */ |
| public abstract void onRouteChanged(MediaRouter router, RouteInfo info); |
| |
| /** |
| * Called when a route is added to a group. |
| * |
| * @param router the MediaRouter reporting the event |
| * @param info The route that was added |
| * @param group The group the route was added to |
| * @param index The route index within group that info was added at |
| */ |
| public abstract void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group, |
| int index); |
| |
| /** |
| * Called when a route is removed from a group. |
| * |
| * @param router the MediaRouter reporting the event |
| * @param info The route that was removed |
| * @param group The group the route was removed from |
| */ |
| public abstract void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group); |
| |
| /** |
| * Called when a route's volume changes. |
| * |
| * @param router the MediaRouter reporting the event |
| * @param info The route with altered volume |
| */ |
| public abstract void onRouteVolumeChanged(MediaRouter router, RouteInfo info); |
| |
| /** |
| * Called when a route's presentation display changes. |
| * <p> |
| * This method is called whenever the route's presentation display becomes |
| * available, is removes or has changes to some of its properties (such as its size). |
| * </p> |
| * |
| * @param router the MediaRouter reporting the event |
| * @param info The route whose presentation display changed |
| * |
| * @see RouteInfo#getPresentationDisplay() |
| */ |
| public void onRoutePresentationDisplayChanged(MediaRouter router, RouteInfo info) { |
| } |
| } |
| |
| /** |
| * Stub implementation of {@link MediaRouter.Callback}. |
| * Each abstract method is defined as a no-op. Override just the ones |
| * you need. |
| */ |
| public static class SimpleCallback extends Callback { |
| |
| @Override |
| public void onRouteSelected(MediaRouter router, int type, RouteInfo info) { |
| } |
| |
| @Override |
| public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) { |
| } |
| |
| @Override |
| public void onRouteAdded(MediaRouter router, RouteInfo info) { |
| } |
| |
| @Override |
| public void onRouteRemoved(MediaRouter router, RouteInfo info) { |
| } |
| |
| @Override |
| public void onRouteChanged(MediaRouter router, RouteInfo info) { |
| } |
| |
| @Override |
| public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group, |
| int index) { |
| } |
| |
| @Override |
| public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) { |
| } |
| |
| @Override |
| public void onRouteVolumeChanged(MediaRouter router, RouteInfo info) { |
| } |
| } |
| |
| static class VolumeCallbackInfo { |
| public final VolumeCallback vcb; |
| public final RouteInfo route; |
| |
| public VolumeCallbackInfo(VolumeCallback vcb, RouteInfo route) { |
| this.vcb = vcb; |
| this.route = route; |
| } |
| } |
| |
| /** |
| * Interface for receiving events about volume changes. |
| * All methods of this interface will be called from the application's main thread. |
| * |
| * <p>A VolumeCallback will only receive events relevant to routes that the callback |
| * was registered for.</p> |
| * |
| * @see UserRouteInfo#setVolumeCallback(VolumeCallback) |
| */ |
| public static abstract class VolumeCallback { |
| /** |
| * Called when the volume for the route should be increased or decreased. |
| * @param info the route affected by this event |
| * @param direction an integer indicating whether the volume is to be increased |
| * (positive value) or decreased (negative value). |
| * For bundled changes, the absolute value indicates the number of changes |
| * in the same direction, e.g. +3 corresponds to three "volume up" changes. |
| */ |
| public abstract void onVolumeUpdateRequest(RouteInfo info, int direction); |
| /** |
| * Called when the volume for the route should be set to the given value |
| * @param info the route affected by this event |
| * @param volume an integer indicating the new volume value that should be used, always |
| * between 0 and the value set by {@link UserRouteInfo#setVolumeMax(int)}. |
| */ |
| public abstract void onVolumeSetRequest(RouteInfo info, int volume); |
| } |
| |
| static class VolumeChangeReceiver extends BroadcastReceiver { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| if (intent.getAction().equals(AudioManager.VOLUME_CHANGED_ACTION)) { |
| final int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, |
| -1); |
| if (streamType != AudioManager.STREAM_MUSIC) { |
| return; |
| } |
| |
| final int newVolume = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, 0); |
| final int oldVolume = intent.getIntExtra( |
| AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, 0); |
| if (newVolume != oldVolume) { |
| systemVolumeChanged(newVolume); |
| } |
| } |
| } |
| } |
| |
| static class WifiDisplayStatusChangedReceiver extends BroadcastReceiver { |
| @Override |
| public void onReceive(Context context, Intent intent) { |
| if (intent.getAction().equals(DisplayManager.ACTION_WIFI_DISPLAY_STATUS_CHANGED)) { |
| updateWifiDisplayStatus((WifiDisplayStatus) intent.getParcelableExtra( |
| DisplayManager.EXTRA_WIFI_DISPLAY_STATUS)); |
| } |
| } |
| } |
| } |