Home | History | Annotate | Download | only in media
      1 /*
      2  * Copyright (C) 2013 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 com.android.server.media;
     18 
     19 import com.android.internal.util.Objects;
     20 import com.android.server.Watchdog;
     21 
     22 import android.Manifest;
     23 import android.app.ActivityManager;
     24 import android.content.BroadcastReceiver;
     25 import android.content.Context;
     26 import android.content.Intent;
     27 import android.content.IntentFilter;
     28 import android.content.pm.PackageManager;
     29 import android.media.AudioSystem;
     30 import android.media.IMediaRouterClient;
     31 import android.media.IMediaRouterService;
     32 import android.media.MediaRouter;
     33 import android.media.MediaRouterClientState;
     34 import android.media.RemoteDisplayState;
     35 import android.media.RemoteDisplayState.RemoteDisplayInfo;
     36 import android.os.Binder;
     37 import android.os.Handler;
     38 import android.os.IBinder;
     39 import android.os.Looper;
     40 import android.os.Message;
     41 import android.os.RemoteException;
     42 import android.os.SystemClock;
     43 import android.text.TextUtils;
     44 import android.util.ArrayMap;
     45 import android.util.Log;
     46 import android.util.Slog;
     47 import android.util.SparseArray;
     48 import android.util.TimeUtils;
     49 
     50 import java.io.FileDescriptor;
     51 import java.io.PrintWriter;
     52 import java.util.ArrayList;
     53 import java.util.Collections;
     54 import java.util.List;
     55 
     56 /**
     57  * Provides a mechanism for discovering media routes and manages media playback
     58  * behalf of applications.
     59  * <p>
     60  * Currently supports discovering remote displays via remote display provider
     61  * services that have been registered by applications.
     62  * </p>
     63  */
     64 public final class MediaRouterService extends IMediaRouterService.Stub
     65         implements Watchdog.Monitor {
     66     private static final String TAG = "MediaRouterService";
     67     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
     68 
     69     /**
     70      * Timeout in milliseconds for a selected route to transition from a
     71      * disconnected state to a connecting state.  If we don't observe any
     72      * progress within this interval, then we will give up and unselect the route.
     73      */
     74     static final long CONNECTING_TIMEOUT = 5000;
     75 
     76     /**
     77      * Timeout in milliseconds for a selected route to transition from a
     78      * connecting state to a connected state.  If we don't observe any
     79      * progress within this interval, then we will give up and unselect the route.
     80      */
     81     static final long CONNECTED_TIMEOUT = 60000;
     82 
     83     private final Context mContext;
     84 
     85     // State guarded by mLock.
     86     private final Object mLock = new Object();
     87     private final SparseArray<UserRecord> mUserRecords = new SparseArray<UserRecord>();
     88     private final ArrayMap<IBinder, ClientRecord> mAllClientRecords =
     89             new ArrayMap<IBinder, ClientRecord>();
     90     private int mCurrentUserId = -1;
     91 
     92     public MediaRouterService(Context context) {
     93         mContext = context;
     94         Watchdog.getInstance().addMonitor(this);
     95     }
     96 
     97     public void systemRunning() {
     98         IntentFilter filter = new IntentFilter(Intent.ACTION_USER_SWITCHED);
     99         mContext.registerReceiver(new BroadcastReceiver() {
    100             @Override
    101             public void onReceive(Context context, Intent intent) {
    102                 if (intent.getAction().equals(Intent.ACTION_USER_SWITCHED)) {
    103                     switchUser();
    104                 }
    105             }
    106         }, filter);
    107 
    108         switchUser();
    109     }
    110 
    111     @Override
    112     public void monitor() {
    113         synchronized (mLock) { /* check for deadlock */ }
    114     }
    115 
    116     // Binder call
    117     @Override
    118     public void registerClientAsUser(IMediaRouterClient client, String packageName, int userId) {
    119         if (client == null) {
    120             throw new IllegalArgumentException("client must not be null");
    121         }
    122 
    123         final int uid = Binder.getCallingUid();
    124         if (!validatePackageName(uid, packageName)) {
    125             throw new SecurityException("packageName must match the calling uid");
    126         }
    127 
    128         final int pid = Binder.getCallingPid();
    129         final int resolvedUserId = ActivityManager.handleIncomingUser(pid, uid, userId,
    130                 false /*allowAll*/, true /*requireFull*/, "registerClientAsUser", packageName);
    131         final boolean trusted = mContext.checkCallingOrSelfPermission(
    132                 android.Manifest.permission.CONFIGURE_WIFI_DISPLAY) ==
    133                 PackageManager.PERMISSION_GRANTED;
    134         final long token = Binder.clearCallingIdentity();
    135         try {
    136             synchronized (mLock) {
    137                 registerClientLocked(client, pid, packageName, resolvedUserId, trusted);
    138             }
    139         } finally {
    140             Binder.restoreCallingIdentity(token);
    141         }
    142     }
    143 
    144     // Binder call
    145     @Override
    146     public void unregisterClient(IMediaRouterClient client) {
    147         if (client == null) {
    148             throw new IllegalArgumentException("client must not be null");
    149         }
    150 
    151         final long token = Binder.clearCallingIdentity();
    152         try {
    153             synchronized (mLock) {
    154                 unregisterClientLocked(client, false);
    155             }
    156         } finally {
    157             Binder.restoreCallingIdentity(token);
    158         }
    159     }
    160 
    161     // Binder call
    162     @Override
    163     public MediaRouterClientState getState(IMediaRouterClient client) {
    164         if (client == null) {
    165             throw new IllegalArgumentException("client must not be null");
    166         }
    167 
    168         final long token = Binder.clearCallingIdentity();
    169         try {
    170             synchronized (mLock) {
    171                 return getStateLocked(client);
    172             }
    173         } finally {
    174             Binder.restoreCallingIdentity(token);
    175         }
    176     }
    177 
    178     // Binder call
    179     @Override
    180     public void setDiscoveryRequest(IMediaRouterClient client,
    181             int routeTypes, boolean activeScan) {
    182         if (client == null) {
    183             throw new IllegalArgumentException("client must not be null");
    184         }
    185 
    186         final long token = Binder.clearCallingIdentity();
    187         try {
    188             synchronized (mLock) {
    189                 setDiscoveryRequestLocked(client, routeTypes, activeScan);
    190             }
    191         } finally {
    192             Binder.restoreCallingIdentity(token);
    193         }
    194     }
    195 
    196     // Binder call
    197     // A null routeId means that the client wants to unselect its current route.
    198     // The explicit flag indicates whether the change was explicitly requested by the
    199     // user or the application which may cause changes to propagate out to the rest
    200     // of the system.  Should be false when the change is in response to a new globally
    201     // selected route or a default selection.
    202     @Override
    203     public void setSelectedRoute(IMediaRouterClient client, String routeId, boolean explicit) {
    204         if (client == null) {
    205             throw new IllegalArgumentException("client must not be null");
    206         }
    207 
    208         final long token = Binder.clearCallingIdentity();
    209         try {
    210             synchronized (mLock) {
    211                 setSelectedRouteLocked(client, routeId, explicit);
    212             }
    213         } finally {
    214             Binder.restoreCallingIdentity(token);
    215         }
    216     }
    217 
    218     // Binder call
    219     @Override
    220     public void requestSetVolume(IMediaRouterClient client, String routeId, int volume) {
    221         if (client == null) {
    222             throw new IllegalArgumentException("client must not be null");
    223         }
    224         if (routeId == null) {
    225             throw new IllegalArgumentException("routeId must not be null");
    226         }
    227 
    228         final long token = Binder.clearCallingIdentity();
    229         try {
    230             synchronized (mLock) {
    231                 requestSetVolumeLocked(client, routeId, volume);
    232             }
    233         } finally {
    234             Binder.restoreCallingIdentity(token);
    235         }
    236     }
    237 
    238     // Binder call
    239     @Override
    240     public void requestUpdateVolume(IMediaRouterClient client, String routeId, int direction) {
    241         if (client == null) {
    242             throw new IllegalArgumentException("client must not be null");
    243         }
    244         if (routeId == null) {
    245             throw new IllegalArgumentException("routeId must not be null");
    246         }
    247 
    248         final long token = Binder.clearCallingIdentity();
    249         try {
    250             synchronized (mLock) {
    251                 requestUpdateVolumeLocked(client, routeId, direction);
    252             }
    253         } finally {
    254             Binder.restoreCallingIdentity(token);
    255         }
    256     }
    257 
    258     // Binder call
    259     @Override
    260     public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) {
    261         if (mContext.checkCallingOrSelfPermission(Manifest.permission.DUMP)
    262                 != PackageManager.PERMISSION_GRANTED) {
    263             pw.println("Permission Denial: can't dump MediaRouterService from from pid="
    264                     + Binder.getCallingPid()
    265                     + ", uid=" + Binder.getCallingUid());
    266             return;
    267         }
    268 
    269         pw.println("MEDIA ROUTER SERVICE (dumpsys media_router)");
    270         pw.println();
    271         pw.println("Global state");
    272         pw.println("  mCurrentUserId=" + mCurrentUserId);
    273 
    274         synchronized (mLock) {
    275             final int count = mUserRecords.size();
    276             for (int i = 0; i < count; i++) {
    277                 UserRecord userRecord = mUserRecords.valueAt(i);
    278                 pw.println();
    279                 userRecord.dump(pw, "");
    280             }
    281         }
    282     }
    283 
    284     void switchUser() {
    285         synchronized (mLock) {
    286             int userId = ActivityManager.getCurrentUser();
    287             if (mCurrentUserId != userId) {
    288                 final int oldUserId = mCurrentUserId;
    289                 mCurrentUserId = userId; // do this first
    290 
    291                 UserRecord oldUser = mUserRecords.get(oldUserId);
    292                 if (oldUser != null) {
    293                     oldUser.mHandler.sendEmptyMessage(UserHandler.MSG_STOP);
    294                     disposeUserIfNeededLocked(oldUser); // since no longer current user
    295                 }
    296 
    297                 UserRecord newUser = mUserRecords.get(userId);
    298                 if (newUser != null) {
    299                     newUser.mHandler.sendEmptyMessage(UserHandler.MSG_START);
    300                 }
    301             }
    302         }
    303     }
    304 
    305     void clientDied(ClientRecord clientRecord) {
    306         synchronized (mLock) {
    307             unregisterClientLocked(clientRecord.mClient, true);
    308         }
    309     }
    310 
    311     private void registerClientLocked(IMediaRouterClient client,
    312             int pid, String packageName, int userId, boolean trusted) {
    313         final IBinder binder = client.asBinder();
    314         ClientRecord clientRecord = mAllClientRecords.get(binder);
    315         if (clientRecord == null) {
    316             boolean newUser = false;
    317             UserRecord userRecord = mUserRecords.get(userId);
    318             if (userRecord == null) {
    319                 userRecord = new UserRecord(userId);
    320                 newUser = true;
    321             }
    322             clientRecord = new ClientRecord(userRecord, client, pid, packageName, trusted);
    323             try {
    324                 binder.linkToDeath(clientRecord, 0);
    325             } catch (RemoteException ex) {
    326                 throw new RuntimeException("Media router client died prematurely.", ex);
    327             }
    328 
    329             if (newUser) {
    330                 mUserRecords.put(userId, userRecord);
    331                 initializeUserLocked(userRecord);
    332             }
    333 
    334             userRecord.mClientRecords.add(clientRecord);
    335             mAllClientRecords.put(binder, clientRecord);
    336             initializeClientLocked(clientRecord);
    337         }
    338     }
    339 
    340     private void unregisterClientLocked(IMediaRouterClient client, boolean died) {
    341         ClientRecord clientRecord = mAllClientRecords.remove(client.asBinder());
    342         if (clientRecord != null) {
    343             UserRecord userRecord = clientRecord.mUserRecord;
    344             userRecord.mClientRecords.remove(clientRecord);
    345             disposeClientLocked(clientRecord, died);
    346             disposeUserIfNeededLocked(userRecord); // since client removed from user
    347         }
    348     }
    349 
    350     private MediaRouterClientState getStateLocked(IMediaRouterClient client) {
    351         ClientRecord clientRecord = mAllClientRecords.get(client.asBinder());
    352         if (clientRecord != null) {
    353             return clientRecord.getState();
    354         }
    355         return null;
    356     }
    357 
    358     private void setDiscoveryRequestLocked(IMediaRouterClient client,
    359             int routeTypes, boolean activeScan) {
    360         final IBinder binder = client.asBinder();
    361         ClientRecord clientRecord = mAllClientRecords.get(binder);
    362         if (clientRecord != null) {
    363             // Only let the system discover remote display routes for now.
    364             if (!clientRecord.mTrusted) {
    365                 routeTypes &= ~MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY;
    366             }
    367 
    368             if (clientRecord.mRouteTypes != routeTypes
    369                     || clientRecord.mActiveScan != activeScan) {
    370                 if (DEBUG) {
    371                     Slog.d(TAG, clientRecord + ": Set discovery request, routeTypes=0x"
    372                             + Integer.toHexString(routeTypes) + ", activeScan=" + activeScan);
    373                 }
    374                 clientRecord.mRouteTypes = routeTypes;
    375                 clientRecord.mActiveScan = activeScan;
    376                 clientRecord.mUserRecord.mHandler.sendEmptyMessage(
    377                         UserHandler.MSG_UPDATE_DISCOVERY_REQUEST);
    378             }
    379         }
    380     }
    381 
    382     private void setSelectedRouteLocked(IMediaRouterClient client,
    383             String routeId, boolean explicit) {
    384         ClientRecord clientRecord = mAllClientRecords.get(client.asBinder());
    385         if (clientRecord != null) {
    386             final String oldRouteId = clientRecord.mSelectedRouteId;
    387             if (!Objects.equal(routeId, oldRouteId)) {
    388                 if (DEBUG) {
    389                     Slog.d(TAG, clientRecord + ": Set selected route, routeId=" + routeId
    390                             + ", oldRouteId=" + oldRouteId
    391                             + ", explicit=" + explicit);
    392                 }
    393 
    394                 clientRecord.mSelectedRouteId = routeId;
    395                 if (explicit) {
    396                     // Any app can disconnect from the globally selected route.
    397                     if (oldRouteId != null) {
    398                         clientRecord.mUserRecord.mHandler.obtainMessage(
    399                                 UserHandler.MSG_UNSELECT_ROUTE, oldRouteId).sendToTarget();
    400                     }
    401                     // Only let the system connect to new global routes for now.
    402                     // A similar check exists in the display manager for wifi display.
    403                     if (routeId != null && clientRecord.mTrusted) {
    404                         clientRecord.mUserRecord.mHandler.obtainMessage(
    405                                 UserHandler.MSG_SELECT_ROUTE, routeId).sendToTarget();
    406                     }
    407                 }
    408             }
    409         }
    410     }
    411 
    412     private void requestSetVolumeLocked(IMediaRouterClient client,
    413             String routeId, int volume) {
    414         final IBinder binder = client.asBinder();
    415         ClientRecord clientRecord = mAllClientRecords.get(binder);
    416         if (clientRecord != null) {
    417             clientRecord.mUserRecord.mHandler.obtainMessage(
    418                     UserHandler.MSG_REQUEST_SET_VOLUME, volume, 0, routeId).sendToTarget();
    419         }
    420     }
    421 
    422     private void requestUpdateVolumeLocked(IMediaRouterClient client,
    423             String routeId, int direction) {
    424         final IBinder binder = client.asBinder();
    425         ClientRecord clientRecord = mAllClientRecords.get(binder);
    426         if (clientRecord != null) {
    427             clientRecord.mUserRecord.mHandler.obtainMessage(
    428                     UserHandler.MSG_REQUEST_UPDATE_VOLUME, direction, 0, routeId).sendToTarget();
    429         }
    430     }
    431 
    432     private void initializeUserLocked(UserRecord userRecord) {
    433         if (DEBUG) {
    434             Slog.d(TAG, userRecord + ": Initialized");
    435         }
    436         if (userRecord.mUserId == mCurrentUserId) {
    437             userRecord.mHandler.sendEmptyMessage(UserHandler.MSG_START);
    438         }
    439     }
    440 
    441     private void disposeUserIfNeededLocked(UserRecord userRecord) {
    442         // If there are no records left and the user is no longer current then go ahead
    443         // and purge the user record and all of its associated state.  If the user is current
    444         // then leave it alone since we might be connected to a route or want to query
    445         // the same route information again soon.
    446         if (userRecord.mUserId != mCurrentUserId
    447                 && userRecord.mClientRecords.isEmpty()) {
    448             if (DEBUG) {
    449                 Slog.d(TAG, userRecord + ": Disposed");
    450             }
    451             mUserRecords.remove(userRecord.mUserId);
    452             // Note: User already stopped (by switchUser) so no need to send stop message here.
    453         }
    454     }
    455 
    456     private void initializeClientLocked(ClientRecord clientRecord) {
    457         if (DEBUG) {
    458             Slog.d(TAG, clientRecord + ": Registered");
    459         }
    460     }
    461 
    462     private void disposeClientLocked(ClientRecord clientRecord, boolean died) {
    463         if (DEBUG) {
    464             if (died) {
    465                 Slog.d(TAG, clientRecord + ": Died!");
    466             } else {
    467                 Slog.d(TAG, clientRecord + ": Unregistered");
    468             }
    469         }
    470         if (clientRecord.mRouteTypes != 0 || clientRecord.mActiveScan) {
    471             clientRecord.mUserRecord.mHandler.sendEmptyMessage(
    472                     UserHandler.MSG_UPDATE_DISCOVERY_REQUEST);
    473         }
    474         clientRecord.dispose();
    475     }
    476 
    477     private boolean validatePackageName(int uid, String packageName) {
    478         if (packageName != null) {
    479             String[] packageNames = mContext.getPackageManager().getPackagesForUid(uid);
    480             if (packageNames != null) {
    481                 for (String n : packageNames) {
    482                     if (n.equals(packageName)) {
    483                         return true;
    484                     }
    485                 }
    486             }
    487         }
    488         return false;
    489     }
    490 
    491     /**
    492      * Information about a particular client of the media router.
    493      * The contents of this object is guarded by mLock.
    494      */
    495     final class ClientRecord implements DeathRecipient {
    496         public final UserRecord mUserRecord;
    497         public final IMediaRouterClient mClient;
    498         public final int mPid;
    499         public final String mPackageName;
    500         public final boolean mTrusted;
    501 
    502         public int mRouteTypes;
    503         public boolean mActiveScan;
    504         public String mSelectedRouteId;
    505 
    506         public ClientRecord(UserRecord userRecord, IMediaRouterClient client,
    507                 int pid, String packageName, boolean trusted) {
    508             mUserRecord = userRecord;
    509             mClient = client;
    510             mPid = pid;
    511             mPackageName = packageName;
    512             mTrusted = trusted;
    513         }
    514 
    515         public void dispose() {
    516             mClient.asBinder().unlinkToDeath(this, 0);
    517         }
    518 
    519         @Override
    520         public void binderDied() {
    521             clientDied(this);
    522         }
    523 
    524         MediaRouterClientState getState() {
    525             return mTrusted ? mUserRecord.mTrustedState : mUserRecord.mUntrustedState;
    526         }
    527 
    528         public void dump(PrintWriter pw, String prefix) {
    529             pw.println(prefix + this);
    530 
    531             final String indent = prefix + "  ";
    532             pw.println(indent + "mTrusted=" + mTrusted);
    533             pw.println(indent + "mRouteTypes=0x" + Integer.toHexString(mRouteTypes));
    534             pw.println(indent + "mActiveScan=" + mActiveScan);
    535             pw.println(indent + "mSelectedRouteId=" + mSelectedRouteId);
    536         }
    537 
    538         @Override
    539         public String toString() {
    540             return "Client " + mPackageName + " (pid " + mPid + ")";
    541         }
    542     }
    543 
    544     /**
    545      * Information about a particular user.
    546      * The contents of this object is guarded by mLock.
    547      */
    548     final class UserRecord {
    549         public final int mUserId;
    550         public final ArrayList<ClientRecord> mClientRecords = new ArrayList<ClientRecord>();
    551         public final UserHandler mHandler;
    552         public MediaRouterClientState mTrustedState;
    553         public MediaRouterClientState mUntrustedState;
    554 
    555         public UserRecord(int userId) {
    556             mUserId = userId;
    557             mHandler = new UserHandler(MediaRouterService.this, this);
    558         }
    559 
    560         public void dump(final PrintWriter pw, String prefix) {
    561             pw.println(prefix + this);
    562 
    563             final String indent = prefix + "  ";
    564             final int clientCount = mClientRecords.size();
    565             if (clientCount != 0) {
    566                 for (int i = 0; i < clientCount; i++) {
    567                     mClientRecords.get(i).dump(pw, indent);
    568                 }
    569             } else {
    570                 pw.println(indent + "<no clients>");
    571             }
    572 
    573             pw.println(indent + "State");
    574             pw.println(indent + "mTrustedState=" + mTrustedState);
    575             pw.println(indent + "mUntrustedState=" + mUntrustedState);
    576 
    577             if (!mHandler.runWithScissors(new Runnable() {
    578                 @Override
    579                 public void run() {
    580                     mHandler.dump(pw, indent);
    581                 }
    582             }, 1000)) {
    583                 pw.println(indent + "<could not dump handler state>");
    584             }
    585          }
    586 
    587         @Override
    588         public String toString() {
    589             return "User " + mUserId;
    590         }
    591     }
    592 
    593     /**
    594      * Media router handler
    595      * <p>
    596      * Since remote display providers are designed to be single-threaded by nature,
    597      * this class encapsulates all of the associated functionality and exports state
    598      * to the service as it evolves.
    599      * </p><p>
    600      * One important task of this class is to keep track of the current globally selected
    601      * route id for certain routes that have global effects, such as remote displays.
    602      * Global route selections override local selections made within apps.  The change
    603      * is propagated to all apps so that they are all in sync.  Synchronization works
    604      * both ways.  Whenever the globally selected route is explicitly unselected by any
    605      * app, then it becomes unselected globally and all apps are informed.
    606      * </p><p>
    607      * This class is currently hardcoded to work with remote display providers but
    608      * it is intended to be eventually extended to support more general route providers
    609      * similar to the support library media router.
    610      * </p>
    611      */
    612     static final class UserHandler extends Handler
    613             implements RemoteDisplayProviderWatcher.Callback,
    614             RemoteDisplayProviderProxy.Callback {
    615         public static final int MSG_START = 1;
    616         public static final int MSG_STOP = 2;
    617         public static final int MSG_UPDATE_DISCOVERY_REQUEST = 3;
    618         public static final int MSG_SELECT_ROUTE = 4;
    619         public static final int MSG_UNSELECT_ROUTE = 5;
    620         public static final int MSG_REQUEST_SET_VOLUME = 6;
    621         public static final int MSG_REQUEST_UPDATE_VOLUME = 7;
    622         private static final int MSG_UPDATE_CLIENT_STATE = 8;
    623         private static final int MSG_CONNECTION_TIMED_OUT = 9;
    624 
    625         private static final int TIMEOUT_REASON_NOT_AVAILABLE = 1;
    626         private static final int TIMEOUT_REASON_CONNECTION_LOST = 2;
    627         private static final int TIMEOUT_REASON_WAITING_FOR_CONNECTING = 3;
    628         private static final int TIMEOUT_REASON_WAITING_FOR_CONNECTED = 4;
    629 
    630         // The relative order of these constants is important and expresses progress
    631         // through the process of connecting to a route.
    632         private static final int PHASE_NOT_AVAILABLE = -1;
    633         private static final int PHASE_NOT_CONNECTED = 0;
    634         private static final int PHASE_CONNECTING = 1;
    635         private static final int PHASE_CONNECTED = 2;
    636 
    637         private final MediaRouterService mService;
    638         private final UserRecord mUserRecord;
    639         private final RemoteDisplayProviderWatcher mWatcher;
    640         private final ArrayList<ProviderRecord> mProviderRecords =
    641                 new ArrayList<ProviderRecord>();
    642         private final ArrayList<IMediaRouterClient> mTempClients =
    643                 new ArrayList<IMediaRouterClient>();
    644 
    645         private boolean mRunning;
    646         private int mDiscoveryMode = RemoteDisplayState.DISCOVERY_MODE_NONE;
    647         private RouteRecord mGloballySelectedRouteRecord;
    648         private int mConnectionPhase = PHASE_NOT_AVAILABLE;
    649         private int mConnectionTimeoutReason;
    650         private long mConnectionTimeoutStartTime;
    651         private boolean mClientStateUpdateScheduled;
    652 
    653         public UserHandler(MediaRouterService service, UserRecord userRecord) {
    654             super(Looper.getMainLooper(), null, true);
    655             mService = service;
    656             mUserRecord = userRecord;
    657             mWatcher = new RemoteDisplayProviderWatcher(service.mContext, this,
    658                     this, mUserRecord.mUserId);
    659         }
    660 
    661         @Override
    662         public void handleMessage(Message msg) {
    663             switch (msg.what) {
    664                 case MSG_START: {
    665                     start();
    666                     break;
    667                 }
    668                 case MSG_STOP: {
    669                     stop();
    670                     break;
    671                 }
    672                 case MSG_UPDATE_DISCOVERY_REQUEST: {
    673                     updateDiscoveryRequest();
    674                     break;
    675                 }
    676                 case MSG_SELECT_ROUTE: {
    677                     selectRoute((String)msg.obj);
    678                     break;
    679                 }
    680                 case MSG_UNSELECT_ROUTE: {
    681                     unselectRoute((String)msg.obj);
    682                     break;
    683                 }
    684                 case MSG_REQUEST_SET_VOLUME: {
    685                     requestSetVolume((String)msg.obj, msg.arg1);
    686                     break;
    687                 }
    688                 case MSG_REQUEST_UPDATE_VOLUME: {
    689                     requestUpdateVolume((String)msg.obj, msg.arg1);
    690                     break;
    691                 }
    692                 case MSG_UPDATE_CLIENT_STATE: {
    693                     updateClientState();
    694                     break;
    695                 }
    696                 case MSG_CONNECTION_TIMED_OUT: {
    697                     connectionTimedOut();
    698                     break;
    699                 }
    700             }
    701         }
    702 
    703         public void dump(PrintWriter pw, String prefix) {
    704             pw.println(prefix + "Handler");
    705 
    706             final String indent = prefix + "  ";
    707             pw.println(indent + "mRunning=" + mRunning);
    708             pw.println(indent + "mDiscoveryMode=" + mDiscoveryMode);
    709             pw.println(indent + "mGloballySelectedRouteRecord=" + mGloballySelectedRouteRecord);
    710             pw.println(indent + "mConnectionPhase=" + mConnectionPhase);
    711             pw.println(indent + "mConnectionTimeoutReason=" + mConnectionTimeoutReason);
    712             pw.println(indent + "mConnectionTimeoutStartTime=" + (mConnectionTimeoutReason != 0 ?
    713                     TimeUtils.formatUptime(mConnectionTimeoutStartTime) : "<n/a>"));
    714 
    715             mWatcher.dump(pw, prefix);
    716 
    717             final int providerCount = mProviderRecords.size();
    718             if (providerCount != 0) {
    719                 for (int i = 0; i < providerCount; i++) {
    720                     mProviderRecords.get(i).dump(pw, prefix);
    721                 }
    722             } else {
    723                 pw.println(indent + "<no providers>");
    724             }
    725         }
    726 
    727         private void start() {
    728             if (!mRunning) {
    729                 mRunning = true;
    730                 mWatcher.start(); // also starts all providers
    731             }
    732         }
    733 
    734         private void stop() {
    735             if (mRunning) {
    736                 mRunning = false;
    737                 unselectGloballySelectedRoute();
    738                 mWatcher.stop(); // also stops all providers
    739             }
    740         }
    741 
    742         private void updateDiscoveryRequest() {
    743             int routeTypes = 0;
    744             boolean activeScan = false;
    745             synchronized (mService.mLock) {
    746                 final int count = mUserRecord.mClientRecords.size();
    747                 for (int i = 0; i < count; i++) {
    748                     ClientRecord clientRecord = mUserRecord.mClientRecords.get(i);
    749                     routeTypes |= clientRecord.mRouteTypes;
    750                     activeScan |= clientRecord.mActiveScan;
    751                 }
    752             }
    753 
    754             final int newDiscoveryMode;
    755             if ((routeTypes & MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY) != 0) {
    756                 if (activeScan) {
    757                     newDiscoveryMode = RemoteDisplayState.DISCOVERY_MODE_ACTIVE;
    758                 } else {
    759                     newDiscoveryMode = RemoteDisplayState.DISCOVERY_MODE_PASSIVE;
    760                 }
    761             } else {
    762                 newDiscoveryMode = RemoteDisplayState.DISCOVERY_MODE_NONE;
    763             }
    764 
    765             if (mDiscoveryMode != newDiscoveryMode) {
    766                 mDiscoveryMode = newDiscoveryMode;
    767                 final int count = mProviderRecords.size();
    768                 for (int i = 0; i < count; i++) {
    769                     mProviderRecords.get(i).getProvider().setDiscoveryMode(mDiscoveryMode);
    770                 }
    771             }
    772         }
    773 
    774         private void selectRoute(String routeId) {
    775             if (routeId != null
    776                     && (mGloballySelectedRouteRecord == null
    777                             || !routeId.equals(mGloballySelectedRouteRecord.getUniqueId()))) {
    778                 RouteRecord routeRecord = findRouteRecord(routeId);
    779                 if (routeRecord != null) {
    780                     unselectGloballySelectedRoute();
    781 
    782                     Slog.i(TAG, "Selected global route:" + routeRecord);
    783                     mGloballySelectedRouteRecord = routeRecord;
    784                     checkGloballySelectedRouteState();
    785                     routeRecord.getProvider().setSelectedDisplay(routeRecord.getDescriptorId());
    786 
    787                     scheduleUpdateClientState();
    788                 }
    789             }
    790         }
    791 
    792         private void unselectRoute(String routeId) {
    793             if (routeId != null
    794                     && mGloballySelectedRouteRecord != null
    795                     && routeId.equals(mGloballySelectedRouteRecord.getUniqueId())) {
    796                 unselectGloballySelectedRoute();
    797             }
    798         }
    799 
    800         private void unselectGloballySelectedRoute() {
    801             if (mGloballySelectedRouteRecord != null) {
    802                 Slog.i(TAG, "Unselected global route:" + mGloballySelectedRouteRecord);
    803                 mGloballySelectedRouteRecord.getProvider().setSelectedDisplay(null);
    804                 mGloballySelectedRouteRecord = null;
    805                 checkGloballySelectedRouteState();
    806 
    807                 scheduleUpdateClientState();
    808             }
    809         }
    810 
    811         private void requestSetVolume(String routeId, int volume) {
    812             if (mGloballySelectedRouteRecord != null
    813                     && routeId.equals(mGloballySelectedRouteRecord.getUniqueId())) {
    814                 mGloballySelectedRouteRecord.getProvider().setDisplayVolume(volume);
    815             }
    816         }
    817 
    818         private void requestUpdateVolume(String routeId, int direction) {
    819             if (mGloballySelectedRouteRecord != null
    820                     && routeId.equals(mGloballySelectedRouteRecord.getUniqueId())) {
    821                 mGloballySelectedRouteRecord.getProvider().adjustDisplayVolume(direction);
    822             }
    823         }
    824 
    825         @Override
    826         public void addProvider(RemoteDisplayProviderProxy provider) {
    827             provider.setCallback(this);
    828             provider.setDiscoveryMode(mDiscoveryMode);
    829             provider.setSelectedDisplay(null); // just to be safe
    830 
    831             ProviderRecord providerRecord = new ProviderRecord(provider);
    832             mProviderRecords.add(providerRecord);
    833             providerRecord.updateDescriptor(provider.getDisplayState());
    834 
    835             scheduleUpdateClientState();
    836         }
    837 
    838         @Override
    839         public void removeProvider(RemoteDisplayProviderProxy provider) {
    840             int index = findProviderRecord(provider);
    841             if (index >= 0) {
    842                 ProviderRecord providerRecord = mProviderRecords.remove(index);
    843                 providerRecord.updateDescriptor(null); // mark routes invalid
    844                 provider.setCallback(null);
    845                 provider.setDiscoveryMode(RemoteDisplayState.DISCOVERY_MODE_NONE);
    846 
    847                 checkGloballySelectedRouteState();
    848                 scheduleUpdateClientState();
    849             }
    850         }
    851 
    852         @Override
    853         public void onDisplayStateChanged(RemoteDisplayProviderProxy provider,
    854                 RemoteDisplayState state) {
    855             updateProvider(provider, state);
    856         }
    857 
    858         private void updateProvider(RemoteDisplayProviderProxy provider,
    859                 RemoteDisplayState state) {
    860             int index = findProviderRecord(provider);
    861             if (index >= 0) {
    862                 ProviderRecord providerRecord = mProviderRecords.get(index);
    863                 if (providerRecord.updateDescriptor(state)) {
    864                     checkGloballySelectedRouteState();
    865                     scheduleUpdateClientState();
    866                 }
    867             }
    868         }
    869 
    870         /**
    871          * This function is called whenever the state of the globally selected route
    872          * may have changed.  It checks the state and updates timeouts or unselects
    873          * the route as appropriate.
    874          */
    875         private void checkGloballySelectedRouteState() {
    876             // Unschedule timeouts when the route is unselected.
    877             if (mGloballySelectedRouteRecord == null) {
    878                 mConnectionPhase = PHASE_NOT_AVAILABLE;
    879                 updateConnectionTimeout(0);
    880                 return;
    881             }
    882 
    883             // Ensure that the route is still present and enabled.
    884             if (!mGloballySelectedRouteRecord.isValid()
    885                     || !mGloballySelectedRouteRecord.isEnabled()) {
    886                 updateConnectionTimeout(TIMEOUT_REASON_NOT_AVAILABLE);
    887                 return;
    888             }
    889 
    890             // Make sure we haven't lost our connection.
    891             final int oldPhase = mConnectionPhase;
    892             mConnectionPhase = getConnectionPhase(mGloballySelectedRouteRecord.getStatus());
    893             if (oldPhase >= PHASE_CONNECTING && mConnectionPhase < PHASE_CONNECTING) {
    894                 updateConnectionTimeout(TIMEOUT_REASON_CONNECTION_LOST);
    895                 return;
    896             }
    897 
    898             // Check the route status.
    899             switch (mConnectionPhase) {
    900                 case PHASE_CONNECTED:
    901                     if (oldPhase != PHASE_CONNECTED) {
    902                         Slog.i(TAG, "Connected to global route: "
    903                                 + mGloballySelectedRouteRecord);
    904                     }
    905                     updateConnectionTimeout(0);
    906                     break;
    907                 case PHASE_CONNECTING:
    908                     if (oldPhase != PHASE_CONNECTING) {
    909                         Slog.i(TAG, "Connecting to global route: "
    910                                 + mGloballySelectedRouteRecord);
    911                     }
    912                     updateConnectionTimeout(TIMEOUT_REASON_WAITING_FOR_CONNECTED);
    913                     break;
    914                 case PHASE_NOT_CONNECTED:
    915                     updateConnectionTimeout(TIMEOUT_REASON_WAITING_FOR_CONNECTING);
    916                     break;
    917                 case PHASE_NOT_AVAILABLE:
    918                 default:
    919                     updateConnectionTimeout(TIMEOUT_REASON_NOT_AVAILABLE);
    920                     break;
    921             }
    922         }
    923 
    924         private void updateConnectionTimeout(int reason) {
    925             if (reason != mConnectionTimeoutReason) {
    926                 if (mConnectionTimeoutReason != 0) {
    927                     removeMessages(MSG_CONNECTION_TIMED_OUT);
    928                 }
    929                 mConnectionTimeoutReason = reason;
    930                 mConnectionTimeoutStartTime = SystemClock.uptimeMillis();
    931                 switch (reason) {
    932                     case TIMEOUT_REASON_NOT_AVAILABLE:
    933                     case TIMEOUT_REASON_CONNECTION_LOST:
    934                         // Route became unavailable or connection lost.
    935                         // Unselect it immediately.
    936                         sendEmptyMessage(MSG_CONNECTION_TIMED_OUT);
    937                         break;
    938                     case TIMEOUT_REASON_WAITING_FOR_CONNECTING:
    939                         // Waiting for route to start connecting.
    940                         sendEmptyMessageDelayed(MSG_CONNECTION_TIMED_OUT, CONNECTING_TIMEOUT);
    941                         break;
    942                     case TIMEOUT_REASON_WAITING_FOR_CONNECTED:
    943                         // Waiting for route to complete connection.
    944                         sendEmptyMessageDelayed(MSG_CONNECTION_TIMED_OUT, CONNECTED_TIMEOUT);
    945                         break;
    946                 }
    947             }
    948         }
    949 
    950         private void connectionTimedOut() {
    951             if (mConnectionTimeoutReason == 0 || mGloballySelectedRouteRecord == null) {
    952                 // Shouldn't get here.  There must be a bug somewhere.
    953                 Log.wtf(TAG, "Handled connection timeout for no reason.");
    954                 return;
    955             }
    956 
    957             switch (mConnectionTimeoutReason) {
    958                 case TIMEOUT_REASON_NOT_AVAILABLE:
    959                     Slog.i(TAG, "Global route no longer available: "
    960                             + mGloballySelectedRouteRecord);
    961                     break;
    962                 case TIMEOUT_REASON_CONNECTION_LOST:
    963                     Slog.i(TAG, "Global route connection lost: "
    964                             + mGloballySelectedRouteRecord);
    965                     break;
    966                 case TIMEOUT_REASON_WAITING_FOR_CONNECTING:
    967                     Slog.i(TAG, "Global route timed out while waiting for "
    968                             + "connection attempt to begin after "
    969                             + (SystemClock.uptimeMillis() - mConnectionTimeoutStartTime)
    970                             + " ms: " + mGloballySelectedRouteRecord);
    971                     break;
    972                 case TIMEOUT_REASON_WAITING_FOR_CONNECTED:
    973                     Slog.i(TAG, "Global route timed out while connecting after "
    974                             + (SystemClock.uptimeMillis() - mConnectionTimeoutStartTime)
    975                             + " ms: " + mGloballySelectedRouteRecord);
    976                     break;
    977             }
    978             mConnectionTimeoutReason = 0;
    979 
    980             unselectGloballySelectedRoute();
    981         }
    982 
    983         private void scheduleUpdateClientState() {
    984             if (!mClientStateUpdateScheduled) {
    985                 mClientStateUpdateScheduled = true;
    986                 sendEmptyMessage(MSG_UPDATE_CLIENT_STATE);
    987             }
    988         }
    989 
    990         private void updateClientState() {
    991             mClientStateUpdateScheduled = false;
    992 
    993             final String globallySelectedRouteId = mGloballySelectedRouteRecord != null ?
    994                     mGloballySelectedRouteRecord.getUniqueId() : null;
    995 
    996             // Build a new client state for trusted clients.
    997             MediaRouterClientState trustedState = new MediaRouterClientState();
    998             trustedState.globallySelectedRouteId = globallySelectedRouteId;
    999             final int providerCount = mProviderRecords.size();
   1000             for (int i = 0; i < providerCount; i++) {
   1001                 mProviderRecords.get(i).appendClientState(trustedState);
   1002             }
   1003 
   1004             // Build a new client state for untrusted clients that can only see
   1005             // the currently selected route.
   1006             MediaRouterClientState untrustedState = new MediaRouterClientState();
   1007             untrustedState.globallySelectedRouteId = globallySelectedRouteId;
   1008             if (globallySelectedRouteId != null) {
   1009                 untrustedState.routes.add(trustedState.getRoute(globallySelectedRouteId));
   1010             }
   1011 
   1012             try {
   1013                 synchronized (mService.mLock) {
   1014                     // Update the UserRecord.
   1015                     mUserRecord.mTrustedState = trustedState;
   1016                     mUserRecord.mUntrustedState = untrustedState;
   1017 
   1018                     // Collect all clients.
   1019                     final int count = mUserRecord.mClientRecords.size();
   1020                     for (int i = 0; i < count; i++) {
   1021                         mTempClients.add(mUserRecord.mClientRecords.get(i).mClient);
   1022                     }
   1023                 }
   1024 
   1025                 // Notify all clients (outside of the lock).
   1026                 final int count = mTempClients.size();
   1027                 for (int i = 0; i < count; i++) {
   1028                     try {
   1029                         mTempClients.get(i).onStateChanged();
   1030                     } catch (RemoteException ex) {
   1031                         // ignore errors, client probably died
   1032                     }
   1033                 }
   1034             } finally {
   1035                 // Clear the list in preparation for the next time.
   1036                 mTempClients.clear();
   1037             }
   1038         }
   1039 
   1040         private int findProviderRecord(RemoteDisplayProviderProxy provider) {
   1041             final int count = mProviderRecords.size();
   1042             for (int i = 0; i < count; i++) {
   1043                 ProviderRecord record = mProviderRecords.get(i);
   1044                 if (record.getProvider() == provider) {
   1045                     return i;
   1046                 }
   1047             }
   1048             return -1;
   1049         }
   1050 
   1051         private RouteRecord findRouteRecord(String uniqueId) {
   1052             final int count = mProviderRecords.size();
   1053             for (int i = 0; i < count; i++) {
   1054                 RouteRecord record = mProviderRecords.get(i).findRouteByUniqueId(uniqueId);
   1055                 if (record != null) {
   1056                     return record;
   1057                 }
   1058             }
   1059             return null;
   1060         }
   1061 
   1062         private static int getConnectionPhase(int status) {
   1063             switch (status) {
   1064                 case MediaRouter.RouteInfo.STATUS_NONE:
   1065                 case MediaRouter.RouteInfo.STATUS_CONNECTED:
   1066                     return PHASE_CONNECTED;
   1067                 case MediaRouter.RouteInfo.STATUS_CONNECTING:
   1068                     return PHASE_CONNECTING;
   1069                 case MediaRouter.RouteInfo.STATUS_SCANNING:
   1070                 case MediaRouter.RouteInfo.STATUS_AVAILABLE:
   1071                     return PHASE_NOT_CONNECTED;
   1072                 case MediaRouter.RouteInfo.STATUS_NOT_AVAILABLE:
   1073                 case MediaRouter.RouteInfo.STATUS_IN_USE:
   1074                 default:
   1075                     return PHASE_NOT_AVAILABLE;
   1076             }
   1077         }
   1078 
   1079         static final class ProviderRecord {
   1080             private final RemoteDisplayProviderProxy mProvider;
   1081             private final String mUniquePrefix;
   1082             private final ArrayList<RouteRecord> mRoutes = new ArrayList<RouteRecord>();
   1083             private RemoteDisplayState mDescriptor;
   1084 
   1085             public ProviderRecord(RemoteDisplayProviderProxy provider) {
   1086                 mProvider = provider;
   1087                 mUniquePrefix = provider.getFlattenedComponentName() + ":";
   1088             }
   1089 
   1090             public RemoteDisplayProviderProxy getProvider() {
   1091                 return mProvider;
   1092             }
   1093 
   1094             public String getUniquePrefix() {
   1095                 return mUniquePrefix;
   1096             }
   1097 
   1098             public boolean updateDescriptor(RemoteDisplayState descriptor) {
   1099                 boolean changed = false;
   1100                 if (mDescriptor != descriptor) {
   1101                     mDescriptor = descriptor;
   1102 
   1103                     // Update all existing routes and reorder them to match
   1104                     // the order of their descriptors.
   1105                     int targetIndex = 0;
   1106                     if (descriptor != null) {
   1107                         if (descriptor.isValid()) {
   1108                             final List<RemoteDisplayInfo> routeDescriptors = descriptor.displays;
   1109                             final int routeCount = routeDescriptors.size();
   1110                             for (int i = 0; i < routeCount; i++) {
   1111                                 final RemoteDisplayInfo routeDescriptor =
   1112                                         routeDescriptors.get(i);
   1113                                 final String descriptorId = routeDescriptor.id;
   1114                                 final int sourceIndex = findRouteByDescriptorId(descriptorId);
   1115                                 if (sourceIndex < 0) {
   1116                                     // Add the route to the provider.
   1117                                     String uniqueId = assignRouteUniqueId(descriptorId);
   1118                                     RouteRecord route =
   1119                                             new RouteRecord(this, descriptorId, uniqueId);
   1120                                     mRoutes.add(targetIndex++, route);
   1121                                     route.updateDescriptor(routeDescriptor);
   1122                                     changed = true;
   1123                                 } else if (sourceIndex < targetIndex) {
   1124                                     // Ignore route with duplicate id.
   1125                                     Slog.w(TAG, "Ignoring route descriptor with duplicate id: "
   1126                                             + routeDescriptor);
   1127                                 } else {
   1128                                     // Reorder existing route within the list.
   1129                                     RouteRecord route = mRoutes.get(sourceIndex);
   1130                                     Collections.swap(mRoutes, sourceIndex, targetIndex++);
   1131                                     changed |= route.updateDescriptor(routeDescriptor);
   1132                                 }
   1133                             }
   1134                         } else {
   1135                             Slog.w(TAG, "Ignoring invalid descriptor from media route provider: "
   1136                                     + mProvider.getFlattenedComponentName());
   1137                         }
   1138                     }
   1139 
   1140                     // Dispose all remaining routes that do not have matching descriptors.
   1141                     for (int i = mRoutes.size() - 1; i >= targetIndex; i--) {
   1142                         RouteRecord route = mRoutes.remove(i);
   1143                         route.updateDescriptor(null); // mark route invalid
   1144                         changed = true;
   1145                     }
   1146                 }
   1147                 return changed;
   1148             }
   1149 
   1150             public void appendClientState(MediaRouterClientState state) {
   1151                 final int routeCount = mRoutes.size();
   1152                 for (int i = 0; i < routeCount; i++) {
   1153                     state.routes.add(mRoutes.get(i).getInfo());
   1154                 }
   1155             }
   1156 
   1157             public RouteRecord findRouteByUniqueId(String uniqueId) {
   1158                 final int routeCount = mRoutes.size();
   1159                 for (int i = 0; i < routeCount; i++) {
   1160                     RouteRecord route = mRoutes.get(i);
   1161                     if (route.getUniqueId().equals(uniqueId)) {
   1162                         return route;
   1163                     }
   1164                 }
   1165                 return null;
   1166             }
   1167 
   1168             private int findRouteByDescriptorId(String descriptorId) {
   1169                 final int routeCount = mRoutes.size();
   1170                 for (int i = 0; i < routeCount; i++) {
   1171                     RouteRecord route = mRoutes.get(i);
   1172                     if (route.getDescriptorId().equals(descriptorId)) {
   1173                         return i;
   1174                     }
   1175                 }
   1176                 return -1;
   1177             }
   1178 
   1179             public void dump(PrintWriter pw, String prefix) {
   1180                 pw.println(prefix + this);
   1181 
   1182                 final String indent = prefix + "  ";
   1183                 mProvider.dump(pw, indent);
   1184 
   1185                 final int routeCount = mRoutes.size();
   1186                 if (routeCount != 0) {
   1187                     for (int i = 0; i < routeCount; i++) {
   1188                         mRoutes.get(i).dump(pw, indent);
   1189                     }
   1190                 } else {
   1191                     pw.println(indent + "<no routes>");
   1192                 }
   1193             }
   1194 
   1195             @Override
   1196             public String toString() {
   1197                 return "Provider " + mProvider.getFlattenedComponentName();
   1198             }
   1199 
   1200             private String assignRouteUniqueId(String descriptorId) {
   1201                 return mUniquePrefix + descriptorId;
   1202             }
   1203         }
   1204 
   1205         static final class RouteRecord {
   1206             private final ProviderRecord mProviderRecord;
   1207             private final String mDescriptorId;
   1208             private final MediaRouterClientState.RouteInfo mMutableInfo;
   1209             private MediaRouterClientState.RouteInfo mImmutableInfo;
   1210             private RemoteDisplayInfo mDescriptor;
   1211 
   1212             public RouteRecord(ProviderRecord providerRecord,
   1213                     String descriptorId, String uniqueId) {
   1214                 mProviderRecord = providerRecord;
   1215                 mDescriptorId = descriptorId;
   1216                 mMutableInfo = new MediaRouterClientState.RouteInfo(uniqueId);
   1217             }
   1218 
   1219             public RemoteDisplayProviderProxy getProvider() {
   1220                 return mProviderRecord.getProvider();
   1221             }
   1222 
   1223             public ProviderRecord getProviderRecord() {
   1224                 return mProviderRecord;
   1225             }
   1226 
   1227             public String getDescriptorId() {
   1228                 return mDescriptorId;
   1229             }
   1230 
   1231             public String getUniqueId() {
   1232                 return mMutableInfo.id;
   1233             }
   1234 
   1235             public MediaRouterClientState.RouteInfo getInfo() {
   1236                 if (mImmutableInfo == null) {
   1237                     mImmutableInfo = new MediaRouterClientState.RouteInfo(mMutableInfo);
   1238                 }
   1239                 return mImmutableInfo;
   1240             }
   1241 
   1242             public boolean isValid() {
   1243                 return mDescriptor != null;
   1244             }
   1245 
   1246             public boolean isEnabled() {
   1247                 return mMutableInfo.enabled;
   1248             }
   1249 
   1250             public int getStatus() {
   1251                 return mMutableInfo.statusCode;
   1252             }
   1253 
   1254             public boolean updateDescriptor(RemoteDisplayInfo descriptor) {
   1255                 boolean changed = false;
   1256                 if (mDescriptor != descriptor) {
   1257                     mDescriptor = descriptor;
   1258                     if (descriptor != null) {
   1259                         final String name = computeName(descriptor);
   1260                         if (!Objects.equal(mMutableInfo.name, name)) {
   1261                             mMutableInfo.name = name;
   1262                             changed = true;
   1263                         }
   1264                         final String description = computeDescription(descriptor);
   1265                         if (!Objects.equal(mMutableInfo.description, description)) {
   1266                             mMutableInfo.description = description;
   1267                             changed = true;
   1268                         }
   1269                         final int supportedTypes = computeSupportedTypes(descriptor);
   1270                         if (mMutableInfo.supportedTypes != supportedTypes) {
   1271                             mMutableInfo.supportedTypes = supportedTypes;
   1272                             changed = true;
   1273                         }
   1274                         final boolean enabled = computeEnabled(descriptor);
   1275                         if (mMutableInfo.enabled != enabled) {
   1276                             mMutableInfo.enabled = enabled;
   1277                             changed = true;
   1278                         }
   1279                         final int statusCode = computeStatusCode(descriptor);
   1280                         if (mMutableInfo.statusCode != statusCode) {
   1281                             mMutableInfo.statusCode = statusCode;
   1282                             changed = true;
   1283                         }
   1284                         final int playbackType = computePlaybackType(descriptor);
   1285                         if (mMutableInfo.playbackType != playbackType) {
   1286                             mMutableInfo.playbackType = playbackType;
   1287                             changed = true;
   1288                         }
   1289                         final int playbackStream = computePlaybackStream(descriptor);
   1290                         if (mMutableInfo.playbackStream != playbackStream) {
   1291                             mMutableInfo.playbackStream = playbackStream;
   1292                             changed = true;
   1293                         }
   1294                         final int volume = computeVolume(descriptor);
   1295                         if (mMutableInfo.volume != volume) {
   1296                             mMutableInfo.volume = volume;
   1297                             changed = true;
   1298                         }
   1299                         final int volumeMax = computeVolumeMax(descriptor);
   1300                         if (mMutableInfo.volumeMax != volumeMax) {
   1301                             mMutableInfo.volumeMax = volumeMax;
   1302                             changed = true;
   1303                         }
   1304                         final int volumeHandling = computeVolumeHandling(descriptor);
   1305                         if (mMutableInfo.volumeHandling != volumeHandling) {
   1306                             mMutableInfo.volumeHandling = volumeHandling;
   1307                             changed = true;
   1308                         }
   1309                         final int presentationDisplayId = computePresentationDisplayId(descriptor);
   1310                         if (mMutableInfo.presentationDisplayId != presentationDisplayId) {
   1311                             mMutableInfo.presentationDisplayId = presentationDisplayId;
   1312                             changed = true;
   1313                         }
   1314                     }
   1315                 }
   1316                 if (changed) {
   1317                     mImmutableInfo = null;
   1318                 }
   1319                 return changed;
   1320             }
   1321 
   1322             public void dump(PrintWriter pw, String prefix) {
   1323                 pw.println(prefix + this);
   1324 
   1325                 final String indent = prefix + "  ";
   1326                 pw.println(indent + "mMutableInfo=" + mMutableInfo);
   1327                 pw.println(indent + "mDescriptorId=" + mDescriptorId);
   1328                 pw.println(indent + "mDescriptor=" + mDescriptor);
   1329             }
   1330 
   1331             @Override
   1332             public String toString() {
   1333                 return "Route " + mMutableInfo.name + " (" + mMutableInfo.id + ")";
   1334             }
   1335 
   1336             private static String computeName(RemoteDisplayInfo descriptor) {
   1337                 // Note that isValid() already ensures the name is non-empty.
   1338                 return descriptor.name;
   1339             }
   1340 
   1341             private static String computeDescription(RemoteDisplayInfo descriptor) {
   1342                 final String description = descriptor.description;
   1343                 return TextUtils.isEmpty(description) ? null : description;
   1344             }
   1345 
   1346             private static int computeSupportedTypes(RemoteDisplayInfo descriptor) {
   1347                 return MediaRouter.ROUTE_TYPE_LIVE_AUDIO
   1348                         | MediaRouter.ROUTE_TYPE_LIVE_VIDEO
   1349                         | MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY;
   1350             }
   1351 
   1352             private static boolean computeEnabled(RemoteDisplayInfo descriptor) {
   1353                 switch (descriptor.status) {
   1354                     case RemoteDisplayInfo.STATUS_CONNECTED:
   1355                     case RemoteDisplayInfo.STATUS_CONNECTING:
   1356                     case RemoteDisplayInfo.STATUS_AVAILABLE:
   1357                         return true;
   1358                     default:
   1359                         return false;
   1360                 }
   1361             }
   1362 
   1363             private static int computeStatusCode(RemoteDisplayInfo descriptor) {
   1364                 switch (descriptor.status) {
   1365                     case RemoteDisplayInfo.STATUS_NOT_AVAILABLE:
   1366                         return MediaRouter.RouteInfo.STATUS_NOT_AVAILABLE;
   1367                     case RemoteDisplayInfo.STATUS_AVAILABLE:
   1368                         return MediaRouter.RouteInfo.STATUS_AVAILABLE;
   1369                     case RemoteDisplayInfo.STATUS_IN_USE:
   1370                         return MediaRouter.RouteInfo.STATUS_IN_USE;
   1371                     case RemoteDisplayInfo.STATUS_CONNECTING:
   1372                         return MediaRouter.RouteInfo.STATUS_CONNECTING;
   1373                     case RemoteDisplayInfo.STATUS_CONNECTED:
   1374                         return MediaRouter.RouteInfo.STATUS_CONNECTED;
   1375                     default:
   1376                         return MediaRouter.RouteInfo.STATUS_NONE;
   1377                 }
   1378             }
   1379 
   1380             private static int computePlaybackType(RemoteDisplayInfo descriptor) {
   1381                 return MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE;
   1382             }
   1383 
   1384             private static int computePlaybackStream(RemoteDisplayInfo descriptor) {
   1385                 return AudioSystem.STREAM_MUSIC;
   1386             }
   1387 
   1388             private static int computeVolume(RemoteDisplayInfo descriptor) {
   1389                 final int volume = descriptor.volume;
   1390                 final int volumeMax = descriptor.volumeMax;
   1391                 if (volume < 0) {
   1392                     return 0;
   1393                 } else if (volume > volumeMax) {
   1394                     return volumeMax;
   1395                 }
   1396                 return volume;
   1397             }
   1398 
   1399             private static int computeVolumeMax(RemoteDisplayInfo descriptor) {
   1400                 final int volumeMax = descriptor.volumeMax;
   1401                 return volumeMax > 0 ? volumeMax : 0;
   1402             }
   1403 
   1404             private static int computeVolumeHandling(RemoteDisplayInfo descriptor) {
   1405                 final int volumeHandling = descriptor.volumeHandling;
   1406                 switch (volumeHandling) {
   1407                     case RemoteDisplayInfo.PLAYBACK_VOLUME_VARIABLE:
   1408                         return MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE;
   1409                     case RemoteDisplayInfo.PLAYBACK_VOLUME_FIXED:
   1410                     default:
   1411                         return MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED;
   1412                 }
   1413             }
   1414 
   1415             private static int computePresentationDisplayId(RemoteDisplayInfo descriptor) {
   1416                 // The MediaRouter class validates that the id corresponds to an extant
   1417                 // presentation display.  So all we do here is canonicalize the null case.
   1418                 final int displayId = descriptor.presentationDisplayId;
   1419                 return displayId < 0 ? -1 : displayId;
   1420             }
   1421         }
   1422     }
   1423 }
   1424