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