diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-03-29 20:48:15 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-03-29 20:48:15 +0000 |
commit | 4a3ce762afe56522365a88b1445dd61fa37d42f7 (patch) | |
tree | 7a384d09fa10861f37a81072e0bc8649479eb219 | |
parent | 5354d747cc0f6e7ec38dfbe53cb874668ac76e87 (diff) | |
parent | 0630c2d6ffb02017172e6cae725834801c6dd690 (diff) | |
download | Media-4a3ce762afe56522365a88b1445dd61fa37d42f7.tar.gz |
Snap for 9847313 from 0630c2d6ffb02017172e6cae725834801c6dd690 to car-apps-release
Change-Id: Ib09c7faaf04f9c1e8cc782807cab998aec21a833
-rw-r--r-- | res/drawable/ic_queue_button.xml | 1 | ||||
-rw-r--r-- | res/layout-port/metadata_normal.xml | 25 | ||||
-rw-r--r-- | res/layout/metadata_normal.xml | 28 | ||||
-rw-r--r-- | res/layout/time_progress_text.xml | 2 | ||||
-rw-r--r-- | res/values/id.xml | 1 | ||||
-rw-r--r-- | res/values/overlayable.xml | 8 | ||||
-rw-r--r-- | res/values/styles.xml | 3 | ||||
-rw-r--r-- | res/xml/menuitems_playback.xml | 2 | ||||
-rw-r--r-- | src/com/android/car/media/BrowseStack.java | 264 | ||||
-rw-r--r-- | src/com/android/car/media/BrowseViewController.java | 50 | ||||
-rw-r--r-- | src/com/android/car/media/MediaActivity.java | 55 | ||||
-rw-r--r-- | src/com/android/car/media/MediaActivityController.java | 458 | ||||
-rw-r--r-- | src/com/android/car/media/PlaybackFragment.java | 17 | ||||
-rw-r--r-- | src/com/android/car/media/browse/BrowseMiniMediaItemView.java | 4 | ||||
-rw-r--r-- | tests/unittests/src/com/android/car/media/BrowseStackTests.java | 168 | ||||
-rw-r--r-- | tests/unittests/src/com/android/car/media/browse/BrowseTestUtils.java | 17 | ||||
-rwxr-xr-x | tools/generate-overlayable.sh | 15 |
17 files changed, 771 insertions, 347 deletions
diff --git a/res/drawable/ic_queue_button.xml b/res/drawable/ic_queue_button.xml index 389c266..5681ab9 100644 --- a/res/drawable/ic_queue_button.xml +++ b/res/drawable/ic_queue_button.xml @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. --> +<!-- Deprecated resource. --> <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <item android:drawable="@drawable/selectable_button_background" android:height="@dimen/queue_button_background_size" diff --git a/res/layout-port/metadata_normal.xml b/res/layout-port/metadata_normal.xml index 65ee652..e2aecbb 100644 --- a/res/layout-port/metadata_normal.xml +++ b/res/layout-port/metadata_normal.xml @@ -48,31 +48,35 @@ app:layout_constraintBottom_toTopOf="@+id/album_title" app:layout_constraintVertical_chainStyle="packed"/> - <TextView + <com.android.car.apps.common.TappableTextView android:id="@+id/artist" - android:layout_width="0dp" + android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" android:textAlignment="center" android:layout_marginTop="@dimen/metadata_title_subtitle_margin" - style="@style/MetadataPlaybackSubtitleStyle" + app:tappableTextStyle="@style/MetadataPlaybackSubtitleClickableStyle" + app:normalTextStyle="@style/MetadataPlaybackSubtitleStyle" + app:hideViewMode="gone" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/title" app:layout_constraintBottom_toTopOf="@+id/album_title"/> - <TextView + <com.android.car.apps.common.TappableTextView android:id="@+id/album_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="@dimen/metadata_subtitles_margin" - style="@style/MetadataPlaybackSubtitleStyle" + app:tappableTextStyle="@style/MetadataPlaybackSubtitleClickableStyle" + app:normalTextStyle="@style/MetadataPlaybackSubtitleStyle" + app:hideViewMode="gone" app:layout_goneMarginTop="@dimen/metadata_title_subtitle_margin" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintEnd_toStartOf="@+id/progress_text_container" + app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/artist" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@id/progress_text_container" app:layout_constraintHorizontal_chainStyle="packed" app:layout_constrainedWidth="true"/> @@ -80,10 +84,11 @@ android:id="@+id/progress_text_container" android:layout_width="wrap_content" android:layout_height="wrap_content" - app:layout_constraintStart_toEndOf="@+id/album_title" + android:layout_marginTop="@dimen/metadata_subtitles_margin" + app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="@+id/album_title" - app:layout_constraintBottom_toBottomOf="@+id/album_title" + app:layout_constraintTop_toBottomOf="@id/album_title" + app:layout_constraintBottom_toBottomOf="parent" layout="@layout/time_progress_text"/> </androidx.constraintlayout.widget.ConstraintLayout> diff --git a/res/layout/metadata_normal.xml b/res/layout/metadata_normal.xml index f5df6ed..bc6dfc9 100644 --- a/res/layout/metadata_normal.xml +++ b/res/layout/metadata_normal.xml @@ -44,40 +44,40 @@ app:layout_constraintBottom_toTopOf="@+id/artist" app:layout_constraintVertical_chainStyle="packed"/> - <TextView + <com.android.car.apps.common.TappableTextView android:id="@+id/artist" - android:layout_width="0dp" + android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="@dimen/metadata_title_subtitle_margin" - style="@style/MetadataPlaybackSubtitleStyle" + app:tappableTextStyle="@style/MetadataPlaybackSubtitleClickableStyle" + app:normalTextStyle="@style/MetadataPlaybackSubtitleStyle" + app:hideViewMode="gone" app:layout_constraintStart_toStartOf="@id/title" - app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/title" app:layout_constraintBottom_toTopOf="@+id/album_title"/> - <TextView + <com.android.car.apps.common.TappableTextView android:id="@+id/album_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="@dimen/metadata_subtitles_margin" - style="@style/MetadataPlaybackSubtitleStyle" + app:tappableTextStyle="@style/MetadataPlaybackSubtitleClickableStyle" + app:normalTextStyle="@style/MetadataPlaybackSubtitleStyle" + app:hideViewMode="gone" app:layout_goneMarginTop="@dimen/metadata_title_subtitle_margin" app:layout_constraintStart_toStartOf="@id/title" - app:layout_constraintEnd_toStartOf="@+id/progress_text_container" app:layout_constraintTop_toBottomOf="@id/artist" - app:layout_constraintBottom_toBottomOf="@id/album_art" - app:layout_constraintHorizontal_chainStyle="packed" - app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintBottom_toTopOf="@id/progress_text_container" app:layout_constrainedWidth="true"/> <include android:id="@+id/progress_text_container" android:layout_width="wrap_content" android:layout_height="wrap_content" - app:layout_constraintStart_toEndOf="@+id/album_title" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="@+id/album_title" - app:layout_constraintBottom_toBottomOf="@+id/album_title" + android:layout_marginTop="@dimen/metadata_subtitles_margin" + app:layout_constraintStart_toStartOf="@id/title" + app:layout_constraintTop_toBottomOf="@+id/album_title" + app:layout_constraintBottom_toBottomOf="@id/album_art" layout="@layout/time_progress_text"/> </androidx.constraintlayout.widget.ConstraintLayout> diff --git a/res/layout/time_progress_text.xml b/res/layout/time_progress_text.xml index a359994..053e262 100644 --- a/res/layout/time_progress_text.xml +++ b/res/layout/time_progress_text.xml @@ -20,6 +20,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content"> + <!-- For overlays where the time needs to be separated from other content, revive this view. <TextView android:id="@+id/outer_separator" android:layout_width="wrap_content" @@ -27,6 +28,7 @@ android:text="@string/dash_separator" style="@style/MetadataPlaybackSubtitleStyle" /> + --> <TextView android:id="@+id/current_time" diff --git a/res/values/id.xml b/res/values/id.xml index c9d5250..341ec12 100644 --- a/res/values/id.xml +++ b/res/values/id.xml @@ -2,4 +2,5 @@ <resources> <item type="id" name="imageDownloadTask"/> <item type="id" name="playback_seek_bar_container"/> + <item type="id" name="outer_separator"/> </resources> diff --git a/res/values/overlayable.xml b/res/values/overlayable.xml index 211e368..a950a0d 100644 --- a/res/values/overlayable.xml +++ b/res/values/overlayable.xml @@ -105,6 +105,11 @@ REGENERATE USING packages/apps/Car/tests/tools/rro/generate-overlayable.py <item type="id" name="item_container"/> <item type="id" name="max_time"/> <item type="id" name="media_activity_root"/> + <item type="id" name="menu_item_equalizer"/> + <item type="id" name="menu_item_search"/> + <item type="id" name="menu_item_selector"/> + <item type="id" name="menu_item_selector_with_source_logo"/> + <item type="id" name="menu_item_setting"/> <item type="id" name="metadata_container"/> <item type="id" name="minimized_playback_controls"/> <item type="id" name="now_playing_icon"/> @@ -116,13 +121,13 @@ REGENERATE USING packages/apps/Car/tests/tools/rro/generate-overlayable.py <item type="id" name="playback_seek_bar_container"/> <item type="id" name="progress_text_container"/> <item type="id" name="queue_container"/> + <item type="id" name="queue_fragment_container"/> <item type="id" name="queue_list"/> <item type="id" name="queue_list_bottom_constraint"/> <item type="id" name="queue_list_item_subtitle"/> <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"/> @@ -196,6 +201,7 @@ REGENERATE USING packages/apps/Car/tests/tools/rro/generate-overlayable.py <item type="style" name="MediaIconContainerStyle"/> <item type="style" name="MediaTintableIconContainerStyle"/> <item type="style" name="MetadataContainerStyle"/> + <item type="style" name="MetadataPlaybackSubtitleClickableStyle"/> <item type="style" name="MetadataPlaybackSubtitleStyle"/> <item type="style" name="MetadataPlaybackTitleStyle"/> <item type="style" name="QueueListItemSubtitleStyle"/> diff --git a/res/values/styles.xml b/res/values/styles.xml index 9d4682a..ab45e81 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -34,6 +34,9 @@ <item name="android:includeFontPadding">false</item> </style> + <style name="MetadataPlaybackSubtitleClickableStyle" parent="MetadataPlaybackSubtitleStyle"> + </style> + <style name="MetadataContainerStyle"> <item name="android:layout_width">match_parent</item> <item name="android:layout_height">wrap_content</item> diff --git a/res/xml/menuitems_playback.xml b/res/xml/menuitems_playback.xml index d7284b5..1a4305e 100644 --- a/res/xml/menuitems_playback.xml +++ b/res/xml/menuitems_playback.xml @@ -18,7 +18,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> <MenuItem app:id="@+id/menu_item_queue" - app:carUiIcon="@drawable/ic_queue_button" + app:carUiIcon="@drawable/ic_tracklist" app:activatable="true"/> <MenuItem app:id="@+id/menu_item_selector_with_source_logo" diff --git a/src/com/android/car/media/BrowseStack.java b/src/com/android/car/media/BrowseStack.java new file mode 100644 index 0000000..b235b6f --- /dev/null +++ b/src/com/android/car/media/BrowseStack.java @@ -0,0 +1,264 @@ +/* + * Copyright 2023 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 android.support.v4.media.MediaBrowserCompat; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.android.car.media.common.MediaItemMetadata; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Stack; +import java.util.function.Predicate; + +/** + * The Browse stack maintains a history of the various media items the user has been exploring, so + * that some navigation steps can easily be undone. + * + * A new entry is pushed onto the stack when the user selects one of the visible items. + * + * Each entry in the stack is decorated with a {@link BrowseEntryType}. The types are grouped into + * three categories that identify the origin of the media items: + * - TREE: the {@link MediaBrowserCompat}'s root. + * - SEARCH: a search query + * - LINK: a linked item + * Note that media apps decide whether searched and linked items are also part of the browse tree. + * + * The fist element of the stack must be a {@link BrowseEntryType#TREE_ROOT} and be the only one of + * that type. If the stack contains a {@link BrowseEntryType#TREE_TAB}, it must be the second + * element (call {@link #insertRootTab} to add it). + */ +public class BrowseStack { + + private static final String TAG = "BrowseStack"; + + public enum BrowseEntryType { + /** The {@link MediaBrowserCompat}'s root. */ + TREE_ROOT, + /** The currently selected tab (one of the children of the root). */ + TREE_TAB, + /** A descendant of the currently selected tab. */ + TREE_BROWSE, + /** The list of search results. */ + SEARCH_RESULTS, + /** An item descending from one of the search results. */ + SEARCH_BROWSE, + /** An item that was linked to, regardless of the modality (NPV or Browse action). */ + LINK, + /** An item descending from a linked entry. */ + LINK_BROWSE; + + BrowseEntryType getNextEntryBrowseType() { + switch (this) { + case TREE_ROOT: + Log.e(TAG, "getNextEntryBrowseType should not be called on the root"); + // fallthrough + case TREE_TAB: + case TREE_BROWSE: + return TREE_BROWSE; + case SEARCH_RESULTS: + case SEARCH_BROWSE: + return SEARCH_BROWSE; + case LINK: + case LINK_BROWSE: + return LINK_BROWSE; + default: + Log.e(TAG, "getNextEntryBrowseType doesn't know about: " + this); + return TREE_BROWSE; + } + } + } + + /** + * Records the key elements of a stack entry: + * - {@link BrowseEntryType} its type + * - {@link MediaItemMetadata} + * + null for {@link BrowseEntryType#TREE_ROOT} | @link BrowseEntryType#SEARCH_RESULTS}. + * + otherwise non null, the item displayed at this level of the stack + * - {@link BrowseViewController} the controller in charge of this level. Note that + * controllers must be destroyed and recreated on UI configuration changes. + */ + static class BrowseEntry { + final BrowseEntryType mType; + @Nullable final MediaItemMetadata mItem; + private @Nullable BrowseViewController mController; + + private BrowseEntry(@NonNull BrowseEntryType type, @Nullable MediaItemMetadata item, + @NonNull BrowseViewController controller) { + mType = type; + mItem = item; + mController = controller; + } + + @Nullable BrowseViewController getController() { + return mController; + } + + void destroyController() { + if (mController != null) { + mController.destroy(); + mController = null; + } + } + + void setRecreatedController(@NonNull BrowseViewController controller) { + mController = controller; + } + } + + private final Stack<BrowseEntry> mEntries = new Stack<>(); + + BrowseStack() { + } + + /** Returns the number of entries in the stack. */ + int size() { + return mEntries.size(); + } + + void pushRoot(@NonNull BrowseViewController controller) { + if (mEntries.isEmpty()) { + mEntries.push(new BrowseEntry(BrowseEntryType.TREE_ROOT, null, controller)); + } else { + Log.e(TAG, "Ignoring pushRoot on a non empty stack."); + } + } + + void pushSearchResults(@NonNull BrowseViewController controller) { + mEntries.push(new BrowseEntry(BrowseEntryType.SEARCH_RESULTS, null, controller)); + } + + void pushEntry(@NonNull BrowseEntryType type, @NonNull MediaItemMetadata item, + @NonNull BrowseViewController controller) { + mEntries.push(new BrowseEntry(type, item, controller)); + } + + /** Inserts a tab at the start of the stack. */ + void insertRootTab(@NonNull MediaItemMetadata item, @NonNull BrowseViewController ctrl) { + if (mEntries.isEmpty() || (BrowseEntryType.TREE_ROOT != mEntries.get(0).mType)) { + Log.e(TAG, "insertRootTab must be called AFTER adding a root."); + } else { + mEntries.insertElementAt(new BrowseEntry(BrowseEntryType.TREE_TAB, item, ctrl), 1); + } + } + + @Nullable + BrowseEntry peek() { + return mEntries.isEmpty() ? null : mEntries.peek(); + } + + BrowseEntry pop() { + return mEntries.pop(); + } + + /** Returns the current controller being displayed. */ + @Nullable + BrowseViewController getCurrentController() { + return mEntries.isEmpty() ? null : mEntries.peek().mController; + } + + /** Returns the {@link BrowseEntryType} of the entry at the top of the stack. */ + @Nullable + BrowseEntryType getCurrentEntryType() { + return mEntries.isEmpty() ? null : mEntries.peek().mType; + } + + /** Returns the current item being displayed. */ + @Nullable + MediaItemMetadata getCurrentMediaItem() { + return mEntries.isEmpty() ? null : mEntries.peek().mItem; + } + + boolean isShowingSearchResults() { + return (getCurrentEntryType() == BrowseEntryType.SEARCH_RESULTS); + } + + List<BrowseEntry> getEntries() { + return Collections.unmodifiableList(mEntries); + } + + List<BrowseEntry> removeAllEntriesExceptRoot() { + List<BrowseEntry> result = new ArrayList<>(mEntries.size()); + List<BrowseEntry> sublist = mEntries.subList(1, mEntries.size()); + result.addAll(sublist); + sublist.clear(); + return result; + } + + List<BrowseEntry> removeTreeEntriesExceptRoot() { + return removeEntries(entry -> (entry.mType == BrowseEntryType.TREE_TAB + || entry.mType == BrowseEntryType.TREE_BROWSE)); + } + + List<BrowseEntry> removeSearchEntries() { + return removeEntries(entry -> (entry.mType == BrowseEntryType.SEARCH_RESULTS + || entry.mType == BrowseEntryType.SEARCH_BROWSE)); + } + + List<BrowseEntry> removeObsoleteEntries(@NonNull BrowseViewController controller, + @NonNull Collection<MediaItemMetadata> removedChildren) { + List<BrowseEntry> result = new ArrayList<>(); + int ctrlIndex = -1; + BrowseEntryType typeToRemove = null; + for (int index = 0; index < mEntries.size(); index++) { + if (controller == mEntries.get(index).mController) { + ctrlIndex = index; + typeToRemove = mEntries.get(index).mType.getNextEntryBrowseType(); + break; + } + } + + if (ctrlIndex < 0 || ctrlIndex >= mEntries.size() - 1) { + // Controller not found or it has no child in the stack. + return result; + } + + final int firstChild = ctrlIndex + 1; + if (removedChildren.contains(mEntries.get(firstChild).mItem)) { + int endRange = ctrlIndex + 2; + while (endRange < mEntries.size() && mEntries.get(endRange).mType == typeToRemove) { + endRange++; + } + List<BrowseEntry> sublist = mEntries.subList(firstChild, endRange); + result.addAll(sublist); + sublist.clear(); + } + return result; + } + + private List<BrowseEntry> removeEntries(@NonNull Predicate<BrowseEntry> shouldRemove) { + List<BrowseEntry> result = new ArrayList<>(mEntries.size()); + Stack<BrowseEntry> kept = new Stack<>(); + for (BrowseEntry entry: mEntries) { + if (shouldRemove.test(entry)) { + result.add(entry); + } else { + kept.push(entry); + } + } + mEntries.clear(); + mEntries.addAll(kept); + return result; + } +} diff --git a/src/com/android/car/media/BrowseViewController.java b/src/com/android/car/media/BrowseViewController.java index 43f97c0..175064d 100644 --- a/src/com/android/car/media/BrowseViewController.java +++ b/src/com/android/car/media/BrowseViewController.java @@ -45,7 +45,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.util.Pair; import androidx.fragment.app.FragmentActivity; -import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProviders; import androidx.recyclerview.widget.RecyclerView; @@ -136,7 +135,7 @@ public class BrowseViewController { @Override protected void onBrowsableItemClicked(@NonNull MediaItemMetadata item) { - mCallbacks.onBrowsableItemClicked(item); + mCallbacks.goToMediaItem(item); } @Override @@ -209,8 +208,8 @@ public class BrowseViewController { */ void onPlayableItemClicked(@NonNull MediaItemMetadata item); - /** Invoked when the user clicks on a browsable item. */ - void onBrowsableItemClicked(@NonNull MediaItemMetadata item); + /** Displays the given item. It may not be a child of the current node. */ + void goToMediaItem(@NonNull MediaItemMetadata item); /** Invoked when user clicks on the mini playback bar in an empty browse * @@ -249,24 +248,19 @@ public class BrowseViewController { Callbacks callbacks, ViewGroup container, @NonNull MediaItemMetadata parentItem, - MediaItemsLiveData mediaItems, MediaItemsRepository mediaRepo, - MutableLiveData<Map<String, CustomBrowseAction>> globalBrowseActions, int rootBrowsableHint, int rootPlayableHint) { - return new BrowseViewController(callbacks, container, parentItem, mediaItems, - rootBrowsableHint, rootPlayableHint, mediaRepo, globalBrowseActions, true); + return new BrowseViewController(callbacks, container, parentItem, + mediaRepo.getMediaChildren(parentItem.getId()), rootBrowsableHint, rootPlayableHint, + mediaRepo, true); } /** Creates a controller to display the top results of a search query (in a list). */ static BrowseViewController newSearchResultsController( - Callbacks callbacks, - ViewGroup container, - MediaItemsLiveData mediaItems, - MediaItemsRepository mediaRepo, - MutableLiveData<Map<String, CustomBrowseAction>> globalBrowseActions) { - return new BrowseViewController( - callbacks, container, null, mediaItems, 0, 0, mediaRepo, globalBrowseActions, true); + Callbacks callbacks, ViewGroup container, MediaItemsRepository mediaRepo) { + return new BrowseViewController(callbacks, container, null, mediaRepo.getSearchMediaItems(), + 0, 0, mediaRepo, true); } /** @@ -277,11 +271,9 @@ public class BrowseViewController { static BrowseViewController newRootController( Callbacks callbacks, ViewGroup container, - MediaItemsLiveData mediaItems, - MediaItemsRepository mediaRepo, - MutableLiveData<Map<String, CustomBrowseAction>> globalBrowseActions) { - return new BrowseViewController(callbacks, container, null, mediaItems, 0, 0, mediaRepo, - globalBrowseActions, false); + MediaItemsRepository mediaRepo) { + return new BrowseViewController(callbacks, container, null, mediaRepo.getRootMediaItems(), + 0, 0, mediaRepo, false); } /** @@ -337,7 +329,6 @@ public class BrowseViewController { int rootBrowsableHint, int rootPlayableHint, MediaItemsRepository mediaRepo, - MutableLiveData<Map<String, CustomBrowseAction>> globalBrowseActions, boolean displayMediaItems) { mCallbacks = callbacks; mParentItem = parentItem; @@ -412,7 +403,7 @@ public class BrowseViewController { mUxrContentLimiter.setAdapter(mLimitedBrowseAdapter); activity.getLifecycle().addObserver(mUxrContentLimiter); - globalBrowseActions.observe(activity, actions -> { + mMediaRepo.getCustomBrowseActions().observe(activity, actions -> { mGlobalActions = actions; browseAdapter.setGlobalCustomActions(actions); configureCustomActionsHeader(actions, maxActions); @@ -424,6 +415,15 @@ public class BrowseViewController { mMediaItems.observe(activity, mItemsObserver); } + /** + * Returns whether the children of the parentItem given to this controller have been loaded and + * the given item is one of them. + */ + boolean hasChild(MediaItemMetadata item) { + List<MediaItemMetadata> children = FutureData.getData(mMediaItems.getValue()); + return (children != null) && (children.contains(item)); + } + private void initCustomActionsHeader(MediaItemMetadata parentItem, int maxActions) { if (parentItem == null || maxActions <= 0) { return; @@ -526,7 +526,7 @@ public class BrowseViewController { new BrowseActionCallback() { @Override public void onItemLoaded(MediaItem item) { - mCallbacks.onBrowsableItemClicked(new MediaItemMetadata(item)); + mCallbacks.goToMediaItem(new MediaItemMetadata(item)); } }); } @@ -591,10 +591,6 @@ public class BrowseViewController { } } - public MediaItemMetadata getParentItem() { - return mParentItem; - } - /** Shares the browse adapter with the given view... #local-hack. */ public void shareBrowseAdapterWith(RecyclerView view) { view.setAdapter(mLimitedBrowseAdapter); diff --git a/src/com/android/car/media/MediaActivity.java b/src/com/android/car/media/MediaActivity.java index d5c962a..eef8888 100644 --- a/src/com/android/car/media/MediaActivity.java +++ b/src/com/android/car/media/MediaActivity.java @@ -75,7 +75,6 @@ import com.android.car.ui.utils.CarUxRestrictionsUtil; import java.util.HashMap; import java.util.Map; import java.util.Objects; -import java.util.Stack; /** * This activity controls the UI of media. It also updates the connection status for the media app @@ -114,9 +113,6 @@ public class MediaActivity extends FragmentActivity implements MediaActivityCont private float mCloseVectorY; private float mCloseVectorNorm; - private final PlaybackFragment.PlaybackFragmentListener mPlaybackFragmentListener = - () -> changeMode(Mode.BROWSING); - private MediaTrampolineHelper mMediaTrampoline; /** @@ -133,7 +129,6 @@ public class MediaActivity extends FragmentActivity implements MediaActivityCont FATAL_ERROR } - @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -160,10 +155,7 @@ public class MediaActivity extends FragmentActivity implements MediaActivityCont localViewModel.getBrowsedMediaSource().observe(this, this::onMediaSourceChanged); mMediaTrampoline = new MediaTrampolineHelper(this); - mPlaybackFragment = new PlaybackFragment(); - mPlaybackFragment.setListener(mPlaybackFragmentListener); - Size maxArtSize = MediaAppConfig.getMediaItemsBitmapMaxSize(this); mMiniPlaybackControls = findViewById(R.id.minimized_playback_controls); @@ -197,6 +189,7 @@ public class MediaActivity extends FragmentActivity implements MediaActivityCont mMediaActivityController = new MediaActivityController(this, getMediaItemsRepository(), mCarPackageManager, mBrowseContainer); + mPlaybackFragment.setListener(mMediaActivityController.getPlaybackFragmentListener()); mPlaybackContainer.setOnTouchListener(new ClosePlaybackDetector(this)); } @@ -268,7 +261,7 @@ public class MediaActivity extends FragmentActivity implements MediaActivityCont boolean isFatalError = false; if (!TextUtils.isEmpty(displayedMessage)) { // If browse content -> not fatal - if (mMediaActivityController.browseTreeHasChildren()) { + if (mMediaActivityController.browseTreeHasChildrenList()) { showToastOrDialog(displayedMessage, intent, label, mediaSource); } else { boolean isDistractionOptimized = @@ -453,7 +446,8 @@ public class MediaActivity extends FragmentActivity implements MediaActivityCont } } - private void changeMode(Mode mode) { + @Override + public void changeMode(Mode mode) { if (mMode == mode) { if (Log.isLoggable(TAG, Log.INFO)) { Log.i(TAG, "Mode " + mMode + " change is ignored"); @@ -463,6 +457,11 @@ public class MediaActivity extends FragmentActivity implements MediaActivityCont changeModeInternal(mode, true); } + @Override + public void changeSource(MediaSource source) { + mMediaTrampoline.setLaunchedMediaSource(source.getBrowseServiceComponentName()); + } + private void changeModeInternal(Mode mode, boolean hideViewAnimated) { if (Log.isLoggable(TAG, Log.INFO)) { Log.i(TAG, "Changing mode from: " + mMode + " to: " + mode); @@ -616,12 +615,7 @@ public class MediaActivity extends FragmentActivity implements MediaActivityCont static class MediaServiceState { Mode mMode = Mode.BROWSING; - Stack<MediaItemMetadata> mBrowseStack = new Stack<>(); - Stack<MediaItemMetadata> mSearchStack = new Stack<>(); - /** True when the search bar has been opened or when the search results are browsed. */ - boolean mSearching; - /** True iif the list of search results is being shown (implies mIsSearching). */ - boolean mShowingSearchResults; + BrowseStack mBrowseStack = new BrowseStack(); String mSearchQuery; boolean mQueueVisible = false; boolean mHasPlayableItem = false; @@ -681,12 +675,6 @@ public class MediaActivity extends FragmentActivity implements MediaActivityCont return getSavedState().mMode; } - @Nullable - MediaItemMetadata getSelectedTab() { - Stack<MediaItemMetadata> stack = getSavedState().mBrowseStack; - return (stack != null && !stack.empty()) ? stack.firstElement() : null; - } - void setQueueVisible(boolean visible) { getSavedState().mQueueVisible = visible; } @@ -712,35 +700,14 @@ public class MediaActivity extends FragmentActivity implements MediaActivityCont return mBrowsedMediaSource; } - @NonNull Stack<MediaItemMetadata> getBrowseStack() { + @NonNull BrowseStack getBrowseStack() { return getSavedState().mBrowseStack; } - @NonNull Stack<MediaItemMetadata> getSearchStack() { - return getSavedState().mSearchStack; - } - - /** Returns whether search mode is on (showing search results or browsing them). */ - boolean isSearching() { - return getSavedState().mSearching; - } - - boolean isShowingSearchResults() { - return getSavedState().mShowingSearchResults; - } - String getSearchQuery() { return getSavedState().mSearchQuery; } - void setSearching(boolean isSearching) { - getSavedState().mSearching = isSearching; - } - - void setShowingSearchResults(boolean isShowing) { - getSavedState().mShowingSearchResults = isShowing; - } - void setSearchQuery(String searchQuery) { getSavedState().mSearchQuery = searchQuery; } diff --git a/src/com/android/car/media/MediaActivityController.java b/src/com/android/car/media/MediaActivityController.java index 63b512a..6875e4e 100644 --- a/src/com/android/car/media/MediaActivityController.java +++ b/src/com/android/car/media/MediaActivityController.java @@ -32,20 +32,19 @@ import android.view.inputmethod.InputMethodManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.FragmentActivity; -import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProviders; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import com.android.car.apps.common.util.FutureData; -import com.android.car.apps.common.util.ViewUtils; import com.android.car.apps.common.util.ViewUtils.ViewAnimEndListener; -import com.android.car.media.common.CustomBrowseAction; +import com.android.car.media.BrowseStack.BrowseEntryType; +import com.android.car.media.MediaActivity.Mode; +import com.android.car.media.PlaybackFragment.PlaybackFragmentListener; import com.android.car.media.common.MediaItemMetadata; import com.android.car.media.common.browse.MediaBrowserViewModelImpl; import com.android.car.media.common.browse.MediaItemsRepository; -import com.android.car.media.common.browse.MediaItemsRepository.MediaItemsLiveData; import com.android.car.media.common.source.MediaBrowserConnector.BrowsingState; import com.android.car.media.common.source.MediaSource; import com.android.car.media.widgets.AppBarController; @@ -62,7 +61,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Stack; /** * Controls the views of the {@link MediaActivity}. @@ -79,30 +77,21 @@ public class MediaActivityController extends ViewControllerBase { private Insets mCarUiInsets; private boolean mPlaybackControlsVisible; - private final Map<MediaItemMetadata, BrowseViewController> mBrowseViewControllersByNode = - new HashMap<>(); + // Entries whose controller should be destroyed once their view is hidden. + private final Map<View, BrowseStack.BrowseEntry> mEntriesToDestroy = new HashMap<>(); - // Controllers that should be destroyed once their view is hidden. - private final Map<View, BrowseViewController> mBrowseViewControllersToDestroy = new HashMap<>(); - - private final BrowseViewController mRootLoadingController; - private final BrowseViewController mSearchResultsController; + private final RecyclerView mToolbarSearchResultsView; /** * Stores the reference to {@link MediaActivity.ViewModel#getBrowseStack}. * Updated in {@link #onMediaSourceChanged}. */ - private Stack<MediaItemMetadata> mBrowseStack; - /** - * Stores the reference to {@link MediaActivity.ViewModel#getSearchStack}. - * Updated in {@link #onMediaSourceChanged}. - */ - private Stack<MediaItemMetadata> mSearchStack; + private BrowseStack mBrowseStack; private final MediaActivity.ViewModel mViewModel; private int mRootBrowsableHint; private int mRootPlayableHint; - private boolean mBrowseTreeHasChildren; + private boolean mBrowseTreeHasChildrenList; private boolean mAcceptTabSelection = true; /** @@ -115,6 +104,8 @@ public class MediaActivityController extends ViewControllerBase { private final Observer<BrowsingState> mMediaBrowsingObserver = this::onMediaBrowsingStateChanged; + private final PlaybackFragment.PlaybackFragmentListener mPlaybackFragmentListener; + /** * Callbacks (implemented by the hosting Activity) */ @@ -134,6 +125,16 @@ public class MediaActivityController extends ViewControllerBase { /** Returns the activity. */ FragmentActivity getActivity(); + + /** Activates the given mode. */ + void changeMode(Mode mode); + + /** Switches to the given source. */ + void changeSource(MediaSource source); + } + + PlaybackFragmentListener getPlaybackFragmentListener() { + return mPlaybackFragmentListener; } /** @@ -141,56 +142,53 @@ public class MediaActivityController extends ViewControllerBase { */ private boolean navigateBack() { boolean result = false; - if (!isAtTopStack()) { - hideAndDestroyControllerForItem(getStack().pop()); + if (isStacked()) { + hideAndDestroyStackEntry(mBrowseStack.pop()); // Show the parent (if any) showCurrentNode(true); - if (isAtTopStack() && mViewModel.isSearching()) { - showSearchResults(true); - } - updateAppBar(); result = true; } return result; } - private void reopenSearch() { - clearStack(mSearchStack); - showSearchResults(true); - updateAppBar(); - } - private FragmentActivity getActivity() { return mCallbacks.getActivity(); } - /** Returns the browse or search stack. */ - private Stack<MediaItemMetadata> getStack() { - return mViewModel.isSearching() ? mSearchStack : mBrowseStack; + /** Returns true when at least one entry can be removed from the stack. */ + private boolean isStacked() { + List<BrowseStack.BrowseEntry> entries = mBrowseStack.getEntries(); + if (entries.size() <= 1) { + // The root can't be removed from the stack. + return false; + } else if (entries.get(1).mType == BrowseEntryType.TREE_TAB) { + // Tabs can't be removed from the stack + return entries.size() > 2; + } + return true; } - /** - * @return whether the user is at the top of the browsing stack. - */ - private boolean isAtTopStack() { - if (mViewModel.isSearching()) { - return mSearchStack.isEmpty(); - } else { - // The mBrowseStack stack includes the tab... - return mBrowseStack.size() <= 1; + @Nullable + private MediaItemMetadata getSelectedTab() { + List<BrowseStack.BrowseEntry> entries = mBrowseStack.getEntries(); + if (2 <= entries.size() && entries.get(1).mType == BrowseEntryType.TREE_TAB) { + return entries.get(1).mItem; } + return null; } + /** Destroys controllers but should NOT change the stack for the source. */ private void clearMediaSource() { - showSearchMode(false); - for (BrowseViewController controller : mBrowseViewControllersByNode.values()) { - controller.destroy(); + for (BrowseStack.BrowseEntry entry : mBrowseStack.getEntries()) { + entry.destroyController(); + } + mBrowseTreeHasChildrenList = false; + if (mToolbarSearchResultsView != null) { + mToolbarSearchResultsView.setAdapter(null); } - mBrowseViewControllersByNode.clear(); - mBrowseTreeHasChildren = false; } private void updateSearchQuery(@Nullable String query) { @@ -204,9 +202,6 @@ public class MediaActivityController extends ViewControllerBase { void onMediaSourceChanged(@Nullable MediaSource mediaSource) { super.onMediaSourceChanged(mediaSource); - updateTabs((mediaSource != null) ? null : new ArrayList<>()); - - mSearchStack = mViewModel.getSearchStack(); mBrowseStack = mViewModel.getBrowseStack(); updateAppBar(); @@ -227,6 +222,12 @@ public class MediaActivityController extends ViewControllerBase { boolean canSearch = MediaBrowserViewModelImpl.getSupportsSearch(browser); mAppBarController.setSearchSupported(canSearch); + + if (mBrowseStack.size() <= 0) { + mBrowseStack.pushRoot(BrowseViewController.newRootController( + mBrowseCallbacks, mBrowseArea, mMediaItemsRepository)); + } + showCurrentNode(true); break; case DISCONNECTING: @@ -249,55 +250,28 @@ public class MediaActivityController extends ViewControllerBase { mCallbacks = callbacks; mMediaItemsRepository = mediaItemsRepo; mViewModel = ViewModelProviders.of(activity).get(MediaActivity.ViewModel.class); - mSearchStack = mViewModel.getSearchStack(); mBrowseStack = mViewModel.getBrowseStack(); mBrowseArea = mContent.requireViewById(R.id.browse_content_area); mFpv = activity.requireViewById(R.id.fpv); - MediaItemsLiveData rootMediaItems = mediaItemsRepo.getRootMediaItems(); - MutableLiveData<Map<String, CustomBrowseAction>> globalBrowseActions = - mediaItemsRepo.getCustomBrowseActions(); - mRootLoadingController = - BrowseViewController.newRootController( - mBrowseCallbacks, - mBrowseArea, - rootMediaItems, - mediaItemsRepo, - globalBrowseActions); - mRootLoadingController.getContent().setAlpha(1f); - - mSearchResultsController = - BrowseViewController.newSearchResultsController( - mBrowseCallbacks, - mBrowseArea, - mMediaItemsRepository.getSearchMediaItems(), - mediaItemsRepo, - globalBrowseActions); - - boolean showingSearch = mViewModel.isShowingSearchResults(); - ViewUtils.setVisible(mSearchResultsController.getContent(), showingSearch); - if (showingSearch) { - mSearchResultsController.getContent().setAlpha(1f); - } - mAppBarController.setListener(mAppBarListener); mAppBarController.setSearchQuery(mViewModel.getSearchQuery()); if (mAppBarController.getSearchCapabilities().canShowSearchResultsView()) { - // TODO(b/180441965) eliminate the need to create a different view and use - // mSearchResultsController.getContent() instead. - RecyclerView toolbarSearchResultsView = new RecyclerView(activity); - mSearchResultsController.shareBrowseAdapterWith(toolbarSearchResultsView); + // TODO(b/180441965) eliminate the need to create a different view + mToolbarSearchResultsView = new RecyclerView(activity); ViewGroup.LayoutParams params = new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); - toolbarSearchResultsView.setLayoutParams(params); - toolbarSearchResultsView.setLayoutManager(new LinearLayoutManager(activity)); - toolbarSearchResultsView.setBackground( + mToolbarSearchResultsView.setLayoutParams(params); + mToolbarSearchResultsView.setLayoutManager(new LinearLayoutManager(activity)); + mToolbarSearchResultsView.setBackground( activity.getDrawable(R.drawable.car_ui_ime_wide_screen_background)); mAppBarController.setSearchConfig(SearchConfig.builder() - .setSearchResultsView(toolbarSearchResultsView) + .setSearchResultsView(mToolbarSearchResultsView) .build()); + } else { + mToolbarSearchResultsView = null; } updateAppBar(); @@ -309,20 +283,64 @@ public class MediaActivityController extends ViewControllerBase { onMediaSourceChanged(future.isLoading() ? null : future.getData()); }); - rootMediaItems.observe(activity, this::onRootMediaItemsUpdate); + mMediaItemsRepository.getRootMediaItems().observe(activity, this::onRootMediaItemsUpdate); mViewModel.getMiniControlsVisible().observe(activity, this::onPlaybackControlsChanged); + + mPlaybackFragmentListener = (source, mediaItem) -> { + if (source == null || mediaItem == null) { + Log.e(TAG, "goToMediaItem error S: " + source + " MI: " + mediaItem); + return; + } else if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "goToMediaItem S: " + source + " MI: " + mediaItem); + } + + mCallbacks.changeMode(Mode.BROWSING); + + if (!Objects.equals(source, mViewModel.getMediaSourceValue())) { + mCallbacks.changeSource(source); + } + + navigateTo(mediaItem); + }; + } + + private BrowseViewController recreateController(BrowseStack.BrowseEntry entry) { + switch (entry.mType) { + case TREE_ROOT: + return BrowseViewController.newRootController( + mBrowseCallbacks, mBrowseArea, mMediaItemsRepository); + case SEARCH_RESULTS: + updateSearchQuery(mViewModel.getSearchQuery()); + return BrowseViewController.newSearchResultsController( + mBrowseCallbacks, mBrowseArea, mMediaItemsRepository); + default: + if (entry.mItem == null) { + Log.e(TAG, "Can't recreate controller for a null item!"); + return null; + } + return BrowseViewController.newBrowseController( + mBrowseCallbacks, mBrowseArea, entry.mItem, + mMediaItemsRepository, mRootBrowsableHint, mRootPlayableHint); + } } void onDestroy() { mMediaItemsRepository.getBrowsingState().removeObserver(mMediaBrowsingObserver); + for (BrowseStack.BrowseEntry entry : mBrowseStack.getEntries()) { + entry.destroyController(); + } + if (mToolbarSearchResultsView != null) { + mToolbarSearchResultsView.setAdapter(null); + } } - private AppBarController.AppBarListener mAppBarListener = new BasicAppBarListener() { + private final AppBarController.AppBarListener mAppBarListener = new BasicAppBarListener() { @Override public void onTabSelected(MediaItemMetadata item) { - if (mAcceptTabSelection && (item != null) && (item != mViewModel.getSelectedTab())) { - clearStack(mBrowseStack); - mBrowseStack.push(item); + if (mAcceptTabSelection && (item != null) && (item != getSelectedTab())) { + // Clear the entire stack, including search and links. + hideAndDestroyStackEntries(mBrowseStack.removeAllEntriesExceptRoot()); + mBrowseStack.insertRootTab(item, createControllerForItem(item)); showCurrentNode(true); updateAppBar(); } @@ -330,13 +348,7 @@ public class MediaActivityController extends ViewControllerBase { @Override public void onSearchSelection() { - if (mViewModel.isSearching()) { - reopenSearch(); - } else { - showSearchMode(true); - updateAppBar(); - mAppBarController.setSearchQuery(mViewModel.getSearchQuery()); - } + showSearchResults(); } @Override @@ -358,15 +370,14 @@ public class MediaActivityController extends ViewControllerBase { } @Override - public void onBrowsableItemClicked(@NonNull MediaItemMetadata item) { + public void goToMediaItem(@NonNull MediaItemMetadata item) { hideKeyboard(); - navigateInto(item); + navigateTo(item); } @Override public void onBrowseEmptyListPlayItemClicked() { mCallbacks.onBrowseEmptyListPlayItemClicked(); - } @Override @@ -378,18 +389,12 @@ public class MediaActivityController extends ViewControllerBase { @Override public void onChildrenNodesRemoved(@NonNull BrowseViewController controller, @NonNull Collection<MediaItemMetadata> removedNodes) { - if (mBrowseStack.contains(controller.getParentItem())) { - for (MediaItemMetadata node : removedNodes) { - int indexOfNode = mBrowseStack.indexOf(node); - if (indexOfNode >= 0) { - clearStack(mBrowseStack.subList(indexOfNode, mBrowseStack.size())); - if (!mViewModel.isShowingSearchResults()) { - showCurrentNode(true); - updateAppBar(); - } - break; // The stack contains at most one of the removed nodes. - } - } + List<BrowseStack.BrowseEntry> entries = + mBrowseStack.removeObsoleteEntries(controller, removedNodes); + if (!entries.isEmpty()) { + hideAndDestroyStackEntries(entries); + showCurrentNode(true); + updateAppBar(); } } @@ -405,19 +410,14 @@ public class MediaActivityController extends ViewControllerBase { }; private final ViewAnimEndListener mViewAnimEndListener = view -> { - BrowseViewController toDestroy = mBrowseViewControllersToDestroy.remove(view); + BrowseStack.BrowseEntry toDestroy = mEntriesToDestroy.remove(view); if (toDestroy != null) { - toDestroy.destroy(); + toDestroy.destroyController(); } }; boolean onBackPressed() { boolean success = navigateBack(); - if (!success && mViewModel.isSearching()) { - showSearchMode(false); - updateAppBar(); - success = true; - } if (success) { // When the back button is pressed, if a CarUiRecyclerView shows up and it's in rotary // mode, restore focus in the CarUiRecyclerView. @@ -426,18 +426,41 @@ public class MediaActivityController extends ViewControllerBase { return success; } - boolean browseTreeHasChildren() { - return mBrowseTreeHasChildren; + boolean browseTreeHasChildrenList() { + return mBrowseTreeHasChildrenList; + } + + private BrowseEntryType getNextEntryType(@NonNull MediaItemMetadata item) { + BrowseViewController topController = mBrowseStack.getCurrentController(); + if (topController == null) { + Log.e(TAG, "topController should not be null in getNextEntryType!!"); + return BrowseEntryType.LINK; + } + + if (topController.hasChild(item)) { + // If the item is a child of the top controller, treat this as a regular browse action + BrowseEntryType currentType = mBrowseStack.getCurrentEntryType(); + if (currentType == null) { + Log.e(TAG, "mBrowseStack.getCurrentEntryType returned null !?!"); + return BrowseEntryType.LINK; + } + return currentType.getNextEntryBrowseType(); + } + + return BrowseEntryType.LINK; } - private void navigateInto(@NonNull MediaItemMetadata item) { - showSearchResults(false); + private void navigateTo(@NonNull MediaItemMetadata item) { + if (Objects.equals(item, mBrowseStack.getCurrentMediaItem())) { + Log.i(TAG, "navigateInto item already shown"); + return; + } - // Hide the current node (parent) + // Hide the current node (eg: parent) showCurrentNode(false); // Make item the current node - getStack().push(item); + mBrowseStack.pushEntry(getNextEntryType(item), item, createControllerForItem(item)); // Show the current node (item) showCurrentNode(true); @@ -446,40 +469,37 @@ public class MediaActivityController extends ViewControllerBase { } @NonNull - private BrowseViewController getControllerForItem(@NonNull MediaItemMetadata item) { - BrowseViewController controller = mBrowseViewControllersByNode.get(item); - MutableLiveData<Map<String, CustomBrowseAction>> globalBrowseActions = - mMediaItemsRepository.getCustomBrowseActions(); - if (controller == null) { - controller = - BrowseViewController.newBrowseController( - mBrowseCallbacks, - mBrowseArea, - item, - mMediaItemsRepository.getMediaChildren(item.getId()), - mMediaItemsRepository, - globalBrowseActions, - mRootBrowsableHint, - mRootPlayableHint); - - if (mCarUiInsets != null) { - controller.onCarUiInsetsChanged(mCarUiInsets); - } - controller.onPlaybackControlsChanged(mPlaybackControlsVisible); + private BrowseViewController createControllerForItem(@NonNull MediaItemMetadata item) { + BrowseViewController controller = + BrowseViewController.newBrowseController(mBrowseCallbacks, mBrowseArea, + item, mMediaItemsRepository, mRootBrowsableHint, mRootPlayableHint); + adjustBoundaries(controller); + return controller; + } - mBrowseViewControllersByNode.put(item, controller); + private void adjustBoundaries(@NonNull BrowseViewController controller) { + if (mCarUiInsets != null) { + controller.onCarUiInsetsChanged(mCarUiInsets); } - return controller; + controller.onPlaybackControlsChanged(mPlaybackControlsVisible); } private void showCurrentNode(boolean show) { - MediaItemMetadata currentNode = getCurrentMediaItem(); - if (currentNode == null) { + BrowseStack.BrowseEntry entry = mBrowseStack.peek(); + if (entry == null) { + Log.e(TAG, "Can't show a null entry!"); return; } - // Only create a controller to show it. - BrowseViewController controller = show ? getControllerForItem(currentNode) : - mBrowseViewControllersByNode.get(currentNode); + + BrowseViewController controller = entry.getController(); + if (controller == null && show) { + // Controller was previously destroyed by a media source or UI config change, recreate. + controller = recreateController(entry); + if (controller != null) { + adjustBoundaries(controller); + entry.setRecreatedController(controller); + } + } if (controller != null) { showHideContentAnimated(show, controller.getContent(), mViewAnimEndListener); @@ -491,14 +511,13 @@ public class MediaActivityController extends ViewControllerBase { // as the controller isn't ready to show the browse data of the new media source (it hasn't // connected to it (b/217159531). private void restoreFocusInCurrentNode() { - MediaItemMetadata currentNode = getCurrentMediaItem(); - if (currentNode == null) { + BrowseViewController controller = mBrowseStack.getCurrentController(); + if (controller == null) { return; } - BrowseViewController controller = getControllerForItem(currentNode); CarUiRecyclerView carUiRecyclerView = controller.getContent().findViewById(R.id.browse_list); - if (carUiRecyclerView != null && carUiRecyclerView instanceof LazyLayoutView + if (carUiRecyclerView instanceof LazyLayoutView && !carUiRecyclerView.getView().hasFocus() && !carUiRecyclerView.getView().isInTouchMode()) { LazyLayoutView lazyLayoutView = (LazyLayoutView) carUiRecyclerView; @@ -509,7 +528,7 @@ public class MediaActivityController extends ViewControllerBase { private void showHideContentAnimated(boolean show, @NonNull View content, @Nullable ViewAnimEndListener listener) { CarUiRecyclerView carUiRecyclerView = content.findViewById(R.id.browse_list); - if (carUiRecyclerView != null && carUiRecyclerView instanceof LazyLayoutView + if (carUiRecyclerView instanceof LazyLayoutView && !carUiRecyclerView.getView().isInTouchMode()) { // If a CarUiRecyclerView is about to hide and it has focus, park the focus on the // FocusParkingView before hiding the CarUiRecyclerView. Otherwise hiding the focused @@ -532,56 +551,46 @@ public class MediaActivityController extends ViewControllerBase { showHideViewAnimated(show, content, mFadeDuration, listener); } + private void showSearchResults() { + // Remove previous search entries from the stack (if any) + hideAndDestroyStackEntries(mBrowseStack.removeSearchEntries()); + // Hide the current node + showCurrentNode(false); - private void showSearchResults(boolean show) { - if (mViewModel.isShowingSearchResults() != show) { - mViewModel.setShowingSearchResults(show); - showHideContentAnimated(show, mSearchResultsController.getContent(), null); + // Push a new search controller + BrowseViewController controller = BrowseViewController.newSearchResultsController( + mBrowseCallbacks, mBrowseArea, mMediaItemsRepository); + adjustBoundaries(controller); + if (mToolbarSearchResultsView != null) { + controller.shareBrowseAdapterWith(mToolbarSearchResultsView); } - } + mBrowseStack.pushSearchResults(controller); - private void showSearchMode(boolean show) { - if (mViewModel.isSearching() != show) { - if (show) { - showCurrentNode(false); - } - - mViewModel.setSearching(show); - showSearchResults(show); - - if (!show) { - showCurrentNode(true); - } - } - } + // Show the search controller + showCurrentNode(true); - /** - * @return the current item being displayed - */ - @Nullable - private MediaItemMetadata getCurrentMediaItem() { - Stack<MediaItemMetadata> stack = getStack(); - return stack.isEmpty() ? null : stack.lastElement(); + updateAppBar(); + mAppBarController.setSearchQuery(mViewModel.getSearchQuery()); } @Override public void onCarUiInsetsChanged(@NonNull Insets insets) { mCarUiInsets = insets; - for (BrowseViewController controller : mBrowseViewControllersByNode.values()) { - controller.onCarUiInsetsChanged(mCarUiInsets); + for (BrowseStack.BrowseEntry entry : mBrowseStack.getEntries()) { + if (entry.getController() != null) { + entry.getController().onCarUiInsetsChanged(mCarUiInsets); + } } - mRootLoadingController.onCarUiInsetsChanged(mCarUiInsets); - mSearchResultsController.onCarUiInsetsChanged(mCarUiInsets); } void onPlaybackControlsChanged(boolean visible) { mPlaybackControlsVisible = visible; - for (BrowseViewController controller : mBrowseViewControllersByNode.values()) { - controller.onPlaybackControlsChanged(mPlaybackControlsVisible); + for (BrowseStack.BrowseEntry entry : mBrowseStack.getEntries()) { + if (entry.getController() != null) { + entry.getController().onPlaybackControlsChanged(mPlaybackControlsVisible); + } } - mRootLoadingController.onPlaybackControlsChanged(mPlaybackControlsVisible); - mSearchResultsController.onPlaybackControlsChanged(mPlaybackControlsVisible); } private void hideKeyboard() { @@ -590,34 +599,32 @@ public class MediaActivityController extends ViewControllerBase { in.hideSoftInputFromWindow(mContent.getWindowToken(), 0); } - private void hideAndDestroyControllerForItem(@Nullable MediaItemMetadata item) { - if (item == null) { - return; - } - BrowseViewController controller = mBrowseViewControllersByNode.get(item); + private void hideAndDestroyStackEntry(@NonNull BrowseStack.BrowseEntry entry) { + BrowseViewController controller = entry.getController(); if (controller == null) { + Log.e(TAG, "Controller already destroyed for: " + entry.mItem); return; } - if (controller.getContent().getVisibility() == View.VISIBLE) { View view = controller.getContent(); - mBrowseViewControllersToDestroy.put(view, controller); + mEntriesToDestroy.put(view, entry); showHideContentAnimated(false, view, mViewAnimEndListener); } else { - controller.destroy(); + entry.destroyController(); + } + + if (mToolbarSearchResultsView != null && entry.mType == BrowseEntryType.SEARCH_RESULTS) { + mToolbarSearchResultsView.setAdapter(null); } - mBrowseViewControllersByNode.remove(item); } /** - * Clears the given stack (or a portion of a stack) and destroys the old controllers (after - * their view is hidden). + * Destroys the given stack entries (after their view is hidden). */ - private void clearStack(List<MediaItemMetadata> stack) { - for (MediaItemMetadata item : stack) { - hideAndDestroyControllerForItem(item); + private void hideAndDestroyStackEntries(List<BrowseStack.BrowseEntry> entries) { + for (BrowseStack.BrowseEntry entry : entries) { + hideAndDestroyStackEntry(entry); } - stack.clear(); } /** @@ -643,13 +650,13 @@ public class MediaActivityController extends ViewControllerBase { mAppBarController.setActiveItem(null); if (items != null) { // Only do this when not loading the tabs or we loose the saved one. - clearStack(mBrowseStack); + hideAndDestroyStackEntries(mBrowseStack.removeTreeEntriesExceptRoot()); } updateAppBar(); return; } - MediaItemMetadata oldTab = mViewModel.getSelectedTab(); + MediaItemMetadata oldTab = getSelectedTab(); MediaItemMetadata newTab = items.contains(oldTab) ? oldTab : items.get(0); try { @@ -659,11 +666,11 @@ public class MediaActivityController extends ViewControllerBase { if (oldTab != newTab) { // Tabs belong to the browse stack. - clearStack(mBrowseStack); - mBrowseStack.push(newTab); + hideAndDestroyStackEntries(mBrowseStack.removeTreeEntriesExceptRoot()); + mBrowseStack.insertRootTab(newTab, createControllerForItem(newTab)); } - if (!mViewModel.isShowingSearchResults()) { + if (mBrowseStack.size() <= 2) { // Needed when coming back to an app after a config change or from another app, // or when the tab actually changes. showCurrentNode(true); @@ -675,12 +682,10 @@ public class MediaActivityController extends ViewControllerBase { } private CharSequence getAppBarTitle() { - boolean isStacked = !isAtTopStack(); - final CharSequence title; - if (isStacked) { + if (isStacked()) { // If not at top level, show the current item as title - MediaItemMetadata item = getCurrentMediaItem(); + MediaItemMetadata item = mBrowseStack.getCurrentMediaItem(); title = item != null ? item.getTitle() : ""; } else if (mTopItems == null) { // If still loading the tabs, force to show an empty bar. @@ -701,17 +706,16 @@ public class MediaActivityController extends ViewControllerBase { * Update elements of the appbar that change depending on where we are in the browse. */ private void updateAppBar() { - boolean isSearching = mViewModel.isSearching(); - boolean isStacked = !isAtTopStack(); + boolean isSearchMode = mBrowseStack.isShowingSearchResults(); + boolean isStacked = isStacked(); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "App bar is in stacked state: " + isStacked); } - mAppBarController.setSearchMode(isSearching ? SearchMode.SEARCH : SearchMode.DISABLED); - mAppBarController.setNavButtonMode(isStacked || isSearching - ? NavButtonMode.BACK : NavButtonMode.DISABLED); - mAppBarController.setTitle(!isSearching ? getAppBarTitle() : null); - mAppBarController.showSearchIfSupported(!isSearching || isStacked); + mAppBarController.setSearchMode(isSearchMode ? SearchMode.SEARCH : SearchMode.DISABLED); + mAppBarController.setNavButtonMode(isStacked ? NavButtonMode.BACK : NavButtonMode.DISABLED); + mAppBarController.setTitle(getAppBarTitle()); + mAppBarController.showSearchIfSupported(!isSearchMode); } private void onRootMediaItemsUpdate(FutureData<List<MediaItemMetadata>> data) { @@ -719,7 +723,7 @@ public class MediaActivityController extends ViewControllerBase { if (Log.isLoggable(TAG, Log.INFO)) { Log.i(TAG, "Loading browse tree..."); } - mBrowseTreeHasChildren = false; + mBrowseTreeHasChildrenList = false; updateTabs(null); return; } @@ -727,12 +731,12 @@ public class MediaActivityController extends ViewControllerBase { List<MediaItemMetadata> items = MediaBrowserViewModelImpl.filterItems(/*forRoot*/ true, data.getData()); - boolean browseTreeHasChildren = items != null && !items.isEmpty(); + boolean browseTreeHasChildrenList = items != null; if (Log.isLoggable(TAG, Log.INFO)) { Log.i(TAG, "Browse tree loaded, status (has children or not) changed: " - + mBrowseTreeHasChildren + " -> " + browseTreeHasChildren); + + mBrowseTreeHasChildrenList + " -> " + browseTreeHasChildrenList); } - mBrowseTreeHasChildren = browseTreeHasChildren; + mBrowseTreeHasChildrenList = browseTreeHasChildrenList; mCallbacks.onRootLoaded(); updateTabs(items != null ? items : new ArrayList<>()); } diff --git a/src/com/android/car/media/PlaybackFragment.java b/src/com/android/car/media/PlaybackFragment.java index 438f739..47d925f 100644 --- a/src/com/android/car/media/PlaybackFragment.java +++ b/src/com/android/car/media/PlaybackFragment.java @@ -48,7 +48,9 @@ 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; +import com.android.car.media.common.browse.MediaItemsRepository; import com.android.car.media.common.playback.PlaybackViewModel; +import com.android.car.media.common.source.MediaSource; import com.android.car.media.common.source.MediaSourceViewModel; import com.android.car.media.widgets.AppBarController; import com.android.car.ui.core.CarUi; @@ -78,6 +80,7 @@ public class PlaybackFragment extends Fragment { private View mControlBarScrim; private PlaybackControlsActionBar mPlaybackControls; private PlaybackViewModel mPlaybackViewModel; + private MediaItemsRepository mMediaItemsRepository; private ViewGroup mSeekBarContainer; private SeekBar mSeekBar; private List<View> mViewsToHideForCustomActions; @@ -118,9 +121,9 @@ public class PlaybackFragment extends Fragment { */ public interface PlaybackFragmentListener { /** - * Invoked when the user clicks on the collapse button + * Invoked when the user clicks on a browse link */ - void onCollapse(); + void goToMediaItem(MediaSource source, MediaItemMetadata mediaItem); } @Override @@ -133,6 +136,8 @@ public class PlaybackFragment extends Fragment { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { mPlaybackViewModel = PlaybackViewModel.get(getActivity().getApplication(), MEDIA_SOURCE_MODE_PLAYBACK); + mMediaItemsRepository = MediaItemsRepository.get(getActivity().getApplication(), + MEDIA_SOURCE_MODE_PLAYBACK); Resources res = getResources(); mAlbumBackground = view.findViewById(R.id.playback_background); @@ -304,8 +309,12 @@ public class PlaybackFragment extends Fragment { Size maxArtSize = MediaAppConfig.getMediaItemsBitmapMaxSize(view.getContext()); mMetadataController = new MetadataController(getViewLifecycleOwner(), mPlaybackViewModel, - title, artist, albumTitle, outerSeparator, curTime, innerSeparator, maxTime, - seekbar, albumArt, null, maxArtSize); + mMediaItemsRepository, title, artist, albumTitle, outerSeparator, curTime, + innerSeparator, maxTime, seekbar, albumArt, null, maxArtSize, + (mediaItem) -> { + MediaSource source = mPlaybackViewModel.getMediaSource().getValue(); + mListener.goToMediaItem(source, mediaItem); + }); } /** diff --git a/src/com/android/car/media/browse/BrowseMiniMediaItemView.java b/src/com/android/car/media/browse/BrowseMiniMediaItemView.java index 4631e15..16ea70f 100644 --- a/src/com/android/car/media/browse/BrowseMiniMediaItemView.java +++ b/src/com/android/car/media/browse/BrowseMiniMediaItemView.java @@ -68,8 +68,8 @@ public class BrowseMiniMediaItemView extends BrowseMiniMediaItemBar { /** Connects the bar to the {@link PlaybackViewModel}. */ public void setModel(@NonNull PlaybackViewModel model, @NonNull LifecycleOwner owner, @NonNull Size maxArtSize) { - mMetadataController = new MetadataController(owner, model, mTitle, mSubtitle, null, null, - null, null, null, null, mContentTile, mAppIcon, maxArtSize); + mMetadataController = new MetadataController(owner, model, null, mTitle, mSubtitle, null, + null, null, null, null, null, mContentTile, mAppIcon, maxArtSize, null); mPlaybackViewModel = model; if (mArtBinder != null) { diff --git a/tests/unittests/src/com/android/car/media/BrowseStackTests.java b/tests/unittests/src/com/android/car/media/BrowseStackTests.java new file mode 100644 index 0000000..f36765d --- /dev/null +++ b/tests/unittests/src/com/android/car/media/BrowseStackTests.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2023 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.support.v4.media.MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE; + +import static com.android.car.media.BrowseStack.BrowseEntryType.LINK; +import static com.android.car.media.BrowseStack.BrowseEntryType.LINK_BROWSE; +import static com.android.car.media.BrowseStack.BrowseEntryType.SEARCH_BROWSE; +import static com.android.car.media.BrowseStack.BrowseEntryType.SEARCH_RESULTS; +import static com.android.car.media.BrowseStack.BrowseEntryType.TREE_BROWSE; +import static com.android.car.media.BrowseStack.BrowseEntryType.TREE_ROOT; +import static com.android.car.media.BrowseStack.BrowseEntryType.TREE_TAB; + +import static junit.framework.Assert.assertEquals; + +import static org.mockito.Mockito.mock; + +import android.support.v4.media.MediaMetadataCompat; + +import androidx.core.util.Preconditions; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.car.media.BrowseStack.BrowseEntryType; +import com.android.car.media.common.MediaItemMetadata; + +import com.google.common.collect.HashBiMap; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; + +@RunWith(AndroidJUnit4.class) +public class BrowseStackTests extends BaseMockitoTest { + + private final HashBiMap<BrowseEntryType, String> mTypeEncoding = HashBiMap.create(); + + @Before + public void setup() { + mTypeEncoding.put(TREE_ROOT, "TR"); + mTypeEncoding.put(TREE_TAB, "TT"); + mTypeEncoding.put(TREE_BROWSE, "TB"); + mTypeEncoding.put(SEARCH_RESULTS, "SR"); + mTypeEncoding.put(SEARCH_BROWSE, "SB"); + mTypeEncoding.put(LINK, "LN"); + mTypeEncoding.put(LINK_BROWSE, "LB"); + } + + @Test + public void removeAllEntriesExceptRoot() { + BrowseStack stack = decodeStack("TR/TT:tab/TB:n1/SR/SB:s1/SB:s2/"); + stack.removeAllEntriesExceptRoot(); + assertEquals("TR/", encodeStack(stack)); + } + + @Test + public void removeTreeEntriesExceptRoot() { + BrowseStack stack = decodeStack("TR/TT:tab/TB:n1/SR/SB:s1/SB:s2/LN:l1/LB:l2/"); + stack.removeTreeEntriesExceptRoot(); + assertEquals("TR/SR/SB:s1/SB:s2/LN:l1/LB:l2/", encodeStack(stack)); + } + + @Test + public void removeTrailingSearchEntries() { + BrowseStack stack = decodeStack("TR/TT:tab/TB:n1/SR/SB:s1/SB:s2/"); + stack.removeSearchEntries(); + assertEquals("TR/TT:tab/TB:n1/", encodeStack(stack)); + } + + @Test + public void removeMiddleSearchEntries() { + BrowseStack stack = decodeStack("TR/SR/SB:s1/SB:s2/LN:l1/LB:l2/"); + stack.removeSearchEntries(); + assertEquals("TR/LN:l1/LB:l2/", encodeStack(stack)); + } + + @Test + public void removeTrailingObsoleteEntries() { + BrowseStack stack = decodeStack("TR/SR/SB:s1/SB:s2/LN:l1/LB:l2/"); + removeObsoleteEntries(stack, 4, 5); + assertEquals("TR/SR/SB:s1/SB:s2/LN:l1/", encodeStack(stack)); + } + + @Test + public void removeObsoleteEntriesIgnoresIrrelevantItem() { + BrowseStack stack = decodeStack("TR/SR/SB:s1/SB:s2/LN:l1/LB:l2/"); + removeObsoleteEntries(stack, 4, 2); + assertEquals("TR/SR/SB:s1/SB:s2/LN:l1/LB:l2/", encodeStack(stack)); + } + + @Test + public void removeMiddleObsoleteEntries() { + BrowseStack stack = decodeStack("TR/TT:tab/TB:n1/TB:n2/TB:n3/SR/SB:s1/SB:s2/"); + removeObsoleteEntries(stack, 1, 2); + assertEquals("TR/TT:tab/SR/SB:s1/SB:s2/", encodeStack(stack)); + } + + /** + * Picks the controller and the item at the given index then calls removeObsoleteEntries. + */ + private void removeObsoleteEntries(BrowseStack stack, int controllerIndex, int itemIndex) { + BrowseViewController ctrl = stack.getEntries().get(controllerIndex).getController(); + ArrayList<MediaItemMetadata> list = new ArrayList<>(1); + list.add(stack.getEntries().get(itemIndex).mItem); + stack.removeObsoleteEntries(Preconditions.checkNotNull(ctrl), list); + } + + private String encodeType(BrowseEntryType type) { + return Preconditions.checkNotNull(mTypeEncoding.get(type)); + } + + private BrowseEntryType decodeType(String code) { + return Preconditions.checkNotNull(mTypeEncoding.inverse().get(code)); + } + + private BrowseStack decodeStack(String encoded) { + BrowseStack result = new BrowseStack(); + String[] entries = encoded.split("/"); + for (String entry : entries) { + String[] codesAndNode = entry.split(":"); + BrowseEntryType type = decodeType(codesAndNode[0]); + switch (type) { + case TREE_ROOT: + result.pushRoot(mock(BrowseViewController.class)); + break; + case SEARCH_RESULTS: + result.pushSearchResults(mock(BrowseViewController.class)); + break; + default: + MediaMetadataCompat.Builder bob = new MediaMetadataCompat.Builder(); + bob.putText(METADATA_KEY_DISPLAY_TITLE, codesAndNode[1]); + MediaItemMetadata item = new MediaItemMetadata(bob.build()); + result.pushEntry(type, item, mock(BrowseViewController.class)); + } + } + return result; + } + + private String encodeStack(BrowseStack stack) { + StringBuilder builder = new StringBuilder(); + for (BrowseStack.BrowseEntry entry : stack.getEntries()) { + builder.append(encodeType(entry.mType)); + if ((entry.mType != TREE_ROOT) && (entry.mType != SEARCH_RESULTS)) { + Preconditions.checkNotNull(entry.mItem); + builder.append(":"); + builder.append(entry.mItem.getTitle()); + } + builder.append("/"); + } + return builder.toString(); + } +} diff --git a/tests/unittests/src/com/android/car/media/browse/BrowseTestUtils.java b/tests/unittests/src/com/android/car/media/browse/BrowseTestUtils.java index faa1488..3a58b07 100644 --- a/tests/unittests/src/com/android/car/media/browse/BrowseTestUtils.java +++ b/tests/unittests/src/com/android/car/media/browse/BrowseTestUtils.java @@ -34,7 +34,7 @@ public class BrowseTestUtils { .setSubtitle("Parent") .setDescription("Parent item desc") .setMediaId("ParentItem"); - return new MediaItemMetadata(builder.build(), 0L, true, false, "Parent", "Parent"); + return new MediaItemMetadata(builder.build(), 0L, true, false, null); } public static List<MediaItemMetadata> generateTestItems() { @@ -44,14 +44,14 @@ public class BrowseTestUtils { .setDescription("Test item 1 desc") .setMediaId("TestItem1"); MediaItemMetadata browsableItem = new MediaItemMetadata(builder.build(), 1L, true, false, - "Item1", "Test1"); + null); builder.setTitle("TestItem2") .setSubtitle("Tester2") .setDescription("Test item 2 desc") .setMediaId("TestItem2"); MediaItemMetadata playableItem = new MediaItemMetadata(builder.build(), 2L, false, true, - "Item2", "Test2"); + null); Bundle extras = new Bundle(); @@ -66,8 +66,7 @@ public class BrowseTestUtils { .setMediaId("TestItem3") .setExtras(extras); MediaItemMetadata downloadedExplicitNewProgress = new MediaItemMetadata(builder.build(), 3L, - false, true, - "Item3", "Test3"); + false, true, null); Bundle newExtras = new Bundle(); newExtras.putLong("android.media.extra.DOWNLOAD_STATUS", 2L); @@ -81,9 +80,7 @@ public class BrowseTestUtils { .setMediaId("TestItem3") .setExtras(newExtras); MediaItemMetadata downloadedExplicitNewProgress2 = new MediaItemMetadata(builder.build(), - 3L, - false, true, - "Item3", "Test3"); + 3L, false, true, null); Bundle finishedPlayExtras = new Bundle(); finishedPlayExtras.putLong("android.media.extra.DOWNLOAD_STATUS", 2L); @@ -97,9 +94,7 @@ public class BrowseTestUtils { .setMediaId("TestItem3") .setExtras(finishedPlayExtras); MediaItemMetadata downloadedExplicitNewProgress3 = new MediaItemMetadata(builder.build(), - 3L, - false, true, - "Item3", "Test3"); + 3L, false, true, null); List<MediaItemMetadata> itemList = new ArrayList<>(); itemList.add(browsableItem); diff --git a/tools/generate-overlayable.sh b/tools/generate-overlayable.sh index b6fe78a..7d51a0c 100755 --- a/tools/generate-overlayable.sh +++ b/tools/generate-overlayable.sh @@ -15,15 +15,18 @@ # Run this script to regenerate the overlayable.xml file. -if [[ -z "$ANDROID_BUILD_TOP" ]]; then - echo 'ANDROID_BUILD_TOP environment variable is empty; did you forget to run `lunch`?' - exit 1 -fi +cd $(dirname $0) +cd .. -PROJECT_TOP=$ANDROID_BUILD_TOP/packages/apps/Car/Media +PROJECT_TOP="$(pwd)" +export PROJECT_TOP +cd ../../../.. + +ANDROID_BUILD_TOP="$(pwd)" +export ANDROID_BUILD_TOP python3 $ANDROID_BUILD_TOP/packages/apps/Car/tests/tools/rro/generate-overlayable.py \ -n CarMediaApp \ -r $PROJECT_TOP/res \ -e $PROJECT_TOP/res/values/overlayable.xml $PROJECT_TOP/res/xml/automotive_app_desc.xml $PROJECT_TOP/res/values/colors.xml $PROJECT_TOP/res/values/dimens.xml $PROJECT_TOP/res/color/progress_bar_thumb_inner_ring_color.xml $PROJECT_TOP/res/color/progress_bar_thumb_outer_ring_color.xml \ - -o $PROJECT_TOP/res/values/overlayable.xml
\ No newline at end of file + -o $PROJECT_TOP/res/values/overlayable.xml |