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 static android.car.media.CarMediaManager.MEDIA_SOURCE_MODE_BROWSE; 19 20 import static com.android.car.apps.common.util.LiveDataFunctions.dataOf; 21 import static com.android.car.apps.common.util.VectorMath.EPSILON; 22 23 import android.annotation.SuppressLint; 24 import android.app.AlertDialog; 25 import android.app.Application; 26 import android.app.PendingIntent; 27 import android.car.Car; 28 import android.car.content.pm.CarPackageManager; 29 import android.car.drivingstate.CarUxRestrictions; 30 import android.car.media.CarMediaIntents; 31 import android.content.ComponentName; 32 import android.content.Context; 33 import android.content.Intent; 34 import android.content.res.Resources; 35 import android.os.Bundle; 36 import android.text.TextUtils; 37 import android.util.Log; 38 import android.util.Size; 39 import android.view.GestureDetector; 40 import android.view.MotionEvent; 41 import android.view.View; 42 import android.view.ViewConfiguration; 43 import android.view.ViewGroup; 44 import android.widget.Toast; 45 46 import androidx.annotation.NonNull; 47 import androidx.annotation.Nullable; 48 import androidx.core.view.GestureDetectorCompat; 49 import androidx.fragment.app.FragmentActivity; 50 import androidx.lifecycle.AndroidViewModel; 51 import androidx.lifecycle.LiveData; 52 import androidx.lifecycle.MutableLiveData; 53 import androidx.lifecycle.ViewModelProviders; 54 55 import com.android.car.apps.common.util.CarPackageManagerUtils; 56 import com.android.car.apps.common.util.FutureData; 57 import com.android.car.apps.common.util.VectorMath; 58 import com.android.car.apps.common.util.ViewUtils; 59 import com.android.car.media.common.MediaItemMetadata; 60 import com.android.car.media.common.MinimizedPlaybackControlBar; 61 import com.android.car.media.common.PlaybackErrorsHelper; 62 import com.android.car.media.common.browse.MediaItemsRepository; 63 import com.android.car.media.common.playback.PlaybackViewModel; 64 import com.android.car.media.common.source.MediaSource; 65 import com.android.car.media.common.source.MediaTrampolineHelper; 66 import com.android.car.ui.AlertDialogBuilder; 67 import com.android.car.ui.utils.CarUxRestrictionsUtil; 68 69 import java.util.HashMap; 70 import java.util.Map; 71 import java.util.Objects; 72 import java.util.Stack; 73 74 /** 75 * This activity controls the UI of media. It also updates the connection status for the media app 76 * by broadcast. 77 */ 78 public class MediaActivity extends FragmentActivity implements MediaActivityController.Callbacks { 79 private static final String TAG = "MediaActivity"; 80 81 /** Configuration (controlled from resources) */ 82 private int mFadeDuration; 83 84 /** Models */ 85 private PlaybackViewModel.PlaybackController mPlaybackController; 86 87 /** Layout views */ 88 private PlaybackFragment mPlaybackFragment; 89 private MediaActivityController mMediaActivityController; 90 private MinimizedPlaybackControlBar mMiniPlaybackControls; 91 private ViewGroup mBrowseContainer; 92 private ViewGroup mPlaybackContainer; 93 private ViewGroup mErrorContainer; 94 private ErrorScreenController mErrorController; 95 96 private Toast mToast; 97 private AlertDialog mDialog; 98 99 /** Current state */ 100 private Mode mMode; 101 private boolean mCanShowMiniPlaybackControls; 102 private PlaybackViewModel.PlaybackStateWrapper mCurrentPlaybackStateWrapper; 103 104 private Car mCar; 105 private CarPackageManager mCarPackageManager; 106 107 private float mCloseVectorX; 108 private float mCloseVectorY; 109 private float mCloseVectorNorm; 110 111 112 private PlaybackFragment.PlaybackFragmentListener mPlaybackFragmentListener = 113 () -> changeMode(Mode.BROWSING); 114 115 private MediaTrampolineHelper mMediaTrampoline; 116 117 /** 118 * Possible modes of the application UI 119 * Todo: refactor into non exclusive flags to allow concurrent modes (eg: play details & browse) 120 * (b/179292793). 121 */ 122 enum Mode { 123 /** The user is browsing or searching a media source */ 124 BROWSING, 125 /** The user is interacting with the full screen playback UI */ 126 PLAYBACK, 127 /** There's no browse tree and playback doesn't work. */ 128 FATAL_ERROR 129 } 130 131 132 @Override onCreate(Bundle savedInstanceState)133 protected void onCreate(Bundle savedInstanceState) { 134 super.onCreate(savedInstanceState); 135 setContentView(R.layout.media_activity); 136 137 Resources res = getResources(); 138 mCloseVectorX = res.getFloat(R.dimen.media_activity_close_vector_x); 139 mCloseVectorY = res.getFloat(R.dimen.media_activity_close_vector_y); 140 mCloseVectorNorm = VectorMath.norm2(mCloseVectorX, mCloseVectorY); 141 142 // TODO(b/151174811): Use appropriate modes, instead of just MEDIA_SOURCE_MODE_BROWSE 143 PlaybackViewModel playbackViewModel = getPlaybackViewModel(); 144 ViewModel localViewModel = getInnerViewModel(); 145 // We can't rely on savedInstanceState to determine whether the model has been initialized 146 // as on a config change savedInstanceState != null and the model is initialized, but if 147 // the app was killed by the system then savedInstanceState != null and the model is NOT 148 // initialized... 149 if (localViewModel.needsInitialization()) { 150 localViewModel.init(playbackViewModel); 151 } 152 mMode = localViewModel.getSavedMode(); 153 154 localViewModel.getBrowsedMediaSource().observe(this, this::onMediaSourceChanged); 155 156 mMediaTrampoline = new MediaTrampolineHelper(this); 157 158 mPlaybackFragment = new PlaybackFragment(); 159 mPlaybackFragment.setListener(mPlaybackFragmentListener); 160 161 162 Size maxArtSize = MediaAppConfig.getMediaItemsBitmapMaxSize(this); 163 mMiniPlaybackControls = findViewById(R.id.minimized_playback_controls); 164 mMiniPlaybackControls.setModel(playbackViewModel, this, maxArtSize); 165 mMiniPlaybackControls.setOnClickListener(view -> changeMode(Mode.PLAYBACK)); 166 167 mFadeDuration = res.getInteger(R.integer.new_album_art_fade_in_duration); 168 mBrowseContainer = findViewById(R.id.fragment_container); 169 mErrorContainer = findViewById(R.id.error_container); 170 mPlaybackContainer = findViewById(R.id.playback_container); 171 getSupportFragmentManager().beginTransaction() 172 .replace(R.id.playback_container, mPlaybackFragment) 173 .commit(); 174 175 playbackViewModel.getPlaybackController().observe(this, 176 playbackController -> { 177 if (playbackController != null) playbackController.prepare(); 178 mPlaybackController = playbackController; 179 }); 180 181 playbackViewModel.getPlaybackStateWrapper().observe(this, 182 state -> handlePlaybackState(state, true)); 183 184 mCar = Car.createCar(this); 185 mCarPackageManager = (CarPackageManager) mCar.getCarManager(Car.PACKAGE_SERVICE); 186 187 mMediaActivityController = new MediaActivityController(this, getMediaItemsRepository(), 188 mCarPackageManager, mBrowseContainer); 189 190 mPlaybackContainer.setOnTouchListener(new ClosePlaybackDetector(this)); 191 } 192 193 @Override onNewIntent(Intent intent)194 protected void onNewIntent(Intent intent) { 195 super.onNewIntent(intent); 196 setIntent(intent); // getIntent() should always return the most recent 197 198 if (Log.isLoggable(TAG, Log.DEBUG)) { 199 Log.d(TAG, "onNewIntent: " + intent); 200 } 201 } 202 203 @Override onResume()204 protected void onResume() { 205 super.onResume(); 206 207 Intent intent = getIntent(); 208 if (Log.isLoggable(TAG, Log.DEBUG)) { 209 Log.d(TAG, "onResume intent: " + intent); 210 } 211 212 if (intent != null) { 213 String compName = getIntent().getStringExtra(CarMediaIntents.EXTRA_MEDIA_COMPONENT); 214 ComponentName launchedSourceComp = (compName == null) ? null : 215 ComponentName.unflattenFromString(compName); 216 217 if (launchedSourceComp == null) { 218 // Might happen if there's no media source at all on the system as the 219 // MediaDispatcherActivity always specifies the component otherwise. 220 Log.w(TAG, "launchedSourceComp should almost never be null: " + compName); 221 } 222 223 mMediaTrampoline.setLaunchedMediaSource(launchedSourceComp); 224 225 // Mark the intent as consumed so that coming back from the media app selector doesn't 226 // set the source again. 227 setIntent(null); 228 } 229 } 230 231 @Override onDestroy()232 protected void onDestroy() { 233 mCar.disconnect(); 234 mMediaActivityController.onDestroy(); 235 super.onDestroy(); 236 } 237 isUxRestricted()238 private boolean isUxRestricted() { 239 return CarUxRestrictionsUtil.isRestricted(CarUxRestrictions.UX_RESTRICTIONS_NO_SETUP, 240 CarUxRestrictionsUtil.getInstance(this).getCurrentRestrictions()); 241 } 242 handlePlaybackState(PlaybackViewModel.PlaybackStateWrapper state, boolean ignoreSameState)243 private void handlePlaybackState(PlaybackViewModel.PlaybackStateWrapper state, 244 boolean ignoreSameState) { 245 mErrorsHelper.handlePlaybackState(TAG, state, ignoreSameState); 246 } 247 248 private final PlaybackErrorsHelper mErrorsHelper = new PlaybackErrorsHelper(this) { 249 250 @Override 251 public void handlePlaybackState(@NonNull String tag, 252 PlaybackViewModel.PlaybackStateWrapper state, boolean ignoreSameState) { 253 254 // TODO rethink interactions between customized layouts and dynamic visibility. 255 mCanShowMiniPlaybackControls = (state != null) && state.shouldDisplay(); 256 updateMiniPlaybackControls(true); 257 super.handlePlaybackState(tag, state, ignoreSameState); 258 } 259 260 @Override 261 public void handleNewPlaybackState(String displayedMessage, PendingIntent intent, 262 String label) { 263 maybeCancelToast(); 264 maybeCancelDialog(); 265 266 boolean isFatalError = false; 267 if (!TextUtils.isEmpty(displayedMessage)) { 268 if (mMediaActivityController.browseTreeHasChildren()) { 269 if (intent != null && !isUxRestricted()) { 270 showDialog(intent, displayedMessage, label, 271 getString(android.R.string.cancel)); 272 } else { 273 showToast(displayedMessage); 274 } 275 } else { 276 boolean isDistractionOptimized = 277 intent != null && CarPackageManagerUtils.isDistractionOptimized( 278 mCarPackageManager, intent); 279 getErrorController().setError(displayedMessage, label, intent, 280 isDistractionOptimized); 281 isFatalError = true; 282 } 283 } 284 if (isFatalError) { 285 changeMode(MediaActivity.Mode.FATAL_ERROR); 286 } else if (mMode == MediaActivity.Mode.FATAL_ERROR) { 287 changeMode(MediaActivity.Mode.BROWSING); 288 } 289 } 290 }; 291 getErrorController()292 private ErrorScreenController getErrorController() { 293 if (mErrorController == null) { 294 mErrorController = new ErrorScreenController(this, mCarPackageManager, mErrorContainer); 295 MediaSource mediaSource = getInnerViewModel().getMediaSourceValue(); 296 mErrorController.onMediaSourceChanged(mediaSource); 297 } 298 return mErrorController; 299 } 300 showDialog(PendingIntent intent, String message, String positiveBtnText, String negativeButtonText)301 private void showDialog(PendingIntent intent, String message, String positiveBtnText, 302 String negativeButtonText) { 303 AlertDialogBuilder dialog = new AlertDialogBuilder(this); 304 mDialog = dialog.setMessage(message) 305 .setNegativeButton(negativeButtonText, null) 306 .setPositiveButton(positiveBtnText, (dialogInterface, i) -> { 307 try { 308 intent.send(); 309 } catch (PendingIntent.CanceledException e) { 310 if (Log.isLoggable(TAG, Log.ERROR)) { 311 Log.e(TAG, "Pending intent canceled"); 312 } 313 } 314 }) 315 .show(); 316 } 317 maybeCancelDialog()318 private void maybeCancelDialog() { 319 if (mDialog != null) { 320 mDialog.cancel(); 321 mDialog = null; 322 } 323 } 324 showToast(String message)325 private void showToast(String message) { 326 mToast = Toast.makeText(this, message, Toast.LENGTH_LONG); 327 mToast.show(); 328 } 329 maybeCancelToast()330 private void maybeCancelToast() { 331 if (mToast != null) { 332 mToast.cancel(); 333 mToast = null; 334 } 335 } 336 337 @Override onBackPressed()338 public void onBackPressed() { 339 switch (mMode) { 340 case PLAYBACK: 341 changeMode(Mode.BROWSING); 342 break; 343 case BROWSING: 344 boolean handled = mMediaActivityController.onBackPressed(); 345 if (handled) return; 346 // Fall through. 347 case FATAL_ERROR: 348 default: 349 super.onBackPressed(); 350 } 351 } 352 353 /** 354 * Sets the media source being browsed. 355 * 356 * @param futureSource contains the new media source we are going to try to browse, as well as 357 * the old one (either could be null). 358 */ onMediaSourceChanged(FutureData<MediaSource> futureSource)359 private void onMediaSourceChanged(FutureData<MediaSource> futureSource) { 360 361 MediaSource newMediaSource = FutureData.getData(futureSource); 362 MediaSource oldMediaSource = FutureData.getPastData(futureSource); 363 364 if (mErrorController != null) { 365 mErrorController.onMediaSourceChanged(newMediaSource); 366 } 367 368 mCurrentPlaybackStateWrapper = null; 369 maybeCancelToast(); 370 maybeCancelDialog(); 371 if (newMediaSource != null) { 372 if (Log.isLoggable(TAG, Log.INFO)) { 373 Log.i(TAG, "Browsing: " + newMediaSource.getDisplayName()); 374 } 375 376 if (Objects.equals(oldMediaSource, newMediaSource)) { 377 // The UI is being restored (eg: after a config change) => restore the mode. 378 Mode mediaSourceMode = getInnerViewModel().getSavedMode(); 379 changeModeInternal(mediaSourceMode, false); 380 } else { 381 // Change the mode regardless of its previous value to update the views. 382 // The saved mode is ignored as the media apps don't always recreate a playback 383 // state that can be displayed (and some send a displayable state after sending a 384 // non displayable one...). 385 changeModeInternal(Mode.BROWSING, false); 386 } 387 } 388 } 389 changeMode(Mode mode)390 private void changeMode(Mode mode) { 391 if (mMode == mode) { 392 if (Log.isLoggable(TAG, Log.INFO)) { 393 Log.i(TAG, "Mode " + mMode + " change is ignored"); 394 } 395 return; 396 } 397 changeModeInternal(mode, true); 398 } 399 changeModeInternal(Mode mode, boolean hideViewAnimated)400 private void changeModeInternal(Mode mode, boolean hideViewAnimated) { 401 if (Log.isLoggable(TAG, Log.INFO)) { 402 Log.i(TAG, "Changing mode from: " + mMode + " to: " + mode); 403 } 404 int fadeOutDuration = hideViewAnimated ? mFadeDuration : 0; 405 406 Mode oldMode = mMode; 407 getInnerViewModel().saveMode(mode); 408 mMode = mode; 409 410 mPlaybackFragment.closeOverflowMenu(); 411 updateMiniPlaybackControls(hideViewAnimated); 412 413 switch (mMode) { 414 case FATAL_ERROR: 415 ViewUtils.showViewAnimated(mErrorContainer, mFadeDuration); 416 ViewUtils.hideViewAnimated(mPlaybackContainer, fadeOutDuration); 417 ViewUtils.hideViewAnimated(mBrowseContainer, fadeOutDuration); 418 break; 419 case PLAYBACK: 420 mPlaybackContainer.setX(0); 421 mPlaybackContainer.setY(0); 422 mPlaybackContainer.setAlpha(0f); 423 ViewUtils.hideViewAnimated(mErrorContainer, fadeOutDuration); 424 ViewUtils.showViewAnimated(mPlaybackContainer, mFadeDuration); 425 ViewUtils.hideViewAnimated(mBrowseContainer, fadeOutDuration); 426 break; 427 case BROWSING: 428 if (oldMode == Mode.PLAYBACK) { 429 // When switching from PLAYBACK mode to BROWSING mode, if a CarUiRecyclerView 430 // shows up and it's in rotary mode, restore focus in the CarUiRecyclerView. 431 mMediaActivityController.restoreFocusInCurrentNode(); 432 433 ViewUtils.hideViewAnimated(mErrorContainer, 0); 434 ViewUtils.showViewAnimated(mBrowseContainer, 0); 435 animateOutPlaybackContainer(fadeOutDuration); 436 } else { 437 ViewUtils.hideViewAnimated(mErrorContainer, fadeOutDuration); 438 ViewUtils.hideViewAnimated(mPlaybackContainer, fadeOutDuration); 439 ViewUtils.showViewAnimated(mBrowseContainer, mFadeDuration); 440 } 441 break; 442 } 443 } 444 animateOutPlaybackContainer(int fadeOutDuration)445 private void animateOutPlaybackContainer(int fadeOutDuration) { 446 if (mCloseVectorNorm <= EPSILON) { 447 ViewUtils.hideViewAnimated(mPlaybackContainer, fadeOutDuration); 448 return; 449 } 450 451 // Assumption: mPlaybackContainer shares 1 edge with the side of the screen the 452 // slide animation brings it towards to. Since only vertical and horizontal translations 453 // are supported mPlaybackContainer only needs to move by its width or its height to be 454 // hidden. 455 456 // Use width and height with and extra pixel for safety. 457 float w = mPlaybackContainer.getWidth() + 1; 458 float h = mPlaybackContainer.getHeight() + 1; 459 460 float tX = 0.0f; 461 float tY = 0.0f; 462 if (Math.abs(mCloseVectorY) <= EPSILON) { 463 // Only moving horizontally 464 tX = mCloseVectorX * w / mCloseVectorNorm; 465 } else if (Math.abs(mCloseVectorX) <= EPSILON) { 466 // Only moving vertically 467 tY = mCloseVectorY * h / mCloseVectorNorm; 468 } else { 469 if (Log.isLoggable(TAG, Log.DEBUG)) { 470 Log.d(TAG, "The vector to close the playback container must be vertical or" 471 + " horizontal"); 472 } 473 ViewUtils.hideViewAnimated(mPlaybackContainer, fadeOutDuration); 474 return; 475 } 476 477 mPlaybackContainer.animate() 478 .translationX(tX) 479 .translationY(tY) 480 .setDuration(fadeOutDuration) 481 .setListener(ViewUtils.hideViewAfterAnimation(mPlaybackContainer)) 482 .start(); 483 } 484 updateMiniPlaybackControls(boolean hideViewAnimated)485 private void updateMiniPlaybackControls(boolean hideViewAnimated) { 486 int fadeOutDuration = hideViewAnimated ? mFadeDuration : 0; 487 // Minimized control bar should be hidden in playback view. 488 final boolean shouldShowMiniPlaybackControls = 489 getResources().getBoolean(R.bool.show_mini_playback_controls) 490 && mCanShowMiniPlaybackControls 491 && mMode != Mode.PLAYBACK; 492 if (shouldShowMiniPlaybackControls) { 493 Boolean visible = getInnerViewModel().getMiniControlsVisible().getValue(); 494 if (visible != Boolean.TRUE) { 495 ViewUtils.showViewAnimated(mMiniPlaybackControls, mFadeDuration); 496 } 497 } else { 498 ViewUtils.hideViewAnimated(mMiniPlaybackControls, fadeOutDuration); 499 } 500 getInnerViewModel().setMiniControlsVisible(shouldShowMiniPlaybackControls); 501 } 502 503 @Override onPlayableItemClicked(@onNull MediaItemMetadata item)504 public void onPlayableItemClicked(@NonNull MediaItemMetadata item) { 505 mPlaybackController.playItem(item); 506 boolean switchToPlayback = getResources().getBoolean( 507 R.bool.switch_to_playback_view_when_playable_item_is_clicked); 508 if (switchToPlayback) { 509 changeMode(Mode.PLAYBACK); 510 } 511 setIntent(null); 512 } 513 514 @Override onRootLoaded()515 public void onRootLoaded() { 516 PlaybackViewModel playbackViewModel = getPlaybackViewModel(); 517 handlePlaybackState(playbackViewModel.getPlaybackStateWrapper().getValue(), false); 518 } 519 520 @Override getActivity()521 public FragmentActivity getActivity() { 522 return this; 523 } 524 getMediaItemsRepository()525 private MediaItemsRepository getMediaItemsRepository() { 526 return MediaItemsRepository.get(getApplication(), MEDIA_SOURCE_MODE_BROWSE); 527 } 528 getPlaybackViewModel()529 private PlaybackViewModel getPlaybackViewModel() { 530 return PlaybackViewModel.get(getApplication(), MEDIA_SOURCE_MODE_BROWSE); 531 } 532 getInnerViewModel()533 private ViewModel getInnerViewModel() { 534 return ViewModelProviders.of(this).get(ViewModel.class); 535 } 536 537 public static class ViewModel extends AndroidViewModel { 538 539 static class MediaServiceState { 540 Mode mMode = Mode.BROWSING; 541 Stack<MediaItemMetadata> mBrowseStack = new Stack<>(); 542 Stack<MediaItemMetadata> mSearchStack = new Stack<>(); 543 /** True when the search bar has been opened or when the search results are browsed. */ 544 boolean mSearching; 545 /** True iif the list of search results is being shown (implies mIsSearching). */ 546 boolean mShowingSearchResults; 547 String mSearchQuery; 548 boolean mQueueVisible = false; 549 } 550 551 private boolean mNeedsInitialization = true; 552 private PlaybackViewModel mPlaybackViewModel; 553 private final MutableLiveData<FutureData<MediaSource>> mBrowsedMediaSource = 554 dataOf(FutureData.newLoadingData()); 555 private final Map<MediaSource, MediaServiceState> mStates = new HashMap<>(); 556 private MutableLiveData<Boolean> mIsMiniControlsVisible = new MutableLiveData<>(); 557 ViewModel(@onNull Application application)558 public ViewModel(@NonNull Application application) { 559 super(application); 560 } 561 init(@onNull PlaybackViewModel playbackViewModel)562 void init(@NonNull PlaybackViewModel playbackViewModel) { 563 if (mPlaybackViewModel == playbackViewModel) { 564 return; 565 } 566 mPlaybackViewModel = playbackViewModel; 567 mNeedsInitialization = false; 568 } 569 needsInitialization()570 boolean needsInitialization() { 571 return mNeedsInitialization; 572 } 573 setMiniControlsVisible(boolean visible)574 void setMiniControlsVisible(boolean visible) { 575 mIsMiniControlsVisible.setValue(visible); 576 } 577 getMiniControlsVisible()578 LiveData<Boolean> getMiniControlsVisible() { 579 return mIsMiniControlsVisible; 580 } 581 582 @Nullable getMediaSourceValue()583 MediaSource getMediaSourceValue() { 584 return FutureData.getData(mBrowsedMediaSource.getValue()); 585 } 586 getSavedState()587 MediaServiceState getSavedState() { 588 MediaSource source = getMediaSourceValue(); 589 MediaServiceState state = mStates.get(source); 590 if (state == null) { 591 state = new MediaServiceState(); 592 mStates.put(source, state); 593 } 594 return state; 595 } 596 saveMode(Mode mode)597 void saveMode(Mode mode) { 598 getSavedState().mMode = mode; 599 } 600 getSavedMode()601 Mode getSavedMode() { 602 return getSavedState().mMode; 603 } 604 605 @Nullable getSelectedTab()606 MediaItemMetadata getSelectedTab() { 607 Stack<MediaItemMetadata> stack = getSavedState().mBrowseStack; 608 return (stack != null && !stack.empty()) ? stack.firstElement() : null; 609 } 610 setQueueVisible(boolean visible)611 void setQueueVisible(boolean visible) { 612 getSavedState().mQueueVisible = visible; 613 } 614 getQueueVisible()615 boolean getQueueVisible() { 616 return getSavedState().mQueueVisible; 617 } 618 saveBrowsedMediaSource(MediaSource mediaSource)619 void saveBrowsedMediaSource(MediaSource mediaSource) { 620 Resources res = getApplication().getResources(); 621 if (MediaDispatcherActivity.isCustomMediaSource(res, mediaSource)) { 622 Log.i(TAG, "Ignoring custom media source: " + mediaSource); 623 return; 624 } 625 MediaSource oldSource = getMediaSourceValue(); 626 if (Log.isLoggable(TAG, Log.INFO)) { 627 Log.i(TAG, "MediaSource changed from " + oldSource + " to " + mediaSource); 628 } 629 mBrowsedMediaSource.setValue(FutureData.newLoadedData(oldSource, mediaSource)); 630 } 631 getBrowsedMediaSource()632 LiveData<FutureData<MediaSource>> getBrowsedMediaSource() { 633 return mBrowsedMediaSource; 634 } 635 getBrowseStack()636 @NonNull Stack<MediaItemMetadata> getBrowseStack() { 637 return getSavedState().mBrowseStack; 638 } 639 getSearchStack()640 @NonNull Stack<MediaItemMetadata> getSearchStack() { 641 return getSavedState().mSearchStack; 642 } 643 644 /** Returns whether search mode is on (showing search results or browsing them). */ isSearching()645 boolean isSearching() { 646 return getSavedState().mSearching; 647 } 648 isShowingSearchResults()649 boolean isShowingSearchResults() { 650 return getSavedState().mShowingSearchResults; 651 } 652 getSearchQuery()653 String getSearchQuery() { 654 return getSavedState().mSearchQuery; 655 } 656 setSearching(boolean isSearching)657 void setSearching(boolean isSearching) { 658 getSavedState().mSearching = isSearching; 659 } 660 setShowingSearchResults(boolean isShowing)661 void setShowingSearchResults(boolean isShowing) { 662 getSavedState().mShowingSearchResults = isShowing; 663 } 664 setSearchQuery(String searchQuery)665 void setSearchQuery(String searchQuery) { 666 getSavedState().mSearchQuery = searchQuery; 667 } 668 } 669 670 private class ClosePlaybackDetector extends GestureDetector.SimpleOnGestureListener 671 implements View.OnTouchListener { 672 673 private static final float COS_30 = 0.866f; 674 675 private final ViewConfiguration mViewConfig; 676 private final GestureDetectorCompat mDetector; 677 678 ClosePlaybackDetector(Context context)679 ClosePlaybackDetector(Context context) { 680 mViewConfig = ViewConfiguration.get(context); 681 mDetector = new GestureDetectorCompat(context, this); 682 } 683 684 @SuppressLint("ClickableViewAccessibility") 685 @Override onTouch(View v, MotionEvent event)686 public boolean onTouch(View v, MotionEvent event) { 687 return mDetector.onTouchEvent(event); 688 } 689 690 @Override onDown(MotionEvent event)691 public boolean onDown(MotionEvent event) { 692 return (mMode == Mode.PLAYBACK) && (mCloseVectorNorm > EPSILON); 693 } 694 695 @Override onFling(MotionEvent e1, MotionEvent e2, float vX, float vY)696 public boolean onFling(MotionEvent e1, MotionEvent e2, float vX, float vY) { 697 float moveX = e2.getX() - e1.getX(); 698 float moveY = e2.getY() - e1.getY(); 699 float moveVectorNorm = VectorMath.norm2(moveX, moveY); 700 if (moveVectorNorm > mViewConfig.getScaledTouchSlop() && 701 VectorMath.norm2(vX, vY) > mViewConfig.getScaledMinimumFlingVelocity()) { 702 float dot = VectorMath.dotProduct(mCloseVectorX, mCloseVectorY, moveX, moveY); 703 float cos = dot / (mCloseVectorNorm * moveVectorNorm); 704 if (cos >= COS_30) { // Accept 30 degrees on each side of the close vector. 705 changeMode(Mode.BROWSING); 706 } 707 } 708 return true; 709 } 710 } 711 } 712