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