Aurimas Liutikas | ac5fe7c | 2018-03-06 14:40:53 -0800 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2013 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 androidx.mediarouter.media; |
| 18 | |
| 19 | import android.content.BroadcastReceiver; |
| 20 | import android.content.ComponentName; |
| 21 | import android.content.Context; |
| 22 | import android.content.Intent; |
| 23 | import android.content.IntentFilter; |
| 24 | import android.content.res.Resources; |
| 25 | import android.media.AudioManager; |
| 26 | import android.os.Build; |
| 27 | import androidx.annotation.RequiresApi; |
| 28 | import androidx.mediarouter.R; |
| 29 | import android.view.Display; |
| 30 | |
| 31 | import java.util.ArrayList; |
| 32 | import java.util.List; |
| 33 | import java.util.Locale; |
| 34 | |
| 35 | /** |
| 36 | * Provides routes for built-in system destinations such as the local display |
| 37 | * and speaker. On Jellybean and newer platform releases, queries the framework |
| 38 | * MediaRouter for framework-provided routes and registers non-framework-provided |
| 39 | * routes as user routes. |
| 40 | */ |
| 41 | abstract class SystemMediaRouteProvider extends MediaRouteProvider { |
| 42 | private static final String TAG = "SystemMediaRouteProvider"; |
| 43 | |
| 44 | public static final String PACKAGE_NAME = "android"; |
| 45 | public static final String DEFAULT_ROUTE_ID = "DEFAULT_ROUTE"; |
| 46 | |
| 47 | protected SystemMediaRouteProvider(Context context) { |
| 48 | super(context, new ProviderMetadata(new ComponentName(PACKAGE_NAME, |
| 49 | SystemMediaRouteProvider.class.getName()))); |
| 50 | } |
| 51 | |
| 52 | public static SystemMediaRouteProvider obtain(Context context, SyncCallback syncCallback) { |
| 53 | if (Build.VERSION.SDK_INT >= 24) { |
| 54 | return new Api24Impl(context, syncCallback); |
| 55 | } |
| 56 | if (Build.VERSION.SDK_INT >= 18) { |
| 57 | return new JellybeanMr2Impl(context, syncCallback); |
| 58 | } |
| 59 | if (Build.VERSION.SDK_INT >= 17) { |
| 60 | return new JellybeanMr1Impl(context, syncCallback); |
| 61 | } |
| 62 | if (Build.VERSION.SDK_INT >= 16) { |
| 63 | return new JellybeanImpl(context, syncCallback); |
| 64 | } |
| 65 | return new LegacyImpl(context); |
| 66 | } |
| 67 | |
| 68 | /** |
| 69 | * Called by the media router when a route is added to synchronize state with |
| 70 | * the framework media router. |
| 71 | */ |
| 72 | public void onSyncRouteAdded(MediaRouter.RouteInfo route) { |
| 73 | } |
| 74 | |
| 75 | /** |
| 76 | * Called by the media router when a route is removed to synchronize state with |
| 77 | * the framework media router. |
| 78 | */ |
| 79 | public void onSyncRouteRemoved(MediaRouter.RouteInfo route) { |
| 80 | } |
| 81 | |
| 82 | /** |
| 83 | * Called by the media router when a route is changed to synchronize state with |
| 84 | * the framework media router. |
| 85 | */ |
| 86 | public void onSyncRouteChanged(MediaRouter.RouteInfo route) { |
| 87 | } |
| 88 | |
| 89 | /** |
| 90 | * Called by the media router when a route is selected to synchronize state with |
| 91 | * the framework media router. |
| 92 | */ |
| 93 | public void onSyncRouteSelected(MediaRouter.RouteInfo route) { |
| 94 | } |
| 95 | |
| 96 | /** |
| 97 | * Callbacks into the media router to synchronize state with the framework media router. |
| 98 | */ |
| 99 | public interface SyncCallback { |
| 100 | void onSystemRouteSelectedByDescriptorId(String id); |
| 101 | } |
| 102 | |
| 103 | protected Object getDefaultRoute() { |
| 104 | return null; |
| 105 | } |
| 106 | |
| 107 | protected Object getSystemRoute(MediaRouter.RouteInfo route) { |
| 108 | return null; |
| 109 | } |
| 110 | |
| 111 | /** |
| 112 | * Legacy implementation for platform versions prior to Jellybean. |
| 113 | */ |
| 114 | static class LegacyImpl extends SystemMediaRouteProvider { |
| 115 | static final int PLAYBACK_STREAM = AudioManager.STREAM_MUSIC; |
| 116 | |
| 117 | private static final ArrayList<IntentFilter> CONTROL_FILTERS; |
| 118 | static { |
| 119 | IntentFilter f = new IntentFilter(); |
| 120 | f.addCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO); |
| 121 | f.addCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO); |
| 122 | |
| 123 | CONTROL_FILTERS = new ArrayList<IntentFilter>(); |
| 124 | CONTROL_FILTERS.add(f); |
| 125 | } |
| 126 | |
| 127 | final AudioManager mAudioManager; |
| 128 | private final VolumeChangeReceiver mVolumeChangeReceiver; |
| 129 | int mLastReportedVolume = -1; |
| 130 | |
| 131 | public LegacyImpl(Context context) { |
| 132 | super(context); |
| 133 | mAudioManager = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE); |
| 134 | mVolumeChangeReceiver = new VolumeChangeReceiver(); |
| 135 | |
| 136 | context.registerReceiver(mVolumeChangeReceiver, |
| 137 | new IntentFilter(VolumeChangeReceiver.VOLUME_CHANGED_ACTION)); |
| 138 | publishRoutes(); |
| 139 | } |
| 140 | |
| 141 | void publishRoutes() { |
| 142 | Resources r = getContext().getResources(); |
| 143 | int maxVolume = mAudioManager.getStreamMaxVolume(PLAYBACK_STREAM); |
| 144 | mLastReportedVolume = mAudioManager.getStreamVolume(PLAYBACK_STREAM); |
| 145 | MediaRouteDescriptor defaultRoute = new MediaRouteDescriptor.Builder( |
| 146 | DEFAULT_ROUTE_ID, r.getString(R.string.mr_system_route_name)) |
| 147 | .addControlFilters(CONTROL_FILTERS) |
| 148 | .setPlaybackStream(PLAYBACK_STREAM) |
| 149 | .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_LOCAL) |
| 150 | .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE) |
| 151 | .setVolumeMax(maxVolume) |
| 152 | .setVolume(mLastReportedVolume) |
| 153 | .build(); |
| 154 | |
| 155 | MediaRouteProviderDescriptor providerDescriptor = |
| 156 | new MediaRouteProviderDescriptor.Builder() |
| 157 | .addRoute(defaultRoute) |
| 158 | .build(); |
| 159 | setDescriptor(providerDescriptor); |
| 160 | } |
| 161 | |
| 162 | @Override |
| 163 | public RouteController onCreateRouteController(String routeId) { |
| 164 | if (routeId.equals(DEFAULT_ROUTE_ID)) { |
| 165 | return new DefaultRouteController(); |
| 166 | } |
| 167 | return null; |
| 168 | } |
| 169 | |
| 170 | final class DefaultRouteController extends RouteController { |
| 171 | @Override |
| 172 | public void onSetVolume(int volume) { |
| 173 | mAudioManager.setStreamVolume(PLAYBACK_STREAM, volume, 0); |
| 174 | publishRoutes(); |
| 175 | } |
| 176 | |
| 177 | @Override |
| 178 | public void onUpdateVolume(int delta) { |
| 179 | int volume = mAudioManager.getStreamVolume(PLAYBACK_STREAM); |
| 180 | int maxVolume = mAudioManager.getStreamMaxVolume(PLAYBACK_STREAM); |
| 181 | int newVolume = Math.min(maxVolume, Math.max(0, volume + delta)); |
| 182 | if (newVolume != volume) { |
| 183 | mAudioManager.setStreamVolume(PLAYBACK_STREAM, volume, 0); |
| 184 | } |
| 185 | publishRoutes(); |
| 186 | } |
| 187 | } |
| 188 | |
| 189 | final class VolumeChangeReceiver extends BroadcastReceiver { |
| 190 | // These constants come from AudioManager. |
| 191 | public static final String VOLUME_CHANGED_ACTION = |
| 192 | "android.media.VOLUME_CHANGED_ACTION"; |
| 193 | public static final String EXTRA_VOLUME_STREAM_TYPE = |
| 194 | "android.media.EXTRA_VOLUME_STREAM_TYPE"; |
| 195 | public static final String EXTRA_VOLUME_STREAM_VALUE = |
| 196 | "android.media.EXTRA_VOLUME_STREAM_VALUE"; |
| 197 | |
| 198 | @Override |
| 199 | public void onReceive(Context context, Intent intent) { |
| 200 | if (intent.getAction().equals(VOLUME_CHANGED_ACTION)) { |
| 201 | final int streamType = intent.getIntExtra(EXTRA_VOLUME_STREAM_TYPE, -1); |
| 202 | if (streamType == PLAYBACK_STREAM) { |
| 203 | final int volume = intent.getIntExtra(EXTRA_VOLUME_STREAM_VALUE, -1); |
| 204 | if (volume >= 0 && volume != mLastReportedVolume) { |
| 205 | publishRoutes(); |
| 206 | } |
| 207 | } |
| 208 | } |
| 209 | } |
| 210 | } |
| 211 | } |
| 212 | |
| 213 | /** |
| 214 | * Jellybean implementation. |
| 215 | */ |
| 216 | @RequiresApi(16) |
| 217 | static class JellybeanImpl extends SystemMediaRouteProvider |
| 218 | implements MediaRouterJellybean.Callback, MediaRouterJellybean.VolumeCallback { |
| 219 | private static final ArrayList<IntentFilter> LIVE_AUDIO_CONTROL_FILTERS; |
| 220 | static { |
| 221 | IntentFilter f = new IntentFilter(); |
| 222 | f.addCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO); |
| 223 | |
| 224 | LIVE_AUDIO_CONTROL_FILTERS = new ArrayList<IntentFilter>(); |
| 225 | LIVE_AUDIO_CONTROL_FILTERS.add(f); |
| 226 | } |
| 227 | |
| 228 | private static final ArrayList<IntentFilter> LIVE_VIDEO_CONTROL_FILTERS; |
| 229 | static { |
| 230 | IntentFilter f = new IntentFilter(); |
| 231 | f.addCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO); |
| 232 | |
| 233 | LIVE_VIDEO_CONTROL_FILTERS = new ArrayList<IntentFilter>(); |
| 234 | LIVE_VIDEO_CONTROL_FILTERS.add(f); |
| 235 | } |
| 236 | |
| 237 | private final SyncCallback mSyncCallback; |
| 238 | |
| 239 | protected final Object mRouterObj; |
| 240 | protected final Object mCallbackObj; |
| 241 | protected final Object mVolumeCallbackObj; |
| 242 | protected final Object mUserRouteCategoryObj; |
| 243 | protected int mRouteTypes; |
| 244 | protected boolean mActiveScan; |
| 245 | protected boolean mCallbackRegistered; |
| 246 | |
| 247 | // Maintains an association from framework routes to support library routes. |
| 248 | // Note that we cannot use the tag field for this because an application may |
| 249 | // have published its own user routes to the framework media router and already |
| 250 | // used the tag for its own purposes. |
| 251 | protected final ArrayList<SystemRouteRecord> mSystemRouteRecords = |
| 252 | new ArrayList<SystemRouteRecord>(); |
| 253 | |
| 254 | // Maintains an association from support library routes to framework routes. |
| 255 | protected final ArrayList<UserRouteRecord> mUserRouteRecords = |
| 256 | new ArrayList<UserRouteRecord>(); |
| 257 | |
| 258 | private MediaRouterJellybean.SelectRouteWorkaround mSelectRouteWorkaround; |
| 259 | private MediaRouterJellybean.GetDefaultRouteWorkaround mGetDefaultRouteWorkaround; |
| 260 | |
| 261 | public JellybeanImpl(Context context, SyncCallback syncCallback) { |
| 262 | super(context); |
| 263 | mSyncCallback = syncCallback; |
| 264 | mRouterObj = MediaRouterJellybean.getMediaRouter(context); |
| 265 | mCallbackObj = createCallbackObj(); |
| 266 | mVolumeCallbackObj = createVolumeCallbackObj(); |
| 267 | |
| 268 | Resources r = context.getResources(); |
| 269 | mUserRouteCategoryObj = MediaRouterJellybean.createRouteCategory( |
| 270 | mRouterObj, r.getString(R.string.mr_user_route_category_name), false); |
| 271 | |
| 272 | updateSystemRoutes(); |
| 273 | } |
| 274 | |
| 275 | @Override |
| 276 | public RouteController onCreateRouteController(String routeId) { |
| 277 | int index = findSystemRouteRecordByDescriptorId(routeId); |
| 278 | if (index >= 0) { |
| 279 | SystemRouteRecord record = mSystemRouteRecords.get(index); |
| 280 | return new SystemRouteController(record.mRouteObj); |
| 281 | } |
| 282 | return null; |
| 283 | } |
| 284 | |
| 285 | @Override |
| 286 | public void onDiscoveryRequestChanged(MediaRouteDiscoveryRequest request) { |
| 287 | int newRouteTypes = 0; |
| 288 | boolean newActiveScan = false; |
| 289 | if (request != null) { |
| 290 | final MediaRouteSelector selector = request.getSelector(); |
| 291 | final List<String> categories = selector.getControlCategories(); |
| 292 | final int count = categories.size(); |
| 293 | for (int i = 0; i < count; i++) { |
| 294 | String category = categories.get(i); |
| 295 | if (category.equals(MediaControlIntent.CATEGORY_LIVE_AUDIO)) { |
| 296 | newRouteTypes |= MediaRouterJellybean.ROUTE_TYPE_LIVE_AUDIO; |
| 297 | } else if (category.equals(MediaControlIntent.CATEGORY_LIVE_VIDEO)) { |
| 298 | newRouteTypes |= MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO; |
| 299 | } else { |
| 300 | newRouteTypes |= MediaRouterJellybean.ROUTE_TYPE_USER; |
| 301 | } |
| 302 | } |
| 303 | newActiveScan = request.isActiveScan(); |
| 304 | } |
| 305 | |
| 306 | if (mRouteTypes != newRouteTypes || mActiveScan != newActiveScan) { |
| 307 | mRouteTypes = newRouteTypes; |
| 308 | mActiveScan = newActiveScan; |
| 309 | updateSystemRoutes(); |
| 310 | } |
| 311 | } |
| 312 | |
| 313 | @Override |
| 314 | public void onRouteAdded(Object routeObj) { |
| 315 | if (addSystemRouteNoPublish(routeObj)) { |
| 316 | publishRoutes(); |
| 317 | } |
| 318 | } |
| 319 | |
| 320 | private void updateSystemRoutes() { |
| 321 | updateCallback(); |
| 322 | boolean changed = false; |
| 323 | for (Object routeObj : MediaRouterJellybean.getRoutes(mRouterObj)) { |
| 324 | changed |= addSystemRouteNoPublish(routeObj); |
| 325 | } |
| 326 | if (changed) { |
| 327 | publishRoutes(); |
| 328 | } |
| 329 | } |
| 330 | |
| 331 | private boolean addSystemRouteNoPublish(Object routeObj) { |
| 332 | if (getUserRouteRecord(routeObj) == null |
| 333 | && findSystemRouteRecord(routeObj) < 0) { |
| 334 | String id = assignRouteId(routeObj); |
| 335 | SystemRouteRecord record = new SystemRouteRecord(routeObj, id); |
| 336 | updateSystemRouteDescriptor(record); |
| 337 | mSystemRouteRecords.add(record); |
| 338 | return true; |
| 339 | } |
| 340 | return false; |
| 341 | } |
| 342 | |
| 343 | private String assignRouteId(Object routeObj) { |
| 344 | // TODO: The framework media router should supply a unique route id that |
| 345 | // we can use here. For now we use a hash of the route name and take care |
| 346 | // to dedupe it. |
| 347 | boolean isDefault = (getDefaultRoute() == routeObj); |
| 348 | String id = isDefault ? DEFAULT_ROUTE_ID : |
| 349 | String.format(Locale.US, "ROUTE_%08x", getRouteName(routeObj).hashCode()); |
| 350 | if (findSystemRouteRecordByDescriptorId(id) < 0) { |
| 351 | return id; |
| 352 | } |
| 353 | for (int i = 2; ; i++) { |
| 354 | String newId = String.format(Locale.US, "%s_%d", id, i); |
| 355 | if (findSystemRouteRecordByDescriptorId(newId) < 0) { |
| 356 | return newId; |
| 357 | } |
| 358 | } |
| 359 | } |
| 360 | |
| 361 | @Override |
| 362 | public void onRouteRemoved(Object routeObj) { |
| 363 | if (getUserRouteRecord(routeObj) == null) { |
| 364 | int index = findSystemRouteRecord(routeObj); |
| 365 | if (index >= 0) { |
| 366 | mSystemRouteRecords.remove(index); |
| 367 | publishRoutes(); |
| 368 | } |
| 369 | } |
| 370 | } |
| 371 | |
| 372 | @Override |
| 373 | public void onRouteChanged(Object routeObj) { |
| 374 | if (getUserRouteRecord(routeObj) == null) { |
| 375 | int index = findSystemRouteRecord(routeObj); |
| 376 | if (index >= 0) { |
| 377 | SystemRouteRecord record = mSystemRouteRecords.get(index); |
| 378 | updateSystemRouteDescriptor(record); |
| 379 | publishRoutes(); |
| 380 | } |
| 381 | } |
| 382 | } |
| 383 | |
| 384 | @Override |
| 385 | public void onRouteVolumeChanged(Object routeObj) { |
| 386 | if (getUserRouteRecord(routeObj) == null) { |
| 387 | int index = findSystemRouteRecord(routeObj); |
| 388 | if (index >= 0) { |
| 389 | SystemRouteRecord record = mSystemRouteRecords.get(index); |
| 390 | int newVolume = MediaRouterJellybean.RouteInfo.getVolume(routeObj); |
| 391 | if (newVolume != record.mRouteDescriptor.getVolume()) { |
| 392 | record.mRouteDescriptor = |
| 393 | new MediaRouteDescriptor.Builder(record.mRouteDescriptor) |
| 394 | .setVolume(newVolume) |
| 395 | .build(); |
| 396 | publishRoutes(); |
| 397 | } |
| 398 | } |
| 399 | } |
| 400 | } |
| 401 | |
| 402 | @Override |
| 403 | public void onRouteSelected(int type, Object routeObj) { |
| 404 | if (routeObj != MediaRouterJellybean.getSelectedRoute(mRouterObj, |
| 405 | MediaRouterJellybean.ALL_ROUTE_TYPES)) { |
| 406 | // The currently selected route has already changed so this callback |
| 407 | // is stale. Drop it to prevent getting into sync loops. |
| 408 | return; |
| 409 | } |
| 410 | |
| 411 | UserRouteRecord userRouteRecord = getUserRouteRecord(routeObj); |
| 412 | if (userRouteRecord != null) { |
| 413 | userRouteRecord.mRoute.select(); |
| 414 | } else { |
| 415 | // Select the route if it already exists in the compat media router. |
| 416 | // If not, we will select it instead when the route is added. |
| 417 | int index = findSystemRouteRecord(routeObj); |
| 418 | if (index >= 0) { |
| 419 | SystemRouteRecord record = mSystemRouteRecords.get(index); |
| 420 | mSyncCallback.onSystemRouteSelectedByDescriptorId(record.mRouteDescriptorId); |
| 421 | } |
| 422 | } |
| 423 | } |
| 424 | |
| 425 | @Override |
| 426 | public void onRouteUnselected(int type, Object routeObj) { |
| 427 | // Nothing to do when a route is unselected. |
| 428 | // We only need to handle when a route is selected. |
| 429 | } |
| 430 | |
| 431 | @Override |
| 432 | public void onRouteGrouped(Object routeObj, Object groupObj, int index) { |
| 433 | // Route grouping is deprecated and no longer supported. |
| 434 | } |
| 435 | |
| 436 | @Override |
| 437 | public void onRouteUngrouped(Object routeObj, Object groupObj) { |
| 438 | // Route grouping is deprecated and no longer supported. |
| 439 | } |
| 440 | |
| 441 | @Override |
| 442 | public void onVolumeSetRequest(Object routeObj, int volume) { |
| 443 | UserRouteRecord record = getUserRouteRecord(routeObj); |
| 444 | if (record != null) { |
| 445 | record.mRoute.requestSetVolume(volume); |
| 446 | } |
| 447 | } |
| 448 | |
| 449 | @Override |
| 450 | public void onVolumeUpdateRequest(Object routeObj, int direction) { |
| 451 | UserRouteRecord record = getUserRouteRecord(routeObj); |
| 452 | if (record != null) { |
| 453 | record.mRoute.requestUpdateVolume(direction); |
| 454 | } |
| 455 | } |
| 456 | |
| 457 | @Override |
| 458 | public void onSyncRouteAdded(MediaRouter.RouteInfo route) { |
| 459 | if (route.getProviderInstance() != this) { |
| 460 | Object routeObj = MediaRouterJellybean.createUserRoute( |
| 461 | mRouterObj, mUserRouteCategoryObj); |
| 462 | UserRouteRecord record = new UserRouteRecord(route, routeObj); |
| 463 | MediaRouterJellybean.RouteInfo.setTag(routeObj, record); |
| 464 | MediaRouterJellybean.UserRouteInfo.setVolumeCallback(routeObj, mVolumeCallbackObj); |
| 465 | updateUserRouteProperties(record); |
| 466 | mUserRouteRecords.add(record); |
| 467 | MediaRouterJellybean.addUserRoute(mRouterObj, routeObj); |
| 468 | } else { |
| 469 | // If the newly added route is the counterpart of the currently selected |
| 470 | // route in the framework media router then ensure it is selected in |
| 471 | // the compat media router. |
| 472 | Object routeObj = MediaRouterJellybean.getSelectedRoute( |
| 473 | mRouterObj, MediaRouterJellybean.ALL_ROUTE_TYPES); |
| 474 | int index = findSystemRouteRecord(routeObj); |
| 475 | if (index >= 0) { |
| 476 | SystemRouteRecord record = mSystemRouteRecords.get(index); |
| 477 | if (record.mRouteDescriptorId.equals(route.getDescriptorId())) { |
| 478 | route.select(); |
| 479 | } |
| 480 | } |
| 481 | } |
| 482 | } |
| 483 | |
| 484 | @Override |
| 485 | public void onSyncRouteRemoved(MediaRouter.RouteInfo route) { |
| 486 | if (route.getProviderInstance() != this) { |
| 487 | int index = findUserRouteRecord(route); |
| 488 | if (index >= 0) { |
| 489 | UserRouteRecord record = mUserRouteRecords.remove(index); |
| 490 | MediaRouterJellybean.RouteInfo.setTag(record.mRouteObj, null); |
| 491 | MediaRouterJellybean.UserRouteInfo.setVolumeCallback(record.mRouteObj, null); |
| 492 | MediaRouterJellybean.removeUserRoute(mRouterObj, record.mRouteObj); |
| 493 | } |
| 494 | } |
| 495 | } |
| 496 | |
| 497 | @Override |
| 498 | public void onSyncRouteChanged(MediaRouter.RouteInfo route) { |
| 499 | if (route.getProviderInstance() != this) { |
| 500 | int index = findUserRouteRecord(route); |
| 501 | if (index >= 0) { |
| 502 | UserRouteRecord record = mUserRouteRecords.get(index); |
| 503 | updateUserRouteProperties(record); |
| 504 | } |
| 505 | } |
| 506 | } |
| 507 | |
| 508 | @Override |
| 509 | public void onSyncRouteSelected(MediaRouter.RouteInfo route) { |
| 510 | if (!route.isSelected()) { |
| 511 | // The currently selected route has already changed so this callback |
| 512 | // is stale. Drop it to prevent getting into sync loops. |
| 513 | return; |
| 514 | } |
| 515 | |
| 516 | if (route.getProviderInstance() != this) { |
| 517 | int index = findUserRouteRecord(route); |
| 518 | if (index >= 0) { |
| 519 | UserRouteRecord record = mUserRouteRecords.get(index); |
| 520 | selectRoute(record.mRouteObj); |
| 521 | } |
| 522 | } else { |
| 523 | int index = findSystemRouteRecordByDescriptorId(route.getDescriptorId()); |
| 524 | if (index >= 0) { |
| 525 | SystemRouteRecord record = mSystemRouteRecords.get(index); |
| 526 | selectRoute(record.mRouteObj); |
| 527 | } |
| 528 | } |
| 529 | } |
| 530 | |
| 531 | protected void publishRoutes() { |
| 532 | MediaRouteProviderDescriptor.Builder builder = |
| 533 | new MediaRouteProviderDescriptor.Builder(); |
| 534 | int count = mSystemRouteRecords.size(); |
| 535 | for (int i = 0; i < count; i++) { |
| 536 | builder.addRoute(mSystemRouteRecords.get(i).mRouteDescriptor); |
| 537 | } |
| 538 | |
| 539 | setDescriptor(builder.build()); |
| 540 | } |
| 541 | |
| 542 | protected int findSystemRouteRecord(Object routeObj) { |
| 543 | final int count = mSystemRouteRecords.size(); |
| 544 | for (int i = 0; i < count; i++) { |
| 545 | if (mSystemRouteRecords.get(i).mRouteObj == routeObj) { |
| 546 | return i; |
| 547 | } |
| 548 | } |
| 549 | return -1; |
| 550 | } |
| 551 | |
| 552 | protected int findSystemRouteRecordByDescriptorId(String id) { |
| 553 | final int count = mSystemRouteRecords.size(); |
| 554 | for (int i = 0; i < count; i++) { |
| 555 | if (mSystemRouteRecords.get(i).mRouteDescriptorId.equals(id)) { |
| 556 | return i; |
| 557 | } |
| 558 | } |
| 559 | return -1; |
| 560 | } |
| 561 | |
| 562 | protected int findUserRouteRecord(MediaRouter.RouteInfo route) { |
| 563 | final int count = mUserRouteRecords.size(); |
| 564 | for (int i = 0; i < count; i++) { |
| 565 | if (mUserRouteRecords.get(i).mRoute == route) { |
| 566 | return i; |
| 567 | } |
| 568 | } |
| 569 | return -1; |
| 570 | } |
| 571 | |
| 572 | protected UserRouteRecord getUserRouteRecord(Object routeObj) { |
| 573 | Object tag = MediaRouterJellybean.RouteInfo.getTag(routeObj); |
| 574 | return tag instanceof UserRouteRecord ? (UserRouteRecord)tag : null; |
| 575 | } |
| 576 | |
| 577 | protected void updateSystemRouteDescriptor(SystemRouteRecord record) { |
| 578 | // We must always recreate the route descriptor when making any changes |
| 579 | // because they are intended to be immutable once published. |
| 580 | MediaRouteDescriptor.Builder builder = new MediaRouteDescriptor.Builder( |
| 581 | record.mRouteDescriptorId, getRouteName(record.mRouteObj)); |
| 582 | onBuildSystemRouteDescriptor(record, builder); |
| 583 | record.mRouteDescriptor = builder.build(); |
| 584 | } |
| 585 | |
| 586 | protected String getRouteName(Object routeObj) { |
| 587 | // Routes should not have null names but it may happen for badly configured |
| 588 | // user routes. We tolerate this by using an empty name string here but |
| 589 | // such unnamed routes will be discarded by the media router upstream |
| 590 | // (with a log message so we can track down the problem). |
| 591 | CharSequence name = MediaRouterJellybean.RouteInfo.getName(routeObj, getContext()); |
| 592 | return name != null ? name.toString() : ""; |
| 593 | } |
| 594 | |
| 595 | protected void onBuildSystemRouteDescriptor(SystemRouteRecord record, |
| 596 | MediaRouteDescriptor.Builder builder) { |
| 597 | int supportedTypes = MediaRouterJellybean.RouteInfo.getSupportedTypes( |
| 598 | record.mRouteObj); |
| 599 | if ((supportedTypes & MediaRouterJellybean.ROUTE_TYPE_LIVE_AUDIO) != 0) { |
| 600 | builder.addControlFilters(LIVE_AUDIO_CONTROL_FILTERS); |
| 601 | } |
| 602 | if ((supportedTypes & MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO) != 0) { |
| 603 | builder.addControlFilters(LIVE_VIDEO_CONTROL_FILTERS); |
| 604 | } |
| 605 | |
| 606 | builder.setPlaybackType( |
| 607 | MediaRouterJellybean.RouteInfo.getPlaybackType(record.mRouteObj)); |
| 608 | builder.setPlaybackStream( |
| 609 | MediaRouterJellybean.RouteInfo.getPlaybackStream(record.mRouteObj)); |
| 610 | builder.setVolume( |
| 611 | MediaRouterJellybean.RouteInfo.getVolume(record.mRouteObj)); |
| 612 | builder.setVolumeMax( |
| 613 | MediaRouterJellybean.RouteInfo.getVolumeMax(record.mRouteObj)); |
| 614 | builder.setVolumeHandling( |
| 615 | MediaRouterJellybean.RouteInfo.getVolumeHandling(record.mRouteObj)); |
| 616 | } |
| 617 | |
| 618 | protected void updateUserRouteProperties(UserRouteRecord record) { |
| 619 | MediaRouterJellybean.UserRouteInfo.setName( |
| 620 | record.mRouteObj, record.mRoute.getName()); |
| 621 | MediaRouterJellybean.UserRouteInfo.setPlaybackType( |
| 622 | record.mRouteObj, record.mRoute.getPlaybackType()); |
| 623 | MediaRouterJellybean.UserRouteInfo.setPlaybackStream( |
| 624 | record.mRouteObj, record.mRoute.getPlaybackStream()); |
| 625 | MediaRouterJellybean.UserRouteInfo.setVolume( |
| 626 | record.mRouteObj, record.mRoute.getVolume()); |
| 627 | MediaRouterJellybean.UserRouteInfo.setVolumeMax( |
| 628 | record.mRouteObj, record.mRoute.getVolumeMax()); |
| 629 | MediaRouterJellybean.UserRouteInfo.setVolumeHandling( |
| 630 | record.mRouteObj, record.mRoute.getVolumeHandling()); |
| 631 | } |
| 632 | |
| 633 | protected void updateCallback() { |
| 634 | if (mCallbackRegistered) { |
| 635 | mCallbackRegistered = false; |
| 636 | MediaRouterJellybean.removeCallback(mRouterObj, mCallbackObj); |
| 637 | } |
| 638 | |
| 639 | if (mRouteTypes != 0) { |
| 640 | mCallbackRegistered = true; |
| 641 | MediaRouterJellybean.addCallback(mRouterObj, mRouteTypes, mCallbackObj); |
| 642 | } |
| 643 | } |
| 644 | |
| 645 | protected Object createCallbackObj() { |
| 646 | return MediaRouterJellybean.createCallback(this); |
| 647 | } |
| 648 | |
| 649 | protected Object createVolumeCallbackObj() { |
| 650 | return MediaRouterJellybean.createVolumeCallback(this); |
| 651 | } |
| 652 | |
| 653 | protected void selectRoute(Object routeObj) { |
| 654 | if (mSelectRouteWorkaround == null) { |
| 655 | mSelectRouteWorkaround = new MediaRouterJellybean.SelectRouteWorkaround(); |
| 656 | } |
| 657 | mSelectRouteWorkaround.selectRoute(mRouterObj, |
| 658 | MediaRouterJellybean.ALL_ROUTE_TYPES, routeObj); |
| 659 | } |
| 660 | |
| 661 | @Override |
| 662 | protected Object getDefaultRoute() { |
| 663 | if (mGetDefaultRouteWorkaround == null) { |
| 664 | mGetDefaultRouteWorkaround = new MediaRouterJellybean.GetDefaultRouteWorkaround(); |
| 665 | } |
| 666 | return mGetDefaultRouteWorkaround.getDefaultRoute(mRouterObj); |
| 667 | } |
| 668 | |
| 669 | @Override |
| 670 | protected Object getSystemRoute(MediaRouter.RouteInfo route) { |
| 671 | if (route == null) { |
| 672 | return null; |
| 673 | } |
| 674 | int index = findSystemRouteRecordByDescriptorId(route.getDescriptorId()); |
| 675 | if (index >= 0) { |
| 676 | return mSystemRouteRecords.get(index).mRouteObj; |
| 677 | } |
| 678 | return null; |
| 679 | } |
| 680 | |
| 681 | /** |
| 682 | * Represents a route that is provided by the framework media router |
| 683 | * and published by this route provider to the support library media router. |
| 684 | */ |
| 685 | protected static final class SystemRouteRecord { |
| 686 | public final Object mRouteObj; |
| 687 | public final String mRouteDescriptorId; |
| 688 | public MediaRouteDescriptor mRouteDescriptor; // assigned immediately after creation |
| 689 | |
| 690 | public SystemRouteRecord(Object routeObj, String id) { |
| 691 | mRouteObj = routeObj; |
| 692 | mRouteDescriptorId = id; |
| 693 | } |
| 694 | } |
| 695 | |
| 696 | /** |
| 697 | * Represents a route that is provided by the support library media router |
| 698 | * and published by this route provider to the framework media router. |
| 699 | */ |
| 700 | protected static final class UserRouteRecord { |
| 701 | public final MediaRouter.RouteInfo mRoute; |
| 702 | public final Object mRouteObj; |
| 703 | |
| 704 | public UserRouteRecord(MediaRouter.RouteInfo route, Object routeObj) { |
| 705 | mRoute = route; |
| 706 | mRouteObj = routeObj; |
| 707 | } |
| 708 | } |
| 709 | |
| 710 | protected static final class SystemRouteController extends RouteController { |
| 711 | private final Object mRouteObj; |
| 712 | |
| 713 | public SystemRouteController(Object routeObj) { |
| 714 | mRouteObj = routeObj; |
| 715 | } |
| 716 | |
| 717 | @Override |
| 718 | public void onSetVolume(int volume) { |
| 719 | MediaRouterJellybean.RouteInfo.requestSetVolume(mRouteObj, volume); |
| 720 | } |
| 721 | |
| 722 | @Override |
| 723 | public void onUpdateVolume(int delta) { |
| 724 | MediaRouterJellybean.RouteInfo.requestUpdateVolume(mRouteObj, delta); |
| 725 | } |
| 726 | } |
| 727 | } |
| 728 | |
| 729 | /** |
| 730 | * Jellybean MR1 implementation. |
| 731 | */ |
| 732 | @RequiresApi(17) |
| 733 | private static class JellybeanMr1Impl extends JellybeanImpl |
| 734 | implements MediaRouterJellybeanMr1.Callback { |
| 735 | private MediaRouterJellybeanMr1.ActiveScanWorkaround mActiveScanWorkaround; |
| 736 | private MediaRouterJellybeanMr1.IsConnectingWorkaround mIsConnectingWorkaround; |
| 737 | |
| 738 | public JellybeanMr1Impl(Context context, SyncCallback syncCallback) { |
| 739 | super(context, syncCallback); |
| 740 | } |
| 741 | |
| 742 | @Override |
| 743 | public void onRoutePresentationDisplayChanged(Object routeObj) { |
| 744 | int index = findSystemRouteRecord(routeObj); |
| 745 | if (index >= 0) { |
| 746 | SystemRouteRecord record = mSystemRouteRecords.get(index); |
| 747 | Display newPresentationDisplay = |
| 748 | MediaRouterJellybeanMr1.RouteInfo.getPresentationDisplay(routeObj); |
| 749 | int newPresentationDisplayId = (newPresentationDisplay != null |
| 750 | ? newPresentationDisplay.getDisplayId() : -1); |
| 751 | if (newPresentationDisplayId |
| 752 | != record.mRouteDescriptor.getPresentationDisplayId()) { |
| 753 | record.mRouteDescriptor = |
| 754 | new MediaRouteDescriptor.Builder(record.mRouteDescriptor) |
| 755 | .setPresentationDisplayId(newPresentationDisplayId) |
| 756 | .build(); |
| 757 | publishRoutes(); |
| 758 | } |
| 759 | } |
| 760 | } |
| 761 | |
| 762 | @Override |
| 763 | protected void onBuildSystemRouteDescriptor(SystemRouteRecord record, |
| 764 | MediaRouteDescriptor.Builder builder) { |
| 765 | super.onBuildSystemRouteDescriptor(record, builder); |
| 766 | |
| 767 | if (!MediaRouterJellybeanMr1.RouteInfo.isEnabled(record.mRouteObj)) { |
| 768 | builder.setEnabled(false); |
| 769 | } |
| 770 | |
| 771 | if (isConnecting(record)) { |
| 772 | builder.setConnecting(true); |
| 773 | } |
| 774 | |
| 775 | Display presentationDisplay = |
| 776 | MediaRouterJellybeanMr1.RouteInfo.getPresentationDisplay(record.mRouteObj); |
| 777 | if (presentationDisplay != null) { |
| 778 | builder.setPresentationDisplayId(presentationDisplay.getDisplayId()); |
| 779 | } |
| 780 | } |
| 781 | |
| 782 | @Override |
| 783 | protected void updateCallback() { |
| 784 | super.updateCallback(); |
| 785 | |
| 786 | if (mActiveScanWorkaround == null) { |
| 787 | mActiveScanWorkaround = new MediaRouterJellybeanMr1.ActiveScanWorkaround( |
| 788 | getContext(), getHandler()); |
| 789 | } |
| 790 | mActiveScanWorkaround.setActiveScanRouteTypes(mActiveScan ? mRouteTypes : 0); |
| 791 | } |
| 792 | |
| 793 | @Override |
| 794 | protected Object createCallbackObj() { |
| 795 | return MediaRouterJellybeanMr1.createCallback(this); |
| 796 | } |
| 797 | |
| 798 | protected boolean isConnecting(SystemRouteRecord record) { |
| 799 | if (mIsConnectingWorkaround == null) { |
| 800 | mIsConnectingWorkaround = new MediaRouterJellybeanMr1.IsConnectingWorkaround(); |
| 801 | } |
| 802 | return mIsConnectingWorkaround.isConnecting(record.mRouteObj); |
| 803 | } |
| 804 | } |
| 805 | |
| 806 | /** |
| 807 | * Jellybean MR2 implementation. |
| 808 | */ |
| 809 | @RequiresApi(18) |
| 810 | private static class JellybeanMr2Impl extends JellybeanMr1Impl { |
| 811 | public JellybeanMr2Impl(Context context, SyncCallback syncCallback) { |
| 812 | super(context, syncCallback); |
| 813 | } |
| 814 | |
| 815 | @Override |
| 816 | protected void onBuildSystemRouteDescriptor(SystemRouteRecord record, |
| 817 | MediaRouteDescriptor.Builder builder) { |
| 818 | super.onBuildSystemRouteDescriptor(record, builder); |
| 819 | |
| 820 | CharSequence description = |
| 821 | MediaRouterJellybeanMr2.RouteInfo.getDescription(record.mRouteObj); |
| 822 | if (description != null) { |
| 823 | builder.setDescription(description.toString()); |
| 824 | } |
| 825 | } |
| 826 | |
| 827 | @Override |
| 828 | protected void selectRoute(Object routeObj) { |
| 829 | MediaRouterJellybean.selectRoute(mRouterObj, |
| 830 | MediaRouterJellybean.ALL_ROUTE_TYPES, routeObj); |
| 831 | } |
| 832 | |
| 833 | @Override |
| 834 | protected Object getDefaultRoute() { |
| 835 | return MediaRouterJellybeanMr2.getDefaultRoute(mRouterObj); |
| 836 | } |
| 837 | |
| 838 | @Override |
| 839 | protected void updateUserRouteProperties(UserRouteRecord record) { |
| 840 | super.updateUserRouteProperties(record); |
| 841 | |
| 842 | MediaRouterJellybeanMr2.UserRouteInfo.setDescription( |
| 843 | record.mRouteObj, record.mRoute.getDescription()); |
| 844 | } |
| 845 | |
| 846 | @Override |
| 847 | protected void updateCallback() { |
| 848 | if (mCallbackRegistered) { |
| 849 | MediaRouterJellybean.removeCallback(mRouterObj, mCallbackObj); |
| 850 | } |
| 851 | |
| 852 | mCallbackRegistered = true; |
| 853 | MediaRouterJellybeanMr2.addCallback(mRouterObj, mRouteTypes, mCallbackObj, |
| 854 | MediaRouter.CALLBACK_FLAG_UNFILTERED_EVENTS |
| 855 | | (mActiveScan ? MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN : 0)); |
| 856 | } |
| 857 | |
| 858 | @Override |
| 859 | protected boolean isConnecting(SystemRouteRecord record) { |
| 860 | return MediaRouterJellybeanMr2.RouteInfo.isConnecting(record.mRouteObj); |
| 861 | } |
| 862 | } |
| 863 | |
| 864 | /** |
| 865 | * Api24 implementation. |
| 866 | */ |
| 867 | @RequiresApi(24) |
| 868 | private static class Api24Impl extends JellybeanMr2Impl { |
| 869 | public Api24Impl(Context context, SyncCallback syncCallback) { |
| 870 | super(context, syncCallback); |
| 871 | } |
| 872 | |
| 873 | @Override |
| 874 | protected void onBuildSystemRouteDescriptor(SystemRouteRecord record, |
| 875 | MediaRouteDescriptor.Builder builder) { |
| 876 | super.onBuildSystemRouteDescriptor(record, builder); |
| 877 | |
| 878 | builder.setDeviceType(MediaRouterApi24.RouteInfo.getDeviceType(record.mRouteObj)); |
| 879 | } |
| 880 | } |
| 881 | } |