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