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