diff options
author | Babak Bostan <babakbo@google.com> | 2023-01-31 19:26:39 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2023-01-31 19:26:39 +0000 |
commit | 2e31dedd394e6f02f002148793b491f4d5adcaa3 (patch) | |
tree | 8742d6089300a7734e1f98e71bf765e094d3637f | |
parent | 524ce85a13b754a755b2d934a461532099b56f68 (diff) | |
parent | 85a419a0d115b542581180577b7d9f2ba5adf890 (diff) | |
download | Media-2e31dedd394e6f02f002148793b491f4d5adcaa3.tar.gz |
Merge "Make the queue section of the playback screen a fragment" into car-apps-dev
-rw-r--r-- | res/layout/fragment_playback.xml | 20 | ||||
-rw-r--r-- | res/layout/fragment_playback_queue.xml | 39 | ||||
-rw-r--r-- | res/values/integers.xml | 2 | ||||
-rw-r--r-- | res/values/overlayable.xml | 2 | ||||
-rw-r--r-- | src/com/android/car/media/PlaybackFragment.java | 450 | ||||
-rw-r--r-- | src/com/android/car/media/PlaybackQueueFragment.java | 531 |
6 files changed, 599 insertions, 445 deletions
diff --git a/res/layout/fragment_playback.xml b/res/layout/fragment_playback.xml index 3854b49..37ff21f 100644 --- a/res/layout/fragment_playback.xml +++ b/res/layout/fragment_playback.xml @@ -96,24 +96,14 @@ android:layout_height="@dimen/fragment_playback_queue_overlap_bottom" app:layout_constraintTop_toTopOf="@+id/control_bar_first_row_guideline"/> - <com.android.car.ui.FocusArea - android:id="@+id/queue_container" + <FrameLayout + android:id="@+id/queue_fragment_container" android:layout_width="match_parent" android:layout_height="0dp" + android:visibility="gone" app:layout_constraintTop_toTopOf="@+id/queue_list_top_constraint" - app:layout_constraintBottom_toBottomOf="@+id/queue_list_bottom_constraint"> - <com.android.car.ui.recyclerview.CarUiRecyclerView - android:id="@+id/queue_list" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:visibility="gone" - android:fadeScrollbars="true" - android:scrollbars="vertical" - android:requiresFadingEdge="vertical" - android:fadingEdgeLength="@dimen/queue_fading_edge_length"/> - <!-- NOTE: we must specify :scrollbars before :requiresFadingEdge to avoid a crash on R - (see b/253505704). --> - </com.android.car.ui.FocusArea> + app:layout_constraintBottom_toBottomOf="@+id/queue_list_bottom_constraint" + /> <include layout="@layout/scrim_overlay" diff --git a/res/layout/fragment_playback_queue.xml b/res/layout/fragment_playback_queue.xml new file mode 100644 index 0000000..4f4e00c --- /dev/null +++ b/res/layout/fragment_playback_queue.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2022, The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <com.android.car.ui.FocusArea + android:id="@+id/queue_container" + android:layout_width="match_parent" + android:layout_height="match_parent"> + <com.android.car.ui.recyclerview.CarUiRecyclerView + android:id="@+id/queue_list" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:fadeScrollbars="true" + android:scrollbars="vertical" + android:requiresFadingEdge="vertical" + android:fadingEdgeLength="@dimen/queue_fading_edge_length"/> + <!-- NOTE: we must specify :scrollbars before :requiresFadingEdge to avoid a crash on R + (see b/253505704). --> + </com.android.car.ui.FocusArea> + +</FrameLayout> diff --git a/res/values/integers.xml b/res/values/integers.xml index 2714052..4d6a8c2 100644 --- a/res/values/integers.xml +++ b/res/values/integers.xml @@ -49,7 +49,7 @@ <!-- Views to show when the queue is visible (to hide when the queue becomes invisible). --> <integer-array name="playback_views_to_show_when_queue_is_visible"> - <item>@id/queue_list</item> + <item>@id/queue_fragment_container</item> <item>@id/background_scrim</item> </integer-array> diff --git a/res/values/overlayable.xml b/res/values/overlayable.xml index c7acccc..755a38a 100644 --- a/res/values/overlayable.xml +++ b/res/values/overlayable.xml @@ -124,6 +124,7 @@ REGENERATE USING packages/apps/Car/tests/tools/rro/generate-overlayable.py <item type="id" name="queue_list_item_title"/> <item type="id" name="queue_list_item_titles_container"/> <item type="id" name="queue_list_top_constraint"/> + <item type="id" name="queue_fragment_container"/> <item type="id" name="right_arrow"/> <item type="id" name="separator"/> <item type="id" name="spacer"/> @@ -161,6 +162,7 @@ REGENERATE USING packages/apps/Car/tests/tools/rro/generate-overlayable.py <item type="layout" name="fragment_browse"/> <item type="layout" name="fragment_error"/> <item type="layout" name="fragment_playback"/> + <item type="layout" name="fragment_playback_queue"/> <item type="layout" name="media_activity"/> <item type="layout" name="media_browse_grid_icons_item"/> <item type="layout" name="media_browse_grid_item"/> diff --git a/src/com/android/car/media/PlaybackFragment.java b/src/com/android/car/media/PlaybackFragment.java index 58d18ac..438f739 100644 --- a/src/com/android/car/media/PlaybackFragment.java +++ b/src/com/android/car/media/PlaybackFragment.java @@ -22,12 +22,10 @@ import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Resources; import android.graphics.PorterDuff; -import android.graphics.Rect; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.os.Bundle; -import android.util.Log; import android.util.Size; import android.view.LayoutInflater; import android.view.View; @@ -41,14 +39,12 @@ import androidx.annotation.Nullable; import androidx.core.util.Preconditions; import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProviders; -import androidx.recyclerview.widget.DefaultItemAnimator; -import androidx.recyclerview.widget.RecyclerView; import com.android.car.apps.common.BackgroundImageView; import com.android.car.apps.common.imaging.ImageBinder; import com.android.car.apps.common.imaging.ImageBinder.PlaceholderType; -import com.android.car.apps.common.imaging.ImageViewBinder; import com.android.car.apps.common.util.ViewUtils; +import com.android.car.media.PlaybackQueueFragment.PlaybackQueueCallback; import com.android.car.media.common.MediaItemMetadata; import com.android.car.media.common.MetadataController; import com.android.car.media.common.PlaybackControlsActionBar; @@ -56,20 +52,13 @@ import com.android.car.media.common.playback.PlaybackViewModel; import com.android.car.media.common.source.MediaSourceViewModel; import com.android.car.media.widgets.AppBarController; import com.android.car.ui.core.CarUi; -import com.android.car.ui.recyclerview.CarUiRecyclerView; -import com.android.car.ui.recyclerview.ContentLimiting; -import com.android.car.ui.recyclerview.ScrollingLimitedViewHolder; import com.android.car.ui.toolbar.MenuItem; import com.android.car.ui.toolbar.NavButtonMode; import com.android.car.ui.toolbar.ToolbarController; import com.android.car.ui.utils.DirectManipulationHelper; import com.android.car.uxr.LifeCycleObserverUxrContentLimiter; -import com.android.car.uxr.UxrContentLimiterImpl; -import java.util.ArrayList; -import java.util.Collections; import java.util.List; -import java.util.Objects; /** @@ -84,12 +73,11 @@ public class PlaybackFragment extends Fragment { private ImageBinder<MediaItemMetadata.ArtworkRef> mAlbumArtBinder; private AppBarController mAppBarController; private BackgroundImageView mAlbumBackground; + private PlaybackQueueFragment mPlaybackQueueFragment; private View mBackgroundScrim; private View mControlBarScrim; private PlaybackControlsActionBar mPlaybackControls; private PlaybackViewModel mPlaybackViewModel; - private QueueItemsAdapter mQueueAdapter; - private CarUiRecyclerView mQueue; private ViewGroup mSeekBarContainer; private SeekBar mSeekBar; private List<View> mViewsToHideForCustomActions; @@ -98,21 +86,12 @@ public class PlaybackFragment extends Fragment { private List<View> mViewsToHideImmediatelyWhenQueueIsVisible; private List<View> mViewsToShowImmediatelyWhenQueueIsVisible; - private DefaultItemAnimator mItemAnimator; - private MetadataController mMetadataController; private PlaybackFragmentListener mListener; - private PlaybackViewModel.PlaybackController mController; - private Long mActiveQueueItemId; - private boolean mHasQueue; private boolean mQueueIsVisible; - private boolean mShowTimeForActiveQueueItem; - private boolean mShowIconForActiveQueueItem; - private boolean mShowThumbnailForQueueItem; - private boolean mShowSubtitleForQueueItem; private boolean mShowLinearProgressBar; @@ -122,6 +101,18 @@ public class PlaybackFragment extends Fragment { private MenuItem mQueueMenuItem; + private PlaybackQueueFragment.PlaybackQueueCallback mPlaybackQueueCallback = + new PlaybackQueueCallback() { + @Override + public void onQueueItemClicked(MediaItemMetadata item) { + boolean switchToPlayback = getResources().getBoolean( + R.bool.switch_to_playback_view_when_playable_item_is_clicked); + if (switchToPlayback) { + toggleQueueVisibility(); + } + } + }; + /** * PlaybackFragment listener */ @@ -132,340 +123,6 @@ public class PlaybackFragment extends Fragment { void onCollapse(); } - public class QueueViewHolder extends RecyclerView.ViewHolder { - - private final View mView; - private final ViewGroup mThumbnailContainer; - private final ImageView mThumbnail; - private final View mSpacer; - private final TextView mTitle; - private final TextView mSubtitle; - private final TextView mCurrentTime; - private final TextView mMaxTime; - private final TextView mTimeSeparator; - private final ImageView mActiveIcon; - - private final ImageViewBinder<MediaItemMetadata.ArtworkRef> mThumbnailBinder; - - QueueViewHolder(View itemView) { - super(itemView); - mView = itemView; - mThumbnailContainer = itemView.findViewById(R.id.thumbnail_container); - mThumbnail = itemView.findViewById(R.id.thumbnail); - mSpacer = itemView.findViewById(R.id.spacer); - mTitle = itemView.findViewById(R.id.queue_list_item_title); - mSubtitle = itemView.findViewById(R.id.queue_list_item_subtitle); - mCurrentTime = itemView.findViewById(R.id.current_time); - mMaxTime = itemView.findViewById(R.id.max_time); - mTimeSeparator = itemView.findViewById(R.id.separator); - mActiveIcon = itemView.findViewById(R.id.now_playing_icon); - - Size maxArtSize = MediaAppConfig.getMediaItemsBitmapMaxSize(itemView.getContext()); - mThumbnailBinder = new ImageViewBinder<>(maxArtSize, mThumbnail); - } - - void bind(MediaItemMetadata item) { - mView.setOnClickListener(v -> onQueueItemClicked(item)); - - ViewUtils.setVisible(mThumbnailContainer, mShowThumbnailForQueueItem); - if (mShowThumbnailForQueueItem) { - Context context = mView.getContext(); - mThumbnailBinder.setImage(context, item != null ? item.getArtworkKey() : null); - } - - ViewUtils.setVisible(mSpacer, !mShowThumbnailForQueueItem); - - mTitle.setText(item.getTitle()); - - boolean active = mActiveQueueItemId != null && Objects.equals(mActiveQueueItemId, - item.getQueueId()); - if (active) { - mCurrentTime.setText(mQueueAdapter.getCurrentTime()); - mMaxTime.setText(mQueueAdapter.getMaxTime()); - } - boolean shouldShowTime = - mShowTimeForActiveQueueItem && active && mQueueAdapter.getTimeVisible(); - ViewUtils.setVisible(mCurrentTime, shouldShowTime); - ViewUtils.setVisible(mMaxTime, shouldShowTime); - ViewUtils.setVisible(mTimeSeparator, shouldShowTime); - - mView.setSelected(active); - - boolean shouldShowIcon = mShowIconForActiveQueueItem && active; - ViewUtils.setVisible(mActiveIcon, shouldShowIcon); - - if (mShowSubtitleForQueueItem) { - mSubtitle.setText(item.getSubtitle()); - } - } - - void onViewAttachedToWindow() { - if (mShowThumbnailForQueueItem) { - Context context = mView.getContext(); - mThumbnailBinder.maybeRestartLoading(context); - } - } - - void onViewDetachedFromWindow() { - if (mShowThumbnailForQueueItem) { - Context context = mView.getContext(); - mThumbnailBinder.maybeCancelLoading(context); - } - } - } - - - private class QueueItemsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> - implements ContentLimiting { - - private static final int CLAMPED_MESSAGE_VIEW_TYPE = -1; - private static final int QUEUE_ITEM_VIEW_TYPE = 0; - - private UxrPivotFilter mUxrPivotFilter; - private List<MediaItemMetadata> mQueueItems = Collections.emptyList(); - private String mCurrentTimeText = ""; - private String mMaxTimeText = ""; - /** Index in {@link #mQueueItems}. */ - private Integer mActiveItemIndex; - private boolean mTimeVisible; - private Integer mScrollingLimitedMessageResId; - - QueueItemsAdapter() { - mUxrPivotFilter = UxrPivotFilter.PASS_THROUGH; - } - - @Override - public void setMaxItems(int maxItems) { - if (maxItems >= 0) { - mUxrPivotFilter = new UxrPivotFilterImpl(this, maxItems); - } else { - mUxrPivotFilter = UxrPivotFilter.PASS_THROUGH; - } - applyFilterToQueue(); - } - - @Override - public void setScrollingLimitedMessageResId(int resId) { - if (mScrollingLimitedMessageResId == null || mScrollingLimitedMessageResId != resId) { - mScrollingLimitedMessageResId = resId; - mUxrPivotFilter.invalidateMessagePositions(); - } - } - - @Override - public int getConfigurationId() { - return R.id.playback_fragment_now_playing_list_uxr_config; - } - - void setItems(@Nullable List<MediaItemMetadata> items) { - List<MediaItemMetadata> newQueueItems = - new ArrayList<>(items != null ? items : Collections.emptyList()); - if (newQueueItems.equals(mQueueItems)) { - return; - } - mQueueItems = newQueueItems; - updateActiveItem(/* listIsNew */ true); - } - - private int getActiveItemIndex() { - return mActiveItemIndex != null ? mActiveItemIndex : 0; - } - - private int getQueueSize() { - return (mQueueItems != null) ? mQueueItems.size() : 0; - } - - - /** - * Returns the position of the active item if there is one, otherwise returns - * @link UxrPivotFilter#INVALID_POSITION}. - */ - private int getActiveItemPosition() { - if (mActiveItemIndex == null) { - return UxrPivotFilter.INVALID_POSITION; - } - return mUxrPivotFilter.indexToPosition(mActiveItemIndex); - } - - private void invalidateActiveItemPosition() { - int position = getActiveItemPosition(); - if (position != UxrPivotFilterImpl.INVALID_POSITION) { - notifyItemChanged(position); - } - } - - private void scrollToActiveItemPosition() { - int position = getActiveItemPosition(); - if (position != UxrPivotFilterImpl.INVALID_POSITION) { - mQueue.scrollToPosition(position); - } - } - - private void applyFilterToQueue() { - mUxrPivotFilter.recompute(getQueueSize(), getActiveItemIndex()); - notifyDataSetChanged(); - } - - // Updates mActiveItemPos, then scrolls the queue to mActiveItemPos. - // It should be called when the active item (mActiveQueueItemId) changed or - // the queue items (mQueueItems) changed. - void updateActiveItem(boolean listIsNew) { - if (mQueueItems == null || mActiveQueueItemId == null) { - mActiveItemIndex = null; - applyFilterToQueue(); - return; - } - Integer activeItemPos = null; - for (int i = 0; i < mQueueItems.size(); i++) { - if (Objects.equals(mQueueItems.get(i).getQueueId(), mActiveQueueItemId)) { - activeItemPos = i; - break; - } - } - - // Invalidate the previous active item so it gets redrawn as a normal one. - invalidateActiveItemPosition(); - - mActiveItemIndex = activeItemPos; - if (listIsNew) { - applyFilterToQueue(); - } else { - mUxrPivotFilter.updatePivotIndex(getActiveItemIndex()); - } - - scrollToActiveItemPosition(); - invalidateActiveItemPosition(); - } - - void setCurrentTime(String currentTime) { - if (!mCurrentTimeText.equals(currentTime)) { - mCurrentTimeText = currentTime; - invalidateActiveItemPosition(); - } - } - - void setMaxTime(String maxTime) { - if (!mMaxTimeText.equals(maxTime)) { - mMaxTimeText = maxTime; - invalidateActiveItemPosition(); - } - } - - void setTimeVisible(boolean visible) { - if (mTimeVisible != visible) { - mTimeVisible = visible; - invalidateActiveItemPosition(); - } - } - - String getCurrentTime() { - return mCurrentTimeText; - } - - String getMaxTime() { - return mMaxTimeText; - } - - boolean getTimeVisible() { - return mTimeVisible; - } - - @Override - public final int getItemViewType(int position) { - if (mUxrPivotFilter.positionToIndex(position) == UxrPivotFilterImpl.INVALID_INDEX) { - return CLAMPED_MESSAGE_VIEW_TYPE; - } else { - return QUEUE_ITEM_VIEW_TYPE; - } - } - - @Override - public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - if (viewType == CLAMPED_MESSAGE_VIEW_TYPE) { - return ScrollingLimitedViewHolder.create(parent); - } - LayoutInflater inflater = LayoutInflater.from(parent.getContext()); - return new QueueViewHolder(inflater.inflate(R.layout.queue_list_item, parent, false)); - } - - @Override - public void onBindViewHolder(RecyclerView.ViewHolder vh, int position) { - if (vh instanceof QueueViewHolder) { - int index = mUxrPivotFilter.positionToIndex(position); - if (index != UxrPivotFilterImpl.INVALID_INDEX) { - int size = mQueueItems.size(); - if (0 <= index && index < size) { - QueueViewHolder holder = (QueueViewHolder) vh; - holder.bind(mQueueItems.get(index)); - } else { - Log.e(TAG, "onBindViewHolder pos: " + position + " gave index: " + index + - " out of bounds size: " + size + " " + mUxrPivotFilter.toString()); - } - } else { - Log.e(TAG, "onBindViewHolder invalid position " + position + " " + - mUxrPivotFilter.toString()); - } - } else if (vh instanceof ScrollingLimitedViewHolder) { - ScrollingLimitedViewHolder holder = (ScrollingLimitedViewHolder) vh; - holder.bind(mScrollingLimitedMessageResId); - } else { - throw new IllegalArgumentException("unknown holder class " + vh.getClass()); - } - } - - @Override - public void onViewAttachedToWindow(@NonNull RecyclerView.ViewHolder vh) { - super.onViewAttachedToWindow(vh); - if (vh instanceof QueueViewHolder) { - QueueViewHolder holder = (QueueViewHolder) vh; - holder.onViewAttachedToWindow(); - } - } - - @Override - public void onViewDetachedFromWindow(@NonNull RecyclerView.ViewHolder vh) { - super.onViewDetachedFromWindow(vh); - if (vh instanceof QueueViewHolder) { - QueueViewHolder holder = (QueueViewHolder) vh; - holder.onViewDetachedFromWindow(); - } - } - - @Override - public int getItemCount() { - return mUxrPivotFilter.getFilteredCount(); - } - - @Override - public long getItemId(int position) { - int index = mUxrPivotFilter.positionToIndex(position); - if (index != UxrPivotFilterImpl.INVALID_INDEX) { - return mQueueItems.get(position).getQueueId(); - } else { - return RecyclerView.NO_ID; - } - } - } - - private static class QueueTopItemDecoration extends RecyclerView.ItemDecoration { - int mHeight; - int mDecorationPosition; - - QueueTopItemDecoration(int height, int decorationPosition) { - mHeight = height; - mDecorationPosition = decorationPosition; - } - - @Override - public void getItemOffsets(Rect outRect, View view, RecyclerView parent, - RecyclerView.State state) { - super.getItemOffsets(outRect, view, parent, state); - if (parent.getChildAdapterPosition(view) == mDecorationPosition) { - outRect.top = mHeight; - } - } - } - @Override public View onCreateView(@NonNull LayoutInflater inflater, final ViewGroup container, Bundle savedInstanceState) { @@ -479,7 +136,13 @@ public class PlaybackFragment extends Fragment { Resources res = getResources(); mAlbumBackground = view.findViewById(R.id.playback_background); - mQueue = view.findViewById(R.id.queue_list); + mPlaybackQueueFragment = new PlaybackQueueFragment(); + mPlaybackQueueFragment.setCallback(mPlaybackQueueCallback); + + getChildFragmentManager().beginTransaction() + .add(R.id.queue_fragment_container, mPlaybackQueueFragment) + .commit(); + mSeekBarContainer = view.findViewById(R.id.playback_seek_bar_container); mSeekBar = view.findViewById(R.id.playback_seek_bar); DirectManipulationHelper.setSupportsRotateDirectly(mSeekBar, true); @@ -514,17 +177,6 @@ public class PlaybackFragment extends Fragment { mControlBarScrim.setClickable(false); } - mShowTimeForActiveQueueItem = res.getBoolean( - R.bool.show_time_for_now_playing_queue_list_item); - mShowIconForActiveQueueItem = res.getBoolean( - R.bool.show_icon_for_now_playing_queue_list_item); - mShowThumbnailForQueueItem = getContext().getResources().getBoolean( - R.bool.show_thumbnail_for_queue_list_item); - mShowLinearProgressBar = getContext().getResources().getBoolean( - R.bool.show_linear_progress_bar); - mShowSubtitleForQueueItem = getContext().getResources().getBoolean( - R.bool.show_subtitle_for_queue_list_item); - if (mSeekBar != null) { if (mShowLinearProgressBar) { boolean useMediaSourceColor = res.getBoolean( @@ -548,8 +200,6 @@ public class PlaybackFragment extends Fragment { mViewModel = ViewModelProviders.of(requireActivity()).get(MediaActivity.ViewModel.class); - mPlaybackViewModel.getPlaybackController().observe(getViewLifecycleOwner(), - controller -> mController = controller); initPlaybackControls(view.findViewById(R.id.playback_controls)); initMetadataController(view); initQueue(); @@ -577,11 +227,6 @@ public class PlaybackFragment extends Fragment { mPlaybackViewModel.getMetadata().observe(getViewLifecycleOwner(), item -> mAlbumArtBinder.setImage(PlaybackFragment.this.getContext(), item != null ? item.getArtworkKey() : null)); - - mUxrContentLimiter = new LifeCycleObserverUxrContentLimiter( - new UxrContentLimiterImpl(getContext(), R.xml.uxr_config)); - mUxrContentLimiter.setAdapter(mQueueAdapter); - getLifecycle().addObserver(mUxrContentLimiter); } @Override @@ -632,60 +277,18 @@ public class PlaybackFragment extends Fragment { mFadeDuration = getResources().getInteger( R.integer.fragment_playback_queue_fade_duration_ms); - int decorationHeight = getResources().getDimensionPixelSize( - R.dimen.playback_queue_list_padding_top); - // TODO (b/206038962): addItemDecoration is not supported anymore. Find another way to - // support this. - // Put the decoration above the first item. - int decorationPosition = 0; - mQueue.addItemDecoration(new QueueTopItemDecoration(decorationHeight, decorationPosition)); - - mQueue.setVerticalFadingEdgeEnabled( - getResources().getBoolean(R.bool.queue_fading_edge_length_enabled)); - mQueueAdapter = new QueueItemsAdapter(); - - mPlaybackViewModel.getPlaybackStateWrapper().observe(getViewLifecycleOwner(), - state -> { - Long itemId = (state != null) ? state.getActiveQueueItemId() : null; - if (!Objects.equals(mActiveQueueItemId, itemId)) { - mActiveQueueItemId = itemId; - mQueueAdapter.updateActiveItem(/* listIsNew */ false); - } - }); - mQueue.setAdapter(mQueueAdapter); - - // Disable item changed animation. - mItemAnimator = new DefaultItemAnimator(); - mItemAnimator.setSupportsChangeAnimations(false); - mQueue.setItemAnimator(mItemAnimator); - // Make sure the AppBar menu reflects the initial state of playback fragment. updateAppBarMenu(mHasQueue); if (mQueueMenuItem != null) { mQueueMenuItem.setActivated(mQueueIsVisible); } - mPlaybackViewModel.getQueue().observe(this, this::setQueue); - mPlaybackViewModel.hasQueue().observe(getViewLifecycleOwner(), hasQueue -> { boolean enableQueue = (hasQueue != null) && hasQueue; boolean isQueueVisible = enableQueue && mViewModel.getQueueVisible(); setQueueState(enableQueue, isQueueVisible); }); - - mPlaybackViewModel.getProgress().observe( - getViewLifecycleOwner(), - playbackProgress -> - { - mQueueAdapter.setCurrentTime(playbackProgress.getCurrentTimeText().toString()); - mQueueAdapter.setMaxTime(playbackProgress.getMaxTimeText().toString()); - mQueueAdapter.setTimeVisible(playbackProgress.hasTime()); - }); - } - - private void setQueue(List<MediaItemMetadata> queueItems) { - mQueueAdapter.setItems(queueItems); } private void initMetadataController(View view) { @@ -746,17 +349,6 @@ public class PlaybackFragment extends Fragment { } } - private void onQueueItemClicked(MediaItemMetadata item) { - if (mController != null) { - mController.skipToQueueItem(item.getQueueId()); - } - boolean switchToPlayback = getResources().getBoolean( - R.bool.switch_to_playback_view_when_playable_item_is_clicked); - if (switchToPlayback) { - toggleQueueVisibility(); - } - } - /** * Collapses the playback controls. */ diff --git a/src/com/android/car/media/PlaybackQueueFragment.java b/src/com/android/car/media/PlaybackQueueFragment.java new file mode 100644 index 0000000..041065b --- /dev/null +++ b/src/com/android/car/media/PlaybackQueueFragment.java @@ -0,0 +1,531 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.car.media; + +import static android.car.media.CarMediaManager.MEDIA_SOURCE_MODE_PLAYBACK; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Rect; +import android.os.Bundle; +import android.util.Log; +import android.util.Size; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.DefaultItemAnimator; +import androidx.recyclerview.widget.RecyclerView; + +import com.android.car.apps.common.imaging.ImageViewBinder; +import com.android.car.apps.common.util.ViewUtils; +import com.android.car.media.common.MediaItemMetadata; +import com.android.car.media.common.playback.PlaybackViewModel; +import com.android.car.ui.recyclerview.CarUiRecyclerView; +import com.android.car.ui.recyclerview.ContentLimiting; +import com.android.car.ui.recyclerview.ScrollingLimitedViewHolder; +import com.android.car.uxr.LifeCycleObserverUxrContentLimiter; +import com.android.car.uxr.UxrContentLimiterImpl; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + + +/** + * A {@link Fragment} that implements the playback queue experience. It observes a {@link + * PlaybackViewModel} and updates its information depending on the currently playing media source + * through the {@link android.media.session.MediaSession} API. + */ +public class PlaybackQueueFragment extends Fragment { + + private static final String TAG = "PlaybackQueueFragment"; + + private LifeCycleObserverUxrContentLimiter mUxrContentLimiter; + private PlaybackViewModel mPlaybackViewModel; + private QueueItemsAdapter mQueueAdapter; + private CarUiRecyclerView mQueue; + private PlaybackQueueCallback mPlaybackQueueCallback; + + private DefaultItemAnimator mItemAnimator; + + private PlaybackViewModel.PlaybackController mController; + private Long mActiveQueueItemId; + + private boolean mShowTimeForActiveQueueItem; + private boolean mShowIconForActiveQueueItem; + private boolean mShowThumbnailForQueueItem; + private boolean mShowSubtitleForQueueItem; + + /** + * The callbacks used to communicate the user interactions to the queue fragment listeners. + */ + public interface PlaybackQueueCallback { + + /** + * Will be called when a queue item is selected by the user. + **/ + void onQueueItemClicked(MediaItemMetadata item); + } + + /** + * The view holder for the queue items. + */ + public class QueueViewHolder extends RecyclerView.ViewHolder { + + private final View mView; + private final ViewGroup mThumbnailContainer; + private final ImageView mThumbnail; + private final View mSpacer; + private final TextView mTitle; + private final TextView mSubtitle; + private final TextView mCurrentTime; + private final TextView mMaxTime; + private final TextView mTimeSeparator; + private final ImageView mActiveIcon; + + private final ImageViewBinder<MediaItemMetadata.ArtworkRef> mThumbnailBinder; + + QueueViewHolder(View itemView) { + super(itemView); + mView = itemView; + mThumbnailContainer = itemView.findViewById(R.id.thumbnail_container); + mThumbnail = itemView.findViewById(R.id.thumbnail); + mSpacer = itemView.findViewById(R.id.spacer); + mTitle = itemView.findViewById(R.id.queue_list_item_title); + mSubtitle = itemView.findViewById(R.id.queue_list_item_subtitle); + mCurrentTime = itemView.findViewById(R.id.current_time); + mMaxTime = itemView.findViewById(R.id.max_time); + mTimeSeparator = itemView.findViewById(R.id.separator); + mActiveIcon = itemView.findViewById(R.id.now_playing_icon); + + Size maxArtSize = MediaAppConfig.getMediaItemsBitmapMaxSize(itemView.getContext()); + mThumbnailBinder = new ImageViewBinder<>(maxArtSize, mThumbnail); + } + + void bind(MediaItemMetadata item) { + mView.setOnClickListener(v -> onQueueItemClicked(item)); + + ViewUtils.setVisible(mThumbnailContainer, mShowThumbnailForQueueItem); + if (mShowThumbnailForQueueItem) { + Context context = mView.getContext(); + mThumbnailBinder.setImage(context, item != null ? item.getArtworkKey() : null); + } + + ViewUtils.setVisible(mSpacer, !mShowThumbnailForQueueItem); + + mTitle.setText(item.getTitle()); + + boolean active = mActiveQueueItemId != null && Objects.equals(mActiveQueueItemId, + item.getQueueId()); + if (active) { + mCurrentTime.setText(mQueueAdapter.getCurrentTime()); + mMaxTime.setText(mQueueAdapter.getMaxTime()); + } + boolean shouldShowTime = + mShowTimeForActiveQueueItem && active && mQueueAdapter.getTimeVisible(); + ViewUtils.setVisible(mCurrentTime, shouldShowTime); + ViewUtils.setVisible(mMaxTime, shouldShowTime); + ViewUtils.setVisible(mTimeSeparator, shouldShowTime); + + mView.setSelected(active); + + boolean shouldShowIcon = mShowIconForActiveQueueItem && active; + ViewUtils.setVisible(mActiveIcon, shouldShowIcon); + + if (mShowSubtitleForQueueItem) { + mSubtitle.setText(item.getSubtitle()); + } + } + + void onViewAttachedToWindow() { + if (mShowThumbnailForQueueItem) { + Context context = mView.getContext(); + mThumbnailBinder.maybeRestartLoading(context); + } + } + + void onViewDetachedFromWindow() { + if (mShowThumbnailForQueueItem) { + Context context = mView.getContext(); + mThumbnailBinder.maybeCancelLoading(context); + } + } + } + + + private class QueueItemsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> + implements ContentLimiting { + + private static final int CLAMPED_MESSAGE_VIEW_TYPE = -1; + private static final int QUEUE_ITEM_VIEW_TYPE = 0; + + private UxrPivotFilter mUxrPivotFilter; + private List<MediaItemMetadata> mQueueItems = Collections.emptyList(); + private String mCurrentTimeText = ""; + private String mMaxTimeText = ""; + /** + * Index in {@link #mQueueItems}. + */ + private Integer mActiveItemIndex; + private boolean mTimeVisible; + private Integer mScrollingLimitedMessageResId; + + QueueItemsAdapter() { + mUxrPivotFilter = UxrPivotFilter.PASS_THROUGH; + } + + @Override + public void setMaxItems(int maxItems) { + if (maxItems >= 0) { + mUxrPivotFilter = new UxrPivotFilterImpl(this, maxItems); + } else { + mUxrPivotFilter = UxrPivotFilter.PASS_THROUGH; + } + applyFilterToQueue(); + } + + @Override + public void setScrollingLimitedMessageResId(int resId) { + if (mScrollingLimitedMessageResId == null || mScrollingLimitedMessageResId != resId) { + mScrollingLimitedMessageResId = resId; + mUxrPivotFilter.invalidateMessagePositions(); + } + } + + @Override + public int getConfigurationId() { + return R.id.playback_fragment_now_playing_list_uxr_config; + } + + void setItems(@Nullable List<MediaItemMetadata> items) { + List<MediaItemMetadata> newQueueItems = + new ArrayList<>(items != null ? items : Collections.emptyList()); + if (newQueueItems.equals(mQueueItems)) { + return; + } + mQueueItems = newQueueItems; + updateActiveItem(/* listIsNew */ true); + } + + private int getActiveItemIndex() { + return mActiveItemIndex != null ? mActiveItemIndex : 0; + } + + private int getQueueSize() { + return (mQueueItems != null) ? mQueueItems.size() : 0; + } + + + /** + * Returns the position of the active item if there is one, otherwise returns + * + * @link UxrPivotFilter#INVALID_POSITION}. + */ + private int getActiveItemPosition() { + if (mActiveItemIndex == null) { + return UxrPivotFilter.INVALID_POSITION; + } + return mUxrPivotFilter.indexToPosition(mActiveItemIndex); + } + + private void invalidateActiveItemPosition() { + int position = getActiveItemPosition(); + if (position != UxrPivotFilterImpl.INVALID_POSITION) { + notifyItemChanged(position); + } + } + + private void scrollToActiveItemPosition() { + int position = getActiveItemPosition(); + if (position != UxrPivotFilterImpl.INVALID_POSITION) { + mQueue.scrollToPosition(position); + } + } + + private void applyFilterToQueue() { + mUxrPivotFilter.recompute(getQueueSize(), getActiveItemIndex()); + notifyDataSetChanged(); + } + + // Updates mActiveItemPos, then scrolls the queue to mActiveItemPos. + // It should be called when the active item (mActiveQueueItemId) changed or + // the queue items (mQueueItems) changed. + void updateActiveItem(boolean listIsNew) { + if (mQueueItems == null || mActiveQueueItemId == null) { + mActiveItemIndex = null; + applyFilterToQueue(); + return; + } + Integer activeItemPos = null; + for (int i = 0; i < mQueueItems.size(); i++) { + if (Objects.equals(mQueueItems.get(i).getQueueId(), mActiveQueueItemId)) { + activeItemPos = i; + break; + } + } + + // Invalidate the previous active item so it gets redrawn as a normal one. + invalidateActiveItemPosition(); + + mActiveItemIndex = activeItemPos; + if (listIsNew) { + applyFilterToQueue(); + } else { + mUxrPivotFilter.updatePivotIndex(getActiveItemIndex()); + } + + scrollToActiveItemPosition(); + invalidateActiveItemPosition(); + } + + void setCurrentTime(String currentTime) { + if (!mCurrentTimeText.equals(currentTime)) { + mCurrentTimeText = currentTime; + invalidateActiveItemPosition(); + } + } + + void setMaxTime(String maxTime) { + if (!mMaxTimeText.equals(maxTime)) { + mMaxTimeText = maxTime; + invalidateActiveItemPosition(); + } + } + + void setTimeVisible(boolean visible) { + if (mTimeVisible != visible) { + mTimeVisible = visible; + invalidateActiveItemPosition(); + } + } + + String getCurrentTime() { + return mCurrentTimeText; + } + + String getMaxTime() { + return mMaxTimeText; + } + + boolean getTimeVisible() { + return mTimeVisible; + } + + @Override + public final int getItemViewType(int position) { + if (mUxrPivotFilter.positionToIndex(position) == UxrPivotFilterImpl.INVALID_INDEX) { + return CLAMPED_MESSAGE_VIEW_TYPE; + } else { + return QUEUE_ITEM_VIEW_TYPE; + } + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + if (viewType == CLAMPED_MESSAGE_VIEW_TYPE) { + return ScrollingLimitedViewHolder.create(parent); + } + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + return new QueueViewHolder(inflater.inflate(R.layout.queue_list_item, parent, false)); + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder vh, int position) { + if (vh instanceof QueueViewHolder) { + int index = mUxrPivotFilter.positionToIndex(position); + if (index != UxrPivotFilterImpl.INVALID_INDEX) { + int size = mQueueItems.size(); + if (0 <= index && index < size) { + QueueViewHolder holder = (QueueViewHolder) vh; + holder.bind(mQueueItems.get(index)); + } else { + Log.e(TAG, "onBindViewHolder pos: " + position + " gave index: " + + index + " out of bounds size: " + size + " " + + mUxrPivotFilter.toString()); + } + } else { + Log.e(TAG, "onBindViewHolder invalid position " + position + " " + + mUxrPivotFilter.toString()); + } + } else if (vh instanceof ScrollingLimitedViewHolder) { + ScrollingLimitedViewHolder holder = (ScrollingLimitedViewHolder) vh; + holder.bind(mScrollingLimitedMessageResId); + } else { + throw new IllegalArgumentException("unknown holder class " + vh.getClass()); + } + } + + @Override + public void onViewAttachedToWindow(@NonNull RecyclerView.ViewHolder vh) { + super.onViewAttachedToWindow(vh); + if (vh instanceof QueueViewHolder) { + QueueViewHolder holder = (QueueViewHolder) vh; + holder.onViewAttachedToWindow(); + } + } + + @Override + public void onViewDetachedFromWindow(@NonNull RecyclerView.ViewHolder vh) { + super.onViewDetachedFromWindow(vh); + if (vh instanceof QueueViewHolder) { + QueueViewHolder holder = (QueueViewHolder) vh; + holder.onViewDetachedFromWindow(); + } + } + + @Override + public int getItemCount() { + return mUxrPivotFilter.getFilteredCount(); + } + + @Override + public long getItemId(int position) { + int index = mUxrPivotFilter.positionToIndex(position); + if (index != UxrPivotFilterImpl.INVALID_INDEX) { + return mQueueItems.get(position).getQueueId(); + } else { + return RecyclerView.NO_ID; + } + } + } + + private static class QueueTopItemDecoration extends RecyclerView.ItemDecoration { + int mHeight; + int mDecorationPosition; + + QueueTopItemDecoration(int height, int decorationPosition) { + mHeight = height; + mDecorationPosition = decorationPosition; + } + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, + RecyclerView.State state) { + super.getItemOffsets(outRect, view, parent, state); + if (parent.getChildAdapterPosition(view) == mDecorationPosition) { + outRect.top = mHeight; + } + } + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, final ViewGroup container, + Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_playback_queue, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + mPlaybackViewModel = PlaybackViewModel.get(getActivity().getApplication(), + MEDIA_SOURCE_MODE_PLAYBACK); + + Resources res = getResources(); + mQueue = view.findViewById(R.id.queue_list); + + mShowTimeForActiveQueueItem = res.getBoolean( + R.bool.show_time_for_now_playing_queue_list_item); + mShowIconForActiveQueueItem = res.getBoolean( + R.bool.show_icon_for_now_playing_queue_list_item); + mShowThumbnailForQueueItem = getContext().getResources().getBoolean( + R.bool.show_thumbnail_for_queue_list_item); + mShowSubtitleForQueueItem = getContext().getResources().getBoolean( + R.bool.show_subtitle_for_queue_list_item); + + mPlaybackViewModel.getPlaybackController().observe(getViewLifecycleOwner(), + controller -> mController = controller); + initQueue(); + + mUxrContentLimiter = new LifeCycleObserverUxrContentLimiter( + new UxrContentLimiterImpl(getContext(), R.xml.uxr_config)); + mUxrContentLimiter.setAdapter(mQueueAdapter); + getLifecycle().addObserver(mUxrContentLimiter); + } + + @Override + public void onAttach(Context context) { + super.onAttach(context); + } + + @Override + public void onDetach() { + super.onDetach(); + } + + + public void setCallback(PlaybackQueueCallback callback) { + mPlaybackQueueCallback = callback; + } + + private void initQueue() { + + int decorationHeight = getResources().getDimensionPixelSize( + R.dimen.playback_queue_list_padding_top); + // TODO (b/206038962): addItemDecoration is not supported anymore. Find another way to + // support this. + // Put the decoration above the first item. + int decorationPosition = 0; + mQueue.addItemDecoration(new QueueTopItemDecoration(decorationHeight, decorationPosition)); + + mQueue.setVerticalFadingEdgeEnabled( + getResources().getBoolean(R.bool.queue_fading_edge_length_enabled)); + mQueueAdapter = new QueueItemsAdapter(); + + mPlaybackViewModel.getPlaybackStateWrapper().observe(getViewLifecycleOwner(), + state -> { + Long itemId = (state != null) ? state.getActiveQueueItemId() : null; + if (!Objects.equals(mActiveQueueItemId, itemId)) { + mActiveQueueItemId = itemId; + mQueueAdapter.updateActiveItem(/* listIsNew */ false); + } + }); + mQueue.setAdapter(mQueueAdapter); + + // Disable item changed animation. + mItemAnimator = new DefaultItemAnimator(); + mItemAnimator.setSupportsChangeAnimations(false); + mQueue.setItemAnimator(mItemAnimator); + mPlaybackViewModel.getQueue().observe(this, this::setQueue); + + mPlaybackViewModel.getProgress().observe( + getViewLifecycleOwner(), + playbackProgress -> { + mQueueAdapter.setCurrentTime(playbackProgress.getCurrentTimeText().toString()); + mQueueAdapter.setMaxTime(playbackProgress.getMaxTimeText().toString()); + mQueueAdapter.setTimeVisible(playbackProgress.hasTime()); + }); + } + + void setQueue(List<MediaItemMetadata> queueItems) { + mQueueAdapter.setItems(queueItems); + } + + private void onQueueItemClicked(MediaItemMetadata item) { + if (mController != null) { + mController.skipToQueueItem(item.getQueueId()); + } + if (mPlaybackQueueCallback != null) { + mPlaybackQueueCallback.onQueueItemClicked(item); + } + } +} |