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