1 /* 2 * Copyright 2022 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.activities; 18 19 import android.Manifest; 20 import android.app.Activity; 21 import android.app.PendingIntent; 22 import android.content.BroadcastReceiver; 23 import android.content.ComponentName; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.pm.PackageManager; 27 import android.net.Uri; 28 import android.os.Build; 29 import android.os.Bundle; 30 import android.os.Environment; 31 import android.os.Handler; 32 import android.os.SystemClock; 33 import android.provider.Settings; 34 import android.support.v4.media.session.MediaSessionCompat; 35 import android.util.Log; 36 import android.view.KeyEvent; 37 import android.view.Menu; 38 import android.view.MenuItem; 39 import android.view.View; 40 import android.widget.CheckBox; 41 import android.widget.ImageButton; 42 import android.widget.ListView; 43 import android.widget.SeekBar; 44 import android.widget.SeekBar.OnSeekBarChangeListener; 45 import android.widget.TabHost; 46 import android.widget.TabHost.TabSpec; 47 import android.widget.TextView; 48 import android.widget.Toast; 49 50 import androidx.annotation.RequiresApi; 51 import androidx.appcompat.app.AppCompatActivity; 52 import androidx.core.content.ContextCompat; 53 import androidx.core.view.MenuItemCompat; 54 import androidx.fragment.app.FragmentManager; 55 import androidx.mediarouter.app.MediaRouteActionProvider; 56 import androidx.mediarouter.app.MediaRouteControllerDialog; 57 import androidx.mediarouter.app.MediaRouteControllerDialogFragment; 58 import androidx.mediarouter.app.MediaRouteDialogFactory; 59 import androidx.mediarouter.app.MediaRouteDiscoveryFragment; 60 import androidx.mediarouter.media.MediaControlIntent; 61 import androidx.mediarouter.media.MediaItemStatus; 62 import androidx.mediarouter.media.MediaRouteSelector; 63 import androidx.mediarouter.media.MediaRouter; 64 import androidx.mediarouter.media.MediaRouter.ProviderInfo; 65 import androidx.mediarouter.media.MediaRouter.RouteInfo; 66 import androidx.mediarouter.media.MediaRouterParams; 67 import androidx.mediarouter.media.RouteListingPreference; 68 69 import com.example.androidx.mediarouting.MyMediaRouteControllerDialog; 70 import com.example.androidx.mediarouting.R; 71 import com.example.androidx.mediarouting.RoutesManager; 72 import com.example.androidx.mediarouting.data.MediaItem; 73 import com.example.androidx.mediarouting.data.PlaylistItem; 74 import com.example.androidx.mediarouting.player.Player; 75 import com.example.androidx.mediarouting.player.RemotePlayer; 76 import com.example.androidx.mediarouting.providers.SampleMediaRouteProvider; 77 import com.example.androidx.mediarouting.providers.WrapperMediaRouteProvider; 78 import com.example.androidx.mediarouting.services.WrapperMediaRouteProviderService; 79 import com.example.androidx.mediarouting.session.SessionManager; 80 import com.example.androidx.mediarouting.ui.LibraryAdapter; 81 import com.example.androidx.mediarouting.ui.PlaylistAdapter; 82 import com.google.common.base.Function; 83 import com.google.common.util.concurrent.Futures; 84 import com.google.common.util.concurrent.ListenableFuture; 85 86 import org.jspecify.annotations.NonNull; 87 import org.jspecify.annotations.Nullable; 88 89 import java.io.File; 90 import java.util.List; 91 92 /** 93 * Demonstrates how to use the {@link MediaRouter} API to build an application that allows the user 94 * to send content to various rendering targets. 95 */ 96 public class MainActivity extends AppCompatActivity { 97 private static final String TAG = "MainActivity"; 98 private static final String DISCOVERY_FRAGMENT_TAG = "DiscoveryFragment"; 99 private static final int POST_NOTIFICATIONS_PERMISSION_REQUEST_CODE = 5001; 100 private static final boolean ENABLE_DEFAULT_CONTROL_CHECK_BOX = false; 101 102 private final Handler mHandler = new Handler(); 103 private final Runnable mUpdateSeekRunnable = 104 new Runnable() { 105 @Override 106 public void run() { 107 updateProgress(); 108 // update Ui every 1 second 109 mHandler.postDelayed(this, 1000); 110 } 111 }; 112 private final SessionManager mSessionManager = new SessionManager("app"); 113 private final MediaRouter.OnPrepareTransferListener mOnPrepareTransferListener = 114 createTransferListener(); 115 private final MediaRouter.Callback mMediaRouterCB = new SampleMediaRouterCallback(); 116 117 private MediaRouter mMediaRouter; 118 private MediaRouteSelector mSelector; 119 private PlaylistAdapter mPlayListItems; 120 private TextView mInfoTextView; 121 private ListView mPlayListView; 122 private CheckBox mUseDefaultControlCheckBox; 123 private ImageButton mPauseResumeButton; 124 private SeekBar mSeekBar; 125 private Player mPlayer; 126 private MediaSessionCompat mMediaSession; 127 private ComponentName mEventReceiver; 128 private PendingIntent mMediaPendingIntent; 129 130 @Override onCreate(@ullable Bundle savedInstanceState)131 protected void onCreate(@Nullable Bundle savedInstanceState) { 132 super.onCreate(savedInstanceState); 133 134 requestRequiredPermissions(); 135 136 mMediaRouter = MediaRouter.getInstance(this); 137 mMediaRouter.setRouterParams(getRouterParams()); 138 139 RoutesManager routesManager = RoutesManager.getInstance(getApplicationContext()); 140 routesManager.reloadDialogType(); 141 142 // Create a route selector for the type of routes that we care about. 143 mSelector = 144 new MediaRouteSelector.Builder() 145 .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK) 146 .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_AUDIO_PLAYBACK) 147 .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_VIDEO_PLAYBACK) 148 .addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO) 149 .addControlCategory(SampleMediaRouteProvider.CATEGORY_SAMPLE_ROUTE) 150 .addControlCategory(WrapperMediaRouteProvider.CATEGORY_WRAPPER_ROUTE) 151 .build(); 152 153 mMediaRouter.setOnPrepareTransferListener(mOnPrepareTransferListener); 154 155 // Add a fragment to take care of media route discovery. 156 // This fragment automatically adds or removes a callback whenever the activity 157 // is started or stopped. 158 FragmentManager fm = getSupportFragmentManager(); 159 DiscoveryFragment fragment = (DiscoveryFragment) fm.findFragmentByTag( 160 DISCOVERY_FRAGMENT_TAG); 161 if (fragment == null) { 162 fragment = new DiscoveryFragment(); 163 fm.beginTransaction() 164 .add(fragment, DISCOVERY_FRAGMENT_TAG) 165 .commit(); 166 } 167 fragment.setCallback(mMediaRouterCB); 168 fragment.setRouteSelector(mSelector); 169 170 // Populate an array adapter with streaming media items. 171 String[] mediaNames = getResources().getStringArray(R.array.media_names); 172 String[] mediaUris = getResources().getStringArray(R.array.media_uris); 173 String[] mediaMimes = getResources().getStringArray(R.array.media_mimes); 174 LibraryAdapter libraryItems = 175 new LibraryAdapter(/* mainActivity= */ this, /* sessionManager= */ mSessionManager); 176 for (int i = 0; i < mediaNames.length; i++) { 177 libraryItems.add(new MediaItem( 178 "[streaming] " + mediaNames[i], Uri.parse(mediaUris[i]), mediaMimes[i])); 179 } 180 181 // Scan local external storage directory for media files. 182 File externalDir = Environment.getExternalStorageDirectory(); 183 if (externalDir != null) { 184 File[] list = externalDir.listFiles(); 185 if (list != null) { 186 for (File file : list) { 187 String filename = file.getName(); 188 if (filename.matches(".*\\.(m4v|mp4)")) { 189 libraryItems.add(new MediaItem("[local] " + filename, 190 Uri.fromFile(file), "video/mp4")); 191 } 192 } 193 } 194 } 195 196 mPlayListItems = new PlaylistAdapter(/* mainActivity= */ this, 197 /* sessionManager= */ mSessionManager); 198 199 // Initialize the layout. 200 setContentView(R.layout.sample_media_router); 201 202 TabHost tabHost = findViewById(R.id.tabHost); 203 tabHost.setup(); 204 String tabName = getResources().getString(R.string.library_tab_text); 205 TabSpec spec1 = tabHost.newTabSpec(tabName); 206 spec1.setContent(R.id.tab1); 207 spec1.setIndicator(tabName); 208 209 tabName = getResources().getString(R.string.playlist_tab_text); 210 TabSpec spec2 = tabHost.newTabSpec(tabName); 211 spec2.setIndicator(tabName); 212 spec2.setContent(R.id.tab2); 213 214 tabName = getResources().getString(R.string.info_tab_text); 215 TabSpec spec3 = tabHost.newTabSpec(tabName); 216 spec3.setIndicator(tabName); 217 spec3.setContent(R.id.tab3); 218 219 tabHost.addTab(spec1); 220 tabHost.addTab(spec2); 221 tabHost.addTab(spec3); 222 tabHost.setOnTabChangedListener(arg0 -> updateUi()); 223 224 ListView libraryView = findViewById(R.id.media); 225 libraryView.setAdapter(libraryItems); 226 libraryView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); 227 libraryView.setOnItemClickListener((parent, view, position, id) -> updateButtons()); 228 229 mPlayListView = findViewById(R.id.playlist); 230 mPlayListView.setAdapter(mPlayListItems); 231 mPlayListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); 232 mPlayListView.setOnItemClickListener((parent, view, position, id) -> updateButtons()); 233 234 mInfoTextView = findViewById(R.id.info); 235 236 mUseDefaultControlCheckBox = findViewById(R.id.custom_control_view_checkbox); 237 if (ENABLE_DEFAULT_CONTROL_CHECK_BOX) { 238 mUseDefaultControlCheckBox.setVisibility(View.VISIBLE); 239 } 240 mPauseResumeButton = findViewById(R.id.pause_resume_button); 241 mPauseResumeButton.setOnClickListener(v -> { 242 if (mSessionManager.isPaused()) { 243 mSessionManager.resume(); 244 } else { 245 mSessionManager.pause(); 246 } 247 }); 248 249 ImageButton stopButton = findViewById(R.id.stop_button); 250 stopButton.setOnClickListener(v -> mSessionManager.stop()); 251 252 mSeekBar = findViewById(R.id.seekbar); 253 mSeekBar.setOnSeekBarChangeListener(new SampleOnSeekBarChangeListener()); 254 255 // Schedule Ui update 256 mHandler.postDelayed(mUpdateSeekRunnable, 1000); 257 258 // Build the PendingIntent for the remote control client 259 mEventReceiver = new ComponentName(getPackageName(), 260 SampleMediaButtonReceiver.class.getName()); 261 Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); 262 mediaButtonIntent.setComponent(mEventReceiver); 263 mMediaPendingIntent = 264 PendingIntent.getBroadcast( 265 this, 266 /* requestCode= */ 0, 267 mediaButtonIntent, 268 PendingIntent.FLAG_IMMUTABLE); 269 270 // Create and register the remote control client 271 createMediaSession(); 272 mMediaRouter.setMediaSessionCompat(mMediaSession); 273 274 // Set up playback manager and player 275 mPlayer = 276 Player.createPlayerForActivity( 277 MainActivity.this, mMediaRouter.getSelectedRoute(), mMediaSession); 278 279 mSessionManager.setPlayer(mPlayer); 280 mSessionManager.setCallback(new SampleSessionManagerCallback()); 281 282 updateUi(); 283 284 if (RouteListingPreference.ACTION_TRANSFER_MEDIA.equals(getIntent().getAction())) { 285 showMediaTransferToast(); 286 } 287 } 288 289 @Override onDestroy()290 protected void onDestroy() { 291 mSessionManager.stop(); 292 mPlayer.release(); 293 mMediaSession.release(); 294 mMediaRouter.removeCallback(mMediaRouterCB); 295 mMediaRouter.setOnPrepareTransferListener(null); 296 297 super.onDestroy(); 298 } 299 300 @Override onCreateOptionsMenu(@onNull Menu menu)301 public boolean onCreateOptionsMenu(@NonNull Menu menu) { 302 // Be sure to call the super class. 303 super.onCreateOptionsMenu(menu); 304 305 // Inflate the menu and configure the media router action provider. 306 getMenuInflater().inflate(R.menu.sample_media_router_menu, menu); 307 308 MenuItem mediaRouteMenuItem = menu.findItem(R.id.media_route_menu_item); 309 MediaRouteActionProvider mediaRouteActionProvider = 310 (MediaRouteActionProvider) MenuItemCompat.getActionProvider(mediaRouteMenuItem); 311 if (mediaRouteActionProvider != null) { 312 mediaRouteActionProvider.setRouteSelector(mSelector); 313 mediaRouteActionProvider.setDialogFactory(new MediaRouteDialogFactory() { 314 @Override 315 public @NonNull MediaRouteControllerDialogFragment 316 onCreateControllerDialogFragment() { 317 return new ControllerDialogFragment(MainActivity.this, 318 mUseDefaultControlCheckBox); 319 } 320 }); 321 } 322 323 // Return true to show the menu. 324 return true; 325 } 326 327 @Override onOptionsItemSelected(@onNull MenuItem item)328 public boolean onOptionsItemSelected(@NonNull MenuItem item) { 329 if (item.getItemId() == R.id.settings_menu_item) { 330 startActivity(new Intent(this, SettingsActivity.class)); 331 return true; 332 } 333 return super.onOptionsItemSelected(item); 334 } 335 336 @Override onKeyDown(int keyCode, @Nullable KeyEvent event)337 public boolean onKeyDown(int keyCode, @Nullable KeyEvent event) { 338 return handleMediaKey(event) || super.onKeyDown(keyCode, event); 339 } 340 341 @Override onKeyUp(int keyCode, @Nullable KeyEvent event)342 public boolean onKeyUp(int keyCode, @Nullable KeyEvent event) { 343 return handleMediaKey(event) || super.onKeyUp(keyCode, event); 344 } 345 requestRequiredPermissions()346 private void requestRequiredPermissions() { 347 requestDisplayOverOtherAppsPermission(); 348 requestPostNotificationsPermission(); 349 } 350 showMediaTransferToast()351 private void showMediaTransferToast() { 352 String routeId = getIntent().getStringExtra(RouteListingPreference.EXTRA_ROUTE_ID); 353 List<RouteInfo> routes = mMediaRouter.getRoutes(); 354 String requestedRouteName = null; 355 for (RouteInfo route : routes) { 356 if (route.getId().equals(routeId)) { 357 requestedRouteName = route.getName(); 358 break; 359 } 360 } 361 String stringToDisplay = 362 requestedRouteName != null 363 ? "Transfer requested to " + requestedRouteName 364 : "Transfer requested to unknown route: " + routeId; 365 366 // TODO(b/266561322): Replace the toast with a Dialog that allows the user to either 367 // transfer playback to the requested route, or dismiss the intent. 368 Toast.makeText(/* context= */ this, stringToDisplay, Toast.LENGTH_LONG).show(); 369 } 370 requestDisplayOverOtherAppsPermission()371 private void requestDisplayOverOtherAppsPermission() { 372 // Need overlay permission for emulating remote display. 373 if (Build.VERSION.SDK_INT >= 23 && !Api23Impl.canDrawOverlays(this)) { 374 Intent intent = 375 new Intent( 376 Settings.ACTION_MANAGE_OVERLAY_PERMISSION, 377 Uri.parse("package:" + getPackageName())); 378 startActivityForResult(intent, 0); 379 } 380 } 381 requestPostNotificationsPermission()382 private void requestPostNotificationsPermission() { 383 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 384 if (ContextCompat.checkSelfPermission( 385 getApplicationContext(), Manifest.permission.POST_NOTIFICATIONS) 386 != PackageManager.PERMISSION_GRANTED) { 387 if (!Api23Impl.shouldShowRequestPermissionRationale( 388 this, Manifest.permission.POST_NOTIFICATIONS)) { 389 Api23Impl.requestPermissions( 390 /* activity= */ this, 391 /* permissions= */ new String[] { 392 Manifest.permission.POST_NOTIFICATIONS 393 }, 394 /* requestCode= */ POST_NOTIFICATIONS_PERMISSION_REQUEST_CODE); 395 } 396 } 397 } 398 } 399 createMediaSession()400 private void createMediaSession() { 401 // Create the MediaSession 402 mMediaSession = new MediaSessionCompat(this, "SampleMediaRouter", mEventReceiver, 403 mMediaPendingIntent); 404 mMediaSession.setCallback(new SampleMediaSessionCompatCallback()); 405 SampleMediaButtonReceiver.setActivity(MainActivity.this); 406 } 407 408 /** 409 * Handle media key events. 410 */ handleMediaKey(@ullable KeyEvent event)411 private boolean handleMediaKey(@Nullable KeyEvent event) { 412 if (event != null && event.getAction() == KeyEvent.ACTION_DOWN 413 && event.getRepeatCount() == 0) { 414 switch (event.getKeyCode()) { 415 case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: 416 case KeyEvent.KEYCODE_HEADSETHOOK: { 417 Log.d(TAG, "Received Play/Pause event from RemoteControlClient"); 418 if (mSessionManager.isPaused()) { 419 mSessionManager.resume(); 420 } else { 421 mSessionManager.pause(); 422 } 423 return true; 424 } 425 case KeyEvent.KEYCODE_MEDIA_PLAY: { 426 Log.d(TAG, "Received Play event from RemoteControlClient"); 427 if (mSessionManager.isPaused()) { 428 mSessionManager.resume(); 429 } 430 return true; 431 } 432 case KeyEvent.KEYCODE_MEDIA_PAUSE: { 433 Log.d(TAG, "Received Pause event from RemoteControlClient"); 434 if (!mSessionManager.isPaused()) { 435 mSessionManager.pause(); 436 } 437 return true; 438 } 439 case KeyEvent.KEYCODE_MEDIA_STOP: { 440 Log.d(TAG, "Received Stop event from RemoteControlClient"); 441 mSessionManager.stop(); 442 return true; 443 } 444 default: 445 break; 446 } 447 } 448 return false; 449 } 450 updateStatusFromSessionManager()451 private void updateStatusFromSessionManager() { 452 if (mPlayer != null && mSessionManager != null) { 453 mSessionManager.updateStatus(); 454 } 455 } 456 updateProgress()457 private void updateProgress() { 458 // Estimate content position from last status time and elapsed time. 459 // (Note this might be slightly out of sync with remote side, however 460 // it avoids frequent polling the MRP.) 461 int progress = 0; 462 PlaylistItem item = getCheckedPlaylistItem(); 463 if (item != null) { 464 int state = item.getState(); 465 long duration = item.getDuration(); 466 if (duration <= 0) { 467 if (state == MediaItemStatus.PLAYBACK_STATE_PLAYING 468 || state == MediaItemStatus.PLAYBACK_STATE_PAUSED) { 469 mSessionManager.updateStatus(); 470 } 471 } else { 472 long position = item.getPosition(); 473 long timeDelta = mSessionManager.isPaused() 474 ? 0 : (SystemClock.elapsedRealtime() - item.getTimestamp()); 475 progress = (int) (100.0 * (position + timeDelta) / duration); 476 } 477 } 478 mSeekBar.setProgress(progress); 479 } 480 updateUi()481 private void updateUi() { 482 updatePlaylist(); 483 updateRouteDescription(); 484 updateButtons(); 485 if (mPlayer != null && mSessionManager != null) { 486 PlaylistItem currentItem = mSessionManager.getCurrentItem(); 487 if (currentItem != null) { 488 mPlayer.updateMetadata(currentItem); 489 int currentItemState = Player.STATE_IDLE; 490 switch (currentItem.getState()) { 491 case MediaItemStatus.PLAYBACK_STATE_PLAYING: 492 currentItemState = Player.STATE_PLAYING; 493 break; 494 case MediaItemStatus.PLAYBACK_STATE_PAUSED: 495 currentItemState = Player.STATE_PAUSED; 496 break; 497 case MediaItemStatus.PLAYBACK_STATE_PENDING: 498 case MediaItemStatus.PLAYBACK_STATE_BUFFERING: 499 currentItemState = Player.STATE_PREPARING_FOR_PLAY; 500 break; 501 } 502 mPlayer.publishState(currentItemState); 503 } 504 } 505 } 506 updatePlaylist()507 private void updatePlaylist() { 508 mPlayListItems.clear(); 509 for (PlaylistItem item : mSessionManager.getPlaylist()) { 510 mPlayListItems.add(item); 511 } 512 mPlayListView.invalidate(); 513 } 514 updateRouteDescription()515 private void updateRouteDescription() { 516 RouteInfo route = mMediaRouter.getSelectedRoute(); 517 mInfoTextView.setText("Currently selected route:" 518 + "\nName: " + route.getName() 519 + "\nProvider: " + route.getProvider().getPackageName()); 520 } 521 updateButtons()522 private void updateButtons() { 523 // show pause or resume icon depending on current state 524 mPauseResumeButton.setImageResource(mSessionManager.isPaused() 525 ? R.drawable.ic_media_play : R.drawable.ic_media_pause); 526 // only enable seek bar when duration is known 527 PlaylistItem item = getCheckedPlaylistItem(); 528 mSeekBar.setEnabled(item != null && item.getDuration() > 0); 529 } 530 getCheckedPlaylistItem()531 private @Nullable PlaylistItem getCheckedPlaylistItem() { 532 int count = mPlayListView.getCount(); 533 int index = mPlayListView.getCheckedItemPosition(); 534 if (count > 0) { 535 if (index < 0 || index >= count) { 536 index = 0; 537 mPlayListView.setItemChecked(0, true); 538 } 539 return mPlayListItems.getItem(index); 540 } 541 return null; 542 } 543 getCurrentEstimatedPosition(@onNull PlaylistItem item)544 private long getCurrentEstimatedPosition(@NonNull PlaylistItem item) { 545 return item.getPosition() + (mSessionManager.isPaused() 546 ? 0 : (SystemClock.elapsedRealtime() - item.getTimestamp())); 547 } 548 getRouterParams()549 private @NonNull MediaRouterParams getRouterParams() { 550 MediaRouterParams.Builder routerParams = 551 new MediaRouterParams.Builder() 552 .setDialogType(MediaRouterParams.DIALOG_TYPE_DEFAULT) 553 .setTransferToLocalEnabled( 554 true); // Phone speaker will be shown when casting. 555 boolean wrapperRouteProviderEnabled = 556 SettingsActivity.isServiceEnabled( 557 getApplicationContext(), WrapperMediaRouteProviderService.class); 558 routerParams.setMediaTransferRestrictedToSelfProviders(wrapperRouteProviderEnabled); 559 return routerParams.build(); 560 } 561 562 /** 563 * Returns a new {@link TransferListener} it the SDK level is at least R. Otherwise, returns 564 * null. 565 */ createTransferListener()566 private MediaRouter.OnPrepareTransferListener createTransferListener() { 567 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 568 return new TransferListener(); 569 } else { 570 return null; 571 } 572 } 573 574 /** Media route discovery fragment. */ 575 public static final class DiscoveryFragment extends MediaRouteDiscoveryFragment { 576 private MediaRouter.Callback mCallback; 577 setCallback(MediaRouter.@ullable Callback cb)578 public void setCallback(MediaRouter.@Nullable Callback cb) { 579 mCallback = cb; 580 } 581 582 @Override onCreateCallback()583 public MediaRouter.@Nullable Callback onCreateCallback() { 584 return mCallback; 585 } 586 587 @Override onPrepareCallbackFlags()588 public int onPrepareCallbackFlags() { 589 return super.onPrepareCallbackFlags(); 590 } 591 } 592 593 private final class SampleOnSeekBarChangeListener implements OnSeekBarChangeListener { 594 @Override onProgressChanged(SeekBar seekBar, int progress, boolean fromUser)595 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 596 PlaylistItem item = getCheckedPlaylistItem(); 597 if (fromUser && item != null && item.getDuration() > 0) { 598 long pos = progress * item.getDuration() / 100; 599 mSessionManager.seek(item.getItemId(), pos); 600 item.setPosition(pos); 601 item.setTimestamp(SystemClock.elapsedRealtime()); 602 } 603 } 604 605 @Override onStartTrackingTouch(SeekBar seekBar)606 public void onStartTrackingTouch(SeekBar seekBar) { 607 } 608 609 @Override onStopTrackingTouch(SeekBar seekBar)610 public void onStopTrackingTouch(SeekBar seekBar) { 611 updateUi(); 612 } 613 } 614 615 private final class SampleSessionManagerCallback implements SessionManager.Callback { 616 @Override onStatusChanged()617 public void onStatusChanged() { 618 updateUi(); 619 } 620 621 @Override onItemChanged(@onNull PlaylistItem item)622 public void onItemChanged(@NonNull PlaylistItem item) { 623 updateUi(); 624 } 625 } 626 627 private final class SampleMediaSessionCompatCallback extends MediaSessionCompat.Callback { 628 @Override onMediaButtonEvent(Intent mediaButtonEvent)629 public boolean onMediaButtonEvent(Intent mediaButtonEvent) { 630 if (mediaButtonEvent != null) { 631 return handleMediaKey( 632 mediaButtonEvent.getParcelableExtra(Intent.EXTRA_KEY_EVENT)); 633 } 634 return super.onMediaButtonEvent(null); 635 } 636 637 @Override onPlay()638 public void onPlay() { 639 mSessionManager.resume(); 640 } 641 642 @Override onPause()643 public void onPause() { 644 mSessionManager.pause(); 645 } 646 }; 647 648 private final class SampleMediaRouterCallback extends MediaRouter.Callback { 649 // Return a custom callback that will simply log all of the route events 650 // for demonstration purposes. 651 @Override onRouteAdded(@onNull MediaRouter router, @NonNull RouteInfo route)652 public void onRouteAdded(@NonNull MediaRouter router, @NonNull RouteInfo route) { 653 Log.d(TAG, "onRouteAdded: route=" + route); 654 } 655 656 @Override onRouteChanged(@onNull MediaRouter router, @NonNull RouteInfo route)657 public void onRouteChanged(@NonNull MediaRouter router, @NonNull RouteInfo route) { 658 Log.d(TAG, "onRouteChanged: route=" + route); 659 if (route.isSelected()) { 660 updateRouteDescription(); 661 } 662 } 663 664 @Override onRouteRemoved(@onNull MediaRouter router, @NonNull RouteInfo route)665 public void onRouteRemoved(@NonNull MediaRouter router, @NonNull RouteInfo route) { 666 Log.d(TAG, "onRouteRemoved: route=" + route); 667 } 668 669 @Override onRouteSelected(@onNull MediaRouter router, @NonNull RouteInfo selectedRoute, int reason, @NonNull RouteInfo requestedRoute)670 public void onRouteSelected(@NonNull MediaRouter router, 671 @NonNull RouteInfo selectedRoute, int reason, @NonNull RouteInfo requestedRoute) { 672 Log.d(TAG, "onRouteSelected: requestedRoute=" + requestedRoute 673 + ", route=" + selectedRoute + ", reason=" + reason); 674 675 boolean needToRecreatePlayer = 676 !selectedRoute.isSystemRoute() || mPlayer.isRemotePlayback(); 677 678 if (needToRecreatePlayer) { 679 Player oldPlayer = mPlayer; 680 PlaylistItem currentItem = mSessionManager.getCurrentItem(); 681 if (currentItem != null 682 && currentItem.getState() != MediaItemStatus.PLAYBACK_STATE_PENDING) { 683 // We haven't received a prepare transfer call for this. We set that up now. 684 if (reason == MediaRouter.UNSELECT_REASON_STOPPED) { 685 mSessionManager.pause(); 686 } 687 mSessionManager.suspend(currentItem.getPosition()); 688 } 689 mPlayer = 690 Player.createPlayerForActivity( 691 MainActivity.this, selectedRoute, mMediaSession); 692 mPlayer.updatePresentation(); 693 mSessionManager.setPlayer(mPlayer); 694 mSessionManager.unsuspend(); 695 updateUi(); 696 oldPlayer.release(); 697 } 698 } 699 700 @Override onRouteUnselected( @onNull MediaRouter router, @NonNull RouteInfo route, int reason)701 public void onRouteUnselected( 702 @NonNull MediaRouter router, @NonNull RouteInfo route, int reason) { 703 Log.d(TAG, "onRouteUnselected: route=" + route); 704 } 705 706 @Override onRouteConnected( @onNull MediaRouter router, @NonNull RouteInfo connectedRoute, @NonNull RouteInfo requestedRoute)707 public void onRouteConnected( 708 @NonNull MediaRouter router, 709 @NonNull RouteInfo connectedRoute, 710 @NonNull RouteInfo requestedRoute) { 711 Log.d( 712 TAG, 713 "onRouteConnected: connectedRoute=" 714 + connectedRoute 715 + ", requestedRoute=" 716 + requestedRoute); 717 } 718 719 @Override onRouteDisconnected( @onNull MediaRouter router, @Nullable RouteInfo disconnectedRoute, @NonNull RouteInfo requestedRoute, int reason)720 public void onRouteDisconnected( 721 @NonNull MediaRouter router, 722 @Nullable RouteInfo disconnectedRoute, 723 @NonNull RouteInfo requestedRoute, 724 int reason) { 725 Log.d( 726 TAG, 727 "onRouteDisconnected: disconnectedRoute=" 728 + disconnectedRoute 729 + ", requestedRoute = " 730 + requestedRoute 731 + " and reason=" 732 + reason); 733 } 734 735 @Override onRouteVolumeChanged(@onNull MediaRouter router, @NonNull RouteInfo route)736 public void onRouteVolumeChanged(@NonNull MediaRouter router, @NonNull RouteInfo route) { 737 Log.d(TAG, "onRouteVolumeChanged: route=" + route); 738 } 739 740 @Override onRoutePresentationDisplayChanged( @onNull MediaRouter router, @NonNull RouteInfo route)741 public void onRoutePresentationDisplayChanged( 742 @NonNull MediaRouter router, @NonNull RouteInfo route) { 743 Log.d(TAG, "onRoutePresentationDisplayChanged: route=" + route); 744 mPlayer.updatePresentation(); 745 } 746 747 @Override onProviderAdded(@onNull MediaRouter router, @NonNull ProviderInfo provider)748 public void onProviderAdded(@NonNull MediaRouter router, @NonNull ProviderInfo provider) { 749 Log.d(TAG, "onRouteProviderAdded: provider=" + provider); 750 } 751 752 @Override onProviderRemoved(@onNull MediaRouter router, @NonNull ProviderInfo provider)753 public void onProviderRemoved(@NonNull MediaRouter router, @NonNull ProviderInfo provider) { 754 Log.d(TAG, "onRouteProviderRemoved: provider=" + provider); 755 } 756 757 @Override onProviderChanged(@onNull MediaRouter router, @NonNull ProviderInfo provider)758 public void onProviderChanged(@NonNull MediaRouter router, @NonNull ProviderInfo provider) { 759 Log.d(TAG, "onRouteProviderChanged: provider=" + provider); 760 } 761 } 762 763 /** 764 * Controller Dialog Fragment for the media router dialog. 765 */ 766 public static class ControllerDialogFragment extends MediaRouteControllerDialogFragment { 767 private MainActivity mMainActivity; 768 private MediaRouteControllerDialog mControllerDialog; 769 private CheckBox mUseDefaultControlCheckBox; 770 ControllerDialogFragment()771 public ControllerDialogFragment() { 772 super(); 773 } 774 ControllerDialogFragment(@onNull MainActivity activity, @Nullable CheckBox customControlViewCheckBox)775 public ControllerDialogFragment(@NonNull MainActivity activity, 776 @Nullable CheckBox customControlViewCheckBox) { 777 mMainActivity = activity; 778 mUseDefaultControlCheckBox = customControlViewCheckBox; 779 } 780 781 @Override onCreateControllerDialog( @onNull Context context, @Nullable Bundle savedInstanceState)782 public @NonNull MediaRouteControllerDialog onCreateControllerDialog( 783 @NonNull Context context, @Nullable Bundle savedInstanceState) { 784 mMainActivity.updateStatusFromSessionManager(); 785 mControllerDialog = 786 mUseDefaultControlCheckBox != null && mUseDefaultControlCheckBox.isChecked() 787 ? super.onCreateControllerDialog(context, savedInstanceState) 788 : new MyMediaRouteControllerDialog(context); 789 mControllerDialog.setOnDismissListener(dialog -> mControllerDialog = null); 790 return mControllerDialog; 791 } 792 } 793 794 /** 795 * Broadcast receiver for handling ACTION_MEDIA_BUTTON. 796 * 797 * This is needed to create the RemoteControlClient for controlling 798 * remote route volume in lock screen. It routes media key events back 799 * to main app activity. 800 */ 801 private static class SampleMediaButtonReceiver extends BroadcastReceiver { 802 private static MainActivity sActivity; 803 setActivity(@onNull MainActivity activity)804 public static void setActivity(@NonNull MainActivity activity) { 805 sActivity = activity; 806 } 807 808 @Override onReceive(Context context, Intent intent)809 public void onReceive(Context context, Intent intent) { 810 if (sActivity != null && Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) { 811 sActivity.handleMediaKey(intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT)); 812 } 813 } 814 } 815 816 @RequiresApi(23) 817 private static class Api23Impl { Api23Impl()818 private Api23Impl() { 819 // This class is not instantiable. 820 } 821 canDrawOverlays(Context context)822 static boolean canDrawOverlays(Context context) { 823 return Settings.canDrawOverlays(context); 824 } 825 shouldShowRequestPermissionRationale(Activity activity, String permission)826 static boolean shouldShowRequestPermissionRationale(Activity activity, String permission) { 827 return activity.shouldShowRequestPermissionRationale(permission); 828 } 829 requestPermissions(Activity activity, String[] permissions, int requestCode)830 static void requestPermissions(Activity activity, String[] permissions, int requestCode) { 831 activity.requestPermissions(permissions, requestCode); 832 } 833 } 834 835 @RequiresApi(30) 836 private class TransferListener implements MediaRouter.OnPrepareTransferListener { 837 @Override onPrepareTransfer(@onNull RouteInfo fromRoute, @NonNull RouteInfo toRoute)838 public @Nullable ListenableFuture<Void> onPrepareTransfer(@NonNull RouteInfo fromRoute, 839 @NonNull RouteInfo toRoute) { 840 Log.d(TAG, "onPrepareTransfer: from=" + fromRoute.getId() 841 + ", to=" + toRoute.getId()); 842 final PlaylistItem currentItem = getCheckedPlaylistItem(); 843 844 if (currentItem == null) { 845 return null; // No ongoing playback. Nothing to prepare. 846 } 847 if (mPlayer.isRemotePlayback()) { 848 RemotePlayer remotePlayer = (RemotePlayer) mPlayer; 849 ListenableFuture<PlaylistItem> cacheRemoteState = 850 remotePlayer.cacheRemoteState(currentItem); 851 Function<PlaylistItem, Void> function = 852 (playlistItem) -> { 853 mSessionManager.suspend(playlistItem.getPosition()); 854 return null; 855 }; 856 return Futures.transform(cacheRemoteState, function, Runnable::run); 857 } else { 858 mSessionManager.suspend(getCurrentEstimatedPosition(currentItem)); 859 return Futures.immediateVoidFuture(); 860 } 861 } 862 } 863 } 864