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