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 android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.res.Resources;
22 import android.os.Bundle;
23 import android.os.Parcel;
24 import android.os.Parcelable;
25 import android.text.TextUtils;
26 
27 import com.android.internal.util.Preconditions;
28 
29 import java.io.FileDescriptor;
30 import java.io.PrintWriter;
31 import java.util.ArrayList;
32 import java.util.Collections;
33 import java.util.List;
34 import java.util.Objects;
35 
36 /**
37  * Describes a routing session which is created when a media route is selected.
38  */
39 public final class RoutingSessionInfo implements Parcelable {
40     @NonNull
41     public static final Creator<RoutingSessionInfo> CREATOR =
42             new Creator<RoutingSessionInfo>() {
43                 @Override
44                 public RoutingSessionInfo createFromParcel(Parcel in) {
45                     return new RoutingSessionInfo(in);
46                 }
47                 @Override
48                 public RoutingSessionInfo[] newArray(int size) {
49                     return new RoutingSessionInfo[size];
50                 }
51             };
52 
53     private static final String TAG = "RoutingSessionInfo";
54 
55     private static final String KEY_GROUP_ROUTE = "androidx.mediarouter.media.KEY_GROUP_ROUTE";
56     private static final String KEY_VOLUME_HANDLING = "volumeHandling";
57 
58     final String mId;
59     final CharSequence mName;
60     final String mOwnerPackageName;
61     final String mClientPackageName;
62     @Nullable
63     final String mProviderId;
64     final List<String> mSelectedRoutes;
65     final List<String> mSelectableRoutes;
66     final List<String> mDeselectableRoutes;
67     final List<String> mTransferableRoutes;
68 
69     final int mVolumeHandling;
70     final int mVolumeMax;
71     final int mVolume;
72 
73     @Nullable
74     final Bundle mControlHints;
75     final boolean mIsSystemSession;
76 
77 
RoutingSessionInfo(@onNull Builder builder)78     RoutingSessionInfo(@NonNull Builder builder) {
79         Objects.requireNonNull(builder, "builder must not be null.");
80 
81         mId = builder.mId;
82         mName = builder.mName;
83         mOwnerPackageName = builder.mOwnerPackageName;
84         mClientPackageName = builder.mClientPackageName;
85         mProviderId = builder.mProviderId;
86 
87         mSelectedRoutes = Collections.unmodifiableList(
88                 convertToUniqueRouteIds(builder.mSelectedRoutes));
89         mSelectableRoutes = Collections.unmodifiableList(
90                 convertToUniqueRouteIds(builder.mSelectableRoutes));
91         mDeselectableRoutes = Collections.unmodifiableList(
92                 convertToUniqueRouteIds(builder.mDeselectableRoutes));
93         mTransferableRoutes = Collections.unmodifiableList(
94                 convertToUniqueRouteIds(builder.mTransferableRoutes));
95 
96         mVolumeMax = builder.mVolumeMax;
97         mVolume = builder.mVolume;
98 
99         mIsSystemSession = builder.mIsSystemSession;
100 
101         boolean volumeAdjustmentForRemoteGroupSessions = Resources.getSystem().getBoolean(
102                 com.android.internal.R.bool.config_volumeAdjustmentForRemoteGroupSessions);
103         mVolumeHandling = defineVolumeHandling(builder.mVolumeHandling, mSelectedRoutes,
104                 volumeAdjustmentForRemoteGroupSessions);
105 
106         mControlHints = updateVolumeHandlingInHints(builder.mControlHints, mVolumeHandling);
107     }
108 
RoutingSessionInfo(@onNull Parcel src)109     RoutingSessionInfo(@NonNull Parcel src) {
110         mId = src.readString();
111         Preconditions.checkArgument(!TextUtils.isEmpty(mId));
112 
113         mName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(src);
114         mOwnerPackageName = src.readString();
115         mClientPackageName = ensureString(src.readString());
116         mProviderId = src.readString();
117 
118         mSelectedRoutes = ensureList(src.createStringArrayList());
119         Preconditions.checkArgument(!mSelectedRoutes.isEmpty());
120 
121         mSelectableRoutes = ensureList(src.createStringArrayList());
122         mDeselectableRoutes = ensureList(src.createStringArrayList());
123         mTransferableRoutes = ensureList(src.createStringArrayList());
124 
125         mVolumeHandling = src.readInt();
126         mVolumeMax = src.readInt();
127         mVolume = src.readInt();
128 
129         mControlHints = src.readBundle();
130         mIsSystemSession = src.readBoolean();
131     }
132 
updateVolumeHandlingInHints(Bundle controlHints, int volumeHandling)133     private static Bundle updateVolumeHandlingInHints(Bundle controlHints, int volumeHandling) {
134         // Workaround to preserve retro-compatibility with androidx.
135         // See b/228021646 for more details.
136         if (controlHints != null && controlHints.containsKey(KEY_GROUP_ROUTE)) {
137             Bundle groupRoute = controlHints.getBundle(KEY_GROUP_ROUTE);
138 
139             if (groupRoute != null && groupRoute.containsKey(KEY_VOLUME_HANDLING)
140                     && volumeHandling != groupRoute.getInt(KEY_VOLUME_HANDLING)) {
141                 //Creating copy of controlHints with updated value.
142                 Bundle newGroupRoute = new Bundle(groupRoute);
143                 newGroupRoute.putInt(KEY_VOLUME_HANDLING, volumeHandling);
144                 Bundle newControlHints = new Bundle(controlHints);
145                 newControlHints.putBundle(KEY_GROUP_ROUTE, newGroupRoute);
146                 return newControlHints;
147             }
148         }
149         //Return same Bundle.
150         return controlHints;
151     }
152 
defineVolumeHandling(int volumeHandling, List<String> selectedRoutes, boolean volumeAdjustmentForRemoteGroupSessions)153     private static int defineVolumeHandling(int volumeHandling, List<String> selectedRoutes,
154             boolean volumeAdjustmentForRemoteGroupSessions) {
155         if (!volumeAdjustmentForRemoteGroupSessions && selectedRoutes.size() > 1) {
156             return MediaRoute2Info.PLAYBACK_VOLUME_FIXED;
157         }
158         return volumeHandling;
159     }
160 
ensureString(String str)161     private static String ensureString(String str) {
162         return str != null ? str : "";
163     }
164 
ensureList(List<? extends T> list)165     private static <T> List<T> ensureList(List<? extends T> list) {
166         if (list != null) {
167             return Collections.unmodifiableList(list);
168         }
169         return Collections.emptyList();
170     }
171 
172     /**
173      * Gets the id of the session. The sessions which are given by {@link MediaRouter2} will have
174      * unique IDs.
175      * <p>
176      * In order to ensure uniqueness in {@link MediaRouter2} side, the value of this method
177      * can be different from what was set in {@link MediaRoute2ProviderService}.
178      *
179      * @see Builder#Builder(String, String)
180      */
181     @NonNull
getId()182     public String getId() {
183         if (!TextUtils.isEmpty(mProviderId)) {
184             return MediaRouter2Utils.toUniqueId(mProviderId, mId);
185         } else {
186             return mId;
187         }
188     }
189 
190     /**
191      * Gets the user-visible name of the session. It may be {@code null}.
192      */
193     @Nullable
getName()194     public CharSequence getName() {
195         return mName;
196     }
197 
198     /**
199      * Gets the original id set by {@link Builder#Builder(String, String)}.
200      * @hide
201      */
202     @NonNull
getOriginalId()203     public String getOriginalId() {
204         return mId;
205     }
206 
207     /**
208      * Gets the package name of the session owner.
209      * @hide
210      */
211     @Nullable
getOwnerPackageName()212     public String getOwnerPackageName() {
213         return mOwnerPackageName;
214     }
215 
216     /**
217      * Gets the client package name of the session
218      */
219     @NonNull
getClientPackageName()220     public String getClientPackageName() {
221         return mClientPackageName;
222     }
223 
224     /**
225      * Gets the provider id of the session.
226      * @hide
227      */
228     @Nullable
getProviderId()229     public String getProviderId() {
230         return mProviderId;
231     }
232 
233     /**
234      * Gets the list of IDs of selected routes for the session. It shouldn't be empty.
235      */
236     @NonNull
getSelectedRoutes()237     public List<String> getSelectedRoutes() {
238         return mSelectedRoutes;
239     }
240 
241     /**
242      * Gets the list of IDs of selectable routes for the session.
243      */
244     @NonNull
getSelectableRoutes()245     public List<String> getSelectableRoutes() {
246         return mSelectableRoutes;
247     }
248 
249     /**
250      * Gets the list of IDs of deselectable routes for the session.
251      */
252     @NonNull
getDeselectableRoutes()253     public List<String> getDeselectableRoutes() {
254         return mDeselectableRoutes;
255     }
256 
257     /**
258      * Gets the list of IDs of transferable routes for the session.
259      */
260     @NonNull
getTransferableRoutes()261     public List<String> getTransferableRoutes() {
262         return mTransferableRoutes;
263     }
264 
265     /**
266      * Gets the information about how volume is handled on the session.
267      *
268      * @return {@link MediaRoute2Info#PLAYBACK_VOLUME_FIXED} or
269      * {@link MediaRoute2Info#PLAYBACK_VOLUME_VARIABLE}.
270      */
271     @MediaRoute2Info.PlaybackVolume
getVolumeHandling()272     public int getVolumeHandling() {
273         return mVolumeHandling;
274     }
275 
276     /**
277      * Gets the maximum volume of the session.
278      */
getVolumeMax()279     public int getVolumeMax() {
280         return mVolumeMax;
281     }
282 
283     /**
284      * Gets the current volume of the session.
285      * <p>
286      * When it's available, it represents the volume of routing session, which is a group
287      * of selected routes. To get the volume of each route, use {@link MediaRoute2Info#getVolume()}.
288      * </p>
289      * @see MediaRoute2Info#getVolume()
290      */
getVolume()291     public int getVolume() {
292         return mVolume;
293     }
294 
295     /**
296      * Gets the control hints
297      */
298     @Nullable
getControlHints()299     public Bundle getControlHints() {
300         return mControlHints;
301     }
302 
303     /**
304      * Gets whether this session is in system media route provider.
305      * @hide
306      */
307     @Nullable
isSystemSession()308     public boolean isSystemSession() {
309         return mIsSystemSession;
310     }
311 
312     @Override
describeContents()313     public int describeContents() {
314         return 0;
315     }
316 
317     @Override
writeToParcel(@onNull Parcel dest, int flags)318     public void writeToParcel(@NonNull Parcel dest, int flags) {
319         dest.writeString(mId);
320         dest.writeCharSequence(mName);
321         dest.writeString(mOwnerPackageName);
322         dest.writeString(mClientPackageName);
323         dest.writeString(mProviderId);
324         dest.writeStringList(mSelectedRoutes);
325         dest.writeStringList(mSelectableRoutes);
326         dest.writeStringList(mDeselectableRoutes);
327         dest.writeStringList(mTransferableRoutes);
328         dest.writeInt(mVolumeHandling);
329         dest.writeInt(mVolumeMax);
330         dest.writeInt(mVolume);
331         dest.writeBundle(mControlHints);
332         dest.writeBoolean(mIsSystemSession);
333     }
334 
335     /**
336      * Dumps current state of the instance. Use with {@code dumpsys}.
337      *
338      * See {@link android.os.Binder#dump(FileDescriptor, PrintWriter, String[])}.
339      *
340      * @hide
341      */
dump(@onNull PrintWriter pw, @NonNull String prefix)342     public void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
343         pw.println(prefix + "RoutingSessionInfo");
344 
345         String indent = prefix + "  ";
346 
347         pw.println(indent + "mId=" + mId);
348         pw.println(indent + "mName=" + mName);
349         pw.println(indent + "mOwnerPackageName=" + mOwnerPackageName);
350         pw.println(indent + "mClientPackageName=" + mClientPackageName);
351         pw.println(indent + "mProviderId=" + mProviderId);
352         pw.println(indent + "mSelectedRoutes=" + mSelectedRoutes);
353         pw.println(indent + "mSelectableRoutes=" + mSelectableRoutes);
354         pw.println(indent + "mDeselectableRoutes=" + mDeselectableRoutes);
355         pw.println(indent + "mTransferableRoutes=" + mTransferableRoutes);
356         pw.println(indent + "mVolumeHandling=" + mVolumeHandling);
357         pw.println(indent + "mVolumeMax=" + mVolumeMax);
358         pw.println(indent + "mVolume=" + mVolume);
359         pw.println(indent + "mControlHints=" + mControlHints);
360         pw.println(indent + "mIsSystemSession=" + mIsSystemSession);
361     }
362 
363     @Override
equals(Object obj)364     public boolean equals(Object obj) {
365         if (this == obj) {
366             return true;
367         }
368         if (!(obj instanceof RoutingSessionInfo)) {
369             return false;
370         }
371 
372         RoutingSessionInfo other = (RoutingSessionInfo) obj;
373         return Objects.equals(mId, other.mId)
374                 && Objects.equals(mName, other.mName)
375                 && Objects.equals(mOwnerPackageName, other.mOwnerPackageName)
376                 && Objects.equals(mClientPackageName, other.mClientPackageName)
377                 && Objects.equals(mProviderId, other.mProviderId)
378                 && Objects.equals(mSelectedRoutes, other.mSelectedRoutes)
379                 && Objects.equals(mSelectableRoutes, other.mSelectableRoutes)
380                 && Objects.equals(mDeselectableRoutes, other.mDeselectableRoutes)
381                 && Objects.equals(mTransferableRoutes, other.mTransferableRoutes)
382                 && (mVolumeHandling == other.mVolumeHandling)
383                 && (mVolumeMax == other.mVolumeMax)
384                 && (mVolume == other.mVolume);
385     }
386 
387     @Override
hashCode()388     public int hashCode() {
389         return Objects.hash(mId, mName, mOwnerPackageName, mClientPackageName, mProviderId,
390                 mSelectedRoutes, mSelectableRoutes, mDeselectableRoutes, mTransferableRoutes,
391                 mVolumeMax, mVolumeHandling, mVolume);
392     }
393 
394     @Override
toString()395     public String toString() {
396         StringBuilder result = new StringBuilder()
397                 .append("RoutingSessionInfo{ ")
398                 .append("sessionId=").append(getId())
399                 .append(", name=").append(getName())
400                 .append(", clientPackageName=").append(getClientPackageName())
401                 .append(", selectedRoutes={")
402                 .append(String.join(",", getSelectedRoutes()))
403                 .append("}")
404                 .append(", selectableRoutes={")
405                 .append(String.join(",", getSelectableRoutes()))
406                 .append("}")
407                 .append(", deselectableRoutes={")
408                 .append(String.join(",", getDeselectableRoutes()))
409                 .append("}")
410                 .append(", transferableRoutes={")
411                 .append(String.join(",", getTransferableRoutes()))
412                 .append("}")
413                 .append(", volumeHandling=").append(getVolumeHandling())
414                 .append(", volumeMax=").append(getVolumeMax())
415                 .append(", volume=").append(getVolume())
416                 .append(" }");
417         return result.toString();
418     }
419 
420     /**
421      * Provides a new list with unique route IDs if {@link #mProviderId} is set, or the original IDs
422      * otherwise.
423      *
424      * @param routeIds list of route IDs to convert
425      * @return new list with unique IDs or original IDs
426      */
427 
428     @NonNull
convertToUniqueRouteIds(@onNull List<String> routeIds)429     private List<String> convertToUniqueRouteIds(@NonNull List<String> routeIds) {
430         Objects.requireNonNull(routeIds, "RouteIds cannot be null.");
431 
432         // mProviderId can be null if not set. Return the original list for this case.
433         if (TextUtils.isEmpty(mProviderId)) {
434             return new ArrayList<>(routeIds);
435         }
436 
437         List<String> result = new ArrayList<>();
438         for (String routeId : routeIds) {
439             result.add(MediaRouter2Utils.toUniqueId(mProviderId, routeId));
440         }
441         return result;
442     }
443 
444     /**
445      * Builder class for {@link RoutingSessionInfo}.
446      */
447     public static final class Builder {
448         // TODO: Reorder these (important ones first)
449         final String mId;
450         CharSequence mName;
451         String mOwnerPackageName;
452         String mClientPackageName;
453         String mProviderId;
454         final List<String> mSelectedRoutes;
455         final List<String> mSelectableRoutes;
456         final List<String> mDeselectableRoutes;
457         final List<String> mTransferableRoutes;
458         int mVolumeHandling = MediaRoute2Info.PLAYBACK_VOLUME_FIXED;
459         int mVolumeMax;
460         int mVolume;
461         Bundle mControlHints;
462         boolean mIsSystemSession;
463 
464         /**
465          * Constructor for builder to create {@link RoutingSessionInfo}.
466          * <p>
467          * In order to ensure ID uniqueness in {@link MediaRouter2} side, the value of
468          * {@link RoutingSessionInfo#getId()} can be different from what was set in
469          * {@link MediaRoute2ProviderService}.
470          * </p>
471          *
472          * @param id ID of the session. Must not be empty.
473          * @param clientPackageName package name of the client app which uses this session.
474          *                          If is is unknown, then just use an empty string.
475          * @see MediaRoute2Info#getId()
476          */
Builder(@onNull String id, @NonNull String clientPackageName)477         public Builder(@NonNull String id, @NonNull String clientPackageName) {
478             if (TextUtils.isEmpty(id)) {
479                 throw new IllegalArgumentException("id must not be empty");
480             }
481 
482             mId = id;
483             mClientPackageName =
484                     Objects.requireNonNull(clientPackageName, "clientPackageName must not be null");
485             mSelectedRoutes = new ArrayList<>();
486             mSelectableRoutes = new ArrayList<>();
487             mDeselectableRoutes = new ArrayList<>();
488             mTransferableRoutes = new ArrayList<>();
489         }
490 
491         /**
492          * Constructor for builder to create {@link RoutingSessionInfo} with
493          * existing {@link RoutingSessionInfo} instance.
494          *
495          * @param sessionInfo the existing instance to copy data from.
496          */
Builder(@onNull RoutingSessionInfo sessionInfo)497         public Builder(@NonNull RoutingSessionInfo sessionInfo) {
498             Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
499 
500             mId = sessionInfo.mId;
501             mName = sessionInfo.mName;
502             mClientPackageName = sessionInfo.mClientPackageName;
503             mProviderId = sessionInfo.mProviderId;
504 
505             mSelectedRoutes = new ArrayList<>(sessionInfo.mSelectedRoutes);
506             mSelectableRoutes = new ArrayList<>(sessionInfo.mSelectableRoutes);
507             mDeselectableRoutes = new ArrayList<>(sessionInfo.mDeselectableRoutes);
508             mTransferableRoutes = new ArrayList<>(sessionInfo.mTransferableRoutes);
509 
510             if (mProviderId != null) {
511                 // They must have unique IDs.
512                 mSelectedRoutes.replaceAll(MediaRouter2Utils::getOriginalId);
513                 mSelectableRoutes.replaceAll(MediaRouter2Utils::getOriginalId);
514                 mDeselectableRoutes.replaceAll(MediaRouter2Utils::getOriginalId);
515                 mTransferableRoutes.replaceAll(MediaRouter2Utils::getOriginalId);
516             }
517 
518             mVolumeHandling = sessionInfo.mVolumeHandling;
519             mVolumeMax = sessionInfo.mVolumeMax;
520             mVolume = sessionInfo.mVolume;
521 
522             mControlHints = sessionInfo.mControlHints;
523             mIsSystemSession = sessionInfo.mIsSystemSession;
524         }
525 
526         /**
527          * Sets the user-visible name of the session.
528          */
529         @NonNull
setName(@ullable CharSequence name)530         public Builder setName(@Nullable CharSequence name) {
531             mName = name;
532             return this;
533         }
534 
535         /**
536          * Sets the package name of the session owner. It is expected to be called by the system.
537          *
538          * @hide
539          */
540         @NonNull
setOwnerPackageName(@ullable String packageName)541         public Builder setOwnerPackageName(@Nullable String packageName) {
542             mOwnerPackageName = packageName;
543             return this;
544         }
545 
546         /**
547          * Sets the client package name of the session.
548          *
549          * @hide
550          */
551         @NonNull
setClientPackageName(@ullable String packageName)552         public Builder setClientPackageName(@Nullable String packageName) {
553             mClientPackageName = packageName;
554             return this;
555         }
556 
557         /**
558          * Sets the provider ID of the session.
559          *
560          * @hide
561          */
562         @NonNull
setProviderId(@onNull String providerId)563         public Builder setProviderId(@NonNull String providerId) {
564             if (TextUtils.isEmpty(providerId)) {
565                 throw new IllegalArgumentException("providerId must not be empty");
566             }
567             mProviderId = providerId;
568             return this;
569         }
570 
571         /**
572          * Clears the selected routes.
573          */
574         @NonNull
clearSelectedRoutes()575         public Builder clearSelectedRoutes() {
576             mSelectedRoutes.clear();
577             return this;
578         }
579 
580         /**
581          * Adds a route to the selected routes. The {@code routeId} must not be empty.
582          */
583         @NonNull
addSelectedRoute(@onNull String routeId)584         public Builder addSelectedRoute(@NonNull String routeId) {
585             if (TextUtils.isEmpty(routeId)) {
586                 throw new IllegalArgumentException("routeId must not be empty");
587             }
588             mSelectedRoutes.add(routeId);
589             return this;
590         }
591 
592         /**
593          * Removes a route from the selected routes. The {@code routeId} must not be empty.
594          */
595         @NonNull
removeSelectedRoute(@onNull String routeId)596         public Builder removeSelectedRoute(@NonNull String routeId) {
597             if (TextUtils.isEmpty(routeId)) {
598                 throw new IllegalArgumentException("routeId must not be empty");
599             }
600             mSelectedRoutes.remove(routeId);
601             return this;
602         }
603 
604         /**
605          * Clears the selectable routes.
606          */
607         @NonNull
clearSelectableRoutes()608         public Builder clearSelectableRoutes() {
609             mSelectableRoutes.clear();
610             return this;
611         }
612 
613         /**
614          * Adds a route to the selectable routes. The {@code routeId} must not be empty.
615          */
616         @NonNull
addSelectableRoute(@onNull String routeId)617         public Builder addSelectableRoute(@NonNull String routeId) {
618             if (TextUtils.isEmpty(routeId)) {
619                 throw new IllegalArgumentException("routeId must not be empty");
620             }
621             mSelectableRoutes.add(routeId);
622             return this;
623         }
624 
625         /**
626          * Removes a route from the selectable routes. The {@code routeId} must not be empty.
627          */
628         @NonNull
removeSelectableRoute(@onNull String routeId)629         public Builder removeSelectableRoute(@NonNull String routeId) {
630             if (TextUtils.isEmpty(routeId)) {
631                 throw new IllegalArgumentException("routeId must not be empty");
632             }
633             mSelectableRoutes.remove(routeId);
634             return this;
635         }
636 
637         /**
638          * Clears the deselectable routes.
639          */
640         @NonNull
clearDeselectableRoutes()641         public Builder clearDeselectableRoutes() {
642             mDeselectableRoutes.clear();
643             return this;
644         }
645 
646         /**
647          * Adds a route to the deselectable routes. The {@code routeId} must not be empty.
648          */
649         @NonNull
addDeselectableRoute(@onNull String routeId)650         public Builder addDeselectableRoute(@NonNull String routeId) {
651             if (TextUtils.isEmpty(routeId)) {
652                 throw new IllegalArgumentException("routeId must not be empty");
653             }
654             mDeselectableRoutes.add(routeId);
655             return this;
656         }
657 
658         /**
659          * Removes a route from the deselectable routes. The {@code routeId} must not be empty.
660          */
661         @NonNull
removeDeselectableRoute(@onNull String routeId)662         public Builder removeDeselectableRoute(@NonNull String routeId) {
663             if (TextUtils.isEmpty(routeId)) {
664                 throw new IllegalArgumentException("routeId must not be empty");
665             }
666             mDeselectableRoutes.remove(routeId);
667             return this;
668         }
669 
670         /**
671          * Clears the transferable routes.
672          */
673         @NonNull
clearTransferableRoutes()674         public Builder clearTransferableRoutes() {
675             mTransferableRoutes.clear();
676             return this;
677         }
678 
679         /**
680          * Adds a route to the transferable routes. The {@code routeId} must not be empty.
681          */
682         @NonNull
addTransferableRoute(@onNull String routeId)683         public Builder addTransferableRoute(@NonNull String routeId) {
684             if (TextUtils.isEmpty(routeId)) {
685                 throw new IllegalArgumentException("routeId must not be empty");
686             }
687             mTransferableRoutes.add(routeId);
688             return this;
689         }
690 
691         /**
692          * Removes a route from the transferable routes. The {@code routeId} must not be empty.
693          */
694         @NonNull
removeTransferableRoute(@onNull String routeId)695         public Builder removeTransferableRoute(@NonNull String routeId) {
696             if (TextUtils.isEmpty(routeId)) {
697                 throw new IllegalArgumentException("routeId must not be empty");
698             }
699             mTransferableRoutes.remove(routeId);
700             return this;
701         }
702 
703         /**
704          * Sets the session's volume handling.
705          * {@link MediaRoute2Info#PLAYBACK_VOLUME_FIXED} or
706          * {@link MediaRoute2Info#PLAYBACK_VOLUME_VARIABLE}.
707          */
708         @NonNull
setVolumeHandling( @ediaRoute2Info.PlaybackVolume int volumeHandling)709         public RoutingSessionInfo.Builder setVolumeHandling(
710                 @MediaRoute2Info.PlaybackVolume int volumeHandling) {
711             mVolumeHandling = volumeHandling;
712             return this;
713         }
714 
715         /**
716          * Sets the session's maximum volume, or 0 if unknown.
717          */
718         @NonNull
setVolumeMax(int volumeMax)719         public RoutingSessionInfo.Builder setVolumeMax(int volumeMax) {
720             mVolumeMax = volumeMax;
721             return this;
722         }
723 
724         /**
725          * Sets the session's current volume, or 0 if unknown.
726          */
727         @NonNull
setVolume(int volume)728         public RoutingSessionInfo.Builder setVolume(int volume) {
729             mVolume = volume;
730             return this;
731         }
732 
733         /**
734          * Sets control hints.
735          */
736         @NonNull
setControlHints(@ullable Bundle controlHints)737         public Builder setControlHints(@Nullable Bundle controlHints) {
738             mControlHints = controlHints;
739             return this;
740         }
741 
742         /**
743          * Sets whether this session is in system media route provider.
744          * @hide
745          */
746         @NonNull
setSystemSession(boolean isSystemSession)747         public Builder setSystemSession(boolean isSystemSession) {
748             mIsSystemSession = isSystemSession;
749             return this;
750         }
751 
752         /**
753          * Builds a routing session info.
754          *
755          * @throws IllegalArgumentException if no selected routes are added.
756          */
757         @NonNull
build()758         public RoutingSessionInfo build() {
759             if (mSelectedRoutes.isEmpty()) {
760                 throw new IllegalArgumentException("selectedRoutes must not be empty");
761             }
762             return new RoutingSessionInfo(this);
763         }
764     }
765 }
766