• 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.media.flags.Flags.FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES;
20 
21 import android.annotation.FlaggedApi;
22 import android.annotation.IntDef;
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.os.Bundle;
26 import android.os.Parcel;
27 import android.os.Parcelable;
28 import android.os.UserHandle;
29 import android.text.TextUtils;
30 
31 import com.android.internal.util.Preconditions;
32 
33 import java.io.FileDescriptor;
34 import java.io.PrintWriter;
35 import java.lang.annotation.Retention;
36 import java.lang.annotation.RetentionPolicy;
37 import java.util.ArrayList;
38 import java.util.Collections;
39 import java.util.List;
40 import java.util.Objects;
41 
42 /**
43  * Describes a routing session which is created when a media route is selected.
44  */
45 public final class RoutingSessionInfo implements Parcelable {
46     @NonNull
47     public static final Creator<RoutingSessionInfo> CREATOR =
48             new Creator<RoutingSessionInfo>() {
49                 @Override
50                 public RoutingSessionInfo createFromParcel(Parcel in) {
51                     return new RoutingSessionInfo(in);
52                 }
53                 @Override
54                 public RoutingSessionInfo[] newArray(int size) {
55                     return new RoutingSessionInfo[size];
56                 }
57             };
58 
59     private static final String KEY_GROUP_ROUTE = "androidx.mediarouter.media.KEY_GROUP_ROUTE";
60     private static final String KEY_VOLUME_HANDLING = "volumeHandling";
61 
62     /**
63      * Indicates that the transfer happened by the default logic without explicit system's or user's
64      * request.
65      *
66      * <p>For example, an automatically connected Bluetooth device will have this transfer reason.
67      */
68     @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES)
69     public static final int TRANSFER_REASON_FALLBACK = 0;
70 
71     /** Indicates that the transfer happened from within a privileged application. */
72     @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES)
73     public static final int TRANSFER_REASON_SYSTEM_REQUEST = 1;
74 
75     /** Indicates that the transfer happened from a non-privileged app. */
76     @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES)
77     public static final int TRANSFER_REASON_APP = 2;
78 
79     /**
80      * Indicates the transfer reason.
81      *
82      * @hide
83      */
84     @IntDef(value = {TRANSFER_REASON_FALLBACK, TRANSFER_REASON_SYSTEM_REQUEST, TRANSFER_REASON_APP})
85     @Retention(RetentionPolicy.SOURCE)
86     public @interface TransferReason {}
87 
88     @NonNull final String mOriginalId;
89     @Nullable
90     final CharSequence mName;
91     @Nullable
92     final String mOwnerPackageName;
93     @NonNull
94     final String mClientPackageName;
95     @Nullable
96     final String mProviderId;
97     @NonNull
98     final List<String> mSelectedRoutes;
99     @NonNull
100     final List<String> mSelectableRoutes;
101     @NonNull
102     final List<String> mDeselectableRoutes;
103     @NonNull
104     final List<String> mTransferableRoutes;
105 
106     @MediaRoute2Info.PlaybackVolume final int mVolumeHandling;
107     final int mVolumeMax;
108     final int mVolume;
109 
110     @Nullable
111     final Bundle mControlHints;
112     final boolean mIsSystemSession;
113 
114     @TransferReason final int mTransferReason;
115 
116     @Nullable final UserHandle mTransferInitiatorUserHandle;
117     @Nullable final String mTransferInitiatorPackageName;
118 
RoutingSessionInfo(@onNull Builder builder)119     RoutingSessionInfo(@NonNull Builder builder) {
120         Objects.requireNonNull(builder, "builder must not be null.");
121 
122         mOriginalId = builder.mOriginalId;
123         mName = builder.mName;
124         mOwnerPackageName = builder.mOwnerPackageName;
125         mClientPackageName = builder.mClientPackageName;
126         mProviderId = builder.mProviderId;
127 
128         mSelectedRoutes = Collections.unmodifiableList(
129                 convertToUniqueRouteIds(builder.mSelectedRoutes));
130         mSelectableRoutes = Collections.unmodifiableList(
131                 convertToUniqueRouteIds(builder.mSelectableRoutes));
132         mDeselectableRoutes = Collections.unmodifiableList(
133                 convertToUniqueRouteIds(builder.mDeselectableRoutes));
134         mTransferableRoutes = Collections.unmodifiableList(
135                 convertToUniqueRouteIds(builder.mTransferableRoutes));
136 
137         mVolumeMax = builder.mVolumeMax;
138         mVolume = builder.mVolume;
139 
140         mIsSystemSession = builder.mIsSystemSession;
141         mVolumeHandling = builder.mVolumeHandling;
142 
143         mControlHints = updateVolumeHandlingInHints(builder.mControlHints, mVolumeHandling);
144         mTransferReason = builder.mTransferReason;
145         mTransferInitiatorUserHandle = builder.mTransferInitiatorUserHandle;
146         mTransferInitiatorPackageName = builder.mTransferInitiatorPackageName;
147     }
148 
RoutingSessionInfo(@onNull Parcel src)149     RoutingSessionInfo(@NonNull Parcel src) {
150         mOriginalId = src.readString();
151         Preconditions.checkArgument(!TextUtils.isEmpty(mOriginalId));
152 
153         mName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(src);
154         mOwnerPackageName = src.readString();
155         mClientPackageName = ensureString(src.readString());
156         mProviderId = src.readString();
157 
158         mSelectedRoutes = ensureList(src.createStringArrayList());
159         Preconditions.checkArgument(!mSelectedRoutes.isEmpty());
160 
161         mSelectableRoutes = ensureList(src.createStringArrayList());
162         mDeselectableRoutes = ensureList(src.createStringArrayList());
163         mTransferableRoutes = ensureList(src.createStringArrayList());
164 
165         mVolumeHandling = src.readInt();
166         mVolumeMax = src.readInt();
167         mVolume = src.readInt();
168 
169         mControlHints = src.readBundle();
170         mIsSystemSession = src.readBoolean();
171         mTransferReason = src.readInt();
172         mTransferInitiatorUserHandle = UserHandle.readFromParcel(src);
173         mTransferInitiatorPackageName = src.readString();
174     }
175 
176     @Nullable
updateVolumeHandlingInHints(@ullable Bundle controlHints, int volumeHandling)177     private static Bundle updateVolumeHandlingInHints(@Nullable Bundle controlHints,
178             int volumeHandling) {
179         // Workaround to preserve retro-compatibility with androidx.
180         // See b/228021646 for more details.
181         if (controlHints != null && controlHints.containsKey(KEY_GROUP_ROUTE)) {
182             Bundle groupRoute = controlHints.getBundle(KEY_GROUP_ROUTE);
183 
184             if (groupRoute != null && groupRoute.containsKey(KEY_VOLUME_HANDLING)
185                     && volumeHandling != groupRoute.getInt(KEY_VOLUME_HANDLING)) {
186                 //Creating copy of controlHints with updated value.
187                 Bundle newGroupRoute = new Bundle(groupRoute);
188                 newGroupRoute.putInt(KEY_VOLUME_HANDLING, volumeHandling);
189                 Bundle newControlHints = new Bundle(controlHints);
190                 newControlHints.putBundle(KEY_GROUP_ROUTE, newGroupRoute);
191                 return newControlHints;
192             }
193         }
194         //Return same Bundle.
195         return controlHints;
196     }
197 
198     @NonNull
ensureString(@ullable String str)199     private static String ensureString(@Nullable String str) {
200         return str != null ? str : "";
201     }
202 
203     @NonNull
ensureList(@ullable List<? extends T> list)204     private static <T> List<T> ensureList(@Nullable List<? extends T> list) {
205         if (list != null) {
206             return Collections.unmodifiableList(list);
207         }
208         return Collections.emptyList();
209     }
210 
211     /**
212      * Gets the id of the session. The sessions which are given by {@link MediaRouter2} will have
213      * unique IDs.
214      * <p>
215      * In order to ensure uniqueness in {@link MediaRouter2} side, the value of this method
216      * can be different from what was set in {@link MediaRoute2ProviderService}.
217      *
218      * @see Builder#Builder(String, String)
219      */
220     @NonNull
getId()221     public String getId() {
222         if (!TextUtils.isEmpty(mProviderId)) {
223             return MediaRouter2Utils.toUniqueId(mProviderId, mOriginalId);
224         } else {
225             return mOriginalId;
226         }
227     }
228 
229     /**
230      * Gets the user-visible name of the session. It may be {@code null}.
231      */
232     @Nullable
getName()233     public CharSequence getName() {
234         return mName;
235     }
236 
237     /**
238      * Gets the original id as assigned by the {@link MediaRoute2ProviderService route provider}.
239      *
240      * <p>This may be different from {@link #getId()}, which may convert this original id into a
241      * unique one by adding information about the provider that created this session info.
242      *
243      * @hide
244      */
245     @NonNull
getOriginalId()246     public String getOriginalId() {
247         return mOriginalId;
248     }
249 
250     /**
251      * Gets the package name of the session owner.
252      * @hide
253      */
254     @Nullable
getOwnerPackageName()255     public String getOwnerPackageName() {
256         return mOwnerPackageName;
257     }
258 
259     /**
260      * Gets the client package name of the session
261      */
262     @NonNull
getClientPackageName()263     public String getClientPackageName() {
264         return mClientPackageName;
265     }
266 
267     /**
268      * Gets the provider ID of the session.
269      *
270      * @hide
271      */
272     @Nullable
getProviderId()273     public String getProviderId() {
274         return mProviderId;
275     }
276 
277     /**
278      * Gets the list of IDs of selected routes for the session.
279      *
280      * <p>Selected routes are the routes that this session is actively routing media to.
281      *
282      * <p>The behavior of a routing session with multiple selected routes is ultimately defined by
283      * the {@link MediaRoute2ProviderService} implementation. However, typically, it's expected that
284      * all the selected routes of a routing session are playing the same media in sync.
285      *
286      * @return A non-empty list of selected route ids.
287      */
288     @NonNull
getSelectedRoutes()289     public List<String> getSelectedRoutes() {
290         return mSelectedRoutes;
291     }
292 
293     /**
294      * Gets the list of IDs of selectable routes for the session.
295      *
296      * <p>Selectable routes can be added to a routing session (via {@link
297      * MediaRouter2.RoutingController#selectRoute}) in order to add them to the {@link
298      * #getSelectedRoutes() selected routes}, so that media plays on the newly selected route along
299      * with the other selected routes.
300      *
301      * <p>Not to be confused with {@link #getTransferableRoutes() transferable routes}. Transferring
302      * to a route makes it the sole selected route.
303      *
304      * @return A possibly empty list of selectable route ids.
305      */
306     @NonNull
getSelectableRoutes()307     public List<String> getSelectableRoutes() {
308         return mSelectableRoutes;
309     }
310 
311     /**
312      * Gets the list of IDs of deselectable routes for the session.
313      *
314      * <p>Deselectable routes can be removed from the {@link #getSelectedRoutes() selected routes},
315      * so that the routing session stops routing to the newly deselected route, but continues on any
316      * remaining selected routes.
317      *
318      * <p>Deselectable routes should be a subset of the {@link #getSelectedRoutes() selected
319      * routes}, meaning not all of the selected routes might be deselectable. For example, one of
320      * the selected routes may be a leader device coordinating group playback, which must always
321      * remain selected while the session is active.
322      *
323      * @return A possibly empty list of deselectable route ids.
324      */
325     @NonNull
getDeselectableRoutes()326     public List<String> getDeselectableRoutes() {
327         return mDeselectableRoutes;
328     }
329 
330     /**
331      * Gets the list of IDs of transferable routes for the session.
332      *
333      * <p>Transferring to a route (for example, using {@link MediaRouter2#transferTo}) replaces the
334      * list of {@link #getSelectedRoutes() selected routes} with the target route, causing playback
335      * to move from one route to another.
336      *
337      * <p>Note that this is different from {@link #getSelectableRoutes() selectable routes}, because
338      * selecting a route makes it part of the selected routes, while transferring to a route makes
339      * it the selected route. A route can be both transferable and selectable.
340      *
341      * <p>Note that playback may transfer across routes without the target route being in the list
342      * of transferable routes. This can happen by creating a new routing session to the target
343      * route, and releasing the routing session being transferred from. The difference is that a
344      * transfer to a route in the transferable list can happen with no intervention from the app,
345      * with the route provider taking care of the entire operation. A transfer to a route that is
346      * not in the list of transferable routes (by creating a new session) requires the app to move
347      * the playback state from one device to the other.
348      *
349      * @return A possibly empty list of transferable route ids.
350      */
351     @NonNull
getTransferableRoutes()352     public List<String> getTransferableRoutes() {
353         return mTransferableRoutes;
354     }
355 
356     /**
357      * Gets the information about how volume is handled on the session.
358      *
359      * @return {@link MediaRoute2Info#PLAYBACK_VOLUME_FIXED} or
360      * {@link MediaRoute2Info#PLAYBACK_VOLUME_VARIABLE}.
361      */
362     @MediaRoute2Info.PlaybackVolume
getVolumeHandling()363     public int getVolumeHandling() {
364         return mVolumeHandling;
365     }
366 
367     /**
368      * Gets the maximum volume of the session.
369      */
getVolumeMax()370     public int getVolumeMax() {
371         return mVolumeMax;
372     }
373 
374     /**
375      * Gets the current volume of the session.
376      * <p>
377      * When it's available, it represents the volume of routing session, which is a group
378      * of selected routes. To get the volume of each route, use {@link MediaRoute2Info#getVolume()}.
379      * </p>
380      * @see MediaRoute2Info#getVolume()
381      */
getVolume()382     public int getVolume() {
383         return mVolume;
384     }
385 
386     /**
387      * Gets the control hints
388      */
389     @Nullable
getControlHints()390     public Bundle getControlHints() {
391         return mControlHints;
392     }
393 
394     /**
395      * Gets whether this session is in system media route provider.
396      * @hide
397      */
398     @Nullable
isSystemSession()399     public boolean isSystemSession() {
400         return mIsSystemSession;
401     }
402 
403     /** Returns the transfer reason for this routing session. */
404     @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES)
405     @TransferReason
getTransferReason()406     public int getTransferReason() {
407         return mTransferReason;
408     }
409 
410     /** @hide */
411     @Nullable
getTransferInitiatorUserHandle()412     public UserHandle getTransferInitiatorUserHandle() {
413         return mTransferInitiatorUserHandle;
414     }
415 
416     /** @hide */
417     @Nullable
getTransferInitiatorPackageName()418     public String getTransferInitiatorPackageName() {
419         return mTransferInitiatorPackageName;
420     }
421 
422     @Override
describeContents()423     public int describeContents() {
424         return 0;
425     }
426 
427     @Override
writeToParcel(@onNull Parcel dest, int flags)428     public void writeToParcel(@NonNull Parcel dest, int flags) {
429         dest.writeString(mOriginalId);
430         dest.writeCharSequence(mName);
431         dest.writeString(mOwnerPackageName);
432         dest.writeString(mClientPackageName);
433         dest.writeString(mProviderId);
434         dest.writeStringList(mSelectedRoutes);
435         dest.writeStringList(mSelectableRoutes);
436         dest.writeStringList(mDeselectableRoutes);
437         dest.writeStringList(mTransferableRoutes);
438         dest.writeInt(mVolumeHandling);
439         dest.writeInt(mVolumeMax);
440         dest.writeInt(mVolume);
441         dest.writeBundle(mControlHints);
442         dest.writeBoolean(mIsSystemSession);
443         dest.writeInt(mTransferReason);
444         UserHandle.writeToParcel(mTransferInitiatorUserHandle, dest);
445         dest.writeString(mTransferInitiatorPackageName);
446     }
447 
448     /**
449      * Dumps current state of the instance. Use with {@code dumpsys}.
450      *
451      * See {@link android.os.Binder#dump(FileDescriptor, PrintWriter, String[])}.
452      *
453      * @hide
454      */
dump(@onNull PrintWriter pw, @NonNull String prefix)455     public void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
456         pw.println(prefix + "RoutingSessionInfo");
457 
458         String indent = prefix + "  ";
459 
460         pw.println(indent + "mOriginalId=" + mOriginalId);
461         pw.println(indent + "mName=" + mName);
462         pw.println(indent + "mOwnerPackageName=" + mOwnerPackageName);
463         pw.println(indent + "mClientPackageName=" + mClientPackageName);
464         pw.println(indent + "mProviderId=" + mProviderId);
465         pw.println(indent + "mSelectedRoutes=" + mSelectedRoutes);
466         pw.println(indent + "mSelectableRoutes=" + mSelectableRoutes);
467         pw.println(indent + "mDeselectableRoutes=" + mDeselectableRoutes);
468         pw.println(indent + "mTransferableRoutes=" + mTransferableRoutes);
469         pw.println(indent + MediaRoute2Info.getVolumeString(mVolume, mVolumeMax, mVolumeHandling));
470         pw.println(indent + "mControlHints=" + mControlHints);
471         pw.println(indent + "mIsSystemSession=" + mIsSystemSession);
472         pw.println(indent + "mTransferReason=" + mTransferReason);
473         pw.println(indent + "mtransferInitiatorUserHandle=" + mTransferInitiatorUserHandle);
474         pw.println(indent + "mtransferInitiatorPackageName=" + mTransferInitiatorPackageName);
475     }
476 
477     @Override
equals(@ullable Object obj)478     public boolean equals(@Nullable Object obj) {
479         if (obj == null) {
480             return false;
481         }
482 
483         if (this == obj) {
484             return true;
485         }
486         if (getClass() != obj.getClass()) {
487             return false;
488         }
489 
490         RoutingSessionInfo other = (RoutingSessionInfo) obj;
491         return Objects.equals(mOriginalId, other.mOriginalId)
492                 && Objects.equals(mName, other.mName)
493                 && Objects.equals(mOwnerPackageName, other.mOwnerPackageName)
494                 && Objects.equals(mClientPackageName, other.mClientPackageName)
495                 && Objects.equals(mProviderId, other.mProviderId)
496                 && Objects.equals(mSelectedRoutes, other.mSelectedRoutes)
497                 && Objects.equals(mSelectableRoutes, other.mSelectableRoutes)
498                 && Objects.equals(mDeselectableRoutes, other.mDeselectableRoutes)
499                 && Objects.equals(mTransferableRoutes, other.mTransferableRoutes)
500                 && (mVolumeHandling == other.mVolumeHandling)
501                 && (mVolumeMax == other.mVolumeMax)
502                 && (mVolume == other.mVolume)
503                 && (mTransferReason == other.mTransferReason)
504                 && Objects.equals(mTransferInitiatorUserHandle, other.mTransferInitiatorUserHandle)
505                 && Objects.equals(
506                         mTransferInitiatorPackageName, other.mTransferInitiatorPackageName);
507     }
508 
509     @Override
hashCode()510     public int hashCode() {
511         return Objects.hash(
512                 mOriginalId,
513                 mName,
514                 mOwnerPackageName,
515                 mClientPackageName,
516                 mProviderId,
517                 mSelectedRoutes,
518                 mSelectableRoutes,
519                 mDeselectableRoutes,
520                 mTransferableRoutes,
521                 mVolumeMax,
522                 mVolumeHandling,
523                 mVolume,
524                 mTransferReason,
525                 mTransferInitiatorUserHandle,
526                 mTransferInitiatorPackageName);
527     }
528 
529     @Override
toString()530     public String toString() {
531         return new StringBuilder()
532                 .append("RoutingSessionInfo{ ")
533                 .append("sessionId=")
534                 .append(getId())
535                 .append(", name=")
536                 .append(getName())
537                 .append(", clientPackageName=")
538                 .append(getClientPackageName())
539                 .append(", selectedRoutes={")
540                 .append(String.join(",", getSelectedRoutes()))
541                 .append("}")
542                 .append(", selectableRoutes={")
543                 .append(String.join(",", getSelectableRoutes()))
544                 .append("}")
545                 .append(", deselectableRoutes={")
546                 .append(String.join(",", getDeselectableRoutes()))
547                 .append("}")
548                 .append(", transferableRoutes={")
549                 .append(String.join(",", getTransferableRoutes()))
550                 .append("}")
551                 .append(", ")
552                 .append(MediaRoute2Info.getVolumeString(mVolume, mVolumeMax, mVolumeHandling))
553                 .append(", transferReason=")
554                 .append(getTransferReason())
555                 .append(", transferInitiatorUserHandle=")
556                 .append(getTransferInitiatorUserHandle())
557                 .append(", transferInitiatorPackageName=")
558                 .append(getTransferInitiatorPackageName())
559                 .append(" }")
560                 .toString();
561     }
562 
563     /**
564      * Provides a new list with unique route IDs if {@link #mProviderId} is set, or the original IDs
565      * otherwise.
566      *
567      * @param routeIds list of route IDs to convert
568      * @return new list with unique IDs or original IDs
569      */
570 
571     @NonNull
convertToUniqueRouteIds(@onNull List<String> routeIds)572     private List<String> convertToUniqueRouteIds(@NonNull List<String> routeIds) {
573         Objects.requireNonNull(routeIds, "RouteIds cannot be null.");
574 
575         // mProviderId can be null if not set. Return the original list for this case.
576         if (TextUtils.isEmpty(mProviderId)) {
577             return new ArrayList<>(routeIds);
578         }
579 
580         List<String> result = new ArrayList<>();
581         for (String routeId : routeIds) {
582             result.add(MediaRouter2Utils.toUniqueId(mProviderId, routeId));
583         }
584         return result;
585     }
586 
587     /**
588      * Builder class for {@link RoutingSessionInfo}.
589      */
590     public static final class Builder {
591         @NonNull private final String mOriginalId;
592         @Nullable
593         private CharSequence mName;
594         @Nullable
595         private String mOwnerPackageName;
596         @NonNull
597         private String mClientPackageName;
598         @Nullable
599         private String mProviderId;
600         @NonNull
601         private final List<String> mSelectedRoutes;
602         @NonNull
603         private final List<String> mSelectableRoutes;
604         @NonNull
605         private final List<String> mDeselectableRoutes;
606         @NonNull
607         private final List<String> mTransferableRoutes;
608         @MediaRoute2Info.PlaybackVolume
609         private int mVolumeHandling = MediaRoute2Info.PLAYBACK_VOLUME_FIXED;
610         private int mVolumeMax;
611         private int mVolume;
612         @Nullable
613         private Bundle mControlHints;
614         private boolean mIsSystemSession;
615         @TransferReason private int mTransferReason = TRANSFER_REASON_FALLBACK;
616         @Nullable private UserHandle mTransferInitiatorUserHandle;
617         @Nullable private String mTransferInitiatorPackageName;
618 
619         /**
620          * Constructor for builder to create {@link RoutingSessionInfo}.
621          *
622          * <p>In order to ensure ID uniqueness in {@link MediaRouter2} side, the value of {@link
623          * RoutingSessionInfo#getId()} can be different from what was set in {@link
624          * MediaRoute2ProviderService}.
625          *
626          * @param originalId ID of the session. Must not be empty.
627          * @param clientPackageName package name of the client app which uses this session. If is is
628          *     unknown, then just use an empty string.
629          * @see MediaRoute2Info#getId()
630          */
Builder(@onNull String originalId, @NonNull String clientPackageName)631         public Builder(@NonNull String originalId, @NonNull String clientPackageName) {
632             if (TextUtils.isEmpty(originalId)) {
633                 throw new IllegalArgumentException("id must not be empty");
634             }
635 
636             mOriginalId = originalId;
637             mClientPackageName =
638                     Objects.requireNonNull(clientPackageName, "clientPackageName must not be null");
639             mSelectedRoutes = new ArrayList<>();
640             mSelectableRoutes = new ArrayList<>();
641             mDeselectableRoutes = new ArrayList<>();
642             mTransferableRoutes = new ArrayList<>();
643         }
644 
645         /**
646          * Constructor for builder to create {@link RoutingSessionInfo} with
647          * existing {@link RoutingSessionInfo} instance.
648          *
649          * @param sessionInfo the existing instance to copy data from.
650          */
Builder(@onNull RoutingSessionInfo sessionInfo)651         public Builder(@NonNull RoutingSessionInfo sessionInfo) {
652             this(sessionInfo, sessionInfo.getOriginalId());
653         }
654 
655         /**
656          * Builds upon the given {@code sessionInfo}, using the given {@link #getOriginalId()} for
657          * the id.
658          *
659          * @hide
660          */
Builder(@onNull RoutingSessionInfo sessionInfo, String originalId)661         public Builder(@NonNull RoutingSessionInfo sessionInfo, String originalId) {
662             Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
663 
664             mOriginalId = originalId;
665             mName = sessionInfo.mName;
666             mClientPackageName = sessionInfo.mClientPackageName;
667             mProviderId = sessionInfo.mProviderId;
668 
669             mSelectedRoutes = new ArrayList<>(sessionInfo.mSelectedRoutes);
670             mSelectableRoutes = new ArrayList<>(sessionInfo.mSelectableRoutes);
671             mDeselectableRoutes = new ArrayList<>(sessionInfo.mDeselectableRoutes);
672             mTransferableRoutes = new ArrayList<>(sessionInfo.mTransferableRoutes);
673 
674             if (mProviderId != null) {
675                 // They must have unique IDs.
676                 mSelectedRoutes.replaceAll(MediaRouter2Utils::getOriginalId);
677                 mSelectableRoutes.replaceAll(MediaRouter2Utils::getOriginalId);
678                 mDeselectableRoutes.replaceAll(MediaRouter2Utils::getOriginalId);
679                 mTransferableRoutes.replaceAll(MediaRouter2Utils::getOriginalId);
680             }
681 
682             mVolumeHandling = sessionInfo.mVolumeHandling;
683             mVolumeMax = sessionInfo.mVolumeMax;
684             mVolume = sessionInfo.mVolume;
685 
686             mControlHints = sessionInfo.mControlHints;
687             mIsSystemSession = sessionInfo.mIsSystemSession;
688             mTransferReason = sessionInfo.mTransferReason;
689             mTransferInitiatorUserHandle = sessionInfo.mTransferInitiatorUserHandle;
690             mTransferInitiatorPackageName = sessionInfo.mTransferInitiatorPackageName;
691         }
692 
693         /**
694          * Sets the user-visible name of the session.
695          */
696         @NonNull
setName(@ullable CharSequence name)697         public Builder setName(@Nullable CharSequence name) {
698             mName = name;
699             return this;
700         }
701 
702         /**
703          * Sets the package name of the session owner. It is expected to be called by the system.
704          *
705          * @hide
706          */
707         @NonNull
setOwnerPackageName(@ullable String packageName)708         public Builder setOwnerPackageName(@Nullable String packageName) {
709             mOwnerPackageName = packageName;
710             return this;
711         }
712 
713         /**
714          * Sets the client package name of the session.
715          *
716          * @hide
717          */
718         @NonNull
setClientPackageName(@ullable String packageName)719         public Builder setClientPackageName(@Nullable String packageName) {
720             mClientPackageName = packageName;
721             return this;
722         }
723 
724         /**
725          * Sets the provider ID of the session.
726          *
727          * @hide
728          */
729         @NonNull
setProviderId(@onNull String providerId)730         public Builder setProviderId(@NonNull String providerId) {
731             if (TextUtils.isEmpty(providerId)) {
732                 throw new IllegalArgumentException("providerId must not be empty");
733             }
734             mProviderId = providerId;
735             return this;
736         }
737 
738         /**
739          * Clears the selected routes.
740          */
741         @NonNull
clearSelectedRoutes()742         public Builder clearSelectedRoutes() {
743             mSelectedRoutes.clear();
744             return this;
745         }
746 
747         /**
748          * Adds a route to the selected routes. The {@code routeId} must not be empty.
749          */
750         @NonNull
addSelectedRoute(@onNull String routeId)751         public Builder addSelectedRoute(@NonNull String routeId) {
752             if (TextUtils.isEmpty(routeId)) {
753                 throw new IllegalArgumentException("routeId must not be empty");
754             }
755             mSelectedRoutes.add(routeId);
756             return this;
757         }
758 
759         /**
760          * Removes a route from the selected routes. The {@code routeId} must not be empty.
761          */
762         @NonNull
removeSelectedRoute(@onNull String routeId)763         public Builder removeSelectedRoute(@NonNull String routeId) {
764             if (TextUtils.isEmpty(routeId)) {
765                 throw new IllegalArgumentException("routeId must not be empty");
766             }
767             mSelectedRoutes.remove(routeId);
768             return this;
769         }
770 
771         /**
772          * Clears the selectable routes.
773          */
774         @NonNull
clearSelectableRoutes()775         public Builder clearSelectableRoutes() {
776             mSelectableRoutes.clear();
777             return this;
778         }
779 
780         /**
781          * Adds a route to the selectable routes. The {@code routeId} must not be empty.
782          */
783         @NonNull
addSelectableRoute(@onNull String routeId)784         public Builder addSelectableRoute(@NonNull String routeId) {
785             if (TextUtils.isEmpty(routeId)) {
786                 throw new IllegalArgumentException("routeId must not be empty");
787             }
788             mSelectableRoutes.add(routeId);
789             return this;
790         }
791 
792         /**
793          * Removes a route from the selectable routes. The {@code routeId} must not be empty.
794          */
795         @NonNull
removeSelectableRoute(@onNull String routeId)796         public Builder removeSelectableRoute(@NonNull String routeId) {
797             if (TextUtils.isEmpty(routeId)) {
798                 throw new IllegalArgumentException("routeId must not be empty");
799             }
800             mSelectableRoutes.remove(routeId);
801             return this;
802         }
803 
804         /**
805          * Clears the deselectable routes.
806          */
807         @NonNull
clearDeselectableRoutes()808         public Builder clearDeselectableRoutes() {
809             mDeselectableRoutes.clear();
810             return this;
811         }
812 
813         /**
814          * Adds a route to the deselectable routes. The {@code routeId} must not be empty.
815          */
816         @NonNull
addDeselectableRoute(@onNull String routeId)817         public Builder addDeselectableRoute(@NonNull String routeId) {
818             if (TextUtils.isEmpty(routeId)) {
819                 throw new IllegalArgumentException("routeId must not be empty");
820             }
821             mDeselectableRoutes.add(routeId);
822             return this;
823         }
824 
825         /**
826          * Removes a route from the deselectable routes. The {@code routeId} must not be empty.
827          */
828         @NonNull
removeDeselectableRoute(@onNull String routeId)829         public Builder removeDeselectableRoute(@NonNull String routeId) {
830             if (TextUtils.isEmpty(routeId)) {
831                 throw new IllegalArgumentException("routeId must not be empty");
832             }
833             mDeselectableRoutes.remove(routeId);
834             return this;
835         }
836 
837         /**
838          * Clears the transferable routes.
839          */
840         @NonNull
clearTransferableRoutes()841         public Builder clearTransferableRoutes() {
842             mTransferableRoutes.clear();
843             return this;
844         }
845 
846         /**
847          * Adds a route to the transferable routes. The {@code routeId} must not be empty.
848          */
849         @NonNull
addTransferableRoute(@onNull String routeId)850         public Builder addTransferableRoute(@NonNull String routeId) {
851             if (TextUtils.isEmpty(routeId)) {
852                 throw new IllegalArgumentException("routeId must not be empty");
853             }
854             mTransferableRoutes.add(routeId);
855             return this;
856         }
857 
858         /**
859          * Removes a route from the transferable routes. The {@code routeId} must not be empty.
860          */
861         @NonNull
removeTransferableRoute(@onNull String routeId)862         public Builder removeTransferableRoute(@NonNull String routeId) {
863             if (TextUtils.isEmpty(routeId)) {
864                 throw new IllegalArgumentException("routeId must not be empty");
865             }
866             mTransferableRoutes.remove(routeId);
867             return this;
868         }
869 
870         /**
871          * Sets the session's volume handling.
872          * {@link MediaRoute2Info#PLAYBACK_VOLUME_FIXED} or
873          * {@link MediaRoute2Info#PLAYBACK_VOLUME_VARIABLE}.
874          */
875         @NonNull
setVolumeHandling( @ediaRoute2Info.PlaybackVolume int volumeHandling)876         public RoutingSessionInfo.Builder setVolumeHandling(
877                 @MediaRoute2Info.PlaybackVolume int volumeHandling) {
878             mVolumeHandling = volumeHandling;
879             return this;
880         }
881 
882         /**
883          * Sets the session's maximum volume, or 0 if unknown.
884          */
885         @NonNull
setVolumeMax(int volumeMax)886         public RoutingSessionInfo.Builder setVolumeMax(int volumeMax) {
887             mVolumeMax = volumeMax;
888             return this;
889         }
890 
891         /**
892          * Sets the session's current volume, or 0 if unknown.
893          */
894         @NonNull
setVolume(int volume)895         public RoutingSessionInfo.Builder setVolume(int volume) {
896             mVolume = volume;
897             return this;
898         }
899 
900         /**
901          * Sets control hints.
902          */
903         @NonNull
setControlHints(@ullable Bundle controlHints)904         public Builder setControlHints(@Nullable Bundle controlHints) {
905             mControlHints = controlHints;
906             return this;
907         }
908 
909         /**
910          * Sets whether this session is in system media route provider.
911          * @hide
912          */
913         @NonNull
setSystemSession(boolean isSystemSession)914         public Builder setSystemSession(boolean isSystemSession) {
915             mIsSystemSession = isSystemSession;
916             return this;
917         }
918 
919         /**
920          * Sets transfer reason for the current session.
921          *
922          * <p>By default the transfer reason is set to {@link
923          * RoutingSessionInfo#TRANSFER_REASON_FALLBACK}.
924          */
925         @NonNull
926         @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES)
setTransferReason(@ransferReason int transferReason)927         public Builder setTransferReason(@TransferReason int transferReason) {
928             mTransferReason = transferReason;
929             return this;
930         }
931 
932         /**
933          * Sets the user handle and package name of the process that initiated the transfer.
934          *
935          * <p>By default the transfer initiation user handle and package name are set to {@code
936          * null}.
937          */
938         // The UserHandleName warning suggests the name should be "doFooAsUser". But the UserHandle
939         // parameter of this function is stored in a field, and not used to execute an operation on
940         // a specific user.
941         // The MissingGetterMatchingBuilder requires a getTransferInitiator function. But said
942         // getter is not included because the returned package name and user handle is always either
943         // null or the values that correspond to the calling app, and that information is obtainable
944         // via RoutingController#wasTransferInitiatedBySelf.
945         @SuppressWarnings({"UserHandleName", "MissingGetterMatchingBuilder"})
946         @NonNull
947         @FlaggedApi(FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES)
setTransferInitiator( @ullable UserHandle transferInitiatorUserHandle, @Nullable String transferInitiatorPackageName)948         public Builder setTransferInitiator(
949                 @Nullable UserHandle transferInitiatorUserHandle,
950                 @Nullable String transferInitiatorPackageName) {
951             mTransferInitiatorUserHandle = transferInitiatorUserHandle;
952             mTransferInitiatorPackageName = transferInitiatorPackageName;
953             return this;
954         }
955 
956         /**
957          * Builds a routing session info.
958          *
959          * @throws IllegalArgumentException if no selected routes are added.
960          */
961         @NonNull
build()962         public RoutingSessionInfo build() {
963             if (mSelectedRoutes.isEmpty()) {
964                 throw new IllegalArgumentException("selectedRoutes must not be empty");
965             }
966             return new RoutingSessionInfo(this);
967         }
968     }
969 }
970