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