summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBabak Bostan <babakbo@google.com>2023-01-31 19:26:39 +0000
committerAndroid (Google) Code Review <android-gerrit@google.com>2023-01-31 19:26:39 +0000
commit2e31dedd394e6f02f002148793b491f4d5adcaa3 (patch)
tree8742d6089300a7734e1f98e71bf765e094d3637f
parent524ce85a13b754a755b2d934a461532099b56f68 (diff)
parent85a419a0d115b542581180577b7d9f2ba5adf890 (diff)
downloadMedia-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.xml20
-rw-r--r--res/layout/fragment_playback_queue.xml39
-rw-r--r--res/values/integers.xml2
-rw-r--r--res/values/overlayable.xml2
-rw-r--r--src/com/android/car/media/PlaybackFragment.java450
-rw-r--r--src/com/android/car/media/PlaybackQueueFragment.java531
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);
+ }
+ }
+}