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