• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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