summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-03-29 20:48:15 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-03-29 20:48:15 +0000
commit4a3ce762afe56522365a88b1445dd61fa37d42f7 (patch)
tree7a384d09fa10861f37a81072e0bc8649479eb219
parent5354d747cc0f6e7ec38dfbe53cb874668ac76e87 (diff)
parent0630c2d6ffb02017172e6cae725834801c6dd690 (diff)
downloadMedia-4a3ce762afe56522365a88b1445dd61fa37d42f7.tar.gz
Snap for 9847313 from 0630c2d6ffb02017172e6cae725834801c6dd690 to car-apps-release
Change-Id: Ib09c7faaf04f9c1e8cc782807cab998aec21a833
-rw-r--r--res/drawable/ic_queue_button.xml1
-rw-r--r--res/layout-port/metadata_normal.xml25
-rw-r--r--res/layout/metadata_normal.xml28
-rw-r--r--res/layout/time_progress_text.xml2
-rw-r--r--res/values/id.xml1
-rw-r--r--res/values/overlayable.xml8
-rw-r--r--res/values/styles.xml3
-rw-r--r--res/xml/menuitems_playback.xml2
-rw-r--r--src/com/android/car/media/BrowseStack.java264
-rw-r--r--src/com/android/car/media/BrowseViewController.java50
-rw-r--r--src/com/android/car/media/MediaActivity.java55
-rw-r--r--src/com/android/car/media/MediaActivityController.java458
-rw-r--r--src/com/android/car/media/PlaybackFragment.java17
-rw-r--r--src/com/android/car/media/browse/BrowseMiniMediaItemView.java4
-rw-r--r--tests/unittests/src/com/android/car/media/BrowseStackTests.java168
-rw-r--r--tests/unittests/src/com/android/car/media/browse/BrowseTestUtils.java17
-rwxr-xr-xtools/generate-overlayable.sh15
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