/* * 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.DumpUtils; import com.android.server.Watchdog; import android.annotation.NonNull; import android.app.ActivityManager; import android.bluetooth.BluetoothA2dp; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothProfile; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.media.AudioPlaybackConfiguration; import android.media.AudioRoutesInfo; import android.media.AudioSystem; import android.media.IAudioRoutesObserver; import android.media.IAudioService; 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.ServiceManager; import android.os.SystemClock; import android.os.UserHandle; import android.text.TextUtils; import android.util.ArrayMap; import android.util.IntArray; 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; import java.util.Objects; /** * 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; private final IAudioService mAudioService; private final AudioPlayerStateMonitor mAudioPlayerStateMonitor; private final Handler mHandler = new Handler(); private final IntArray mActivePlayerMinPriorityQueue = new IntArray(); private final IntArray mActivePlayerUidMinPriorityQueue = new IntArray(); private final BroadcastReceiver mReceiver = new MediaRouterServiceBroadcastReceiver(); BluetoothDevice mActiveBluetoothDevice; int mAudioRouteMainType = AudioRoutesInfo.MAIN_SPEAKER; boolean mGlobalBluetoothA2dpOn = false; public MediaRouterService(Context context) { mContext = context; Watchdog.getInstance().addMonitor(this); mAudioService = IAudioService.Stub.asInterface( ServiceManager.getService(Context.AUDIO_SERVICE)); mAudioPlayerStateMonitor = AudioPlayerStateMonitor.getInstance(); mAudioPlayerStateMonitor.registerListener( new AudioPlayerStateMonitor.OnAudioPlayerActiveStateChangedListener() { static final long WAIT_MS = 500; final Runnable mRestoreBluetoothA2dpRunnable = new Runnable() { @Override public void run() { restoreBluetoothA2dp(); } }; @Override public void onAudioPlayerActiveStateChanged( @NonNull AudioPlaybackConfiguration config, boolean isRemoved) { final boolean active = !isRemoved && config.isActive(); final int pii = config.getPlayerInterfaceId(); final int uid = config.getClientUid(); final int idx = mActivePlayerMinPriorityQueue.indexOf(pii); // Keep the latest active player and its uid at the end of the queue. if (idx >= 0) { mActivePlayerMinPriorityQueue.remove(idx); mActivePlayerUidMinPriorityQueue.remove(idx); } int restoreUid = -1; if (active) { mActivePlayerMinPriorityQueue.add(config.getPlayerInterfaceId()); mActivePlayerUidMinPriorityQueue.add(uid); restoreUid = uid; } else if (mActivePlayerUidMinPriorityQueue.size() > 0) { restoreUid = mActivePlayerUidMinPriorityQueue.get( mActivePlayerUidMinPriorityQueue.size() - 1); } mHandler.removeCallbacks(mRestoreBluetoothA2dpRunnable); if (restoreUid >= 0) { restoreRoute(restoreUid); if (DEBUG) { Slog.d(TAG, "onAudioPlayerActiveStateChanged: " + "uid=" + uid + ", active=" + active + ", restoreUid=" + restoreUid); } } else { mHandler.postDelayed(mRestoreBluetoothA2dpRunnable, WAIT_MS); if (DEBUG) { Slog.d(TAG, "onAudioPlayerActiveStateChanged: " + "uid=" + uid + ", active=" + active + ", delaying"); } } } }, mHandler); mAudioPlayerStateMonitor.registerSelfIntoAudioServiceIfNeeded(mAudioService); AudioRoutesInfo audioRoutes = null; try { audioRoutes = mAudioService.startWatchingRoutes(new IAudioRoutesObserver.Stub() { @Override public void dispatchAudioRoutesChanged(final AudioRoutesInfo newRoutes) { synchronized (mLock) { if (newRoutes.mainType != mAudioRouteMainType) { if ((newRoutes.mainType & (AudioRoutesInfo.MAIN_HEADSET | AudioRoutesInfo.MAIN_HEADPHONES | AudioRoutesInfo.MAIN_USB)) == 0) { // headset was plugged out. mGlobalBluetoothA2dpOn = (newRoutes.bluetoothName != null || mActiveBluetoothDevice != null); } else { // headset was plugged in. mGlobalBluetoothA2dpOn = false; } mAudioRouteMainType = newRoutes.mainType; } // The new audio routes info could be delivered with several seconds delay. // In order to avoid such delay, Bluetooth device info will be updated // via MediaRouterServiceBroadcastReceiver. } } }); } catch (RemoteException e) { Slog.w(TAG, "RemoteException in the audio service."); } IntentFilter intentFilter = new IntentFilter(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED); context.registerReceiverAsUser(mReceiver, UserHandle.ALL, intentFilter, null, null); } 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, uid, 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 boolean isPlaybackActive(IMediaRouterClient client) { if (client == null) { throw new IllegalArgumentException("client must not be null"); } final long token = Binder.clearCallingIdentity(); try { ClientRecord clientRecord; synchronized (mLock) { clientRecord = mAllClientRecords.get(client.asBinder()); } if (clientRecord != null) { return mAudioPlayerStateMonitor.isPlaybackActive(clientRecord.mUid); } return false; } 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 // 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 (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) 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 restoreBluetoothA2dp() { try { boolean a2dpOn; BluetoothDevice btDevice; synchronized (mLock) { a2dpOn = mGlobalBluetoothA2dpOn; btDevice = mActiveBluetoothDevice; } // We don't need to change a2dp status when bluetooth is not connected. if (btDevice != null) { Slog.v(TAG, "restoreBluetoothA2dp(" + a2dpOn + ")"); mAudioService.setBluetoothA2dpOn(a2dpOn); } } catch (RemoteException e) { Slog.w(TAG, "RemoteException while calling setBluetoothA2dpOn."); } } void restoreRoute(int uid) { ClientRecord clientRecord = null; synchronized (mLock) { UserRecord userRecord = mUserRecords.get(UserHandle.getUserId(uid)); if (userRecord != null && userRecord.mClientRecords != null) { for (ClientRecord cr : userRecord.mClientRecords) { if (validatePackageName(uid, cr.mPackageName)) { clientRecord = cr; break; } } } } if (clientRecord != null) { try { clientRecord.mClient.onRestoreRoute(); } catch (RemoteException e) { Slog.w(TAG, "Failed to call onRestoreRoute. Client probably died."); } } else { restoreBluetoothA2dp(); } } 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 uid, 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, uid, 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.equals(routeId, oldRouteId)) { if (DEBUG) { Slog.d(TAG, clientRecord + ": Set selected route, routeId=" + routeId + ", oldRouteId=" + oldRouteId + ", explicit=" + explicit); } clientRecord.mSelectedRouteId = routeId; // Only let the system connect to new global routes for now. // A similar check exists in the display manager for wifi display. if (explicit && clientRecord.mTrusted) { if (oldRouteId != null) { clientRecord.mUserRecord.mHandler.obtainMessage( UserHandler.MSG_UNSELECT_ROUTE, oldRouteId).sendToTarget(); } if (routeId != null) { 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; } final class MediaRouterServiceBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED)) { BluetoothDevice btDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); synchronized (mLock) { mActiveBluetoothDevice = btDevice; mGlobalBluetoothA2dpOn = btDevice != null; } } } } /** * 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 mUid; 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 uid, int pid, String packageName, boolean trusted) { mUserRecord = userRecord; mClient = client; mUid = uid; 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.mRouterState : null; } 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 mRouterState; 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 + "mRouterState=" + mRouterState); 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. *

* 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 mSelectedRouteRecord; 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 + "mSelectedRouteRecord=" + mSelectedRouteRecord); 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; unselectSelectedRoute(); 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 && (mSelectedRouteRecord == null || !routeId.equals(mSelectedRouteRecord.getUniqueId()))) { RouteRecord routeRecord = findRouteRecord(routeId); if (routeRecord != null) { unselectSelectedRoute(); Slog.i(TAG, "Selected route:" + routeRecord); mSelectedRouteRecord = routeRecord; checkSelectedRouteState(); routeRecord.getProvider().setSelectedDisplay(routeRecord.getDescriptorId()); scheduleUpdateClientState(); } } } private void unselectRoute(String routeId) { if (routeId != null && mSelectedRouteRecord != null && routeId.equals(mSelectedRouteRecord.getUniqueId())) { unselectSelectedRoute(); } } private void unselectSelectedRoute() { if (mSelectedRouteRecord != null) { Slog.i(TAG, "Unselected route:" + mSelectedRouteRecord); mSelectedRouteRecord.getProvider().setSelectedDisplay(null); mSelectedRouteRecord = null; checkSelectedRouteState(); scheduleUpdateClientState(); } } private void requestSetVolume(String routeId, int volume) { if (mSelectedRouteRecord != null && routeId.equals(mSelectedRouteRecord.getUniqueId())) { mSelectedRouteRecord.getProvider().setDisplayVolume(volume); } } private void requestUpdateVolume(String routeId, int direction) { if (mSelectedRouteRecord != null && routeId.equals(mSelectedRouteRecord.getUniqueId())) { mSelectedRouteRecord.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); checkSelectedRouteState(); 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)) { checkSelectedRouteState(); scheduleUpdateClientState(); } } } /** * This function is called whenever the state of the selected route may have changed. * It checks the state and updates timeouts or unselects the route as appropriate. */ private void checkSelectedRouteState() { // Unschedule timeouts when the route is unselected. if (mSelectedRouteRecord == null) { mConnectionPhase = PHASE_NOT_AVAILABLE; updateConnectionTimeout(0); return; } // Ensure that the route is still present and enabled. if (!mSelectedRouteRecord.isValid() || !mSelectedRouteRecord.isEnabled()) { updateConnectionTimeout(TIMEOUT_REASON_NOT_AVAILABLE); return; } // Make sure we haven't lost our connection. final int oldPhase = mConnectionPhase; mConnectionPhase = getConnectionPhase(mSelectedRouteRecord.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 route: " + mSelectedRouteRecord); } updateConnectionTimeout(0); break; case PHASE_CONNECTING: if (oldPhase != PHASE_CONNECTING) { Slog.i(TAG, "Connecting to route: " + mSelectedRouteRecord); } 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 || mSelectedRouteRecord == 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, "Selected route no longer available: " + mSelectedRouteRecord); break; case TIMEOUT_REASON_CONNECTION_LOST: Slog.i(TAG, "Selected route connection lost: " + mSelectedRouteRecord); break; case TIMEOUT_REASON_WAITING_FOR_CONNECTING: Slog.i(TAG, "Selected route timed out while waiting for " + "connection attempt to begin after " + (SystemClock.uptimeMillis() - mConnectionTimeoutStartTime) + " ms: " + mSelectedRouteRecord); break; case TIMEOUT_REASON_WAITING_FOR_CONNECTED: Slog.i(TAG, "Selected route timed out while connecting after " + (SystemClock.uptimeMillis() - mConnectionTimeoutStartTime) + " ms: " + mSelectedRouteRecord); break; } mConnectionTimeoutReason = 0; unselectSelectedRoute(); } private void scheduleUpdateClientState() { if (!mClientStateUpdateScheduled) { mClientStateUpdateScheduled = true; sendEmptyMessage(MSG_UPDATE_CLIENT_STATE); } } private void updateClientState() { mClientStateUpdateScheduled = false; // Build a new client state for trusted clients. MediaRouterClientState routerState = new MediaRouterClientState(); final int providerCount = mProviderRecords.size(); for (int i = 0; i < providerCount; i++) { mProviderRecords.get(i).appendClientState(routerState); } try { synchronized (mService.mLock) { // Update the UserRecord. mUserRecord.mRouterState = routerState; // 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) { Slog.w(TAG, "Failed to call onStateChanged. 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.equals(mMutableInfo.name, name)) { mMutableInfo.name = name; changed = true; } final String description = computeDescription(descriptor); if (!Objects.equals(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; } } } }