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 android.media; 18 19 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; 20 21 import static java.util.Objects.requireNonNull; 22 23 import android.Manifest; 24 import android.annotation.CallSuper; 25 import android.annotation.FlaggedApi; 26 import android.annotation.IntDef; 27 import android.annotation.NonNull; 28 import android.annotation.Nullable; 29 import android.annotation.RequiresPermission; 30 import android.annotation.SdkConstant; 31 import android.app.Service; 32 import android.content.Intent; 33 import android.media.audiopolicy.AudioMix; 34 import android.media.audiopolicy.AudioMixingRule; 35 import android.media.audiopolicy.AudioPolicy; 36 import android.os.Binder; 37 import android.os.Bundle; 38 import android.os.Handler; 39 import android.os.IBinder; 40 import android.os.Looper; 41 import android.os.Process; 42 import android.os.RemoteException; 43 import android.text.TextUtils; 44 import android.util.ArrayMap; 45 import android.util.Log; 46 import android.util.LongSparseArray; 47 48 import com.android.internal.annotations.GuardedBy; 49 import com.android.media.flags.Flags; 50 51 import java.lang.annotation.Retention; 52 import java.lang.annotation.RetentionPolicy; 53 import java.util.ArrayDeque; 54 import java.util.ArrayList; 55 import java.util.Collection; 56 import java.util.Deque; 57 import java.util.List; 58 import java.util.Objects; 59 import java.util.concurrent.atomic.AtomicBoolean; 60 61 /** 62 * Base class for media route provider services. 63 * <p> 64 * Media route provider services are used to publish {@link MediaRoute2Info media routes} such as 65 * speakers, TVs, etc. The routes are published by calling {@link #notifyRoutes(Collection)}. 66 * Media apps which use {@link MediaRouter2} can request to play their media on the routes. 67 * </p><p> 68 * When {@link MediaRouter2 media router} wants to play media on a route, 69 * {@link #onCreateSession(long, String, String, Bundle)} will be called to handle the request. 70 * A session can be considered as a group of currently selected routes for each connection. 71 * Create and manage the sessions by yourself, and notify the {@link RoutingSessionInfo 72 * session infos} when there are any changes. 73 * </p><p> 74 * The system media router service will bind to media route provider services when a 75 * {@link RouteDiscoveryPreference discovery preference} is registered via 76 * a {@link MediaRouter2 media router} by an application. See 77 * {@link #onDiscoveryPreferenceChanged(RouteDiscoveryPreference)} for the details. 78 * </p> 79 * Use {@link #notifyRequestFailed(long, int)} to notify the failure with previously received 80 * request ID. 81 */ 82 public abstract class MediaRoute2ProviderService extends Service { 83 private static final String TAG = "MR2ProviderService"; 84 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 85 86 /** 87 * The {@link Intent} action that must be declared as handled by the service. 88 * Put this in your manifest to provide media routes. 89 */ 90 @SdkConstant(SdkConstant.SdkConstantType.SERVICE_ACTION) 91 public static final String SERVICE_INTERFACE = "android.media.MediaRoute2ProviderService"; 92 93 /** 94 * A category that indicates that the declaring service supports routing of the system media. 95 * 96 * <p>Providers must include this action if they intend to publish routes that support the 97 * system media, as described by {@link MediaRoute2Info#getSupportedRoutingTypes()}. 98 * 99 * @see #onCreateSystemRoutingSession 100 */ 101 @FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2) 102 @SdkConstant(SdkConstant.SdkConstantType.INTENT_CATEGORY) 103 public static final String CATEGORY_SYSTEM_MEDIA = 104 "android.media.MediaRoute2ProviderService.SYSTEM_MEDIA"; 105 106 /** 107 * A category indicating that the associated provider is only intended for use within the app 108 * that hosts the provider. 109 * 110 * <p>Declaring this category helps the system save resources by avoiding the launch of services 111 * whose routes are known to be private to the app that provides them. 112 * 113 * @hide 114 */ 115 public static final String CATEGORY_SELF_SCAN_ONLY = 116 "android.media.MediaRoute2ProviderService.SELF_SCAN_ONLY"; 117 118 /** 119 * The request ID to pass {@link #notifySessionCreated(long, RoutingSessionInfo)} 120 * when {@link MediaRoute2ProviderService} created a session although there was no creation 121 * request. 122 * 123 * @see #notifySessionCreated(long, RoutingSessionInfo) 124 */ 125 public static final long REQUEST_ID_NONE = 0; 126 127 /** 128 * The request has failed due to unknown reason. 129 * 130 * @see #notifyRequestFailed(long, int) 131 */ 132 public static final int REASON_UNKNOWN_ERROR = 0; 133 134 /** 135 * The request has failed since this service rejected the request. 136 * 137 * @see #notifyRequestFailed(long, int) 138 */ 139 public static final int REASON_REJECTED = 1; 140 141 /** 142 * The request has failed due to a network error. 143 * 144 * @see #notifyRequestFailed(long, int) 145 */ 146 public static final int REASON_NETWORK_ERROR = 2; 147 148 /** 149 * The request has failed since the requested route is no longer available. 150 * 151 * @see #notifyRequestFailed(long, int) 152 */ 153 public static final int REASON_ROUTE_NOT_AVAILABLE = 3; 154 155 /** 156 * The request has failed since the request is not valid. For example, selecting a route 157 * which is not selectable. 158 * 159 * @see #notifyRequestFailed(long, int) 160 */ 161 public static final int REASON_INVALID_COMMAND = 4; 162 163 /** 164 * The request has failed because the requested operation is not implemented by the provider. 165 * 166 * @see #notifyRequestFailed 167 */ 168 @FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2) 169 public static final int REASON_UNIMPLEMENTED = 5; 170 171 /** 172 * The request has failed because the provider has failed to route system media. 173 * 174 * @see #notifyRequestFailed 175 */ 176 @FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2) 177 public static final int REASON_FAILED_TO_REROUTE_SYSTEM_MEDIA = 6; 178 179 /** @hide */ 180 @IntDef( 181 prefix = "REASON_", 182 value = { 183 REASON_UNKNOWN_ERROR, 184 REASON_REJECTED, 185 REASON_NETWORK_ERROR, 186 REASON_ROUTE_NOT_AVAILABLE, 187 REASON_INVALID_COMMAND, 188 REASON_UNIMPLEMENTED, 189 REASON_FAILED_TO_REROUTE_SYSTEM_MEDIA 190 }) 191 @Retention(RetentionPolicy.SOURCE) 192 public @interface Reason {} 193 194 private static final int MAX_REQUEST_IDS_SIZE = 500; 195 196 private final Handler mHandler; 197 private final Object mSessionLock = new Object(); 198 private final Object mRequestIdsLock = new Object(); 199 private final AtomicBoolean mStatePublishScheduled = new AtomicBoolean(false); 200 private final AtomicBoolean mSessionUpdateScheduled = new AtomicBoolean(false); 201 private MediaRoute2ProviderServiceStub mStub; 202 /** Populated by system_server in {@link #setCallback}. Monotonically non-null. */ 203 private IMediaRoute2ProviderServiceCallback mRemoteCallback; 204 private volatile MediaRoute2ProviderInfo mProviderInfo; 205 206 @GuardedBy("mRequestIdsLock") 207 private final Deque<Long> mRequestIds = new ArrayDeque<>(MAX_REQUEST_IDS_SIZE); 208 209 /** 210 * Maps system media session creation request ids to a package uid whose media to route. The 211 * value may be {@link Process#INVALID_UID} for routing sessions that don't affect a specific 212 * package (for example, if they affect the entire system). 213 */ 214 @GuardedBy("mRequestIdsLock") 215 private final LongSparseArray<Integer> mSystemRoutingSessionCreationRequests = 216 new LongSparseArray<>(); 217 218 @GuardedBy("mSessionLock") 219 private final ArrayMap<String, RoutingSessionInfo> mSessionInfos = new ArrayMap<>(); 220 221 @GuardedBy("mSessionLock") 222 private final ArrayMap<String, MediaStreams> mOngoingMediaStreams = new ArrayMap<>(); 223 224 @GuardedBy("mSessionLock") 225 private final ArrayMap<String, RoutingSessionInfo> mPendingSystemSessionReleases = 226 new ArrayMap<>(); 227 MediaRoute2ProviderService()228 public MediaRoute2ProviderService() { 229 mHandler = new Handler(Looper.getMainLooper()); 230 } 231 232 /** 233 * If overriding this method, call through to the super method for any unknown actions. 234 * <p> 235 * {@inheritDoc} 236 */ 237 @CallSuper 238 @Override 239 @Nullable onBind(@onNull Intent intent)240 public IBinder onBind(@NonNull Intent intent) { 241 if (SERVICE_INTERFACE.equals(intent.getAction())) { 242 if (mStub == null) { 243 mStub = new MediaRoute2ProviderServiceStub(); 244 } 245 return mStub; 246 } 247 return null; 248 } 249 250 /** 251 * Called when a volume setting is requested on a route of the provider 252 * 253 * @param requestId the ID of this request 254 * @param routeId the ID of the route 255 * @param volume the target volume 256 * @see MediaRoute2Info.Builder#setVolume(int) 257 */ onSetRouteVolume(long requestId, @NonNull String routeId, int volume)258 public abstract void onSetRouteVolume(long requestId, @NonNull String routeId, int volume); 259 260 /** 261 * Called when {@link MediaRouter2.RoutingController#setVolume(int)} is called on 262 * a routing session of the provider 263 * 264 * @param requestId the ID of this request 265 * @param sessionId the ID of the routing session 266 * @param volume the target volume 267 * @see RoutingSessionInfo.Builder#setVolume(int) 268 */ onSetSessionVolume(long requestId, @NonNull String sessionId, int volume)269 public abstract void onSetSessionVolume(long requestId, @NonNull String sessionId, int volume); 270 271 /** 272 * Gets information of the session with the given id. 273 * 274 * @param sessionId the ID of the session 275 * @return information of the session with the given id. 276 * null if the session is released or ID is not valid. 277 */ 278 @Nullable getSessionInfo(@onNull String sessionId)279 public final RoutingSessionInfo getSessionInfo(@NonNull String sessionId) { 280 if (TextUtils.isEmpty(sessionId)) { 281 throw new IllegalArgumentException("sessionId must not be empty"); 282 } 283 synchronized (mSessionLock) { 284 return mSessionInfos.get(sessionId); 285 } 286 } 287 288 /** 289 * Gets the list of {@link RoutingSessionInfo session info} that the provider service maintains. 290 */ 291 @NonNull getAllSessionInfo()292 public final List<RoutingSessionInfo> getAllSessionInfo() { 293 synchronized (mSessionLock) { 294 return new ArrayList<>(mSessionInfos.values()); 295 } 296 } 297 298 /** 299 * Notifies clients of that the session is created and ready for use. 300 * <p> 301 * If this session is created without any creation request, use {@link #REQUEST_ID_NONE} 302 * as the request ID. 303 * 304 * @param requestId the ID of the previous request to create this session provided in 305 * {@link #onCreateSession(long, String, String, Bundle)}. Can be 306 * {@link #REQUEST_ID_NONE} if this session is created without any request. 307 * @param sessionInfo information of the new session. 308 * The {@link RoutingSessionInfo#getId() id} of the session must be unique. 309 * @see #onCreateSession(long, String, String, Bundle) 310 * @see #getSessionInfo(String) 311 */ notifySessionCreated(long requestId, @NonNull RoutingSessionInfo sessionInfo)312 public final void notifySessionCreated(long requestId, 313 @NonNull RoutingSessionInfo sessionInfo) { 314 requireNonNull(sessionInfo, "sessionInfo must not be null"); 315 316 if (DEBUG) { 317 Log.d(TAG, "notifySessionCreated: Creating a session. requestId=" + requestId 318 + ", sessionInfo=" + sessionInfo); 319 } 320 321 if (requestId != REQUEST_ID_NONE && !removeRequestId(requestId)) { 322 Log.w(TAG, "notifySessionCreated: The requestId doesn't exist. requestId=" + requestId); 323 return; 324 } 325 326 String sessionId = sessionInfo.getId(); 327 synchronized (mSessionLock) { 328 if (mSessionInfos.containsKey(sessionId)) { 329 Log.w(TAG, "notifySessionCreated: Ignoring duplicate session id."); 330 return; 331 } 332 mSessionInfos.put(sessionInfo.getId(), sessionInfo); 333 334 if (mRemoteCallback == null) { 335 return; 336 } 337 try { 338 mRemoteCallback.notifySessionCreated(requestId, sessionInfo); 339 } catch (RemoteException ex) { 340 Log.w(TAG, "Failed to notify session created."); 341 } 342 } 343 } 344 345 /** 346 * Notifies the system of the successful creation of a system media routing session. 347 * 348 * <p>This method must only be called as the result of a prior call to {@link 349 * #onCreateSystemRoutingSession}. 350 * 351 * <p>This method returns a {@link MediaStreams} instance that holds the media streams to route 352 * as part of the newly created routing session. May be null if system media capture failed, in 353 * which case you can ignore the return value, as you will receive a call to {@link 354 * #onReleaseSession} where you can clean up this session. {@link AudioRecord#startRecording()} 355 * must be called immediately on {@link MediaStreams#getAudioRecord()} after calling this 356 * method, in order to start streaming audio to the receiver. 357 * 358 * @param requestId the ID of the {@link #onCreateSystemRoutingSession} request which this call 359 * is in response to. 360 * @param sessionInfo a {@link RoutingSessionInfo} that describes the newly created routing 361 * session. 362 * @param formats the {@link MediaStreamsFormats} that describes the format for the {@link 363 * MediaStreams} to return. 364 * @return The {@link MediaStreams} to route as part of the new session, or null if system media 365 * capture failed and the result can be ignored. 366 * @throws IllegalStateException If the provided {@code requestId} doesn't correspond to a 367 * previous call to {@link #onCreateSystemRoutingSession}. 368 */ 369 @FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2) 370 @RequiresPermission(Manifest.permission.MODIFY_AUDIO_ROUTING) 371 @Nullable notifySystemRoutingSessionCreated( long requestId, @NonNull RoutingSessionInfo sessionInfo, @NonNull MediaStreamsFormats formats)372 public final MediaStreams notifySystemRoutingSessionCreated( 373 long requestId, 374 @NonNull RoutingSessionInfo sessionInfo, 375 @NonNull MediaStreamsFormats formats) { 376 requireNonNull(sessionInfo, "sessionInfo must not be null"); 377 requireNonNull(formats, "formats must not be null"); 378 if (DEBUG) { 379 Log.d( 380 TAG, 381 "notifySystemRoutingSessionCreated: Creating a session. requestId=" 382 + requestId 383 + ", sessionInfo=" 384 + sessionInfo); 385 } 386 387 Integer uid; 388 synchronized (mRequestIdsLock) { 389 uid = mSystemRoutingSessionCreationRequests.get(requestId); 390 mSystemRoutingSessionCreationRequests.remove(requestId); 391 } 392 393 if (uid == null) { 394 throw new IllegalStateException( 395 "Unexpected system routing session created (request id=" 396 + requestId 397 + "):" 398 + sessionInfo); 399 } 400 401 if (mRemoteCallback == null) { 402 throw new IllegalStateException("Unexpected: remote callback is null."); 403 } 404 405 int routingTypes = 0; 406 var providerInfo = mProviderInfo; 407 for (String selectedRouteId : sessionInfo.getSelectedRoutes()) { 408 MediaRoute2Info route = providerInfo.mRoutes.get(selectedRouteId); 409 if (route == null) { 410 throw new IllegalArgumentException( 411 "Invalid selected route with id: " + selectedRouteId); 412 } 413 routingTypes |= route.getSupportedRoutingTypes(); 414 } 415 416 if ((routingTypes & MediaRoute2Info.FLAG_ROUTING_TYPE_SYSTEM_AUDIO) == 0) { 417 // TODO: b/380431086 - Populate video stream once we add support for video. 418 throw new IllegalArgumentException( 419 "Selected routes for system media don't support any system media routing" 420 + " types."); 421 } 422 423 AudioFormat audioFormat = formats.mAudioFormat; 424 var mediaStreamsBuilder = new MediaStreams.Builder(sessionInfo); 425 if (audioFormat != null) { 426 populateAudioStream(audioFormat, uid, mediaStreamsBuilder); 427 } 428 // TODO: b/380431086 - Populate video stream once we add support for video. 429 430 MediaStreams streams = mediaStreamsBuilder.build(); 431 var audioRecord = streams.mAudioRecord; 432 if (audioRecord == null) { 433 Log.e( 434 TAG, 435 "Audio record is not populated. Returning an empty stream and scheduling the" 436 + " session release for: " 437 + sessionInfo); 438 mHandler.post(() -> onReleaseSession(REQUEST_ID_NONE, sessionInfo.getOriginalId())); 439 notifyRequestFailed(requestId, REASON_FAILED_TO_REROUTE_SYSTEM_MEDIA); 440 return null; 441 } 442 443 synchronized (mSessionLock) { 444 try { 445 mRemoteCallback.notifySessionCreated(requestId, sessionInfo); 446 } catch (RemoteException ex) { 447 ex.rethrowFromSystemServer(); 448 } 449 mOngoingMediaStreams.put(sessionInfo.getOriginalId(), streams); 450 return streams; 451 } 452 } 453 454 @RequiresPermission(Manifest.permission.MODIFY_AUDIO_ROUTING) populateAudioStream( AudioFormat audioFormat, int uid, MediaStreams.Builder builder)455 private void populateAudioStream( 456 AudioFormat audioFormat, int uid, MediaStreams.Builder builder) { 457 var audioAttributes = 458 new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build(); 459 var audioMixingRuleBuilder = 460 new AudioMixingRule.Builder() 461 .addRule(audioAttributes, AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE); 462 if (uid != Process.INVALID_UID) { 463 audioMixingRuleBuilder.addMixRule(AudioMixingRule.RULE_MATCH_UID, uid); 464 } 465 AudioMix mix = 466 new AudioMix.Builder(audioMixingRuleBuilder.build()) 467 .setFormat(audioFormat) 468 .setRouteFlags(AudioMix.ROUTE_FLAG_LOOP_BACK) 469 .build(); 470 AudioPolicy audioPolicy = 471 new AudioPolicy.Builder(this).setLooper(mHandler.getLooper()).addMix(mix).build(); 472 var audioManager = getSystemService(AudioManager.class); 473 if (audioManager == null) { 474 Log.e(TAG, "Couldn't fetch the audio manager."); 475 return; 476 } 477 int audioPolicyResult = audioManager.registerAudioPolicy(audioPolicy); 478 if (audioPolicyResult != AudioManager.SUCCESS) { 479 Log.e(TAG, "Failed to register the audio policy."); 480 return; 481 } 482 var audioRecord = audioPolicy.createAudioRecordSink(mix); 483 if (audioRecord == null) { 484 Log.e(TAG, "Audio record creation failed."); 485 audioManager.unregisterAudioPolicy(audioPolicy); 486 return; 487 } 488 builder.setAudioStream(audioPolicy, audioRecord); 489 } 490 491 /** 492 * Notifies the existing session is updated. For example, when 493 * {@link RoutingSessionInfo#getSelectedRoutes() selected routes} are changed. 494 */ notifySessionUpdated(@onNull RoutingSessionInfo sessionInfo)495 public final void notifySessionUpdated(@NonNull RoutingSessionInfo sessionInfo) { 496 requireNonNull(sessionInfo, "sessionInfo must not be null"); 497 498 if (DEBUG) { 499 Log.d(TAG, "notifySessionUpdated: Updating session id=" + sessionInfo); 500 } 501 502 String sessionId = sessionInfo.getId(); 503 synchronized (mSessionLock) { 504 var mediaStreams = mOngoingMediaStreams.get(sessionId); 505 if (Flags.enableMirroringInMediaRouter2() && mediaStreams != null) { 506 mediaStreams.mSessionInfo = sessionInfo; 507 } else if (mSessionInfos.containsKey(sessionId)) { 508 mSessionInfos.put(sessionId, sessionInfo); 509 } else { 510 Log.w(TAG, "notifySessionUpdated: Ignoring unknown session info."); 511 return; 512 } 513 } 514 scheduleUpdateSessions(); 515 } 516 517 /** 518 * Notifies that the session is released. 519 * 520 * @param sessionId the ID of the released session. 521 * @see #onReleaseSession(long, String) 522 */ notifySessionReleased(@onNull String sessionId)523 public final void notifySessionReleased(@NonNull String sessionId) { 524 if (TextUtils.isEmpty(sessionId)) { 525 throw new IllegalArgumentException("sessionId must not be empty"); 526 } 527 if (DEBUG) { 528 Log.d(TAG, "notifySessionReleased: Releasing session id=" + sessionId); 529 } 530 531 RoutingSessionInfo sessionInfo; 532 synchronized (mSessionLock) { 533 sessionInfo = mSessionInfos.remove(sessionId); 534 if (Flags.enableMirroringInMediaRouter2()) { 535 if (sessionInfo == null) { 536 sessionInfo = maybeReleaseMediaStreams(sessionId); 537 } 538 if (sessionInfo == null) { 539 sessionInfo = mPendingSystemSessionReleases.remove(sessionId); 540 } 541 } 542 if (sessionInfo == null) { 543 Log.w(TAG, "notifySessionReleased: Ignoring unknown session info."); 544 return; 545 } 546 547 if (mRemoteCallback == null) { 548 return; 549 } 550 try { 551 mRemoteCallback.notifySessionReleased(sessionInfo); 552 } catch (RemoteException ex) { 553 Log.w(TAG, "Failed to notify session released.", ex); 554 } 555 } 556 } 557 558 /** 559 * Releases any system media routing resources associated with the given {@code sessionId}. 560 * 561 * @return The {@link RoutingSessionInfo} that corresponds to the released media streams, or 562 * null if no streams were released. 563 */ 564 @Nullable maybeReleaseMediaStreams(String sessionId)565 private RoutingSessionInfo maybeReleaseMediaStreams(String sessionId) { 566 if (!Flags.enableMirroringInMediaRouter2()) { 567 return null; 568 } 569 synchronized (mSessionLock) { 570 var streams = mOngoingMediaStreams.remove(sessionId); 571 if (streams != null) { 572 releaseAudioStream(streams.mAudioPolicy, streams.mAudioRecord); 573 // TODO: b/380431086: Release the video stream once implemented. 574 return streams.mSessionInfo; 575 } 576 } 577 return null; 578 } 579 580 // We cannot reach the code that requires MODIFY_AUDIO_ROUTING without holding it. 581 @SuppressWarnings("MissingPermission") releaseAudioStream(AudioPolicy audioPolicy, AudioRecord audioRecord)582 private void releaseAudioStream(AudioPolicy audioPolicy, AudioRecord audioRecord) { 583 if (audioPolicy == null) { 584 return; 585 } 586 var audioManager = getSystemService(AudioManager.class); 587 if (audioManager == null) { 588 return; 589 } 590 audioRecord.stop(); 591 audioManager.unregisterAudioPolicy(audioPolicy); 592 } 593 594 /** 595 * Notifies to the client that the request has failed. 596 * 597 * @param requestId the ID of the previous request 598 * @param reason the reason why the request has failed 599 * 600 * @see #REASON_UNKNOWN_ERROR 601 * @see #REASON_REJECTED 602 * @see #REASON_NETWORK_ERROR 603 * @see #REASON_ROUTE_NOT_AVAILABLE 604 * @see #REASON_INVALID_COMMAND 605 */ notifyRequestFailed(long requestId, @Reason int reason)606 public final void notifyRequestFailed(long requestId, @Reason int reason) { 607 if (mRemoteCallback == null) { 608 return; 609 } 610 611 if (!removeRequestId(requestId)) { 612 Log.w(TAG, "notifyRequestFailed: The requestId doesn't exist. requestId=" 613 + requestId); 614 return; 615 } 616 617 try { 618 mRemoteCallback.notifyRequestFailed(requestId, reason); 619 } catch (RemoteException ex) { 620 Log.w(TAG, "Failed to notify that the request has failed."); 621 } 622 } 623 624 /** 625 * Called when the service receives a request to create a session. 626 * <p> 627 * You should create and maintain your own session and notifies the client of 628 * session info. Call {@link #notifySessionCreated(long, RoutingSessionInfo)} 629 * with the given {@code requestId} to notify the information of a new session. 630 * The created session must have the same route feature and must include the given route 631 * specified by {@code routeId}. 632 * <p> 633 * If the session can be controlled, you can optionally pass the control hints to 634 * {@link RoutingSessionInfo.Builder#setControlHints(Bundle)}. Control hints is a 635 * {@link Bundle} which contains how to control the session. 636 * <p> 637 * If you can't create the session or want to reject the request, call 638 * {@link #notifyRequestFailed(long, int)} with the given {@code requestId}. 639 * 640 * @param requestId the ID of this request 641 * @param packageName the package name of the application that selected the route 642 * @param routeId the ID of the route initially being connected 643 * @param sessionHints an optional bundle of app-specific arguments sent by 644 * {@link MediaRouter2}, or null if none. The contents of this bundle 645 * may affect the result of session creation. 646 * 647 * @see RoutingSessionInfo.Builder#Builder(String, String) 648 * @see RoutingSessionInfo.Builder#addSelectedRoute(String) 649 * @see RoutingSessionInfo.Builder#setControlHints(Bundle) 650 */ onCreateSession(long requestId, @NonNull String packageName, @NonNull String routeId, @Nullable Bundle sessionHints)651 public abstract void onCreateSession(long requestId, @NonNull String packageName, 652 @NonNull String routeId, @Nullable Bundle sessionHints); 653 654 /** 655 * Called when the service receives a request to create a system routing session. 656 * 657 * <p>This method must be overridden by subclasses that support routes that support routing 658 * {@link MediaRoute2Info#getSupportedRoutingTypes() system media}. The provided {@code routeId} 659 * will always correspond to a route that supports routing of the system media, as per {@link 660 * MediaRoute2Info#getSupportedRoutingTypes()}. 661 * 662 * <p>Implementors of this method must call {@link #notifySystemRoutingSessionCreated} with the 663 * given {@code requestId} to indicate a successful session creation. If the session creation 664 * fails (for example, if the connection to the receiver device fails), the implementor must 665 * call {@link #notifyRequestFailed}, passing the {@code requestId}. 666 * 667 * <p>Unlike {@link #onCreateSession}, system sessions route the system media (for example, 668 * audio and/or video) which is to be retrieved by calling {@link 669 * #notifySystemRoutingSessionCreated}. 670 * 671 * <p>Changes to the session can be notified by calling {@link #notifySessionUpdated}. 672 * 673 * @param requestId the ID of this request 674 * @param routeId the ID of the route initially being {@link 675 * RoutingSessionInfo#getSelectedRoutes() selected}. 676 * @param parameters {@link SystemRoutingSessionParams} for the session creation. 677 * @see RoutingSessionInfo.Builder 678 * @see #notifySystemRoutingSessionCreated 679 */ 680 @FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2) onCreateSystemRoutingSession( long requestId, @NonNull String routeId, @NonNull SystemRoutingSessionParams parameters)681 public void onCreateSystemRoutingSession( 682 long requestId, 683 @NonNull String routeId, 684 @NonNull SystemRoutingSessionParams parameters) { 685 mHandler.post(() -> notifyRequestFailed(requestId, REASON_UNIMPLEMENTED)); 686 } 687 688 /** 689 * Called when the session should be released. A client of the session or system can request 690 * a session to be released. 691 * <p> 692 * After releasing the session, call {@link #notifySessionReleased(String)} 693 * with the ID of the released session. 694 * 695 * Note: Calling {@link #notifySessionReleased(String)} will <em>NOT</em> trigger 696 * this method to be called. 697 * 698 * @param requestId the ID of this request 699 * @param sessionId the ID of the session being released. 700 * @see #notifySessionReleased(String) 701 * @see #getSessionInfo(String) 702 */ onReleaseSession(long requestId, @NonNull String sessionId)703 public abstract void onReleaseSession(long requestId, @NonNull String sessionId); 704 705 /** 706 * Called when a client requests selecting a route for the session. 707 * After the route is selected, call {@link #notifySessionUpdated(RoutingSessionInfo)} 708 * to update session info. 709 * 710 * @param requestId the ID of this request 711 * @param sessionId the ID of the session 712 * @param routeId the ID of the route 713 */ onSelectRoute(long requestId, @NonNull String sessionId, @NonNull String routeId)714 public abstract void onSelectRoute(long requestId, @NonNull String sessionId, 715 @NonNull String routeId); 716 717 /** 718 * Called when a client requests deselecting a route from the session. 719 * After the route is deselected, call {@link #notifySessionUpdated(RoutingSessionInfo)} 720 * to update session info. 721 * 722 * @param requestId the ID of this request 723 * @param sessionId the ID of the session 724 * @param routeId the ID of the route 725 */ onDeselectRoute(long requestId, @NonNull String sessionId, @NonNull String routeId)726 public abstract void onDeselectRoute(long requestId, @NonNull String sessionId, 727 @NonNull String routeId); 728 729 /** 730 * Called when a client requests transferring a session to a route. 731 * After the transfer is finished, call {@link #notifySessionUpdated(RoutingSessionInfo)} 732 * to update session info. 733 * 734 * @param requestId the ID of this request 735 * @param sessionId the ID of the session 736 * @param routeId the ID of the route 737 */ onTransferToRoute(long requestId, @NonNull String sessionId, @NonNull String routeId)738 public abstract void onTransferToRoute(long requestId, @NonNull String sessionId, 739 @NonNull String routeId); 740 741 /** 742 * Called when the {@link RouteDiscoveryPreference discovery preference} has changed. 743 * <p> 744 * Whenever an application registers a {@link MediaRouter2.RouteCallback callback}, 745 * it also provides a discovery preference to specify features of routes that it is interested 746 * in. The media router combines all of these discovery request into a single discovery 747 * preference and notifies each provider. 748 * </p><p> 749 * The provider should examine {@link RouteDiscoveryPreference#getPreferredFeatures() 750 * preferred features} in the discovery preference to determine what kind of routes it should 751 * try to discover and whether it should perform active or passive scans. In many cases, 752 * the provider may be able to save power by not performing any scans when the request doesn't 753 * have any matching route features. 754 * </p> 755 * 756 * @param preference the new discovery preference 757 */ onDiscoveryPreferenceChanged(@onNull RouteDiscoveryPreference preference)758 public void onDiscoveryPreferenceChanged(@NonNull RouteDiscoveryPreference preference) {} 759 760 /** 761 * Updates routes of the provider and notifies the system media router service. 762 * 763 * @throws IllegalArgumentException If {@code routes} contains a route that {@link 764 * MediaRoute2Info#getSupportedRoutingTypes() supports} both system media routing and remote 765 * routing but doesn't contain any {@link MediaRoute2Info#getDeduplicationIds() 766 * deduplication ids}. 767 */ notifyRoutes(@onNull Collection<MediaRoute2Info> routes)768 public final void notifyRoutes(@NonNull Collection<MediaRoute2Info> routes) { 769 requireNonNull(routes, "routes must not be null"); 770 List<MediaRoute2Info> sanitizedRoutes = new ArrayList<>(routes.size()); 771 772 for (MediaRoute2Info route : routes) { 773 if (Flags.enableMirroringInMediaRouter2() 774 && route.supportsRemoteRouting() 775 && route.supportsSystemMediaRouting() 776 && route.getDeduplicationIds().isEmpty()) { 777 String errorMessage = 778 TextUtils.formatSimple( 779 "Route with id='%s' name='%s' supports both system media and remote" 780 + " type routing, but doesn't contain a deduplication id, which" 781 + " it needs. You can add the route id as a deduplication id.", 782 route.getOriginalId(), route.getName()); 783 throw new IllegalArgumentException(errorMessage); 784 } 785 if (route.isSystemRouteType()) { 786 Log.w( 787 TAG, 788 "Attempting to add a system route type from a non-system route " 789 + "provider. Overriding type to TYPE_UNKNOWN. Route: " 790 + route); 791 sanitizedRoutes.add( 792 new MediaRoute2Info.Builder(route) 793 .setType(MediaRoute2Info.TYPE_UNKNOWN) 794 .build()); 795 } else { 796 sanitizedRoutes.add(route); 797 } 798 } 799 800 mProviderInfo = new MediaRoute2ProviderInfo.Builder().addRoutes(sanitizedRoutes).build(); 801 schedulePublishState(); 802 } 803 setCallback(IMediaRoute2ProviderServiceCallback callback)804 void setCallback(IMediaRoute2ProviderServiceCallback callback) { 805 mRemoteCallback = callback; 806 schedulePublishState(); 807 scheduleUpdateSessions(); 808 } 809 schedulePublishState()810 void schedulePublishState() { 811 if (mStatePublishScheduled.compareAndSet(false, true)) { 812 mHandler.post(this::publishState); 813 } 814 } 815 publishState()816 private void publishState() { 817 if (!mStatePublishScheduled.compareAndSet(true, false)) { 818 return; 819 } 820 821 if (mRemoteCallback == null) { 822 return; 823 } 824 825 if (mProviderInfo == null) { 826 return; 827 } 828 829 try { 830 mRemoteCallback.notifyProviderUpdated(mProviderInfo); 831 } catch (RemoteException ex) { 832 Log.w(TAG, "Failed to publish provider state.", ex); 833 } 834 } 835 scheduleUpdateSessions()836 void scheduleUpdateSessions() { 837 if (mSessionUpdateScheduled.compareAndSet(false, true)) { 838 mHandler.post(this::updateSessions); 839 } 840 } 841 updateSessions()842 private void updateSessions() { 843 if (!mSessionUpdateScheduled.compareAndSet(true, false)) { 844 return; 845 } 846 847 if (mRemoteCallback == null) { 848 return; 849 } 850 851 List<RoutingSessionInfo> sessions; 852 synchronized (mSessionLock) { 853 sessions = new ArrayList<>(mSessionInfos.values()); 854 if (Flags.enableMirroringInMediaRouter2()) { 855 mOngoingMediaStreams.values().forEach(it -> sessions.add(it.mSessionInfo)); 856 } 857 } 858 859 try { 860 mRemoteCallback.notifySessionsUpdated(sessions); 861 } catch (RemoteException ex) { 862 Log.w(TAG, "Failed to notify session info changed."); 863 } 864 865 } 866 867 /** 868 * Adds a requestId in the request ID list whose max size is {@link #MAX_REQUEST_IDS_SIZE}. 869 * When the max size is reached, the first element is removed (FIFO). 870 */ addRequestId(long requestId)871 private void addRequestId(long requestId) { 872 synchronized (mRequestIdsLock) { 873 if (mRequestIds.size() >= MAX_REQUEST_IDS_SIZE) { 874 mRequestIds.removeFirst(); 875 } 876 mRequestIds.addLast(requestId); 877 } 878 } 879 880 /** 881 * Removes the given {@code requestId} from received request ID list. 882 * <p> 883 * Returns whether the list contains the {@code requestId}. These are the cases when the list 884 * doesn't contain the given {@code requestId}: 885 * <ul> 886 * <li>This service has never received a request with the requestId. </li> 887 * <li>{@link #notifyRequestFailed} or {@link #notifySessionCreated} already has been called 888 * for the requestId. </li> 889 * </ul> 890 */ removeRequestId(long requestId)891 private boolean removeRequestId(long requestId) { 892 synchronized (mRequestIdsLock) { 893 return mRequestIds.removeFirstOccurrence(requestId); 894 } 895 } 896 897 final class MediaRoute2ProviderServiceStub extends IMediaRoute2ProviderService.Stub { MediaRoute2ProviderServiceStub()898 MediaRoute2ProviderServiceStub() { } 899 checkCallerIsSystem()900 private boolean checkCallerIsSystem() { 901 return Binder.getCallingUid() == Process.SYSTEM_UID; 902 } 903 checkSessionIdIsValid(String sessionId, String description)904 private boolean checkSessionIdIsValid(String sessionId, String description) { 905 if (TextUtils.isEmpty(sessionId)) { 906 Log.w(TAG, description + ": Ignoring empty sessionId from system service."); 907 return false; 908 } 909 boolean idMatchesSystemSession = false; 910 if (Flags.enableMirroringInMediaRouter2()) { 911 synchronized (mSessionLock) { 912 idMatchesSystemSession = mOngoingMediaStreams.containsKey(sessionId); 913 } 914 } 915 if (!idMatchesSystemSession && getSessionInfo(sessionId) == null) { 916 Log.w(TAG, description + ": Ignoring unknown session from system service. " 917 + "sessionId=" + sessionId); 918 return false; 919 } 920 return true; 921 } 922 checkRouteIdIsValid(String routeId, String description)923 private boolean checkRouteIdIsValid(String routeId, String description) { 924 if (TextUtils.isEmpty(routeId)) { 925 Log.w(TAG, description + ": Ignoring empty routeId from system service."); 926 return false; 927 } 928 if (mProviderInfo == null || mProviderInfo.getRoute(routeId) == null) { 929 Log.w(TAG, description + ": Ignoring unknown route from system service. " 930 + "routeId=" + routeId); 931 return false; 932 } 933 return true; 934 } 935 936 @Override setCallback(IMediaRoute2ProviderServiceCallback callback)937 public void setCallback(IMediaRoute2ProviderServiceCallback callback) { 938 if (!checkCallerIsSystem()) { 939 return; 940 } 941 mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::setCallback, 942 MediaRoute2ProviderService.this, callback)); 943 } 944 945 @Override updateDiscoveryPreference(RouteDiscoveryPreference discoveryPreference)946 public void updateDiscoveryPreference(RouteDiscoveryPreference discoveryPreference) { 947 if (!checkCallerIsSystem()) { 948 return; 949 } 950 mHandler.sendMessage(obtainMessage( 951 MediaRoute2ProviderService::onDiscoveryPreferenceChanged, 952 MediaRoute2ProviderService.this, discoveryPreference)); 953 } 954 955 @Override setRouteVolume(long requestId, String routeId, int volume)956 public void setRouteVolume(long requestId, String routeId, int volume) { 957 if (!checkCallerIsSystem()) { 958 return; 959 } 960 if (!checkRouteIdIsValid(routeId, "setRouteVolume")) { 961 return; 962 } 963 addRequestId(requestId); 964 mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::onSetRouteVolume, 965 MediaRoute2ProviderService.this, requestId, routeId, volume)); 966 } 967 968 @Override requestCreateSession(long requestId, String packageName, String routeId, @Nullable Bundle requestCreateSession)969 public void requestCreateSession(long requestId, String packageName, String routeId, 970 @Nullable Bundle requestCreateSession) { 971 if (!checkCallerIsSystem()) { 972 return; 973 } 974 if (!checkRouteIdIsValid(routeId, "requestCreateSession")) { 975 return; 976 } 977 addRequestId(requestId); 978 mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::onCreateSession, 979 MediaRoute2ProviderService.this, requestId, packageName, routeId, 980 requestCreateSession)); 981 } 982 983 @Override requestCreateSystemMediaSession( long requestId, int uid, String packageName, String routeId, @Nullable Bundle extras)984 public void requestCreateSystemMediaSession( 985 long requestId, 986 int uid, 987 String packageName, 988 String routeId, 989 @Nullable Bundle extras) { 990 if (!Flags.enableMirroringInMediaRouter2() || !checkCallerIsSystem()) { 991 return; 992 } 993 if (!checkRouteIdIsValid(routeId, "requestCreateSession")) { 994 return; 995 } 996 synchronized (mRequestIdsLock) { 997 mSystemRoutingSessionCreationRequests.put(requestId, uid); 998 } 999 var sessionParamsBuilder = 1000 new SystemRoutingSessionParams.Builder().setPackageName(packageName); 1001 if (extras != null) { 1002 sessionParamsBuilder.setExtras(extras); 1003 } 1004 var sessionParams = sessionParamsBuilder.build(); 1005 mHandler.sendMessage( 1006 obtainMessage( 1007 MediaRoute2ProviderService::onCreateSystemRoutingSession, 1008 MediaRoute2ProviderService.this, 1009 requestId, 1010 routeId, 1011 sessionParams)); 1012 } 1013 1014 @Override selectRoute(long requestId, String sessionId, String routeId)1015 public void selectRoute(long requestId, String sessionId, String routeId) { 1016 if (!checkCallerIsSystem()) { 1017 return; 1018 } 1019 if (!checkSessionIdIsValid(sessionId, "selectRoute") 1020 || !checkRouteIdIsValid(routeId, "selectRoute")) { 1021 return; 1022 } 1023 addRequestId(requestId); 1024 mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::onSelectRoute, 1025 MediaRoute2ProviderService.this, requestId, sessionId, routeId)); 1026 } 1027 1028 @Override deselectRoute(long requestId, String sessionId, String routeId)1029 public void deselectRoute(long requestId, String sessionId, String routeId) { 1030 if (!checkCallerIsSystem()) { 1031 return; 1032 } 1033 if (!checkSessionIdIsValid(sessionId, "deselectRoute") 1034 || !checkRouteIdIsValid(routeId, "deselectRoute")) { 1035 return; 1036 } 1037 addRequestId(requestId); 1038 mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::onDeselectRoute, 1039 MediaRoute2ProviderService.this, requestId, sessionId, routeId)); 1040 } 1041 1042 @Override transferToRoute(long requestId, String sessionId, String routeId)1043 public void transferToRoute(long requestId, String sessionId, String routeId) { 1044 if (!checkCallerIsSystem()) { 1045 return; 1046 } 1047 if (!checkSessionIdIsValid(sessionId, "transferToRoute") 1048 || !checkRouteIdIsValid(routeId, "transferToRoute")) { 1049 return; 1050 } 1051 addRequestId(requestId); 1052 mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::onTransferToRoute, 1053 MediaRoute2ProviderService.this, requestId, sessionId, routeId)); 1054 } 1055 1056 @Override setSessionVolume(long requestId, String sessionId, int volume)1057 public void setSessionVolume(long requestId, String sessionId, int volume) { 1058 if (!checkCallerIsSystem()) { 1059 return; 1060 } 1061 if (!checkSessionIdIsValid(sessionId, "setSessionVolume")) { 1062 return; 1063 } 1064 addRequestId(requestId); 1065 mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::onSetSessionVolume, 1066 MediaRoute2ProviderService.this, requestId, sessionId, volume)); 1067 } 1068 1069 @Override releaseSession(long requestId, String sessionId)1070 public void releaseSession(long requestId, String sessionId) { 1071 if (!checkCallerIsSystem()) { 1072 return; 1073 } 1074 synchronized (mSessionLock) { 1075 // We proactively release the system media routing session resources when the 1076 // system requests it, to ensure it happens immediately. 1077 RoutingSessionInfo releasedSession = maybeReleaseMediaStreams(sessionId); 1078 if (releasedSession != null) { 1079 mPendingSystemSessionReleases.put(sessionId, releasedSession); 1080 } else if (!checkSessionIdIsValid(sessionId, "releaseSession")) { 1081 return; 1082 } 1083 } 1084 1085 addRequestId(requestId); 1086 mHandler.sendMessage(obtainMessage(MediaRoute2ProviderService::onReleaseSession, 1087 MediaRoute2ProviderService.this, requestId, sessionId)); 1088 } 1089 } 1090 1091 /** 1092 * Holds the streams to be routed as part of a {@link #onCreateSystemRoutingSession system media 1093 * routing session}. 1094 * 1095 * <p>The encoded data format will match the {@link MediaStreamsFormats} passed to {@link 1096 * #notifySystemRoutingSessionCreated}. 1097 */ 1098 @FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2) 1099 public static final class MediaStreams { 1100 1101 @Nullable private final AudioPolicy mAudioPolicy; 1102 @Nullable private final AudioRecord mAudioRecord; 1103 1104 /** 1105 * Holds the last {@link RoutingSessionInfo} associated with these streams. 1106 */ 1107 @NonNull 1108 // Access guarded by mSessionsLock, but it's not convenient to enforce through @GuardedBy. 1109 private RoutingSessionInfo mSessionInfo; 1110 1111 // TODO: b/380431086: Add the video equivalent. 1112 MediaStreams(Builder builder)1113 private MediaStreams(Builder builder) { 1114 this.mSessionInfo = builder.mSessionInfo; 1115 this.mAudioPolicy = builder.mAudioPolicy; 1116 this.mAudioRecord = builder.mAudioRecord; 1117 } 1118 1119 /** 1120 * Returns the {@link AudioRecord} from which to read the audio data to route, or null if 1121 * the routing session doesn't include audio. 1122 */ 1123 @Nullable getAudioRecord()1124 public AudioRecord getAudioRecord() { 1125 return mAudioRecord; 1126 } 1127 1128 /** 1129 * Builder for {@link MediaStreams}. 1130 * 1131 * @hide 1132 */ 1133 public static final class Builder { 1134 1135 @NonNull private RoutingSessionInfo mSessionInfo; 1136 @Nullable private AudioPolicy mAudioPolicy; 1137 @Nullable private AudioRecord mAudioRecord; 1138 1139 /** 1140 * Constructor. 1141 * 1142 * @param sessionInfo The {@link RoutingSessionInfo} associated with these streams. 1143 */ Builder(@onNull RoutingSessionInfo sessionInfo)1144 Builder(@NonNull RoutingSessionInfo sessionInfo) { 1145 mSessionInfo = requireNonNull(sessionInfo); 1146 } 1147 1148 /** Populates system media audio-related structures. */ setAudioStream( @onNull AudioPolicy audioPolicy, @NonNull AudioRecord audioRecord)1149 public Builder setAudioStream( 1150 @NonNull AudioPolicy audioPolicy, @NonNull AudioRecord audioRecord) { 1151 mAudioPolicy = requireNonNull(audioPolicy); 1152 mAudioRecord = requireNonNull(audioRecord); 1153 return this; 1154 } 1155 1156 /** Builds a {@link MediaStreams} instance. */ build()1157 public MediaStreams build() { 1158 return new MediaStreams(this); 1159 } 1160 } 1161 } 1162 1163 /** 1164 * Holds parameters associated with a {@link #onCreateSystemRoutingSession session creation 1165 * request}. 1166 */ 1167 @FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2) 1168 public static final class SystemRoutingSessionParams { 1169 1170 private final String mPackageName; 1171 private final Bundle mExtras; 1172 SystemRoutingSessionParams(Builder builder)1173 private SystemRoutingSessionParams(Builder builder) { 1174 this.mPackageName = builder.mPackageName; 1175 this.mExtras = builder.mExtras; 1176 } 1177 1178 /** 1179 * Returns the name of the package associated with the session, or an empty string if not 1180 * applicable. 1181 * 1182 * <p>The package name is not applicable if the session is not associated with a specific 1183 * package, for example is the session affects the entire system. 1184 */ 1185 @FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2) 1186 @NonNull getPackageName()1187 public String getPackageName() { 1188 return mPackageName; 1189 } 1190 1191 /** Returns a bundle provided by the client that triggered the session creation request. */ 1192 @FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2) 1193 @NonNull getExtras()1194 public Bundle getExtras() { 1195 return mExtras; 1196 } 1197 1198 /** A builder for {@link SystemRoutingSessionParams}. */ 1199 public static final class Builder { 1200 private String mPackageName; 1201 private Bundle mExtras; 1202 1203 /** Constructor. */ Builder()1204 public Builder() { 1205 mPackageName = ""; 1206 mExtras = Bundle.EMPTY; 1207 } 1208 1209 /** 1210 * Sets the {@link #getExtras() extras}. 1211 * 1212 * <p>The default value is an empty {@link Bundle}. 1213 * 1214 * <p>Do not mutate the given {@link Bundle} after passing it to this method. You can 1215 * use {@link Bundle#deepCopy()} to keep a mutable copy. 1216 */ 1217 @NonNull setExtras(@onNull Bundle extras)1218 public Builder setExtras(@NonNull Bundle extras) { 1219 mExtras = Objects.requireNonNull(extras); 1220 return this; 1221 } 1222 1223 /** 1224 * Sets the {@link #getPackageName()}. 1225 * 1226 * <p>The default value is an empty string. 1227 */ 1228 @NonNull setPackageName(@onNull String packageName)1229 public Builder setPackageName(@NonNull String packageName) { 1230 mPackageName = Objects.requireNonNull(packageName); 1231 return this; 1232 } 1233 1234 /** Returns a new {@link SystemRoutingSessionParams} instance. */ 1235 @NonNull build()1236 public SystemRoutingSessionParams build() { 1237 return new SystemRoutingSessionParams(this); 1238 } 1239 } 1240 } 1241 1242 /** 1243 * Holds the formats to encode media data to be read from {@link MediaStreams}. 1244 * 1245 * @see MediaStreams 1246 * @see #notifySystemRoutingSessionCreated 1247 */ 1248 @FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2) 1249 public static final class MediaStreamsFormats { 1250 1251 private final AudioFormat mAudioFormat; 1252 1253 // TODO: b/380431086: Add the video equivalent. 1254 MediaStreamsFormats(Builder builder)1255 private MediaStreamsFormats(Builder builder) { 1256 this.mAudioFormat = builder.mAudioFormat; 1257 } 1258 1259 /** 1260 * Returns the audio format to use for creating the {@link MediaStreams#getAudioRecord} to 1261 * return from {@link #notifySystemRoutingSessionCreated}. May be null if the session 1262 * doesn't support system audio. 1263 */ 1264 @FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2) 1265 @Nullable getAudioFormat()1266 public AudioFormat getAudioFormat() { 1267 return mAudioFormat; 1268 } 1269 1270 /** 1271 * Builder for {@link MediaStreamsFormats} 1272 */ 1273 @FlaggedApi(Flags.FLAG_ENABLE_MIRRORING_IN_MEDIA_ROUTER_2) 1274 public static final class Builder { 1275 private AudioFormat mAudioFormat; 1276 1277 /** 1278 * Sets the audio format to use for creating the {@link MediaStreams#getAudioRecord} to 1279 * return from {@link #notifySystemRoutingSessionCreated}. 1280 * 1281 * @param audioFormat the audio format 1282 * @return this builder 1283 */ 1284 @NonNull setAudioFormat(@onNull AudioFormat audioFormat)1285 public Builder setAudioFormat(@NonNull AudioFormat audioFormat) { 1286 this.mAudioFormat = requireNonNull(audioFormat); 1287 return this; 1288 } 1289 1290 /** 1291 * Builds the {@link MediaStreamsFormats} instance. 1292 * 1293 * @return the built {@link MediaStreamsFormats} instance 1294 */ 1295 @NonNull build()1296 public MediaStreamsFormats build() { 1297 return new MediaStreamsFormats(this); 1298 } 1299 } 1300 } 1301 } 1302