• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.graphics.Bitmap;
23 import android.graphics.Rect;
24 import android.content.res.Resources;
25 import android.content.res.TypedArray;
26 import android.os.Bundle;
27 import android.util.Log;
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.recyclerview.widget.DefaultItemAnimator;
39 import androidx.recyclerview.widget.LinearLayoutManager;
40 import androidx.recyclerview.widget.RecyclerView;
41 
42 import com.android.car.apps.common.BackgroundImageView;
43 import com.android.car.apps.common.util.ViewUtils;
44 import com.android.car.media.common.MediaAppSelectorWidget;
45 import com.android.car.media.common.MediaItemMetadata;
46 import com.android.car.media.common.MetadataController;
47 import com.android.car.media.common.PlaybackControlsActionBar;
48 import com.android.car.media.common.playback.PlaybackViewModel;
49 
50 import java.util.ArrayList;
51 import java.util.Collections;
52 import java.util.List;
53 import java.util.Objects;
54 import java.util.concurrent.CompletableFuture;
55 
56 /**
57  * A {@link Fragment} that implements both the playback and the content forward browsing experience.
58  * It observes a {@link PlaybackViewModel} and updates its information depending on the currently
59  * playing media source through the {@link android.media.session.MediaSession} API.
60  */
61 public class PlaybackFragment extends Fragment {
62     private static final String TAG = "PlaybackFragment";
63 
64     private CompletableFuture<Bitmap> mFutureAlbumBackground;
65     private BackgroundImageView mAlbumBackground;
66     private View mBackgroundScrim;
67     private View mControlBarScrim;
68     private PlaybackControlsActionBar mPlaybackControls;
69     private QueueItemsAdapter mQueueAdapter;
70     private RecyclerView mQueue;
71     private ConstraintLayout mMetadataContainer;
72     private SeekBar mSeekBar;
73     private View mQueueButton;
74     private ViewGroup mNavIconContainer;
75     private List<View> mViewsToHideForCustomActions;
76 
77     private DefaultItemAnimator mItemAnimator;
78 
79     private MetadataController mMetadataController;
80 
81     private PlaybackFragmentListener mListener;
82 
83     private PlaybackViewModel.PlaybackController mController;
84     private Long mActiveQueueItemId;
85 
86     private boolean mHasQueue;
87     private boolean mQueueIsVisible;
88     private boolean mShowTimeForActiveQueueItem;
89     private boolean mShowIconForActiveQueueItem;
90     private boolean mShowThumbnailForQueueItem;
91 
92     private int mFadeDuration;
93     private float mPlaybackQueueBackgroundAlpha;
94 
95     /**
96      * PlaybackFragment listener
97      */
98     public interface PlaybackFragmentListener {
99         /**
100          * Invoked when the user clicks on the collapse button
101          */
onCollapse()102         void onCollapse();
103     }
104 
105     public class QueueViewHolder extends RecyclerView.ViewHolder {
106 
107         private final View mView;
108         private final ViewGroup mThumbnailContainer;
109         private final ImageView mThumbnail;
110         private final View mSpacer;
111         private final TextView mTitle;
112         private final TextView mCurrentTime;
113         private final TextView mMaxTime;
114         private final TextView mTimeSeparator;
115         private final ImageView mActiveIcon;
116 
QueueViewHolder(View itemView)117         QueueViewHolder(View itemView) {
118             super(itemView);
119             mView = itemView;
120             mThumbnailContainer = itemView.findViewById(R.id.thumbnail_container);
121             mThumbnail = itemView.findViewById(R.id.thumbnail);
122             mSpacer = itemView.findViewById(R.id.spacer);
123             mTitle = itemView.findViewById(R.id.title);
124             mCurrentTime = itemView.findViewById(R.id.current_time);
125             mMaxTime = itemView.findViewById(R.id.max_time);
126             mTimeSeparator = itemView.findViewById(R.id.separator);
127             mActiveIcon = itemView.findViewById(R.id.now_playing_icon);
128         }
129 
bind(MediaItemMetadata item)130         boolean bind(MediaItemMetadata item) {
131             mView.setOnClickListener(v -> onQueueItemClicked(item));
132 
133             ViewUtils.setVisible(mThumbnailContainer, mShowThumbnailForQueueItem);
134             if (mShowThumbnailForQueueItem) {
135                 MediaItemMetadata.updateImageView(mThumbnail.getContext(), item, mThumbnail, 0,
136                         true);
137             }
138 
139             ViewUtils.setVisible(mSpacer, !mShowThumbnailForQueueItem);
140 
141             mTitle.setText(item.getTitle());
142 
143             boolean active = mActiveQueueItemId != null && Objects.equals(mActiveQueueItemId,
144                     item.getQueueId());
145             if (active) {
146                 mCurrentTime.setText(mQueueAdapter.getCurrentTime());
147                 mMaxTime.setText(mQueueAdapter.getMaxTime());
148             }
149             boolean shouldShowTime =
150                     mShowTimeForActiveQueueItem && active && mQueueAdapter.getTimeVisible();
151             ViewUtils.setVisible(mCurrentTime, shouldShowTime);
152             ViewUtils.setVisible(mMaxTime, shouldShowTime);
153             ViewUtils.setVisible(mTimeSeparator, shouldShowTime);
154 
155             boolean shouldShowIcon = mShowIconForActiveQueueItem && active;
156             ViewUtils.setVisible(mActiveIcon, shouldShowIcon);
157 
158             return active;
159         }
160     }
161 
162 
163     private class QueueItemsAdapter extends RecyclerView.Adapter<QueueViewHolder> {
164 
165         private List<MediaItemMetadata> mQueueItems;
166         private String mCurrentTimeText;
167         private String mMaxTimeText;
168         private Integer mActiveItemPos;
169         private boolean mTimeVisible;
170 
setItems(@ullable List<MediaItemMetadata> items)171         void setItems(@Nullable List<MediaItemMetadata> items) {
172             mQueueItems = new ArrayList<>(items != null ? items : Collections.emptyList());
173             notifyDataSetChanged();
174         }
175 
setCurrentTime(String currentTime)176         void setCurrentTime(String currentTime) {
177             mCurrentTimeText = currentTime;
178             if (mActiveItemPos != null) {
179                 notifyItemChanged(mActiveItemPos.intValue());
180             }
181         }
182 
setMaxTime(String maxTime)183         void setMaxTime(String maxTime) {
184             mMaxTimeText = maxTime;
185             if (mActiveItemPos != null) {
186                 notifyItemChanged(mActiveItemPos.intValue());
187             }
188         }
189 
setTimeVisible(boolean visible)190         void setTimeVisible(boolean visible) {
191             mTimeVisible = visible;
192             if (mActiveItemPos != null) {
193                 notifyItemChanged(mActiveItemPos.intValue());
194             }
195         }
196 
getCurrentTime()197         String getCurrentTime() {
198             return mCurrentTimeText;
199         }
200 
getMaxTime()201         String getMaxTime() {
202             return mMaxTimeText;
203         }
204 
getTimeVisible()205         boolean getTimeVisible() {
206             return mTimeVisible;
207         }
208 
209         @Override
onCreateViewHolder(ViewGroup parent, int viewType)210         public QueueViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
211             LayoutInflater inflater = LayoutInflater.from(parent.getContext());
212             return new QueueViewHolder(inflater.inflate(R.layout.queue_list_item, parent, false));
213         }
214 
215         @Override
onBindViewHolder(QueueViewHolder holder, int position)216         public void onBindViewHolder(QueueViewHolder holder, int position) {
217             int size = mQueueItems.size();
218             if (0 <= position && position < size) {
219                 boolean active = holder.bind(mQueueItems.get(position));
220                 if (active) {
221                     mActiveItemPos = position;
222                 }
223             } else {
224                 Log.e(TAG, "onBindViewHolder invalid position " + position + " of " + size);
225             }
226         }
227 
228         @Override
getItemCount()229         public int getItemCount() {
230             return mQueueItems.size();
231         }
232 
refresh()233         void refresh() {
234             // TODO: Perform a diff between current and new content and trigger the proper
235             // RecyclerView updates.
236             this.notifyDataSetChanged();
237         }
238 
239         @Override
getItemId(int position)240         public long getItemId(int position) {
241             return mQueueItems.get(position).getQueueId();
242         }
243     }
244 
245     private class QueueTopItemDecoration extends RecyclerView.ItemDecoration {
246         int mHeight;
247         int mDecorationPosition;
248 
QueueTopItemDecoration(int height, int decorationPosition)249         QueueTopItemDecoration(int height, int decorationPosition) {
250             mHeight = height;
251             mDecorationPosition = decorationPosition;
252         }
253 
254         @Override
getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state)255         public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
256                 RecyclerView.State state) {
257             super.getItemOffsets(outRect, view, parent, state);
258             if (parent.getChildAdapterPosition(view) == mDecorationPosition) {
259                 outRect.top = mHeight;
260             }
261         }
262     }
263 
264     @Override
onCreateView(@onNull LayoutInflater inflater, final ViewGroup container, Bundle savedInstanceState)265     public View onCreateView(@NonNull LayoutInflater inflater, final ViewGroup container,
266             Bundle savedInstanceState) {
267         View view = inflater.inflate(R.layout.fragment_playback, container, false);
268         mAlbumBackground = view.findViewById(R.id.playback_background);
269         mQueue = view.findViewById(R.id.queue_list);
270         mMetadataContainer = view.findViewById(R.id.metadata_container);
271         mSeekBar = view.findViewById(R.id.seek_bar);
272         mQueueButton = view.findViewById(R.id.queue_button);
273         mQueueButton.setOnClickListener(button -> toggleQueueVisibility());
274         mNavIconContainer = view.findViewById(R.id.nav_icon_container);
275         mNavIconContainer.setOnClickListener(nav -> onCollapse());
276         mBackgroundScrim = view.findViewById(R.id.background_scrim);
277         ViewUtils.setVisible(mBackgroundScrim, false);
278         mControlBarScrim = view.findViewById(R.id.control_bar_scrim);
279         ViewUtils.setVisible(mControlBarScrim, false);
280         mControlBarScrim.setOnClickListener(scrim -> mPlaybackControls.close());
281         mControlBarScrim.setClickable(false);
282 
283         Resources res = getResources();
284         mShowTimeForActiveQueueItem = res.getBoolean(
285                 R.bool.show_time_for_now_playing_queue_list_item);
286         mShowIconForActiveQueueItem = res.getBoolean(
287                 R.bool.show_icon_for_now_playing_queue_list_item);
288         mShowThumbnailForQueueItem = getContext().getResources().getBoolean(
289                 R.bool.show_thumbnail_for_queue_list_item);
290 
291         boolean useMediaSourceColor = res.getBoolean(
292                 R.bool.use_media_source_color_for_progress_bar);
293         int defaultColor = res.getColor(R.color.progress_bar_highlight, null);
294         if (useMediaSourceColor) {
295             getPlaybackViewModel().getMediaSourceColors().observe(getViewLifecycleOwner(),
296                     sourceColors -> {
297                         int color = sourceColors != null ? sourceColors.getAccentColor(defaultColor)
298                                 : defaultColor;
299                         mSeekBar.setThumbTintList(ColorStateList.valueOf(color));
300                         mSeekBar.setProgressTintList(ColorStateList.valueOf(color));
301                     });
302         } else {
303             mSeekBar.setThumbTintList(ColorStateList.valueOf(defaultColor));
304             mSeekBar.setProgressTintList(ColorStateList.valueOf(defaultColor));
305         }
306 
307         MediaAppSelectorWidget appIcon = view.findViewById(R.id.app_icon_container);
308         appIcon.setFragmentActivity(getActivity());
309 
310         getPlaybackViewModel().getPlaybackController().observe(getViewLifecycleOwner(),
311                 controller -> mController = controller);
312         initPlaybackControls(view.findViewById(R.id.playback_controls));
313         initMetadataController(view);
314         initQueue();
315 
316         TypedArray hideViewIds =
317                 res.obtainTypedArray(R.array.playback_views_to_hide_when_showing_custom_actions);
318         mViewsToHideForCustomActions = new ArrayList<>(hideViewIds.length());
319         for (int i = 0; i < hideViewIds.length(); i++) {
320             int viewId = hideViewIds.getResourceId(i, 0);
321             if (viewId != 0) {
322                 View viewToHide = view.findViewById(viewId);
323                 if (viewToHide != null) {
324                     mViewsToHideForCustomActions.add(viewToHide);
325                 }
326             }
327         }
328         hideViewIds.recycle();
329 
330         int albumBgSizePx = getResources().getInteger(
331                 com.android.car.apps.common.R.integer.background_bitmap_target_size_px);
332 
333         getPlaybackViewModel().getMetadata().observe(getViewLifecycleOwner(),
334                 metadata -> {
335                     if (mFutureAlbumBackground != null && !mFutureAlbumBackground.isDone()) {
336                         mFutureAlbumBackground.cancel(true);
337                     }
338                     if (metadata == null) {
339                         setBackgroundImage(null);
340                         mFutureAlbumBackground = null;
341                     } else {
342                         mFutureAlbumBackground = metadata.getAlbumArt(
343                                 getContext(), albumBgSizePx, albumBgSizePx, false);
344                         mFutureAlbumBackground.whenComplete((result, throwable) -> {
345                             if (throwable != null) {
346                                 setBackgroundImage(null);
347                             } else {
348                                 setBackgroundImage(result);
349                             }
350                         });
351                     }
352                 });
353 
354         return view;
355     }
356 
357     @Override
onAttach(Context context)358     public void onAttach(Context context) {
359         super.onAttach(context);
360     }
361 
362     @Override
onDetach()363     public void onDetach() {
364         super.onDetach();
365     }
366 
setBackgroundImage(Bitmap bitmap)367     private void setBackgroundImage(Bitmap bitmap) {
368         mAlbumBackground.setBackgroundImage(bitmap, bitmap != null);
369     }
370 
initPlaybackControls(PlaybackControlsActionBar playbackControls)371     private void initPlaybackControls(PlaybackControlsActionBar playbackControls) {
372         mPlaybackControls = playbackControls;
373         mPlaybackControls.setModel(getPlaybackViewModel(), getViewLifecycleOwner());
374         mPlaybackControls.registerExpandCollapseCallback((expanding) -> {
375             mControlBarScrim.setClickable(expanding);
376 
377             Resources res = getContext().getResources();
378             int millis = expanding ? res.getInteger(R.integer.control_bar_expand_anim_duration) :
379                     res.getInteger(R.integer.control_bar_collapse_anim_duration);
380 
381             if (expanding) {
382                 ViewUtils.showViewAnimated(mControlBarScrim, millis);
383             } else {
384                 ViewUtils.hideViewAnimated(mControlBarScrim, millis);
385             }
386 
387             if (!mQueueIsVisible) {
388                 for (View view : mViewsToHideForCustomActions) {
389                     if (expanding) {
390                         ViewUtils.hideViewAnimated(view, millis);
391                     } else {
392                         ViewUtils.showViewAnimated(view, millis);
393                     }
394                 }
395             }
396         });
397     }
398 
initQueue()399     private void initQueue() {
400         mFadeDuration = getResources().getInteger(
401                 R.integer.fragment_playback_queue_fade_duration_ms);
402         mPlaybackQueueBackgroundAlpha = getResources().getFloat(
403                 R.dimen.playback_queue_background_alpha);
404 
405         int decorationHeight = getResources().getDimensionPixelSize(
406                 R.dimen.playback_queue_list_padding_top);
407         // Put the decoration above the first item.
408         int decorationPosition = 0;
409         mQueue.addItemDecoration(new QueueTopItemDecoration(decorationHeight, decorationPosition));
410 
411         mQueue.setVerticalFadingEdgeEnabled(
412                 getResources().getBoolean(R.bool.queue_fading_edge_length_enabled));
413         mQueueAdapter = new QueueItemsAdapter();
414 
415         getPlaybackViewModel().getPlaybackStateWrapper().observe(getViewLifecycleOwner(),
416                 state -> {
417                     Long itemId = (state != null) ? state.getActiveQueueItemId() : null;
418                     if (!Objects.equals(mActiveQueueItemId, itemId)) {
419                         mActiveQueueItemId = itemId;
420                         mQueueAdapter.refresh();
421                     }
422                 });
423         mQueue.setAdapter(mQueueAdapter);
424         mQueue.setLayoutManager(new LinearLayoutManager(getContext()));
425 
426         // Disable item changed animation.
427         mItemAnimator = new DefaultItemAnimator();
428         mItemAnimator.setSupportsChangeAnimations(false);
429         mQueue.setItemAnimator(mItemAnimator);
430 
431         getPlaybackViewModel().getQueue().observe(this, this::setQueue);
432 
433         getPlaybackViewModel().hasQueue().observe(getViewLifecycleOwner(), hasQueue -> {
434             boolean enableQueue = (hasQueue != null) && hasQueue;
435             setHasQueue(enableQueue);
436             if (mQueueIsVisible && !enableQueue) {
437                 toggleQueueVisibility();
438             }
439         });
440         getPlaybackViewModel().getProgress().observe(getViewLifecycleOwner(),
441                 playbackProgress ->
442                 {
443                     mQueueAdapter.setCurrentTime(playbackProgress.getCurrentTimeText().toString());
444                     mQueueAdapter.setMaxTime(playbackProgress.getMaxTimeText().toString());
445                     mQueueAdapter.setTimeVisible(playbackProgress.hasTime());
446                 });
447     }
448 
setQueue(List<MediaItemMetadata> queueItems)449     private void setQueue(List<MediaItemMetadata> queueItems) {
450         mQueueAdapter.setItems(queueItems);
451         mQueueAdapter.refresh();
452     }
453 
initMetadataController(View view)454     private void initMetadataController(View view) {
455         ImageView albumArt = view.findViewById(R.id.album_art);
456         TextView title = view.findViewById(R.id.title);
457         TextView artist = view.findViewById(R.id.artist);
458         TextView albumTitle = view.findViewById(R.id.album_title);
459         TextView outerSeparator = view.findViewById(R.id.outer_separator);
460         TextView curTime = view.findViewById(R.id.current_time);
461         TextView innerSeparator = view.findViewById(R.id.inner_separator);
462         TextView maxTime = view.findViewById(R.id.max_time);
463         SeekBar seekbar = view.findViewById(R.id.seek_bar);
464 
465         mMetadataController = new MetadataController(getViewLifecycleOwner(),
466                 getPlaybackViewModel(), title, artist, albumTitle, outerSeparator,
467                 curTime, innerSeparator, maxTime, seekbar, albumArt,
468                 getResources().getDimensionPixelSize(R.dimen.playback_album_art_size));
469     }
470 
471     /**
472      * Hides or shows the playback queue.
473      */
toggleQueueVisibility()474     private void toggleQueueVisibility() {
475         mQueueIsVisible = !mQueueIsVisible;
476         mQueueButton.setActivated(mQueueIsVisible);
477         mQueueButton.setSelected(mQueueIsVisible);
478         if (mQueueIsVisible) {
479             ViewUtils.hideViewAnimated(mMetadataContainer, mFadeDuration);
480             ViewUtils.hideViewAnimated(mSeekBar, mFadeDuration);
481             ViewUtils.showViewAnimated(mQueue, mFadeDuration);
482             ViewUtils.showViewAnimated(mBackgroundScrim, mFadeDuration);
483         } else {
484             ViewUtils.hideViewAnimated(mQueue, mFadeDuration);
485             ViewUtils.showViewAnimated(mMetadataContainer, mFadeDuration);
486             ViewUtils.showViewAnimated(mSeekBar, mFadeDuration);
487             ViewUtils.hideViewAnimated(mBackgroundScrim, mFadeDuration);
488         }
489     }
490 
491     /** Sets whether the source has a queue. */
setHasQueue(boolean hasQueue)492     private void setHasQueue(boolean hasQueue) {
493         mHasQueue = hasQueue;
494         updateQueueVisibility();
495     }
496 
updateQueueVisibility()497     private void updateQueueVisibility() {
498         mQueueButton.setVisibility(mHasQueue ? View.VISIBLE : View.GONE);
499     }
500 
onQueueItemClicked(MediaItemMetadata item)501     private void onQueueItemClicked(MediaItemMetadata item) {
502         if (mController != null) {
503             mController.skipToQueueItem(item.getQueueId());
504         }
505         boolean switchToPlayback = getResources().getBoolean(
506                 R.bool.switch_to_playback_view_when_playable_item_is_clicked);
507         if (switchToPlayback) {
508             toggleQueueVisibility();
509         }
510     }
511 
512     /**
513      * Collapses the playback controls.
514      */
closeOverflowMenu()515     public void closeOverflowMenu() {
516         mPlaybackControls.close();
517     }
518 
getPlaybackViewModel()519     private PlaybackViewModel getPlaybackViewModel() {
520         return PlaybackViewModel.get(getActivity().getApplication());
521     }
522 
523     /**
524      * Sets a listener of this PlaybackFragment events. In order to avoid memory leaks, consumers
525      * must reset this reference by setting the listener to null.
526      */
setListener(PlaybackFragmentListener listener)527     public void setListener(PlaybackFragmentListener listener) {
528         mListener = listener;
529     }
530 
onCollapse()531     private void onCollapse() {
532         if (mListener != null) {
533             mListener.onCollapse();
534         }
535     }
536 }
537