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 android.annotation.Nullable;
20 import android.content.Context;
21 import android.content.res.ColorStateList;
22 import android.content.res.Resources;
23 import android.content.res.TypedArray;
24 import android.graphics.Rect;
25 import android.os.Bundle;
26 import android.util.Log;
27 import android.util.Size;
28 import android.view.LayoutInflater;
29 import android.view.View;
30 import android.view.ViewGroup;
31 import android.widget.ImageView;
32 import android.widget.SeekBar;
33 import android.widget.TextView;
34 
35 import androidx.annotation.NonNull;
36 import androidx.constraintlayout.widget.ConstraintLayout;
37 import androidx.fragment.app.Fragment;
38 import androidx.lifecycle.ViewModelProviders;
39 import androidx.recyclerview.widget.DefaultItemAnimator;
40 import androidx.recyclerview.widget.LinearLayoutManager;
41 import androidx.recyclerview.widget.RecyclerView;
42 
43 import com.android.car.apps.common.BackgroundImageView;
44 import com.android.car.apps.common.imaging.ImageBinder;
45 import com.android.car.apps.common.imaging.ImageBinder.PlaceholderType;
46 import com.android.car.apps.common.imaging.ImageViewBinder;
47 import com.android.car.apps.common.util.ViewUtils;
48 import com.android.car.media.common.MediaItemMetadata;
49 import com.android.car.media.common.MetadataController;
50 import com.android.car.media.common.PlaybackControlsActionBar;
51 import com.android.car.media.common.playback.PlaybackViewModel;
52 import com.android.car.media.common.source.MediaSourceViewModel;
53 
54 import java.util.ArrayList;
55 import java.util.Collections;
56 import java.util.List;
57 import java.util.Objects;
58 
59 
60 /**
61  * A {@link Fragment} that implements both the playback and the content forward browsing experience.
62  * It observes a {@link PlaybackViewModel} and updates its information depending on the currently
63  * playing media source through the {@link android.media.session.MediaSession} API.
64  */
65 public class PlaybackFragment extends Fragment {
66     private static final String TAG = "PlaybackFragment";
67 
68     private ImageBinder<MediaItemMetadata.ArtworkRef> mAlbumArtBinder;
69     private BackgroundImageView mAlbumBackground;
70     private View mBackgroundScrim;
71     private View mControlBarScrim;
72     private PlaybackControlsActionBar mPlaybackControls;
73     private QueueItemsAdapter mQueueAdapter;
74     private RecyclerView mQueue;
75     private ViewGroup mSeekBarContainer;
76     private SeekBar mSeekBar;
77     private View mQueueButton;
78     private ViewGroup mNavIconContainer;
79     private List<View> mViewsToHideForCustomActions;
80     private List<View> mViewsToHideWhenQueueIsVisible;
81     private List<View> mViewsToShowWhenQueueIsVisible;
82     private List<View> mViewsToHideImmediatelyWhenQueueIsVisible;
83     private List<View> mViewsToShowImmediatelyWhenQueueIsVisible;
84 
85     private DefaultItemAnimator mItemAnimator;
86 
87     private MetadataController mMetadataController;
88 
89     private PlaybackFragmentListener mListener;
90 
91     private PlaybackViewModel.PlaybackController mController;
92     private Long mActiveQueueItemId;
93 
94     private boolean mHasQueue;
95     private boolean mQueueIsVisible;
96     private boolean mShowTimeForActiveQueueItem;
97     private boolean mShowIconForActiveQueueItem;
98     private boolean mShowThumbnailForQueueItem;
99     private boolean mShowSubtitleForQueueItem;
100 
101     private boolean mShowLinearProgressBar;
102 
103     private int mFadeDuration;
104 
105     private MediaActivity.ViewModel mViewModel;
106 
107     /**
108      * PlaybackFragment listener
109      */
110     public interface PlaybackFragmentListener {
111         /**
112          * Invoked when the user clicks on the collapse button
113          */
onCollapse()114         void onCollapse();
115     }
116 
117     public class QueueViewHolder extends RecyclerView.ViewHolder {
118 
119         private final View mView;
120         private final ViewGroup mThumbnailContainer;
121         private final ImageView mThumbnail;
122         private final View mSpacer;
123         private final TextView mTitle;
124         private final TextView mSubtitle;
125         private final TextView mCurrentTime;
126         private final TextView mMaxTime;
127         private final TextView mTimeSeparator;
128         private final ImageView mActiveIcon;
129 
130         private final ImageViewBinder<MediaItemMetadata.ArtworkRef> mThumbnailBinder;
131 
QueueViewHolder(View itemView)132         QueueViewHolder(View itemView) {
133             super(itemView);
134             mView = itemView;
135             mThumbnailContainer = itemView.findViewById(R.id.thumbnail_container);
136             mThumbnail = itemView.findViewById(R.id.thumbnail);
137             mSpacer = itemView.findViewById(R.id.spacer);
138             mTitle = itemView.findViewById(R.id.queue_list_item_title);
139             mSubtitle = itemView.findViewById(R.id.queue_list_item_subtitle);
140             mCurrentTime = itemView.findViewById(R.id.current_time);
141             mMaxTime = itemView.findViewById(R.id.max_time);
142             mTimeSeparator = itemView.findViewById(R.id.separator);
143             mActiveIcon = itemView.findViewById(R.id.now_playing_icon);
144 
145             Size maxArtSize = MediaAppConfig.getMediaItemsBitmapMaxSize(itemView.getContext());
146             mThumbnailBinder = new ImageViewBinder<>(maxArtSize, mThumbnail);
147         }
148 
bind(MediaItemMetadata item)149         void bind(MediaItemMetadata item) {
150             mView.setOnClickListener(v -> onQueueItemClicked(item));
151 
152             ViewUtils.setVisible(mThumbnailContainer, mShowThumbnailForQueueItem);
153             if (mShowThumbnailForQueueItem) {
154                 Context context = mView.getContext();
155                 mThumbnailBinder.setImage(context, item != null ? item.getArtworkKey() : null);
156             }
157 
158             ViewUtils.setVisible(mSpacer, !mShowThumbnailForQueueItem);
159 
160             mTitle.setText(item.getTitle());
161 
162             boolean active = mActiveQueueItemId != null && Objects.equals(mActiveQueueItemId,
163                     item.getQueueId());
164             if (active) {
165                 mCurrentTime.setText(mQueueAdapter.getCurrentTime());
166                 mMaxTime.setText(mQueueAdapter.getMaxTime());
167             }
168             boolean shouldShowTime =
169                     mShowTimeForActiveQueueItem && active && mQueueAdapter.getTimeVisible();
170             ViewUtils.setVisible(mCurrentTime, shouldShowTime);
171             ViewUtils.setVisible(mMaxTime, shouldShowTime);
172             ViewUtils.setVisible(mTimeSeparator, shouldShowTime);
173 
174             boolean shouldShowIcon = mShowIconForActiveQueueItem && active;
175             ViewUtils.setVisible(mActiveIcon, shouldShowIcon);
176 
177             if (mShowSubtitleForQueueItem) {
178                 mSubtitle.setText(item.getSubtitle());
179             }
180         }
181 
onViewAttachedToWindow()182         void onViewAttachedToWindow() {
183             if (mShowThumbnailForQueueItem) {
184                 Context context = mView.getContext();
185                 mThumbnailBinder.maybeRestartLoading(context);
186             }
187         }
188 
onViewDetachedFromWindow()189         void onViewDetachedFromWindow() {
190             if (mShowThumbnailForQueueItem) {
191                 Context context = mView.getContext();
192                 mThumbnailBinder.maybeCancelLoading(context);
193             }
194         }
195     }
196 
197 
198     private class QueueItemsAdapter extends RecyclerView.Adapter<QueueViewHolder> {
199 
200         private List<MediaItemMetadata> mQueueItems = Collections.emptyList();
201         private String mCurrentTimeText = "";
202         private String mMaxTimeText = "";
203         private Integer mActiveItemPos;
204         private boolean mTimeVisible;
205 
setItems(@ullable List<MediaItemMetadata> items)206         void setItems(@Nullable List<MediaItemMetadata> items) {
207             List<MediaItemMetadata> newQueueItems =
208                 new ArrayList<>(items != null ? items : Collections.emptyList());
209             if (newQueueItems.equals(mQueueItems)) {
210                 return;
211             }
212             mQueueItems = newQueueItems;
213             updateActiveItem();
214             notifyDataSetChanged();
215         }
216 
217         // Updates mActiveItemPos, then scrolls the queue to mActiveItemPos.
218         // It should be called when the active item (mActiveQueueItemId) changed or
219         // the queue items (mQueueItems) changed.
updateActiveItem()220         void updateActiveItem() {
221             if (mQueueItems == null || mActiveQueueItemId == null) {
222                 mActiveItemPos = null;
223                 return;
224             }
225             Integer activeItemPos = null;
226             for (int i = 0; i < mQueueItems.size(); i++) {
227                 if (mQueueItems.get(i).getQueueId() == mActiveQueueItemId) {
228                     activeItemPos = i;
229                     break;
230                 }
231             }
232 
233             if (mActiveItemPos != activeItemPos) {
234                 if (mActiveItemPos != null) {
235                     notifyItemChanged(mActiveItemPos.intValue());
236                 }
237                 mActiveItemPos = activeItemPos;
238                 if (mActiveItemPos != null) {
239                     mQueue.scrollToPosition(mActiveItemPos.intValue());
240                     notifyItemChanged(mActiveItemPos.intValue());
241                 }
242             }
243         }
244 
setCurrentTime(String currentTime)245         void setCurrentTime(String currentTime) {
246             if (!mCurrentTimeText.equals(currentTime)) {
247                 mCurrentTimeText = currentTime;
248                 if (mActiveItemPos != null) {
249                     notifyItemChanged(mActiveItemPos.intValue());
250                 }
251             }
252         }
253 
setMaxTime(String maxTime)254         void setMaxTime(String maxTime) {
255             if (!mMaxTimeText.equals(maxTime)) {
256                 mMaxTimeText = maxTime;
257                 if (mActiveItemPos != null) {
258                     notifyItemChanged(mActiveItemPos.intValue());
259                 }
260             }
261         }
262 
setTimeVisible(boolean visible)263         void setTimeVisible(boolean visible) {
264             if (mTimeVisible != visible) {
265                 mTimeVisible = visible;
266                 if (mActiveItemPos != null) {
267                     notifyItemChanged(mActiveItemPos.intValue());
268                 }
269             }
270         }
271 
getCurrentTime()272         String getCurrentTime() {
273             return mCurrentTimeText;
274         }
275 
getMaxTime()276         String getMaxTime() {
277             return mMaxTimeText;
278         }
279 
getTimeVisible()280         boolean getTimeVisible() {
281             return mTimeVisible;
282         }
283 
284         @Override
onCreateViewHolder(ViewGroup parent, int viewType)285         public QueueViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
286             LayoutInflater inflater = LayoutInflater.from(parent.getContext());
287             return new QueueViewHolder(inflater.inflate(R.layout.queue_list_item, parent, false));
288         }
289 
290         @Override
onBindViewHolder(QueueViewHolder holder, int position)291         public void onBindViewHolder(QueueViewHolder holder, int position) {
292             int size = mQueueItems.size();
293             if (0 <= position && position < size) {
294                 holder.bind(mQueueItems.get(position));
295             } else {
296                 Log.e(TAG, "onBindViewHolder invalid position " + position + " of " + size);
297             }
298         }
299 
300         @Override
onViewAttachedToWindow(@onNull QueueViewHolder holder)301         public void onViewAttachedToWindow(@NonNull QueueViewHolder holder) {
302             super.onViewAttachedToWindow(holder);
303             holder.onViewAttachedToWindow();
304         }
305 
306         @Override
onViewDetachedFromWindow(@onNull QueueViewHolder holder)307         public void onViewDetachedFromWindow(@NonNull QueueViewHolder holder) {
308             super.onViewDetachedFromWindow(holder);
309             holder.onViewDetachedFromWindow();
310         }
311 
312         @Override
getItemCount()313         public int getItemCount() {
314             return mQueueItems.size();
315         }
316 
refresh()317         void refresh() {
318             // TODO: Perform a diff between current and new content and trigger the proper
319             // RecyclerView updates.
320             this.notifyDataSetChanged();
321         }
322 
323         @Override
getItemId(int position)324         public long getItemId(int position) {
325             return mQueueItems.get(position).getQueueId();
326         }
327     }
328 
329     private class QueueTopItemDecoration extends RecyclerView.ItemDecoration {
330         int mHeight;
331         int mDecorationPosition;
332 
QueueTopItemDecoration(int height, int decorationPosition)333         QueueTopItemDecoration(int height, int decorationPosition) {
334             mHeight = height;
335             mDecorationPosition = decorationPosition;
336         }
337 
338         @Override
getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)339         public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
340                 RecyclerView.State state) {
341             super.getItemOffsets(outRect, view, parent, state);
342             if (parent.getChildAdapterPosition(view) == mDecorationPosition) {
343                 outRect.top = mHeight;
344             }
345         }
346     }
347 
348     @Override
onCreateView(@onNull LayoutInflater inflater, final ViewGroup container, Bundle savedInstanceState)349     public View onCreateView(@NonNull LayoutInflater inflater, final ViewGroup container,
350             Bundle savedInstanceState) {
351         View view = inflater.inflate(R.layout.fragment_playback, container, false);
352         mAlbumBackground = view.findViewById(R.id.playback_background);
353         mQueue = view.findViewById(R.id.queue_list);
354         mSeekBarContainer = view.findViewById(R.id.seek_bar_container);
355         mSeekBar = view.findViewById(R.id.seek_bar);
356         mQueueButton = view.findViewById(R.id.queue_button);
357         mQueueButton.setOnClickListener(button -> toggleQueueVisibility());
358         mNavIconContainer = view.findViewById(R.id.nav_icon_container);
359         mNavIconContainer.setOnClickListener(nav -> onCollapse());
360         mBackgroundScrim = view.findViewById(R.id.background_scrim);
361         ViewUtils.setVisible(mBackgroundScrim, false);
362         mControlBarScrim = view.findViewById(R.id.control_bar_scrim);
363         if (mControlBarScrim != null) {
364             ViewUtils.setVisible(mControlBarScrim, false);
365             mControlBarScrim.setOnClickListener(scrim -> mPlaybackControls.close());
366             mControlBarScrim.setClickable(false);
367         }
368 
369         Resources res = getResources();
370         mShowTimeForActiveQueueItem = res.getBoolean(
371                 R.bool.show_time_for_now_playing_queue_list_item);
372         mShowIconForActiveQueueItem = res.getBoolean(
373                 R.bool.show_icon_for_now_playing_queue_list_item);
374         mShowThumbnailForQueueItem = getContext().getResources().getBoolean(
375                 R.bool.show_thumbnail_for_queue_list_item);
376         mShowLinearProgressBar = getContext().getResources().getBoolean(
377                 R.bool.show_linear_progress_bar);
378         mShowSubtitleForQueueItem = getContext().getResources().getBoolean(
379             R.bool.show_subtitle_for_queue_list_item);
380 
381         if (mSeekBar != null) {
382             if (mShowLinearProgressBar) {
383                 boolean useMediaSourceColor = res.getBoolean(
384                         R.bool.use_media_source_color_for_progress_bar);
385                 int defaultColor = res.getColor(R.color.progress_bar_highlight, null);
386                 if (useMediaSourceColor) {
387                     getPlaybackViewModel().getMediaSourceColors().observe(getViewLifecycleOwner(),
388                             sourceColors -> {
389                                 int color = sourceColors != null ? sourceColors.getAccentColor(
390                                         defaultColor)
391                                         : defaultColor;
392                                 mSeekBar.setThumbTintList(ColorStateList.valueOf(color));
393                                 mSeekBar.setProgressTintList(ColorStateList.valueOf(color));
394                             });
395                 } else {
396                     mSeekBar.setThumbTintList(ColorStateList.valueOf(defaultColor));
397                     mSeekBar.setProgressTintList(ColorStateList.valueOf(defaultColor));
398                 }
399             } else {
400                 mSeekBar.setVisibility(View.GONE);
401             }
402         }
403 
404         ImageView logoView = view.findViewById(R.id.car_ui_toolbar_title_logo);
405         MediaSourceViewModel mediaSourceViewModel = getMediaSourceViewModel();
406         mediaSourceViewModel.getPrimaryMediaSource().observe(this, mediaSource ->
407                 logoView.setImageBitmap(mediaSource != null
408                         ? mediaSource.getRoundPackageIcon() : null));
409 
410         mViewModel = ViewModelProviders.of(requireActivity()).get(MediaActivity.ViewModel.class);
411 
412         getPlaybackViewModel().getPlaybackController().observe(getViewLifecycleOwner(),
413                 controller -> mController = controller);
414         initPlaybackControls(view.findViewById(R.id.playback_controls));
415         initMetadataController(view);
416         initQueue();
417 
418         // Don't update the visibility of seekBar if show_linear_progress_bar is false.
419         ViewUtils.Filter ignoreSeekBarFilter =
420             (viewToFilter) -> mShowLinearProgressBar || viewToFilter != mSeekBarContainer;
421 
422         mViewsToHideForCustomActions = ViewUtils.getViewsById(view, res,
423             R.array.playback_views_to_hide_when_showing_custom_actions, ignoreSeekBarFilter);
424         mViewsToHideWhenQueueIsVisible = ViewUtils.getViewsById(view, res,
425             R.array.playback_views_to_hide_when_queue_is_visible, ignoreSeekBarFilter);
426         mViewsToShowWhenQueueIsVisible = ViewUtils.getViewsById(view, res,
427             R.array.playback_views_to_show_when_queue_is_visible, null);
428         mViewsToHideImmediatelyWhenQueueIsVisible = ViewUtils.getViewsById(view, res,
429             R.array.playback_views_to_hide_immediately_when_queue_is_visible, ignoreSeekBarFilter);
430         mViewsToShowImmediatelyWhenQueueIsVisible = ViewUtils.getViewsById(view, res,
431             R.array.playback_views_to_show_immediately_when_queue_is_visible, null);
432 
433         mAlbumArtBinder = new ImageBinder<>(
434                 PlaceholderType.BACKGROUND,
435                 MediaAppConfig.getMediaItemsBitmapMaxSize(getContext()),
436                 drawable -> mAlbumBackground.setBackgroundDrawable(drawable));
437 
438         getPlaybackViewModel().getMetadata().observe(getViewLifecycleOwner(),
439                 item -> mAlbumArtBinder.setImage(PlaybackFragment.this.getContext(),
440                         item != null ? item.getArtworkKey() : null));
441 
442         return view;
443     }
444 
445     @Override
onAttach(Context context)446     public void onAttach(Context context) {
447         super.onAttach(context);
448     }
449 
450     @Override
onDetach()451     public void onDetach() {
452         super.onDetach();
453     }
454 
initPlaybackControls(PlaybackControlsActionBar playbackControls)455     private void initPlaybackControls(PlaybackControlsActionBar playbackControls) {
456         mPlaybackControls = playbackControls;
457         mPlaybackControls.setModel(getPlaybackViewModel(), getViewLifecycleOwner());
458         mPlaybackControls.registerExpandCollapseCallback((expanding) -> {
459             Resources res = getContext().getResources();
460             int millis = expanding ? res.getInteger(R.integer.control_bar_expand_anim_duration) :
461                 res.getInteger(R.integer.control_bar_collapse_anim_duration);
462 
463             if (mControlBarScrim != null) {
464                 mControlBarScrim.setClickable(expanding);
465             }
466 
467             if (expanding) {
468                 if (mControlBarScrim != null) {
469                     ViewUtils.showViewAnimated(mControlBarScrim, millis);
470                 }
471             } else {
472                 if (mControlBarScrim != null) {
473                     ViewUtils.hideViewAnimated(mControlBarScrim, millis);
474                 }
475             }
476 
477             if (!mQueueIsVisible) {
478                 for (View view : mViewsToHideForCustomActions) {
479                     if (expanding) {
480                         ViewUtils.hideViewAnimated(view, millis);
481                     } else {
482                         ViewUtils.showViewAnimated(view, millis);
483                     }
484                 }
485             }
486         });
487     }
488 
initQueue()489     private void initQueue() {
490         mFadeDuration = getResources().getInteger(
491                 R.integer.fragment_playback_queue_fade_duration_ms);
492 
493         int decorationHeight = getResources().getDimensionPixelSize(
494                 R.dimen.playback_queue_list_padding_top);
495         // Put the decoration above the first item.
496         int decorationPosition = 0;
497         mQueue.addItemDecoration(new QueueTopItemDecoration(decorationHeight, decorationPosition));
498 
499         mQueue.setVerticalFadingEdgeEnabled(
500                 getResources().getBoolean(R.bool.queue_fading_edge_length_enabled));
501         mQueueAdapter = new QueueItemsAdapter();
502 
503         getPlaybackViewModel().getPlaybackStateWrapper().observe(getViewLifecycleOwner(),
504                 state -> {
505                     Long itemId = (state != null) ? state.getActiveQueueItemId() : null;
506                     if (!Objects.equals(mActiveQueueItemId, itemId)) {
507                         mActiveQueueItemId = itemId;
508                         mQueueAdapter.updateActiveItem();
509                     }
510                 });
511         mQueue.setAdapter(mQueueAdapter);
512 
513         // Disable item changed animation.
514         mItemAnimator = new DefaultItemAnimator();
515         mItemAnimator.setSupportsChangeAnimations(false);
516         mQueue.setItemAnimator(mItemAnimator);
517 
518         getPlaybackViewModel().getQueue().observe(this, this::setQueue);
519 
520         getPlaybackViewModel().hasQueue().observe(getViewLifecycleOwner(), hasQueue -> {
521             boolean enableQueue = (hasQueue != null) && hasQueue;
522             mQueueIsVisible = mViewModel.getQueueVisible();
523             setHasQueue(enableQueue);
524         });
525         getPlaybackViewModel().getProgress().observe(getViewLifecycleOwner(),
526                 playbackProgress ->
527                 {
528                     mQueueAdapter.setCurrentTime(playbackProgress.getCurrentTimeText().toString());
529                     mQueueAdapter.setMaxTime(playbackProgress.getMaxTimeText().toString());
530                     mQueueAdapter.setTimeVisible(playbackProgress.hasTime());
531                 });
532     }
533 
setQueue(List<MediaItemMetadata> queueItems)534     private void setQueue(List<MediaItemMetadata> queueItems) {
535         mQueueAdapter.setItems(queueItems);
536     }
537 
initMetadataController(View view)538     private void initMetadataController(View view) {
539         ImageView albumArt = view.findViewById(R.id.album_art);
540         TextView title = view.findViewById(R.id.title);
541         TextView artist = view.findViewById(R.id.artist);
542         TextView albumTitle = view.findViewById(R.id.album_title);
543         TextView outerSeparator = view.findViewById(R.id.outer_separator);
544         TextView curTime = view.findViewById(R.id.current_time);
545         TextView innerSeparator = view.findViewById(R.id.inner_separator);
546         TextView maxTime = view.findViewById(R.id.max_time);
547         SeekBar seekbar = mShowLinearProgressBar ? mSeekBar : null;
548 
549         Size maxArtSize = MediaAppConfig.getMediaItemsBitmapMaxSize(view.getContext());
550         mMetadataController = new MetadataController(getViewLifecycleOwner(),
551                 getPlaybackViewModel(), title, artist, albumTitle, outerSeparator,
552                 curTime, innerSeparator, maxTime, seekbar, albumArt, maxArtSize);
553     }
554 
555     /**
556      * Hides or shows the playback queue when the user clicks the queue button.
557      */
toggleQueueVisibility()558     private void toggleQueueVisibility() {
559         boolean updatedQueueVisibility = !mQueueIsVisible;
560         setQueueVisible(updatedQueueVisibility);
561 
562         // When the visibility of queue is changed by the user, save the visibility into ViewModel
563         // so that we can restore PlaybackFragment properly when needed. If it's changed by media
564         // source change (media source changes -> hasQueue becomes false -> queue is hidden), don't
565         // save it.
566         mViewModel.setQueueVisible(updatedQueueVisibility);
567     }
568 
setQueueVisible(boolean visible)569     private void setQueueVisible(boolean visible) {
570         mQueueIsVisible = visible;
571         mQueueButton.setActivated(mQueueIsVisible);
572         mQueueButton.setSelected(mQueueIsVisible);
573         if (mQueueIsVisible) {
574             ViewUtils.showViewsAnimated(mViewsToShowWhenQueueIsVisible, mFadeDuration);
575             ViewUtils.hideViewsAnimated(mViewsToHideWhenQueueIsVisible, mFadeDuration);
576             ViewUtils.setVisible(mViewsToShowImmediatelyWhenQueueIsVisible, true);
577             ViewUtils.setVisible(mViewsToHideImmediatelyWhenQueueIsVisible, false);
578         } else {
579             ViewUtils.hideViewsAnimated(mViewsToShowWhenQueueIsVisible, mFadeDuration);
580             ViewUtils.showViewsAnimated(mViewsToHideWhenQueueIsVisible, mFadeDuration);
581             ViewUtils.setVisible(mViewsToShowImmediatelyWhenQueueIsVisible, false);
582             ViewUtils.setVisible(mViewsToHideImmediatelyWhenQueueIsVisible, true);
583         }
584     }
585 
586     /** Sets whether the source has a queue. */
setHasQueue(boolean hasQueue)587     private void setHasQueue(boolean hasQueue) {
588         mHasQueue = hasQueue;
589         mQueueButton.setVisibility(mHasQueue ? View.VISIBLE : View.GONE);
590         setQueueVisible(hasQueue && mQueueIsVisible);
591     }
592 
onQueueItemClicked(MediaItemMetadata item)593     private void onQueueItemClicked(MediaItemMetadata item) {
594         if (mController != null) {
595             mController.skipToQueueItem(item.getQueueId());
596         }
597         boolean switchToPlayback = getResources().getBoolean(
598                 R.bool.switch_to_playback_view_when_playable_item_is_clicked);
599         if (switchToPlayback) {
600             toggleQueueVisibility();
601         }
602     }
603 
604     /**
605      * Collapses the playback controls.
606      */
closeOverflowMenu()607     public void closeOverflowMenu() {
608         mPlaybackControls.close();
609     }
610 
getPlaybackViewModel()611     private PlaybackViewModel getPlaybackViewModel() {
612         return PlaybackViewModel.get(getActivity().getApplication());
613     }
614 
getMediaSourceViewModel()615     private MediaSourceViewModel getMediaSourceViewModel() {
616         return MediaSourceViewModel.get(getActivity().getApplication());
617     }
618 
619     /**
620      * Sets a listener of this PlaybackFragment events. In order to avoid memory leaks, consumers
621      * must reset this reference by setting the listener to null.
622      */
setListener(PlaybackFragmentListener listener)623     public void setListener(PlaybackFragmentListener listener) {
624         mListener = listener;
625     }
626 
onCollapse()627     private void onCollapse() {
628         if (mListener != null) {
629             mListener.onCollapse();
630         }
631     }
632 }
633