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