Home | History | Annotate | Download | only in media
      1 /*
      2  * Copyright (C) 2012 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.media;
     18 
     19 import android.Manifest;
     20 import android.annotation.DrawableRes;
     21 import android.annotation.IntDef;
     22 import android.annotation.NonNull;
     23 import android.annotation.SystemService;
     24 import android.app.ActivityThread;
     25 import android.content.BroadcastReceiver;
     26 import android.content.Context;
     27 import android.content.Intent;
     28 import android.content.IntentFilter;
     29 import android.content.pm.PackageManager;
     30 import android.content.res.Resources;
     31 import android.graphics.drawable.Drawable;
     32 import android.hardware.display.DisplayManager;
     33 import android.hardware.display.WifiDisplay;
     34 import android.hardware.display.WifiDisplayStatus;
     35 import android.media.session.MediaSession;
     36 import android.os.Handler;
     37 import android.os.IBinder;
     38 import android.os.Process;
     39 import android.os.RemoteException;
     40 import android.os.ServiceManager;
     41 import android.os.UserHandle;
     42 import android.text.TextUtils;
     43 import android.util.Log;
     44 import android.view.Display;
     45 
     46 import java.lang.annotation.Retention;
     47 import java.lang.annotation.RetentionPolicy;
     48 import java.util.ArrayList;
     49 import java.util.HashMap;
     50 import java.util.List;
     51 import java.util.Objects;
     52 import java.util.concurrent.CopyOnWriteArrayList;
     53 
     54 /**
     55  * MediaRouter allows applications to control the routing of media channels
     56  * and streams from the current device to external speakers and destination devices.
     57  *
     58  * <p>A MediaRouter is retrieved through {@link Context#getSystemService(String)
     59  * Context.getSystemService()} of a {@link Context#MEDIA_ROUTER_SERVICE
     60  * Context.MEDIA_ROUTER_SERVICE}.
     61  *
     62  * <p>The media router API is not thread-safe; all interactions with it must be
     63  * done from the main thread of the process.</p>
     64  */
     65 @SystemService(Context.MEDIA_ROUTER_SERVICE)
     66 public class MediaRouter {
     67     private static final String TAG = "MediaRouter";
     68     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
     69 
     70     static class Static implements DisplayManager.DisplayListener {
     71         final String mPackageName;
     72         final Resources mResources;
     73         final IAudioService mAudioService;
     74         final DisplayManager mDisplayService;
     75         final IMediaRouterService mMediaRouterService;
     76         final Handler mHandler;
     77         final CopyOnWriteArrayList<CallbackInfo> mCallbacks =
     78                 new CopyOnWriteArrayList<CallbackInfo>();
     79 
     80         final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>();
     81         final ArrayList<RouteCategory> mCategories = new ArrayList<RouteCategory>();
     82 
     83         final RouteCategory mSystemCategory;
     84 
     85         final AudioRoutesInfo mCurAudioRoutesInfo = new AudioRoutesInfo();
     86 
     87         RouteInfo mDefaultAudioVideo;
     88         RouteInfo mBluetoothA2dpRoute;
     89 
     90         RouteInfo mSelectedRoute;
     91 
     92         final boolean mCanConfigureWifiDisplays;
     93         boolean mActivelyScanningWifiDisplays;
     94         String mPreviousActiveWifiDisplayAddress;
     95 
     96         int mDiscoveryRequestRouteTypes;
     97         boolean mDiscoverRequestActiveScan;
     98 
     99         int mCurrentUserId = -1;
    100         IMediaRouterClient mClient;
    101         MediaRouterClientState mClientState;
    102 
    103         final IAudioRoutesObserver.Stub mAudioRoutesObserver = new IAudioRoutesObserver.Stub() {
    104             @Override
    105             public void dispatchAudioRoutesChanged(final AudioRoutesInfo newRoutes) {
    106                 mHandler.post(new Runnable() {
    107                     @Override public void run() {
    108                         updateAudioRoutes(newRoutes);
    109                     }
    110                 });
    111             }
    112         };
    113 
    114         Static(Context appContext) {
    115             mPackageName = appContext.getPackageName();
    116             mResources = appContext.getResources();
    117             mHandler = new Handler(appContext.getMainLooper());
    118 
    119             IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
    120             mAudioService = IAudioService.Stub.asInterface(b);
    121 
    122             mDisplayService = (DisplayManager) appContext.getSystemService(Context.DISPLAY_SERVICE);
    123 
    124             mMediaRouterService = IMediaRouterService.Stub.asInterface(
    125                     ServiceManager.getService(Context.MEDIA_ROUTER_SERVICE));
    126 
    127             mSystemCategory = new RouteCategory(
    128                     com.android.internal.R.string.default_audio_route_category_name,
    129                     ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO, false);
    130             mSystemCategory.mIsSystem = true;
    131 
    132             // Only the system can configure wifi displays.  The display manager
    133             // enforces this with a permission check.  Set a flag here so that we
    134             // know whether this process is actually allowed to scan and connect.
    135             mCanConfigureWifiDisplays = appContext.checkPermission(
    136                     Manifest.permission.CONFIGURE_WIFI_DISPLAY,
    137                     Process.myPid(), Process.myUid()) == PackageManager.PERMISSION_GRANTED;
    138         }
    139 
    140         // Called after sStatic is initialized
    141         void startMonitoringRoutes(Context appContext) {
    142             mDefaultAudioVideo = new RouteInfo(mSystemCategory);
    143             mDefaultAudioVideo.mNameResId = com.android.internal.R.string.default_audio_route_name;
    144             mDefaultAudioVideo.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO;
    145             mDefaultAudioVideo.updatePresentationDisplay();
    146             if (((AudioManager) appContext.getSystemService(Context.AUDIO_SERVICE))
    147                     .isVolumeFixed()) {
    148                 mDefaultAudioVideo.mVolumeHandling = RouteInfo.PLAYBACK_VOLUME_FIXED;
    149             }
    150 
    151             addRouteStatic(mDefaultAudioVideo);
    152 
    153             // This will select the active wifi display route if there is one.
    154             updateWifiDisplayStatus(mDisplayService.getWifiDisplayStatus());
    155 
    156             appContext.registerReceiver(new WifiDisplayStatusChangedReceiver(),
    157                     new IntentFilter(DisplayManager.ACTION_WIFI_DISPLAY_STATUS_CHANGED));
    158             appContext.registerReceiver(new VolumeChangeReceiver(),
    159                     new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION));
    160 
    161             mDisplayService.registerDisplayListener(this, mHandler);
    162 
    163             AudioRoutesInfo newAudioRoutes = null;
    164             try {
    165                 newAudioRoutes = mAudioService.startWatchingRoutes(mAudioRoutesObserver);
    166             } catch (RemoteException e) {
    167             }
    168             if (newAudioRoutes != null) {
    169                 // This will select the active BT route if there is one and the current
    170                 // selected route is the default system route, or if there is no selected
    171                 // route yet.
    172                 updateAudioRoutes(newAudioRoutes);
    173             }
    174 
    175             // Bind to the media router service.
    176             rebindAsUser(UserHandle.myUserId());
    177 
    178             // Select the default route if the above didn't sync us up
    179             // appropriately with relevant system state.
    180             if (mSelectedRoute == null) {
    181                 selectDefaultRouteStatic();
    182             }
    183         }
    184 
    185         void updateAudioRoutes(AudioRoutesInfo newRoutes) {
    186             boolean audioRoutesChanged = false;
    187             boolean forceUseDefaultRoute = false;
    188 
    189             if (newRoutes.mainType != mCurAudioRoutesInfo.mainType) {
    190                 mCurAudioRoutesInfo.mainType = newRoutes.mainType;
    191                 int name;
    192                 if ((newRoutes.mainType & AudioRoutesInfo.MAIN_HEADPHONES) != 0
    193                         || (newRoutes.mainType & AudioRoutesInfo.MAIN_HEADSET) != 0) {
    194                     name = com.android.internal.R.string.default_audio_route_name_headphones;
    195                 } else if ((newRoutes.mainType & AudioRoutesInfo.MAIN_DOCK_SPEAKERS) != 0) {
    196                     name = com.android.internal.R.string.default_audio_route_name_dock_speakers;
    197                 } else if ((newRoutes.mainType&AudioRoutesInfo.MAIN_HDMI) != 0) {
    198                     name = com.android.internal.R.string.default_audio_route_name_hdmi;
    199                 } else if ((newRoutes.mainType&AudioRoutesInfo.MAIN_USB) != 0) {
    200                     name = com.android.internal.R.string.default_audio_route_name_usb;
    201                 } else {
    202                     name = com.android.internal.R.string.default_audio_route_name;
    203                 }
    204                 mDefaultAudioVideo.mNameResId = name;
    205                 dispatchRouteChanged(mDefaultAudioVideo);
    206 
    207                 if ((newRoutes.mainType & (AudioRoutesInfo.MAIN_HEADSET
    208                         | AudioRoutesInfo.MAIN_HEADPHONES | AudioRoutesInfo.MAIN_USB)) != 0) {
    209                     forceUseDefaultRoute = true;
    210                 }
    211                 audioRoutesChanged = true;
    212             }
    213 
    214             if (!TextUtils.equals(newRoutes.bluetoothName, mCurAudioRoutesInfo.bluetoothName)) {
    215                 forceUseDefaultRoute = false;
    216                 mCurAudioRoutesInfo.bluetoothName = newRoutes.bluetoothName;
    217                 if (mCurAudioRoutesInfo.bluetoothName != null) {
    218                     if (mBluetoothA2dpRoute == null) {
    219                         // BT connected
    220                         final RouteInfo info = new RouteInfo(mSystemCategory);
    221                         info.mName = mCurAudioRoutesInfo.bluetoothName;
    222                         info.mDescription = mResources.getText(
    223                                 com.android.internal.R.string.bluetooth_a2dp_audio_route_name);
    224                         info.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO;
    225                         info.mDeviceType = RouteInfo.DEVICE_TYPE_BLUETOOTH;
    226                         mBluetoothA2dpRoute = info;
    227                         addRouteStatic(mBluetoothA2dpRoute);
    228                     } else {
    229                         mBluetoothA2dpRoute.mName = mCurAudioRoutesInfo.bluetoothName;
    230                         dispatchRouteChanged(mBluetoothA2dpRoute);
    231                     }
    232                 } else if (mBluetoothA2dpRoute != null) {
    233                     // BT disconnected
    234                     removeRouteStatic(mBluetoothA2dpRoute);
    235                     mBluetoothA2dpRoute = null;
    236                 }
    237                 audioRoutesChanged = true;
    238             }
    239 
    240             if (audioRoutesChanged) {
    241                 Log.v(TAG, "Audio routes updated: " + newRoutes + ", a2dp=" + isBluetoothA2dpOn());
    242                 if (mSelectedRoute == null || mSelectedRoute == mDefaultAudioVideo
    243                         || mSelectedRoute == mBluetoothA2dpRoute) {
    244                     if (forceUseDefaultRoute || mBluetoothA2dpRoute == null) {
    245                         selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO, mDefaultAudioVideo, false);
    246                     } else {
    247                         selectRouteStatic(ROUTE_TYPE_LIVE_AUDIO, mBluetoothA2dpRoute, false);
    248                     }
    249                 }
    250             }
    251         }
    252 
    253         boolean isBluetoothA2dpOn() {
    254             try {
    255                 return mBluetoothA2dpRoute != null && mAudioService.isBluetoothA2dpOn();
    256             } catch (RemoteException e) {
    257                 Log.e(TAG, "Error querying Bluetooth A2DP state", e);
    258                 return false;
    259             }
    260         }
    261 
    262         void updateDiscoveryRequest() {
    263             // What are we looking for today?
    264             int routeTypes = 0;
    265             int passiveRouteTypes = 0;
    266             boolean activeScan = false;
    267             boolean activeScanWifiDisplay = false;
    268             final int count = mCallbacks.size();
    269             for (int i = 0; i < count; i++) {
    270                 CallbackInfo cbi = mCallbacks.get(i);
    271                 if ((cbi.flags & (CALLBACK_FLAG_PERFORM_ACTIVE_SCAN
    272                         | CALLBACK_FLAG_REQUEST_DISCOVERY)) != 0) {
    273                     // Discovery explicitly requested.
    274                     routeTypes |= cbi.type;
    275                 } else if ((cbi.flags & CALLBACK_FLAG_PASSIVE_DISCOVERY) != 0) {
    276                     // Discovery only passively requested.
    277                     passiveRouteTypes |= cbi.type;
    278                 } else {
    279                     // Legacy case since applications don't specify the discovery flag.
    280                     // Unfortunately we just have to assume they always need discovery
    281                     // whenever they have a callback registered.
    282                     routeTypes |= cbi.type;
    283                 }
    284                 if ((cbi.flags & CALLBACK_FLAG_PERFORM_ACTIVE_SCAN) != 0) {
    285                     activeScan = true;
    286                     if ((cbi.type & ROUTE_TYPE_REMOTE_DISPLAY) != 0) {
    287                         activeScanWifiDisplay = true;
    288                     }
    289                 }
    290             }
    291             if (routeTypes != 0 || activeScan) {
    292                 // If someone else requests discovery then enable the passive listeners.
    293                 // This is used by the MediaRouteButton and MediaRouteActionProvider since
    294                 // they don't receive lifecycle callbacks from the Activity.
    295                 routeTypes |= passiveRouteTypes;
    296             }
    297 
    298             // Update wifi display scanning.
    299             // TODO: All of this should be managed by the media router service.
    300             if (mCanConfigureWifiDisplays) {
    301                 if (mSelectedRoute != null
    302                         && mSelectedRoute.matchesTypes(ROUTE_TYPE_REMOTE_DISPLAY)) {
    303                     // Don't scan while already connected to a remote display since
    304                     // it may interfere with the ongoing transmission.
    305                     activeScanWifiDisplay = false;
    306                 }
    307                 if (activeScanWifiDisplay) {
    308                     if (!mActivelyScanningWifiDisplays) {
    309                         mActivelyScanningWifiDisplays = true;
    310                         mDisplayService.startWifiDisplayScan();
    311                     }
    312                 } else {
    313                     if (mActivelyScanningWifiDisplays) {
    314                         mActivelyScanningWifiDisplays = false;
    315                         mDisplayService.stopWifiDisplayScan();
    316                     }
    317                 }
    318             }
    319 
    320             // Tell the media router service all about it.
    321             if (routeTypes != mDiscoveryRequestRouteTypes
    322                     || activeScan != mDiscoverRequestActiveScan) {
    323                 mDiscoveryRequestRouteTypes = routeTypes;
    324                 mDiscoverRequestActiveScan = activeScan;
    325                 publishClientDiscoveryRequest();
    326             }
    327         }
    328 
    329         @Override
    330         public void onDisplayAdded(int displayId) {
    331             updatePresentationDisplays(displayId);
    332         }
    333 
    334         @Override
    335         public void onDisplayChanged(int displayId) {
    336             updatePresentationDisplays(displayId);
    337         }
    338 
    339         @Override
    340         public void onDisplayRemoved(int displayId) {
    341             updatePresentationDisplays(displayId);
    342         }
    343 
    344         public Display[] getAllPresentationDisplays() {
    345             return mDisplayService.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION);
    346         }
    347 
    348         private void updatePresentationDisplays(int changedDisplayId) {
    349             final int count = mRoutes.size();
    350             for (int i = 0; i < count; i++) {
    351                 final RouteInfo route = mRoutes.get(i);
    352                 if (route.updatePresentationDisplay() || (route.mPresentationDisplay != null
    353                         && route.mPresentationDisplay.getDisplayId() == changedDisplayId)) {
    354                     dispatchRoutePresentationDisplayChanged(route);
    355                 }
    356             }
    357         }
    358 
    359         void setSelectedRoute(RouteInfo info, boolean explicit) {
    360             // Must be non-reentrant.
    361             mSelectedRoute = info;
    362             publishClientSelectedRoute(explicit);
    363         }
    364 
    365         void rebindAsUser(int userId) {
    366             if (mCurrentUserId != userId || userId < 0 || mClient == null) {
    367                 if (mClient != null) {
    368                     try {
    369                         mMediaRouterService.unregisterClient(mClient);
    370                     } catch (RemoteException ex) {
    371                         Log.e(TAG, "Unable to unregister media router client.", ex);
    372                     }
    373                     mClient = null;
    374                 }
    375 
    376                 mCurrentUserId = userId;
    377 
    378                 try {
    379                     Client client = new Client();
    380                     mMediaRouterService.registerClientAsUser(client, mPackageName, userId);
    381                     mClient = client;
    382                 } catch (RemoteException ex) {
    383                     Log.e(TAG, "Unable to register media router client.", ex);
    384                 }
    385 
    386                 publishClientDiscoveryRequest();
    387                 publishClientSelectedRoute(false);
    388                 updateClientState();
    389             }
    390         }
    391 
    392         void publishClientDiscoveryRequest() {
    393             if (mClient != null) {
    394                 try {
    395                     mMediaRouterService.setDiscoveryRequest(mClient,
    396                             mDiscoveryRequestRouteTypes, mDiscoverRequestActiveScan);
    397                 } catch (RemoteException ex) {
    398                     Log.e(TAG, "Unable to publish media router client discovery request.", ex);
    399                 }
    400             }
    401         }
    402 
    403         void publishClientSelectedRoute(boolean explicit) {
    404             if (mClient != null) {
    405                 try {
    406                     mMediaRouterService.setSelectedRoute(mClient,
    407                             mSelectedRoute != null ? mSelectedRoute.mGlobalRouteId : null,
    408                             explicit);
    409                 } catch (RemoteException ex) {
    410                     Log.e(TAG, "Unable to publish media router client selected route.", ex);
    411                 }
    412             }
    413         }
    414 
    415         void updateClientState() {
    416             // Update the client state.
    417             mClientState = null;
    418             if (mClient != null) {
    419                 try {
    420                     mClientState = mMediaRouterService.getState(mClient);
    421                 } catch (RemoteException ex) {
    422                     Log.e(TAG, "Unable to retrieve media router client state.", ex);
    423                 }
    424             }
    425             final ArrayList<MediaRouterClientState.RouteInfo> globalRoutes =
    426                     mClientState != null ? mClientState.routes : null;
    427 
    428             // Add or update routes.
    429             final int globalRouteCount = globalRoutes != null ? globalRoutes.size() : 0;
    430             for (int i = 0; i < globalRouteCount; i++) {
    431                 final MediaRouterClientState.RouteInfo globalRoute = globalRoutes.get(i);
    432                 RouteInfo route = findGlobalRoute(globalRoute.id);
    433                 if (route == null) {
    434                     route = makeGlobalRoute(globalRoute);
    435                     addRouteStatic(route);
    436                 } else {
    437                     updateGlobalRoute(route, globalRoute);
    438                 }
    439             }
    440 
    441             // Remove defunct routes.
    442             outer: for (int i = mRoutes.size(); i-- > 0; ) {
    443                 final RouteInfo route = mRoutes.get(i);
    444                 final String globalRouteId = route.mGlobalRouteId;
    445                 if (globalRouteId != null) {
    446                     for (int j = 0; j < globalRouteCount; j++) {
    447                         MediaRouterClientState.RouteInfo globalRoute = globalRoutes.get(j);
    448                         if (globalRouteId.equals(globalRoute.id)) {
    449                             continue outer; // found
    450                         }
    451                     }
    452                     // not found
    453                     removeRouteStatic(route);
    454                 }
    455             }
    456         }
    457 
    458         void requestSetVolume(RouteInfo route, int volume) {
    459             if (route.mGlobalRouteId != null && mClient != null) {
    460                 try {
    461                     mMediaRouterService.requestSetVolume(mClient,
    462                             route.mGlobalRouteId, volume);
    463                 } catch (RemoteException ex) {
    464                     Log.w(TAG, "Unable to request volume change.", ex);
    465                 }
    466             }
    467         }
    468 
    469         void requestUpdateVolume(RouteInfo route, int direction) {
    470             if (route.mGlobalRouteId != null && mClient != null) {
    471                 try {
    472                     mMediaRouterService.requestUpdateVolume(mClient,
    473                             route.mGlobalRouteId, direction);
    474                 } catch (RemoteException ex) {
    475                     Log.w(TAG, "Unable to request volume change.", ex);
    476                 }
    477             }
    478         }
    479 
    480         RouteInfo makeGlobalRoute(MediaRouterClientState.RouteInfo globalRoute) {
    481             RouteInfo route = new RouteInfo(mSystemCategory);
    482             route.mGlobalRouteId = globalRoute.id;
    483             route.mName = globalRoute.name;
    484             route.mDescription = globalRoute.description;
    485             route.mSupportedTypes = globalRoute.supportedTypes;
    486             route.mDeviceType = globalRoute.deviceType;
    487             route.mEnabled = globalRoute.enabled;
    488             route.setRealStatusCode(globalRoute.statusCode);
    489             route.mPlaybackType = globalRoute.playbackType;
    490             route.mPlaybackStream = globalRoute.playbackStream;
    491             route.mVolume = globalRoute.volume;
    492             route.mVolumeMax = globalRoute.volumeMax;
    493             route.mVolumeHandling = globalRoute.volumeHandling;
    494             route.mPresentationDisplayId = globalRoute.presentationDisplayId;
    495             route.updatePresentationDisplay();
    496             return route;
    497         }
    498 
    499         void updateGlobalRoute(RouteInfo route, MediaRouterClientState.RouteInfo globalRoute) {
    500             boolean changed = false;
    501             boolean volumeChanged = false;
    502             boolean presentationDisplayChanged = false;
    503 
    504             if (!Objects.equals(route.mName, globalRoute.name)) {
    505                 route.mName = globalRoute.name;
    506                 changed = true;
    507             }
    508             if (!Objects.equals(route.mDescription, globalRoute.description)) {
    509                 route.mDescription = globalRoute.description;
    510                 changed = true;
    511             }
    512             final int oldSupportedTypes = route.mSupportedTypes;
    513             if (oldSupportedTypes != globalRoute.supportedTypes) {
    514                 route.mSupportedTypes = globalRoute.supportedTypes;
    515                 changed = true;
    516             }
    517             if (route.mEnabled != globalRoute.enabled) {
    518                 route.mEnabled = globalRoute.enabled;
    519                 changed = true;
    520             }
    521             if (route.mRealStatusCode != globalRoute.statusCode) {
    522                 route.setRealStatusCode(globalRoute.statusCode);
    523                 changed = true;
    524             }
    525             if (route.mPlaybackType != globalRoute.playbackType) {
    526                 route.mPlaybackType = globalRoute.playbackType;
    527                 changed = true;
    528             }
    529             if (route.mPlaybackStream != globalRoute.playbackStream) {
    530                 route.mPlaybackStream = globalRoute.playbackStream;
    531                 changed = true;
    532             }
    533             if (route.mVolume != globalRoute.volume) {
    534                 route.mVolume = globalRoute.volume;
    535                 changed = true;
    536                 volumeChanged = true;
    537             }
    538             if (route.mVolumeMax != globalRoute.volumeMax) {
    539                 route.mVolumeMax = globalRoute.volumeMax;
    540                 changed = true;
    541                 volumeChanged = true;
    542             }
    543             if (route.mVolumeHandling != globalRoute.volumeHandling) {
    544                 route.mVolumeHandling = globalRoute.volumeHandling;
    545                 changed = true;
    546                 volumeChanged = true;
    547             }
    548             if (route.mPresentationDisplayId != globalRoute.presentationDisplayId) {
    549                 route.mPresentationDisplayId = globalRoute.presentationDisplayId;
    550                 route.updatePresentationDisplay();
    551                 changed = true;
    552                 presentationDisplayChanged = true;
    553             }
    554 
    555             if (changed) {
    556                 dispatchRouteChanged(route, oldSupportedTypes);
    557             }
    558             if (volumeChanged) {
    559                 dispatchRouteVolumeChanged(route);
    560             }
    561             if (presentationDisplayChanged) {
    562                 dispatchRoutePresentationDisplayChanged(route);
    563             }
    564         }
    565 
    566         RouteInfo findGlobalRoute(String globalRouteId) {
    567             final int count = mRoutes.size();
    568             for (int i = 0; i < count; i++) {
    569                 final RouteInfo route = mRoutes.get(i);
    570                 if (globalRouteId.equals(route.mGlobalRouteId)) {
    571                     return route;
    572                 }
    573             }
    574             return null;
    575         }
    576 
    577         boolean isPlaybackActive() {
    578             if (mClient != null) {
    579                 try {
    580                     return mMediaRouterService.isPlaybackActive(mClient);
    581                 } catch (RemoteException ex) {
    582                     Log.e(TAG, "Unable to retrieve playback active state.", ex);
    583                 }
    584             }
    585             return false;
    586         }
    587 
    588         final class Client extends IMediaRouterClient.Stub {
    589             @Override
    590             public void onStateChanged() {
    591                 mHandler.post(new Runnable() {
    592                     @Override
    593                     public void run() {
    594                         if (Client.this == mClient) {
    595                             updateClientState();
    596                         }
    597                     }
    598                 });
    599             }
    600 
    601             @Override
    602             public void onRestoreRoute() {
    603                 mHandler.post(new Runnable() {
    604                     @Override
    605                     public void run() {
    606                         // Skip restoring route if the selected route is not a system audio route,
    607                         // MediaRouter is initializing, or mClient was changed.
    608                         if (Client.this != mClient || mSelectedRoute == null
    609                                 || (mSelectedRoute != mDefaultAudioVideo
    610                                         && mSelectedRoute != mBluetoothA2dpRoute)) {
    611                             return;
    612                         }
    613                         Log.v(TAG, "onRestoreRoute() : route=" + mSelectedRoute);
    614                         mSelectedRoute.select();
    615                     }
    616                 });
    617             }
    618         }
    619     }
    620 
    621     static Static sStatic;
    622 
    623     /**
    624      * Route type flag for live audio.
    625      *
    626      * <p>A device that supports live audio routing will allow the media audio stream
    627      * to be routed to supported destinations. This can include internal speakers or
    628      * audio jacks on the device itself, A2DP devices, and more.</p>
    629      *
    630      * <p>Once initiated this routing is transparent to the application. All audio
    631      * played on the media stream will be routed to the selected destination.</p>
    632      */
    633     public static final int ROUTE_TYPE_LIVE_AUDIO = 1 << 0;
    634 
    635     /**
    636      * Route type flag for live video.
    637      *
    638      * <p>A device that supports live video routing will allow a mirrored version
    639      * of the device's primary display or a customized
    640      * {@link android.app.Presentation Presentation} to be routed to supported destinations.</p>
    641      *
    642      * <p>Once initiated, display mirroring is transparent to the application.
    643      * While remote routing is active the application may use a
    644      * {@link android.app.Presentation Presentation} to replace the mirrored view
    645      * on the external display with different content.</p>
    646      *
    647      * @see RouteInfo#getPresentationDisplay()
    648      * @see android.app.Presentation
    649      */
    650     public static final int ROUTE_TYPE_LIVE_VIDEO = 1 << 1;
    651 
    652     /**
    653      * Temporary interop constant to identify remote displays.
    654      * @hide To be removed when media router API is updated.
    655      */
    656     public static final int ROUTE_TYPE_REMOTE_DISPLAY = 1 << 2;
    657 
    658     /**
    659      * Route type flag for application-specific usage.
    660      *
    661      * <p>Unlike other media route types, user routes are managed by the application.
    662      * The MediaRouter will manage and dispatch events for user routes, but the application
    663      * is expected to interpret the meaning of these events and perform the requested
    664      * routing tasks.</p>
    665      */
    666     public static final int ROUTE_TYPE_USER = 1 << 23;
    667 
    668     static final int ROUTE_TYPE_ANY = ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO
    669             | ROUTE_TYPE_REMOTE_DISPLAY | ROUTE_TYPE_USER;
    670 
    671     /**
    672      * Flag for {@link #addCallback}: Actively scan for routes while this callback
    673      * is registered.
    674      * <p>
    675      * When this flag is specified, the media router will actively scan for new
    676      * routes.  Certain routes, such as wifi display routes, may not be discoverable
    677      * except when actively scanning.  This flag is typically used when the route picker
    678      * dialog has been opened by the user to ensure that the route information is
    679      * up to date.
    680      * </p><p>
    681      * Active scanning may consume a significant amount of power and may have intrusive
    682      * effects on wireless connectivity.  Therefore it is important that active scanning
    683      * only be requested when it is actually needed to satisfy a user request to
    684      * discover and select a new route.
    685      * </p>
    686      */
    687     public static final int CALLBACK_FLAG_PERFORM_ACTIVE_SCAN = 1 << 0;
    688 
    689     /**
    690      * Flag for {@link #addCallback}: Do not filter route events.
    691      * <p>
    692      * When this flag is specified, the callback will be invoked for event that affect any
    693      * route even if they do not match the callback's filter.
    694      * </p>
    695      */
    696     public static final int CALLBACK_FLAG_UNFILTERED_EVENTS = 1 << 1;
    697 
    698     /**
    699      * Explicitly requests discovery.
    700      *
    701      * @hide Future API ported from support library.  Revisit this later.
    702      */
    703     public static final int CALLBACK_FLAG_REQUEST_DISCOVERY = 1 << 2;
    704 
    705     /**
    706      * Requests that discovery be performed but only if there is some other active
    707      * callback already registered.
    708      *
    709      * @hide Compatibility workaround for the fact that applications do not currently
    710      * request discovery explicitly (except when using the support library API).
    711      */
    712     public static final int CALLBACK_FLAG_PASSIVE_DISCOVERY = 1 << 3;
    713 
    714     /**
    715      * Flag for {@link #isRouteAvailable}: Ignore the default route.
    716      * <p>
    717      * This flag is used to determine whether a matching non-default route is available.
    718      * This constraint may be used to decide whether to offer the route chooser dialog
    719      * to the user.  There is no point offering the chooser if there are no
    720      * non-default choices.
    721      * </p>
    722      *
    723      * @hide Future API ported from support library.  Revisit this later.
    724      */
    725     public static final int AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE = 1 << 0;
    726 
    727     // Maps application contexts
    728     static final HashMap<Context, MediaRouter> sRouters = new HashMap<Context, MediaRouter>();
    729 
    730     static String typesToString(int types) {
    731         final StringBuilder result = new StringBuilder();
    732         if ((types & ROUTE_TYPE_LIVE_AUDIO) != 0) {
    733             result.append("ROUTE_TYPE_LIVE_AUDIO ");
    734         }
    735         if ((types & ROUTE_TYPE_LIVE_VIDEO) != 0) {
    736             result.append("ROUTE_TYPE_LIVE_VIDEO ");
    737         }
    738         if ((types & ROUTE_TYPE_REMOTE_DISPLAY) != 0) {
    739             result.append("ROUTE_TYPE_REMOTE_DISPLAY ");
    740         }
    741         if ((types & ROUTE_TYPE_USER) != 0) {
    742             result.append("ROUTE_TYPE_USER ");
    743         }
    744         return result.toString();
    745     }
    746 
    747     /** @hide */
    748     public MediaRouter(Context context) {
    749         synchronized (Static.class) {
    750             if (sStatic == null) {
    751                 final Context appContext = context.getApplicationContext();
    752                 sStatic = new Static(appContext);
    753                 sStatic.startMonitoringRoutes(appContext);
    754             }
    755         }
    756     }
    757 
    758     /**
    759      * Gets the default route for playing media content on the system.
    760      * <p>
    761      * The system always provides a default route.
    762      * </p>
    763      *
    764      * @return The default route, which is guaranteed to never be null.
    765      */
    766     public RouteInfo getDefaultRoute() {
    767         return sStatic.mDefaultAudioVideo;
    768     }
    769 
    770     /**
    771      * Returns a Bluetooth route if available, otherwise the default route.
    772      * @hide
    773      */
    774     public RouteInfo getFallbackRoute() {
    775         return (sStatic.mBluetoothA2dpRoute != null)
    776                 ? sStatic.mBluetoothA2dpRoute : sStatic.mDefaultAudioVideo;
    777     }
    778 
    779     /**
    780      * @hide for use by framework routing UI
    781      */
    782     public RouteCategory getSystemCategory() {
    783         return sStatic.mSystemCategory;
    784     }
    785 
    786     /** @hide */
    787     public RouteInfo getSelectedRoute() {
    788         return getSelectedRoute(ROUTE_TYPE_ANY);
    789     }
    790 
    791     /**
    792      * Return the currently selected route for any of the given types
    793      *
    794      * @param type route types
    795      * @return the selected route
    796      */
    797     public RouteInfo getSelectedRoute(int type) {
    798         if (sStatic.mSelectedRoute != null &&
    799                 (sStatic.mSelectedRoute.mSupportedTypes & type) != 0) {
    800             // If the selected route supports any of the types supplied, it's still considered
    801             // 'selected' for that type.
    802             return sStatic.mSelectedRoute;
    803         } else if (type == ROUTE_TYPE_USER) {
    804             // The caller specifically asked for a user route and the currently selected route
    805             // doesn't qualify.
    806             return null;
    807         }
    808         // If the above didn't match and we're not specifically asking for a user route,
    809         // consider the default selected.
    810         return sStatic.mDefaultAudioVideo;
    811     }
    812 
    813     /**
    814      * Returns true if there is a route that matches the specified types.
    815      * <p>
    816      * This method returns true if there are any available routes that match the types
    817      * regardless of whether they are enabled or disabled.  If the
    818      * {@link #AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE} flag is specified, then
    819      * the method will only consider non-default routes.
    820      * </p>
    821      *
    822      * @param types The types to match.
    823      * @param flags Flags to control the determination of whether a route may be available.
    824      * May be zero or {@link #AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE}.
    825      * @return True if a matching route may be available.
    826      *
    827      * @hide Future API ported from support library.  Revisit this later.
    828      */
    829     public boolean isRouteAvailable(int types, int flags) {
    830         final int count = sStatic.mRoutes.size();
    831         for (int i = 0; i < count; i++) {
    832             RouteInfo route = sStatic.mRoutes.get(i);
    833             if (route.matchesTypes(types)) {
    834                 if ((flags & AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE) == 0
    835                         || route != sStatic.mDefaultAudioVideo) {
    836                     return true;
    837                 }
    838             }
    839         }
    840 
    841         // It doesn't look like we can find a matching route right now.
    842         return false;
    843     }
    844 
    845     /**
    846      * Add a callback to listen to events about specific kinds of media routes.
    847      * If the specified callback is already registered, its registration will be updated for any
    848      * additional route types specified.
    849      * <p>
    850      * This is a convenience method that has the same effect as calling
    851      * {@link #addCallback(int, Callback, int)} without flags.
    852      * </p>
    853      *
    854      * @param types Types of routes this callback is interested in
    855      * @param cb Callback to add
    856      */
    857     public void addCallback(int types, Callback cb) {
    858         addCallback(types, cb, 0);
    859     }
    860 
    861     /**
    862      * Add a callback to listen to events about specific kinds of media routes.
    863      * If the specified callback is already registered, its registration will be updated for any
    864      * additional route types specified.
    865      * <p>
    866      * By default, the callback will only be invoked for events that affect routes
    867      * that match the specified selector.  The filtering may be disabled by specifying
    868      * the {@link #CALLBACK_FLAG_UNFILTERED_EVENTS} flag.
    869      * </p>
    870      *
    871      * @param types Types of routes this callback is interested in
    872      * @param cb Callback to add
    873      * @param flags Flags to control the behavior of the callback.
    874      * May be zero or a combination of {@link #CALLBACK_FLAG_PERFORM_ACTIVE_SCAN} and
    875      * {@link #CALLBACK_FLAG_UNFILTERED_EVENTS}.
    876      */
    877     public void addCallback(int types, Callback cb, int flags) {
    878         CallbackInfo info;
    879         int index = findCallbackInfo(cb);
    880         if (index >= 0) {
    881             info = sStatic.mCallbacks.get(index);
    882             info.type |= types;
    883             info.flags |= flags;
    884         } else {
    885             info = new CallbackInfo(cb, types, flags, this);
    886             sStatic.mCallbacks.add(info);
    887         }
    888         sStatic.updateDiscoveryRequest();
    889     }
    890 
    891     /**
    892      * Remove the specified callback. It will no longer receive events about media routing.
    893      *
    894      * @param cb Callback to remove
    895      */
    896     public void removeCallback(Callback cb) {
    897         int index = findCallbackInfo(cb);
    898         if (index >= 0) {
    899             sStatic.mCallbacks.remove(index);
    900             sStatic.updateDiscoveryRequest();
    901         } else {
    902             Log.w(TAG, "removeCallback(" + cb + "): callback not registered");
    903         }
    904     }
    905 
    906     private int findCallbackInfo(Callback cb) {
    907         final int count = sStatic.mCallbacks.size();
    908         for (int i = 0; i < count; i++) {
    909             final CallbackInfo info = sStatic.mCallbacks.get(i);
    910             if (info.cb == cb) {
    911                 return i;
    912             }
    913         }
    914         return -1;
    915     }
    916 
    917     /**
    918      * Select the specified route to use for output of the given media types.
    919      * <p class="note">
    920      * As API version 18, this function may be used to select any route.
    921      * In prior versions, this function could only be used to select user
    922      * routes and would ignore any attempt to select a system route.
    923      * </p>
    924      *
    925      * @param types type flags indicating which types this route should be used for.
    926      *              The route must support at least a subset.
    927      * @param route Route to select
    928      * @throws IllegalArgumentException if the given route is {@code null}
    929      */
    930     public void selectRoute(int types, @NonNull RouteInfo route) {
    931         if (route == null) {
    932             throw new IllegalArgumentException("Route cannot be null.");
    933         }
    934         selectRouteStatic(types, route, true);
    935     }
    936 
    937     /**
    938      * @hide internal use
    939      */
    940     public void selectRouteInt(int types, RouteInfo route, boolean explicit) {
    941         selectRouteStatic(types, route, explicit);
    942     }
    943 
    944     static void selectRouteStatic(int types, @NonNull RouteInfo route, boolean explicit) {
    945         Log.v(TAG, "Selecting route: " + route);
    946         assert(route != null);
    947         final RouteInfo oldRoute = sStatic.mSelectedRoute;
    948         final RouteInfo currentSystemRoute = sStatic.isBluetoothA2dpOn()
    949                 ? sStatic.mBluetoothA2dpRoute : sStatic.mDefaultAudioVideo;
    950         boolean wasDefaultOrBluetoothRoute = (oldRoute == sStatic.mDefaultAudioVideo
    951                 || oldRoute == sStatic.mBluetoothA2dpRoute);
    952         if (oldRoute == route
    953                 && (!wasDefaultOrBluetoothRoute || route == currentSystemRoute)) {
    954             return;
    955         }
    956         if (!route.matchesTypes(types)) {
    957             Log.w(TAG, "selectRoute ignored; cannot select route with supported types " +
    958                     typesToString(route.getSupportedTypes()) + " into route types " +
    959                     typesToString(types));
    960             return;
    961         }
    962 
    963         final RouteInfo btRoute = sStatic.mBluetoothA2dpRoute;
    964         if (sStatic.isPlaybackActive() && btRoute != null && (types & ROUTE_TYPE_LIVE_AUDIO) != 0
    965                 && (route == btRoute || route == sStatic.mDefaultAudioVideo)) {
    966             try {
    967                 sStatic.mAudioService.setBluetoothA2dpOn(route == btRoute);
    968                 // TODO: Remove the following logging when no longer needed.
    969                 if (route != btRoute) {
    970                     StackTraceElement[] callStack = Thread.currentThread().getStackTrace();
    971                     StringBuffer sb = new StringBuffer();
    972                     // callStack[3] is the caller of this method.
    973                     for (int i = 3; i < callStack.length; i++) {
    974                         StackTraceElement caller = callStack[i];
    975                         sb.append(caller.getClassName() + "." + caller.getMethodName()
    976                                 + ":" + caller.getLineNumber()).append("  ");
    977                     }
    978                     Log.w(TAG, "Default route is selected while a BT route is available: pkgName="
    979                             + sStatic.mPackageName + ", callers=" + sb.toString());
    980                 }
    981             } catch (RemoteException e) {
    982                 Log.e(TAG, "Error changing Bluetooth A2DP state", e);
    983             }
    984         }
    985 
    986         final WifiDisplay activeDisplay =
    987                 sStatic.mDisplayService.getWifiDisplayStatus().getActiveDisplay();
    988         final boolean oldRouteHasAddress = oldRoute != null && oldRoute.mDeviceAddress != null;
    989         final boolean newRouteHasAddress = route.mDeviceAddress != null;
    990         if (activeDisplay != null || oldRouteHasAddress || newRouteHasAddress) {
    991             if (newRouteHasAddress && !matchesDeviceAddress(activeDisplay, route)) {
    992                 if (sStatic.mCanConfigureWifiDisplays) {
    993                     sStatic.mDisplayService.connectWifiDisplay(route.mDeviceAddress);
    994                 } else {
    995                     Log.e(TAG, "Cannot connect to wifi displays because this process "
    996                             + "is not allowed to do so.");
    997                 }
    998             } else if (activeDisplay != null && !newRouteHasAddress) {
    999                 sStatic.mDisplayService.disconnectWifiDisplay();
   1000             }
   1001         }
   1002 
   1003         sStatic.setSelectedRoute(route, explicit);
   1004 
   1005         if (oldRoute != null) {
   1006             dispatchRouteUnselected(types & oldRoute.getSupportedTypes(), oldRoute);
   1007             if (oldRoute.resolveStatusCode()) {
   1008                 dispatchRouteChanged(oldRoute);
   1009             }
   1010         }
   1011         if (route != null) {
   1012             if (route.resolveStatusCode()) {
   1013                 dispatchRouteChanged(route);
   1014             }
   1015             dispatchRouteSelected(types & route.getSupportedTypes(), route);
   1016         }
   1017 
   1018         // The behavior of active scans may depend on the currently selected route.
   1019         sStatic.updateDiscoveryRequest();
   1020     }
   1021 
   1022     static void selectDefaultRouteStatic() {
   1023         // TODO: Be smarter about the route types here; this selects for all valid.
   1024         if (sStatic.mSelectedRoute != sStatic.mBluetoothA2dpRoute && sStatic.isBluetoothA2dpOn()) {
   1025             selectRouteStatic(ROUTE_TYPE_ANY, sStatic.mBluetoothA2dpRoute, false);
   1026         } else {
   1027             selectRouteStatic(ROUTE_TYPE_ANY, sStatic.mDefaultAudioVideo, false);
   1028         }
   1029     }
   1030 
   1031     /**
   1032      * Compare the device address of a display and a route.
   1033      * Nulls/no device address will match another null/no address.
   1034      */
   1035     static boolean matchesDeviceAddress(WifiDisplay display, RouteInfo info) {
   1036         final boolean routeHasAddress = info != null && info.mDeviceAddress != null;
   1037         if (display == null && !routeHasAddress) {
   1038             return true;
   1039         }
   1040 
   1041         if (display != null && routeHasAddress) {
   1042             return display.getDeviceAddress().equals(info.mDeviceAddress);
   1043         }
   1044         return false;
   1045     }
   1046 
   1047     /**
   1048      * Add an app-specified route for media to the MediaRouter.
   1049      * App-specified route definitions are created using {@link #createUserRoute(RouteCategory)}
   1050      *
   1051      * @param info Definition of the route to add
   1052      * @see #createUserRoute(RouteCategory)
   1053      * @see #removeUserRoute(UserRouteInfo)
   1054      */
   1055     public void addUserRoute(UserRouteInfo info) {
   1056         addRouteStatic(info);
   1057     }
   1058 
   1059     /**
   1060      * @hide Framework use only
   1061      */
   1062     public void addRouteInt(RouteInfo info) {
   1063         addRouteStatic(info);
   1064     }
   1065 
   1066     static void addRouteStatic(RouteInfo info) {
   1067         Log.v(TAG, "Adding route: " + info);
   1068         final RouteCategory cat = info.getCategory();
   1069         if (!sStatic.mCategories.contains(cat)) {
   1070             sStatic.mCategories.add(cat);
   1071         }
   1072         if (cat.isGroupable() && !(info instanceof RouteGroup)) {
   1073             // Enforce that any added route in a groupable category must be in a group.
   1074             final RouteGroup group = new RouteGroup(info.getCategory());
   1075             group.mSupportedTypes = info.mSupportedTypes;
   1076             sStatic.mRoutes.add(group);
   1077             dispatchRouteAdded(group);
   1078             group.addRoute(info);
   1079 
   1080             info = group;
   1081         } else {
   1082             sStatic.mRoutes.add(info);
   1083             dispatchRouteAdded(info);
   1084         }
   1085     }
   1086 
   1087     /**
   1088      * Remove an app-specified route for media from the MediaRouter.
   1089      *
   1090      * @param info Definition of the route to remove
   1091      * @see #addUserRoute(UserRouteInfo)
   1092      */
   1093     public void removeUserRoute(UserRouteInfo info) {
   1094         removeRouteStatic(info);
   1095     }
   1096 
   1097     /**
   1098      * Remove all app-specified routes from the MediaRouter.
   1099      *
   1100      * @see #removeUserRoute(UserRouteInfo)
   1101      */
   1102     public void clearUserRoutes() {
   1103         for (int i = 0; i < sStatic.mRoutes.size(); i++) {
   1104             final RouteInfo info = sStatic.mRoutes.get(i);
   1105             // TODO Right now, RouteGroups only ever contain user routes.
   1106             // The code below will need to change if this assumption does.
   1107             if (info instanceof UserRouteInfo || info instanceof RouteGroup) {
   1108                 removeRouteStatic(info);
   1109                 i--;
   1110             }
   1111         }
   1112     }
   1113 
   1114     /**
   1115      * @hide internal use only
   1116      */
   1117     public void removeRouteInt(RouteInfo info) {
   1118         removeRouteStatic(info);
   1119     }
   1120 
   1121     static void removeRouteStatic(RouteInfo info) {
   1122         Log.v(TAG, "Removing route: " + info);
   1123         if (sStatic.mRoutes.remove(info)) {
   1124             final RouteCategory removingCat = info.getCategory();
   1125             final int count = sStatic.mRoutes.size();
   1126             boolean found = false;
   1127             for (int i = 0; i < count; i++) {
   1128                 final RouteCategory cat = sStatic.mRoutes.get(i).getCategory();
   1129                 if (removingCat == cat) {
   1130                     found = true;
   1131                     break;
   1132                 }
   1133             }
   1134             if (info.isSelected()) {
   1135                 // Removing the currently selected route? Select the default before we remove it.
   1136                 selectDefaultRouteStatic();
   1137             }
   1138             if (!found) {
   1139                 sStatic.mCategories.remove(removingCat);
   1140             }
   1141             dispatchRouteRemoved(info);
   1142         }
   1143     }
   1144 
   1145     /**
   1146      * Return the number of {@link MediaRouter.RouteCategory categories} currently
   1147      * represented by routes known to this MediaRouter.
   1148      *
   1149      * @return the number of unique categories represented by this MediaRouter's known routes
   1150      */
   1151     public int getCategoryCount() {
   1152         return sStatic.mCategories.size();
   1153     }
   1154 
   1155     /**
   1156      * Return the {@link MediaRouter.RouteCategory category} at the given index.
   1157      * Valid indices are in the range [0-getCategoryCount).
   1158      *
   1159      * @param index which category to return
   1160      * @return the category at index
   1161      */
   1162     public RouteCategory getCategoryAt(int index) {
   1163         return sStatic.mCategories.get(index);
   1164     }
   1165 
   1166     /**
   1167      * Return the number of {@link MediaRouter.RouteInfo routes} currently known
   1168      * to this MediaRouter.
   1169      *
   1170      * @return the number of routes tracked by this router
   1171      */
   1172     public int getRouteCount() {
   1173         return sStatic.mRoutes.size();
   1174     }
   1175 
   1176     /**
   1177      * Return the route at the specified index.
   1178      *
   1179      * @param index index of the route to return
   1180      * @return the route at index
   1181      */
   1182     public RouteInfo getRouteAt(int index) {
   1183         return sStatic.mRoutes.get(index);
   1184     }
   1185 
   1186     static int getRouteCountStatic() {
   1187         return sStatic.mRoutes.size();
   1188     }
   1189 
   1190     static RouteInfo getRouteAtStatic(int index) {
   1191         return sStatic.mRoutes.get(index);
   1192     }
   1193 
   1194     /**
   1195      * Create a new user route that may be modified and registered for use by the application.
   1196      *
   1197      * @param category The category the new route will belong to
   1198      * @return A new UserRouteInfo for use by the application
   1199      *
   1200      * @see #addUserRoute(UserRouteInfo)
   1201      * @see #removeUserRoute(UserRouteInfo)
   1202      * @see #createRouteCategory(CharSequence, boolean)
   1203      */
   1204     public UserRouteInfo createUserRoute(RouteCategory category) {
   1205         return new UserRouteInfo(category);
   1206     }
   1207 
   1208     /**
   1209      * Create a new route category. Each route must belong to a category.
   1210      *
   1211      * @param name Name of the new category
   1212      * @param isGroupable true if routes in this category may be grouped with one another
   1213      * @return the new RouteCategory
   1214      */
   1215     public RouteCategory createRouteCategory(CharSequence name, boolean isGroupable) {
   1216         return new RouteCategory(name, ROUTE_TYPE_USER, isGroupable);
   1217     }
   1218 
   1219     /**
   1220      * Create a new route category. Each route must belong to a category.
   1221      *
   1222      * @param nameResId Resource ID of the name of the new category
   1223      * @param isGroupable true if routes in this category may be grouped with one another
   1224      * @return the new RouteCategory
   1225      */
   1226     public RouteCategory createRouteCategory(int nameResId, boolean isGroupable) {
   1227         return new RouteCategory(nameResId, ROUTE_TYPE_USER, isGroupable);
   1228     }
   1229 
   1230     /**
   1231      * Rebinds the media router to handle routes that belong to the specified user.
   1232      * Requires the interact across users permission to access the routes of another user.
   1233      * <p>
   1234      * This method is a complete hack to work around the singleton nature of the
   1235      * media router when running inside of singleton processes like QuickSettings.
   1236      * This mechanism should be burned to the ground when MediaRouter is redesigned.
   1237      * Ideally the current user would be pulled from the Context but we need to break
   1238      * down MediaRouter.Static before we can get there.
   1239      * </p>
   1240      *
   1241      * @hide
   1242      */
   1243     public void rebindAsUser(int userId) {
   1244         sStatic.rebindAsUser(userId);
   1245     }
   1246 
   1247     static void updateRoute(final RouteInfo info) {
   1248         dispatchRouteChanged(info);
   1249     }
   1250 
   1251     static void dispatchRouteSelected(int type, RouteInfo info) {
   1252         for (CallbackInfo cbi : sStatic.mCallbacks) {
   1253             if (cbi.filterRouteEvent(info)) {
   1254                 cbi.cb.onRouteSelected(cbi.router, type, info);
   1255             }
   1256         }
   1257     }
   1258 
   1259     static void dispatchRouteUnselected(int type, RouteInfo info) {
   1260         for (CallbackInfo cbi : sStatic.mCallbacks) {
   1261             if (cbi.filterRouteEvent(info)) {
   1262                 cbi.cb.onRouteUnselected(cbi.router, type, info);
   1263             }
   1264         }
   1265     }
   1266 
   1267     static void dispatchRouteChanged(RouteInfo info) {
   1268         dispatchRouteChanged(info, info.mSupportedTypes);
   1269     }
   1270 
   1271     static void dispatchRouteChanged(RouteInfo info, int oldSupportedTypes) {
   1272         if (DEBUG) {
   1273             Log.d(TAG, "Dispatching route change: " + info);
   1274         }
   1275         final int newSupportedTypes = info.mSupportedTypes;
   1276         for (CallbackInfo cbi : sStatic.mCallbacks) {
   1277             // Reconstruct some of the history for callbacks that may not have observed
   1278             // all of the events needed to correctly interpret the current state.
   1279             // FIXME: This is a strong signal that we should deprecate route type filtering
   1280             // completely in the future because it can lead to inconsistencies in
   1281             // applications.
   1282             final boolean oldVisibility = cbi.filterRouteEvent(oldSupportedTypes);
   1283             final boolean newVisibility = cbi.filterRouteEvent(newSupportedTypes);
   1284             if (!oldVisibility && newVisibility) {
   1285                 cbi.cb.onRouteAdded(cbi.router, info);
   1286                 if (info.isSelected()) {
   1287                     cbi.cb.onRouteSelected(cbi.router, newSupportedTypes, info);
   1288                 }
   1289             }
   1290             if (oldVisibility || newVisibility) {
   1291                 cbi.cb.onRouteChanged(cbi.router, info);
   1292             }
   1293             if (oldVisibility && !newVisibility) {
   1294                 if (info.isSelected()) {
   1295                     cbi.cb.onRouteUnselected(cbi.router, oldSupportedTypes, info);
   1296                 }
   1297                 cbi.cb.onRouteRemoved(cbi.router, info);
   1298             }
   1299         }
   1300     }
   1301 
   1302     static void dispatchRouteAdded(RouteInfo info) {
   1303         for (CallbackInfo cbi : sStatic.mCallbacks) {
   1304             if (cbi.filterRouteEvent(info)) {
   1305                 cbi.cb.onRouteAdded(cbi.router, info);
   1306             }
   1307         }
   1308     }
   1309 
   1310     static void dispatchRouteRemoved(RouteInfo info) {
   1311         for (CallbackInfo cbi : sStatic.mCallbacks) {
   1312             if (cbi.filterRouteEvent(info)) {
   1313                 cbi.cb.onRouteRemoved(cbi.router, info);
   1314             }
   1315         }
   1316     }
   1317 
   1318     static void dispatchRouteGrouped(RouteInfo info, RouteGroup group, int index) {
   1319         for (CallbackInfo cbi : sStatic.mCallbacks) {
   1320             if (cbi.filterRouteEvent(group)) {
   1321                 cbi.cb.onRouteGrouped(cbi.router, info, group, index);
   1322             }
   1323         }
   1324     }
   1325 
   1326     static void dispatchRouteUngrouped(RouteInfo info, RouteGroup group) {
   1327         for (CallbackInfo cbi : sStatic.mCallbacks) {
   1328             if (cbi.filterRouteEvent(group)) {
   1329                 cbi.cb.onRouteUngrouped(cbi.router, info, group);
   1330             }
   1331         }
   1332     }
   1333 
   1334     static void dispatchRouteVolumeChanged(RouteInfo info) {
   1335         for (CallbackInfo cbi : sStatic.mCallbacks) {
   1336             if (cbi.filterRouteEvent(info)) {
   1337                 cbi.cb.onRouteVolumeChanged(cbi.router, info);
   1338             }
   1339         }
   1340     }
   1341 
   1342     static void dispatchRoutePresentationDisplayChanged(RouteInfo info) {
   1343         for (CallbackInfo cbi : sStatic.mCallbacks) {
   1344             if (cbi.filterRouteEvent(info)) {
   1345                 cbi.cb.onRoutePresentationDisplayChanged(cbi.router, info);
   1346             }
   1347         }
   1348     }
   1349 
   1350     static void systemVolumeChanged(int newValue) {
   1351         final RouteInfo selectedRoute = sStatic.mSelectedRoute;
   1352         if (selectedRoute == null) return;
   1353 
   1354         if (selectedRoute == sStatic.mBluetoothA2dpRoute ||
   1355                 selectedRoute == sStatic.mDefaultAudioVideo) {
   1356             dispatchRouteVolumeChanged(selectedRoute);
   1357         } else if (sStatic.mBluetoothA2dpRoute != null) {
   1358             try {
   1359                 dispatchRouteVolumeChanged(sStatic.mAudioService.isBluetoothA2dpOn() ?
   1360                         sStatic.mBluetoothA2dpRoute : sStatic.mDefaultAudioVideo);
   1361             } catch (RemoteException e) {
   1362                 Log.e(TAG, "Error checking Bluetooth A2DP state to report volume change", e);
   1363             }
   1364         } else {
   1365             dispatchRouteVolumeChanged(sStatic.mDefaultAudioVideo);
   1366         }
   1367     }
   1368 
   1369     static void updateWifiDisplayStatus(WifiDisplayStatus status) {
   1370         WifiDisplay[] displays;
   1371         WifiDisplay activeDisplay;
   1372         if (status.getFeatureState() == WifiDisplayStatus.FEATURE_STATE_ON) {
   1373             displays = status.getDisplays();
   1374             activeDisplay = status.getActiveDisplay();
   1375 
   1376             // Only the system is able to connect to wifi display routes.
   1377             // The display manager will enforce this with a permission check but it
   1378             // still publishes information about all available displays.
   1379             // Filter the list down to just the active display.
   1380             if (!sStatic.mCanConfigureWifiDisplays) {
   1381                 if (activeDisplay != null) {
   1382                     displays = new WifiDisplay[] { activeDisplay };
   1383                 } else {
   1384                     displays = WifiDisplay.EMPTY_ARRAY;
   1385                 }
   1386             }
   1387         } else {
   1388             displays = WifiDisplay.EMPTY_ARRAY;
   1389             activeDisplay = null;
   1390         }
   1391         String activeDisplayAddress = activeDisplay != null ?
   1392                 activeDisplay.getDeviceAddress() : null;
   1393 
   1394         // Add or update routes.
   1395         for (int i = 0; i < displays.length; i++) {
   1396             final WifiDisplay d = displays[i];
   1397             if (shouldShowWifiDisplay(d, activeDisplay)) {
   1398                 RouteInfo route = findWifiDisplayRoute(d);
   1399                 if (route == null) {
   1400                     route = makeWifiDisplayRoute(d, status);
   1401                     addRouteStatic(route);
   1402                 } else {
   1403                     String address = d.getDeviceAddress();
   1404                     boolean disconnected = !address.equals(activeDisplayAddress)
   1405                             && address.equals(sStatic.mPreviousActiveWifiDisplayAddress);
   1406                     updateWifiDisplayRoute(route, d, status, disconnected);
   1407                 }
   1408                 if (d.equals(activeDisplay)) {
   1409                     selectRouteStatic(route.getSupportedTypes(), route, false);
   1410                 }
   1411             }
   1412         }
   1413 
   1414         // Remove stale routes.
   1415         for (int i = sStatic.mRoutes.size(); i-- > 0; ) {
   1416             RouteInfo route = sStatic.mRoutes.get(i);
   1417             if (route.mDeviceAddress != null) {
   1418                 WifiDisplay d = findWifiDisplay(displays, route.mDeviceAddress);
   1419                 if (d == null || !shouldShowWifiDisplay(d, activeDisplay)) {
   1420                     removeRouteStatic(route);
   1421                 }
   1422             }
   1423         }
   1424 
   1425         // Remember the current active wifi display address so that we can infer disconnections.
   1426         // TODO: This hack will go away once all of this is moved into the media router service.
   1427         sStatic.mPreviousActiveWifiDisplayAddress = activeDisplayAddress;
   1428     }
   1429 
   1430     private static boolean shouldShowWifiDisplay(WifiDisplay d, WifiDisplay activeDisplay) {
   1431         return d.isRemembered() || d.equals(activeDisplay);
   1432     }
   1433 
   1434     static int getWifiDisplayStatusCode(WifiDisplay d, WifiDisplayStatus wfdStatus) {
   1435         int newStatus;
   1436         if (wfdStatus.getScanState() == WifiDisplayStatus.SCAN_STATE_SCANNING) {
   1437             newStatus = RouteInfo.STATUS_SCANNING;
   1438         } else if (d.isAvailable()) {
   1439             newStatus = d.canConnect() ?
   1440                     RouteInfo.STATUS_AVAILABLE: RouteInfo.STATUS_IN_USE;
   1441         } else {
   1442             newStatus = RouteInfo.STATUS_NOT_AVAILABLE;
   1443         }
   1444 
   1445         if (d.equals(wfdStatus.getActiveDisplay())) {
   1446             final int activeState = wfdStatus.getActiveDisplayState();
   1447             switch (activeState) {
   1448                 case WifiDisplayStatus.DISPLAY_STATE_CONNECTED:
   1449                     newStatus = RouteInfo.STATUS_CONNECTED;
   1450                     break;
   1451                 case WifiDisplayStatus.DISPLAY_STATE_CONNECTING:
   1452                     newStatus = RouteInfo.STATUS_CONNECTING;
   1453                     break;
   1454                 case WifiDisplayStatus.DISPLAY_STATE_NOT_CONNECTED:
   1455                     Log.e(TAG, "Active display is not connected!");
   1456                     break;
   1457             }
   1458         }
   1459 
   1460         return newStatus;
   1461     }
   1462 
   1463     static boolean isWifiDisplayEnabled(WifiDisplay d, WifiDisplayStatus wfdStatus) {
   1464         return d.isAvailable() && (d.canConnect() || d.equals(wfdStatus.getActiveDisplay()));
   1465     }
   1466 
   1467     static RouteInfo makeWifiDisplayRoute(WifiDisplay display, WifiDisplayStatus wfdStatus) {
   1468         final RouteInfo newRoute = new RouteInfo(sStatic.mSystemCategory);
   1469         newRoute.mDeviceAddress = display.getDeviceAddress();
   1470         newRoute.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO
   1471                 | ROUTE_TYPE_REMOTE_DISPLAY;
   1472         newRoute.mVolumeHandling = RouteInfo.PLAYBACK_VOLUME_FIXED;
   1473         newRoute.mPlaybackType = RouteInfo.PLAYBACK_TYPE_REMOTE;
   1474 
   1475         newRoute.setRealStatusCode(getWifiDisplayStatusCode(display, wfdStatus));
   1476         newRoute.mEnabled = isWifiDisplayEnabled(display, wfdStatus);
   1477         newRoute.mName = display.getFriendlyDisplayName();
   1478         newRoute.mDescription = sStatic.mResources.getText(
   1479                 com.android.internal.R.string.wireless_display_route_description);
   1480         newRoute.updatePresentationDisplay();
   1481         newRoute.mDeviceType = RouteInfo.DEVICE_TYPE_TV;
   1482         return newRoute;
   1483     }
   1484 
   1485     private static void updateWifiDisplayRoute(
   1486             RouteInfo route, WifiDisplay display, WifiDisplayStatus wfdStatus,
   1487             boolean disconnected) {
   1488         boolean changed = false;
   1489         final String newName = display.getFriendlyDisplayName();
   1490         if (!route.getName().equals(newName)) {
   1491             route.mName = newName;
   1492             changed = true;
   1493         }
   1494 
   1495         boolean enabled = isWifiDisplayEnabled(display, wfdStatus);
   1496         changed |= route.mEnabled != enabled;
   1497         route.mEnabled = enabled;
   1498 
   1499         changed |= route.setRealStatusCode(getWifiDisplayStatusCode(display, wfdStatus));
   1500 
   1501         if (changed) {
   1502             dispatchRouteChanged(route);
   1503         }
   1504 
   1505         if ((!enabled || disconnected) && route.isSelected()) {
   1506             // Oops, no longer available. Reselect the default.
   1507             selectDefaultRouteStatic();
   1508         }
   1509     }
   1510 
   1511     private static WifiDisplay findWifiDisplay(WifiDisplay[] displays, String deviceAddress) {
   1512         for (int i = 0; i < displays.length; i++) {
   1513             final WifiDisplay d = displays[i];
   1514             if (d.getDeviceAddress().equals(deviceAddress)) {
   1515                 return d;
   1516             }
   1517         }
   1518         return null;
   1519     }
   1520 
   1521     private static RouteInfo findWifiDisplayRoute(WifiDisplay d) {
   1522         final int count = sStatic.mRoutes.size();
   1523         for (int i = 0; i < count; i++) {
   1524             final RouteInfo info = sStatic.mRoutes.get(i);
   1525             if (d.getDeviceAddress().equals(info.mDeviceAddress)) {
   1526                 return info;
   1527             }
   1528         }
   1529         return null;
   1530     }
   1531 
   1532     /**
   1533      * Information about a media route.
   1534      */
   1535     public static class RouteInfo {
   1536         CharSequence mName;
   1537         int mNameResId;
   1538         CharSequence mDescription;
   1539         private CharSequence mStatus;
   1540         int mSupportedTypes;
   1541         int mDeviceType;
   1542         RouteGroup mGroup;
   1543         final RouteCategory mCategory;
   1544         Drawable mIcon;
   1545         // playback information
   1546         int mPlaybackType = PLAYBACK_TYPE_LOCAL;
   1547         int mVolumeMax = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME;
   1548         int mVolume = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME;
   1549         int mVolumeHandling = RemoteControlClient.DEFAULT_PLAYBACK_VOLUME_HANDLING;
   1550         int mPlaybackStream = AudioManager.STREAM_MUSIC;
   1551         VolumeCallbackInfo mVcb;
   1552         Display mPresentationDisplay;
   1553         int mPresentationDisplayId = -1;
   1554 
   1555         String mDeviceAddress;
   1556         boolean mEnabled = true;
   1557 
   1558         // An id by which the route is known to the media router service.
   1559         // Null if this route only exists as an artifact within this process.
   1560         String mGlobalRouteId;
   1561 
   1562         // A predetermined connection status that can override mStatus
   1563         private int mRealStatusCode;
   1564         private int mResolvedStatusCode;
   1565 
   1566         /** @hide */ public static final int STATUS_NONE = 0;
   1567         /** @hide */ public static final int STATUS_SCANNING = 1;
   1568         /** @hide */ public static final int STATUS_CONNECTING = 2;
   1569         /** @hide */ public static final int STATUS_AVAILABLE = 3;
   1570         /** @hide */ public static final int STATUS_NOT_AVAILABLE = 4;
   1571         /** @hide */ public static final int STATUS_IN_USE = 5;
   1572         /** @hide */ public static final int STATUS_CONNECTED = 6;
   1573 
   1574         /** @hide */
   1575         @IntDef({DEVICE_TYPE_UNKNOWN, DEVICE_TYPE_TV, DEVICE_TYPE_SPEAKER, DEVICE_TYPE_BLUETOOTH})
   1576         @Retention(RetentionPolicy.SOURCE)
   1577         public @interface DeviceType {}
   1578 
   1579         /**
   1580          * The default receiver device type of the route indicating the type is unknown.
   1581          *
   1582          * @see #getDeviceType
   1583          */
   1584         public static final int DEVICE_TYPE_UNKNOWN = 0;
   1585 
   1586         /**
   1587          * A receiver device type of the route indicating the presentation of the media is happening
   1588          * on a TV.
   1589          *
   1590          * @see #getDeviceType
   1591          */
   1592         public static final int DEVICE_TYPE_TV = 1;
   1593 
   1594         /**
   1595          * A receiver device type of the route indicating the presentation of the media is happening
   1596          * on a speaker.
   1597          *
   1598          * @see #getDeviceType
   1599          */
   1600         public static final int DEVICE_TYPE_SPEAKER = 2;
   1601 
   1602         /**
   1603          * A receiver device type of the route indicating the presentation of the media is happening
   1604          * on a bluetooth device such as a bluetooth speaker.
   1605          *
   1606          * @see #getDeviceType
   1607          */
   1608         public static final int DEVICE_TYPE_BLUETOOTH = 3;
   1609 
   1610         private Object mTag;
   1611 
   1612         /** @hide */
   1613         @IntDef({PLAYBACK_TYPE_LOCAL, PLAYBACK_TYPE_REMOTE})
   1614         @Retention(RetentionPolicy.SOURCE)
   1615         public @interface PlaybackType {}
   1616 
   1617         /**
   1618          * The default playback type, "local", indicating the presentation of the media is happening
   1619          * on the same device (e&#46;g&#46; a phone, a tablet) as where it is controlled from.
   1620          * @see #getPlaybackType()
   1621          */
   1622         public final static int PLAYBACK_TYPE_LOCAL = 0;
   1623 
   1624         /**
   1625          * A playback type indicating the presentation of the media is happening on
   1626          * a different device (i&#46;e&#46; the remote device) than where it is controlled from.
   1627          * @see #getPlaybackType()
   1628          */
   1629         public final static int PLAYBACK_TYPE_REMOTE = 1;
   1630 
   1631         /** @hide */
   1632          @IntDef({PLAYBACK_VOLUME_FIXED,PLAYBACK_VOLUME_VARIABLE})
   1633          @Retention(RetentionPolicy.SOURCE)
   1634          private @interface PlaybackVolume {}
   1635 
   1636         /**
   1637          * Playback information indicating the playback volume is fixed, i&#46;e&#46; it cannot be
   1638          * controlled from this object. An example of fixed playback volume is a remote player,
   1639          * playing over HDMI where the user prefers to control the volume on the HDMI sink, rather
   1640          * than attenuate at the source.
   1641          * @see #getVolumeHandling()
   1642          */
   1643         public final static int PLAYBACK_VOLUME_FIXED = 0;
   1644         /**
   1645          * Playback information indicating the playback volume is variable and can be controlled
   1646          * from this object.
   1647          * @see #getVolumeHandling()
   1648          */
   1649         public final static int PLAYBACK_VOLUME_VARIABLE = 1;
   1650 
   1651         RouteInfo(RouteCategory category) {
   1652             mCategory = category;
   1653             mDeviceType = DEVICE_TYPE_UNKNOWN;
   1654         }
   1655 
   1656         /**
   1657          * Gets the user-visible name of the route.
   1658          * <p>
   1659          * The route name identifies the destination represented by the route.
   1660          * It may be a user-supplied name, an alias, or device serial number.
   1661          * </p>
   1662          *
   1663          * @return The user-visible name of a media route.  This is the string presented
   1664          * to users who may select this as the active route.
   1665          */
   1666         public CharSequence getName() {
   1667             return getName(sStatic.mResources);
   1668         }
   1669 
   1670         /**
   1671          * Return the properly localized/resource user-visible name of this route.
   1672          * <p>
   1673          * The route name identifies the destination represented by the route.
   1674          * It may be a user-supplied name, an alias, or device serial number.
   1675          * </p>
   1676          *
   1677          * @param context Context used to resolve the correct configuration to load
   1678          * @return The user-visible name of a media route.  This is the string presented
   1679          * to users who may select this as the active route.
   1680          */
   1681         public CharSequence getName(Context context) {
   1682             return getName(context.getResources());
   1683         }
   1684 
   1685         CharSequence getName(Resources res) {
   1686             if (mNameResId != 0) {
   1687                 return res.getText(mNameResId);
   1688             }
   1689             return mName;
   1690         }
   1691 
   1692         /**
   1693          * Gets the user-visible description of the route.
   1694          * <p>
   1695          * The route description describes the kind of destination represented by the route.
   1696          * It may be a user-supplied string, a model number or brand of device.
   1697          * </p>
   1698          *
   1699          * @return The description of the route, or null if none.
   1700          */
   1701         public CharSequence getDescription() {
   1702             return mDescription;
   1703         }
   1704 
   1705         /**
   1706          * @return The user-visible status for a media route. This may include a description
   1707          * of the currently playing media, if available.
   1708          */
   1709         public CharSequence getStatus() {
   1710             return mStatus;
   1711         }
   1712 
   1713         /**
   1714          * Set this route's status by predetermined status code. If the caller
   1715          * should dispatch a route changed event this call will return true;
   1716          */
   1717         boolean setRealStatusCode(int statusCode) {
   1718             if (mRealStatusCode != statusCode) {
   1719                 mRealStatusCode = statusCode;
   1720                 return resolveStatusCode();
   1721             }
   1722             return false;
   1723         }
   1724 
   1725         /**
   1726          * Resolves the status code whenever the real status code or selection state
   1727          * changes.
   1728          */
   1729         boolean resolveStatusCode() {
   1730             int statusCode = mRealStatusCode;
   1731             if (isSelected()) {
   1732                 switch (statusCode) {
   1733                     // If the route is selected and its status appears to be between states
   1734                     // then report it as connecting even though it has not yet had a chance
   1735                     // to officially move into the CONNECTING state.  Note that routes in
   1736                     // the NONE state are assumed to not require an explicit connection
   1737                     // lifecycle whereas those that are AVAILABLE are assumed to have
   1738                     // to eventually proceed to CONNECTED.
   1739                     case STATUS_AVAILABLE:
   1740                     case STATUS_SCANNING:
   1741                         statusCode = STATUS_CONNECTING;
   1742                         break;
   1743                 }
   1744             }
   1745             if (mResolvedStatusCode == statusCode) {
   1746                 return false;
   1747             }
   1748 
   1749             mResolvedStatusCode = statusCode;
   1750             int resId;
   1751             switch (statusCode) {
   1752                 case STATUS_SCANNING:
   1753                     resId = com.android.internal.R.string.media_route_status_scanning;
   1754                     break;
   1755                 case STATUS_CONNECTING:
   1756                     resId = com.android.internal.R.string.media_route_status_connecting;
   1757                     break;
   1758                 case STATUS_AVAILABLE:
   1759                     resId = com.android.internal.R.string.media_route_status_available;
   1760                     break;
   1761                 case STATUS_NOT_AVAILABLE:
   1762                     resId = com.android.internal.R.string.media_route_status_not_available;
   1763                     break;
   1764                 case STATUS_IN_USE:
   1765                     resId = com.android.internal.R.string.media_route_status_in_use;
   1766                     break;
   1767                 case STATUS_CONNECTED:
   1768                 case STATUS_NONE:
   1769                 default:
   1770                     resId = 0;
   1771                     break;
   1772             }
   1773             mStatus = resId != 0 ? sStatic.mResources.getText(resId) : null;
   1774             return true;
   1775         }
   1776 
   1777         /**
   1778          * @hide
   1779          */
   1780         public int getStatusCode() {
   1781             return mResolvedStatusCode;
   1782         }
   1783 
   1784         /**
   1785          * @return A media type flag set describing which types this route supports.
   1786          */
   1787         public int getSupportedTypes() {
   1788             return mSupportedTypes;
   1789         }
   1790 
   1791         /**
   1792          * Gets the type of the receiver device associated with this route.
   1793          *
   1794          * @return The type of the receiver device associated with this route:
   1795          * {@link #DEVICE_TYPE_BLUETOOTH}, {@link #DEVICE_TYPE_TV}, {@link #DEVICE_TYPE_SPEAKER},
   1796          * or {@link #DEVICE_TYPE_UNKNOWN}.
   1797          */
   1798         @DeviceType
   1799         public int getDeviceType() {
   1800             return mDeviceType;
   1801         }
   1802 
   1803         /** @hide */
   1804         public boolean matchesTypes(int types) {
   1805             return (mSupportedTypes & types) != 0;
   1806         }
   1807 
   1808         /**
   1809          * @return The group that this route belongs to.
   1810          */
   1811         public RouteGroup getGroup() {
   1812             return mGroup;
   1813         }
   1814 
   1815         /**
   1816          * @return the category this route belongs to.
   1817          */
   1818         public RouteCategory getCategory() {
   1819             return mCategory;
   1820         }
   1821 
   1822         /**
   1823          * Get the icon representing this route.
   1824          * This icon will be used in picker UIs if available.
   1825          *
   1826          * @return the icon representing this route or null if no icon is available
   1827          */
   1828         public Drawable getIconDrawable() {
   1829             return mIcon;
   1830         }
   1831 
   1832         /**
   1833          * Set an application-specific tag object for this route.
   1834          * The application may use this to store arbitrary data associated with the
   1835          * route for internal tracking.
   1836          *
   1837          * <p>Note that the lifespan of a route may be well past the lifespan of
   1838          * an Activity or other Context; take care that objects you store here
   1839          * will not keep more data in memory alive than you intend.</p>
   1840          *
   1841          * @param tag Arbitrary, app-specific data for this route to hold for later use
   1842          */
   1843         public void setTag(Object tag) {
   1844             mTag = tag;
   1845             routeUpdated();
   1846         }
   1847 
   1848         /**
   1849          * @return The tag object previously set by the application
   1850          * @see #setTag(Object)
   1851          */
   1852         public Object getTag() {
   1853             return mTag;
   1854         }
   1855 
   1856         /**
   1857          * @return the type of playback associated with this route
   1858          * @see UserRouteInfo#setPlaybackType(int)
   1859          */
   1860         @PlaybackType
   1861         public int getPlaybackType() {
   1862             return mPlaybackType;
   1863         }
   1864 
   1865         /**
   1866          * @return the stream over which the playback associated with this route is performed
   1867          * @see UserRouteInfo#setPlaybackStream(int)
   1868          */
   1869         public int getPlaybackStream() {
   1870             return mPlaybackStream;
   1871         }
   1872 
   1873         /**
   1874          * Return the current volume for this route. Depending on the route, this may only
   1875          * be valid if the route is currently selected.
   1876          *
   1877          * @return the volume at which the playback associated with this route is performed
   1878          * @see UserRouteInfo#setVolume(int)
   1879          */
   1880         public int getVolume() {
   1881             if (mPlaybackType == PLAYBACK_TYPE_LOCAL) {
   1882                 int vol = 0;
   1883                 try {
   1884                     vol = sStatic.mAudioService.getStreamVolume(mPlaybackStream);
   1885                 } catch (RemoteException e) {
   1886                     Log.e(TAG, "Error getting local stream volume", e);
   1887                 }
   1888                 return vol;
   1889             } else {
   1890                 return mVolume;
   1891             }
   1892         }
   1893 
   1894         /**
   1895          * Request a volume change for this route.
   1896          * @param volume value between 0 and getVolumeMax
   1897          */
   1898         public void requestSetVolume(int volume) {
   1899             if (mPlaybackType == PLAYBACK_TYPE_LOCAL) {
   1900                 try {
   1901                     sStatic.mAudioService.setStreamVolume(mPlaybackStream, volume, 0,
   1902                             ActivityThread.currentPackageName());
   1903                 } catch (RemoteException e) {
   1904                     Log.e(TAG, "Error setting local stream volume", e);
   1905                 }
   1906             } else {
   1907                 sStatic.requestSetVolume(this, volume);
   1908             }
   1909         }
   1910 
   1911         /**
   1912          * Request an incremental volume update for this route.
   1913          * @param direction Delta to apply to the current volume
   1914          */
   1915         public void requestUpdateVolume(int direction) {
   1916             if (mPlaybackType == PLAYBACK_TYPE_LOCAL) {
   1917                 try {
   1918                     final int volume =
   1919                             Math.max(0, Math.min(getVolume() + direction, getVolumeMax()));
   1920                     sStatic.mAudioService.setStreamVolume(mPlaybackStream, volume, 0,
   1921                             ActivityThread.currentPackageName());
   1922                 } catch (RemoteException e) {
   1923                     Log.e(TAG, "Error setting local stream volume", e);
   1924                 }
   1925             } else {
   1926                 sStatic.requestUpdateVolume(this, direction);
   1927             }
   1928         }
   1929 
   1930         /**
   1931          * @return the maximum volume at which the playback associated with this route is performed
   1932          * @see UserRouteInfo#setVolumeMax(int)
   1933          */
   1934         public int getVolumeMax() {
   1935             if (mPlaybackType == PLAYBACK_TYPE_LOCAL) {
   1936                 int volMax = 0;
   1937                 try {
   1938                     volMax = sStatic.mAudioService.getStreamMaxVolume(mPlaybackStream);
   1939                 } catch (RemoteException e) {
   1940                     Log.e(TAG, "Error getting local stream volume", e);
   1941                 }
   1942                 return volMax;
   1943             } else {
   1944                 return mVolumeMax;
   1945             }
   1946         }
   1947 
   1948         /**
   1949          * @return how volume is handling on the route
   1950          * @see UserRouteInfo#setVolumeHandling(int)
   1951          */
   1952         @PlaybackVolume
   1953         public int getVolumeHandling() {
   1954             return mVolumeHandling;
   1955         }
   1956 
   1957         /**
   1958          * Gets the {@link Display} that should be used by the application to show
   1959          * a {@link android.app.Presentation} on an external display when this route is selected.
   1960          * Depending on the route, this may only be valid if the route is currently
   1961          * selected.
   1962          * <p>
   1963          * The preferred presentation display may change independently of the route
   1964          * being selected or unselected.  For example, the presentation display
   1965          * of the default system route may change when an external HDMI display is connected
   1966          * or disconnected even though the route itself has not changed.
   1967          * </p><p>
   1968          * This method may return null if there is no external display associated with
   1969          * the route or if the display is not ready to show UI yet.
   1970          * </p><p>
   1971          * The application should listen for changes to the presentation display
   1972          * using the {@link Callback#onRoutePresentationDisplayChanged} callback and
   1973          * show or dismiss its {@link android.app.Presentation} accordingly when the display
   1974          * becomes available or is removed.
   1975          * </p><p>
   1976          * This method only makes sense for {@link #ROUTE_TYPE_LIVE_VIDEO live video} routes.
   1977          * </p>
   1978          *
   1979          * @return The preferred presentation display to use when this route is
   1980          * selected or null if none.
   1981          *
   1982          * @see #ROUTE_TYPE_LIVE_VIDEO
   1983          * @see android.app.Presentation
   1984          */
   1985         public Display getPresentationDisplay() {
   1986             return mPresentationDisplay;
   1987         }
   1988 
   1989         boolean updatePresentationDisplay() {
   1990             Display display = choosePresentationDisplay();
   1991             if (mPresentationDisplay != display) {
   1992                 mPresentationDisplay = display;
   1993                 return true;
   1994             }
   1995             return false;
   1996         }
   1997 
   1998         private Display choosePresentationDisplay() {
   1999             if ((mSupportedTypes & ROUTE_TYPE_LIVE_VIDEO) != 0) {
   2000                 Display[] displays = sStatic.getAllPresentationDisplays();
   2001 
   2002                 // Ensure that the specified display is valid for presentations.
   2003                 // This check will normally disallow the default display unless it was
   2004                 // configured as a presentation display for some reason.
   2005                 if (mPresentationDisplayId >= 0) {
   2006                     for (Display display : displays) {
   2007                         if (display.getDisplayId() == mPresentationDisplayId) {
   2008                             return display;
   2009                         }
   2010                     }
   2011                     return null;
   2012                 }
   2013 
   2014                 // Find the indicated Wifi display by its address.
   2015                 if (mDeviceAddress != null) {
   2016                     for (Display display : displays) {
   2017                         if (display.getType() == Display.TYPE_WIFI
   2018                                 && mDeviceAddress.equals(display.getAddress())) {
   2019                             return display;
   2020                         }
   2021                     }
   2022                     return null;
   2023                 }
   2024 
   2025                 // For the default route, choose the first presentation display from the list.
   2026                 if (this == sStatic.mDefaultAudioVideo && displays.length > 0) {
   2027                     return displays[0];
   2028                 }
   2029             }
   2030             return null;
   2031         }
   2032 
   2033         /** @hide */
   2034         public String getDeviceAddress() {
   2035             return mDeviceAddress;
   2036         }
   2037 
   2038         /**
   2039          * Returns true if this route is enabled and may be selected.
   2040          *
   2041          * @return True if this route is enabled.
   2042          */
   2043         public boolean isEnabled() {
   2044             return mEnabled;
   2045         }
   2046 
   2047         /**
   2048          * Returns true if the route is in the process of connecting and is not
   2049          * yet ready for use.
   2050          *
   2051          * @return True if this route is in the process of connecting.
   2052          */
   2053         public boolean isConnecting() {
   2054             return mResolvedStatusCode == STATUS_CONNECTING;
   2055         }
   2056 
   2057         /** @hide */
   2058         public boolean isSelected() {
   2059             return this == sStatic.mSelectedRoute;
   2060         }
   2061 
   2062         /** @hide */
   2063         public boolean isDefault() {
   2064             return this == sStatic.mDefaultAudioVideo;
   2065         }
   2066 
   2067         /** @hide */
   2068         public boolean isBluetooth() {
   2069             return this == sStatic.mBluetoothA2dpRoute;
   2070         }
   2071 
   2072         /** @hide */
   2073         public void select() {
   2074             selectRouteStatic(mSupportedTypes, this, true);
   2075         }
   2076 
   2077         void setStatusInt(CharSequence status) {
   2078             if (!status.equals(mStatus)) {
   2079                 mStatus = status;
   2080                 if (mGroup != null) {
   2081                     mGroup.memberStatusChanged(this, status);
   2082                 }
   2083                 routeUpdated();
   2084             }
   2085         }
   2086 
   2087         final IRemoteVolumeObserver.Stub mRemoteVolObserver = new IRemoteVolumeObserver.Stub() {
   2088             @Override
   2089             public void dispatchRemoteVolumeUpdate(final int direction, final int value) {
   2090                 sStatic.mHandler.post(new Runnable() {
   2091                     @Override
   2092                     public void run() {
   2093                         if (mVcb != null) {
   2094                             if (direction != 0) {
   2095                                 mVcb.vcb.onVolumeUpdateRequest(mVcb.route, direction);
   2096                             } else {
   2097                                 mVcb.vcb.onVolumeSetRequest(mVcb.route, value);
   2098                             }
   2099                         }
   2100                     }
   2101                 });
   2102             }
   2103         };
   2104 
   2105         void routeUpdated() {
   2106             updateRoute(this);
   2107         }
   2108 
   2109         @Override
   2110         public String toString() {
   2111             String supportedTypes = typesToString(getSupportedTypes());
   2112             return getClass().getSimpleName() + "{ name=" + getName() +
   2113                     ", description=" + getDescription() +
   2114                     ", status=" + getStatus() +
   2115                     ", category=" + getCategory() +
   2116                     ", supportedTypes=" + supportedTypes +
   2117                     ", presentationDisplay=" + mPresentationDisplay + " }";
   2118         }
   2119     }
   2120 
   2121     /**
   2122      * Information about a route that the application may define and modify.
   2123      * A user route defaults to {@link RouteInfo#PLAYBACK_TYPE_REMOTE} and
   2124      * {@link RouteInfo#PLAYBACK_VOLUME_FIXED}.
   2125      *
   2126      * @see MediaRouter.RouteInfo
   2127      */
   2128     public static class UserRouteInfo extends RouteInfo {
   2129         RemoteControlClient mRcc;
   2130         SessionVolumeProvider mSvp;
   2131 
   2132         UserRouteInfo(RouteCategory category) {
   2133             super(category);
   2134             mSupportedTypes = ROUTE_TYPE_USER;
   2135             mPlaybackType = PLAYBACK_TYPE_REMOTE;
   2136             mVolumeHandling = PLAYBACK_VOLUME_FIXED;
   2137         }
   2138 
   2139         /**
   2140          * Set the user-visible name of this route.
   2141          * @param name Name to display to the user to describe this route
   2142          */
   2143         public void setName(CharSequence name) {
   2144             mNameResId = 0;
   2145             mName = name;
   2146             routeUpdated();
   2147         }
   2148 
   2149         /**
   2150          * Set the user-visible name of this route.
   2151          * <p>
   2152          * The route name identifies the destination represented by the route.
   2153          * It may be a user-supplied name, an alias, or device serial number.
   2154          * </p>
   2155          *
   2156          * @param resId Resource ID of the name to display to the user to describe this route
   2157          */
   2158         public void setName(int resId) {
   2159             mNameResId = resId;
   2160             mName = null;
   2161             routeUpdated();
   2162         }
   2163 
   2164         /**
   2165          * Set the user-visible description of this route.
   2166          * <p>
   2167          * The route description describes the kind of destination represented by the route.
   2168          * It may be a user-supplied string, a model number or brand of device.
   2169          * </p>
   2170          *
   2171          * @param description The description of the route, or null if none.
   2172          */
   2173         public void setDescription(CharSequence description) {
   2174             mDescription = description;
   2175             routeUpdated();
   2176         }
   2177 
   2178         /**
   2179          * Set the current user-visible status for this route.
   2180          * @param status Status to display to the user to describe what the endpoint
   2181          * of this route is currently doing
   2182          */
   2183         public void setStatus(CharSequence status) {
   2184             setStatusInt(status);
   2185         }
   2186 
   2187         /**
   2188          * Set the RemoteControlClient responsible for reporting playback info for this
   2189          * user route.
   2190          *
   2191          * <p>If this route manages remote playback, the data exposed by this
   2192          * RemoteControlClient will be used to reflect and update information
   2193          * such as route volume info in related UIs.</p>
   2194          *
   2195          * <p>The RemoteControlClient must have been previously registered with
   2196          * {@link AudioManager#registerRemoteControlClient(RemoteControlClient)}.</p>
   2197          *
   2198          * @param rcc RemoteControlClient associated with this route
   2199          */
   2200         public void setRemoteControlClient(RemoteControlClient rcc) {
   2201             mRcc = rcc;
   2202             updatePlaybackInfoOnRcc();
   2203         }
   2204 
   2205         /**
   2206          * Retrieve the RemoteControlClient associated with this route, if one has been set.
   2207          *
   2208          * @return the RemoteControlClient associated with this route
   2209          * @see #setRemoteControlClient(RemoteControlClient)
   2210          */
   2211         public RemoteControlClient getRemoteControlClient() {
   2212             return mRcc;
   2213         }
   2214 
   2215         /**
   2216          * Set an icon that will be used to represent this route.
   2217          * The system may use this icon in picker UIs or similar.
   2218          *
   2219          * @param icon icon drawable to use to represent this route
   2220          */
   2221         public void setIconDrawable(Drawable icon) {
   2222             mIcon = icon;
   2223         }
   2224 
   2225         /**
   2226          * Set an icon that will be used to represent this route.
   2227          * The system may use this icon in picker UIs or similar.
   2228          *
   2229          * @param resId Resource ID of an icon drawable to use to represent this route
   2230          */
   2231         public void setIconResource(@DrawableRes int resId) {
   2232             setIconDrawable(sStatic.mResources.getDrawable(resId));
   2233         }
   2234 
   2235         /**
   2236          * Set a callback to be notified of volume update requests
   2237          * @param vcb
   2238          */
   2239         public void setVolumeCallback(VolumeCallback vcb) {
   2240             mVcb = new VolumeCallbackInfo(vcb, this);
   2241         }
   2242 
   2243         /**
   2244          * Defines whether playback associated with this route is "local"
   2245          *    ({@link RouteInfo#PLAYBACK_TYPE_LOCAL}) or "remote"
   2246          *    ({@link RouteInfo#PLAYBACK_TYPE_REMOTE}).
   2247          * @param type
   2248          */
   2249         public void setPlaybackType(@RouteInfo.PlaybackType int type) {
   2250             if (mPlaybackType != type) {
   2251                 mPlaybackType = type;
   2252                 configureSessionVolume();
   2253             }
   2254         }
   2255 
   2256         /**
   2257          * Defines whether volume for the playback associated with this route is fixed
   2258          * ({@link RouteInfo#PLAYBACK_VOLUME_FIXED}) or can modified
   2259          * ({@link RouteInfo#PLAYBACK_VOLUME_VARIABLE}).
   2260          * @param volumeHandling
   2261          */
   2262         public void setVolumeHandling(@RouteInfo.PlaybackVolume int volumeHandling) {
   2263             if (mVolumeHandling != volumeHandling) {
   2264                 mVolumeHandling = volumeHandling;
   2265                 configureSessionVolume();
   2266             }
   2267         }
   2268 
   2269         /**
   2270          * Defines at what volume the playback associated with this route is performed (for user
   2271          * feedback purposes). This information is only used when the playback is not local.
   2272          * @param volume
   2273          */
   2274         public void setVolume(int volume) {
   2275             volume = Math.max(0, Math.min(volume, getVolumeMax()));
   2276             if (mVolume != volume) {
   2277                 mVolume = volume;
   2278                 if (mSvp != null) {
   2279                     mSvp.setCurrentVolume(mVolume);
   2280                 }
   2281                 dispatchRouteVolumeChanged(this);
   2282                 if (mGroup != null) {
   2283                     mGroup.memberVolumeChanged(this);
   2284                 }
   2285             }
   2286         }
   2287 
   2288         @Override
   2289         public void requestSetVolume(int volume) {
   2290             if (mVolumeHandling == PLAYBACK_VOLUME_VARIABLE) {
   2291                 if (mVcb == null) {
   2292                     Log.e(TAG, "Cannot requestSetVolume on user route - no volume callback set");
   2293                     return;
   2294                 }
   2295                 mVcb.vcb.onVolumeSetRequest(this, volume);
   2296             }
   2297         }
   2298 
   2299         @Override
   2300         public void requestUpdateVolume(int direction) {
   2301             if (mVolumeHandling == PLAYBACK_VOLUME_VARIABLE) {
   2302                 if (mVcb == null) {
   2303                     Log.e(TAG, "Cannot requestChangeVolume on user route - no volumec callback set");
   2304                     return;
   2305                 }
   2306                 mVcb.vcb.onVolumeUpdateRequest(this, direction);
   2307             }
   2308         }
   2309 
   2310         /**
   2311          * Defines the maximum volume at which the playback associated with this route is performed
   2312          * (for user feedback purposes). This information is only used when the playback is not
   2313          * local.
   2314          * @param volumeMax
   2315          */
   2316         public void setVolumeMax(int volumeMax) {
   2317             if (mVolumeMax != volumeMax) {
   2318                 mVolumeMax = volumeMax;
   2319                 configureSessionVolume();
   2320             }
   2321         }
   2322 
   2323         /**
   2324          * Defines over what stream type the media is presented.
   2325          * @param stream
   2326          */
   2327         public void setPlaybackStream(int stream) {
   2328             if (mPlaybackStream != stream) {
   2329                 mPlaybackStream = stream;
   2330                 configureSessionVolume();
   2331             }
   2332         }
   2333 
   2334         private void updatePlaybackInfoOnRcc() {
   2335             configureSessionVolume();
   2336         }
   2337 
   2338         private void configureSessionVolume() {
   2339             if (mRcc == null) {
   2340                 if (DEBUG) {
   2341                     Log.d(TAG, "No Rcc to configure volume for route " + getName());
   2342                 }
   2343                 return;
   2344             }
   2345             MediaSession session = mRcc.getMediaSession();
   2346             if (session == null) {
   2347                 if (DEBUG) {
   2348                     Log.d(TAG, "Rcc has no session to configure volume");
   2349                 }
   2350                 return;
   2351             }
   2352             if (mPlaybackType == RemoteControlClient.PLAYBACK_TYPE_REMOTE) {
   2353                 @VolumeProvider.ControlType int volumeControl =
   2354                         VolumeProvider.VOLUME_CONTROL_FIXED;
   2355                 switch (mVolumeHandling) {
   2356                     case RemoteControlClient.PLAYBACK_VOLUME_VARIABLE:
   2357                         volumeControl = VolumeProvider.VOLUME_CONTROL_ABSOLUTE;
   2358                         break;
   2359                     case RemoteControlClient.PLAYBACK_VOLUME_FIXED:
   2360                     default:
   2361                         break;
   2362                 }
   2363                 // Only register a new listener if necessary
   2364                 if (mSvp == null || mSvp.getVolumeControl() != volumeControl
   2365                         || mSvp.getMaxVolume() != mVolumeMax) {
   2366                     mSvp = new SessionVolumeProvider(volumeControl, mVolumeMax, mVolume);
   2367                     session.setPlaybackToRemote(mSvp);
   2368                 }
   2369             } else {
   2370                 // We only know how to handle local and remote, fall back to local if not remote.
   2371                 AudioAttributes.Builder bob = new AudioAttributes.Builder();
   2372                 bob.setLegacyStreamType(mPlaybackStream);
   2373                 session.setPlaybackToLocal(bob.build());
   2374                 mSvp = null;
   2375             }
   2376         }
   2377 
   2378         class SessionVolumeProvider extends VolumeProvider {
   2379 
   2380             public SessionVolumeProvider(@VolumeProvider.ControlType int volumeControl,
   2381                     int maxVolume, int currentVolume) {
   2382                 super(volumeControl, maxVolume, currentVolume);
   2383             }
   2384 
   2385             @Override
   2386             public void onSetVolumeTo(final int volume) {
   2387                 sStatic.mHandler.post(new Runnable() {
   2388                     @Override
   2389                     public void run() {
   2390                         if (mVcb != null) {
   2391                             mVcb.vcb.onVolumeSetRequest(mVcb.route, volume);
   2392                         }
   2393                     }
   2394                 });
   2395             }
   2396 
   2397             @Override
   2398             public void onAdjustVolume(final int direction) {
   2399                 sStatic.mHandler.post(new Runnable() {
   2400                     @Override
   2401                     public void run() {
   2402                         if (mVcb != null) {
   2403                             mVcb.vcb.onVolumeUpdateRequest(mVcb.route, direction);
   2404                         }
   2405                     }
   2406                 });
   2407             }
   2408         }
   2409     }
   2410 
   2411     /**
   2412      * Information about a route that consists of multiple other routes in a group.
   2413      */
   2414     public static class RouteGroup extends RouteInfo {
   2415         final ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>();
   2416         private boolean mUpdateName;
   2417 
   2418         RouteGroup(RouteCategory category) {
   2419             super(category);
   2420             mGroup = this;
   2421             mVolumeHandling = PLAYBACK_VOLUME_FIXED;
   2422         }
   2423 
   2424         @Override
   2425         CharSequence getName(Resources res) {
   2426             if (mUpdateName) updateName();
   2427             return super.getName(res);
   2428         }
   2429 
   2430         /**
   2431          * Add a route to this group. The route must not currently belong to another group.
   2432          *
   2433          * @param route route to add to this group
   2434          */
   2435         public void addRoute(RouteInfo route) {
   2436             if (route.getGroup() != null) {
   2437                 throw new IllegalStateException("Route " + route + " is already part of a group.");
   2438             }
   2439             if (route.getCategory() != mCategory) {
   2440                 throw new IllegalArgumentException(
   2441                         "Route cannot be added to a group with a different category. " +
   2442                             "(Route category=" + route.getCategory() +
   2443                             " group category=" + mCategory + ")");
   2444             }
   2445             final int at = mRoutes.size();
   2446             mRoutes.add(route);
   2447             route.mGroup = this;
   2448             mUpdateName = true;
   2449             updateVolume();
   2450             routeUpdated();
   2451             dispatchRouteGrouped(route, this, at);
   2452         }
   2453 
   2454         /**
   2455          * Add a route to this group before the specified index.
   2456          *
   2457          * @param route route to add
   2458          * @param insertAt insert the new route before this index
   2459          */
   2460         public void addRoute(RouteInfo route, int insertAt) {
   2461             if (route.getGroup() != null) {
   2462                 throw new IllegalStateException("Route " + route + " is already part of a group.");
   2463             }
   2464             if (route.getCategory() != mCategory) {
   2465                 throw new IllegalArgumentException(
   2466                         "Route cannot be added to a group with a different category. " +
   2467                             "(Route category=" + route.getCategory() +
   2468                             " group category=" + mCategory + ")");
   2469             }
   2470             mRoutes.add(insertAt, route);
   2471             route.mGroup = this;
   2472             mUpdateName = true;
   2473             updateVolume();
   2474             routeUpdated();
   2475             dispatchRouteGrouped(route, this, insertAt);
   2476         }
   2477 
   2478         /**
   2479          * Remove a route from this group.
   2480          *
   2481          * @param route route to remove
   2482          */
   2483         public void removeRoute(RouteInfo route) {
   2484             if (route.getGroup() != this) {
   2485                 throw new IllegalArgumentException("Route " + route +
   2486                         " is not a member of this group.");
   2487             }
   2488             mRoutes.remove(route);
   2489             route.mGroup = null;
   2490             mUpdateName = true;
   2491             updateVolume();
   2492             dispatchRouteUngrouped(route, this);
   2493             routeUpdated();
   2494         }
   2495 
   2496         /**
   2497          * Remove the route at the specified index from this group.
   2498          *
   2499          * @param index index of the route to remove
   2500          */
   2501         public void removeRoute(int index) {
   2502             RouteInfo route = mRoutes.remove(index);
   2503             route.mGroup = null;
   2504             mUpdateName = true;
   2505             updateVolume();
   2506             dispatchRouteUngrouped(route, this);
   2507             routeUpdated();
   2508         }
   2509 
   2510         /**
   2511          * @return The number of routes in this group
   2512          */
   2513         public int getRouteCount() {
   2514             return mRoutes.size();
   2515         }
   2516 
   2517         /**
   2518          * Return the route in this group at the specified index
   2519          *
   2520          * @param index Index to fetch
   2521          * @return The route at index
   2522          */
   2523         public RouteInfo getRouteAt(int index) {
   2524             return mRoutes.get(index);
   2525         }
   2526 
   2527         /**
   2528          * Set an icon that will be used to represent this group.
   2529          * The system may use this icon in picker UIs or similar.
   2530          *
   2531          * @param icon icon drawable to use to represent this group
   2532          */
   2533         public void setIconDrawable(Drawable icon) {
   2534             mIcon = icon;
   2535         }
   2536 
   2537         /**
   2538          * Set an icon that will be used to represent this group.
   2539          * The system may use this icon in picker UIs or similar.
   2540          *
   2541          * @param resId Resource ID of an icon drawable to use to represent this group
   2542          */
   2543         public void setIconResource(@DrawableRes int resId) {
   2544             setIconDrawable(sStatic.mResources.getDrawable(resId));
   2545         }
   2546 
   2547         @Override
   2548         public void requestSetVolume(int volume) {
   2549             final int maxVol = getVolumeMax();
   2550             if (maxVol == 0) {
   2551                 return;
   2552             }
   2553 
   2554             final float scaledVolume = (float) volume / maxVol;
   2555             final int routeCount = getRouteCount();
   2556             for (int i = 0; i < routeCount; i++) {
   2557                 final RouteInfo route = getRouteAt(i);
   2558                 final int routeVol = (int) (scaledVolume * route.getVolumeMax());
   2559                 route.requestSetVolume(routeVol);
   2560             }
   2561             if (volume != mVolume) {
   2562                 mVolume = volume;
   2563                 dispatchRouteVolumeChanged(this);
   2564             }
   2565         }
   2566 
   2567         @Override
   2568         public void requestUpdateVolume(int direction) {
   2569             final int maxVol = getVolumeMax();
   2570             if (maxVol == 0) {
   2571                 return;
   2572             }
   2573 
   2574             final int routeCount = getRouteCount();
   2575             int volume = 0;
   2576             for (int i = 0; i < routeCount; i++) {
   2577                 final RouteInfo route = getRouteAt(i);
   2578                 route.requestUpdateVolume(direction);
   2579                 final int routeVol = route.getVolume();
   2580                 if (routeVol > volume) {
   2581                     volume = routeVol;
   2582                 }
   2583             }
   2584             if (volume != mVolume) {
   2585                 mVolume = volume;
   2586                 dispatchRouteVolumeChanged(this);
   2587             }
   2588         }
   2589 
   2590         void memberNameChanged(RouteInfo info, CharSequence name) {
   2591             mUpdateName = true;
   2592             routeUpdated();
   2593         }
   2594 
   2595         void memberStatusChanged(RouteInfo info, CharSequence status) {
   2596             setStatusInt(status);
   2597         }
   2598 
   2599         void memberVolumeChanged(RouteInfo info) {
   2600             updateVolume();
   2601         }
   2602 
   2603         void updateVolume() {
   2604             // A group always represents the highest component volume value.
   2605             final int routeCount = getRouteCount();
   2606             int volume = 0;
   2607             for (int i = 0; i < routeCount; i++) {
   2608                 final int routeVol = getRouteAt(i).getVolume();
   2609                 if (routeVol > volume) {
   2610                     volume = routeVol;
   2611                 }
   2612             }
   2613             if (volume != mVolume) {
   2614                 mVolume = volume;
   2615                 dispatchRouteVolumeChanged(this);
   2616             }
   2617         }
   2618 
   2619         @Override
   2620         void routeUpdated() {
   2621             int types = 0;
   2622             final int count = mRoutes.size();
   2623             if (count == 0) {
   2624                 // Don't keep empty groups in the router.
   2625                 MediaRouter.removeRouteStatic(this);
   2626                 return;
   2627             }
   2628 
   2629             int maxVolume = 0;
   2630             boolean isLocal = true;
   2631             boolean isFixedVolume = true;
   2632             for (int i = 0; i < count; i++) {
   2633                 final RouteInfo route = mRoutes.get(i);
   2634                 types |= route.mSupportedTypes;
   2635                 final int routeMaxVolume = route.getVolumeMax();
   2636                 if (routeMaxVolume > maxVolume) {
   2637                     maxVolume = routeMaxVolume;
   2638                 }
   2639                 isLocal &= route.getPlaybackType() == PLAYBACK_TYPE_LOCAL;
   2640                 isFixedVolume &= route.getVolumeHandling() == PLAYBACK_VOLUME_FIXED;
   2641             }
   2642             mPlaybackType = isLocal ? PLAYBACK_TYPE_LOCAL : PLAYBACK_TYPE_REMOTE;
   2643             mVolumeHandling = isFixedVolume ? PLAYBACK_VOLUME_FIXED : PLAYBACK_VOLUME_VARIABLE;
   2644             mSupportedTypes = types;
   2645             mVolumeMax = maxVolume;
   2646             mIcon = count == 1 ? mRoutes.get(0).getIconDrawable() : null;
   2647             super.routeUpdated();
   2648         }
   2649 
   2650         void updateName() {
   2651             final StringBuilder sb = new StringBuilder();
   2652             final int count = mRoutes.size();
   2653             for (int i = 0; i < count; i++) {
   2654                 final RouteInfo info = mRoutes.get(i);
   2655                 // TODO: There's probably a much more correct way to localize this.
   2656                 if (i > 0) {
   2657                     sb.append(", ");
   2658                 }
   2659                 sb.append(info.getName());
   2660             }
   2661             mName = sb.toString();
   2662             mUpdateName = false;
   2663         }
   2664 
   2665         @Override
   2666         public String toString() {
   2667             StringBuilder sb = new StringBuilder(super.toString());
   2668             sb.append('[');
   2669             final int count = mRoutes.size();
   2670             for (int i = 0; i < count; i++) {
   2671                 if (i > 0) sb.append(", ");
   2672                 sb.append(mRoutes.get(i));
   2673             }
   2674             sb.append(']');
   2675             return sb.toString();
   2676         }
   2677     }
   2678 
   2679     /**
   2680      * Definition of a category of routes. All routes belong to a category.
   2681      */
   2682     public static class RouteCategory {
   2683         CharSequence mName;
   2684         int mNameResId;
   2685         int mTypes;
   2686         final boolean mGroupable;
   2687         boolean mIsSystem;
   2688 
   2689         RouteCategory(CharSequence name, int types, boolean groupable) {
   2690             mName = name;
   2691             mTypes = types;
   2692             mGroupable = groupable;
   2693         }
   2694 
   2695         RouteCategory(int nameResId, int types, boolean groupable) {
   2696             mNameResId = nameResId;
   2697             mTypes = types;
   2698             mGroupable = groupable;
   2699         }
   2700 
   2701         /**
   2702          * @return the name of this route category
   2703          */
   2704         public CharSequence getName() {
   2705             return getName(sStatic.mResources);
   2706         }
   2707 
   2708         /**
   2709          * Return the properly localized/configuration dependent name of this RouteCategory.
   2710          *
   2711          * @param context Context to resolve name resources
   2712          * @return the name of this route category
   2713          */
   2714         public CharSequence getName(Context context) {
   2715             return getName(context.getResources());
   2716         }
   2717 
   2718         CharSequence getName(Resources res) {
   2719             if (mNameResId != 0) {
   2720                 return res.getText(mNameResId);
   2721             }
   2722             return mName;
   2723         }
   2724 
   2725         /**
   2726          * Return the current list of routes in this category that have been added
   2727          * to the MediaRouter.
   2728          *
   2729          * <p>This list will not include routes that are nested within RouteGroups.
   2730          * A RouteGroup is treated as a single route within its category.</p>
   2731          *
   2732          * @param out a List to fill with the routes in this category. If this parameter is
   2733          *            non-null, it will be cleared, filled with the current routes with this
   2734          *            category, and returned. If this parameter is null, a new List will be
   2735          *            allocated to report the category's current routes.
   2736          * @return A list with the routes in this category that have been added to the MediaRouter.
   2737          */
   2738         public List<RouteInfo> getRoutes(List<RouteInfo> out) {
   2739             if (out == null) {
   2740                 out = new ArrayList<RouteInfo>();
   2741             } else {
   2742                 out.clear();
   2743             }
   2744 
   2745             final int count = getRouteCountStatic();
   2746             for (int i = 0; i < count; i++) {
   2747                 final RouteInfo route = getRouteAtStatic(i);
   2748                 if (route.mCategory == this) {
   2749                     out.add(route);
   2750                 }
   2751             }
   2752             return out;
   2753         }
   2754 
   2755         /**
   2756          * @return Flag set describing the route types supported by this category
   2757          */
   2758         public int getSupportedTypes() {
   2759             return mTypes;
   2760         }
   2761 
   2762         /**
   2763          * Return whether or not this category supports grouping.
   2764          *
   2765          * <p>If this method returns true, all routes obtained from this category
   2766          * via calls to {@link #getRouteAt(int)} will be {@link MediaRouter.RouteGroup}s.</p>
   2767          *
   2768          * @return true if this category supports
   2769          */
   2770         public boolean isGroupable() {
   2771             return mGroupable;
   2772         }
   2773 
   2774         /**
   2775          * @return true if this is the category reserved for system routes.
   2776          * @hide
   2777          */
   2778         public boolean isSystem() {
   2779             return mIsSystem;
   2780         }
   2781 
   2782         @Override
   2783         public String toString() {
   2784             return "RouteCategory{ name=" + getName() + " types=" + typesToString(mTypes) +
   2785                     " groupable=" + mGroupable + " }";
   2786         }
   2787     }
   2788 
   2789     static class CallbackInfo {
   2790         public int type;
   2791         public int flags;
   2792         public final Callback cb;
   2793         public final MediaRouter router;
   2794 
   2795         public CallbackInfo(Callback cb, int type, int flags, MediaRouter router) {
   2796             this.cb = cb;
   2797             this.type = type;
   2798             this.flags = flags;
   2799             this.router = router;
   2800         }
   2801 
   2802         public boolean filterRouteEvent(RouteInfo route) {
   2803             return filterRouteEvent(route.mSupportedTypes);
   2804         }
   2805 
   2806         public boolean filterRouteEvent(int supportedTypes) {
   2807             return (flags & CALLBACK_FLAG_UNFILTERED_EVENTS) != 0
   2808                     || (type & supportedTypes) != 0;
   2809         }
   2810     }
   2811 
   2812     /**
   2813      * Interface for receiving events about media routing changes.
   2814      * All methods of this interface will be called from the application's main thread.
   2815      * <p>
   2816      * A Callback will only receive events relevant to routes that the callback
   2817      * was registered for unless the {@link MediaRouter#CALLBACK_FLAG_UNFILTERED_EVENTS}
   2818      * flag was specified in {@link MediaRouter#addCallback(int, Callback, int)}.
   2819      * </p>
   2820      *
   2821      * @see MediaRouter#addCallback(int, Callback, int)
   2822      * @see MediaRouter#removeCallback(Callback)
   2823      */
   2824     public static abstract class Callback {
   2825         /**
   2826          * Called when the supplied route becomes selected as the active route
   2827          * for the given route type.
   2828          *
   2829          * @param router the MediaRouter reporting the event
   2830          * @param type Type flag set indicating the routes that have been selected
   2831          * @param info Route that has been selected for the given route types
   2832          */
   2833         public abstract void onRouteSelected(MediaRouter router, int type, RouteInfo info);
   2834 
   2835         /**
   2836          * Called when the supplied route becomes unselected as the active route
   2837          * for the given route type.
   2838          *
   2839          * @param router the MediaRouter reporting the event
   2840          * @param type Type flag set indicating the routes that have been unselected
   2841          * @param info Route that has been unselected for the given route types
   2842          */
   2843         public abstract void onRouteUnselected(MediaRouter router, int type, RouteInfo info);
   2844 
   2845         /**
   2846          * Called when a route for the specified type was added.
   2847          *
   2848          * @param router the MediaRouter reporting the event
   2849          * @param info Route that has become available for use
   2850          */
   2851         public abstract void onRouteAdded(MediaRouter router, RouteInfo info);
   2852 
   2853         /**
   2854          * Called when a route for the specified type was removed.
   2855          *
   2856          * @param router the MediaRouter reporting the event
   2857          * @param info Route that has been removed from availability
   2858          */
   2859         public abstract void onRouteRemoved(MediaRouter router, RouteInfo info);
   2860 
   2861         /**
   2862          * Called when an aspect of the indicated route has changed.
   2863          *
   2864          * <p>This will not indicate that the types supported by this route have
   2865          * changed, only that cosmetic info such as name or status have been updated.</p>
   2866          *
   2867          * @param router the MediaRouter reporting the event
   2868          * @param info The route that was changed
   2869          */
   2870         public abstract void onRouteChanged(MediaRouter router, RouteInfo info);
   2871 
   2872         /**
   2873          * Called when a route is added to a group.
   2874          *
   2875          * @param router the MediaRouter reporting the event
   2876          * @param info The route that was added
   2877          * @param group The group the route was added to
   2878          * @param index The route index within group that info was added at
   2879          */
   2880         public abstract void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group,
   2881                 int index);
   2882 
   2883         /**
   2884          * Called when a route is removed from a group.
   2885          *
   2886          * @param router the MediaRouter reporting the event
   2887          * @param info The route that was removed
   2888          * @param group The group the route was removed from
   2889          */
   2890         public abstract void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group);
   2891 
   2892         /**
   2893          * Called when a route's volume changes.
   2894          *
   2895          * @param router the MediaRouter reporting the event
   2896          * @param info The route with altered volume
   2897          */
   2898         public abstract void onRouteVolumeChanged(MediaRouter router, RouteInfo info);
   2899 
   2900         /**
   2901          * Called when a route's presentation display changes.
   2902          * <p>
   2903          * This method is called whenever the route's presentation display becomes
   2904          * available, is removes or has changes to some of its properties (such as its size).
   2905          * </p>
   2906          *
   2907          * @param router the MediaRouter reporting the event
   2908          * @param info The route whose presentation display changed
   2909          *
   2910          * @see RouteInfo#getPresentationDisplay()
   2911          */
   2912         public void onRoutePresentationDisplayChanged(MediaRouter router, RouteInfo info) {
   2913         }
   2914     }
   2915 
   2916     /**
   2917      * Stub implementation of {@link MediaRouter.Callback}.
   2918      * Each abstract method is defined as a no-op. Override just the ones
   2919      * you need.
   2920      */
   2921     public static class SimpleCallback extends Callback {
   2922 
   2923         @Override
   2924         public void onRouteSelected(MediaRouter router, int type, RouteInfo info) {
   2925         }
   2926 
   2927         @Override
   2928         public void onRouteUnselected(MediaRouter router, int type, RouteInfo info) {
   2929         }
   2930 
   2931         @Override
   2932         public void onRouteAdded(MediaRouter router, RouteInfo info) {
   2933         }
   2934 
   2935         @Override
   2936         public void onRouteRemoved(MediaRouter router, RouteInfo info) {
   2937         }
   2938 
   2939         @Override
   2940         public void onRouteChanged(MediaRouter router, RouteInfo info) {
   2941         }
   2942 
   2943         @Override
   2944         public void onRouteGrouped(MediaRouter router, RouteInfo info, RouteGroup group,
   2945                 int index) {
   2946         }
   2947 
   2948         @Override
   2949         public void onRouteUngrouped(MediaRouter router, RouteInfo info, RouteGroup group) {
   2950         }
   2951 
   2952         @Override
   2953         public void onRouteVolumeChanged(MediaRouter router, RouteInfo info) {
   2954         }
   2955     }
   2956 
   2957     static class VolumeCallbackInfo {
   2958         public final VolumeCallback vcb;
   2959         public final RouteInfo route;
   2960 
   2961         public VolumeCallbackInfo(VolumeCallback vcb, RouteInfo route) {
   2962             this.vcb = vcb;
   2963             this.route = route;
   2964         }
   2965     }
   2966 
   2967     /**
   2968      * Interface for receiving events about volume changes.
   2969      * All methods of this interface will be called from the application's main thread.
   2970      *
   2971      * <p>A VolumeCallback will only receive events relevant to routes that the callback
   2972      * was registered for.</p>
   2973      *
   2974      * @see UserRouteInfo#setVolumeCallback(VolumeCallback)
   2975      */
   2976     public static abstract class VolumeCallback {
   2977         /**
   2978          * Called when the volume for the route should be increased or decreased.
   2979          * @param info the route affected by this event
   2980          * @param direction an integer indicating whether the volume is to be increased
   2981          *     (positive value) or decreased (negative value).
   2982          *     For bundled changes, the absolute value indicates the number of changes
   2983          *     in the same direction, e.g. +3 corresponds to three "volume up" changes.
   2984          */
   2985         public abstract void onVolumeUpdateRequest(RouteInfo info, int direction);
   2986         /**
   2987          * Called when the volume for the route should be set to the given value
   2988          * @param info the route affected by this event
   2989          * @param volume an integer indicating the new volume value that should be used, always
   2990          *     between 0 and the value set by {@link UserRouteInfo#setVolumeMax(int)}.
   2991          */
   2992         public abstract void onVolumeSetRequest(RouteInfo info, int volume);
   2993     }
   2994 
   2995     static class VolumeChangeReceiver extends BroadcastReceiver {
   2996         @Override
   2997         public void onReceive(Context context, Intent intent) {
   2998             if (intent.getAction().equals(AudioManager.VOLUME_CHANGED_ACTION)) {
   2999                 final int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE,
   3000                         -1);
   3001                 if (streamType != AudioManager.STREAM_MUSIC) {
   3002                     return;
   3003                 }
   3004 
   3005                 final int newVolume = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_VALUE, 0);
   3006                 final int oldVolume = intent.getIntExtra(
   3007                         AudioManager.EXTRA_PREV_VOLUME_STREAM_VALUE, 0);
   3008                 if (newVolume != oldVolume) {
   3009                     systemVolumeChanged(newVolume);
   3010                 }
   3011             }
   3012         }
   3013     }
   3014 
   3015     static class WifiDisplayStatusChangedReceiver extends BroadcastReceiver {
   3016         @Override
   3017         public void onReceive(Context context, Intent intent) {
   3018             if (intent.getAction().equals(DisplayManager.ACTION_WIFI_DISPLAY_STATUS_CHANGED)) {
   3019                 updateWifiDisplayStatus((WifiDisplayStatus) intent.getParcelableExtra(
   3020                         DisplayManager.EXTRA_WIFI_DISPLAY_STATUS));
   3021             }
   3022         }
   3023     }
   3024 }
   3025