/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.media; import com.android.internal.util.Objects; import com.android.server.Watchdog; import android.Manifest; import android.app.ActivityManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.media.AudioSystem; import android.media.IMediaRouterClient; import android.media.IMediaRouterService; import android.media.MediaRouter; import android.media.MediaRouterClientState; import android.media.RemoteDisplayState; import android.media.RemoteDisplayState.RemoteDisplayInfo; import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.RemoteException; import android.os.SystemClock; import android.text.TextUtils; import android.util.ArrayMap; import android.util.Log; import android.util.Slog; import android.util.SparseArray; import android.util.TimeUtils; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * Provides a mechanism for discovering media routes and manages media playback * behalf of applications. *

* Currently supports discovering remote displays via remote display provider * services that have been registered by applications. *

*/ public final class MediaRouterService extends IMediaRouterService.Stub implements Watchdog.Monitor { private static final String TAG = "MediaRouterService"; private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); /** * Timeout in milliseconds for a selected route to transition from a * disconnected state to a connecting state. If we don't observe any * progress within this interval, then we will give up and unselect the route. */ static final long CONNECTING_TIMEOUT = 5000; /** * Timeout in milliseconds for a selected route to transition from a * connecting state to a connected state. If we don't observe any * progress within this interval, then we will give up and unselect the route. */ static final long CONNECTED_TIMEOUT = 60000; private final Context mContext; // State guarded by mLock. private final Object mLock = new Object(); private final SparseArray mUserRecords = new SparseArray(); private final ArrayMap mAllClientRecords = new ArrayMap(); private int mCurrentUserId = -1; public MediaRouterService(Context context) { mContext = context; Watchdog.getInstance().addMonitor(this); } public void systemRunning() { IntentFilter filter = new IntentFilter(Intent.ACTION_USER_SWITCHED); mContext.registerReceiver(new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(Intent.ACTION_USER_SWITCHED)) { switchUser(); } } }, filter); switchUser(); } @Override public void monitor() { synchronized (mLock) { /* check for deadlock */ } } // Binder call @Override public void registerClientAsUser(IMediaRouterClient client, String packageName, int userId) { if (client == null) { throw new IllegalArgumentException("client must not be null"); } final int uid = Binder.getCallingUid(); if (!validatePackageName(uid, packageName)) { throw new SecurityException("packageName must match the calling uid"); } final int pid = Binder.getCallingPid(); final int resolvedUserId = ActivityManager.handleIncomingUser(pid, uid, userId, false /*allowAll*/, true /*requireFull*/, "registerClientAsUser", packageName); final boolean trusted = mContext.checkCallingOrSelfPermission( android.Manifest.permission.CONFIGURE_WIFI_DISPLAY) == PackageManager.PERMISSION_GRANTED; final long token = Binder.clearCallingIdentity(); try { synchronized (mLock) { registerClientLocked(client, pid, packageName, resolvedUserId, trusted); } } finally { Binder.restoreCallingIdentity(token); } } // Binder call @Override public void unregisterClient(IMediaRouterClient client) { if (client == null) { throw new IllegalArgumentException("client must not be null"); } final long token = Binder.clearCallingIdentity(); try { synchronized (mLock) { unregisterClientLocked(client, false); } } finally { Binder.restoreCallingIdentity(token); } } // Binder call @Override public MediaRouterClientState getState(IMediaRouterClient client) { if (client == null) { throw new IllegalArgumentException("client must not be null"); } final long token = Binder.clearCallingIdentity(); try { synchronized (mLock) { return getStateLocked(client); } } finally { Binder.restoreCallingIdentity(token); } } // Binder call @Override public void setDiscoveryRequest(IMediaRouterClient client, int routeTypes, boolean activeScan) { if (client == null) { throw new IllegalArgumentException("client must not be null"); } final long token = Binder.clearCallingIdentity(); try { synchronized (mLock) { setDiscoveryRequestLocked(client, routeTypes, activeScan); } } finally { Binder.restoreCallingIdentity(token); } } // Binder call // A null routeId means that the client wants to unselect its current route. // The explicit flag indicates whether the change was explicitly requested by the // user or the application which may cause changes to propagate out to the rest // of the system. Should be false when the change is in response to a new globally // selected route or a default selection. @Override public void setSelectedRoute(IMediaRouterClient client, String routeId, boolean explicit) { if (client == null) { throw new IllegalArgumentException("client must not be null"); } final long token = Binder.clearCallingIdentity(); try { synchronized (mLock) { setSelectedRouteLocked(client, routeId, explicit); } } finally { Binder.restoreCallingIdentity(token); } } // Binder call @Override public void requestSetVolume(IMediaRouterClient client, String routeId, int volume) { if (client == null) { throw new IllegalArgumentException("client must not be null"); } if (routeId == null) { throw new IllegalArgumentException("routeId must not be null"); } final long token = Binder.clearCallingIdentity(); try { synchronized (mLock) { requestSetVolumeLocked(client, routeId, volume); } } finally { Binder.restoreCallingIdentity(token); } } // Binder call @Override public void requestUpdateVolume(IMediaRouterClient client, String routeId, int direction) { if (client == null) { throw new IllegalArgumentException("client must not be null"); } if (routeId == null) { throw new IllegalArgumentException("routeId must not be null"); } final long token = Binder.clearCallingIdentity(); try { synchronized (mLock) { requestUpdateVolumeLocked(client, routeId, direction); } } finally { Binder.restoreCallingIdentity(token); } } // Binder call @Override public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) { if (mContext.checkCallingOrSelfPermission(Manifest.permission.DUMP) != PackageManager.PERMISSION_GRANTED) { pw.println("Permission Denial: can't dump MediaRouterService from from pid=" + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()); return; } pw.println("MEDIA ROUTER SERVICE (dumpsys media_router)"); pw.println(); pw.println("Global state"); pw.println(" mCurrentUserId=" + mCurrentUserId); synchronized (mLock) { final int count = mUserRecords.size(); for (int i = 0; i < count; i++) { UserRecord userRecord = mUserRecords.valueAt(i); pw.println(); userRecord.dump(pw, ""); } } } void switchUser() { synchronized (mLock) { int userId = ActivityManager.getCurrentUser(); if (mCurrentUserId != userId) { final int oldUserId = mCurrentUserId; mCurrentUserId = userId; // do this first UserRecord oldUser = mUserRecords.get(oldUserId); if (oldUser != null) { oldUser.mHandler.sendEmptyMessage(UserHandler.MSG_STOP); disposeUserIfNeededLocked(oldUser); // since no longer current user } UserRecord newUser = mUserRecords.get(userId); if (newUser != null) { newUser.mHandler.sendEmptyMessage(UserHandler.MSG_START); } } } } void clientDied(ClientRecord clientRecord) { synchronized (mLock) { unregisterClientLocked(clientRecord.mClient, true); } } private void registerClientLocked(IMediaRouterClient client, int pid, String packageName, int userId, boolean trusted) { final IBinder binder = client.asBinder(); ClientRecord clientRecord = mAllClientRecords.get(binder); if (clientRecord == null) { boolean newUser = false; UserRecord userRecord = mUserRecords.get(userId); if (userRecord == null) { userRecord = new UserRecord(userId); newUser = true; } clientRecord = new ClientRecord(userRecord, client, pid, packageName, trusted); try { binder.linkToDeath(clientRecord, 0); } catch (RemoteException ex) { throw new RuntimeException("Media router client died prematurely.", ex); } if (newUser) { mUserRecords.put(userId, userRecord); initializeUserLocked(userRecord); } userRecord.mClientRecords.add(clientRecord); mAllClientRecords.put(binder, clientRecord); initializeClientLocked(clientRecord); } } private void unregisterClientLocked(IMediaRouterClient client, boolean died) { ClientRecord clientRecord = mAllClientRecords.remove(client.asBinder()); if (clientRecord != null) { UserRecord userRecord = clientRecord.mUserRecord; userRecord.mClientRecords.remove(clientRecord); disposeClientLocked(clientRecord, died); disposeUserIfNeededLocked(userRecord); // since client removed from user } } private MediaRouterClientState getStateLocked(IMediaRouterClient client) { ClientRecord clientRecord = mAllClientRecords.get(client.asBinder()); if (clientRecord != null) { return clientRecord.getState(); } return null; } private void setDiscoveryRequestLocked(IMediaRouterClient client, int routeTypes, boolean activeScan) { final IBinder binder = client.asBinder(); ClientRecord clientRecord = mAllClientRecords.get(binder); if (clientRecord != null) { // Only let the system discover remote display routes for now. if (!clientRecord.mTrusted) { routeTypes &= ~MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY; } if (clientRecord.mRouteTypes != routeTypes || clientRecord.mActiveScan != activeScan) { if (DEBUG) { Slog.d(TAG, clientRecord + ": Set discovery request, routeTypes=0x" + Integer.toHexString(routeTypes) + ", activeScan=" + activeScan); } clientRecord.mRouteTypes = routeTypes; clientRecord.mActiveScan = activeScan; clientRecord.mUserRecord.mHandler.sendEmptyMessage( UserHandler.MSG_UPDATE_DISCOVERY_REQUEST); } } } private void setSelectedRouteLocked(IMediaRouterClient client, String routeId, boolean explicit) { ClientRecord clientRecord = mAllClientRecords.get(client.asBinder()); if (clientRecord != null) { final String oldRouteId = clientRecord.mSelectedRouteId; if (!Objects.equal(routeId, oldRouteId)) { if (DEBUG) { Slog.d(TAG, clientRecord + ": Set selected route, routeId=" + routeId + ", oldRouteId=" + oldRouteId + ", explicit=" + explicit); } clientRecord.mSelectedRouteId = routeId; if (explicit) { // Any app can disconnect from the globally selected route. if (oldRouteId != null) { clientRecord.mUserRecord.mHandler.obtainMessage( UserHandler.MSG_UNSELECT_ROUTE, oldRouteId).sendToTarget(); } // Only let the system connect to new global routes for now. // A similar check exists in the display manager for wifi display. if (routeId != null && clientRecord.mTrusted) { clientRecord.mUserRecord.mHandler.obtainMessage( UserHandler.MSG_SELECT_ROUTE, routeId).sendToTarget(); } } } } } private void requestSetVolumeLocked(IMediaRouterClient client, String routeId, int volume) { final IBinder binder = client.asBinder(); ClientRecord clientRecord = mAllClientRecords.get(binder); if (clientRecord != null) { clientRecord.mUserRecord.mHandler.obtainMessage( UserHandler.MSG_REQUEST_SET_VOLUME, volume, 0, routeId).sendToTarget(); } } private void requestUpdateVolumeLocked(IMediaRouterClient client, String routeId, int direction) { final IBinder binder = client.asBinder(); ClientRecord clientRecord = mAllClientRecords.get(binder); if (clientRecord != null) { clientRecord.mUserRecord.mHandler.obtainMessage( UserHandler.MSG_REQUEST_UPDATE_VOLUME, direction, 0, routeId).sendToTarget(); } } private void initializeUserLocked(UserRecord userRecord) { if (DEBUG) { Slog.d(TAG, userRecord + ": Initialized"); } if (userRecord.mUserId == mCurrentUserId) { userRecord.mHandler.sendEmptyMessage(UserHandler.MSG_START); } } private void disposeUserIfNeededLocked(UserRecord userRecord) { // If there are no records left and the user is no longer current then go ahead // and purge the user record and all of its associated state. If the user is current // then leave it alone since we might be connected to a route or want to query // the same route information again soon. if (userRecord.mUserId != mCurrentUserId && userRecord.mClientRecords.isEmpty()) { if (DEBUG) { Slog.d(TAG, userRecord + ": Disposed"); } mUserRecords.remove(userRecord.mUserId); // Note: User already stopped (by switchUser) so no need to send stop message here. } } private void initializeClientLocked(ClientRecord clientRecord) { if (DEBUG) { Slog.d(TAG, clientRecord + ": Registered"); } } private void disposeClientLocked(ClientRecord clientRecord, boolean died) { if (DEBUG) { if (died) { Slog.d(TAG, clientRecord + ": Died!"); } else { Slog.d(TAG, clientRecord + ": Unregistered"); } } if (clientRecord.mRouteTypes != 0 || clientRecord.mActiveScan) { clientRecord.mUserRecord.mHandler.sendEmptyMessage( UserHandler.MSG_UPDATE_DISCOVERY_REQUEST); } clientRecord.dispose(); } private boolean validatePackageName(int uid, String packageName) { if (packageName != null) { String[] packageNames = mContext.getPackageManager().getPackagesForUid(uid); if (packageNames != null) { for (String n : packageNames) { if (n.equals(packageName)) { return true; } } } } return false; } /** * Information about a particular client of the media router. * The contents of this object is guarded by mLock. */ final class ClientRecord implements DeathRecipient { public final UserRecord mUserRecord; public final IMediaRouterClient mClient; public final int mPid; public final String mPackageName; public final boolean mTrusted; public int mRouteTypes; public boolean mActiveScan; public String mSelectedRouteId; public ClientRecord(UserRecord userRecord, IMediaRouterClient client, int pid, String packageName, boolean trusted) { mUserRecord = userRecord; mClient = client; mPid = pid; mPackageName = packageName; mTrusted = trusted; } public void dispose() { mClient.asBinder().unlinkToDeath(this, 0); } @Override public void binderDied() { clientDied(this); } MediaRouterClientState getState() { return mTrusted ? mUserRecord.mTrustedState : mUserRecord.mUntrustedState; } public void dump(PrintWriter pw, String prefix) { pw.println(prefix + this); final String indent = prefix + " "; pw.println(indent + "mTrusted=" + mTrusted); pw.println(indent + "mRouteTypes=0x" + Integer.toHexString(mRouteTypes)); pw.println(indent + "mActiveScan=" + mActiveScan); pw.println(indent + "mSelectedRouteId=" + mSelectedRouteId); } @Override public String toString() { return "Client " + mPackageName + " (pid " + mPid + ")"; } } /** * Information about a particular user. * The contents of this object is guarded by mLock. */ final class UserRecord { public final int mUserId; public final ArrayList mClientRecords = new ArrayList(); public final UserHandler mHandler; public MediaRouterClientState mTrustedState; public MediaRouterClientState mUntrustedState; public UserRecord(int userId) { mUserId = userId; mHandler = new UserHandler(MediaRouterService.this, this); } public void dump(final PrintWriter pw, String prefix) { pw.println(prefix + this); final String indent = prefix + " "; final int clientCount = mClientRecords.size(); if (clientCount != 0) { for (int i = 0; i < clientCount; i++) { mClientRecords.get(i).dump(pw, indent); } } else { pw.println(indent + ""); } pw.println(indent + "State"); pw.println(indent + "mTrustedState=" + mTrustedState); pw.println(indent + "mUntrustedState=" + mUntrustedState); if (!mHandler.runWithScissors(new Runnable() { @Override public void run() { mHandler.dump(pw, indent); } }, 1000)) { pw.println(indent + ""); } } @Override public String toString() { return "User " + mUserId; } } /** * Media router handler *

* Since remote display providers are designed to be single-threaded by nature, * this class encapsulates all of the associated functionality and exports state * to the service as it evolves. *

* One important task of this class is to keep track of the current globally selected * route id for certain routes that have global effects, such as remote displays. * Global route selections override local selections made within apps. The change * is propagated to all apps so that they are all in sync. Synchronization works * both ways. Whenever the globally selected route is explicitly unselected by any * app, then it becomes unselected globally and all apps are informed. *

* This class is currently hardcoded to work with remote display providers but * it is intended to be eventually extended to support more general route providers * similar to the support library media router. *

*/ static final class UserHandler extends Handler implements RemoteDisplayProviderWatcher.Callback, RemoteDisplayProviderProxy.Callback { public static final int MSG_START = 1; public static final int MSG_STOP = 2; public static final int MSG_UPDATE_DISCOVERY_REQUEST = 3; public static final int MSG_SELECT_ROUTE = 4; public static final int MSG_UNSELECT_ROUTE = 5; public static final int MSG_REQUEST_SET_VOLUME = 6; public static final int MSG_REQUEST_UPDATE_VOLUME = 7; private static final int MSG_UPDATE_CLIENT_STATE = 8; private static final int MSG_CONNECTION_TIMED_OUT = 9; private static final int TIMEOUT_REASON_NOT_AVAILABLE = 1; private static final int TIMEOUT_REASON_CONNECTION_LOST = 2; private static final int TIMEOUT_REASON_WAITING_FOR_CONNECTING = 3; private static final int TIMEOUT_REASON_WAITING_FOR_CONNECTED = 4; // The relative order of these constants is important and expresses progress // through the process of connecting to a route. private static final int PHASE_NOT_AVAILABLE = -1; private static final int PHASE_NOT_CONNECTED = 0; private static final int PHASE_CONNECTING = 1; private static final int PHASE_CONNECTED = 2; private final MediaRouterService mService; private final UserRecord mUserRecord; private final RemoteDisplayProviderWatcher mWatcher; private final ArrayList mProviderRecords = new ArrayList(); private final ArrayList mTempClients = new ArrayList(); private boolean mRunning; private int mDiscoveryMode = RemoteDisplayState.DISCOVERY_MODE_NONE; private RouteRecord mGloballySelectedRouteRecord; private int mConnectionPhase = PHASE_NOT_AVAILABLE; private int mConnectionTimeoutReason; private long mConnectionTimeoutStartTime; private boolean mClientStateUpdateScheduled; public UserHandler(MediaRouterService service, UserRecord userRecord) { super(Looper.getMainLooper(), null, true); mService = service; mUserRecord = userRecord; mWatcher = new RemoteDisplayProviderWatcher(service.mContext, this, this, mUserRecord.mUserId); } @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_START: { start(); break; } case MSG_STOP: { stop(); break; } case MSG_UPDATE_DISCOVERY_REQUEST: { updateDiscoveryRequest(); break; } case MSG_SELECT_ROUTE: { selectRoute((String)msg.obj); break; } case MSG_UNSELECT_ROUTE: { unselectRoute((String)msg.obj); break; } case MSG_REQUEST_SET_VOLUME: { requestSetVolume((String)msg.obj, msg.arg1); break; } case MSG_REQUEST_UPDATE_VOLUME: { requestUpdateVolume((String)msg.obj, msg.arg1); break; } case MSG_UPDATE_CLIENT_STATE: { updateClientState(); break; } case MSG_CONNECTION_TIMED_OUT: { connectionTimedOut(); break; } } } public void dump(PrintWriter pw, String prefix) { pw.println(prefix + "Handler"); final String indent = prefix + " "; pw.println(indent + "mRunning=" + mRunning); pw.println(indent + "mDiscoveryMode=" + mDiscoveryMode); pw.println(indent + "mGloballySelectedRouteRecord=" + mGloballySelectedRouteRecord); pw.println(indent + "mConnectionPhase=" + mConnectionPhase); pw.println(indent + "mConnectionTimeoutReason=" + mConnectionTimeoutReason); pw.println(indent + "mConnectionTimeoutStartTime=" + (mConnectionTimeoutReason != 0 ? TimeUtils.formatUptime(mConnectionTimeoutStartTime) : "")); mWatcher.dump(pw, prefix); final int providerCount = mProviderRecords.size(); if (providerCount != 0) { for (int i = 0; i < providerCount; i++) { mProviderRecords.get(i).dump(pw, prefix); } } else { pw.println(indent + ""); } } private void start() { if (!mRunning) { mRunning = true; mWatcher.start(); // also starts all providers } } private void stop() { if (mRunning) { mRunning = false; unselectGloballySelectedRoute(); mWatcher.stop(); // also stops all providers } } private void updateDiscoveryRequest() { int routeTypes = 0; boolean activeScan = false; synchronized (mService.mLock) { final int count = mUserRecord.mClientRecords.size(); for (int i = 0; i < count; i++) { ClientRecord clientRecord = mUserRecord.mClientRecords.get(i); routeTypes |= clientRecord.mRouteTypes; activeScan |= clientRecord.mActiveScan; } } final int newDiscoveryMode; if ((routeTypes & MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY) != 0) { if (activeScan) { newDiscoveryMode = RemoteDisplayState.DISCOVERY_MODE_ACTIVE; } else { newDiscoveryMode = RemoteDisplayState.DISCOVERY_MODE_PASSIVE; } } else { newDiscoveryMode = RemoteDisplayState.DISCOVERY_MODE_NONE; } if (mDiscoveryMode != newDiscoveryMode) { mDiscoveryMode = newDiscoveryMode; final int count = mProviderRecords.size(); for (int i = 0; i < count; i++) { mProviderRecords.get(i).getProvider().setDiscoveryMode(mDiscoveryMode); } } } private void selectRoute(String routeId) { if (routeId != null && (mGloballySelectedRouteRecord == null || !routeId.equals(mGloballySelectedRouteRecord.getUniqueId()))) { RouteRecord routeRecord = findRouteRecord(routeId); if (routeRecord != null) { unselectGloballySelectedRoute(); Slog.i(TAG, "Selected global route:" + routeRecord); mGloballySelectedRouteRecord = routeRecord; checkGloballySelectedRouteState(); routeRecord.getProvider().setSelectedDisplay(routeRecord.getDescriptorId()); scheduleUpdateClientState(); } } } private void unselectRoute(String routeId) { if (routeId != null && mGloballySelectedRouteRecord != null && routeId.equals(mGloballySelectedRouteRecord.getUniqueId())) { unselectGloballySelectedRoute(); } } private void unselectGloballySelectedRoute() { if (mGloballySelectedRouteRecord != null) { Slog.i(TAG, "Unselected global route:" + mGloballySelectedRouteRecord); mGloballySelectedRouteRecord.getProvider().setSelectedDisplay(null); mGloballySelectedRouteRecord = null; checkGloballySelectedRouteState(); scheduleUpdateClientState(); } } private void requestSetVolume(String routeId, int volume) { if (mGloballySelectedRouteRecord != null && routeId.equals(mGloballySelectedRouteRecord.getUniqueId())) { mGloballySelectedRouteRecord.getProvider().setDisplayVolume(volume); } } private void requestUpdateVolume(String routeId, int direction) { if (mGloballySelectedRouteRecord != null && routeId.equals(mGloballySelectedRouteRecord.getUniqueId())) { mGloballySelectedRouteRecord.getProvider().adjustDisplayVolume(direction); } } @Override public void addProvider(RemoteDisplayProviderProxy provider) { provider.setCallback(this); provider.setDiscoveryMode(mDiscoveryMode); provider.setSelectedDisplay(null); // just to be safe ProviderRecord providerRecord = new ProviderRecord(provider); mProviderRecords.add(providerRecord); providerRecord.updateDescriptor(provider.getDisplayState()); scheduleUpdateClientState(); } @Override public void removeProvider(RemoteDisplayProviderProxy provider) { int index = findProviderRecord(provider); if (index >= 0) { ProviderRecord providerRecord = mProviderRecords.remove(index); providerRecord.updateDescriptor(null); // mark routes invalid provider.setCallback(null); provider.setDiscoveryMode(RemoteDisplayState.DISCOVERY_MODE_NONE); checkGloballySelectedRouteState(); scheduleUpdateClientState(); } } @Override public void onDisplayStateChanged(RemoteDisplayProviderProxy provider, RemoteDisplayState state) { updateProvider(provider, state); } private void updateProvider(RemoteDisplayProviderProxy provider, RemoteDisplayState state) { int index = findProviderRecord(provider); if (index >= 0) { ProviderRecord providerRecord = mProviderRecords.get(index); if (providerRecord.updateDescriptor(state)) { checkGloballySelectedRouteState(); scheduleUpdateClientState(); } } } /** * This function is called whenever the state of the globally selected route * may have changed. It checks the state and updates timeouts or unselects * the route as appropriate. */ private void checkGloballySelectedRouteState() { // Unschedule timeouts when the route is unselected. if (mGloballySelectedRouteRecord == null) { mConnectionPhase = PHASE_NOT_AVAILABLE; updateConnectionTimeout(0); return; } // Ensure that the route is still present and enabled. if (!mGloballySelectedRouteRecord.isValid() || !mGloballySelectedRouteRecord.isEnabled()) { updateConnectionTimeout(TIMEOUT_REASON_NOT_AVAILABLE); return; } // Make sure we haven't lost our connection. final int oldPhase = mConnectionPhase; mConnectionPhase = getConnectionPhase(mGloballySelectedRouteRecord.getStatus()); if (oldPhase >= PHASE_CONNECTING && mConnectionPhase < PHASE_CONNECTING) { updateConnectionTimeout(TIMEOUT_REASON_CONNECTION_LOST); return; } // Check the route status. switch (mConnectionPhase) { case PHASE_CONNECTED: if (oldPhase != PHASE_CONNECTED) { Slog.i(TAG, "Connected to global route: " + mGloballySelectedRouteRecord); } updateConnectionTimeout(0); break; case PHASE_CONNECTING: if (oldPhase != PHASE_CONNECTING) { Slog.i(TAG, "Connecting to global route: " + mGloballySelectedRouteRecord); } updateConnectionTimeout(TIMEOUT_REASON_WAITING_FOR_CONNECTED); break; case PHASE_NOT_CONNECTED: updateConnectionTimeout(TIMEOUT_REASON_WAITING_FOR_CONNECTING); break; case PHASE_NOT_AVAILABLE: default: updateConnectionTimeout(TIMEOUT_REASON_NOT_AVAILABLE); break; } } private void updateConnectionTimeout(int reason) { if (reason != mConnectionTimeoutReason) { if (mConnectionTimeoutReason != 0) { removeMessages(MSG_CONNECTION_TIMED_OUT); } mConnectionTimeoutReason = reason; mConnectionTimeoutStartTime = SystemClock.uptimeMillis(); switch (reason) { case TIMEOUT_REASON_NOT_AVAILABLE: case TIMEOUT_REASON_CONNECTION_LOST: // Route became unavailable or connection lost. // Unselect it immediately. sendEmptyMessage(MSG_CONNECTION_TIMED_OUT); break; case TIMEOUT_REASON_WAITING_FOR_CONNECTING: // Waiting for route to start connecting. sendEmptyMessageDelayed(MSG_CONNECTION_TIMED_OUT, CONNECTING_TIMEOUT); break; case TIMEOUT_REASON_WAITING_FOR_CONNECTED: // Waiting for route to complete connection. sendEmptyMessageDelayed(MSG_CONNECTION_TIMED_OUT, CONNECTED_TIMEOUT); break; } } } private void connectionTimedOut() { if (mConnectionTimeoutReason == 0 || mGloballySelectedRouteRecord == null) { // Shouldn't get here. There must be a bug somewhere. Log.wtf(TAG, "Handled connection timeout for no reason."); return; } switch (mConnectionTimeoutReason) { case TIMEOUT_REASON_NOT_AVAILABLE: Slog.i(TAG, "Global route no longer available: " + mGloballySelectedRouteRecord); break; case TIMEOUT_REASON_CONNECTION_LOST: Slog.i(TAG, "Global route connection lost: " + mGloballySelectedRouteRecord); break; case TIMEOUT_REASON_WAITING_FOR_CONNECTING: Slog.i(TAG, "Global route timed out while waiting for " + "connection attempt to begin after " + (SystemClock.uptimeMillis() - mConnectionTimeoutStartTime) + " ms: " + mGloballySelectedRouteRecord); break; case TIMEOUT_REASON_WAITING_FOR_CONNECTED: Slog.i(TAG, "Global route timed out while connecting after " + (SystemClock.uptimeMillis() - mConnectionTimeoutStartTime) + " ms: " + mGloballySelectedRouteRecord); break; } mConnectionTimeoutReason = 0; unselectGloballySelectedRoute(); } private void scheduleUpdateClientState() { if (!mClientStateUpdateScheduled) { mClientStateUpdateScheduled = true; sendEmptyMessage(MSG_UPDATE_CLIENT_STATE); } } private void updateClientState() { mClientStateUpdateScheduled = false; final String globallySelectedRouteId = mGloballySelectedRouteRecord != null ? mGloballySelectedRouteRecord.getUniqueId() : null; // Build a new client state for trusted clients. MediaRouterClientState trustedState = new MediaRouterClientState(); trustedState.globallySelectedRouteId = globallySelectedRouteId; final int providerCount = mProviderRecords.size(); for (int i = 0; i < providerCount; i++) { mProviderRecords.get(i).appendClientState(trustedState); } // Build a new client state for untrusted clients that can only see // the currently selected route. MediaRouterClientState untrustedState = new MediaRouterClientState(); untrustedState.globallySelectedRouteId = globallySelectedRouteId; if (globallySelectedRouteId != null) { untrustedState.routes.add(trustedState.getRoute(globallySelectedRouteId)); } try { synchronized (mService.mLock) { // Update the UserRecord. mUserRecord.mTrustedState = trustedState; mUserRecord.mUntrustedState = untrustedState; // Collect all clients. final int count = mUserRecord.mClientRecords.size(); for (int i = 0; i < count; i++) { mTempClients.add(mUserRecord.mClientRecords.get(i).mClient); } } // Notify all clients (outside of the lock). final int count = mTempClients.size(); for (int i = 0; i < count; i++) { try { mTempClients.get(i).onStateChanged(); } catch (RemoteException ex) { // ignore errors, client probably died } } } finally { // Clear the list in preparation for the next time. mTempClients.clear(); } } private int findProviderRecord(RemoteDisplayProviderProxy provider) { final int count = mProviderRecords.size(); for (int i = 0; i < count; i++) { ProviderRecord record = mProviderRecords.get(i); if (record.getProvider() == provider) { return i; } } return -1; } private RouteRecord findRouteRecord(String uniqueId) { final int count = mProviderRecords.size(); for (int i = 0; i < count; i++) { RouteRecord record = mProviderRecords.get(i).findRouteByUniqueId(uniqueId); if (record != null) { return record; } } return null; } private static int getConnectionPhase(int status) { switch (status) { case MediaRouter.RouteInfo.STATUS_NONE: case MediaRouter.RouteInfo.STATUS_CONNECTED: return PHASE_CONNECTED; case MediaRouter.RouteInfo.STATUS_CONNECTING: return PHASE_CONNECTING; case MediaRouter.RouteInfo.STATUS_SCANNING: case MediaRouter.RouteInfo.STATUS_AVAILABLE: return PHASE_NOT_CONNECTED; case MediaRouter.RouteInfo.STATUS_NOT_AVAILABLE: case MediaRouter.RouteInfo.STATUS_IN_USE: default: return PHASE_NOT_AVAILABLE; } } static final class ProviderRecord { private final RemoteDisplayProviderProxy mProvider; private final String mUniquePrefix; private final ArrayList mRoutes = new ArrayList(); private RemoteDisplayState mDescriptor; public ProviderRecord(RemoteDisplayProviderProxy provider) { mProvider = provider; mUniquePrefix = provider.getFlattenedComponentName() + ":"; } public RemoteDisplayProviderProxy getProvider() { return mProvider; } public String getUniquePrefix() { return mUniquePrefix; } public boolean updateDescriptor(RemoteDisplayState descriptor) { boolean changed = false; if (mDescriptor != descriptor) { mDescriptor = descriptor; // Update all existing routes and reorder them to match // the order of their descriptors. int targetIndex = 0; if (descriptor != null) { if (descriptor.isValid()) { final List routeDescriptors = descriptor.displays; final int routeCount = routeDescriptors.size(); for (int i = 0; i < routeCount; i++) { final RemoteDisplayInfo routeDescriptor = routeDescriptors.get(i); final String descriptorId = routeDescriptor.id; final int sourceIndex = findRouteByDescriptorId(descriptorId); if (sourceIndex < 0) { // Add the route to the provider. String uniqueId = assignRouteUniqueId(descriptorId); RouteRecord route = new RouteRecord(this, descriptorId, uniqueId); mRoutes.add(targetIndex++, route); route.updateDescriptor(routeDescriptor); changed = true; } else if (sourceIndex < targetIndex) { // Ignore route with duplicate id. Slog.w(TAG, "Ignoring route descriptor with duplicate id: " + routeDescriptor); } else { // Reorder existing route within the list. RouteRecord route = mRoutes.get(sourceIndex); Collections.swap(mRoutes, sourceIndex, targetIndex++); changed |= route.updateDescriptor(routeDescriptor); } } } else { Slog.w(TAG, "Ignoring invalid descriptor from media route provider: " + mProvider.getFlattenedComponentName()); } } // Dispose all remaining routes that do not have matching descriptors. for (int i = mRoutes.size() - 1; i >= targetIndex; i--) { RouteRecord route = mRoutes.remove(i); route.updateDescriptor(null); // mark route invalid changed = true; } } return changed; } public void appendClientState(MediaRouterClientState state) { final int routeCount = mRoutes.size(); for (int i = 0; i < routeCount; i++) { state.routes.add(mRoutes.get(i).getInfo()); } } public RouteRecord findRouteByUniqueId(String uniqueId) { final int routeCount = mRoutes.size(); for (int i = 0; i < routeCount; i++) { RouteRecord route = mRoutes.get(i); if (route.getUniqueId().equals(uniqueId)) { return route; } } return null; } private int findRouteByDescriptorId(String descriptorId) { final int routeCount = mRoutes.size(); for (int i = 0; i < routeCount; i++) { RouteRecord route = mRoutes.get(i); if (route.getDescriptorId().equals(descriptorId)) { return i; } } return -1; } public void dump(PrintWriter pw, String prefix) { pw.println(prefix + this); final String indent = prefix + " "; mProvider.dump(pw, indent); final int routeCount = mRoutes.size(); if (routeCount != 0) { for (int i = 0; i < routeCount; i++) { mRoutes.get(i).dump(pw, indent); } } else { pw.println(indent + ""); } } @Override public String toString() { return "Provider " + mProvider.getFlattenedComponentName(); } private String assignRouteUniqueId(String descriptorId) { return mUniquePrefix + descriptorId; } } static final class RouteRecord { private final ProviderRecord mProviderRecord; private final String mDescriptorId; private final MediaRouterClientState.RouteInfo mMutableInfo; private MediaRouterClientState.RouteInfo mImmutableInfo; private RemoteDisplayInfo mDescriptor; public RouteRecord(ProviderRecord providerRecord, String descriptorId, String uniqueId) { mProviderRecord = providerRecord; mDescriptorId = descriptorId; mMutableInfo = new MediaRouterClientState.RouteInfo(uniqueId); } public RemoteDisplayProviderProxy getProvider() { return mProviderRecord.getProvider(); } public ProviderRecord getProviderRecord() { return mProviderRecord; } public String getDescriptorId() { return mDescriptorId; } public String getUniqueId() { return mMutableInfo.id; } public MediaRouterClientState.RouteInfo getInfo() { if (mImmutableInfo == null) { mImmutableInfo = new MediaRouterClientState.RouteInfo(mMutableInfo); } return mImmutableInfo; } public boolean isValid() { return mDescriptor != null; } public boolean isEnabled() { return mMutableInfo.enabled; } public int getStatus() { return mMutableInfo.statusCode; } public boolean updateDescriptor(RemoteDisplayInfo descriptor) { boolean changed = false; if (mDescriptor != descriptor) { mDescriptor = descriptor; if (descriptor != null) { final String name = computeName(descriptor); if (!Objects.equal(mMutableInfo.name, name)) { mMutableInfo.name = name; changed = true; } final String description = computeDescription(descriptor); if (!Objects.equal(mMutableInfo.description, description)) { mMutableInfo.description = description; changed = true; } final int supportedTypes = computeSupportedTypes(descriptor); if (mMutableInfo.supportedTypes != supportedTypes) { mMutableInfo.supportedTypes = supportedTypes; changed = true; } final boolean enabled = computeEnabled(descriptor); if (mMutableInfo.enabled != enabled) { mMutableInfo.enabled = enabled; changed = true; } final int statusCode = computeStatusCode(descriptor); if (mMutableInfo.statusCode != statusCode) { mMutableInfo.statusCode = statusCode; changed = true; } final int playbackType = computePlaybackType(descriptor); if (mMutableInfo.playbackType != playbackType) { mMutableInfo.playbackType = playbackType; changed = true; } final int playbackStream = computePlaybackStream(descriptor); if (mMutableInfo.playbackStream != playbackStream) { mMutableInfo.playbackStream = playbackStream; changed = true; } final int volume = computeVolume(descriptor); if (mMutableInfo.volume != volume) { mMutableInfo.volume = volume; changed = true; } final int volumeMax = computeVolumeMax(descriptor); if (mMutableInfo.volumeMax != volumeMax) { mMutableInfo.volumeMax = volumeMax; changed = true; } final int volumeHandling = computeVolumeHandling(descriptor); if (mMutableInfo.volumeHandling != volumeHandling) { mMutableInfo.volumeHandling = volumeHandling; changed = true; } final int presentationDisplayId = computePresentationDisplayId(descriptor); if (mMutableInfo.presentationDisplayId != presentationDisplayId) { mMutableInfo.presentationDisplayId = presentationDisplayId; changed = true; } } } if (changed) { mImmutableInfo = null; } return changed; } public void dump(PrintWriter pw, String prefix) { pw.println(prefix + this); final String indent = prefix + " "; pw.println(indent + "mMutableInfo=" + mMutableInfo); pw.println(indent + "mDescriptorId=" + mDescriptorId); pw.println(indent + "mDescriptor=" + mDescriptor); } @Override public String toString() { return "Route " + mMutableInfo.name + " (" + mMutableInfo.id + ")"; } private static String computeName(RemoteDisplayInfo descriptor) { // Note that isValid() already ensures the name is non-empty. return descriptor.name; } private static String computeDescription(RemoteDisplayInfo descriptor) { final String description = descriptor.description; return TextUtils.isEmpty(description) ? null : description; } private static int computeSupportedTypes(RemoteDisplayInfo descriptor) { return MediaRouter.ROUTE_TYPE_LIVE_AUDIO | MediaRouter.ROUTE_TYPE_LIVE_VIDEO | MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY; } private static boolean computeEnabled(RemoteDisplayInfo descriptor) { switch (descriptor.status) { case RemoteDisplayInfo.STATUS_CONNECTED: case RemoteDisplayInfo.STATUS_CONNECTING: case RemoteDisplayInfo.STATUS_AVAILABLE: return true; default: return false; } } private static int computeStatusCode(RemoteDisplayInfo descriptor) { switch (descriptor.status) { case RemoteDisplayInfo.STATUS_NOT_AVAILABLE: return MediaRouter.RouteInfo.STATUS_NOT_AVAILABLE; case RemoteDisplayInfo.STATUS_AVAILABLE: return MediaRouter.RouteInfo.STATUS_AVAILABLE; case RemoteDisplayInfo.STATUS_IN_USE: return MediaRouter.RouteInfo.STATUS_IN_USE; case RemoteDisplayInfo.STATUS_CONNECTING: return MediaRouter.RouteInfo.STATUS_CONNECTING; case RemoteDisplayInfo.STATUS_CONNECTED: return MediaRouter.RouteInfo.STATUS_CONNECTED; default: return MediaRouter.RouteInfo.STATUS_NONE; } } private static int computePlaybackType(RemoteDisplayInfo descriptor) { return MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE; } private static int computePlaybackStream(RemoteDisplayInfo descriptor) { return AudioSystem.STREAM_MUSIC; } private static int computeVolume(RemoteDisplayInfo descriptor) { final int volume = descriptor.volume; final int volumeMax = descriptor.volumeMax; if (volume < 0) { return 0; } else if (volume > volumeMax) { return volumeMax; } return volume; } private static int computeVolumeMax(RemoteDisplayInfo descriptor) { final int volumeMax = descriptor.volumeMax; return volumeMax > 0 ? volumeMax : 0; } private static int computeVolumeHandling(RemoteDisplayInfo descriptor) { final int volumeHandling = descriptor.volumeHandling; switch (volumeHandling) { case RemoteDisplayInfo.PLAYBACK_VOLUME_VARIABLE: return MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE; case RemoteDisplayInfo.PLAYBACK_VOLUME_FIXED: default: return MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED; } } private static int computePresentationDisplayId(RemoteDisplayInfo descriptor) { // The MediaRouter class validates that the id corresponds to an extant // presentation display. So all we do here is canonicalize the null case. final int displayId = descriptor.presentationDisplayId; return displayId < 0 ? -1 : displayId; } } } }