blob: 3eb732157a44daecf73468d733a14d98fe15510b [file] [log] [blame]
Jeff Brown69b07162013-11-07 00:30:16 -08001/*
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
17package com.android.server.media;
18
Jeff Sharkeyfe9a53b2017-03-31 14:08:23 -060019import com.android.internal.util.DumpUtils;
Jeff Brown69b07162013-11-07 00:30:16 -080020import com.android.server.Watchdog;
21
Sungsoo Lim2afdbc42017-11-01 13:45:59 +090022import android.annotation.NonNull;
Jeff Brown69b07162013-11-07 00:30:16 -080023import android.app.ActivityManager;
Sungsoo Lim7dafa9a2017-12-07 20:14:24 +090024import android.bluetooth.BluetoothA2dp;
25import android.bluetooth.BluetoothDevice;
26import android.bluetooth.BluetoothProfile;
Jeff Brown69b07162013-11-07 00:30:16 -080027import android.content.BroadcastReceiver;
28import android.content.Context;
29import android.content.Intent;
30import android.content.IntentFilter;
31import android.content.pm.PackageManager;
Sungsoo Lim875e6972017-11-03 02:22:35 +000032import android.media.AudioPlaybackConfiguration;
Sungsoob3658562017-05-22 17:10:44 +090033import android.media.AudioRoutesInfo;
Jeff Brown69b07162013-11-07 00:30:16 -080034import android.media.AudioSystem;
Sungsoob3658562017-05-22 17:10:44 +090035import android.media.IAudioRoutesObserver;
36import android.media.IAudioService;
Jeff Brown69b07162013-11-07 00:30:16 -080037import android.media.IMediaRouterClient;
38import android.media.IMediaRouterService;
39import android.media.MediaRouter;
40import android.media.MediaRouterClientState;
41import android.media.RemoteDisplayState;
42import android.media.RemoteDisplayState.RemoteDisplayInfo;
43import android.os.Binder;
44import android.os.Handler;
45import android.os.IBinder;
46import android.os.Looper;
47import android.os.Message;
48import android.os.RemoteException;
Sungsoob3658562017-05-22 17:10:44 +090049import android.os.ServiceManager;
Jeff Brown69b07162013-11-07 00:30:16 -080050import android.os.SystemClock;
Sungsoob3658562017-05-22 17:10:44 +090051import android.os.UserHandle;
Jeff Brown69b07162013-11-07 00:30:16 -080052import android.text.TextUtils;
53import android.util.ArrayMap;
Sungsoob3658562017-05-22 17:10:44 +090054import android.util.IntArray;
Jeff Brown69b07162013-11-07 00:30:16 -080055import android.util.Log;
56import android.util.Slog;
57import android.util.SparseArray;
58import android.util.TimeUtils;
59
60import java.io.FileDescriptor;
61import java.io.PrintWriter;
62import java.util.ArrayList;
63import java.util.Collections;
64import java.util.List;
Kenny Roote6585b32013-12-13 12:00:26 -080065import java.util.Objects;
Jeff Brown69b07162013-11-07 00:30:16 -080066
67/**
68 * Provides a mechanism for discovering media routes and manages media playback
69 * behalf of applications.
70 * <p>
71 * Currently supports discovering remote displays via remote display provider
72 * services that have been registered by applications.
73 * </p>
74 */
75public final class MediaRouterService extends IMediaRouterService.Stub
76 implements Watchdog.Monitor {
77 private static final String TAG = "MediaRouterService";
78 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
79
80 /**
81 * Timeout in milliseconds for a selected route to transition from a
82 * disconnected state to a connecting state. If we don't observe any
83 * progress within this interval, then we will give up and unselect the route.
84 */
85 static final long CONNECTING_TIMEOUT = 5000;
86
87 /**
88 * Timeout in milliseconds for a selected route to transition from a
89 * connecting state to a connected state. If we don't observe any
90 * progress within this interval, then we will give up and unselect the route.
91 */
92 static final long CONNECTED_TIMEOUT = 60000;
93
94 private final Context mContext;
95
96 // State guarded by mLock.
97 private final Object mLock = new Object();
Sungsoo Limcc4b8a42018-05-24 00:28:27 +090098 private final SparseArray<UserRecord> mUserRecords = new SparseArray<>();
99 private final ArrayMap<IBinder, ClientRecord> mAllClientRecords = new ArrayMap<>();
Jeff Brown69b07162013-11-07 00:30:16 -0800100 private int mCurrentUserId = -1;
Sungsoob3658562017-05-22 17:10:44 +0900101 private final IAudioService mAudioService;
Sungsoo Lim875e6972017-11-03 02:22:35 +0000102 private final AudioPlayerStateMonitor mAudioPlayerStateMonitor;
103 private final Handler mHandler = new Handler();
Sungsoo Lim2afdbc42017-11-01 13:45:59 +0900104 private final IntArray mActivePlayerMinPriorityQueue = new IntArray();
105 private final IntArray mActivePlayerUidMinPriorityQueue = new IntArray();
Jeff Brown69b07162013-11-07 00:30:16 -0800106
Sungsoo Lim7dafa9a2017-12-07 20:14:24 +0900107 private final BroadcastReceiver mReceiver = new MediaRouterServiceBroadcastReceiver();
Sungsoo Limcc4b8a42018-05-24 00:28:27 +0900108 BluetoothDevice mActiveBluetoothDevice;
Sungsoo Lim7dafa9a2017-12-07 20:14:24 +0900109 int mAudioRouteMainType = AudioRoutesInfo.MAIN_SPEAKER;
110 boolean mGlobalBluetoothA2dpOn = false;
111
Jeff Brown69b07162013-11-07 00:30:16 -0800112 public MediaRouterService(Context context) {
113 mContext = context;
114 Watchdog.getInstance().addMonitor(this);
Sungsoob3658562017-05-22 17:10:44 +0900115
116 mAudioService = IAudioService.Stub.asInterface(
117 ServiceManager.getService(Context.AUDIO_SERVICE));
Sungsoo Lim875e6972017-11-03 02:22:35 +0000118 mAudioPlayerStateMonitor = AudioPlayerStateMonitor.getInstance();
119 mAudioPlayerStateMonitor.registerListener(
Sungsoo Lim2afdbc42017-11-01 13:45:59 +0900120 new AudioPlayerStateMonitor.OnAudioPlayerActiveStateChangedListener() {
Sungsoo Lim875e6972017-11-03 02:22:35 +0000121 static final long WAIT_MS = 500;
122 final Runnable mRestoreBluetoothA2dpRunnable = new Runnable() {
123 @Override
124 public void run() {
125 restoreBluetoothA2dp();
126 }
127 };
128
Sungsoob3658562017-05-22 17:10:44 +0900129 @Override
Sungsoo Lim2afdbc42017-11-01 13:45:59 +0900130 public void onAudioPlayerActiveStateChanged(
131 @NonNull AudioPlaybackConfiguration config, boolean isRemoved) {
132 final boolean active = !isRemoved && config.isActive();
133 final int pii = config.getPlayerInterfaceId();
134 final int uid = config.getClientUid();
135
136 final int idx = mActivePlayerMinPriorityQueue.indexOf(pii);
137 // Keep the latest active player and its uid at the end of the queue.
138 if (idx >= 0) {
139 mActivePlayerMinPriorityQueue.remove(idx);
140 mActivePlayerUidMinPriorityQueue.remove(idx);
141 }
142
Sungsoo Lim875e6972017-11-03 02:22:35 +0000143 int restoreUid = -1;
Sungsoob3658562017-05-22 17:10:44 +0900144 if (active) {
Sungsoo Lim2afdbc42017-11-01 13:45:59 +0900145 mActivePlayerMinPriorityQueue.add(config.getPlayerInterfaceId());
146 mActivePlayerUidMinPriorityQueue.add(uid);
Sungsoo Lim875e6972017-11-03 02:22:35 +0000147 restoreUid = uid;
Sungsoo Lim2afdbc42017-11-01 13:45:59 +0900148 } else if (mActivePlayerUidMinPriorityQueue.size() > 0) {
149 restoreUid = mActivePlayerUidMinPriorityQueue.get(
150 mActivePlayerUidMinPriorityQueue.size() - 1);
Sungsoo Lim875e6972017-11-03 02:22:35 +0000151 }
152
153 mHandler.removeCallbacks(mRestoreBluetoothA2dpRunnable);
154 if (restoreUid >= 0) {
155 restoreRoute(restoreUid);
156 if (DEBUG) {
Sungsoo Lim2afdbc42017-11-01 13:45:59 +0900157 Slog.d(TAG, "onAudioPlayerActiveStateChanged: " + "uid=" + uid
158 + ", active=" + active + ", restoreUid=" + restoreUid);
Sungsoo Lim875e6972017-11-03 02:22:35 +0000159 }
160 } else {
161 mHandler.postDelayed(mRestoreBluetoothA2dpRunnable, WAIT_MS);
162 if (DEBUG) {
Sungsoo Lim90f4c8952017-11-21 13:12:24 +0900163 Slog.d(TAG, "onAudioPlayerActiveStateChanged: " + "uid=" + uid
Sungsoo Lim2afdbc42017-11-01 13:45:59 +0900164 + ", active=" + active + ", delaying");
Sungsoob3658562017-05-22 17:10:44 +0900165 }
166 }
167 }
Sungsoo Lim875e6972017-11-03 02:22:35 +0000168 }, mHandler);
169 mAudioPlayerStateMonitor.registerSelfIntoAudioServiceIfNeeded(mAudioService);
170
Sungsoob3658562017-05-22 17:10:44 +0900171 AudioRoutesInfo audioRoutes = null;
172 try {
173 audioRoutes = mAudioService.startWatchingRoutes(new IAudioRoutesObserver.Stub() {
174 @Override
175 public void dispatchAudioRoutesChanged(final AudioRoutesInfo newRoutes) {
Sungsoo Lim76512a32017-08-24 10:25:06 +0900176 synchronized (mLock) {
Sungsoo Lim7dafa9a2017-12-07 20:14:24 +0900177 if (newRoutes.mainType != mAudioRouteMainType) {
Sungsoo Lim76512a32017-08-24 10:25:06 +0900178 if ((newRoutes.mainType & (AudioRoutesInfo.MAIN_HEADSET
179 | AudioRoutesInfo.MAIN_HEADPHONES
180 | AudioRoutesInfo.MAIN_USB)) == 0) {
181 // headset was plugged out.
Sungsoo Limcc4b8a42018-05-24 00:28:27 +0900182 mGlobalBluetoothA2dpOn = (newRoutes.bluetoothName != null
183 || mActiveBluetoothDevice != null);
Sungsoo Lim76512a32017-08-24 10:25:06 +0900184 } else {
185 // headset was plugged in.
186 mGlobalBluetoothA2dpOn = false;
187 }
Sungsoo Lim7dafa9a2017-12-07 20:14:24 +0900188 mAudioRouteMainType = newRoutes.mainType;
Sungsoo Lim76512a32017-08-24 10:25:06 +0900189 }
Sungsoo Lim7dafa9a2017-12-07 20:14:24 +0900190 // The new audio routes info could be delivered with several seconds delay.
191 // In order to avoid such delay, Bluetooth device info will be updated
192 // via MediaRouterServiceBroadcastReceiver.
Sungsoo Lim76512a32017-08-24 10:25:06 +0900193 }
Sungsoob3658562017-05-22 17:10:44 +0900194 }
195 });
196 } catch (RemoteException e) {
197 Slog.w(TAG, "RemoteException in the audio service.");
198 }
Sungsoo Lim7dafa9a2017-12-07 20:14:24 +0900199
Sungsoo Limcc4b8a42018-05-24 00:28:27 +0900200 IntentFilter intentFilter = new IntentFilter(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED);
Sungsoo Lim7dafa9a2017-12-07 20:14:24 +0900201 context.registerReceiverAsUser(mReceiver, UserHandle.ALL, intentFilter, null, null);
Jeff Brown69b07162013-11-07 00:30:16 -0800202 }
203
204 public void systemRunning() {
205 IntentFilter filter = new IntentFilter(Intent.ACTION_USER_SWITCHED);
206 mContext.registerReceiver(new BroadcastReceiver() {
207 @Override
208 public void onReceive(Context context, Intent intent) {
209 if (intent.getAction().equals(Intent.ACTION_USER_SWITCHED)) {
210 switchUser();
211 }
212 }
213 }, filter);
214
215 switchUser();
216 }
217
218 @Override
219 public void monitor() {
220 synchronized (mLock) { /* check for deadlock */ }
221 }
222
223 // Binder call
224 @Override
225 public void registerClientAsUser(IMediaRouterClient client, String packageName, int userId) {
226 if (client == null) {
227 throw new IllegalArgumentException("client must not be null");
228 }
229
230 final int uid = Binder.getCallingUid();
231 if (!validatePackageName(uid, packageName)) {
232 throw new SecurityException("packageName must match the calling uid");
233 }
234
235 final int pid = Binder.getCallingPid();
236 final int resolvedUserId = ActivityManager.handleIncomingUser(pid, uid, userId,
237 false /*allowAll*/, true /*requireFull*/, "registerClientAsUser", packageName);
Jeff Brownaf574182013-11-14 18:16:08 -0800238 final boolean trusted = mContext.checkCallingOrSelfPermission(
239 android.Manifest.permission.CONFIGURE_WIFI_DISPLAY) ==
240 PackageManager.PERMISSION_GRANTED;
Jeff Brown69b07162013-11-07 00:30:16 -0800241 final long token = Binder.clearCallingIdentity();
242 try {
243 synchronized (mLock) {
Sungsoob3658562017-05-22 17:10:44 +0900244 registerClientLocked(client, uid, pid, packageName, resolvedUserId, trusted);
Jeff Brown69b07162013-11-07 00:30:16 -0800245 }
246 } finally {
247 Binder.restoreCallingIdentity(token);
248 }
249 }
250
251 // Binder call
252 @Override
253 public void unregisterClient(IMediaRouterClient client) {
254 if (client == null) {
255 throw new IllegalArgumentException("client must not be null");
256 }
257
258 final long token = Binder.clearCallingIdentity();
259 try {
260 synchronized (mLock) {
261 unregisterClientLocked(client, false);
262 }
263 } finally {
264 Binder.restoreCallingIdentity(token);
265 }
266 }
267
268 // Binder call
269 @Override
270 public MediaRouterClientState getState(IMediaRouterClient client) {
271 if (client == null) {
272 throw new IllegalArgumentException("client must not be null");
273 }
274
275 final long token = Binder.clearCallingIdentity();
276 try {
277 synchronized (mLock) {
278 return getStateLocked(client);
279 }
280 } finally {
281 Binder.restoreCallingIdentity(token);
282 }
283 }
284
285 // Binder call
286 @Override
Sungsoob3658562017-05-22 17:10:44 +0900287 public boolean isPlaybackActive(IMediaRouterClient client) {
288 if (client == null) {
289 throw new IllegalArgumentException("client must not be null");
290 }
291
292 final long token = Binder.clearCallingIdentity();
293 try {
Sungsoo Lim875e6972017-11-03 02:22:35 +0000294 ClientRecord clientRecord;
Sungsoob3658562017-05-22 17:10:44 +0900295 synchronized (mLock) {
Sungsoo Lim875e6972017-11-03 02:22:35 +0000296 clientRecord = mAllClientRecords.get(client.asBinder());
Sungsoob3658562017-05-22 17:10:44 +0900297 }
Sungsoo Lim875e6972017-11-03 02:22:35 +0000298 if (clientRecord != null) {
299 return mAudioPlayerStateMonitor.isPlaybackActive(clientRecord.mUid);
300 }
301 return false;
Sungsoob3658562017-05-22 17:10:44 +0900302 } finally {
303 Binder.restoreCallingIdentity(token);
304 }
305 }
306
307 // Binder call
308 @Override
Jeff Brown69b07162013-11-07 00:30:16 -0800309 public void setDiscoveryRequest(IMediaRouterClient client,
310 int routeTypes, boolean activeScan) {
311 if (client == null) {
312 throw new IllegalArgumentException("client must not be null");
313 }
314
315 final long token = Binder.clearCallingIdentity();
316 try {
317 synchronized (mLock) {
318 setDiscoveryRequestLocked(client, routeTypes, activeScan);
319 }
320 } finally {
321 Binder.restoreCallingIdentity(token);
322 }
323 }
324
325 // Binder call
326 // A null routeId means that the client wants to unselect its current route.
327 // The explicit flag indicates whether the change was explicitly requested by the
328 // user or the application which may cause changes to propagate out to the rest
Sungsood103e562017-03-09 15:35:07 +0900329 // of the system. Should be false when the change is in response to a new
Jeff Brown69b07162013-11-07 00:30:16 -0800330 // selected route or a default selection.
331 @Override
332 public void setSelectedRoute(IMediaRouterClient client, String routeId, boolean explicit) {
333 if (client == null) {
334 throw new IllegalArgumentException("client must not be null");
335 }
336
337 final long token = Binder.clearCallingIdentity();
338 try {
339 synchronized (mLock) {
340 setSelectedRouteLocked(client, routeId, explicit);
341 }
342 } finally {
343 Binder.restoreCallingIdentity(token);
344 }
345 }
346
347 // Binder call
348 @Override
349 public void requestSetVolume(IMediaRouterClient client, String routeId, int volume) {
350 if (client == null) {
351 throw new IllegalArgumentException("client must not be null");
352 }
353 if (routeId == null) {
354 throw new IllegalArgumentException("routeId must not be null");
355 }
356
357 final long token = Binder.clearCallingIdentity();
358 try {
359 synchronized (mLock) {
360 requestSetVolumeLocked(client, routeId, volume);
361 }
362 } finally {
363 Binder.restoreCallingIdentity(token);
364 }
365 }
366
367 // Binder call
368 @Override
369 public void requestUpdateVolume(IMediaRouterClient client, String routeId, int direction) {
370 if (client == null) {
371 throw new IllegalArgumentException("client must not be null");
372 }
373 if (routeId == null) {
374 throw new IllegalArgumentException("routeId must not be null");
375 }
376
377 final long token = Binder.clearCallingIdentity();
378 try {
379 synchronized (mLock) {
380 requestUpdateVolumeLocked(client, routeId, direction);
381 }
382 } finally {
383 Binder.restoreCallingIdentity(token);
384 }
385 }
386
387 // Binder call
388 @Override
389 public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) {
Jeff Sharkeyfe9a53b2017-03-31 14:08:23 -0600390 if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
Jeff Brown69b07162013-11-07 00:30:16 -0800391
392 pw.println("MEDIA ROUTER SERVICE (dumpsys media_router)");
393 pw.println();
394 pw.println("Global state");
395 pw.println(" mCurrentUserId=" + mCurrentUserId);
396
397 synchronized (mLock) {
398 final int count = mUserRecords.size();
399 for (int i = 0; i < count; i++) {
400 UserRecord userRecord = mUserRecords.valueAt(i);
401 pw.println();
402 userRecord.dump(pw, "");
403 }
404 }
405 }
406
Sungsoob3658562017-05-22 17:10:44 +0900407 void restoreBluetoothA2dp() {
408 try {
Sungsoo Limcc4b8a42018-05-24 00:28:27 +0900409 boolean a2dpOn;
410 BluetoothDevice btDevice;
Sungsoo Lim76512a32017-08-24 10:25:06 +0900411 synchronized (mLock) {
412 a2dpOn = mGlobalBluetoothA2dpOn;
Sungsoo Limcc4b8a42018-05-24 00:28:27 +0900413 btDevice = mActiveBluetoothDevice;
Sungsoo Lim76512a32017-08-24 10:25:06 +0900414 }
Sungsoo Lim90f4c8952017-11-21 13:12:24 +0900415 // We don't need to change a2dp status when bluetooth is not connected.
Sungsoo Limcc4b8a42018-05-24 00:28:27 +0900416 if (btDevice != null) {
Sungsoo Limc3b52952018-06-11 12:55:25 +0900417 if (DEBUG) {
418 Slog.d(TAG, "restoreBluetoothA2dp(" + a2dpOn + ")");
419 }
Sungsoo Lim90f4c8952017-11-21 13:12:24 +0900420 mAudioService.setBluetoothA2dpOn(a2dpOn);
421 }
Sungsoob3658562017-05-22 17:10:44 +0900422 } catch (RemoteException e) {
423 Slog.w(TAG, "RemoteException while calling setBluetoothA2dpOn.");
424 }
425 }
426
427 void restoreRoute(int uid) {
428 ClientRecord clientRecord = null;
Sungsoo Lim76512a32017-08-24 10:25:06 +0900429 synchronized (mLock) {
430 UserRecord userRecord = mUserRecords.get(UserHandle.getUserId(uid));
431 if (userRecord != null && userRecord.mClientRecords != null) {
432 for (ClientRecord cr : userRecord.mClientRecords) {
433 if (validatePackageName(uid, cr.mPackageName)) {
434 clientRecord = cr;
435 break;
436 }
Sungsoob3658562017-05-22 17:10:44 +0900437 }
438 }
439 }
440 if (clientRecord != null) {
441 try {
442 clientRecord.mClient.onRestoreRoute();
443 } catch (RemoteException e) {
444 Slog.w(TAG, "Failed to call onRestoreRoute. Client probably died.");
445 }
446 } else {
447 restoreBluetoothA2dp();
448 }
449 }
450
Jeff Brown69b07162013-11-07 00:30:16 -0800451 void switchUser() {
452 synchronized (mLock) {
453 int userId = ActivityManager.getCurrentUser();
454 if (mCurrentUserId != userId) {
455 final int oldUserId = mCurrentUserId;
456 mCurrentUserId = userId; // do this first
457
458 UserRecord oldUser = mUserRecords.get(oldUserId);
459 if (oldUser != null) {
460 oldUser.mHandler.sendEmptyMessage(UserHandler.MSG_STOP);
461 disposeUserIfNeededLocked(oldUser); // since no longer current user
462 }
463
464 UserRecord newUser = mUserRecords.get(userId);
465 if (newUser != null) {
466 newUser.mHandler.sendEmptyMessage(UserHandler.MSG_START);
467 }
468 }
469 }
470 }
471
472 void clientDied(ClientRecord clientRecord) {
473 synchronized (mLock) {
474 unregisterClientLocked(clientRecord.mClient, true);
475 }
476 }
477
478 private void registerClientLocked(IMediaRouterClient client,
Sungsoob3658562017-05-22 17:10:44 +0900479 int uid, int pid, String packageName, int userId, boolean trusted) {
Jeff Brown69b07162013-11-07 00:30:16 -0800480 final IBinder binder = client.asBinder();
481 ClientRecord clientRecord = mAllClientRecords.get(binder);
482 if (clientRecord == null) {
483 boolean newUser = false;
484 UserRecord userRecord = mUserRecords.get(userId);
485 if (userRecord == null) {
486 userRecord = new UserRecord(userId);
487 newUser = true;
488 }
Sungsoob3658562017-05-22 17:10:44 +0900489 clientRecord = new ClientRecord(userRecord, client, uid, pid, packageName, trusted);
Jeff Brown69b07162013-11-07 00:30:16 -0800490 try {
491 binder.linkToDeath(clientRecord, 0);
492 } catch (RemoteException ex) {
493 throw new RuntimeException("Media router client died prematurely.", ex);
494 }
495
496 if (newUser) {
497 mUserRecords.put(userId, userRecord);
498 initializeUserLocked(userRecord);
499 }
500
501 userRecord.mClientRecords.add(clientRecord);
502 mAllClientRecords.put(binder, clientRecord);
503 initializeClientLocked(clientRecord);
504 }
505 }
506
507 private void unregisterClientLocked(IMediaRouterClient client, boolean died) {
508 ClientRecord clientRecord = mAllClientRecords.remove(client.asBinder());
509 if (clientRecord != null) {
510 UserRecord userRecord = clientRecord.mUserRecord;
511 userRecord.mClientRecords.remove(clientRecord);
512 disposeClientLocked(clientRecord, died);
513 disposeUserIfNeededLocked(userRecord); // since client removed from user
514 }
515 }
516
517 private MediaRouterClientState getStateLocked(IMediaRouterClient client) {
518 ClientRecord clientRecord = mAllClientRecords.get(client.asBinder());
519 if (clientRecord != null) {
Jeff Brownaf574182013-11-14 18:16:08 -0800520 return clientRecord.getState();
Jeff Brown69b07162013-11-07 00:30:16 -0800521 }
522 return null;
523 }
524
525 private void setDiscoveryRequestLocked(IMediaRouterClient client,
526 int routeTypes, boolean activeScan) {
527 final IBinder binder = client.asBinder();
528 ClientRecord clientRecord = mAllClientRecords.get(binder);
529 if (clientRecord != null) {
Jeff Brownaf574182013-11-14 18:16:08 -0800530 // Only let the system discover remote display routes for now.
531 if (!clientRecord.mTrusted) {
532 routeTypes &= ~MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY;
533 }
534
Jeff Brown69b07162013-11-07 00:30:16 -0800535 if (clientRecord.mRouteTypes != routeTypes
536 || clientRecord.mActiveScan != activeScan) {
537 if (DEBUG) {
538 Slog.d(TAG, clientRecord + ": Set discovery request, routeTypes=0x"
539 + Integer.toHexString(routeTypes) + ", activeScan=" + activeScan);
540 }
541 clientRecord.mRouteTypes = routeTypes;
542 clientRecord.mActiveScan = activeScan;
543 clientRecord.mUserRecord.mHandler.sendEmptyMessage(
544 UserHandler.MSG_UPDATE_DISCOVERY_REQUEST);
545 }
546 }
547 }
548
549 private void setSelectedRouteLocked(IMediaRouterClient client,
550 String routeId, boolean explicit) {
551 ClientRecord clientRecord = mAllClientRecords.get(client.asBinder());
552 if (clientRecord != null) {
553 final String oldRouteId = clientRecord.mSelectedRouteId;
Kenny Roote6585b32013-12-13 12:00:26 -0800554 if (!Objects.equals(routeId, oldRouteId)) {
Jeff Brown69b07162013-11-07 00:30:16 -0800555 if (DEBUG) {
556 Slog.d(TAG, clientRecord + ": Set selected route, routeId=" + routeId
557 + ", oldRouteId=" + oldRouteId
558 + ", explicit=" + explicit);
559 }
560
561 clientRecord.mSelectedRouteId = routeId;
Sungsood103e562017-03-09 15:35:07 +0900562 // Only let the system connect to new global routes for now.
563 // A similar check exists in the display manager for wifi display.
564 if (explicit && clientRecord.mTrusted) {
Jeff Brown69b07162013-11-07 00:30:16 -0800565 if (oldRouteId != null) {
566 clientRecord.mUserRecord.mHandler.obtainMessage(
567 UserHandler.MSG_UNSELECT_ROUTE, oldRouteId).sendToTarget();
568 }
Sungsood103e562017-03-09 15:35:07 +0900569 if (routeId != null) {
Jeff Brown69b07162013-11-07 00:30:16 -0800570 clientRecord.mUserRecord.mHandler.obtainMessage(
571 UserHandler.MSG_SELECT_ROUTE, routeId).sendToTarget();
572 }
573 }
574 }
575 }
576 }
577
578 private void requestSetVolumeLocked(IMediaRouterClient client,
579 String routeId, int volume) {
580 final IBinder binder = client.asBinder();
581 ClientRecord clientRecord = mAllClientRecords.get(binder);
582 if (clientRecord != null) {
583 clientRecord.mUserRecord.mHandler.obtainMessage(
584 UserHandler.MSG_REQUEST_SET_VOLUME, volume, 0, routeId).sendToTarget();
585 }
586 }
587
588 private void requestUpdateVolumeLocked(IMediaRouterClient client,
589 String routeId, int direction) {
590 final IBinder binder = client.asBinder();
591 ClientRecord clientRecord = mAllClientRecords.get(binder);
592 if (clientRecord != null) {
593 clientRecord.mUserRecord.mHandler.obtainMessage(
594 UserHandler.MSG_REQUEST_UPDATE_VOLUME, direction, 0, routeId).sendToTarget();
595 }
596 }
597
598 private void initializeUserLocked(UserRecord userRecord) {
599 if (DEBUG) {
600 Slog.d(TAG, userRecord + ": Initialized");
601 }
602 if (userRecord.mUserId == mCurrentUserId) {
603 userRecord.mHandler.sendEmptyMessage(UserHandler.MSG_START);
604 }
605 }
606
607 private void disposeUserIfNeededLocked(UserRecord userRecord) {
608 // If there are no records left and the user is no longer current then go ahead
609 // and purge the user record and all of its associated state. If the user is current
610 // then leave it alone since we might be connected to a route or want to query
611 // the same route information again soon.
612 if (userRecord.mUserId != mCurrentUserId
613 && userRecord.mClientRecords.isEmpty()) {
614 if (DEBUG) {
615 Slog.d(TAG, userRecord + ": Disposed");
616 }
617 mUserRecords.remove(userRecord.mUserId);
618 // Note: User already stopped (by switchUser) so no need to send stop message here.
619 }
620 }
621
622 private void initializeClientLocked(ClientRecord clientRecord) {
623 if (DEBUG) {
624 Slog.d(TAG, clientRecord + ": Registered");
625 }
626 }
627
628 private void disposeClientLocked(ClientRecord clientRecord, boolean died) {
629 if (DEBUG) {
630 if (died) {
631 Slog.d(TAG, clientRecord + ": Died!");
632 } else {
633 Slog.d(TAG, clientRecord + ": Unregistered");
634 }
635 }
636 if (clientRecord.mRouteTypes != 0 || clientRecord.mActiveScan) {
637 clientRecord.mUserRecord.mHandler.sendEmptyMessage(
638 UserHandler.MSG_UPDATE_DISCOVERY_REQUEST);
639 }
640 clientRecord.dispose();
641 }
642
643 private boolean validatePackageName(int uid, String packageName) {
644 if (packageName != null) {
645 String[] packageNames = mContext.getPackageManager().getPackagesForUid(uid);
646 if (packageNames != null) {
647 for (String n : packageNames) {
648 if (n.equals(packageName)) {
649 return true;
650 }
651 }
652 }
653 }
654 return false;
655 }
656
Sungsoo Lim7dafa9a2017-12-07 20:14:24 +0900657 final class MediaRouterServiceBroadcastReceiver extends BroadcastReceiver {
658 @Override
659 public void onReceive(Context context, Intent intent) {
Sungsoo Limcc4b8a42018-05-24 00:28:27 +0900660 if (intent.getAction().equals(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED)) {
661 BluetoothDevice btDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
662 synchronized (mLock) {
663 mActiveBluetoothDevice = btDevice;
664 mGlobalBluetoothA2dpOn = btDevice != null;
Sungsoo Lim7dafa9a2017-12-07 20:14:24 +0900665 }
666 }
667 }
668 }
669
Jeff Brown69b07162013-11-07 00:30:16 -0800670 /**
671 * Information about a particular client of the media router.
672 * The contents of this object is guarded by mLock.
673 */
674 final class ClientRecord implements DeathRecipient {
675 public final UserRecord mUserRecord;
676 public final IMediaRouterClient mClient;
Sungsoob3658562017-05-22 17:10:44 +0900677 public final int mUid;
Jeff Brown69b07162013-11-07 00:30:16 -0800678 public final int mPid;
679 public final String mPackageName;
Jeff Brownaf574182013-11-14 18:16:08 -0800680 public final boolean mTrusted;
Jeff Brown69b07162013-11-07 00:30:16 -0800681
682 public int mRouteTypes;
683 public boolean mActiveScan;
684 public String mSelectedRouteId;
685
686 public ClientRecord(UserRecord userRecord, IMediaRouterClient client,
Sungsoob3658562017-05-22 17:10:44 +0900687 int uid, int pid, String packageName, boolean trusted) {
Jeff Brown69b07162013-11-07 00:30:16 -0800688 mUserRecord = userRecord;
689 mClient = client;
Sungsoob3658562017-05-22 17:10:44 +0900690 mUid = uid;
Jeff Brown69b07162013-11-07 00:30:16 -0800691 mPid = pid;
692 mPackageName = packageName;
Jeff Brownaf574182013-11-14 18:16:08 -0800693 mTrusted = trusted;
Jeff Brown69b07162013-11-07 00:30:16 -0800694 }
695
696 public void dispose() {
697 mClient.asBinder().unlinkToDeath(this, 0);
698 }
699
700 @Override
701 public void binderDied() {
702 clientDied(this);
703 }
704
Jeff Brownaf574182013-11-14 18:16:08 -0800705 MediaRouterClientState getState() {
Sungsood103e562017-03-09 15:35:07 +0900706 return mTrusted ? mUserRecord.mRouterState : null;
Jeff Brownaf574182013-11-14 18:16:08 -0800707 }
708
Jeff Brown69b07162013-11-07 00:30:16 -0800709 public void dump(PrintWriter pw, String prefix) {
710 pw.println(prefix + this);
711
712 final String indent = prefix + " ";
Jeff Brownaf574182013-11-14 18:16:08 -0800713 pw.println(indent + "mTrusted=" + mTrusted);
Jeff Brown69b07162013-11-07 00:30:16 -0800714 pw.println(indent + "mRouteTypes=0x" + Integer.toHexString(mRouteTypes));
715 pw.println(indent + "mActiveScan=" + mActiveScan);
716 pw.println(indent + "mSelectedRouteId=" + mSelectedRouteId);
717 }
718
719 @Override
720 public String toString() {
721 return "Client " + mPackageName + " (pid " + mPid + ")";
722 }
723 }
724
725 /**
726 * Information about a particular user.
727 * The contents of this object is guarded by mLock.
728 */
729 final class UserRecord {
730 public final int mUserId;
731 public final ArrayList<ClientRecord> mClientRecords = new ArrayList<ClientRecord>();
732 public final UserHandler mHandler;
Sungsood103e562017-03-09 15:35:07 +0900733 public MediaRouterClientState mRouterState;
Jeff Brown69b07162013-11-07 00:30:16 -0800734
735 public UserRecord(int userId) {
736 mUserId = userId;
737 mHandler = new UserHandler(MediaRouterService.this, this);
738 }
739
740 public void dump(final PrintWriter pw, String prefix) {
741 pw.println(prefix + this);
742
743 final String indent = prefix + " ";
744 final int clientCount = mClientRecords.size();
745 if (clientCount != 0) {
746 for (int i = 0; i < clientCount; i++) {
747 mClientRecords.get(i).dump(pw, indent);
748 }
749 } else {
750 pw.println(indent + "<no clients>");
751 }
752
Jeff Brownaf574182013-11-14 18:16:08 -0800753 pw.println(indent + "State");
Sungsood103e562017-03-09 15:35:07 +0900754 pw.println(indent + "mRouterState=" + mRouterState);
Jeff Brownaf574182013-11-14 18:16:08 -0800755
Jeff Brown69b07162013-11-07 00:30:16 -0800756 if (!mHandler.runWithScissors(new Runnable() {
757 @Override
758 public void run() {
759 mHandler.dump(pw, indent);
760 }
761 }, 1000)) {
762 pw.println(indent + "<could not dump handler state>");
763 }
764 }
765
766 @Override
767 public String toString() {
768 return "User " + mUserId;
769 }
770 }
771
772 /**
773 * Media router handler
774 * <p>
775 * Since remote display providers are designed to be single-threaded by nature,
776 * this class encapsulates all of the associated functionality and exports state
777 * to the service as it evolves.
778 * </p><p>
Jeff Brown69b07162013-11-07 00:30:16 -0800779 * This class is currently hardcoded to work with remote display providers but
780 * it is intended to be eventually extended to support more general route providers
781 * similar to the support library media router.
782 * </p>
783 */
784 static final class UserHandler extends Handler
785 implements RemoteDisplayProviderWatcher.Callback,
786 RemoteDisplayProviderProxy.Callback {
787 public static final int MSG_START = 1;
788 public static final int MSG_STOP = 2;
789 public static final int MSG_UPDATE_DISCOVERY_REQUEST = 3;
790 public static final int MSG_SELECT_ROUTE = 4;
791 public static final int MSG_UNSELECT_ROUTE = 5;
792 public static final int MSG_REQUEST_SET_VOLUME = 6;
793 public static final int MSG_REQUEST_UPDATE_VOLUME = 7;
794 private static final int MSG_UPDATE_CLIENT_STATE = 8;
795 private static final int MSG_CONNECTION_TIMED_OUT = 9;
796
797 private static final int TIMEOUT_REASON_NOT_AVAILABLE = 1;
Jeff Brown39ad0e52013-11-11 17:55:08 -0800798 private static final int TIMEOUT_REASON_CONNECTION_LOST = 2;
799 private static final int TIMEOUT_REASON_WAITING_FOR_CONNECTING = 3;
800 private static final int TIMEOUT_REASON_WAITING_FOR_CONNECTED = 4;
801
802 // The relative order of these constants is important and expresses progress
803 // through the process of connecting to a route.
804 private static final int PHASE_NOT_AVAILABLE = -1;
805 private static final int PHASE_NOT_CONNECTED = 0;
806 private static final int PHASE_CONNECTING = 1;
807 private static final int PHASE_CONNECTED = 2;
Jeff Brown69b07162013-11-07 00:30:16 -0800808
809 private final MediaRouterService mService;
810 private final UserRecord mUserRecord;
811 private final RemoteDisplayProviderWatcher mWatcher;
812 private final ArrayList<ProviderRecord> mProviderRecords =
813 new ArrayList<ProviderRecord>();
814 private final ArrayList<IMediaRouterClient> mTempClients =
815 new ArrayList<IMediaRouterClient>();
816
817 private boolean mRunning;
818 private int mDiscoveryMode = RemoteDisplayState.DISCOVERY_MODE_NONE;
Sungsood103e562017-03-09 15:35:07 +0900819 private RouteRecord mSelectedRouteRecord;
Jeff Brown39ad0e52013-11-11 17:55:08 -0800820 private int mConnectionPhase = PHASE_NOT_AVAILABLE;
Jeff Brown69b07162013-11-07 00:30:16 -0800821 private int mConnectionTimeoutReason;
822 private long mConnectionTimeoutStartTime;
823 private boolean mClientStateUpdateScheduled;
824
825 public UserHandler(MediaRouterService service, UserRecord userRecord) {
826 super(Looper.getMainLooper(), null, true);
827 mService = service;
828 mUserRecord = userRecord;
829 mWatcher = new RemoteDisplayProviderWatcher(service.mContext, this,
830 this, mUserRecord.mUserId);
831 }
832
833 @Override
834 public void handleMessage(Message msg) {
835 switch (msg.what) {
836 case MSG_START: {
837 start();
838 break;
839 }
840 case MSG_STOP: {
841 stop();
842 break;
843 }
844 case MSG_UPDATE_DISCOVERY_REQUEST: {
845 updateDiscoveryRequest();
846 break;
847 }
848 case MSG_SELECT_ROUTE: {
849 selectRoute((String)msg.obj);
850 break;
851 }
852 case MSG_UNSELECT_ROUTE: {
853 unselectRoute((String)msg.obj);
854 break;
855 }
856 case MSG_REQUEST_SET_VOLUME: {
857 requestSetVolume((String)msg.obj, msg.arg1);
858 break;
859 }
860 case MSG_REQUEST_UPDATE_VOLUME: {
861 requestUpdateVolume((String)msg.obj, msg.arg1);
862 break;
863 }
864 case MSG_UPDATE_CLIENT_STATE: {
865 updateClientState();
866 break;
867 }
868 case MSG_CONNECTION_TIMED_OUT: {
869 connectionTimedOut();
870 break;
871 }
872 }
873 }
874
875 public void dump(PrintWriter pw, String prefix) {
876 pw.println(prefix + "Handler");
877
878 final String indent = prefix + " ";
879 pw.println(indent + "mRunning=" + mRunning);
880 pw.println(indent + "mDiscoveryMode=" + mDiscoveryMode);
Sungsood103e562017-03-09 15:35:07 +0900881 pw.println(indent + "mSelectedRouteRecord=" + mSelectedRouteRecord);
Jeff Brown39ad0e52013-11-11 17:55:08 -0800882 pw.println(indent + "mConnectionPhase=" + mConnectionPhase);
Jeff Brown69b07162013-11-07 00:30:16 -0800883 pw.println(indent + "mConnectionTimeoutReason=" + mConnectionTimeoutReason);
884 pw.println(indent + "mConnectionTimeoutStartTime=" + (mConnectionTimeoutReason != 0 ?
885 TimeUtils.formatUptime(mConnectionTimeoutStartTime) : "<n/a>"));
886
887 mWatcher.dump(pw, prefix);
888
889 final int providerCount = mProviderRecords.size();
890 if (providerCount != 0) {
891 for (int i = 0; i < providerCount; i++) {
892 mProviderRecords.get(i).dump(pw, prefix);
893 }
894 } else {
895 pw.println(indent + "<no providers>");
896 }
897 }
898
899 private void start() {
900 if (!mRunning) {
901 mRunning = true;
902 mWatcher.start(); // also starts all providers
903 }
904 }
905
906 private void stop() {
907 if (mRunning) {
908 mRunning = false;
Sungsood103e562017-03-09 15:35:07 +0900909 unselectSelectedRoute();
Jeff Brown69b07162013-11-07 00:30:16 -0800910 mWatcher.stop(); // also stops all providers
911 }
912 }
913
914 private void updateDiscoveryRequest() {
915 int routeTypes = 0;
916 boolean activeScan = false;
917 synchronized (mService.mLock) {
918 final int count = mUserRecord.mClientRecords.size();
919 for (int i = 0; i < count; i++) {
920 ClientRecord clientRecord = mUserRecord.mClientRecords.get(i);
921 routeTypes |= clientRecord.mRouteTypes;
922 activeScan |= clientRecord.mActiveScan;
923 }
924 }
925
926 final int newDiscoveryMode;
Jeff Brownaf574182013-11-14 18:16:08 -0800927 if ((routeTypes & MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY) != 0) {
Jeff Brown69b07162013-11-07 00:30:16 -0800928 if (activeScan) {
929 newDiscoveryMode = RemoteDisplayState.DISCOVERY_MODE_ACTIVE;
930 } else {
931 newDiscoveryMode = RemoteDisplayState.DISCOVERY_MODE_PASSIVE;
932 }
933 } else {
934 newDiscoveryMode = RemoteDisplayState.DISCOVERY_MODE_NONE;
935 }
936
937 if (mDiscoveryMode != newDiscoveryMode) {
938 mDiscoveryMode = newDiscoveryMode;
939 final int count = mProviderRecords.size();
940 for (int i = 0; i < count; i++) {
941 mProviderRecords.get(i).getProvider().setDiscoveryMode(mDiscoveryMode);
942 }
943 }
944 }
945
946 private void selectRoute(String routeId) {
947 if (routeId != null
Sungsood103e562017-03-09 15:35:07 +0900948 && (mSelectedRouteRecord == null
949 || !routeId.equals(mSelectedRouteRecord.getUniqueId()))) {
Jeff Brown69b07162013-11-07 00:30:16 -0800950 RouteRecord routeRecord = findRouteRecord(routeId);
951 if (routeRecord != null) {
Sungsood103e562017-03-09 15:35:07 +0900952 unselectSelectedRoute();
Jeff Brown69b07162013-11-07 00:30:16 -0800953
Sungsood103e562017-03-09 15:35:07 +0900954 Slog.i(TAG, "Selected route:" + routeRecord);
955 mSelectedRouteRecord = routeRecord;
956 checkSelectedRouteState();
Jeff Brown69b07162013-11-07 00:30:16 -0800957 routeRecord.getProvider().setSelectedDisplay(routeRecord.getDescriptorId());
958
959 scheduleUpdateClientState();
960 }
961 }
962 }
963
964 private void unselectRoute(String routeId) {
965 if (routeId != null
Sungsood103e562017-03-09 15:35:07 +0900966 && mSelectedRouteRecord != null
967 && routeId.equals(mSelectedRouteRecord.getUniqueId())) {
968 unselectSelectedRoute();
Jeff Brown69b07162013-11-07 00:30:16 -0800969 }
970 }
971
Sungsood103e562017-03-09 15:35:07 +0900972 private void unselectSelectedRoute() {
973 if (mSelectedRouteRecord != null) {
974 Slog.i(TAG, "Unselected route:" + mSelectedRouteRecord);
975 mSelectedRouteRecord.getProvider().setSelectedDisplay(null);
976 mSelectedRouteRecord = null;
977 checkSelectedRouteState();
Jeff Brown69b07162013-11-07 00:30:16 -0800978
979 scheduleUpdateClientState();
980 }
981 }
982
983 private void requestSetVolume(String routeId, int volume) {
Sungsood103e562017-03-09 15:35:07 +0900984 if (mSelectedRouteRecord != null
985 && routeId.equals(mSelectedRouteRecord.getUniqueId())) {
986 mSelectedRouteRecord.getProvider().setDisplayVolume(volume);
Jeff Brown69b07162013-11-07 00:30:16 -0800987 }
988 }
989
990 private void requestUpdateVolume(String routeId, int direction) {
Sungsood103e562017-03-09 15:35:07 +0900991 if (mSelectedRouteRecord != null
992 && routeId.equals(mSelectedRouteRecord.getUniqueId())) {
993 mSelectedRouteRecord.getProvider().adjustDisplayVolume(direction);
Jeff Brown69b07162013-11-07 00:30:16 -0800994 }
995 }
996
997 @Override
998 public void addProvider(RemoteDisplayProviderProxy provider) {
999 provider.setCallback(this);
1000 provider.setDiscoveryMode(mDiscoveryMode);
1001 provider.setSelectedDisplay(null); // just to be safe
1002
1003 ProviderRecord providerRecord = new ProviderRecord(provider);
1004 mProviderRecords.add(providerRecord);
1005 providerRecord.updateDescriptor(provider.getDisplayState());
1006
1007 scheduleUpdateClientState();
1008 }
1009
1010 @Override
1011 public void removeProvider(RemoteDisplayProviderProxy provider) {
1012 int index = findProviderRecord(provider);
1013 if (index >= 0) {
1014 ProviderRecord providerRecord = mProviderRecords.remove(index);
1015 providerRecord.updateDescriptor(null); // mark routes invalid
1016 provider.setCallback(null);
1017 provider.setDiscoveryMode(RemoteDisplayState.DISCOVERY_MODE_NONE);
1018
Sungsood103e562017-03-09 15:35:07 +09001019 checkSelectedRouteState();
Jeff Brown69b07162013-11-07 00:30:16 -08001020 scheduleUpdateClientState();
1021 }
1022 }
1023
1024 @Override
1025 public void onDisplayStateChanged(RemoteDisplayProviderProxy provider,
1026 RemoteDisplayState state) {
1027 updateProvider(provider, state);
1028 }
1029
1030 private void updateProvider(RemoteDisplayProviderProxy provider,
1031 RemoteDisplayState state) {
1032 int index = findProviderRecord(provider);
1033 if (index >= 0) {
1034 ProviderRecord providerRecord = mProviderRecords.get(index);
1035 if (providerRecord.updateDescriptor(state)) {
Sungsood103e562017-03-09 15:35:07 +09001036 checkSelectedRouteState();
Jeff Brown69b07162013-11-07 00:30:16 -08001037 scheduleUpdateClientState();
1038 }
1039 }
1040 }
1041
1042 /**
Sungsood103e562017-03-09 15:35:07 +09001043 * This function is called whenever the state of the selected route may have changed.
1044 * It checks the state and updates timeouts or unselects the route as appropriate.
Jeff Brown69b07162013-11-07 00:30:16 -08001045 */
Sungsood103e562017-03-09 15:35:07 +09001046 private void checkSelectedRouteState() {
Jeff Brown69b07162013-11-07 00:30:16 -08001047 // Unschedule timeouts when the route is unselected.
Sungsood103e562017-03-09 15:35:07 +09001048 if (mSelectedRouteRecord == null) {
Jeff Brown39ad0e52013-11-11 17:55:08 -08001049 mConnectionPhase = PHASE_NOT_AVAILABLE;
Jeff Brown69b07162013-11-07 00:30:16 -08001050 updateConnectionTimeout(0);
1051 return;
1052 }
1053
1054 // Ensure that the route is still present and enabled.
Sungsood103e562017-03-09 15:35:07 +09001055 if (!mSelectedRouteRecord.isValid()
1056 || !mSelectedRouteRecord.isEnabled()) {
Jeff Brown69b07162013-11-07 00:30:16 -08001057 updateConnectionTimeout(TIMEOUT_REASON_NOT_AVAILABLE);
1058 return;
1059 }
1060
Jeff Brown39ad0e52013-11-11 17:55:08 -08001061 // Make sure we haven't lost our connection.
1062 final int oldPhase = mConnectionPhase;
Sungsood103e562017-03-09 15:35:07 +09001063 mConnectionPhase = getConnectionPhase(mSelectedRouteRecord.getStatus());
Jeff Brown39ad0e52013-11-11 17:55:08 -08001064 if (oldPhase >= PHASE_CONNECTING && mConnectionPhase < PHASE_CONNECTING) {
1065 updateConnectionTimeout(TIMEOUT_REASON_CONNECTION_LOST);
1066 return;
1067 }
1068
Jeff Brown69b07162013-11-07 00:30:16 -08001069 // Check the route status.
Jeff Brown39ad0e52013-11-11 17:55:08 -08001070 switch (mConnectionPhase) {
1071 case PHASE_CONNECTED:
1072 if (oldPhase != PHASE_CONNECTED) {
Sungsood103e562017-03-09 15:35:07 +09001073 Slog.i(TAG, "Connected to route: " + mSelectedRouteRecord);
Jeff Brown69b07162013-11-07 00:30:16 -08001074 }
1075 updateConnectionTimeout(0);
1076 break;
Jeff Brown39ad0e52013-11-11 17:55:08 -08001077 case PHASE_CONNECTING:
1078 if (oldPhase != PHASE_CONNECTING) {
Sungsood103e562017-03-09 15:35:07 +09001079 Slog.i(TAG, "Connecting to route: " + mSelectedRouteRecord);
Jeff Brown69b07162013-11-07 00:30:16 -08001080 }
1081 updateConnectionTimeout(TIMEOUT_REASON_WAITING_FOR_CONNECTED);
1082 break;
Jeff Brown39ad0e52013-11-11 17:55:08 -08001083 case PHASE_NOT_CONNECTED:
Jeff Brown69b07162013-11-07 00:30:16 -08001084 updateConnectionTimeout(TIMEOUT_REASON_WAITING_FOR_CONNECTING);
1085 break;
Jeff Brown39ad0e52013-11-11 17:55:08 -08001086 case PHASE_NOT_AVAILABLE:
Jeff Brown69b07162013-11-07 00:30:16 -08001087 default:
1088 updateConnectionTimeout(TIMEOUT_REASON_NOT_AVAILABLE);
1089 break;
1090 }
1091 }
1092
1093 private void updateConnectionTimeout(int reason) {
1094 if (reason != mConnectionTimeoutReason) {
1095 if (mConnectionTimeoutReason != 0) {
1096 removeMessages(MSG_CONNECTION_TIMED_OUT);
1097 }
1098 mConnectionTimeoutReason = reason;
1099 mConnectionTimeoutStartTime = SystemClock.uptimeMillis();
1100 switch (reason) {
1101 case TIMEOUT_REASON_NOT_AVAILABLE:
Jeff Brown39ad0e52013-11-11 17:55:08 -08001102 case TIMEOUT_REASON_CONNECTION_LOST:
1103 // Route became unavailable or connection lost.
1104 // Unselect it immediately.
Jeff Brown69b07162013-11-07 00:30:16 -08001105 sendEmptyMessage(MSG_CONNECTION_TIMED_OUT);
1106 break;
1107 case TIMEOUT_REASON_WAITING_FOR_CONNECTING:
1108 // Waiting for route to start connecting.
1109 sendEmptyMessageDelayed(MSG_CONNECTION_TIMED_OUT, CONNECTING_TIMEOUT);
1110 break;
1111 case TIMEOUT_REASON_WAITING_FOR_CONNECTED:
1112 // Waiting for route to complete connection.
1113 sendEmptyMessageDelayed(MSG_CONNECTION_TIMED_OUT, CONNECTED_TIMEOUT);
1114 break;
1115 }
1116 }
1117 }
1118
1119 private void connectionTimedOut() {
Sungsood103e562017-03-09 15:35:07 +09001120 if (mConnectionTimeoutReason == 0 || mSelectedRouteRecord == null) {
Jeff Brown69b07162013-11-07 00:30:16 -08001121 // Shouldn't get here. There must be a bug somewhere.
1122 Log.wtf(TAG, "Handled connection timeout for no reason.");
1123 return;
1124 }
1125
1126 switch (mConnectionTimeoutReason) {
1127 case TIMEOUT_REASON_NOT_AVAILABLE:
Sungsood103e562017-03-09 15:35:07 +09001128 Slog.i(TAG, "Selected route no longer available: "
1129 + mSelectedRouteRecord);
Jeff Brown69b07162013-11-07 00:30:16 -08001130 break;
Jeff Brown39ad0e52013-11-11 17:55:08 -08001131 case TIMEOUT_REASON_CONNECTION_LOST:
Sungsood103e562017-03-09 15:35:07 +09001132 Slog.i(TAG, "Selected route connection lost: "
1133 + mSelectedRouteRecord);
Jeff Brown39ad0e52013-11-11 17:55:08 -08001134 break;
Jeff Brown69b07162013-11-07 00:30:16 -08001135 case TIMEOUT_REASON_WAITING_FOR_CONNECTING:
Sungsood103e562017-03-09 15:35:07 +09001136 Slog.i(TAG, "Selected route timed out while waiting for "
Jeff Brown69b07162013-11-07 00:30:16 -08001137 + "connection attempt to begin after "
1138 + (SystemClock.uptimeMillis() - mConnectionTimeoutStartTime)
Sungsood103e562017-03-09 15:35:07 +09001139 + " ms: " + mSelectedRouteRecord);
Jeff Brown69b07162013-11-07 00:30:16 -08001140 break;
1141 case TIMEOUT_REASON_WAITING_FOR_CONNECTED:
Sungsood103e562017-03-09 15:35:07 +09001142 Slog.i(TAG, "Selected route timed out while connecting after "
Jeff Brown69b07162013-11-07 00:30:16 -08001143 + (SystemClock.uptimeMillis() - mConnectionTimeoutStartTime)
Sungsood103e562017-03-09 15:35:07 +09001144 + " ms: " + mSelectedRouteRecord);
Jeff Brown69b07162013-11-07 00:30:16 -08001145 break;
1146 }
1147 mConnectionTimeoutReason = 0;
1148
Sungsood103e562017-03-09 15:35:07 +09001149 unselectSelectedRoute();
Jeff Brown69b07162013-11-07 00:30:16 -08001150 }
1151
1152 private void scheduleUpdateClientState() {
1153 if (!mClientStateUpdateScheduled) {
1154 mClientStateUpdateScheduled = true;
1155 sendEmptyMessage(MSG_UPDATE_CLIENT_STATE);
1156 }
1157 }
1158
1159 private void updateClientState() {
1160 mClientStateUpdateScheduled = false;
1161
Jeff Brownaf574182013-11-14 18:16:08 -08001162 // Build a new client state for trusted clients.
Sungsood103e562017-03-09 15:35:07 +09001163 MediaRouterClientState routerState = new MediaRouterClientState();
Jeff Brown69b07162013-11-07 00:30:16 -08001164 final int providerCount = mProviderRecords.size();
1165 for (int i = 0; i < providerCount; i++) {
Sungsood103e562017-03-09 15:35:07 +09001166 mProviderRecords.get(i).appendClientState(routerState);
Jeff Brown69b07162013-11-07 00:30:16 -08001167 }
1168
1169 try {
1170 synchronized (mService.mLock) {
1171 // Update the UserRecord.
Sungsood103e562017-03-09 15:35:07 +09001172 mUserRecord.mRouterState = routerState;
Jeff Brown69b07162013-11-07 00:30:16 -08001173
1174 // Collect all clients.
1175 final int count = mUserRecord.mClientRecords.size();
1176 for (int i = 0; i < count; i++) {
1177 mTempClients.add(mUserRecord.mClientRecords.get(i).mClient);
1178 }
1179 }
1180
1181 // Notify all clients (outside of the lock).
1182 final int count = mTempClients.size();
1183 for (int i = 0; i < count; i++) {
1184 try {
1185 mTempClients.get(i).onStateChanged();
1186 } catch (RemoteException ex) {
Sungsoob3658562017-05-22 17:10:44 +09001187 Slog.w(TAG, "Failed to call onStateChanged. Client probably died.");
Jeff Brown69b07162013-11-07 00:30:16 -08001188 }
1189 }
1190 } finally {
1191 // Clear the list in preparation for the next time.
1192 mTempClients.clear();
1193 }
1194 }
1195
1196 private int findProviderRecord(RemoteDisplayProviderProxy provider) {
1197 final int count = mProviderRecords.size();
1198 for (int i = 0; i < count; i++) {
1199 ProviderRecord record = mProviderRecords.get(i);
1200 if (record.getProvider() == provider) {
1201 return i;
1202 }
1203 }
1204 return -1;
1205 }
1206
1207 private RouteRecord findRouteRecord(String uniqueId) {
1208 final int count = mProviderRecords.size();
1209 for (int i = 0; i < count; i++) {
1210 RouteRecord record = mProviderRecords.get(i).findRouteByUniqueId(uniqueId);
1211 if (record != null) {
1212 return record;
1213 }
1214 }
1215 return null;
1216 }
1217
Jeff Brown39ad0e52013-11-11 17:55:08 -08001218 private static int getConnectionPhase(int status) {
1219 switch (status) {
1220 case MediaRouter.RouteInfo.STATUS_NONE:
1221 case MediaRouter.RouteInfo.STATUS_CONNECTED:
1222 return PHASE_CONNECTED;
1223 case MediaRouter.RouteInfo.STATUS_CONNECTING:
1224 return PHASE_CONNECTING;
1225 case MediaRouter.RouteInfo.STATUS_SCANNING:
1226 case MediaRouter.RouteInfo.STATUS_AVAILABLE:
1227 return PHASE_NOT_CONNECTED;
1228 case MediaRouter.RouteInfo.STATUS_NOT_AVAILABLE:
1229 case MediaRouter.RouteInfo.STATUS_IN_USE:
1230 default:
1231 return PHASE_NOT_AVAILABLE;
1232 }
1233 }
1234
Jeff Brown69b07162013-11-07 00:30:16 -08001235 static final class ProviderRecord {
1236 private final RemoteDisplayProviderProxy mProvider;
1237 private final String mUniquePrefix;
1238 private final ArrayList<RouteRecord> mRoutes = new ArrayList<RouteRecord>();
1239 private RemoteDisplayState mDescriptor;
1240
1241 public ProviderRecord(RemoteDisplayProviderProxy provider) {
1242 mProvider = provider;
1243 mUniquePrefix = provider.getFlattenedComponentName() + ":";
1244 }
1245
1246 public RemoteDisplayProviderProxy getProvider() {
1247 return mProvider;
1248 }
1249
1250 public String getUniquePrefix() {
1251 return mUniquePrefix;
1252 }
1253
1254 public boolean updateDescriptor(RemoteDisplayState descriptor) {
1255 boolean changed = false;
1256 if (mDescriptor != descriptor) {
1257 mDescriptor = descriptor;
1258
1259 // Update all existing routes and reorder them to match
1260 // the order of their descriptors.
1261 int targetIndex = 0;
1262 if (descriptor != null) {
1263 if (descriptor.isValid()) {
1264 final List<RemoteDisplayInfo> routeDescriptors = descriptor.displays;
1265 final int routeCount = routeDescriptors.size();
1266 for (int i = 0; i < routeCount; i++) {
1267 final RemoteDisplayInfo routeDescriptor =
1268 routeDescriptors.get(i);
1269 final String descriptorId = routeDescriptor.id;
1270 final int sourceIndex = findRouteByDescriptorId(descriptorId);
1271 if (sourceIndex < 0) {
1272 // Add the route to the provider.
1273 String uniqueId = assignRouteUniqueId(descriptorId);
1274 RouteRecord route =
1275 new RouteRecord(this, descriptorId, uniqueId);
1276 mRoutes.add(targetIndex++, route);
1277 route.updateDescriptor(routeDescriptor);
1278 changed = true;
1279 } else if (sourceIndex < targetIndex) {
1280 // Ignore route with duplicate id.
1281 Slog.w(TAG, "Ignoring route descriptor with duplicate id: "
1282 + routeDescriptor);
1283 } else {
1284 // Reorder existing route within the list.
1285 RouteRecord route = mRoutes.get(sourceIndex);
1286 Collections.swap(mRoutes, sourceIndex, targetIndex++);
1287 changed |= route.updateDescriptor(routeDescriptor);
1288 }
1289 }
1290 } else {
1291 Slog.w(TAG, "Ignoring invalid descriptor from media route provider: "
1292 + mProvider.getFlattenedComponentName());
1293 }
1294 }
1295
1296 // Dispose all remaining routes that do not have matching descriptors.
1297 for (int i = mRoutes.size() - 1; i >= targetIndex; i--) {
1298 RouteRecord route = mRoutes.remove(i);
1299 route.updateDescriptor(null); // mark route invalid
1300 changed = true;
1301 }
1302 }
1303 return changed;
1304 }
1305
1306 public void appendClientState(MediaRouterClientState state) {
1307 final int routeCount = mRoutes.size();
1308 for (int i = 0; i < routeCount; i++) {
1309 state.routes.add(mRoutes.get(i).getInfo());
1310 }
1311 }
1312
1313 public RouteRecord findRouteByUniqueId(String uniqueId) {
1314 final int routeCount = mRoutes.size();
1315 for (int i = 0; i < routeCount; i++) {
1316 RouteRecord route = mRoutes.get(i);
1317 if (route.getUniqueId().equals(uniqueId)) {
1318 return route;
1319 }
1320 }
1321 return null;
1322 }
1323
1324 private int findRouteByDescriptorId(String descriptorId) {
1325 final int routeCount = mRoutes.size();
1326 for (int i = 0; i < routeCount; i++) {
1327 RouteRecord route = mRoutes.get(i);
1328 if (route.getDescriptorId().equals(descriptorId)) {
1329 return i;
1330 }
1331 }
1332 return -1;
1333 }
1334
1335 public void dump(PrintWriter pw, String prefix) {
1336 pw.println(prefix + this);
1337
1338 final String indent = prefix + " ";
1339 mProvider.dump(pw, indent);
1340
1341 final int routeCount = mRoutes.size();
1342 if (routeCount != 0) {
1343 for (int i = 0; i < routeCount; i++) {
1344 mRoutes.get(i).dump(pw, indent);
1345 }
1346 } else {
1347 pw.println(indent + "<no routes>");
1348 }
1349 }
1350
1351 @Override
1352 public String toString() {
1353 return "Provider " + mProvider.getFlattenedComponentName();
1354 }
1355
1356 private String assignRouteUniqueId(String descriptorId) {
1357 return mUniquePrefix + descriptorId;
1358 }
1359 }
1360
1361 static final class RouteRecord {
1362 private final ProviderRecord mProviderRecord;
1363 private final String mDescriptorId;
1364 private final MediaRouterClientState.RouteInfo mMutableInfo;
1365 private MediaRouterClientState.RouteInfo mImmutableInfo;
1366 private RemoteDisplayInfo mDescriptor;
1367
1368 public RouteRecord(ProviderRecord providerRecord,
1369 String descriptorId, String uniqueId) {
1370 mProviderRecord = providerRecord;
1371 mDescriptorId = descriptorId;
1372 mMutableInfo = new MediaRouterClientState.RouteInfo(uniqueId);
1373 }
1374
1375 public RemoteDisplayProviderProxy getProvider() {
1376 return mProviderRecord.getProvider();
1377 }
1378
1379 public ProviderRecord getProviderRecord() {
1380 return mProviderRecord;
1381 }
1382
1383 public String getDescriptorId() {
1384 return mDescriptorId;
1385 }
1386
1387 public String getUniqueId() {
1388 return mMutableInfo.id;
1389 }
1390
1391 public MediaRouterClientState.RouteInfo getInfo() {
1392 if (mImmutableInfo == null) {
1393 mImmutableInfo = new MediaRouterClientState.RouteInfo(mMutableInfo);
1394 }
1395 return mImmutableInfo;
1396 }
1397
1398 public boolean isValid() {
1399 return mDescriptor != null;
1400 }
1401
1402 public boolean isEnabled() {
1403 return mMutableInfo.enabled;
1404 }
1405
1406 public int getStatus() {
1407 return mMutableInfo.statusCode;
1408 }
1409
1410 public boolean updateDescriptor(RemoteDisplayInfo descriptor) {
1411 boolean changed = false;
1412 if (mDescriptor != descriptor) {
1413 mDescriptor = descriptor;
1414 if (descriptor != null) {
1415 final String name = computeName(descriptor);
Kenny Roote6585b32013-12-13 12:00:26 -08001416 if (!Objects.equals(mMutableInfo.name, name)) {
Jeff Brown69b07162013-11-07 00:30:16 -08001417 mMutableInfo.name = name;
1418 changed = true;
1419 }
1420 final String description = computeDescription(descriptor);
Kenny Roote6585b32013-12-13 12:00:26 -08001421 if (!Objects.equals(mMutableInfo.description, description)) {
Jeff Brown69b07162013-11-07 00:30:16 -08001422 mMutableInfo.description = description;
1423 changed = true;
1424 }
1425 final int supportedTypes = computeSupportedTypes(descriptor);
1426 if (mMutableInfo.supportedTypes != supportedTypes) {
1427 mMutableInfo.supportedTypes = supportedTypes;
1428 changed = true;
1429 }
1430 final boolean enabled = computeEnabled(descriptor);
1431 if (mMutableInfo.enabled != enabled) {
1432 mMutableInfo.enabled = enabled;
1433 changed = true;
1434 }
1435 final int statusCode = computeStatusCode(descriptor);
1436 if (mMutableInfo.statusCode != statusCode) {
1437 mMutableInfo.statusCode = statusCode;
1438 changed = true;
1439 }
1440 final int playbackType = computePlaybackType(descriptor);
1441 if (mMutableInfo.playbackType != playbackType) {
1442 mMutableInfo.playbackType = playbackType;
1443 changed = true;
1444 }
1445 final int playbackStream = computePlaybackStream(descriptor);
1446 if (mMutableInfo.playbackStream != playbackStream) {
1447 mMutableInfo.playbackStream = playbackStream;
1448 changed = true;
1449 }
1450 final int volume = computeVolume(descriptor);
1451 if (mMutableInfo.volume != volume) {
1452 mMutableInfo.volume = volume;
1453 changed = true;
1454 }
1455 final int volumeMax = computeVolumeMax(descriptor);
1456 if (mMutableInfo.volumeMax != volumeMax) {
1457 mMutableInfo.volumeMax = volumeMax;
1458 changed = true;
1459 }
1460 final int volumeHandling = computeVolumeHandling(descriptor);
1461 if (mMutableInfo.volumeHandling != volumeHandling) {
1462 mMutableInfo.volumeHandling = volumeHandling;
1463 changed = true;
1464 }
1465 final int presentationDisplayId = computePresentationDisplayId(descriptor);
1466 if (mMutableInfo.presentationDisplayId != presentationDisplayId) {
1467 mMutableInfo.presentationDisplayId = presentationDisplayId;
1468 changed = true;
1469 }
1470 }
1471 }
1472 if (changed) {
1473 mImmutableInfo = null;
1474 }
1475 return changed;
1476 }
1477
1478 public void dump(PrintWriter pw, String prefix) {
1479 pw.println(prefix + this);
1480
1481 final String indent = prefix + " ";
1482 pw.println(indent + "mMutableInfo=" + mMutableInfo);
1483 pw.println(indent + "mDescriptorId=" + mDescriptorId);
1484 pw.println(indent + "mDescriptor=" + mDescriptor);
1485 }
1486
1487 @Override
1488 public String toString() {
1489 return "Route " + mMutableInfo.name + " (" + mMutableInfo.id + ")";
1490 }
1491
1492 private static String computeName(RemoteDisplayInfo descriptor) {
1493 // Note that isValid() already ensures the name is non-empty.
1494 return descriptor.name;
1495 }
1496
1497 private static String computeDescription(RemoteDisplayInfo descriptor) {
1498 final String description = descriptor.description;
1499 return TextUtils.isEmpty(description) ? null : description;
1500 }
1501
1502 private static int computeSupportedTypes(RemoteDisplayInfo descriptor) {
1503 return MediaRouter.ROUTE_TYPE_LIVE_AUDIO
1504 | MediaRouter.ROUTE_TYPE_LIVE_VIDEO
1505 | MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY;
1506 }
1507
1508 private static boolean computeEnabled(RemoteDisplayInfo descriptor) {
1509 switch (descriptor.status) {
1510 case RemoteDisplayInfo.STATUS_CONNECTED:
1511 case RemoteDisplayInfo.STATUS_CONNECTING:
1512 case RemoteDisplayInfo.STATUS_AVAILABLE:
1513 return true;
1514 default:
1515 return false;
1516 }
1517 }
1518
1519 private static int computeStatusCode(RemoteDisplayInfo descriptor) {
1520 switch (descriptor.status) {
1521 case RemoteDisplayInfo.STATUS_NOT_AVAILABLE:
1522 return MediaRouter.RouteInfo.STATUS_NOT_AVAILABLE;
1523 case RemoteDisplayInfo.STATUS_AVAILABLE:
1524 return MediaRouter.RouteInfo.STATUS_AVAILABLE;
1525 case RemoteDisplayInfo.STATUS_IN_USE:
1526 return MediaRouter.RouteInfo.STATUS_IN_USE;
1527 case RemoteDisplayInfo.STATUS_CONNECTING:
1528 return MediaRouter.RouteInfo.STATUS_CONNECTING;
1529 case RemoteDisplayInfo.STATUS_CONNECTED:
1530 return MediaRouter.RouteInfo.STATUS_CONNECTED;
1531 default:
1532 return MediaRouter.RouteInfo.STATUS_NONE;
1533 }
1534 }
1535
1536 private static int computePlaybackType(RemoteDisplayInfo descriptor) {
1537 return MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE;
1538 }
1539
1540 private static int computePlaybackStream(RemoteDisplayInfo descriptor) {
1541 return AudioSystem.STREAM_MUSIC;
1542 }
1543
1544 private static int computeVolume(RemoteDisplayInfo descriptor) {
1545 final int volume = descriptor.volume;
1546 final int volumeMax = descriptor.volumeMax;
1547 if (volume < 0) {
1548 return 0;
1549 } else if (volume > volumeMax) {
1550 return volumeMax;
1551 }
1552 return volume;
1553 }
1554
1555 private static int computeVolumeMax(RemoteDisplayInfo descriptor) {
1556 final int volumeMax = descriptor.volumeMax;
1557 return volumeMax > 0 ? volumeMax : 0;
1558 }
1559
1560 private static int computeVolumeHandling(RemoteDisplayInfo descriptor) {
1561 final int volumeHandling = descriptor.volumeHandling;
1562 switch (volumeHandling) {
1563 case RemoteDisplayInfo.PLAYBACK_VOLUME_VARIABLE:
1564 return MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE;
1565 case RemoteDisplayInfo.PLAYBACK_VOLUME_FIXED:
1566 default:
1567 return MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED;
1568 }
1569 }
1570
1571 private static int computePresentationDisplayId(RemoteDisplayInfo descriptor) {
1572 // The MediaRouter class validates that the id corresponds to an extant
1573 // presentation display. So all we do here is canonicalize the null case.
1574 final int displayId = descriptor.presentationDisplayId;
1575 return displayId < 0 ? -1 : displayId;
1576 }
1577 }
1578 }
1579}