• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.SuppressLint;
19 import android.app.AlertDialog;
20 import android.app.Application;
21 import android.app.PendingIntent;
22 import android.car.Car;
23 import android.car.drivingstate.CarUxRestrictions;
24 import android.content.ActivityNotFoundException;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.pm.ResolveInfo;
28 import android.os.Bundle;
29 import android.support.v4.media.session.PlaybackStateCompat;
30 import android.text.TextUtils;
31 import android.transition.Fade;
32 import android.util.Log;
33 import android.view.GestureDetector;
34 import android.view.MotionEvent;
35 import android.view.View;
36 import android.view.ViewConfiguration;
37 import android.view.ViewGroup;
38 import android.widget.Toast;
39 
40 import androidx.annotation.NonNull;
41 import androidx.annotation.Nullable;
42 import androidx.core.view.GestureDetectorCompat;
43 import androidx.fragment.app.Fragment;
44 import androidx.fragment.app.FragmentActivity;
45 import androidx.lifecycle.AndroidViewModel;
46 import androidx.lifecycle.LiveData;
47 import androidx.lifecycle.MutableLiveData;
48 import androidx.lifecycle.ViewModelProviders;
49 
50 import com.android.car.apps.common.CarUxRestrictionsUtil;
51 import com.android.car.apps.common.util.ViewUtils;
52 import com.android.car.media.common.AppSelectionFragment;
53 import com.android.car.media.common.MediaAppSelectorWidget;
54 import com.android.car.media.common.MediaConstants;
55 import com.android.car.media.common.MediaItemMetadata;
56 import com.android.car.media.common.MinimizedPlaybackControlBar;
57 import com.android.car.media.common.browse.MediaBrowserViewModel;
58 import com.android.car.media.common.playback.PlaybackViewModel;
59 import com.android.car.media.common.source.MediaSource;
60 import com.android.car.media.common.source.MediaSourceViewModel;
61 import com.android.car.media.widgets.AppBarView;
62 import com.android.car.media.widgets.SearchBar;
63 
64 import java.util.List;
65 import java.util.Objects;
66 import java.util.stream.Collectors;
67 
68 /**
69  * This activity controls the UI of media. It also updates the connection status for the media app
70  * by broadcast.
71  */
72 public class MediaActivity extends FragmentActivity implements BrowseFragment.Callbacks,
73         AppBarView.AppBarProvider {
74     private static final String TAG = "MediaActivity";
75 
76     /** Configuration (controlled from resources) */
77     private int mFadeDuration;
78 
79     /** Models */
80     private PlaybackViewModel.PlaybackController mPlaybackController;
81 
82     /** Layout views */
83     private View mRootView;
84     private AppBarView mAppBarView;
85     private PlaybackFragment mPlaybackFragment;
86     private BrowseFragment mSearchFragment;
87     private BrowseFragment mBrowseFragment;
88     private AppSelectionFragment mAppSelectionFragment;
89     private ViewGroup mMiniPlaybackControls;
90     private EmptyFragment mEmptyFragment;
91     private ViewGroup mBrowseContainer;
92     private ViewGroup mPlaybackContainer;
93     private ViewGroup mErrorContainer;
94     private ErrorFragment mErrorFragment;
95     private ViewGroup mSearchContainer;
96 
97     private Toast mToast;
98 
99     /** Current state */
100     private Mode mMode;
101     private Intent mCurrentSourcePreferences;
102     private boolean mCanShowMiniPlaybackControls;
103     private boolean mIsBrowseTreeReady;
104     private Integer mCurrentPlaybackState;
105     private List<MediaItemMetadata> mTopItems;
106 
107     private CarUxRestrictionsUtil mCarUxRestrictionsUtil;
108     private CarUxRestrictions mActiveCarUxRestrictions;
109     @CarUxRestrictions.CarUxRestrictionsInfo
110     private int mRestrictions;
111     private final CarUxRestrictionsUtil.OnUxRestrictionsChangedListener mListener =
112             (carUxRestrictions) -> mActiveCarUxRestrictions = carUxRestrictions;
113 
114     private AppBarView.AppBarListener mAppBarListener = new AppBarView.AppBarListener() {
115         @Override
116         public void onTabSelected(MediaItemMetadata item) {
117             showTopItem(item);
118             changeMode(Mode.BROWSING);
119         }
120 
121         @Override
122         public void onBack() {
123             BrowseFragment fragment = getCurrentBrowseFragment();
124             if (fragment != null) {
125                 boolean success = fragment.navigateBack();
126                 if (!success && (fragment == mSearchFragment)) {
127                     changeMode(Mode.BROWSING);
128                 }
129             }
130         }
131 
132         @Override
133         public void onSettingsSelection() {
134             if (Log.isLoggable(TAG, Log.DEBUG)) {
135                 Log.d(TAG, "onSettingsSelection");
136             }
137             try {
138                 if (mCurrentSourcePreferences != null) {
139                     startActivity(mCurrentSourcePreferences);
140                 }
141             } catch (ActivityNotFoundException e) {
142                 if (Log.isLoggable(TAG, Log.ERROR)) {
143                     Log.e(TAG, "onSettingsSelection " + e);
144                 }
145             }
146         }
147 
148         @Override
149         public void onSearchSelection() {
150             changeMode(Mode.SEARCHING);
151         }
152 
153         @Override
154         public void onSearch(String query) {
155             if (Log.isLoggable(TAG, Log.DEBUG)) {
156                 Log.d(TAG, "onSearch: " + query);
157             }
158             mSearchFragment.updateSearchQuery(query);
159         }
160     };
161 
162     private PlaybackFragment.PlaybackFragmentListener mPlaybackFragmentListener =
163             () -> changeMode(Mode.BROWSING);
164 
165     /**
166      * Possible modes of the application UI
167      */
168     private enum Mode {
169         /** The user is browsing a media source */
170         BROWSING,
171         /** The user is interacting with the full screen playback UI */
172         PLAYBACK,
173         /** The user is searching within a media source */
174         SEARCHING,
175         /** There's no browse tree and playback doesn't work.*/
176         FATAL_ERROR
177     }
178 
179     @Override
onCreate(Bundle savedInstanceState)180     protected void onCreate(Bundle savedInstanceState) {
181         super.onCreate(savedInstanceState);
182         setContentView(R.layout.media_activity);
183 
184         MediaSourceViewModel mediaSourceViewModel = getMediaSourceViewModel();
185         PlaybackViewModel playbackViewModel = getPlaybackViewModel();
186         ViewModel localViewModel = getInnerViewModel();
187         // We can't rely on savedInstanceState to determine whether the model has been initialized
188         // as on a config change savedInstanceState != null and the model is initialized, but if
189         // the app was killed by the system then savedInstanceState != null and the model is NOT
190         // initialized...
191         if (localViewModel.needsInitialization()) {
192             localViewModel.init(playbackViewModel);
193         }
194         mMode = localViewModel.getSavedMode();
195 
196         mRootView = findViewById(R.id.media_activity_root);
197         mAppBarView = findViewById(R.id.app_bar);
198         mAppBarView.setListener(mAppBarListener);
199         mediaSourceViewModel.getPrimaryMediaSource().observe(this,
200                 this::onMediaSourceChanged);
201 
202         MediaAppSelectorWidget appSelector = findViewById(R.id.app_switch_container);
203         appSelector.setFragmentActivity(this);
204         SearchBar searchBar = findViewById(R.id.search_bar_container);
205         searchBar.setFragmentActivity(this);
206         searchBar.setAppBarListener(mAppBarListener);
207 
208         mEmptyFragment = new EmptyFragment();
209         MediaBrowserViewModel mediaBrowserViewModel = getRootBrowserViewModel();
210         mediaBrowserViewModel.getBrowseState().observe(this,
211                 browseState -> {
212                     mEmptyFragment.setState(browseState,
213                             mediaSourceViewModel.getPrimaryMediaSource().getValue());
214                 });
215         mediaBrowserViewModel.getBrowsedMediaItems().observe(this, futureData -> {
216             if (!futureData.isLoading()) {
217                 if (futureData.getData() != null) {
218                     mIsBrowseTreeReady = true;
219                     handlePlaybackState(playbackViewModel.getPlaybackStateWrapper().getValue());
220                 }
221                 updateTabs(futureData.getData());
222             }
223         });
224         mediaBrowserViewModel.supportsSearch().observe(this,
225                 mAppBarView::setSearchSupported);
226 
227         mPlaybackFragment = new PlaybackFragment();
228         mPlaybackFragment.setListener(mPlaybackFragmentListener);
229         mSearchFragment = BrowseFragment.newSearchInstance();
230         mAppSelectionFragment = new AppSelectionFragment();
231         int fadeDuration = getResources().getInteger(R.integer.app_selector_fade_duration);
232         mAppSelectionFragment.setEnterTransition(new Fade().setDuration(fadeDuration));
233         mAppSelectionFragment.setExitTransition(new Fade().setDuration(fadeDuration));
234 
235         MinimizedPlaybackControlBar browsePlaybackControls =
236                 findViewById(R.id.minimized_playback_controls);
237         browsePlaybackControls.setModel(playbackViewModel, this);
238 
239         mMiniPlaybackControls = findViewById(R.id.minimized_playback_controls);
240         mMiniPlaybackControls.setOnClickListener(view -> changeMode(Mode.PLAYBACK));
241 
242         mFadeDuration = getResources().getInteger(R.integer.new_album_art_fade_in_duration);
243         mBrowseContainer = findViewById(R.id.fragment_container);
244         mErrorContainer = findViewById(R.id.error_container);
245         mPlaybackContainer = findViewById(R.id.playback_container);
246         mSearchContainer = findViewById(R.id.search_container);
247         getSupportFragmentManager().beginTransaction()
248                 .replace(R.id.playback_container, mPlaybackFragment)
249                 .commit();
250         getSupportFragmentManager().beginTransaction()
251                 .replace(R.id.search_container, mSearchFragment)
252                 .commit();
253 
254         playbackViewModel.getPlaybackController().observe(this,
255                 playbackController -> {
256                     if (playbackController != null) playbackController.prepare();
257                     mPlaybackController = playbackController;
258                 });
259 
260         playbackViewModel.getPlaybackStateWrapper().observe(this, this::handlePlaybackState);
261 
262         mCarUxRestrictionsUtil = CarUxRestrictionsUtil.getInstance(this);
263         mRestrictions = CarUxRestrictions.UX_RESTRICTIONS_NO_SETUP;
264         mCarUxRestrictionsUtil.register(mListener);
265 
266         mPlaybackContainer.setOnTouchListener(new ClosePlaybackDetector(this));
267     }
268 
269     @Override
onDestroy()270     protected void onDestroy() {
271         mCarUxRestrictionsUtil.unregister(mListener);
272         super.onDestroy();
273     }
274 
isUxRestricted()275     private boolean isUxRestricted() {
276         return CarUxRestrictionsUtil.isRestricted(mRestrictions, mActiveCarUxRestrictions);
277     }
278 
handlePlaybackState(PlaybackViewModel.PlaybackStateWrapper state)279     private void handlePlaybackState(PlaybackViewModel.PlaybackStateWrapper state) {
280         // TODO(arnaudberry) rethink interactions between customized layouts and dynamic visibility.
281         mCanShowMiniPlaybackControls = (state != null) && state.shouldDisplay();
282 
283         if (mMode != Mode.PLAYBACK) {
284             ViewUtils.setVisible(mMiniPlaybackControls, mCanShowMiniPlaybackControls);
285             getInnerViewModel().setMiniControlsVisible(mCanShowMiniPlaybackControls);
286         }
287         if (state == null) {
288             return;
289         }
290         if (mCurrentPlaybackState == null || mCurrentPlaybackState != state.getState()) {
291             mCurrentPlaybackState = state.getState();
292             if (Log.isLoggable(TAG, Log.VERBOSE)) {
293                 Log.v(TAG, "handlePlaybackState(); state change: " + mCurrentPlaybackState);
294             }
295         }
296 
297         Bundle extras = state.getExtras();
298         PendingIntent intent = extras == null ? null : extras.getParcelable(
299                 MediaConstants.ERROR_RESOLUTION_ACTION_INTENT);
300 
301         String label = extras == null ? null : extras.getString(
302                 MediaConstants.ERROR_RESOLUTION_ACTION_LABEL);
303 
304         String displayedMessage = null;
305         if (!TextUtils.isEmpty(state.getErrorMessage())) {
306             displayedMessage = state.getErrorMessage().toString();
307         } else if (state.getErrorCode() != PlaybackStateCompat.ERROR_CODE_UNKNOWN_ERROR) {
308             // TODO(b/131601881): convert the error codes to prebuilt error messages
309             displayedMessage = getString(R.string.default_error_message);
310         } else if (state.getState() == PlaybackStateCompat.STATE_ERROR) {
311             displayedMessage = getString(R.string.default_error_message);
312         }
313 
314         if (!TextUtils.isEmpty(displayedMessage)) {
315             if (mIsBrowseTreeReady) {
316                 if (intent != null && !isUxRestricted()) {
317                     showDialog(intent, displayedMessage, label, getString(android.R.string.cancel));
318                 } else {
319                     showToast(displayedMessage);
320                 }
321             } else {
322                 mErrorFragment = ErrorFragment.newInstance(displayedMessage, label, intent);
323                 setErrorFragment(mErrorFragment);
324                 changeMode(Mode.FATAL_ERROR);
325             }
326         }
327     }
328 
showDialog(PendingIntent intent, String message, String positiveBtnText, String negativeButtonText)329     private void showDialog(PendingIntent intent, String message, String positiveBtnText,
330             String negativeButtonText) {
331         AlertDialog.Builder dialog = new AlertDialog.Builder(this);
332         dialog.setMessage(message)
333                 .setNegativeButton(negativeButtonText, null)
334                 .setPositiveButton(positiveBtnText, (dialogInterface, i) -> {
335                     try {
336                         intent.send();
337                     } catch (PendingIntent.CanceledException e) {
338                         if (Log.isLoggable(TAG, Log.ERROR)) {
339                             Log.e(TAG, "Pending intent canceled");
340                         }
341                     }
342                 })
343                 .show();
344     }
345 
showToast(String message)346     private void showToast(String message) {
347         maybeCancelToast();
348         mToast = Toast.makeText(this, message, Toast.LENGTH_LONG);
349         mToast.show();
350     }
351 
maybeCancelToast()352     private void maybeCancelToast() {
353         if (mToast != null) {
354             mToast.cancel();
355             mToast = null;
356         }
357     }
358 
359     @Override
onBackPressed()360     public void onBackPressed() {
361         super.onBackPressed();
362     }
363 
364     @Override
onResume()365     protected void onResume() {
366         super.onResume();
367 
368         MediaSource mediaSource = getMediaSourceViewModel().getPrimaryMediaSource().getValue();
369         if (mediaSource == null) {
370             mAppBarView.openAppSelector();
371         } else {
372             mAppBarView.closeAppSelector();
373         }
374     }
375 
376     /**
377      * Sets the media source being browsed.
378      *
379      * @param mediaSource the new media source we are going to try to browse
380      */
onMediaSourceChanged(@ullable MediaSource mediaSource)381     private void onMediaSourceChanged(@Nullable MediaSource mediaSource) {
382         mIsBrowseTreeReady = false;
383         maybeCancelToast();
384         if (mediaSource != null) {
385             if (Log.isLoggable(TAG, Log.INFO)) {
386                 Log.i(TAG, "Browsing: " + mediaSource.getName());
387             }
388             mAppBarView.setMediaAppTitle(mediaSource.getName());
389             mAppBarView.setTitle(null);
390             updateTabs(null);
391             mSearchFragment.resetSearchState();
392             changeMode(Mode.BROWSING);
393             String packageName = mediaSource.getPackageName();
394             updateSourcePreferences(packageName);
395 
396             // Always go through the trampoline activity to keep all the dispatching logic there.
397             startActivity(new Intent(Car.CAR_INTENT_ACTION_MEDIA_TEMPLATE));
398         } else {
399             mAppBarView.setMediaAppTitle(null);
400             mAppBarView.setTitle(null);
401             updateTabs(null);
402             updateSourcePreferences(null);
403         }
404     }
405 
updateSourcePreferences(@ullable String packageName)406     private void updateSourcePreferences(@Nullable String packageName) {
407         mCurrentSourcePreferences = null;
408         if (packageName != null) {
409             Intent prefsIntent = new Intent(Intent.ACTION_APPLICATION_PREFERENCES);
410             prefsIntent.setPackage(packageName);
411             ResolveInfo info = getPackageManager().resolveActivity(prefsIntent, 0);
412             if (info != null) {
413                 mCurrentSourcePreferences = new Intent(prefsIntent.getAction())
414                         .setClassName(info.activityInfo.packageName, info.activityInfo.name);
415             }
416         }
417         mAppBarView.setHasSettings(mCurrentSourcePreferences != null);
418     }
419 
420     /**
421      * Updates the tabs displayed on the app bar, based on the top level items on the browse tree.
422      * If there is at least one browsable item, we show the browse content of that node. If there
423      * are only playable items, then we show those items. If there are not items at all, we show the
424      * empty message. If we receive null, we show the error message.
425      *
426      * @param items top level items, or null if there was an error trying load those items.
427      */
updateTabs(List<MediaItemMetadata> items)428     private void updateTabs(List<MediaItemMetadata> items) {
429         if (items == null || items.isEmpty()) {
430             mAppBarView.setActiveItem(null);
431             mAppBarView.setItems(null);
432             setCurrentFragment(mEmptyFragment);
433             mBrowseFragment = null;
434             mTopItems = items;
435             return;
436         }
437 
438         if (Objects.equals(mTopItems, items)) {
439             // When coming back to the app, the live data sends an update even if the list hasn't
440             // changed. Updating the tabs then recreates the browse fragment, which produces jank
441             // (b/131830876), and also resets the navigation to the top of the first tab...
442             return;
443         }
444         mTopItems = items;
445 
446         List<MediaItemMetadata> browsableTopLevel = items.stream()
447                 .filter(MediaItemMetadata::isBrowsable)
448                 .collect(Collectors.toList());
449 
450         if (browsableTopLevel.size() == 1) {
451             // If there is only a single tab, use it as a header instead
452             mAppBarView.setMediaAppTitle(browsableTopLevel.get(0).getTitle());
453             mAppBarView.setTitle(null);
454             mAppBarView.setItems(null);
455         } else {
456             mAppBarView.setItems(browsableTopLevel);
457         }
458         showTopItem(browsableTopLevel.isEmpty() ? null : browsableTopLevel.get(0));
459     }
460 
showTopItem(@ullable MediaItemMetadata topItem)461     private void showTopItem(@Nullable MediaItemMetadata topItem) {
462         mBrowseFragment = BrowseFragment.newInstance(topItem);
463         setCurrentFragment(mBrowseFragment);
464         mAppBarView.setActiveItem(topItem);
465     }
466 
setErrorFragment(Fragment fragment)467     private void setErrorFragment(Fragment fragment) {
468         getSupportFragmentManager().beginTransaction()
469                 .replace(R.id.error_container, fragment)
470                 .commitAllowingStateLoss();
471     }
472 
setCurrentFragment(Fragment fragment)473     private void setCurrentFragment(Fragment fragment) {
474         getSupportFragmentManager().beginTransaction()
475                 .replace(R.id.fragment_container, fragment)
476                 .commitAllowingStateLoss();
477     }
478 
479     @Nullable
getCurrentBrowseFragment()480     private BrowseFragment getCurrentBrowseFragment() {
481         return mMode == Mode.SEARCHING ? mSearchFragment : mBrowseFragment;
482     }
483 
changeMode(Mode mode)484     private void changeMode(Mode mode) {
485         if (mMode == mode) return;
486 
487         if (Log.isLoggable(TAG, Log.INFO)) {
488             Log.i(TAG, "Changing mode from: " + mMode+ " to: " + mode);
489         }
490 
491         Mode oldMode = mMode;
492         getInnerViewModel().saveMode(mode);
493         mMode = mode;
494 
495         if (mode == Mode.FATAL_ERROR) {
496             ViewUtils.showViewAnimated(mErrorContainer, mFadeDuration);
497             ViewUtils.hideViewAnimated(mPlaybackContainer, mFadeDuration);
498             ViewUtils.hideViewAnimated(mBrowseContainer, mFadeDuration);
499             ViewUtils.hideViewAnimated(mSearchContainer, mFadeDuration);
500             mAppBarView.setState(AppBarView.State.EMPTY);
501             return;
502         }
503 
504         updateMetadata(mode);
505 
506         switch (mode) {
507             case PLAYBACK:
508                 mPlaybackContainer.setY(0);
509                 mPlaybackContainer.setAlpha(0f);
510                 ViewUtils.hideViewAnimated(mErrorContainer, mFadeDuration);
511                 ViewUtils.showViewAnimated(mPlaybackContainer, mFadeDuration);
512                 ViewUtils.hideViewAnimated(mBrowseContainer, mFadeDuration);
513                 ViewUtils.hideViewAnimated(mSearchContainer, mFadeDuration);
514                 ViewUtils.hideViewAnimated(mAppBarView, mFadeDuration);
515                 break;
516             case BROWSING:
517                 if (oldMode == Mode.PLAYBACK) {
518                     ViewUtils.hideViewAnimated(mErrorContainer, 0);
519                     ViewUtils.showViewAnimated(mBrowseContainer, 0);
520                     ViewUtils.hideViewAnimated(mSearchContainer, 0);
521                     ViewUtils.showViewAnimated(mAppBarView, 0);
522                     mPlaybackContainer.animate()
523                             .translationY(mRootView.getHeight())
524                             .setDuration(mFadeDuration)
525                             .setListener(ViewUtils.hideViewAfterAnimation(mPlaybackContainer))
526                             .start();
527                 } else {
528                     ViewUtils.hideViewAnimated(mErrorContainer, mFadeDuration);
529                     ViewUtils.hideViewAnimated(mPlaybackContainer, mFadeDuration);
530                     ViewUtils.showViewAnimated(mBrowseContainer, mFadeDuration);
531                     ViewUtils.hideViewAnimated(mSearchContainer, mFadeDuration);
532                     ViewUtils.showViewAnimated(mAppBarView, mFadeDuration);
533                 }
534                 updateAppBar();
535                 break;
536             case SEARCHING:
537                 ViewUtils.hideViewAnimated(mErrorContainer, mFadeDuration);
538                 ViewUtils.hideViewAnimated(mPlaybackContainer, mFadeDuration);
539                 ViewUtils.hideViewAnimated(mBrowseContainer, mFadeDuration);
540                 ViewUtils.showViewAnimated(mSearchContainer, mFadeDuration);
541                 ViewUtils.showViewAnimated(mAppBarView, mFadeDuration);
542                 updateAppBar();
543                 break;
544         }
545     }
546 
updateAppBar()547     private void updateAppBar() {
548         BrowseFragment fragment = getCurrentBrowseFragment();
549         boolean isStacked = fragment != null && !fragment.isAtTopStack();
550         AppBarView.State unstackedState = mMode == Mode.SEARCHING
551                 ? AppBarView.State.SEARCHING
552                 : AppBarView.State.BROWSING;
553         mAppBarView.setTitle(isStacked ? fragment.getCurrentMediaItem().getTitle() : null);
554         mAppBarView.setState(isStacked ? AppBarView.State.STACKED : unstackedState);
555     }
556 
updateMetadata(Mode mode)557     private void updateMetadata(Mode mode) {
558         if (mode != Mode.PLAYBACK) {
559             mPlaybackFragment.closeOverflowMenu();
560             if (mCanShowMiniPlaybackControls) {
561                 ViewUtils.showViewAnimated(mMiniPlaybackControls, mFadeDuration);
562                 getInnerViewModel().setMiniControlsVisible(true);
563             }
564         }
565     }
566 
567     @Override
onBackStackChanged()568     public void onBackStackChanged() {
569         updateAppBar();
570     }
571 
572     @Override
onPlayableItemClicked(MediaItemMetadata item)573     public void onPlayableItemClicked(MediaItemMetadata item) {
574         mPlaybackController.stop();
575         mPlaybackController.playItem(item.getId());
576         boolean switchToPlayback = getResources().getBoolean(
577                 R.bool.switch_to_playback_view_when_playable_item_is_clicked);
578         if (switchToPlayback) {
579             changeMode(Mode.PLAYBACK);
580         } else if (mMode == Mode.SEARCHING) {
581             changeMode(Mode.BROWSING);
582         }
583         setIntent(null);
584     }
585 
getMediaSourceViewModel()586     public MediaSourceViewModel getMediaSourceViewModel() {
587         return MediaSourceViewModel.get(getApplication());
588     }
589 
getPlaybackViewModel()590     public PlaybackViewModel getPlaybackViewModel() {
591         return PlaybackViewModel.get(getApplication());
592     }
593 
getRootBrowserViewModel()594     private MediaBrowserViewModel getRootBrowserViewModel() {
595         return MediaBrowserViewModel.Factory.getInstanceForBrowseRoot(getMediaSourceViewModel(),
596                 ViewModelProviders.of(this));
597     }
598 
getInnerViewModel()599     public ViewModel getInnerViewModel() {
600         return ViewModelProviders.of(this).get(ViewModel.class);
601     }
602 
603     @Override
getAppBar()604     public AppBarView getAppBar() {
605         return mAppBarView;
606     }
607 
608     public static class ViewModel extends AndroidViewModel {
609         private boolean mNeedsInitialization = true;
610         private PlaybackViewModel mPlaybackViewModel;
611         /** Saves the Mode across config changes. */
612         private Mode mSavedMode;
613 
614         private MutableLiveData<Boolean> mIsMiniControlsVisible = new MutableLiveData<>();
615 
ViewModel(@onNull Application application)616         public ViewModel(@NonNull Application application) {
617             super(application);
618         }
619 
init(@onNull PlaybackViewModel playbackViewModel)620         void init(@NonNull PlaybackViewModel playbackViewModel) {
621             if (mPlaybackViewModel == playbackViewModel) {
622                 return;
623             }
624             mPlaybackViewModel = playbackViewModel;
625             mSavedMode = Mode.BROWSING;
626             mNeedsInitialization = false;
627         }
628 
needsInitialization()629         boolean needsInitialization() {
630             return mNeedsInitialization;
631         }
632 
setMiniControlsVisible(boolean visible)633         void setMiniControlsVisible(boolean visible) {
634             mIsMiniControlsVisible.setValue(visible);
635         }
636 
getMiniControlsVisible()637         LiveData<Boolean> getMiniControlsVisible() {
638             return mIsMiniControlsVisible;
639         }
640 
saveMode(Mode mode)641         void saveMode(Mode mode) {
642             mSavedMode = mode;
643         }
644 
getSavedMode()645         Mode getSavedMode() {
646             return mSavedMode;
647         }
648     }
649 
650 
651     private class ClosePlaybackDetector extends GestureDetector.SimpleOnGestureListener
652             implements View.OnTouchListener {
653 
654         private final ViewConfiguration mViewConfig;
655         private final GestureDetectorCompat mDetector;
656 
657 
ClosePlaybackDetector(Context context)658         ClosePlaybackDetector(Context context) {
659             mViewConfig = ViewConfiguration.get(context);
660             mDetector = new GestureDetectorCompat(context, this);
661         }
662 
663         @SuppressLint("ClickableViewAccessibility")
664         @Override
onTouch(View v, MotionEvent event)665         public boolean onTouch(View v, MotionEvent event) {
666             return mDetector.onTouchEvent(event);
667         }
668 
669         @Override
onDown(MotionEvent event)670         public boolean onDown(MotionEvent event) {
671             return (mMode == Mode.PLAYBACK);
672         }
673 
674         @Override
onFling(MotionEvent e1, MotionEvent e2, float vX, float vY)675         public boolean onFling(MotionEvent e1, MotionEvent e2, float vX, float vY) {
676             float dY = e2.getY() - e1.getY();
677             if (dY > mViewConfig.getScaledTouchSlop() &&
678                     Math.abs(vY) > mViewConfig.getScaledMinimumFlingVelocity()) {
679                 float dX = e2.getX() - e1.getX();
680                 float tan = Math.abs(dX) / dY;
681                 if (tan <= 0.58) { // Accept 30 degrees on each side of the down vector.
682                     changeMode(Mode.BROWSING);
683                 }
684             }
685             return true;
686         }
687     }
688 }
689