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