1 /* 2 * Copyright 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.media; 18 19 import static android.media.MediaRoute2ProviderService.REQUEST_ID_NONE; 20 21 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; 22 23 import android.annotation.NonNull; 24 import android.content.ComponentName; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.ServiceConnection; 28 import android.media.IMediaRoute2ProviderService; 29 import android.media.IMediaRoute2ProviderServiceCallback; 30 import android.media.MediaRoute2ProviderInfo; 31 import android.media.MediaRoute2ProviderService; 32 import android.media.RouteDiscoveryPreference; 33 import android.media.RoutingSessionInfo; 34 import android.os.Bundle; 35 import android.os.Handler; 36 import android.os.IBinder; 37 import android.os.IBinder.DeathRecipient; 38 import android.os.Looper; 39 import android.os.RemoteException; 40 import android.os.UserHandle; 41 import android.text.TextUtils; 42 import android.util.Log; 43 import android.util.Slog; 44 45 import com.android.internal.annotations.GuardedBy; 46 47 import java.io.PrintWriter; 48 import java.lang.ref.WeakReference; 49 import java.util.ArrayList; 50 import java.util.Collections; 51 import java.util.List; 52 import java.util.Objects; 53 54 /** 55 * Maintains a connection to a particular {@link MediaRoute2ProviderService}. 56 */ 57 final class MediaRoute2ProviderServiceProxy extends MediaRoute2Provider 58 implements ServiceConnection { 59 private static final String TAG = "MR2ProviderSvcProxy"; 60 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 61 62 private final Context mContext; 63 private final int mUserId; 64 private final Handler mHandler; 65 66 // Connection state 67 private boolean mRunning; 68 private boolean mBound; 69 private Connection mActiveConnection; 70 private boolean mConnectionReady; 71 72 private boolean mIsManagerScanning; 73 private RouteDiscoveryPreference mLastDiscoveryPreference = null; 74 75 @GuardedBy("mLock") 76 final List<RoutingSessionInfo> mReleasingSessions = new ArrayList<>(); 77 MediaRoute2ProviderServiceProxy(@onNull Context context, @NonNull ComponentName componentName, int userId)78 MediaRoute2ProviderServiceProxy(@NonNull Context context, @NonNull ComponentName componentName, 79 int userId) { 80 super(componentName); 81 mContext = Objects.requireNonNull(context, "Context must not be null."); 82 mUserId = userId; 83 mHandler = new Handler(Looper.myLooper()); 84 } 85 dump(PrintWriter pw, String prefix)86 public void dump(PrintWriter pw, String prefix) { 87 pw.println(prefix + "Proxy"); 88 pw.println(prefix + " mUserId=" + mUserId); 89 pw.println(prefix + " mRunning=" + mRunning); 90 pw.println(prefix + " mBound=" + mBound); 91 pw.println(prefix + " mActiveConnection=" + mActiveConnection); 92 pw.println(prefix + " mConnectionReady=" + mConnectionReady); 93 } 94 setManagerScanning(boolean managerScanning)95 public void setManagerScanning(boolean managerScanning) { 96 if (mIsManagerScanning != managerScanning) { 97 mIsManagerScanning = managerScanning; 98 updateBinding(); 99 } 100 } 101 102 @Override requestCreateSession(long requestId, String packageName, String routeId, Bundle sessionHints)103 public void requestCreateSession(long requestId, String packageName, String routeId, 104 Bundle sessionHints) { 105 if (mConnectionReady) { 106 mActiveConnection.requestCreateSession(requestId, packageName, routeId, sessionHints); 107 updateBinding(); 108 } 109 } 110 111 @Override releaseSession(long requestId, String sessionId)112 public void releaseSession(long requestId, String sessionId) { 113 if (mConnectionReady) { 114 mActiveConnection.releaseSession(requestId, sessionId); 115 updateBinding(); 116 } 117 } 118 119 @Override updateDiscoveryPreference(RouteDiscoveryPreference discoveryPreference)120 public void updateDiscoveryPreference(RouteDiscoveryPreference discoveryPreference) { 121 mLastDiscoveryPreference = discoveryPreference; 122 if (mConnectionReady) { 123 mActiveConnection.updateDiscoveryPreference(discoveryPreference); 124 } 125 updateBinding(); 126 } 127 128 @Override selectRoute(long requestId, String sessionId, String routeId)129 public void selectRoute(long requestId, String sessionId, String routeId) { 130 if (mConnectionReady) { 131 mActiveConnection.selectRoute(requestId, sessionId, routeId); 132 } 133 } 134 135 @Override deselectRoute(long requestId, String sessionId, String routeId)136 public void deselectRoute(long requestId, String sessionId, String routeId) { 137 if (mConnectionReady) { 138 mActiveConnection.deselectRoute(requestId, sessionId, routeId); 139 } 140 } 141 142 @Override transferToRoute(long requestId, String sessionId, String routeId)143 public void transferToRoute(long requestId, String sessionId, String routeId) { 144 if (mConnectionReady) { 145 mActiveConnection.transferToRoute(requestId, sessionId, routeId); 146 } 147 } 148 149 @Override setRouteVolume(long requestId, String routeId, int volume)150 public void setRouteVolume(long requestId, String routeId, int volume) { 151 if (mConnectionReady) { 152 mActiveConnection.setRouteVolume(requestId, routeId, volume); 153 updateBinding(); 154 } 155 } 156 157 @Override setSessionVolume(long requestId, String sessionId, int volume)158 public void setSessionVolume(long requestId, String sessionId, int volume) { 159 if (mConnectionReady) { 160 mActiveConnection.setSessionVolume(requestId, sessionId, volume); 161 updateBinding(); 162 } 163 } 164 165 @Override prepareReleaseSession(@onNull String sessionId)166 public void prepareReleaseSession(@NonNull String sessionId) { 167 synchronized (mLock) { 168 for (RoutingSessionInfo session : mSessionInfos) { 169 if (TextUtils.equals(session.getId(), sessionId)) { 170 mSessionInfos.remove(session); 171 mReleasingSessions.add(session); 172 break; 173 } 174 } 175 } 176 } 177 hasComponentName(String packageName, String className)178 public boolean hasComponentName(String packageName, String className) { 179 return mComponentName.getPackageName().equals(packageName) 180 && mComponentName.getClassName().equals(className); 181 } 182 start()183 public void start() { 184 if (!mRunning) { 185 if (DEBUG) { 186 Slog.d(TAG, this + ": Starting"); 187 } 188 mRunning = true; 189 updateBinding(); 190 } 191 } 192 stop()193 public void stop() { 194 if (mRunning) { 195 if (DEBUG) { 196 Slog.d(TAG, this + ": Stopping"); 197 } 198 mRunning = false; 199 updateBinding(); 200 } 201 } 202 rebindIfDisconnected()203 public void rebindIfDisconnected() { 204 //TODO: When we are connecting to the service, calling this will unbind and bind again. 205 // We'd better not unbind if we are connecting. 206 if (mActiveConnection == null && shouldBind()) { 207 unbind(); 208 bind(); 209 } 210 } 211 updateBinding()212 private void updateBinding() { 213 if (shouldBind()) { 214 bind(); 215 } else { 216 unbind(); 217 } 218 } 219 shouldBind()220 private boolean shouldBind() { 221 if (mRunning) { 222 // Bind when there is a discovery preference or an active route session. 223 return (mLastDiscoveryPreference != null 224 && !mLastDiscoveryPreference.getPreferredFeatures().isEmpty()) 225 || !getSessionInfos().isEmpty() 226 || mIsManagerScanning; 227 } 228 return false; 229 } 230 bind()231 private void bind() { 232 if (!mBound) { 233 if (DEBUG) { 234 Slog.d(TAG, this + ": Binding"); 235 } 236 237 Intent service = new Intent(MediaRoute2ProviderService.SERVICE_INTERFACE); 238 service.setComponent(mComponentName); 239 try { 240 mBound = mContext.bindServiceAsUser(service, this, 241 Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE, 242 new UserHandle(mUserId)); 243 if (!mBound && DEBUG) { 244 Slog.d(TAG, this + ": Bind failed"); 245 } 246 } catch (SecurityException ex) { 247 if (DEBUG) { 248 Slog.d(TAG, this + ": Bind failed", ex); 249 } 250 } 251 } 252 } 253 unbind()254 private void unbind() { 255 if (mBound) { 256 if (DEBUG) { 257 Slog.d(TAG, this + ": Unbinding"); 258 } 259 260 mBound = false; 261 disconnect(); 262 mContext.unbindService(this); 263 } 264 } 265 266 @Override onServiceConnected(ComponentName name, IBinder service)267 public void onServiceConnected(ComponentName name, IBinder service) { 268 if (DEBUG) { 269 Slog.d(TAG, this + ": Connected"); 270 } 271 272 if (mBound) { 273 disconnect(); 274 IMediaRoute2ProviderService serviceBinder = 275 IMediaRoute2ProviderService.Stub.asInterface(service); 276 if (serviceBinder != null) { 277 Connection connection = new Connection(serviceBinder); 278 if (connection.register()) { 279 mActiveConnection = connection; 280 } else { 281 if (DEBUG) { 282 Slog.d(TAG, this + ": Registration failed"); 283 } 284 } 285 } else { 286 Slog.e(TAG, this + ": Service returned invalid binder"); 287 } 288 } 289 } 290 291 @Override onServiceDisconnected(ComponentName name)292 public void onServiceDisconnected(ComponentName name) { 293 if (DEBUG) { 294 Slog.d(TAG, this + ": Service disconnected"); 295 } 296 disconnect(); 297 } 298 299 @Override onBindingDied(ComponentName name)300 public void onBindingDied(ComponentName name) { 301 if (DEBUG) { 302 Slog.d(TAG, this + ": Service binding died"); 303 } 304 unbind(); 305 if (shouldBind()) { 306 bind(); 307 } 308 } 309 onConnectionReady(Connection connection)310 private void onConnectionReady(Connection connection) { 311 if (mActiveConnection == connection) { 312 mConnectionReady = true; 313 if (mLastDiscoveryPreference != null) { 314 updateDiscoveryPreference(mLastDiscoveryPreference); 315 } 316 } 317 } 318 onConnectionDied(Connection connection)319 private void onConnectionDied(Connection connection) { 320 if (mActiveConnection == connection) { 321 if (DEBUG) { 322 Slog.d(TAG, this + ": Service connection died"); 323 } 324 disconnect(); 325 } 326 } 327 onProviderUpdated(Connection connection, MediaRoute2ProviderInfo providerInfo)328 private void onProviderUpdated(Connection connection, MediaRoute2ProviderInfo providerInfo) { 329 if (mActiveConnection != connection) { 330 return; 331 } 332 if (DEBUG) { 333 Slog.d(TAG, this + ": updated"); 334 } 335 setAndNotifyProviderState(providerInfo); 336 } 337 onSessionCreated(Connection connection, long requestId, RoutingSessionInfo newSession)338 private void onSessionCreated(Connection connection, long requestId, 339 RoutingSessionInfo newSession) { 340 if (mActiveConnection != connection) { 341 return; 342 } 343 344 if (newSession == null) { 345 Slog.w(TAG, "onSessionCreated: Ignoring null session sent from " + mComponentName); 346 return; 347 } 348 349 newSession = assignProviderIdForSession(newSession); 350 String newSessionId = newSession.getId(); 351 352 synchronized (mLock) { 353 if (mSessionInfos.stream() 354 .anyMatch(session -> TextUtils.equals(session.getId(), newSessionId)) 355 || mReleasingSessions.stream() 356 .anyMatch(session -> TextUtils.equals(session.getId(), newSessionId))) { 357 Slog.w(TAG, "onSessionCreated: Duplicate session already exists. Ignoring."); 358 return; 359 } 360 mSessionInfos.add(newSession); 361 } 362 363 mCallback.onSessionCreated(this, requestId, newSession); 364 } 365 findSessionByIdLocked(RoutingSessionInfo session)366 private int findSessionByIdLocked(RoutingSessionInfo session) { 367 for (int i = 0; i < mSessionInfos.size(); i++) { 368 if (TextUtils.equals(mSessionInfos.get(i).getId(), session.getId())) { 369 return i; 370 } 371 } 372 return -1; 373 } 374 375 onSessionsUpdated(Connection connection, List<RoutingSessionInfo> sessions)376 private void onSessionsUpdated(Connection connection, List<RoutingSessionInfo> sessions) { 377 if (mActiveConnection != connection) { 378 return; 379 } 380 381 int targetIndex = 0; 382 synchronized (mLock) { 383 for (RoutingSessionInfo session : sessions) { 384 if (session == null) continue; 385 session = assignProviderIdForSession(session); 386 387 int sourceIndex = findSessionByIdLocked(session); 388 if (sourceIndex < 0) { 389 mSessionInfos.add(targetIndex++, session); 390 dispatchSessionCreated(REQUEST_ID_NONE, session); 391 } else if (sourceIndex < targetIndex) { 392 Slog.w(TAG, "Ignoring duplicate session ID: " + session.getId()); 393 } else { 394 mSessionInfos.set(sourceIndex, session); 395 Collections.swap(mSessionInfos, sourceIndex, targetIndex++); 396 dispatchSessionUpdated(session); 397 } 398 } 399 for (int i = mSessionInfos.size() - 1; i >= targetIndex; i--) { 400 RoutingSessionInfo releasedSession = mSessionInfos.remove(i); 401 dispatchSessionReleased(releasedSession); 402 } 403 } 404 } 405 onSessionReleased(Connection connection, RoutingSessionInfo releaedSession)406 private void onSessionReleased(Connection connection, RoutingSessionInfo releaedSession) { 407 if (mActiveConnection != connection) { 408 return; 409 } 410 if (releaedSession == null) { 411 Slog.w(TAG, "onSessionReleased: Ignoring null session sent from " + mComponentName); 412 return; 413 } 414 415 releaedSession = assignProviderIdForSession(releaedSession); 416 417 boolean found = false; 418 synchronized (mLock) { 419 for (RoutingSessionInfo session : mSessionInfos) { 420 if (TextUtils.equals(session.getId(), releaedSession.getId())) { 421 mSessionInfos.remove(session); 422 found = true; 423 break; 424 } 425 } 426 if (!found) { 427 for (RoutingSessionInfo session : mReleasingSessions) { 428 if (TextUtils.equals(session.getId(), releaedSession.getId())) { 429 mReleasingSessions.remove(session); 430 return; 431 } 432 } 433 } 434 } 435 436 if (!found) { 437 Slog.w(TAG, "onSessionReleased: Matching session info not found"); 438 return; 439 } 440 441 mCallback.onSessionReleased(this, releaedSession); 442 } 443 dispatchSessionCreated(long requestId, RoutingSessionInfo session)444 private void dispatchSessionCreated(long requestId, RoutingSessionInfo session) { 445 mHandler.sendMessage( 446 obtainMessage(mCallback::onSessionCreated, this, requestId, session)); 447 } 448 dispatchSessionUpdated(RoutingSessionInfo session)449 private void dispatchSessionUpdated(RoutingSessionInfo session) { 450 mHandler.sendMessage( 451 obtainMessage(mCallback::onSessionUpdated, this, session)); 452 } 453 dispatchSessionReleased(RoutingSessionInfo session)454 private void dispatchSessionReleased(RoutingSessionInfo session) { 455 mHandler.sendMessage( 456 obtainMessage(mCallback::onSessionReleased, this, session)); 457 } 458 assignProviderIdForSession(RoutingSessionInfo sessionInfo)459 private RoutingSessionInfo assignProviderIdForSession(RoutingSessionInfo sessionInfo) { 460 return new RoutingSessionInfo.Builder(sessionInfo) 461 .setOwnerPackageName(mComponentName.getPackageName()) 462 .setProviderId(getUniqueId()) 463 .build(); 464 } 465 onRequestFailed(Connection connection, long requestId, int reason)466 private void onRequestFailed(Connection connection, long requestId, int reason) { 467 if (mActiveConnection != connection) { 468 return; 469 } 470 471 if (requestId == REQUEST_ID_NONE) { 472 Slog.w(TAG, "onRequestFailed: Ignoring requestId REQUEST_ID_NONE"); 473 return; 474 } 475 476 mCallback.onRequestFailed(this, requestId, reason); 477 } 478 disconnect()479 private void disconnect() { 480 if (mActiveConnection != null) { 481 mConnectionReady = false; 482 mActiveConnection.dispose(); 483 mActiveConnection = null; 484 setAndNotifyProviderState(null); 485 synchronized (mLock) { 486 for (RoutingSessionInfo sessionInfo : mSessionInfos) { 487 mCallback.onSessionReleased(this, sessionInfo); 488 } 489 mSessionInfos.clear(); 490 mReleasingSessions.clear(); 491 } 492 } 493 } 494 495 @Override toString()496 public String toString() { 497 return "Service connection " + mComponentName.flattenToShortString(); 498 } 499 500 private final class Connection implements DeathRecipient { 501 private final IMediaRoute2ProviderService mService; 502 private final ServiceCallbackStub mCallbackStub; 503 Connection(IMediaRoute2ProviderService serviceBinder)504 Connection(IMediaRoute2ProviderService serviceBinder) { 505 mService = serviceBinder; 506 mCallbackStub = new ServiceCallbackStub(this); 507 } 508 register()509 public boolean register() { 510 try { 511 mService.asBinder().linkToDeath(this, 0); 512 mService.setCallback(mCallbackStub); 513 mHandler.post(() -> onConnectionReady(Connection.this)); 514 return true; 515 } catch (RemoteException ex) { 516 binderDied(); 517 } 518 return false; 519 } 520 dispose()521 public void dispose() { 522 mService.asBinder().unlinkToDeath(this, 0); 523 mCallbackStub.dispose(); 524 } 525 requestCreateSession(long requestId, String packageName, String routeId, Bundle sessionHints)526 public void requestCreateSession(long requestId, String packageName, String routeId, 527 Bundle sessionHints) { 528 try { 529 mService.requestCreateSession(requestId, packageName, routeId, sessionHints); 530 } catch (RemoteException ex) { 531 Slog.e(TAG, "requestCreateSession: Failed to deliver request."); 532 } 533 } 534 releaseSession(long requestId, String sessionId)535 public void releaseSession(long requestId, String sessionId) { 536 try { 537 mService.releaseSession(requestId, sessionId); 538 } catch (RemoteException ex) { 539 Slog.e(TAG, "releaseSession: Failed to deliver request."); 540 } 541 } 542 updateDiscoveryPreference(RouteDiscoveryPreference discoveryPreference)543 public void updateDiscoveryPreference(RouteDiscoveryPreference discoveryPreference) { 544 try { 545 mService.updateDiscoveryPreference(discoveryPreference); 546 } catch (RemoteException ex) { 547 Slog.e(TAG, "updateDiscoveryPreference: Failed to deliver request."); 548 } 549 } 550 selectRoute(long requestId, String sessionId, String routeId)551 public void selectRoute(long requestId, String sessionId, String routeId) { 552 try { 553 mService.selectRoute(requestId, sessionId, routeId); 554 } catch (RemoteException ex) { 555 Slog.e(TAG, "selectRoute: Failed to deliver request.", ex); 556 } 557 } 558 deselectRoute(long requestId, String sessionId, String routeId)559 public void deselectRoute(long requestId, String sessionId, String routeId) { 560 try { 561 mService.deselectRoute(requestId, sessionId, routeId); 562 } catch (RemoteException ex) { 563 Slog.e(TAG, "deselectRoute: Failed to deliver request.", ex); 564 } 565 } 566 transferToRoute(long requestId, String sessionId, String routeId)567 public void transferToRoute(long requestId, String sessionId, String routeId) { 568 try { 569 mService.transferToRoute(requestId, sessionId, routeId); 570 } catch (RemoteException ex) { 571 Slog.e(TAG, "transferToRoute: Failed to deliver request.", ex); 572 } 573 } 574 setRouteVolume(long requestId, String routeId, int volume)575 public void setRouteVolume(long requestId, String routeId, int volume) { 576 try { 577 mService.setRouteVolume(requestId, routeId, volume); 578 } catch (RemoteException ex) { 579 Slog.e(TAG, "setRouteVolume: Failed to deliver request.", ex); 580 } 581 } 582 setSessionVolume(long requestId, String sessionId, int volume)583 public void setSessionVolume(long requestId, String sessionId, int volume) { 584 try { 585 mService.setSessionVolume(requestId, sessionId, volume); 586 } catch (RemoteException ex) { 587 Slog.e(TAG, "setSessionVolume: Failed to deliver request.", ex); 588 } 589 } 590 591 @Override binderDied()592 public void binderDied() { 593 mHandler.post(() -> onConnectionDied(Connection.this)); 594 } 595 postProviderUpdated(MediaRoute2ProviderInfo providerInfo)596 void postProviderUpdated(MediaRoute2ProviderInfo providerInfo) { 597 mHandler.post(() -> onProviderUpdated(Connection.this, providerInfo)); 598 } 599 postSessionCreated(long requestId, RoutingSessionInfo sessionInfo)600 void postSessionCreated(long requestId, RoutingSessionInfo sessionInfo) { 601 mHandler.post(() -> onSessionCreated(Connection.this, requestId, sessionInfo)); 602 } 603 postSessionsUpdated(List<RoutingSessionInfo> sessionInfo)604 void postSessionsUpdated(List<RoutingSessionInfo> sessionInfo) { 605 mHandler.post(() -> onSessionsUpdated(Connection.this, sessionInfo)); 606 } 607 postSessionReleased(RoutingSessionInfo sessionInfo)608 void postSessionReleased(RoutingSessionInfo sessionInfo) { 609 mHandler.post(() -> onSessionReleased(Connection.this, sessionInfo)); 610 } 611 postRequestFailed(long requestId, int reason)612 void postRequestFailed(long requestId, int reason) { 613 mHandler.post(() -> onRequestFailed(Connection.this, requestId, reason)); 614 } 615 } 616 617 private static final class ServiceCallbackStub extends 618 IMediaRoute2ProviderServiceCallback.Stub { 619 private final WeakReference<Connection> mConnectionRef; 620 ServiceCallbackStub(Connection connection)621 ServiceCallbackStub(Connection connection) { 622 mConnectionRef = new WeakReference<>(connection); 623 } 624 dispose()625 public void dispose() { 626 mConnectionRef.clear(); 627 } 628 629 @Override notifyProviderUpdated(MediaRoute2ProviderInfo providerInfo)630 public void notifyProviderUpdated(MediaRoute2ProviderInfo providerInfo) { 631 Connection connection = mConnectionRef.get(); 632 if (connection != null) { 633 connection.postProviderUpdated(providerInfo); 634 } 635 } 636 637 @Override notifySessionCreated(long requestId, RoutingSessionInfo sessionInfo)638 public void notifySessionCreated(long requestId, RoutingSessionInfo sessionInfo) { 639 Connection connection = mConnectionRef.get(); 640 if (connection != null) { 641 connection.postSessionCreated(requestId, sessionInfo); 642 } 643 } 644 645 @Override notifySessionsUpdated(List<RoutingSessionInfo> sessionInfo)646 public void notifySessionsUpdated(List<RoutingSessionInfo> sessionInfo) { 647 Connection connection = mConnectionRef.get(); 648 if (connection != null) { 649 connection.postSessionsUpdated(sessionInfo); 650 } 651 } 652 653 @Override notifySessionReleased(RoutingSessionInfo sessionInfo)654 public void notifySessionReleased(RoutingSessionInfo sessionInfo) { 655 Connection connection = mConnectionRef.get(); 656 if (connection != null) { 657 connection.postSessionReleased(sessionInfo); 658 } 659 } 660 661 @Override notifyRequestFailed(long requestId, int reason)662 public void notifyRequestFailed(long requestId, int reason) { 663 Connection connection = mConnectionRef.get(); 664 if (connection != null) { 665 connection.postRequestFailed(requestId, reason); 666 } 667 } 668 } 669 } 670