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