Home | History | Annotate | Download | only in media
      1 /*
      2  * Copyright (C) 2016 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 package com.android.car.media;
     17 
     18 import android.annotation.NonNull;
     19 import android.car.Car;
     20 import android.content.ComponentName;
     21 import android.content.Context;
     22 import android.content.Intent;
     23 import android.content.SharedPreferences;
     24 import android.graphics.Bitmap;
     25 import android.media.session.MediaController;
     26 import android.os.Bundle;
     27 import android.support.v4.app.Fragment;
     28 import android.support.v4.app.FragmentManager;
     29 import android.support.v4.widget.DrawerLayout;
     30 import android.transition.Fade;
     31 import android.util.Log;
     32 import android.util.TypedValue;
     33 import android.view.View;
     34 import android.view.ViewGroup;
     35 
     36 import com.android.car.media.common.ActiveMediaSourceManager;
     37 import com.android.car.media.common.CrossfadeImageView;
     38 import com.android.car.media.common.MediaItemMetadata;
     39 import com.android.car.media.common.MediaSource;
     40 import com.android.car.media.common.MediaSourcesManager;
     41 import com.android.car.media.common.PlaybackControls;
     42 import com.android.car.media.common.PlaybackModel;
     43 import com.android.car.media.drawer.MediaDrawerController;
     44 import com.android.car.media.widgets.AppBarView;
     45 import com.android.car.media.widgets.MetadataView;
     46 import com.android.car.media.widgets.ViewUtils;
     47 
     48 import java.util.ArrayList;
     49 import java.util.List;
     50 import java.util.Objects;
     51 import java.util.stream.Collectors;
     52 
     53 import androidx.car.drawer.CarDrawerActivity;
     54 import androidx.car.drawer.CarDrawerAdapter;
     55 
     56 /**
     57  * This activity controls the UI of media. It also updates the connection status for the media app
     58  * by broadcast. Drawer menu is controlled by {@link MediaDrawerController}.
     59  */
     60 public class MediaActivity extends CarDrawerActivity implements BrowseFragment.Callbacks,
     61         AppSelectionFragment.Callbacks, PlaybackFragment.Callbacks {
     62     private static final String TAG = "MediaActivity";
     63 
     64     /** Intent extra specifying the package with the MediaBrowser */
     65     public static final String KEY_MEDIA_PACKAGE = "media_package";
     66     /** Shared preferences files */
     67     public static final String SHARED_PREF = "com.android.car.media";
     68     /** Shared preference containing the last controlled source */
     69     public static final String LAST_MEDIA_SOURCE_SHARED_PREF_KEY = "last_media_source";
     70 
     71     /** Configuration (controlled from resources) */
     72     private boolean mContentForwardBrowseEnabled;
     73     private float mBackgroundBlurRadius;
     74     private float mBackgroundBlurScale;
     75     private int mFadeDuration;
     76 
     77     /** Models */
     78     private MediaDrawerController mDrawerController;
     79     private ActiveMediaSourceManager mActiveMediaSourceManager;
     80     private MediaSource mMediaSource;
     81     private PlaybackModel mPlaybackModel;
     82     private MediaSourcesManager mMediaSourcesManager;
     83     private SharedPreferences mSharedPreferences;
     84 
     85     /** Layout views */
     86     private AppBarView mAppBarView;
     87     private CrossfadeImageView mAlbumBackground;
     88     private PlaybackFragment mPlaybackFragment;
     89     private AppSelectionFragment mAppSelectionFragment;
     90     private PlaybackControls mPlaybackControls;
     91     private MetadataView mMetadataView;
     92     private ViewGroup mBrowseControlsContainer;
     93     private EmptyFragment mEmptyFragment;
     94     private ViewGroup mBrowseContainer;
     95     private ViewGroup mPlaybackContainer;
     96 
     97     /** Current state */
     98     private Fragment mCurrentFragment;
     99     private Mode mMode = Mode.BROWSING;
    100     private boolean mIsAppSelectorOpen;
    101     private MediaItemMetadata mCurrentMetadata;
    102 
    103     private MediaSource.Observer mMediaSourceObserver = new MediaSource.Observer() {
    104         @Override
    105         protected void onBrowseConnected(boolean success) {
    106             MediaActivity.this.onBrowseConnected(success);
    107         }
    108 
    109         @Override
    110         protected void onBrowseDisconnected() {
    111             MediaActivity.this.onBrowseConnected(false);
    112         }
    113     };
    114     private PlaybackModel.PlaybackObserver mPlaybackObserver =
    115             new PlaybackModel.PlaybackObserver() {
    116         @Override
    117         protected void onSourceChanged() {
    118             updateMetadata();
    119         }
    120 
    121         @Override
    122         public void onMetadataChanged() {
    123             mCurrentMetadata = null;
    124             updateMetadata();
    125         }
    126     };
    127     private ActiveMediaSourceManager.Observer mActiveSourceObserver = () -> {
    128         // If the active media source changes and it is the one currently being browsed, then
    129         // we should capture the controller.
    130         MediaController controller = mActiveMediaSourceManager.getMediaController();
    131         if (mPlaybackModel.getMediaController() == null
    132                 && mMediaSource != null
    133                 && controller != null
    134                 && Objects.equals(controller.getPackageName(), mMediaSource.getPackageName())) {
    135             mPlaybackModel.setMediaController(controller);
    136         }
    137     };
    138     private MediaSource.ItemsSubscription mItemsSubscription =
    139             new MediaSource.ItemsSubscription() {
    140         @Override
    141         public void onChildrenLoaded(MediaSource mediaSource, String parentId,
    142                 List<MediaItemMetadata> items) {
    143             if (mediaSource == mMediaSource) {
    144                 updateTabs(items);
    145             } else {
    146                 Log.w(TAG, "Received items for a wrong source: " +
    147                         mediaSource.getPackageName());
    148             }
    149         }
    150     };
    151     private AppBarView.AppBarListener mAppBarListener = new AppBarView.AppBarListener() {
    152         @Override
    153         public void onTabSelected(MediaItemMetadata item) {
    154             updateBrowseFragment(BrowseState.LOADED, item);
    155             switchToMode(Mode.BROWSING);
    156         }
    157 
    158         @Override
    159         public void onBack() {
    160             if (mCurrentFragment != null && mCurrentFragment instanceof BrowseFragment) {
    161                 BrowseFragment fragment = (BrowseFragment) mCurrentFragment;
    162                 fragment.navigateBack();
    163             }
    164         }
    165 
    166         @Override
    167         public void onCollapse() {
    168             switchToMode(Mode.BROWSING);
    169         }
    170 
    171         @Override
    172         public void onAppSelection() {
    173             Log.d(TAG, "onAppSelection clicked");
    174             if (mIsAppSelectorOpen) {
    175                 closeAppSelector();
    176             } else {
    177                 openAppSelector();
    178             }
    179         }
    180     };
    181     private MediaSourcesManager.Observer mMediaSourcesManagerObserver = () -> {
    182         mAppBarView.setAppSelection(!mMediaSourcesManager.getMediaSources().isEmpty());
    183         mAppSelectionFragment.refresh();
    184     };
    185     private DrawerLayout.DrawerListener mDrawerListener = new DrawerLayout.DrawerListener() {
    186         @Override
    187         public void onDrawerSlide(@android.support.annotation.NonNull View view, float v) {
    188         }
    189 
    190         @Override
    191         public void onDrawerOpened(@android.support.annotation.NonNull View view) {
    192             closeAppSelector();
    193         }
    194 
    195         @Override
    196         public void onDrawerClosed(@android.support.annotation.NonNull View view) {
    197         }
    198 
    199         @Override
    200         public void onDrawerStateChanged(int i) {
    201         }
    202     };
    203 
    204     /**
    205      * Possible modes of the application UI
    206      */
    207     private enum Mode {
    208         /** The user is browsing a media source */
    209         BROWSING,
    210         /** The user is interacting with the full screen playback UI */
    211         PLAYBACK
    212     }
    213 
    214     /**
    215      * Possible states of the application UI
    216      */
    217     public enum BrowseState {
    218         /** There is no content to show */
    219         EMPTY,
    220         /** We are still in the process of obtaining data */
    221         LOADING,
    222         /** Data has been loaded */
    223         LOADED,
    224         /** The content can't be shown due an error */
    225         ERROR
    226     }
    227 
    228     @Override
    229     protected void onCreate(Bundle savedInstanceState) {
    230         super.onCreate(savedInstanceState);
    231 
    232         setMainContent(R.layout.media_activity);
    233         setToolbarElevation(0f);
    234 
    235         mContentForwardBrowseEnabled = getResources()
    236                 .getBoolean(R.bool.forward_content_browse_enabled);
    237         mDrawerController = new MediaDrawerController(this, getDrawerController());
    238         getDrawerController().setRootAdapter(getRootAdapter());
    239         getDrawerController().addDrawerListener(mDrawerListener);
    240         if (mContentForwardBrowseEnabled) {
    241             getSupportActionBar().hide();
    242         }
    243         mAppBarView = findViewById(R.id.app_bar);
    244         mAppBarView.setListener(mAppBarListener);
    245         mAppBarView.setContentForwardEnabled(mContentForwardBrowseEnabled);
    246         mPlaybackFragment = new PlaybackFragment();
    247         mAppSelectionFragment = new AppSelectionFragment();
    248         int fadeDuration = getResources().getInteger(R.integer.app_selector_fade_duration);
    249         mAppSelectionFragment.setEnterTransition(new Fade().setDuration(fadeDuration));
    250         mAppSelectionFragment.setExitTransition(new Fade().setDuration(fadeDuration));
    251         mActiveMediaSourceManager = new ActiveMediaSourceManager(this);
    252         mPlaybackModel = new PlaybackModel(this);
    253         mMediaSourcesManager = new MediaSourcesManager(this);
    254         mAlbumBackground = findViewById(R.id.media_background);
    255         mPlaybackControls = findViewById(R.id.browse_controls);
    256         mPlaybackControls.setModel(mPlaybackModel);
    257         mMetadataView = findViewById(R.id.browse_metadata);
    258         mMetadataView.setModel(mPlaybackModel);
    259         mBrowseControlsContainer = findViewById(R.id.browse_controls_container);
    260         mBrowseControlsContainer.setOnClickListener(view -> switchToMode(Mode.PLAYBACK));
    261         TypedValue outValue = new TypedValue();
    262         getResources().getValue(R.dimen.playback_background_blur_radius, outValue, true);
    263         mBackgroundBlurRadius = outValue.getFloat();
    264         getResources().getValue(R.dimen.playback_background_blur_scale, outValue, true);
    265         mBackgroundBlurScale = outValue.getFloat();
    266         mSharedPreferences = getSharedPreferences(SHARED_PREF, Context.MODE_PRIVATE);
    267         mFadeDuration = getResources().getInteger(
    268                 R.integer.new_album_art_fade_in_duration);
    269         mEmptyFragment = new EmptyFragment();
    270         mBrowseContainer = findViewById(R.id.fragment_container);
    271         mPlaybackContainer = findViewById(R.id.playback_container);
    272         getSupportFragmentManager().beginTransaction()
    273                 .replace(R.id.playback_container, mPlaybackFragment)
    274                 .commit();
    275     }
    276 
    277     @Override
    278     public void onResume() {
    279         super.onResume();
    280         mPlaybackModel.registerObserver(mPlaybackObserver);
    281         mActiveMediaSourceManager.registerObserver(mActiveSourceObserver);
    282         mMediaSourcesManager.registerObserver(mMediaSourcesManagerObserver);
    283         handleIntent();
    284     }
    285 
    286     @Override
    287     public void onPause() {
    288         super.onPause();
    289         mPlaybackModel.unregisterObserver(mPlaybackObserver);
    290         mActiveMediaSourceManager.unregisterObserver(mActiveSourceObserver);
    291         mMediaSourcesManager.unregisterObserver(mMediaSourcesManagerObserver);
    292     }
    293 
    294     @Override
    295     public void onDestroy() {
    296         super.onDestroy();
    297         mDrawerController.cleanup();
    298         mPlaybackControls.setModel(null);
    299         mMetadataView.setModel(null);
    300     }
    301 
    302     @Override
    303     protected CarDrawerAdapter getRootAdapter() {
    304         return mDrawerController == null ? null : mDrawerController.getRootAdapter();
    305     }
    306 
    307     @Override
    308     protected void onNewIntent(Intent intent) {
    309         super.onNewIntent(intent);
    310         if (Log.isLoggable(TAG, Log.VERBOSE)) {
    311             Log.v(TAG, "onNewIntent(); intent: " + (intent == null ? "<< NULL >>" : intent));
    312         }
    313 
    314         setIntent(intent);
    315         handleIntent();
    316     }
    317 
    318     @Override
    319     public void onBackPressed() {
    320         mPlaybackFragment.closeOverflowMenu();
    321         super.onBackPressed();
    322     }
    323 
    324     private void onBrowseConnected(boolean success) {
    325         if (!success) {
    326             updateTabs(null);
    327             mMediaSource.unsubscribeChildren(null, mItemsSubscription);
    328             mMediaSource.unsubscribe(mMediaSourceObserver);
    329             updateBrowseFragment(BrowseState.ERROR, null);
    330             return;
    331         }
    332         mMediaSource.subscribeChildren(null, mItemsSubscription);
    333         if (mPlaybackModel.getMediaController() == null) {
    334             mPlaybackModel.setMediaController(mMediaSource.getMediaController());
    335         }
    336     }
    337 
    338     private void handleIntent() {
    339         Intent intent = getIntent();
    340         String action = intent != null ? intent.getAction() : null;
    341 
    342         getDrawerController().closeDrawer();
    343 
    344         if (Car.CAR_INTENT_ACTION_MEDIA_TEMPLATE.equals(action)) {
    345             // The user either wants to browse a particular media source or switch to the
    346             // playback UI.
    347             String packageName = intent.getStringExtra(KEY_MEDIA_PACKAGE);
    348             if (packageName != null) {
    349                 // We were told to navigate to a particular package: we open browse for it.
    350                 closeAppSelector();
    351                 changeMediaSource(new MediaSource(this, packageName), null);
    352                 switchToMode(Mode.BROWSING);
    353                 return;
    354             }
    355 
    356             // If we didn't receive a package name and we are playing something: show the playback
    357             // UI for the playing media source.
    358             MediaController controller = mActiveMediaSourceManager.getMediaController();
    359             if (controller != null) {
    360                 closeAppSelector();
    361                 changeMediaSource(new MediaSource(this, controller.getPackageName()), controller);
    362                 switchToMode(Mode.PLAYBACK);
    363                 return;
    364             }
    365         }
    366 
    367         // In any other case, if we were already browsing something: just close drawers/overlays
    368         // and display what we have.
    369         if (mMediaSource != null) {
    370             closeAppSelector();
    371             return;
    372         }
    373 
    374         // If we don't have a current media source, we try with the last one we remember.
    375         MediaSource lastMediaSource = getLastMediaSource();
    376         if (lastMediaSource != null) {
    377             closeAppSelector();
    378             changeMediaSource(lastMediaSource, null);
    379             switchToMode(Mode.BROWSING);
    380         } else {
    381             // If we don't have anything from before: open the app selector.
    382             openAppSelector();
    383         }
    384     }
    385 
    386     /**
    387      * Sets the media source being browsed.
    388      *
    389      * @param mediaSource the media source we are going to try to browse
    390      * @param controller a controller we can use to control the playback state of the given
    391      *                   source. If not provided, we will try to obtain it from the session manager.
    392      *                   Otherwise, we will obtain a controller once the media browser is connected.
    393      */
    394     private void changeMediaSource(MediaSource mediaSource, MediaController controller) {
    395         if (Objects.equals(mediaSource, mMediaSource)) {
    396             // No change, nothing to do.
    397             return;
    398         }
    399         if (mMediaSource != null) {
    400             mMediaSource.unsubscribeChildren(null, mItemsSubscription);
    401             mMediaSource.unsubscribe(mMediaSourceObserver);
    402             updateTabs(new ArrayList<>());
    403         }
    404         mMediaSource = mediaSource;
    405         mPlaybackModel.setMediaController(controller != null ? controller
    406                 : mActiveMediaSourceManager.getControllerForPackage(mediaSource.getPackageName()));
    407         setLastMediaSource(mMediaSource);
    408         if (mMediaSource != null) {
    409             if (Log.isLoggable(TAG, Log.INFO)) {
    410                 Log.i(TAG, "Browsing: " + mediaSource.getName());
    411             }
    412             // Prepare the media source for playback
    413             mPlaybackModel.onPrepare();
    414             // Make the drawer display browse information of the selected source
    415             ComponentName component = mMediaSource.getBrowseServiceComponentName();
    416             MediaManager.getInstance(this).setMediaClientComponent(component);
    417             // If content forward browsing is disabled, then no need to subscribe to this media
    418             // source, we will use the drawer instead.
    419             if (mContentForwardBrowseEnabled) {
    420                 Log.i(TAG, "Content forward is enabled: subscribing to " +
    421                         mMediaSource.getPackageName());
    422                 updateBrowseFragment(BrowseState.LOADING, null);
    423                 mMediaSource.subscribe(mMediaSourceObserver);
    424             }
    425             mAppBarView.setAppIcon(mMediaSource.getRoundPackageIcon());
    426             mAppBarView.setTitle(mMediaSource.getName());
    427         } else {
    428             mAppBarView.setAppIcon(null);
    429             mAppBarView.setTitle(null);
    430         }
    431     }
    432 
    433     private boolean isCurrentMediaSourcePlaying() {
    434         return mMediaSource != null
    435                 && mActiveMediaSourceManager.isPlaying(mMediaSource.getPackageName());
    436     }
    437 
    438     /**
    439      * Updates the tabs displayed on the app bar, based on the top level items on the browse tree.
    440      * If there is at least one browsable item, we show the browse content of that node.
    441      * If there are only playable items, then we show those items.
    442      * If there are not items at all, we show the empty message.
    443      * If we receive null, we show the error message.
    444      *
    445      * @param items top level items, or null if there was an error trying load those items.
    446      */
    447     private void updateTabs(List<MediaItemMetadata> items) {
    448         if (items == null || items.isEmpty()) {
    449             mAppBarView.setItems(null);
    450             updateBrowseFragment(items == null ? BrowseState.ERROR : BrowseState.EMPTY, null);
    451             return;
    452         }
    453 
    454         items = customizeTabs(mMediaSource, items);
    455         List<MediaItemMetadata> browsableTopLevel = items.stream()
    456                 .filter(item -> item.isBrowsable())
    457                 .collect(Collectors.toList());
    458 
    459         if (!browsableTopLevel.isEmpty()) {
    460             // If we have at least a few browsable items, we show the tabs
    461             mAppBarView.setItems(browsableTopLevel);
    462             updateBrowseFragment(BrowseState.LOADED, browsableTopLevel.get(0));
    463         } else {
    464             // Otherwise, we show the top of the tree with no fabs
    465             mAppBarView.setItems(null);
    466             updateBrowseFragment(BrowseState.LOADED, null);
    467         }
    468     }
    469 
    470     /**
    471      * Extension point used to customize media items displayed on the tabs.
    472      *
    473      * @param mediaSource media source these items belong to.
    474      * @param items items to override.
    475      * @return an updated list of items.
    476      * @deprecated This method will be removed on b/79089344
    477      */
    478     @Deprecated
    479     protected List<MediaItemMetadata> customizeTabs(MediaSource mediaSource,
    480             List<MediaItemMetadata> items) {
    481         return items;
    482     }
    483 
    484     private void switchToMode(Mode mode) {
    485         // If content forward is not enable, then we always show the playback UI (browse will be
    486         // done in the drawer)
    487         mMode = mContentForwardBrowseEnabled ? mode : Mode.PLAYBACK;
    488         updateMetadata();
    489         switch (mMode) {
    490             case PLAYBACK:
    491                 ViewUtils.showViewAnimated(mPlaybackContainer, mFadeDuration);
    492                 ViewUtils.hideViewAnimated(mBrowseContainer, mFadeDuration);
    493                 mAppBarView.setState(AppBarView.State.PLAYING);
    494                 break;
    495             case BROWSING:
    496                 ViewUtils.hideViewAnimated(mPlaybackContainer, mFadeDuration);
    497                 ViewUtils.showViewAnimated(mBrowseContainer, mFadeDuration);
    498                 mAppBarView.setState(AppBarView.State.BROWSING);
    499                 break;
    500         }
    501     }
    502 
    503     /**
    504      * Updates the browse area with either a loading state, the root node content, or the
    505      * content of a particular media item.
    506      *
    507      * @param state state in the process of loading browse information.
    508      * @param topItem if state == IDLE, this will contain the item to display,
    509      *                or null to display the root node.
    510      */
    511     private void updateBrowseFragment(BrowseState state, MediaItemMetadata topItem) {
    512         switch(state) {
    513             case LOADED:
    514                 if (topItem != null) {
    515                     mCurrentFragment = BrowseFragment.newInstance(mMediaSource, topItem);
    516                     mAppBarView.setActiveItem(topItem);
    517                 } else {
    518                     mCurrentFragment = BrowseFragment.newInstance(mMediaSource, null);
    519                     mAppBarView.setActiveItem(null);
    520                 }
    521                 break;
    522             case EMPTY:
    523             case LOADING:
    524             case ERROR:
    525                 mCurrentFragment = mEmptyFragment;
    526                 mEmptyFragment.setState(state, mMediaSource);
    527                 mAppBarView.setActiveItem(null);
    528                 break;
    529         }
    530         getSupportFragmentManager().beginTransaction()
    531                 .replace(R.id.fragment_container, mCurrentFragment)
    532                 .commitAllowingStateLoss();
    533     }
    534 
    535     private void updateMetadata() {
    536         if (isCurrentMediaSourcePlaying()) {
    537             if (mMode == Mode.PLAYBACK) {
    538                 ViewUtils.hideViewAnimated(mBrowseControlsContainer, mFadeDuration);
    539             } else {
    540                 ViewUtils.showViewAnimated(mBrowseControlsContainer, mFadeDuration);
    541             }
    542             MediaItemMetadata metadata = mPlaybackModel.getMetadata();
    543             if (Objects.equals(mCurrentMetadata, metadata)) {
    544                 return;
    545             }
    546             mCurrentMetadata = metadata;
    547             mUpdateAlbumArtRunnable.run();
    548         } else {
    549             mAlbumBackground.setImageBitmap(null, true);
    550             ViewUtils.hideViewAnimated(mBrowseControlsContainer, mFadeDuration);
    551         }
    552     }
    553 
    554     /**
    555      * We might receive new album art before we are ready to display it. If that situation happens
    556      * we will retrieve and render the album art when the views are already laid out.
    557      */
    558     private Runnable mUpdateAlbumArtRunnable = new Runnable() {
    559         @Override
    560         public void run() {
    561             MediaItemMetadata metadata = mPlaybackModel.getMetadata();
    562             if (metadata != null) {
    563                 if (mAlbumBackground.getWidth() == 0 || mAlbumBackground.getHeight() == 0) {
    564                     // We need to wait for the view to be measured before we can render this
    565                     // album art.
    566                     mAlbumBackground.setImageBitmap(null, false);
    567                     mAlbumBackground.post(this);
    568                 } else {
    569                     mAlbumBackground.removeCallbacks(this);
    570                     metadata.getAlbumArt(MediaActivity.this,
    571                             mAlbumBackground.getWidth(),
    572                             mAlbumBackground.getHeight(),
    573                             false)
    574                             .thenAccept(bitmap -> setBackgroundImage(bitmap));
    575                 }
    576             } else {
    577                 mAlbumBackground.removeCallbacks(this);
    578                 mAlbumBackground.setImageBitmap(null, true);
    579             }
    580         }
    581     };
    582 
    583     private void setBackgroundImage(Bitmap bitmap) {
    584         // TODO(b/77551865): Implement image blurring once the following issue is solved:
    585         // b/77551557
    586         // bitmap = ImageUtils.blur(getContext(), bitmap, mBackgroundBlurScale,
    587         //        mBackgroundBlurRadius);
    588         mAlbumBackground.setImageBitmap(bitmap, true);
    589     }
    590 
    591     @Override
    592     public MediaSource getMediaSource(String packageName) {
    593         if (mMediaSource != null && mMediaSource.getPackageName().equals(packageName)) {
    594             return mMediaSource;
    595         }
    596         return new MediaSource(this, packageName);
    597     }
    598 
    599     @Override
    600     public void onBackStackChanged() {
    601         // TODO: Update ActionBar
    602     }
    603 
    604     @Override
    605     public void onPlayableItemClicked(MediaSource mediaSource, MediaItemMetadata item) {
    606         mPlaybackModel.onStop();
    607         if (!Objects.equals(mediaSource, mPlaybackModel.getMediaSource())) {
    608             Log.w(TAG, "Trying to play an item from a different source "
    609                 + "(expected: " + mPlaybackModel.getMediaSource() + ", received"
    610                 + mediaSource + ")");
    611             changeMediaSource(mediaSource, mediaSource.getMediaController());
    612         }
    613         mPlaybackModel.onPlayItem(item.getId());
    614         setIntent(null);
    615     }
    616 
    617     private void openAppSelector() {
    618         mIsAppSelectorOpen = true;
    619         FragmentManager manager = getSupportFragmentManager();
    620         mAppBarView.setState(AppBarView.State.APP_SELECTION);
    621         manager.beginTransaction()
    622                 .replace(R.id.app_selection_container, mAppSelectionFragment)
    623                 .commit();
    624     }
    625 
    626     private void closeAppSelector() {
    627         mIsAppSelectorOpen = false;
    628         FragmentManager manager = getSupportFragmentManager();
    629         mAppBarView.setState(mMode == Mode.PLAYBACK ? AppBarView.State.PLAYING
    630                 : AppBarView.State.BROWSING);
    631         manager.beginTransaction()
    632                 .remove(mAppSelectionFragment)
    633                 .commit();
    634     }
    635 
    636     @Override
    637     public List<MediaSource> getMediaSources() {
    638         return mMediaSourcesManager.getMediaSources()
    639                 .stream()
    640                 .filter(source -> source.getMediaBrowser() != null || source.isCustom())
    641                 .collect(Collectors.toList());
    642     }
    643 
    644     @Override
    645     public void onMediaSourceSelected(MediaSource mediaSource) {
    646         closeAppSelector();
    647         if (mediaSource.getMediaBrowser() != null && !mediaSource.isCustom()) {
    648             mCurrentMetadata = null;
    649             changeMediaSource(mediaSource, null);
    650             switchToMode(Mode.BROWSING);
    651         } else {
    652             String packageName = mediaSource.getPackageName();
    653             Intent intent = getPackageManager().getLaunchIntentForPackage(packageName);
    654             startActivity(intent);
    655         }
    656     }
    657 
    658     private MediaSource getLastMediaSource() {
    659         String packageName = mSharedPreferences.getString(LAST_MEDIA_SOURCE_SHARED_PREF_KEY, null);
    660         if (packageName == null) {
    661             return null;
    662         }
    663         // Verify that the stored package name corresponds to a currently installed media source.
    664         for (MediaSource mediaSource : mMediaSourcesManager.getMediaSources()) {
    665             if (mediaSource.getPackageName().equals(packageName)) {
    666                 return mediaSource;
    667             }
    668         }
    669         return null;
    670     }
    671 
    672     private void setLastMediaSource(@NonNull MediaSource mediaSource) {
    673         mSharedPreferences.edit()
    674                 .putString(LAST_MEDIA_SOURCE_SHARED_PREF_KEY, mediaSource.getPackageName())
    675                 .apply();
    676     }
    677 
    678 
    679     @Override
    680     public PlaybackModel getPlaybackModel() {
    681         return mPlaybackModel;
    682     }
    683 
    684     @Override
    685     public void onQueueButtonClicked() {
    686         if (mContentForwardBrowseEnabled) {
    687             mPlaybackFragment.toggleQueueVisibility();
    688         } else {
    689             mDrawerController.showPlayQueue();
    690         }
    691     }
    692 }
    693