1 /*
2  * Copyright (C) 2023 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 com.example.androidx.mediarouting;
18 
19 import static com.example.androidx.mediarouting.data.RouteItem.ControlFilter.BASIC;
20 import static com.example.androidx.mediarouting.data.RouteItem.DeviceType.SPEAKER;
21 import static com.example.androidx.mediarouting.data.RouteItem.DeviceType.TV;
22 import static com.example.androidx.mediarouting.data.RouteItem.PlaybackStream.MUSIC;
23 import static com.example.androidx.mediarouting.data.RouteItem.PlaybackType.REMOTE;
24 import static com.example.androidx.mediarouting.data.RouteItem.VolumeHandling.VARIABLE;
25 
26 import android.content.ComponentName;
27 import android.content.Context;
28 import android.content.res.Resources;
29 
30 import androidx.mediarouter.media.MediaRouter;
31 import androidx.mediarouter.media.MediaRouterParams;
32 import androidx.mediarouter.media.RouteListingPreference;
33 
34 import com.example.androidx.mediarouting.activities.MainActivity;
35 import com.example.androidx.mediarouting.data.RouteItem;
36 
37 import org.jspecify.annotations.NonNull;
38 import org.jspecify.annotations.Nullable;
39 
40 import java.util.ArrayList;
41 import java.util.Collections;
42 import java.util.HashMap;
43 import java.util.List;
44 import java.util.Map;
45 
46 /** Holds the data needed to control the provider for the routes dynamically. */
47 public final class RoutesManager {
48 
49     private static final String VARIABLE_VOLUME_BASIC_ROUTE_ID = "variable_basic";
50     private static final String SENDER_DRIVEN_BASIC_ROUTE_ID = "sender_driven_route";
51     private static final int VOLUME_MAX = 25;
52     private static final int VOLUME_DEFAULT = 5;
53 
54     private static RoutesManager sInstance;
55 
56     private final Context mContext;
57     private final Map<String, RouteItem> mRouteItems;
58     private boolean mDynamicRoutingEnabled;
59     private DialogType mDialogType;
60     private final MediaRouter mMediaRouter;
61     private boolean mRouteListingPreferenceEnabled;
62     private boolean mRouteListingSystemOrderingPreferred;
63     private List<RouteListingPreferenceItemHolder> mRouteListingPreferenceItems;
64 
RoutesManager(Context context)65     private RoutesManager(Context context) {
66         mContext = context;
67         mDynamicRoutingEnabled = true;
68         mDialogType = DialogType.OUTPUT_SWITCHER;
69         mRouteItems = new HashMap<>();
70         mRouteListingPreferenceItems = Collections.emptyList();
71         mMediaRouter = MediaRouter.getInstance(context);
72         initTestRoutes();
73     }
74 
75     /** Singleton method. */
getInstance(@onNull Context context)76     public static @NonNull RoutesManager getInstance(@NonNull Context context) {
77         synchronized (RoutesManager.class) {
78             if (sInstance == null) {
79                 sInstance = new RoutesManager(context);
80             }
81         }
82         return sInstance;
83     }
84 
getRouteItems()85     public @NonNull List<RouteItem> getRouteItems() {
86         return new ArrayList<>(mRouteItems.values());
87     }
88 
isDynamicRoutingEnabled()89     public boolean isDynamicRoutingEnabled() {
90         return mDynamicRoutingEnabled;
91     }
92 
setDynamicRoutingEnabled(boolean dynamicRoutingEnabled)93     public void setDynamicRoutingEnabled(boolean dynamicRoutingEnabled) {
94         this.mDynamicRoutingEnabled = dynamicRoutingEnabled;
95     }
96 
setDialogType(@onNull DialogType dialogType)97     public void setDialogType(@NonNull DialogType dialogType) {
98         this.mDialogType = dialogType;
99     }
100 
101     /**
102      * Deletes the route with the passed id.
103      *
104      * @param id of the route to be deleted.
105      */
deleteRouteWithId(@onNull String id)106     public void deleteRouteWithId(@NonNull String id) {
107         mRouteItems.remove(id);
108     }
109 
110     /**
111      * Gets the route with the passed id, or null if no route with the given id exists.
112      *
113      * @param id of the route to search for.
114      * @return the route with the passed id, or null if it does not exist.
115      */
getRouteWithId(@ullable String id)116     public @Nullable RouteItem getRouteWithId(@Nullable String id) {
117         return mRouteItems.get(id);
118     }
119 
120     /** Adds the given route to the manager, replacing any existing route with a matching id. */
addRoute(@onNull RouteItem routeItem)121     public void addRoute(@NonNull RouteItem routeItem) {
122         mRouteItems.put(routeItem.getId(), routeItem);
123     }
124 
125     /**
126      * Returns whether route listing preference is enabled.
127      *
128      * @see #setRouteListingPreferenceEnabled
129      */
isRouteListingPreferenceEnabled()130     public boolean isRouteListingPreferenceEnabled() {
131         return mRouteListingPreferenceEnabled;
132     }
133 
134     /**
135      * Sets whether the use of route listing preference is enabled or not.
136      *
137      * <p>If route listing preference is enabled, the route listing preference configuration for
138      * this app is maintained following the item list provided via {@link
139      * #setRouteListingPreferenceItems}. Otherwise, if route listing preference is disabled, the
140      * route listing preference for this app is set to null.
141      *
142      * <p>Does not affect the system's state if called on a device running API 33 or older.
143      */
setRouteListingPreferenceEnabled(boolean routeListingPreferenceEnabled)144     public void setRouteListingPreferenceEnabled(boolean routeListingPreferenceEnabled) {
145         mRouteListingPreferenceEnabled = routeListingPreferenceEnabled;
146         onRouteListingPreferenceChanged();
147     }
148 
149     /** Returns whether the system ordering for route listing is preferred. */
getRouteListingSystemOrderingPreferred()150     public boolean getRouteListingSystemOrderingPreferred() {
151         return mRouteListingSystemOrderingPreferred;
152     }
153 
154     /**
155      * Sets whether to prefer the system ordering for route listing.
156      *
157      * <p>True means that the ordering for route listing is the one in the {@link #getRouteItems()}
158      * list. If false, the ordering of said list is ignored, and the system uses its builtin
159      * ordering for the items.
160      *
161      * <p>Does not affect the system's state if called on a device running API 33 or older.
162      */
setRouteListingSystemOrderingPreferred( boolean routeListingSystemOrderringPreferred)163     public void setRouteListingSystemOrderingPreferred(
164             boolean routeListingSystemOrderringPreferred) {
165             mRouteListingSystemOrderingPreferred = routeListingSystemOrderringPreferred;
166         onRouteListingPreferenceChanged();
167     }
168 
169     /**
170      * The current list of route listing preference items, as set via {@link
171      * #setRouteListingPreferenceItems}.
172      */
getRouteListingPreferenceItems()173     public @NonNull List<RouteListingPreferenceItemHolder> getRouteListingPreferenceItems() {
174         return mRouteListingPreferenceItems;
175     }
176 
177     /**
178      * Sets the route listing preference items.
179      *
180      * <p>Does not affect the system's state if called on a device running API 33 or older.
181      *
182      * @see #setRouteListingPreferenceEnabled
183      */
setRouteListingPreferenceItems( @onNull List<RouteListingPreferenceItemHolder> preference)184     public void setRouteListingPreferenceItems(
185             @NonNull List<RouteListingPreferenceItemHolder> preference) {
186             mRouteListingPreferenceItems =
187                     Collections.unmodifiableList(new ArrayList<>(preference));
188         onRouteListingPreferenceChanged();
189     }
190 
191     /** Changes the media router dialog type with the type stored in {@link RoutesManager} */
reloadDialogType()192     public void reloadDialogType() {
193         MediaRouter mediaRouter = MediaRouter.getInstance(mContext.getApplicationContext());
194         MediaRouterParams.Builder builder =
195                 new MediaRouterParams.Builder(mediaRouter.getRouterParams());
196         switch (mDialogType) {
197             case DEFAULT:
198                 builder.setDialogType(MediaRouterParams.DIALOG_TYPE_DEFAULT)
199                         .setOutputSwitcherEnabled(false);
200                 mediaRouter.setRouterParams(builder.build());
201                 break;
202             case DYNAMIC_GROUP:
203                 builder.setDialogType(MediaRouterParams.DIALOG_TYPE_DYNAMIC_GROUP)
204                         .setOutputSwitcherEnabled(false);
205                 mediaRouter.setRouterParams(builder.build());
206                 break;
207             case OUTPUT_SWITCHER:
208                 builder.setOutputSwitcherEnabled(true);
209                 mediaRouter.setRouterParams(builder.build());
210                 break;
211         }
212     }
213 
initTestRoutes()214     private void initTestRoutes() {
215         Resources r = mContext.getResources();
216 
217         RouteItem r1 = new RouteItem();
218         r1.setId(VARIABLE_VOLUME_BASIC_ROUTE_ID + "1");
219         r1.setName(r.getString(R.string.dg_tv_route_name1));
220         r1.setDescription(r.getString(R.string.sample_route_description));
221         r1.setControlFilter(BASIC);
222         r1.setDeviceType(TV);
223         r1.setPlaybackStream(MUSIC);
224         r1.setPlaybackType(REMOTE);
225         r1.setVolumeHandling(VARIABLE);
226         r1.setVolumeMax(VOLUME_MAX);
227         r1.setVolume(VOLUME_DEFAULT);
228         r1.setCanDisconnect(true);
229 
230         RouteItem r2 = new RouteItem();
231         r2.setId(VARIABLE_VOLUME_BASIC_ROUTE_ID + "2");
232         r2.setName(r.getString(R.string.dg_tv_route_name2));
233         r2.setDescription(r.getString(R.string.sample_route_description));
234         r2.setControlFilter(BASIC);
235         r2.setDeviceType(TV);
236         r2.setPlaybackStream(MUSIC);
237         r2.setPlaybackType(REMOTE);
238         r2.setVolumeHandling(VARIABLE);
239         r2.setVolumeMax(VOLUME_MAX);
240         r2.setVolume(VOLUME_DEFAULT);
241         r2.setCanDisconnect(true);
242 
243         RouteItem r3 = new RouteItem();
244         r3.setId(VARIABLE_VOLUME_BASIC_ROUTE_ID + "3");
245         r3.setName(r.getString(R.string.dg_speaker_route_name3));
246         r3.setDescription(r.getString(R.string.sample_route_description));
247         r3.setControlFilter(BASIC);
248         r3.setDeviceType(SPEAKER);
249         r3.setPlaybackStream(MUSIC);
250         r3.setPlaybackType(REMOTE);
251         r3.setVolumeHandling(VARIABLE);
252         r3.setVolumeMax(VOLUME_MAX);
253         r3.setVolume(VOLUME_DEFAULT);
254         r3.setCanDisconnect(true);
255 
256         RouteItem r4 = new RouteItem();
257         r4.setId(VARIABLE_VOLUME_BASIC_ROUTE_ID + "4");
258         r4.setName(r.getString(R.string.dg_speaker_route_name4));
259         r4.setDescription(r.getString(R.string.sample_route_description));
260         r4.setControlFilter(BASIC);
261         r4.setDeviceType(SPEAKER);
262         r4.setPlaybackStream(MUSIC);
263         r4.setPlaybackType(REMOTE);
264         r4.setVolumeHandling(VARIABLE);
265         r4.setVolumeMax(VOLUME_MAX);
266         r4.setVolume(VOLUME_DEFAULT);
267         r4.setCanDisconnect(true);
268 
269         RouteItem r5 = new RouteItem();
270         r5.setId(SENDER_DRIVEN_BASIC_ROUTE_ID + "1");
271         r5.setName(r.getString(R.string.sender_driven_route_name1));
272         r5.setDescription(r.getString(R.string.sample_route_description));
273         r5.setControlFilter(BASIC);
274         r5.setDeviceType(TV);
275         r5.setPlaybackStream(MUSIC);
276         r5.setPlaybackType(REMOTE);
277         r5.setVolumeHandling(VARIABLE);
278         r5.setVolumeMax(VOLUME_MAX);
279         r5.setVolume(VOLUME_DEFAULT);
280         r5.setCanDisconnect(true);
281         r5.setSenderDriven(true);
282 
283         RouteItem r6 = new RouteItem();
284         r6.setId(SENDER_DRIVEN_BASIC_ROUTE_ID + "2");
285         r6.setName(r.getString(R.string.sender_driven_route_name2));
286         r6.setDescription(r.getString(R.string.sample_route_description));
287         r6.setControlFilter(BASIC);
288         r6.setDeviceType(TV);
289         r6.setPlaybackStream(MUSIC);
290         r6.setPlaybackType(REMOTE);
291         r6.setVolumeHandling(VARIABLE);
292         r6.setVolumeMax(VOLUME_MAX);
293         r6.setVolume(VOLUME_DEFAULT);
294         r6.setCanDisconnect(true);
295         r6.setSenderDriven(true);
296 
297         mRouteItems.put(r1.getId(), r1);
298         mRouteItems.put(r2.getId(), r2);
299         mRouteItems.put(r3.getId(), r3);
300         mRouteItems.put(r4.getId(), r4);
301         mRouteItems.put(r5.getId(), r5);
302         mRouteItems.put(r6.getId(), r6);
303     }
304 
onRouteListingPreferenceChanged()305     private void onRouteListingPreferenceChanged() {
306         RouteListingPreference routeListingPreference = null;
307         if (mRouteListingPreferenceEnabled) {
308             ArrayList<RouteListingPreference.Item> items = new ArrayList<>();
309             for (RouteListingPreferenceItemHolder item : mRouteListingPreferenceItems) {
310                 items.add(item.mItem);
311             }
312             routeListingPreference =
313                     new RouteListingPreference.Builder()
314                             .setItems(items)
315                             .setLinkedItemComponentName(
316                                     new ComponentName(mContext, MainActivity.class))
317                             .setSystemOrderingEnabled(mRouteListingSystemOrderingPreferred)
318                             .build();
319         }
320         mMediaRouter.setRouteListingPreference(routeListingPreference);
321     }
322 
323     public enum DialogType {
324         DEFAULT,
325         DYNAMIC_GROUP,
326         OUTPUT_SWITCHER
327     }
328 
329     /**
330      * Holds a {@link RouteListingPreference.Item} and the associated route's name.
331      *
332      * <p>Convenient pair-like class for populating UI elements, ensuring we have an associated
333      * route name for each route listing preference item even after the corresponding route no
334      * longer exists.
335      */
336     public static final class RouteListingPreferenceItemHolder {
337 
338         public final RouteListingPreference.@NonNull Item mItem;
339         public final @NonNull String mRouteName;
340 
RouteListingPreferenceItemHolder( RouteListingPreference.@onNull Item item, @NonNull String routeName)341         public RouteListingPreferenceItemHolder(
342                 RouteListingPreference.@NonNull Item item, @NonNull String routeName) {
343             mItem = item;
344             mRouteName = routeName;
345         }
346 
347         /** Returns the name of the corresponding route. */
348         @Override
toString()349         public @NonNull String toString() {
350             return mRouteName;
351         }
352 
353         /**
354          * Returns whether the contained {@link RouteListingPreference.Item} has the given {@code
355          * flag} set.
356          */
hasFlag(int flag)357         public boolean hasFlag(int flag) {
358             return (mItem.getFlags() & flag) == flag;
359         }
360     }
361 }
362