1 /* 2 * Copyright 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.car.media; 18 19 import static android.car.media.CarMediaManager.MEDIA_SOURCE_MODE_BROWSE; 20 21 import android.content.Context; 22 import android.content.res.ColorStateList; 23 import android.content.res.Resources; 24 import android.graphics.PorterDuff; 25 import android.graphics.Rect; 26 import android.graphics.drawable.BitmapDrawable; 27 import android.graphics.drawable.Drawable; 28 import android.graphics.drawable.LayerDrawable; 29 import android.os.Bundle; 30 import android.util.Log; 31 import android.util.Size; 32 import android.view.LayoutInflater; 33 import android.view.View; 34 import android.view.ViewGroup; 35 import android.widget.ImageView; 36 import android.widget.SeekBar; 37 import android.widget.TextView; 38 39 import androidx.annotation.NonNull; 40 import androidx.annotation.Nullable; 41 import androidx.fragment.app.Fragment; 42 import androidx.lifecycle.ViewModelProviders; 43 import androidx.recyclerview.widget.DefaultItemAnimator; 44 import androidx.recyclerview.widget.RecyclerView; 45 46 import com.android.car.apps.common.BackgroundImageView; 47 import com.android.car.apps.common.imaging.ImageBinder; 48 import com.android.car.apps.common.imaging.ImageBinder.PlaceholderType; 49 import com.android.car.apps.common.imaging.ImageViewBinder; 50 import com.android.car.apps.common.util.ViewUtils; 51 import com.android.car.media.common.MediaItemMetadata; 52 import com.android.car.media.common.MetadataController; 53 import com.android.car.media.common.PlaybackControlsActionBar; 54 import com.android.car.media.common.playback.PlaybackViewModel; 55 import com.android.car.media.common.source.MediaSourceViewModel; 56 import com.android.car.media.widgets.AppBarController; 57 import com.android.car.ui.core.CarUi; 58 import com.android.car.ui.recyclerview.CarUiRecyclerView; 59 import com.android.car.ui.recyclerview.ContentLimiting; 60 import com.android.car.ui.recyclerview.ScrollingLimitedViewHolder; 61 import com.android.car.ui.toolbar.MenuItem; 62 import com.android.car.ui.toolbar.NavButtonMode; 63 import com.android.car.ui.toolbar.ToolbarController; 64 import com.android.car.ui.utils.DirectManipulationHelper; 65 import com.android.car.uxr.LifeCycleObserverUxrContentLimiter; 66 import com.android.car.uxr.UxrContentLimiterImpl; 67 68 import java.util.ArrayList; 69 import java.util.Collections; 70 import java.util.List; 71 import java.util.Objects; 72 73 74 /** 75 * A {@link Fragment} that implements both the playback and the content forward browsing experience. 76 * It observes a {@link PlaybackViewModel} and updates its information depending on the currently 77 * playing media source through the {@link android.media.session.MediaSession} API. 78 */ 79 public class PlaybackFragment extends Fragment { 80 private static final String TAG = "PlaybackFragment"; 81 82 private LifeCycleObserverUxrContentLimiter mUxrContentLimiter; 83 private ImageBinder<MediaItemMetadata.ArtworkRef> mAlbumArtBinder; 84 private AppBarController mAppBarController; 85 private BackgroundImageView mAlbumBackground; 86 private View mBackgroundScrim; 87 private View mControlBarScrim; 88 private PlaybackControlsActionBar mPlaybackControls; 89 private QueueItemsAdapter mQueueAdapter; 90 private CarUiRecyclerView mQueue; 91 private ViewGroup mSeekBarContainer; 92 private SeekBar mSeekBar; 93 private List<View> mViewsToHideForCustomActions; 94 private List<View> mViewsToHideWhenQueueIsVisible; 95 private List<View> mViewsToShowWhenQueueIsVisible; 96 private List<View> mViewsToHideImmediatelyWhenQueueIsVisible; 97 private List<View> mViewsToShowImmediatelyWhenQueueIsVisible; 98 99 private DefaultItemAnimator mItemAnimator; 100 101 private MetadataController mMetadataController; 102 103 private PlaybackFragmentListener mListener; 104 105 private PlaybackViewModel.PlaybackController mController; 106 private Long mActiveQueueItemId; 107 108 private boolean mHasQueue; 109 private boolean mQueueIsVisible; 110 private boolean mShowTimeForActiveQueueItem; 111 private boolean mShowIconForActiveQueueItem; 112 private boolean mShowThumbnailForQueueItem; 113 private boolean mShowSubtitleForQueueItem; 114 115 private boolean mShowLinearProgressBar; 116 117 private int mFadeDuration; 118 119 private MediaActivity.ViewModel mViewModel; 120 121 private MenuItem mQueueMenuItem; 122 123 /** 124 * PlaybackFragment listener 125 */ 126 public interface PlaybackFragmentListener { 127 /** 128 * Invoked when the user clicks on the collapse button 129 */ onCollapse()130 void onCollapse(); 131 } 132 133 public class QueueViewHolder extends RecyclerView.ViewHolder { 134 135 private final View mView; 136 private final ViewGroup mThumbnailContainer; 137 private final ImageView mThumbnail; 138 private final View mSpacer; 139 private final TextView mTitle; 140 private final TextView mSubtitle; 141 private final TextView mCurrentTime; 142 private final TextView mMaxTime; 143 private final TextView mTimeSeparator; 144 private final ImageView mActiveIcon; 145 146 private final ImageViewBinder<MediaItemMetadata.ArtworkRef> mThumbnailBinder; 147 QueueViewHolder(View itemView)148 QueueViewHolder(View itemView) { 149 super(itemView); 150 mView = itemView; 151 mThumbnailContainer = itemView.findViewById(R.id.thumbnail_container); 152 mThumbnail = itemView.findViewById(R.id.thumbnail); 153 mSpacer = itemView.findViewById(R.id.spacer); 154 mTitle = itemView.findViewById(R.id.queue_list_item_title); 155 mSubtitle = itemView.findViewById(R.id.queue_list_item_subtitle); 156 mCurrentTime = itemView.findViewById(R.id.current_time); 157 mMaxTime = itemView.findViewById(R.id.max_time); 158 mTimeSeparator = itemView.findViewById(R.id.separator); 159 mActiveIcon = itemView.findViewById(R.id.now_playing_icon); 160 161 Size maxArtSize = MediaAppConfig.getMediaItemsBitmapMaxSize(itemView.getContext()); 162 mThumbnailBinder = new ImageViewBinder<>(maxArtSize, mThumbnail); 163 } 164 bind(MediaItemMetadata item)165 void bind(MediaItemMetadata item) { 166 mView.setOnClickListener(v -> onQueueItemClicked(item)); 167 168 ViewUtils.setVisible(mThumbnailContainer, mShowThumbnailForQueueItem); 169 if (mShowThumbnailForQueueItem) { 170 Context context = mView.getContext(); 171 mThumbnailBinder.setImage(context, item != null ? item.getArtworkKey() : null); 172 } 173 174 ViewUtils.setVisible(mSpacer, !mShowThumbnailForQueueItem); 175 176 mTitle.setText(item.getTitle()); 177 178 boolean active = mActiveQueueItemId != null && Objects.equals(mActiveQueueItemId, 179 item.getQueueId()); 180 if (active) { 181 mCurrentTime.setText(mQueueAdapter.getCurrentTime()); 182 mMaxTime.setText(mQueueAdapter.getMaxTime()); 183 } 184 boolean shouldShowTime = 185 mShowTimeForActiveQueueItem && active && mQueueAdapter.getTimeVisible(); 186 ViewUtils.setVisible(mCurrentTime, shouldShowTime); 187 ViewUtils.setVisible(mMaxTime, shouldShowTime); 188 ViewUtils.setVisible(mTimeSeparator, shouldShowTime); 189 190 mView.setSelected(active); 191 192 boolean shouldShowIcon = mShowIconForActiveQueueItem && active; 193 ViewUtils.setVisible(mActiveIcon, shouldShowIcon); 194 195 if (mShowSubtitleForQueueItem) { 196 mSubtitle.setText(item.getSubtitle()); 197 } 198 } 199 onViewAttachedToWindow()200 void onViewAttachedToWindow() { 201 if (mShowThumbnailForQueueItem) { 202 Context context = mView.getContext(); 203 mThumbnailBinder.maybeRestartLoading(context); 204 } 205 } 206 onViewDetachedFromWindow()207 void onViewDetachedFromWindow() { 208 if (mShowThumbnailForQueueItem) { 209 Context context = mView.getContext(); 210 mThumbnailBinder.maybeCancelLoading(context); 211 } 212 } 213 } 214 215 216 private class QueueItemsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> 217 implements ContentLimiting { 218 219 private static final int CLAMPED_MESSAGE_VIEW_TYPE = -1; 220 private static final int QUEUE_ITEM_VIEW_TYPE = 0; 221 222 private UxrPivotFilter mUxrPivotFilter; 223 private List<MediaItemMetadata> mQueueItems = Collections.emptyList(); 224 private String mCurrentTimeText = ""; 225 private String mMaxTimeText = ""; 226 /** Index in {@link #mQueueItems}. */ 227 private Integer mActiveItemIndex; 228 private boolean mTimeVisible; 229 private Integer mScrollingLimitedMessageResId; 230 QueueItemsAdapter()231 QueueItemsAdapter() { 232 mUxrPivotFilter = UxrPivotFilter.PASS_THROUGH; 233 } 234 235 @Override setMaxItems(int maxItems)236 public void setMaxItems(int maxItems) { 237 if (maxItems >= 0) { 238 mUxrPivotFilter = new UxrPivotFilterImpl(this, maxItems); 239 } else { 240 mUxrPivotFilter = UxrPivotFilter.PASS_THROUGH; 241 } 242 applyFilterToQueue(); 243 } 244 245 @Override setScrollingLimitedMessageResId(int resId)246 public void setScrollingLimitedMessageResId(int resId) { 247 if (mScrollingLimitedMessageResId == null || mScrollingLimitedMessageResId != resId) { 248 mScrollingLimitedMessageResId = resId; 249 mUxrPivotFilter.invalidateMessagePositions(); 250 } 251 } 252 253 @Override getConfigurationId()254 public int getConfigurationId() { 255 return R.id.playback_fragment_now_playing_list_uxr_config; 256 } 257 setItems(@ullable List<MediaItemMetadata> items)258 void setItems(@Nullable List<MediaItemMetadata> items) { 259 List<MediaItemMetadata> newQueueItems = 260 new ArrayList<>(items != null ? items : Collections.emptyList()); 261 if (newQueueItems.equals(mQueueItems)) { 262 return; 263 } 264 mQueueItems = newQueueItems; 265 updateActiveItem(/* listIsNew */ true); 266 } 267 getActiveItemIndex()268 private int getActiveItemIndex() { 269 return mActiveItemIndex != null ? mActiveItemIndex : 0; 270 } 271 getQueueSize()272 private int getQueueSize() { 273 return (mQueueItems != null) ? mQueueItems.size() : 0; 274 } 275 276 277 /** 278 * Returns the position of the active item if there is one, otherwise returns 279 * @link UxrPivotFilter#INVALID_POSITION}. 280 */ getActiveItemPosition()281 private int getActiveItemPosition() { 282 if (mActiveItemIndex == null) { 283 return UxrPivotFilter.INVALID_POSITION; 284 } 285 return mUxrPivotFilter.indexToPosition(mActiveItemIndex); 286 } 287 invalidateActiveItemPosition()288 private void invalidateActiveItemPosition() { 289 int position = getActiveItemPosition(); 290 if (position != UxrPivotFilterImpl.INVALID_POSITION) { 291 notifyItemChanged(position); 292 } 293 } 294 scrollToActiveItemPosition()295 private void scrollToActiveItemPosition() { 296 int position = getActiveItemPosition(); 297 if (position != UxrPivotFilterImpl.INVALID_POSITION) { 298 mQueue.scrollToPosition(position); 299 } 300 } 301 applyFilterToQueue()302 private void applyFilterToQueue() { 303 mUxrPivotFilter.recompute(getQueueSize(), getActiveItemIndex()); 304 notifyDataSetChanged(); 305 } 306 307 // Updates mActiveItemPos, then scrolls the queue to mActiveItemPos. 308 // It should be called when the active item (mActiveQueueItemId) changed or 309 // the queue items (mQueueItems) changed. updateActiveItem(boolean listIsNew)310 void updateActiveItem(boolean listIsNew) { 311 if (mQueueItems == null || mActiveQueueItemId == null) { 312 mActiveItemIndex = null; 313 applyFilterToQueue(); 314 return; 315 } 316 Integer activeItemPos = null; 317 for (int i = 0; i < mQueueItems.size(); i++) { 318 if (Objects.equals(mQueueItems.get(i).getQueueId(), mActiveQueueItemId)) { 319 activeItemPos = i; 320 break; 321 } 322 } 323 324 // Invalidate the previous active item so it gets redrawn as a normal one. 325 invalidateActiveItemPosition(); 326 327 mActiveItemIndex = activeItemPos; 328 if (listIsNew) { 329 applyFilterToQueue(); 330 } else { 331 mUxrPivotFilter.updatePivotIndex(getActiveItemIndex()); 332 } 333 334 scrollToActiveItemPosition(); 335 invalidateActiveItemPosition(); 336 } 337 setCurrentTime(String currentTime)338 void setCurrentTime(String currentTime) { 339 if (!mCurrentTimeText.equals(currentTime)) { 340 mCurrentTimeText = currentTime; 341 invalidateActiveItemPosition(); 342 } 343 } 344 setMaxTime(String maxTime)345 void setMaxTime(String maxTime) { 346 if (!mMaxTimeText.equals(maxTime)) { 347 mMaxTimeText = maxTime; 348 invalidateActiveItemPosition(); 349 } 350 } 351 setTimeVisible(boolean visible)352 void setTimeVisible(boolean visible) { 353 if (mTimeVisible != visible) { 354 mTimeVisible = visible; 355 invalidateActiveItemPosition(); 356 } 357 } 358 getCurrentTime()359 String getCurrentTime() { 360 return mCurrentTimeText; 361 } 362 getMaxTime()363 String getMaxTime() { 364 return mMaxTimeText; 365 } 366 getTimeVisible()367 boolean getTimeVisible() { 368 return mTimeVisible; 369 } 370 371 @Override getItemViewType(int position)372 public final int getItemViewType(int position) { 373 if (mUxrPivotFilter.positionToIndex(position) == UxrPivotFilterImpl.INVALID_INDEX) { 374 return CLAMPED_MESSAGE_VIEW_TYPE; 375 } else { 376 return QUEUE_ITEM_VIEW_TYPE; 377 } 378 } 379 380 @Override onCreateViewHolder(@onNull ViewGroup parent, int viewType)381 public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 382 if (viewType == CLAMPED_MESSAGE_VIEW_TYPE) { 383 return ScrollingLimitedViewHolder.create(parent); 384 } 385 LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 386 return new QueueViewHolder(inflater.inflate(R.layout.queue_list_item, parent, false)); 387 } 388 389 @Override onBindViewHolder(RecyclerView.ViewHolder vh, int position)390 public void onBindViewHolder(RecyclerView.ViewHolder vh, int position) { 391 if (vh instanceof QueueViewHolder) { 392 int index = mUxrPivotFilter.positionToIndex(position); 393 if (index != UxrPivotFilterImpl.INVALID_INDEX) { 394 int size = mQueueItems.size(); 395 if (0 <= index && index < size) { 396 QueueViewHolder holder = (QueueViewHolder) vh; 397 holder.bind(mQueueItems.get(index)); 398 } else { 399 Log.e(TAG, "onBindViewHolder pos: " + position + " gave index: " + index + 400 " out of bounds size: " + size + " " + mUxrPivotFilter.toString()); 401 } 402 } else { 403 Log.e(TAG, "onBindViewHolder invalid position " + position + " " + 404 mUxrPivotFilter.toString()); 405 } 406 } else if (vh instanceof ScrollingLimitedViewHolder) { 407 ScrollingLimitedViewHolder holder = (ScrollingLimitedViewHolder) vh; 408 holder.bind(mScrollingLimitedMessageResId); 409 } else { 410 throw new IllegalArgumentException("unknown holder class " + vh.getClass()); 411 } 412 } 413 414 @Override onViewAttachedToWindow(@onNull RecyclerView.ViewHolder vh)415 public void onViewAttachedToWindow(@NonNull RecyclerView.ViewHolder vh) { 416 super.onViewAttachedToWindow(vh); 417 if (vh instanceof QueueViewHolder) { 418 QueueViewHolder holder = (QueueViewHolder) vh; 419 holder.onViewAttachedToWindow(); 420 } 421 } 422 423 @Override onViewDetachedFromWindow(@onNull RecyclerView.ViewHolder vh)424 public void onViewDetachedFromWindow(@NonNull RecyclerView.ViewHolder vh) { 425 super.onViewDetachedFromWindow(vh); 426 if (vh instanceof QueueViewHolder) { 427 QueueViewHolder holder = (QueueViewHolder) vh; 428 holder.onViewDetachedFromWindow(); 429 } 430 } 431 432 @Override getItemCount()433 public int getItemCount() { 434 return mUxrPivotFilter.getFilteredCount(); 435 } 436 437 @Override getItemId(int position)438 public long getItemId(int position) { 439 int index = mUxrPivotFilter.positionToIndex(position); 440 if (index != UxrPivotFilterImpl.INVALID_INDEX) { 441 return mQueueItems.get(position).getQueueId(); 442 } else { 443 return RecyclerView.NO_ID; 444 } 445 } 446 } 447 448 private static class QueueTopItemDecoration extends RecyclerView.ItemDecoration { 449 int mHeight; 450 int mDecorationPosition; 451 QueueTopItemDecoration(int height, int decorationPosition)452 QueueTopItemDecoration(int height, int decorationPosition) { 453 mHeight = height; 454 mDecorationPosition = decorationPosition; 455 } 456 457 @Override getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)458 public void getItemOffsets(Rect outRect, View view, RecyclerView parent, 459 RecyclerView.State state) { 460 super.getItemOffsets(outRect, view, parent, state); 461 if (parent.getChildAdapterPosition(view) == mDecorationPosition) { 462 outRect.top = mHeight; 463 } 464 } 465 } 466 467 @Override onCreateView(@onNull LayoutInflater inflater, final ViewGroup container, Bundle savedInstanceState)468 public View onCreateView(@NonNull LayoutInflater inflater, final ViewGroup container, 469 Bundle savedInstanceState) { 470 return inflater.inflate(R.layout.fragment_playback, container, false); 471 } 472 473 @Override onViewCreated(@onNull View view, @Nullable Bundle savedInstanceState)474 public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 475 mAlbumBackground = view.findViewById(R.id.playback_background); 476 mQueue = view.findViewById(R.id.queue_list); 477 mSeekBarContainer = view.findViewById(R.id.playback_seek_bar_container); 478 mSeekBar = view.findViewById(R.id.playback_seek_bar); 479 DirectManipulationHelper.setSupportsRotateDirectly(mSeekBar, true); 480 481 GuidelinesUpdater updater = new GuidelinesUpdater(view); 482 ToolbarController toolbarController = CarUi.installBaseLayoutAround(view, updater, true); 483 mAppBarController = new AppBarController(view.getContext(), toolbarController); 484 485 mAppBarController.setTitle(R.string.fragment_playback_title); 486 mAppBarController.setBackgroundShown(false); 487 mAppBarController.setNavButtonMode(NavButtonMode.DOWN); 488 489 // Update toolbar's logo 490 MediaSourceViewModel mediaSourceViewModel = getMediaSourceViewModel(); 491 mediaSourceViewModel.getPrimaryMediaSource().observe(this, mediaSource -> 492 mAppBarController.setLogo(mediaSource != null 493 ? new BitmapDrawable(getResources(), mediaSource.getCroppedPackageIcon()) 494 : null)); 495 496 mBackgroundScrim = view.findViewById(R.id.background_scrim); 497 ViewUtils.setVisible(mBackgroundScrim, false); 498 mControlBarScrim = view.findViewById(R.id.control_bar_scrim); 499 if (mControlBarScrim != null) { 500 ViewUtils.setVisible(mControlBarScrim, false); 501 mControlBarScrim.setOnClickListener(scrim -> mPlaybackControls.close()); 502 mControlBarScrim.setClickable(false); 503 } 504 505 Resources res = getResources(); 506 mShowTimeForActiveQueueItem = res.getBoolean( 507 R.bool.show_time_for_now_playing_queue_list_item); 508 mShowIconForActiveQueueItem = res.getBoolean( 509 R.bool.show_icon_for_now_playing_queue_list_item); 510 mShowThumbnailForQueueItem = getContext().getResources().getBoolean( 511 R.bool.show_thumbnail_for_queue_list_item); 512 mShowLinearProgressBar = getContext().getResources().getBoolean( 513 R.bool.show_linear_progress_bar); 514 mShowSubtitleForQueueItem = getContext().getResources().getBoolean( 515 R.bool.show_subtitle_for_queue_list_item); 516 517 if (mSeekBar != null) { 518 if (mShowLinearProgressBar) { 519 boolean useMediaSourceColor = res.getBoolean( 520 R.bool.use_media_source_color_for_progress_bar); 521 int defaultColor = res.getColor(R.color.progress_bar_highlight, null); 522 if (useMediaSourceColor) { 523 getPlaybackViewModel().getMediaSourceColors().observe(getViewLifecycleOwner(), 524 sourceColors -> { 525 int color = sourceColors != null 526 ? sourceColors.getAccentColor(defaultColor) 527 : defaultColor; 528 setSeekBarColor(color); 529 }); 530 } else { 531 setSeekBarColor(defaultColor); 532 } 533 } else { 534 mSeekBar.setVisibility(View.GONE); 535 } 536 } 537 538 mViewModel = ViewModelProviders.of(requireActivity()).get(MediaActivity.ViewModel.class); 539 540 getPlaybackViewModel().getPlaybackController().observe(getViewLifecycleOwner(), 541 controller -> mController = controller); 542 initPlaybackControls(view.findViewById(R.id.playback_controls)); 543 initMetadataController(view); 544 initQueue(); 545 546 // Don't update the visibility of seekBar if show_linear_progress_bar is false. 547 ViewUtils.Filter ignoreSeekBarFilter = 548 (viewToFilter) -> mShowLinearProgressBar || viewToFilter != mSeekBarContainer; 549 550 mViewsToHideForCustomActions = ViewUtils.getViewsById(view, res, 551 R.array.playback_views_to_hide_when_showing_custom_actions, ignoreSeekBarFilter); 552 mViewsToHideWhenQueueIsVisible = ViewUtils.getViewsById(view, res, 553 R.array.playback_views_to_hide_when_queue_is_visible, ignoreSeekBarFilter); 554 mViewsToShowWhenQueueIsVisible = ViewUtils.getViewsById(view, res, 555 R.array.playback_views_to_show_when_queue_is_visible, null); 556 mViewsToHideImmediatelyWhenQueueIsVisible = ViewUtils.getViewsById(view, res, 557 R.array.playback_views_to_hide_immediately_when_queue_is_visible, ignoreSeekBarFilter); 558 mViewsToShowImmediatelyWhenQueueIsVisible = ViewUtils.getViewsById(view, res, 559 R.array.playback_views_to_show_immediately_when_queue_is_visible, null); 560 561 mAlbumArtBinder = new ImageBinder<>( 562 PlaceholderType.BACKGROUND, 563 MediaAppConfig.getMediaItemsBitmapMaxSize(getContext()), 564 drawable -> mAlbumBackground.setBackgroundDrawable(drawable)); 565 566 getPlaybackViewModel().getMetadata().observe(getViewLifecycleOwner(), 567 item -> mAlbumArtBinder.setImage(PlaybackFragment.this.getContext(), 568 item != null ? item.getArtworkKey() : null)); 569 570 mUxrContentLimiter = new LifeCycleObserverUxrContentLimiter( 571 new UxrContentLimiterImpl(getContext(), R.xml.uxr_config)); 572 mUxrContentLimiter.setAdapter(mQueueAdapter); 573 getLifecycle().addObserver(mUxrContentLimiter); 574 } 575 576 @Override onAttach(Context context)577 public void onAttach(Context context) { 578 super.onAttach(context); 579 } 580 581 @Override onDetach()582 public void onDetach() { 583 super.onDetach(); 584 } 585 initPlaybackControls(PlaybackControlsActionBar playbackControls)586 private void initPlaybackControls(PlaybackControlsActionBar playbackControls) { 587 mPlaybackControls = playbackControls; 588 mPlaybackControls.setModel(getPlaybackViewModel(), getViewLifecycleOwner()); 589 mPlaybackControls.registerExpandCollapseCallback((expanding) -> { 590 Resources res = getContext().getResources(); 591 int millis = expanding ? res.getInteger(R.integer.control_bar_expand_anim_duration) : 592 res.getInteger(R.integer.control_bar_collapse_anim_duration); 593 594 if (mControlBarScrim != null) { 595 mControlBarScrim.setClickable(expanding); 596 } 597 598 if (expanding) { 599 if (mControlBarScrim != null) { 600 ViewUtils.showViewAnimated(mControlBarScrim, millis); 601 } 602 } else { 603 if (mControlBarScrim != null) { 604 ViewUtils.hideViewAnimated(mControlBarScrim, millis); 605 } 606 } 607 608 if (!mQueueIsVisible) { 609 for (View view : mViewsToHideForCustomActions) { 610 if (expanding) { 611 ViewUtils.hideViewAnimated(view, millis); 612 } else { 613 ViewUtils.showViewAnimated(view, millis); 614 } 615 } 616 } 617 }); 618 } 619 initQueue()620 private void initQueue() { 621 mFadeDuration = getResources().getInteger( 622 R.integer.fragment_playback_queue_fade_duration_ms); 623 624 int decorationHeight = getResources().getDimensionPixelSize( 625 R.dimen.playback_queue_list_padding_top); 626 // TODO (b/206038962): addItemDecoration is not supported anymore. Find another way to 627 // support this. 628 // Put the decoration above the first item. 629 int decorationPosition = 0; 630 mQueue.addItemDecoration(new QueueTopItemDecoration(decorationHeight, decorationPosition)); 631 632 mQueue.setVerticalFadingEdgeEnabled( 633 getResources().getBoolean(R.bool.queue_fading_edge_length_enabled)); 634 mQueueAdapter = new QueueItemsAdapter(); 635 636 getPlaybackViewModel().getPlaybackStateWrapper().observe(getViewLifecycleOwner(), 637 state -> { 638 Long itemId = (state != null) ? state.getActiveQueueItemId() : null; 639 if (!Objects.equals(mActiveQueueItemId, itemId)) { 640 mActiveQueueItemId = itemId; 641 mQueueAdapter.updateActiveItem(/* listIsNew */ false); 642 } 643 }); 644 mQueue.setAdapter(mQueueAdapter); 645 646 // Disable item changed animation. 647 mItemAnimator = new DefaultItemAnimator(); 648 mItemAnimator.setSupportsChangeAnimations(false); 649 mQueue.setItemAnimator(mItemAnimator); 650 651 // Make sure the AppBar menu reflects the initial state of playback fragment. 652 updateAppBarMenu(mHasQueue); 653 if (mQueueMenuItem != null) { 654 mQueueMenuItem.setActivated(mQueueIsVisible); 655 } 656 657 getPlaybackViewModel().getQueue().observe(this, this::setQueue); 658 659 getPlaybackViewModel().hasQueue().observe(getViewLifecycleOwner(), hasQueue -> { 660 boolean enableQueue = (hasQueue != null) && hasQueue; 661 boolean isQueueVisible = enableQueue && mViewModel.getQueueVisible(); 662 663 setQueueState(enableQueue, isQueueVisible); 664 }); 665 getPlaybackViewModel().getProgress().observe(getViewLifecycleOwner(), 666 playbackProgress -> 667 { 668 mQueueAdapter.setCurrentTime(playbackProgress.getCurrentTimeText().toString()); 669 mQueueAdapter.setMaxTime(playbackProgress.getMaxTimeText().toString()); 670 mQueueAdapter.setTimeVisible(playbackProgress.hasTime()); 671 }); 672 } 673 setQueue(List<MediaItemMetadata> queueItems)674 private void setQueue(List<MediaItemMetadata> queueItems) { 675 mQueueAdapter.setItems(queueItems); 676 } 677 initMetadataController(View view)678 private void initMetadataController(View view) { 679 ImageView albumArt = view.findViewById(R.id.album_art); 680 TextView title = view.findViewById(R.id.title); 681 TextView artist = view.findViewById(R.id.artist); 682 TextView albumTitle = view.findViewById(R.id.album_title); 683 TextView outerSeparator = view.findViewById(R.id.outer_separator); 684 TextView curTime = view.findViewById(R.id.current_time); 685 TextView innerSeparator = view.findViewById(R.id.inner_separator); 686 TextView maxTime = view.findViewById(R.id.max_time); 687 SeekBar seekbar = mShowLinearProgressBar ? mSeekBar : null; 688 689 Size maxArtSize = MediaAppConfig.getMediaItemsBitmapMaxSize(view.getContext()); 690 mMetadataController = new MetadataController(getViewLifecycleOwner(), 691 getPlaybackViewModel(), title, artist, albumTitle, outerSeparator, 692 curTime, innerSeparator, maxTime, seekbar, albumArt, maxArtSize); 693 } 694 695 /** 696 * Hides or shows the playback queue when the user clicks the queue button. 697 */ toggleQueueVisibility()698 private void toggleQueueVisibility() { 699 boolean updatedQueueVisibility = !mQueueIsVisible; 700 setQueueState(mHasQueue, updatedQueueVisibility); 701 702 // When the visibility of queue is changed by the user, save the visibility into ViewModel 703 // so that we can restore PlaybackFragment properly when needed. If it's changed by media 704 // source change (media source changes -> hasQueue becomes false -> queue is hidden), don't 705 // save it. 706 mViewModel.setQueueVisible(updatedQueueVisibility); 707 } 708 updateAppBarMenu(boolean hasQueue)709 private void updateAppBarMenu(boolean hasQueue) { 710 if (hasQueue && mQueueMenuItem == null) { 711 mQueueMenuItem = MenuItem.builder(getContext()) 712 .setIcon(R.drawable.ic_queue_button) 713 .setActivatable() 714 .setOnClickListener(button -> toggleQueueVisibility()) 715 .build(); 716 } 717 mAppBarController.setMenuItems( 718 hasQueue ? Collections.singletonList(mQueueMenuItem) : Collections.emptyList()); 719 } 720 setQueueState(boolean hasQueue, boolean visible)721 private void setQueueState(boolean hasQueue, boolean visible) { 722 if (mHasQueue != hasQueue) { 723 mHasQueue = hasQueue; 724 updateAppBarMenu(hasQueue); 725 } 726 if (mQueueMenuItem != null) { 727 mQueueMenuItem.setActivated(visible); 728 } 729 730 if (mQueueIsVisible != visible) { 731 mQueueIsVisible = visible; 732 if (mQueueIsVisible) { 733 ViewUtils.showViewsAnimated(mViewsToShowWhenQueueIsVisible, mFadeDuration); 734 ViewUtils.hideViewsAnimated(mViewsToHideWhenQueueIsVisible, mFadeDuration); 735 } else { 736 ViewUtils.hideViewsAnimated(mViewsToShowWhenQueueIsVisible, mFadeDuration); 737 ViewUtils.showViewsAnimated(mViewsToHideWhenQueueIsVisible, mFadeDuration); 738 } 739 ViewUtils.setVisible(mViewsToShowImmediatelyWhenQueueIsVisible, mQueueIsVisible); 740 ViewUtils.setVisible(mViewsToHideImmediatelyWhenQueueIsVisible, !mQueueIsVisible); 741 } 742 } 743 onQueueItemClicked(MediaItemMetadata item)744 private void onQueueItemClicked(MediaItemMetadata item) { 745 if (mController != null) { 746 mController.skipToQueueItem(item.getQueueId()); 747 } 748 boolean switchToPlayback = getResources().getBoolean( 749 R.bool.switch_to_playback_view_when_playable_item_is_clicked); 750 if (switchToPlayback) { 751 toggleQueueVisibility(); 752 } 753 } 754 755 /** 756 * Collapses the playback controls. 757 */ closeOverflowMenu()758 public void closeOverflowMenu() { 759 mPlaybackControls.close(); 760 } 761 762 // TODO(b/151174811): Use appropriate modes, instead of just MEDIA_SOURCE_MODE_BROWSE getPlaybackViewModel()763 private PlaybackViewModel getPlaybackViewModel() { 764 return PlaybackViewModel.get(getActivity().getApplication(), MEDIA_SOURCE_MODE_BROWSE); 765 } 766 getMediaSourceViewModel()767 private MediaSourceViewModel getMediaSourceViewModel() { 768 return MediaSourceViewModel.get(getActivity().getApplication(), MEDIA_SOURCE_MODE_BROWSE); 769 } 770 setSeekBarColor(int color)771 private void setSeekBarColor(int color) { 772 mSeekBar.setProgressTintList(ColorStateList.valueOf(color)); 773 774 // If the thumb drawable consists of a center drawable, only change the color of the center 775 // drawable. Otherwise change the color of the entire thumb drawable. 776 Drawable thumb = mSeekBar.getThumb(); 777 if (thumb instanceof LayerDrawable) { 778 LayerDrawable thumbDrawable = (LayerDrawable) thumb; 779 Drawable thumbCenter = thumbDrawable.findDrawableByLayerId(R.id.thumb_center); 780 if (thumbCenter != null) { 781 thumbCenter.setColorFilter(color, PorterDuff.Mode.SRC); 782 thumbDrawable.setDrawableByLayerId(R.id.thumb_center, thumbCenter); 783 return; 784 } 785 } 786 mSeekBar.setThumbTintList(ColorStateList.valueOf(color)); 787 } 788 789 /** 790 * Sets a listener of this PlaybackFragment events. In order to avoid memory leaks, consumers 791 * must reset this reference by setting the listener to null. 792 */ setListener(PlaybackFragmentListener listener)793 public void setListener(PlaybackFragmentListener listener) { 794 mListener = listener; 795 } 796 } 797