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