Snap for 7316203 from ae75cc93e61e099e1aebe06e5b8b7f2a075832dc to rvc-platform-release
Change-Id: Id7a24defb92fd36c8cfb6fde8036715482858e00
diff --git a/res/layout/browse_node.xml b/res/layout/browse_node.xml
new file mode 100644
index 0000000..11cdaf5
--- /dev/null
+++ b/res/layout/browse_node.xml
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2020, 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.
+-->
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <!-- Clone the top guideline since we don't need the others here. -->
+ <Space
+ android:id="@+id/ui_content_top_guideline2"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_marginTop="@dimen/car_ui_toolbar_first_row_height"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ />
+
+ <ImageView
+ android:id="@+id/error_icon"
+ android:layout_width="@dimen/missing_permission_icon_size"
+ android:layout_height="@dimen/missing_permission_icon_size"
+ app:layout_constraintTop_toBottomOf="@+id/ui_content_top_guideline2"
+ app:layout_constraintBottom_toTopOf="@+id/error_message"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintVertical_chainStyle="packed"
+ android:src="@drawable/error_illustration"
+ android:visibility="gone"
+ android:tint="@color/icon_tint"/>
+
+ <TextView
+ android:id="@+id/error_message"
+ android:layout_width="wrap_content"
+ style="@style/ErrorTextStyle"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/browse_state_error_margin_top"
+ app:layout_constraintTop_toBottomOf="@+id/error_icon"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ android:maxLines="3"
+ android:text="@string/nothing_to_play"
+ android:visibility="gone"/>
+
+ <com.android.car.ui.FocusArea
+ android:id="@+id/focus_area"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintBottom_toBottomOf="parent">
+ <com.android.car.ui.recyclerview.CarUiRecyclerView
+ android:id="@+id/browse_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:clickable="true"
+ android:clipToPadding="false"
+ android:visibility="gone"
+ app:layoutStyle="grid"
+ app:numOfColumns="@integer/num_browse_columns"/>
+ </com.android.car.ui.FocusArea>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/layout/fragment_browse.xml b/res/layout/fragment_browse.xml
index 688ed26..597c48c 100644
--- a/res/layout/fragment_browse.xml
+++ b/res/layout/fragment_browse.xml
@@ -22,50 +22,14 @@
<include layout="@layout/ui_guides"/>
- <ImageView
- android:id="@+id/error_icon"
- android:layout_width="@dimen/missing_permission_icon_size"
- android:layout_height="@dimen/missing_permission_icon_size"
- app:layout_constraintTop_toBottomOf="@+id/ui_content_top_guideline"
- app:layout_constraintBottom_toTopOf="@+id/error_message"
- app:layout_constraintStart_toStartOf="@+id/ui_content_start_guideline"
- app:layout_constraintEnd_toEndOf="@+id/ui_content_end_guideline"
- app:layout_constraintVertical_chainStyle="packed"
- android:src="@drawable/error_illustration"
- android:visibility="gone"
- android:tint="@color/icon_tint"/>
-
- <TextView
- android:id="@+id/error_message"
- android:layout_width="wrap_content"
- style="@style/ErrorTextStyle"
- android:layout_height="wrap_content"
- android:layout_marginTop="@dimen/browse_state_error_margin_top"
- app:layout_constraintTop_toBottomOf="@+id/error_icon"
- app:layout_constraintBottom_toBottomOf="@+id/ui_content_bottom_guideline"
- app:layout_constraintStart_toStartOf="@+id/ui_content_start_guideline"
- app:layout_constraintEnd_toEndOf="@+id/ui_content_end_guideline"
- android:maxLines="3"
- android:text="@string/nothing_to_play"
- android:visibility="gone"/>
-
- <com.android.car.ui.FocusArea
- android:id="@+id/focus_area"
+ <FrameLayout
+ android:id="@+id/browse_content_area"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintStart_toStartOf="@+id/ui_content_start_guideline"
app:layout_constraintEnd_toEndOf="@+id/ui_content_end_guideline"
app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintBottom_toTopOf="@+id/ui_content_bottom_guideline">
- <com.android.car.ui.recyclerview.CarUiRecyclerView
- android:id="@+id/browse_list"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:clickable="true"
- android:clipToPadding="false"
- android:visibility="gone"
- app:layoutStyle="grid"
- app:numOfColumns="@integer/num_browse_columns"/>
- </com.android.car.ui.FocusArea>
+ app:layout_constraintBottom_toTopOf="@+id/ui_content_bottom_guideline"
+ />
</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/res/layout/media_activity.xml b/res/layout/media_activity.xml
index f5c0d33..07cb83b 100644
--- a/res/layout/media_activity.xml
+++ b/res/layout/media_activity.xml
@@ -33,13 +33,6 @@
/>
<FrameLayout
- android:id="@+id/search_container"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:visibility="gone"
- />
-
- <FrameLayout
android:id="@+id/error_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
diff --git a/src/com/android/car/media/BrowseViewController.java b/src/com/android/car/media/BrowseViewController.java
index 01ddabd..5a4eb3c 100644
--- a/src/com/android/car/media/BrowseViewController.java
+++ b/src/com/android/car/media/BrowseViewController.java
@@ -16,23 +16,21 @@
package com.android.car.media;
-import static com.android.car.arch.common.LiveDataFunctions.ifThenElse;
+import static com.android.car.apps.common.util.ViewUtils.removeFromParent;
-import android.car.content.pm.CarPackageManager;
-import android.content.Context;
+import android.content.res.Resources;
import android.os.Handler;
-import android.text.TextUtils;
import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
import android.view.ViewGroup;
-import android.view.inputmethod.InputMethodManager;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
-import androidx.lifecycle.LiveData;
-import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -43,89 +41,68 @@
import com.android.car.media.browse.LimitedBrowseAdapter;
import com.android.car.media.common.GridSpacingItemDecoration;
import com.android.car.media.common.MediaItemMetadata;
-import com.android.car.media.common.browse.BrowsedMediaItems;
-import com.android.car.media.common.browse.MediaBrowserViewModel;
+import com.android.car.media.common.browse.MediaBrowserViewModelImpl;
+import com.android.car.media.common.browse.MediaItemsRepository.MediaItemsLiveData;
import com.android.car.media.common.source.MediaSource;
-import com.android.car.media.widgets.AppBarController;
import com.android.car.ui.FocusArea;
import com.android.car.ui.baselayout.Insets;
-import com.android.car.ui.toolbar.Toolbar;
import com.android.car.uxr.LifeCycleObserverUxrContentLimiter;
import com.android.car.uxr.UxrContentLimiterImpl;
-import java.util.ArrayList;
+import java.util.Collection;
import java.util.List;
-import java.util.Objects;
-import java.util.Stack;
/**
- * A view controller that implements the content forward browsing experience.
- *
- * This can be used to display either search or browse results at the root level. Deeper levels will
- * be handled the same way between search and browse, using a back stack to return to the root.
+ * A view controller that displays the media item children of a {@link MediaItemMetadata}.
+ * The controller manages a recycler view where the items can be displayed as a list or a grid, as
+ * well as an error icon and a message used to indicate loading and errors.
+ * The content view is initialized with 0 alpha and needs to be animated or set to to full opacity
+ * to become visible.
*/
-public class BrowseViewController extends ViewControllerBase {
+public class BrowseViewController {
private static final String TAG = "BrowseViewController";
- private static final String REGULAR_BROWSER_VIEW_MODEL_KEY
- = "com.android.car.media.regular_browser_view_model";
- private static final String SEARCH_BROWSER_VIEW_MODEL_KEY
- = "com.android.car.media.search_browser_view_model";
-
private final Callbacks mCallbacks;
private final FocusArea mFocusArea;
+ private final MediaItemMetadata mParentItem;
+ private final MediaItemsLiveData mMediaItems;
+ private final boolean mDisplayMediaItems;
private final LifeCycleObserverUxrContentLimiter mUxrContentLimiter;
+ private final View mContent;
private final RecyclerView mBrowseList;
private final ImageView mErrorIcon;
private final TextView mMessage;
private final LimitedBrowseAdapter mLimitedBrowseAdapter;
- private String mSearchQuery;
+
private final int mFadeDuration;
private final int mLoadingIndicatorDelay;
- private final boolean mIsSearchController;
+
private final boolean mSetFocusAreaHighlightBottom;
- private final MutableLiveData<Boolean> mShowSearchResults = new MutableLiveData<>();
+
private final Handler mHandler = new Handler();
- /**
- * Stores the reference to {@link MediaActivity.ViewModel#getSearchStack} or to
- * {@link MediaActivity.ViewModel#getBrowseStack}. Updated in {@link #onMediaSourceChanged}.
- */
- private Stack<MediaItemMetadata> mBrowseStack = new Stack<>();
+
private final MediaActivity.ViewModel mViewModel;
- private final MediaBrowserViewModel mRootMediaBrowserViewModel;
- private final MediaBrowserViewModel.WithMutableBrowseId mMediaBrowserViewModel;
+
private final BrowseAdapter.Observer mBrowseAdapterObserver = new BrowseAdapter.Observer() {
@Override
- protected void onPlayableItemClicked(MediaItemMetadata item) {
- hideKeyboard();
- getParent().onPlayableItemClicked(item);
+ protected void onPlayableItemClicked(@NonNull MediaItemMetadata item) {
+ mCallbacks.onPlayableItemClicked(item);
}
@Override
- protected void onBrowsableItemClicked(MediaItemMetadata item) {
- hideKeyboard();
- navigateInto(item);
+ protected void onBrowsableItemClicked(@NonNull MediaItemMetadata item) {
+ mCallbacks.onBrowsableItemClicked(item);
}
};
- private boolean mBrowseTreeHasChildren;
- private boolean mAcceptTabSelection = true;
-
- /**
- * Media items to display as tabs. If null, it means we haven't finished loading them yet. If
- * empty, it means there are no tabs to show
- */
- @Nullable
- private List<MediaItemMetadata> mTopItems;
-
/**
* The bottom padding of the FocusArea highlight.
*/
private int mFocusAreaHighlightBottomPadding;
/**
- * Callbacks (implemented by the hosting Activity)
+ * Callbacks (implemented by the host)
*/
public interface Callbacks {
/**
@@ -133,136 +110,69 @@
*
* @param item item to be played.
*/
- void onPlayableItemClicked(MediaItemMetadata item);
+ void onPlayableItemClicked(@NonNull MediaItemMetadata item);
- /** Called once the list of the root node's children has been loaded. */
- void onRootLoaded();
+ /** Invoked when the user clicks on a browsable item. */
+ void onBrowsableItemClicked(@NonNull MediaItemMetadata item);
- /** Change to a new UI mode. */
- void changeMode(MediaActivity.Mode mode);
+ /** Invoked when child nodes have been removed from this controller. */
+ void onChildrenNodesRemoved(@NonNull BrowseViewController controller,
+ @NonNull Collection<MediaItemMetadata> removedNodes);
FragmentActivity getActivity();
}
- /**
- * Moves the user one level up in the browse tree. Returns whether that was possible.
- */
- private boolean navigateBack() {
- boolean result = false;
- if (!isAtTopStack()) {
- mBrowseStack.pop();
- mMediaBrowserViewModel.search(mSearchQuery);
- mMediaBrowserViewModel.setCurrentBrowseId(getCurrentMediaItemId());
- updateAppBar();
- result = true;
- }
- if (isAtTopStack()) {
- mShowSearchResults.setValue(mIsSearchController);
- }
- return result;
- }
-
- private void reopenSearch() {
- if (mIsSearchController) {
- mBrowseStack.clear();
- updateAppBar();
- mShowSearchResults.setValue(true);
- } else {
- Log.e(TAG, "reopenSearch called on browse controller");
- }
- }
-
- @NonNull
- private Callbacks getParent() {
- return mCallbacks;
- }
-
private FragmentActivity getActivity() {
return mCallbacks.getActivity();
}
/**
- * @return whether the user is at the top of the browsing stack.
+ * Creates a controller to display the children of the given parent {@link MediaItemMetadata}.
+ * This parent node can have been obtained from the browse tree, or from browsing the search
+ * results.
*/
- private boolean isAtTopStack() {
- if (mIsSearchController) {
- return mBrowseStack.isEmpty();
- } else {
- // The mBrowseStack stack includes the tab...
- return mBrowseStack.size() <= 1;
- }
+ static BrowseViewController newBrowseController(Callbacks callbacks, ViewGroup container,
+ @NonNull MediaItemMetadata parentItem, MediaItemsLiveData mediaItems,
+ int rootBrowsableHint, int rootPlayableHint) {
+ return new BrowseViewController(callbacks, container, parentItem, mediaItems,
+ rootBrowsableHint, rootPlayableHint, 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) {
+ return new BrowseViewController(callbacks, container, null, mediaItems, 0, 0, true);
}
/**
- * Creates a new instance of this controller meant to browse the root node.
- * @return a fully initialized {@link BrowseViewController}
+ * Creates a controller to "display" the children of the root: the children are actually hidden
+ * since they are shown as tabs, and the controller is only used to display loading and error
+ * messages.
*/
- public static BrowseViewController newInstance(Callbacks callbacks,
- CarPackageManager carPackageManager, ViewGroup container) {
- boolean isSearchController = false;
- return new BrowseViewController(callbacks, carPackageManager, container, isSearchController);
+ static BrowseViewController newRootController(Callbacks callbacks, ViewGroup container,
+ MediaItemsLiveData mediaItems) {
+ return new BrowseViewController(callbacks, container, null, mediaItems, 0, 0, false);
}
- /**
- * Creates a new instance of this controller meant to display search results. The root browse
- * screen will be the search results for the provided query.
- *
- * @return a fully initialized {@link BrowseViewController}
- */
- static BrowseViewController newSearchInstance(Callbacks callbacks,
- CarPackageManager carPackageManager, ViewGroup container) {
- boolean isSearchController = true;
- return new BrowseViewController(callbacks, carPackageManager, container, isSearchController);
- }
- private void updateSearchQuery(@Nullable String query) {
- mSearchQuery = query;
- mMediaBrowserViewModel.search(query);
- }
-
- /**
- * Clears search state, removes any UI elements from previous results.
- */
- @Override
- void onMediaSourceChanged(@Nullable MediaSource mediaSource) {
- super.onMediaSourceChanged(mediaSource);
-
- mBrowseTreeHasChildren = false;
-
- if (mIsSearchController) {
- updateSearchQuery(mViewModel.getSearchQuery());
- mAppBarController.setSearchQuery(mSearchQuery);
- mBrowseStack = mViewModel.getSearchStack();
- mShowSearchResults.setValue(isAtTopStack());
- } else {
- mBrowseStack = mViewModel.getBrowseStack();
- mShowSearchResults.setValue(false);
- updateTabs((mediaSource != null) ? null : new ArrayList<>());
- }
-
- mLimitedBrowseAdapter.submitItems(null, null);
- stopLoadingIndicator();
- ViewUtils.hideViewAnimated(mErrorIcon, mFadeDuration);
- ViewUtils.hideViewAnimated(mMessage, mFadeDuration);
-
- mMediaBrowserViewModel.setCurrentBrowseId(getCurrentMediaItemId());
-
- updateAppBar();
- }
-
- private BrowseViewController(Callbacks callbacks, CarPackageManager carPackageManager,
- ViewGroup container, boolean isSearchController) {
- super(callbacks.getActivity(), carPackageManager, container, R.layout.fragment_browse);
-
+ private BrowseViewController(Callbacks callbacks, ViewGroup container,
+ @Nullable MediaItemMetadata parentItem, MediaItemsLiveData mediaItems,
+ int rootBrowsableHint, int rootPlayableHint, boolean displayMediaItems) {
mCallbacks = callbacks;
- mIsSearchController = isSearchController;
+ mParentItem = parentItem;
+ mMediaItems = mediaItems;
+ mDisplayMediaItems = displayMediaItems;
+
+ LayoutInflater inflater = LayoutInflater.from(container.getContext());
+ mContent = inflater.inflate(R.layout.browse_node, container, false);
+ mContent.setAlpha(0f);
+ container.addView(mContent);
mLoadingIndicatorDelay = mContent.getContext().getResources()
.getInteger(R.integer.progress_indicator_delay);
mSetFocusAreaHighlightBottom = mContent.getContext().getResources().getBoolean(
R.bool.set_browse_list_focus_area_highlight_above_minimized_control_bar);
- mAppBarController.setListener(mAppBarListener);
mFocusArea = mContent.findViewById(R.id.focus_area);
mBrowseList = mContent.findViewById(R.id.browse_list);
mErrorIcon = mContent.findViewById(R.id.error_icon);
@@ -272,31 +182,15 @@
FragmentActivity activity = callbacks.getActivity();
-
mViewModel = ViewModelProviders.of(activity).get(MediaActivity.ViewModel.class);
- // Browse logic for the root node
- mRootMediaBrowserViewModel = MediaBrowserViewModel.Factory.getInstanceForBrowseRoot(
- mMediaSourceVM, ViewModelProviders.of(activity));
- mRootMediaBrowserViewModel.getBrowsedMediaItems()
- .observe(activity, futureData -> onItemsUpdate(/* forRoot */ true, futureData));
-
- mRootMediaBrowserViewModel.supportsSearch().observe(activity,
- mAppBarController::setSearchSupported);
-
-
- // Browse logic for current node
- mMediaBrowserViewModel = MediaBrowserViewModel.Factory.getInstanceWithMediaBrowser(
- mIsSearchController ? SEARCH_BROWSER_VIEW_MODEL_KEY : REGULAR_BROWSER_VIEW_MODEL_KEY,
- ViewModelProviders.of(activity),
- mMediaSourceVM.getConnectedMediaBrowser());
-
mBrowseList.addItemDecoration(new GridSpacingItemDecoration(
activity.getResources().getDimensionPixelSize(R.dimen.grid_item_spacing)));
GridLayoutManager manager = (GridLayoutManager) mBrowseList.getLayoutManager();
- mLimitedBrowseAdapter = new LimitedBrowseAdapter(
- new BrowseAdapter(mBrowseList.getContext()), manager, mBrowseAdapterObserver);
+ BrowseAdapter browseAdapter = new BrowseAdapter(mBrowseList.getContext());
+ mLimitedBrowseAdapter = new LimitedBrowseAdapter(browseAdapter, manager,
+ mBrowseAdapterObserver);
mBrowseList.setAdapter(mLimitedBrowseAdapter);
mUxrContentLimiter = new LifeCycleObserverUxrContentLimiter(
@@ -304,46 +198,65 @@
mUxrContentLimiter.setAdapter(mLimitedBrowseAdapter);
activity.getLifecycle().addObserver(mUxrContentLimiter);
- mMediaBrowserViewModel.rootBrowsableHint().observe(activity,
- hint -> mLimitedBrowseAdapter.getBrowseAdapter().setRootBrowsableViewType(hint));
- mMediaBrowserViewModel.rootPlayableHint().observe(activity,
- hint -> mLimitedBrowseAdapter.getBrowseAdapter().setRootPlayableViewType(hint));
- LiveData<FutureData<List<MediaItemMetadata>>> mediaItems = ifThenElse(mShowSearchResults,
- mMediaBrowserViewModel.getSearchedMediaItems(),
- mMediaBrowserViewModel.getBrowsedMediaItems());
+ browseAdapter.setRootBrowsableViewType(rootBrowsableHint);
+ browseAdapter.setRootPlayableViewType(rootPlayableHint);
- mediaItems.observe(activity, futureData -> onItemsUpdate(/* forRoot */ false, futureData));
-
- updateAppBar();
+ mMediaItems.observe(activity, mItemsObserver);
}
- private AppBarController.AppBarListener mAppBarListener = new BasicAppBarListener() {
- @Override
- public void onTabSelected(MediaItemMetadata item) {
- if (mAcceptTabSelection) {
- showTopItem(item);
- }
- }
+ public MediaItemMetadata getParentItem() {
+ return mParentItem;
+ }
- @Override
- public void onSearchSelection() {
- if (mIsSearchController) {
- reopenSearch();
- } else {
- mCallbacks.changeMode(MediaActivity.Mode.SEARCHING);
- }
- }
+ /** Shares the browse adapter with the given view... #local-hack. */
+ public void shareBrowseAdapterWith(RecyclerView view) {
+ view.setAdapter(mLimitedBrowseAdapter);
+ }
- @Override
- public void onSearch(String query) {
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "onSearch: " + query);
- }
- mViewModel.setSearchQuery(query);
- updateSearchQuery(query);
- }
- };
+ private final Observer<FutureData<List<MediaItemMetadata>>> mItemsObserver =
+ this::onItemsUpdate;
+ View getContent() {
+ return mContent;
+ }
+
+ String getDebugInfo() {
+ StringBuilder log = new StringBuilder();
+ log.append("[");
+ log.append((mParentItem != null) ? mParentItem.getTitle() : "Root");
+ log.append("]");
+ FutureData<List<MediaItemMetadata>> children = mMediaItems.getValue();
+ if (children == null) {
+ log.append(" null future data");
+ } else if (children.isLoading()) {
+ log.append(" loading");
+ } else if (children.getData() == null) {
+ log.append(" null list");
+ } else {
+ List<MediaItemMetadata> nodes = children.getData();
+ log.append(" ");
+ log.append(nodes.size());
+ log.append(" {");
+ if (nodes.size() > 0) {
+ log.append(nodes.get(0).getTitle().toString());
+ }
+ if (nodes.size() > 1) {
+ log.append(", ");
+ log.append(nodes.get(1).getTitle().toString());
+ }
+ if (nodes.size() > 2) {
+ log.append(", ...");
+ }
+ log.append(" }");
+ }
+ return log.toString();
+ }
+
+ void destroy() {
+ mCallbacks.getActivity().getLifecycle().removeObserver(mUxrContentLimiter);
+ mMediaItems.removeObserver(mItemsObserver);
+ removeFromParent(mContent);
+ }
private Runnable mLoadingIndicatorRunnable = new Runnable() {
@Override
@@ -353,19 +266,6 @@
}
};
- boolean onBackPressed() {
- boolean success = navigateBack();
- if (!success && (mIsSearchController)) {
- mCallbacks.changeMode(MediaActivity.Mode.BROWSING);
- return true;
- }
- return success;
- }
-
- boolean browseTreeHasChildren() {
- return mBrowseTreeHasChildren;
- }
-
private void startLoadingIndicator() {
// Display the indicator after a certain time, to avoid flashing the indicator constantly,
// even when performance is acceptable.
@@ -377,33 +277,6 @@
ViewUtils.hideViewAnimated(mMessage, mFadeDuration);
}
- private void navigateInto(@Nullable MediaItemMetadata item) {
- if (item != null) {
- mBrowseStack.push(item);
- mMediaBrowserViewModel.setCurrentBrowseId(item.getId());
- } else {
- mMediaBrowserViewModel.setCurrentBrowseId(null);
- }
-
- mShowSearchResults.setValue(false);
- updateAppBar();
- }
-
- /**
- * @return the current item being displayed
- */
- @Nullable
- private MediaItemMetadata getCurrentMediaItem() {
- return mBrowseStack.isEmpty() ? null : mBrowseStack.lastElement();
- }
-
- @Nullable
- private String getCurrentMediaItemId() {
- MediaItemMetadata currentItem = getCurrentMediaItem();
- return currentItem != null ? currentItem.getId() : null;
- }
-
- @Override
public void onCarUiInsetsChanged(@NonNull Insets insets) {
int leftPadding = mBrowseList.getPaddingLeft();
int rightPadding = mBrowseList.getPaddingRight();
@@ -421,10 +294,9 @@
int leftPadding = mBrowseList.getPaddingLeft();
int topPadding = mBrowseList.getPaddingTop();
int rightPadding = mBrowseList.getPaddingRight();
+ Resources res = getActivity().getResources();
int bottomPadding = visible
- ? getActivity().getResources().getDimensionPixelOffset(
- R.dimen.browse_fragment_bottom_padding)
- : 0;
+ ? res.getDimensionPixelOffset(R.dimen.browse_fragment_bottom_padding) : 0;
mBrowseList.setPadding(leftPadding, topPadding, rightPadding, bottomPadding);
int highlightBottomPadding = mSetFocusAreaHighlightBottom ? bottomPadding : 0;
if (highlightBottomPadding > mFocusAreaHighlightBottomPadding) {
@@ -442,102 +314,9 @@
mMessage.setLayoutParams(messageLayout);
}
- private void hideKeyboard() {
- InputMethodManager in =
- (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
- in.hideSoftInputFromWindow(mContent.getWindowToken(), 0);
- }
-
- private void showTopItem(@Nullable MediaItemMetadata item) {
- mViewModel.getBrowseStack().clear();
- navigateInto(item);
- }
-
- /**
- * Updates the tabs displayed on the app bar, based on the top level items on the browse tree.
- * If there is at least one browsable item, we show the browse content of that node. If there
- * are only playable items, then we show those items. If there are not items at all, we show the
- * empty message. If we receive null, we show the error message.
- *
- * @param items top level items, null if the items are still being loaded, or empty list if
- * items couldn't be loaded.
- */
- private void updateTabs(@Nullable List<MediaItemMetadata> items) {
- if (Objects.equals(mTopItems, items)) {
- // When coming back to the app, the live data sends an update even if the list hasn't
- // changed. Updating the tabs then recreates the browse view, which produces jank
- // (b/131830876), and also resets the navigation to the top of the first tab...
- return;
- }
- mTopItems = items;
-
- if (mTopItems == null || mTopItems.isEmpty()) {
- mAppBarController.setItems(null);
- mAppBarController.setActiveItem(null);
- if (items != null) {
- // Only do this when not loading the tabs or we loose the saved one.
- showTopItem(null);
- }
- updateAppBar();
- return;
- }
-
- MediaItemMetadata oldTab = mViewModel.getSelectedTab();
- try {
- mAcceptTabSelection = false;
- mAppBarController.setItems(mTopItems.size() == 1 ? null : mTopItems);
- updateAppBar();
-
- if (items.contains(oldTab)) {
- mAppBarController.setActiveItem(oldTab);
- } else {
- showTopItem(items.get(0));
- }
- } finally {
- mAcceptTabSelection = true;
- }
- }
-
- private void updateAppBarTitle() {
- boolean isStacked = !isAtTopStack();
-
- final CharSequence title;
- if (isStacked) {
- // If not at top level, show the current item as title
- title = getCurrentMediaItem().getTitle();
- } else if (mTopItems == null) {
- // If still loading the tabs, force to show an empty bar.
- title = "";
- } else if (mTopItems.size() == 1) {
- // If we finished loading tabs and there is only one, use that as title.
- title = mTopItems.get(0).getTitle();
- } else {
- // Otherwise (no tabs or more than 1 tabs), show the current media source title.
- MediaSource mediaSource = mMediaSourceVM.getPrimaryMediaSource().getValue();
- title = getAppBarDefaultTitle(mediaSource);
- }
-
- mAppBarController.setTitle(title);
- }
-
- /**
- * Update elements of the appbar that change depending on where we are in the browse.
- */
- private void updateAppBar() {
- boolean isStacked = !isAtTopStack();
- if (Log.isLoggable(TAG, Log.DEBUG)) {
- Log.d(TAG, "App bar is in stacked state: " + isStacked);
- }
- Toolbar.State unstackedState =
- mIsSearchController ? Toolbar.State.SEARCH : Toolbar.State.HOME;
- updateAppBarTitle();
- mAppBarController.setState(isStacked ? Toolbar.State.SUBPAGE : unstackedState);
- mAppBarController.showSearchIfSupported(!mIsSearchController || isStacked);
- }
-
- private String getErrorMessage(boolean forRoot) {
- if (forRoot) {
- MediaSource mediaSource = mMediaSourceVM.getPrimaryMediaSource().getValue();
+ private String getErrorMessage() {
+ if (/*root*/ !mDisplayMediaItems) {
+ MediaSource mediaSource = mViewModel.getBrowsedMediaSource().getValue();
return getActivity().getString(
R.string.cannot_connect_to_app,
mediaSource != null
@@ -549,58 +328,40 @@
}
}
-
-
- private void onItemsUpdate(boolean forRoot, FutureData<List<MediaItemMetadata>> futureData) {
-
- // Prevent showing loading spinner or any error messages if search is uninitialized
- if (mIsSearchController && TextUtils.isEmpty(mSearchQuery)) {
- return;
- }
-
- if (!forRoot && !mBrowseTreeHasChildren && !mIsSearchController) {
- // Ignore live data ghost values
- return;
- }
-
- if (futureData.isLoading()) {
- startLoadingIndicator();
+ private void onItemsUpdate(@Nullable FutureData<List<MediaItemMetadata>> futureData) {
+ if (futureData == null || futureData.isLoading()) {
ViewUtils.hideViewAnimated(mErrorIcon, 0);
ViewUtils.hideViewAnimated(mMessage, 0);
+
// TODO(b/139759881) build a jank-free animation of the transition.
mBrowseList.setAlpha(0f);
mLimitedBrowseAdapter.submitItems(null, null);
- if (forRoot) {
- if (Log.isLoggable(TAG, Log.INFO)) {
- Log.i(TAG, "Loading browse tree...");
- }
- mBrowseTreeHasChildren = false;
- updateTabs(null);
+ if (futureData != null) {
+ startLoadingIndicator();
}
return;
}
stopLoadingIndicator();
- List<MediaItemMetadata> items =
- BrowsedMediaItems.filterItems(forRoot, futureData.getData());
- if (forRoot) {
- boolean browseTreeHasChildren = items != null && !items.isEmpty();
- if (Log.isLoggable(TAG, Log.INFO)) {
- Log.i(TAG, "Browse tree loaded, status (has children or not) changed: "
- + mBrowseTreeHasChildren + " -> " + browseTreeHasChildren);
+ List<MediaItemMetadata> items = MediaBrowserViewModelImpl.filterItems(
+ /*root*/ !mDisplayMediaItems, futureData.getData());
+ if (mDisplayMediaItems) {
+ mLimitedBrowseAdapter.submitItems(mParentItem, items);
+
+ List<MediaItemMetadata> lastNodes =
+ MediaBrowserViewModelImpl.selectBrowseableItems(futureData.getPastData());
+ Collection<MediaItemMetadata> removedNodes =
+ MediaBrowserViewModelImpl.computeRemovedItems(lastNodes, items);
+ if (!removedNodes.isEmpty()) {
+ mCallbacks.onChildrenNodesRemoved(this, removedNodes);
}
- mBrowseTreeHasChildren = browseTreeHasChildren;
- mCallbacks.onRootLoaded();
- updateTabs(items != null ? items : new ArrayList<>());
- } else {
- mLimitedBrowseAdapter.submitItems(getCurrentMediaItem(), items);
}
- int duration = forRoot ? 0 : mFadeDuration;
+ int duration = mFadeDuration;
if (items == null) {
- mMessage.setText(getErrorMessage(forRoot));
+ mMessage.setText(getErrorMessage());
ViewUtils.hideViewAnimated(mBrowseList, duration);
ViewUtils.showViewAnimated(mMessage, duration);
ViewUtils.showViewAnimated(mErrorIcon, duration);
@@ -609,10 +370,14 @@
ViewUtils.hideViewAnimated(mBrowseList, duration);
ViewUtils.hideViewAnimated(mErrorIcon, duration);
ViewUtils.showViewAnimated(mMessage, duration);
- } else if (!forRoot) {
+ } else {
ViewUtils.showViewAnimated(mBrowseList, duration);
ViewUtils.hideViewAnimated(mErrorIcon, duration);
ViewUtils.hideViewAnimated(mMessage, duration);
}
+
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, "onItemsUpdate " + getDebugInfo());
+ }
}
}
diff --git a/src/com/android/car/media/MediaActivity.java b/src/com/android/car/media/MediaActivity.java
index a2991fd..e2d6394 100644
--- a/src/com/android/car/media/MediaActivity.java
+++ b/src/com/android/car/media/MediaActivity.java
@@ -18,6 +18,7 @@
import static android.car.media.CarMediaManager.MEDIA_SOURCE_MODE_BROWSE;
import static com.android.car.apps.common.util.VectorMath.EPSILON;
+import static com.android.car.arch.common.LiveDataFunctions.dataOf;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
@@ -26,7 +27,6 @@
import android.car.Car;
import android.car.content.pm.CarPackageManager;
import android.car.drivingstate.CarUxRestrictions;
-import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
@@ -56,9 +56,9 @@
import com.android.car.media.common.MediaItemMetadata;
import com.android.car.media.common.MinimizedPlaybackControlBar;
import com.android.car.media.common.PlaybackErrorsHelper;
+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.ui.AlertDialogBuilder;
import com.android.car.ui.utils.CarUxRestrictionsUtil;
@@ -70,7 +70,7 @@
* This activity controls the UI of media. It also updates the connection status for the media app
* by broadcast.
*/
-public class MediaActivity extends FragmentActivity implements BrowseViewController.Callbacks {
+public class MediaActivity extends FragmentActivity implements MediaActivityController.Callbacks {
private static final String TAG = "MediaActivity";
/** Configuration (controlled from resources) */
@@ -80,16 +80,13 @@
private PlaybackViewModel.PlaybackController mPlaybackController;
/** Layout views */
- private View mRootView;
private PlaybackFragment mPlaybackFragment;
- private BrowseViewController mSearchController;
- private BrowseViewController mBrowseController;
+ private MediaActivityController mMediaActivityController;
private MinimizedPlaybackControlBar mMiniPlaybackControls;
private ViewGroup mBrowseContainer;
private ViewGroup mPlaybackContainer;
private ViewGroup mErrorContainer;
private ErrorScreenController mErrorController;
- private ViewGroup mSearchContainer;
private Toast mToast;
private AlertDialog mDialog;
@@ -119,14 +116,14 @@
/**
* Possible modes of the application UI
+ * Todo: refactor into non exclusive flags to allow concurrent modes (eg: play details & browse)
+ * (b/179292793).
*/
enum Mode {
- /** The user is browsing a media source */
+ /** The user is browsing or searching a media source */
BROWSING,
/** The user is interacting with the full screen playback UI */
PLAYBACK,
- /** The user is searching within a media source */
- SEARCHING,
/** There's no browse tree and playback doesn't work. */
FATAL_ERROR
}
@@ -142,8 +139,6 @@
mCloseVectorY = res.getFloat(R.dimen.media_activity_close_vector_y);
mCloseVectorNorm = VectorMath.norm2(mCloseVectorX, mCloseVectorY);
-
- MediaSourceViewModel mediaSourceViewModel = getMediaSourceViewModel();
// TODO(b/151174811): Use appropriate modes, instead of just MEDIA_SOURCE_MODE_BROWSE
PlaybackViewModel playbackViewModel = getPlaybackViewModel();
ViewModel localViewModel = getInnerViewModel();
@@ -156,10 +151,7 @@
}
mMode = localViewModel.getSavedMode();
- mRootView = findViewById(R.id.media_activity_root);
-
- mediaSourceViewModel.getPrimaryMediaSource().observe(this,
- this::onMediaSourceChanged);
+ localViewModel.getBrowsedMediaSource().observe(this, this::onMediaSourceChanged);
mPlaybackFragment = new PlaybackFragment();
mPlaybackFragment.setListener(mPlaybackFragmentListener);
@@ -174,15 +166,12 @@
mBrowseContainer = findViewById(R.id.fragment_container);
mErrorContainer = findViewById(R.id.error_container);
mPlaybackContainer = findViewById(R.id.playback_container);
- mSearchContainer = findViewById(R.id.search_container);
getSupportFragmentManager().beginTransaction()
.replace(R.id.playback_container, mPlaybackFragment)
.commit();
- mBrowseController = BrowseViewController.newInstance(this,
+ mMediaActivityController = new MediaActivityController(this, getMediaItemsRepository(),
mCarPackageManager, mBrowseContainer);
- mSearchController = BrowseViewController.newSearchInstance(this,
- mCarPackageManager, mSearchContainer);
playbackViewModel.getPlaybackController().observe(this,
playbackController -> {
@@ -201,17 +190,13 @@
mCarUxRestrictionsUtil.register(mListener);
mPlaybackContainer.setOnTouchListener(new ClosePlaybackDetector(this));
-
- localViewModel.getMiniControlsVisible().observe(this, visible -> {
- mBrowseController.onPlaybackControlsChanged(visible);
- mSearchController.onPlaybackControlsChanged(visible);
- });
}
@Override
protected void onDestroy() {
mCarUxRestrictionsUtil.unregister(mListener);
mCar.disconnect();
+ mMediaActivityController.onDestroy();
super.onDestroy();
}
@@ -244,7 +229,7 @@
boolean isFatalError = false;
if (!TextUtils.isEmpty(displayedMessage)) {
- if (mBrowseController.browseTreeHasChildren()) {
+ if (mMediaActivityController.browseTreeHasChildren()) {
if (intent != null && !isUxRestricted()) {
showDialog(intent, displayedMessage, label,
getString(android.R.string.cancel));
@@ -271,7 +256,7 @@
private ErrorScreenController getErrorController() {
if (mErrorController == null) {
mErrorController = new ErrorScreenController(this, mCarPackageManager, mErrorContainer);
- MediaSource mediaSource = getMediaSourceViewModel().getPrimaryMediaSource().getValue();
+ MediaSource mediaSource = getInnerViewModel().getBrowsedMediaSource().getValue();
mErrorController.onMediaSourceChanged(mediaSource);
}
return mErrorController;
@@ -319,11 +304,8 @@
case PLAYBACK:
changeMode(Mode.BROWSING);
break;
- case SEARCHING:
- mSearchController.onBackPressed();
- break;
case BROWSING:
- boolean handled = mBrowseController.onBackPressed();
+ boolean handled = mMediaActivityController.onBackPressed();
if (handled) return;
// Fall through.
case FATAL_ERROR:
@@ -338,16 +320,7 @@
* @param mediaSource the new media source we are going to try to browse
*/
private void onMediaSourceChanged(@Nullable MediaSource mediaSource) {
- ComponentName savedMediaSource = getInnerViewModel().getSavedMediaSource();
- if (Log.isLoggable(TAG, Log.INFO)) {
- Log.i(TAG, "MediaSource changed from " + savedMediaSource + " to " + mediaSource);
- }
- savedMediaSource = mediaSource != null ? mediaSource.getBrowseServiceComponentName() : null;
- getInnerViewModel().saveMediaSource(savedMediaSource);
-
- mBrowseController.onMediaSourceChanged(mediaSource);
- mSearchController.onMediaSourceChanged(mediaSource);
if (mErrorController != null) {
mErrorController.onMediaSourceChanged(mediaSource);
}
@@ -369,8 +342,7 @@
}
}
- @Override
- public void changeMode(Mode mode) {
+ private void changeMode(Mode mode) {
if (mMode == mode) {
if (Log.isLoggable(TAG, Log.INFO)) {
Log.i(TAG, "Mode " + mMode + " change is ignored");
@@ -398,7 +370,6 @@
ViewUtils.showViewAnimated(mErrorContainer, mFadeDuration);
ViewUtils.hideViewAnimated(mPlaybackContainer, fadeOutDuration);
ViewUtils.hideViewAnimated(mBrowseContainer, fadeOutDuration);
- ViewUtils.hideViewAnimated(mSearchContainer, fadeOutDuration);
break;
case PLAYBACK:
mPlaybackContainer.setX(0);
@@ -407,27 +378,18 @@
ViewUtils.hideViewAnimated(mErrorContainer, fadeOutDuration);
ViewUtils.showViewAnimated(mPlaybackContainer, mFadeDuration);
ViewUtils.hideViewAnimated(mBrowseContainer, fadeOutDuration);
- ViewUtils.hideViewAnimated(mSearchContainer, fadeOutDuration);
break;
case BROWSING:
if (oldMode == Mode.PLAYBACK) {
ViewUtils.hideViewAnimated(mErrorContainer, 0);
ViewUtils.showViewAnimated(mBrowseContainer, 0);
- ViewUtils.hideViewAnimated(mSearchContainer, 0);
animateOutPlaybackContainer(fadeOutDuration);
} else {
ViewUtils.hideViewAnimated(mErrorContainer, fadeOutDuration);
ViewUtils.hideViewAnimated(mPlaybackContainer, fadeOutDuration);
ViewUtils.showViewAnimated(mBrowseContainer, mFadeDuration);
- ViewUtils.hideViewAnimated(mSearchContainer, fadeOutDuration);
}
break;
- case SEARCHING:
- ViewUtils.hideViewAnimated(mErrorContainer, fadeOutDuration);
- ViewUtils.hideViewAnimated(mPlaybackContainer, fadeOutDuration);
- ViewUtils.hideViewAnimated(mBrowseContainer, fadeOutDuration);
- ViewUtils.showViewAnimated(mSearchContainer, mFadeDuration);
- break;
}
}
@@ -477,7 +439,10 @@
final boolean shouldShowMiniPlaybackControls =
mCanShowMiniPlaybackControls && mMode != Mode.PLAYBACK;
if (shouldShowMiniPlaybackControls) {
- ViewUtils.showViewAnimated(mMiniPlaybackControls, mFadeDuration);
+ Boolean visible = getInnerViewModel().getMiniControlsVisible().getValue();
+ if (visible != Boolean.TRUE) {
+ ViewUtils.showViewAnimated(mMiniPlaybackControls, mFadeDuration);
+ }
} else {
ViewUtils.hideViewAnimated(mMiniPlaybackControls, fadeOutDuration);
}
@@ -485,14 +450,12 @@
}
@Override
- public void onPlayableItemClicked(MediaItemMetadata item) {
+ public void onPlayableItemClicked(@NonNull MediaItemMetadata item) {
mPlaybackController.playItem(item);
boolean switchToPlayback = getResources().getBoolean(
R.bool.switch_to_playback_view_when_playable_item_is_clicked);
if (switchToPlayback) {
changeMode(Mode.PLAYBACK);
- } else if (mMode == Mode.SEARCHING) {
- changeMode(Mode.BROWSING);
}
setIntent(null);
}
@@ -508,8 +471,8 @@
return this;
}
- private MediaSourceViewModel getMediaSourceViewModel() {
- return MediaSourceViewModel.get(getApplication(), MEDIA_SOURCE_MODE_BROWSE);
+ private MediaItemsRepository getMediaItemsRepository() {
+ return MediaItemsRepository.get(getApplication(), MEDIA_SOURCE_MODE_BROWSE);
}
private PlaybackViewModel getPlaybackViewModel() {
@@ -526,14 +489,18 @@
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;
String mSearchQuery;
boolean mQueueVisible = false;
}
private boolean mNeedsInitialization = true;
private PlaybackViewModel mPlaybackViewModel;
- private ComponentName mMediaSource;
- private final Map<ComponentName, MediaServiceState> mStates = new HashMap<>();
+ private final MutableLiveData<MediaSource> mBrowsedMediaSource = dataOf(null);
+ private final Map<MediaSource, MediaServiceState> mStates = new HashMap<>();
private MutableLiveData<Boolean> mIsMiniControlsVisible = new MutableLiveData<>();
public ViewModel(@NonNull Application application) {
@@ -561,10 +528,11 @@
}
MediaServiceState getSavedState() {
- MediaServiceState state = mStates.get(mMediaSource);
+ MediaSource source = mBrowsedMediaSource.getValue();
+ MediaServiceState state = mStates.get(source);
if (state == null) {
state = new MediaServiceState();
- mStates.put(mMediaSource, state);
+ mStates.put(source, state);
}
return state;
}
@@ -591,26 +559,43 @@
return getSavedState().mQueueVisible;
}
- void saveMediaSource(ComponentName mediaSource) {
- mMediaSource = mediaSource;
+ void saveBrowsedMediaSource(MediaSource mediaSource) {
+ mBrowsedMediaSource.setValue(mediaSource);
}
- ComponentName getSavedMediaSource() {
- return mMediaSource;
+ LiveData<MediaSource> getBrowsedMediaSource() {
+ return mBrowsedMediaSource;
}
- Stack<MediaItemMetadata> getBrowseStack() {
+ @NonNull Stack<MediaItemMetadata> getBrowseStack() {
return getSavedState().mBrowseStack;
}
- Stack<MediaItemMetadata> getSearchStack() {
+ @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
new file mode 100644
index 0000000..a00088f
--- /dev/null
+++ b/src/com/android/car/media/MediaActivityController.java
@@ -0,0 +1,624 @@
+/*
+ * Copyright (C) 2020 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 com.android.car.apps.common.util.ViewUtils.showHideViewAnimated;
+
+import android.car.content.pm.CarPackageManager;
+import android.content.Context;
+import android.support.v4.media.MediaBrowserCompat;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.FragmentActivity;
+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.ViewUtils;
+import com.android.car.apps.common.util.ViewUtils.ViewAnimEndListener;
+import com.android.car.arch.common.FutureData;
+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;
+import com.android.car.ui.baselayout.Insets;
+import com.android.car.ui.toolbar.Toolbar;
+
+import java.util.ArrayList;
+import java.util.Collection;
+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}.
+ * TODO: finish moving control code out of MediaActivity (b/179292809).
+ */
+public class MediaActivityController extends ViewControllerBase {
+
+ private static final String TAG = "MediaActivityCtr";
+
+ private final MediaItemsRepository mMediaItemsRepository;
+ private final Callbacks mCallbacks;
+ private final ViewGroup mBrowseArea;
+ private Insets mCarUiInsets;
+ private boolean mPlaybackControlsVisible;
+
+ private final Map<MediaItemMetadata, BrowseViewController> mBrowseViewControllersByNode =
+ 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;
+
+ /**
+ * 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 final MediaActivity.ViewModel mViewModel;
+
+ private int mRootBrowsableHint;
+ private int mRootPlayableHint;
+ private boolean mBrowseTreeHasChildren;
+ private boolean mAcceptTabSelection = true;
+
+ /**
+ * Media items to display as tabs. If null, it means we haven't finished loading them yet. If
+ * empty, it means there are no tabs to show
+ */
+ @Nullable
+ private List<MediaItemMetadata> mTopItems;
+
+ private final Observer<BrowsingState> mMediaBrowsingObserver =
+ this::onMediaBrowsingStateChanged;
+
+ /**
+ * Callbacks (implemented by the hosting Activity)
+ */
+ public interface Callbacks {
+
+ /** Invoked when the user clicks on a browsable item. */
+ void onPlayableItemClicked(@NonNull MediaItemMetadata item);
+
+ /** Called once the list of the root node's children has been loaded. */
+ void onRootLoaded();
+
+ /** Returns the activity. */
+ FragmentActivity getActivity();
+ }
+
+ /**
+ * Moves the user one level up in the browse/search tree. Returns whether that was possible.
+ */
+ private boolean navigateBack() {
+ boolean result = false;
+ if (!isAtTopStack()) {
+ hideAndDestroyControllerForItem(getStack().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;
+ }
+
+ /**
+ * @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;
+ }
+ }
+
+ private void clearMediaSource() {
+ showSearchMode(false);
+ for (BrowseViewController controller : mBrowseViewControllersByNode.values()) {
+ controller.destroy();
+ }
+ mBrowseViewControllersByNode.clear();
+ mBrowseTreeHasChildren = false;
+ }
+
+ private void updateSearchQuery(@Nullable String query) {
+ mMediaItemsRepository.setSearchQuery(query);
+ }
+
+ /**
+ * Clears search state, removes any UI elements from previous results.
+ */
+ @Override
+ void onMediaSourceChanged(@Nullable MediaSource mediaSource) {
+ super.onMediaSourceChanged(mediaSource);
+
+ updateTabs((mediaSource != null) ? null : new ArrayList<>());
+
+ mSearchStack = mViewModel.getSearchStack();
+ mBrowseStack = mViewModel.getBrowseStack();
+
+ updateAppBar();
+ }
+
+ private void onMediaBrowsingStateChanged(BrowsingState newBrowsingState) {
+ switch (newBrowsingState.mConnectionStatus) {
+ case CONNECTING:
+ break;
+ case CONNECTED:
+ MediaBrowserCompat browser = newBrowsingState.mBrowser;
+ mRootBrowsableHint = MediaBrowserViewModelImpl.getRootBrowsableHint(browser);
+ mRootPlayableHint = MediaBrowserViewModelImpl.getRootPlayableHint(browser);
+
+ boolean canSearch = MediaBrowserViewModelImpl.getSupportsSearch(browser);
+ mAppBarController.setSearchSupported(canSearch);
+ break;
+
+ case DISCONNECTING:
+ case REJECTED:
+ case SUSPENDED:
+ clearMediaSource();
+ break;
+ }
+
+ MediaSource savedSource = mViewModel.getBrowsedMediaSource().getValue();
+ MediaSource mediaSource = newBrowsingState.mMediaSource;
+ if (Log.isLoggable(TAG, Log.INFO)) {
+ Log.i(TAG, "MediaSource changed from " + savedSource + " to " + mediaSource);
+ }
+
+ mViewModel.saveBrowsedMediaSource(mediaSource);
+ onMediaSourceChanged(mediaSource);
+ }
+
+
+ MediaActivityController(Callbacks callbacks, MediaItemsRepository mediaItemsRepo,
+ CarPackageManager carPackageManager, ViewGroup container) {
+ super(callbacks.getActivity(), carPackageManager, container, R.layout.fragment_browse);
+
+ FragmentActivity activity = callbacks.getActivity();
+ 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);
+
+ MediaItemsLiveData rootMediaItems = mediaItemsRepo.getRootMediaItems();
+ mRootLoadingController = BrowseViewController.newRootController(
+ mBrowseCallbacks, mBrowseArea, rootMediaItems);
+ mRootLoadingController.getContent().setAlpha(1f);
+
+ mSearchResultsController = BrowseViewController.newSearchResultsController(
+ mBrowseCallbacks, mBrowseArea, mMediaItemsRepository.getSearchMediaItems());
+
+ boolean showingSearch = mViewModel.isShowingSearchResults();
+ ViewUtils.setVisible(mSearchResultsController.getContent(), showingSearch);
+ if (showingSearch) {
+ mSearchResultsController.getContent().setAlpha(1f);
+ }
+
+ mAppBarController.setListener(mAppBarListener);
+ mAppBarController.setSearchQuery(mViewModel.getSearchQuery());
+ if (mAppBarController.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);
+
+ ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
+ toolbarSearchResultsView.setLayoutParams(params);
+ toolbarSearchResultsView.setLayoutManager(new LinearLayoutManager(activity));
+ toolbarSearchResultsView.setBackground(
+ activity.getDrawable(R.drawable.car_ui_ime_wide_screen_background));
+
+ mAppBarController.setSearchResultsView(toolbarSearchResultsView);
+ }
+
+ updateAppBar();
+
+ // Observe forever ensures the caches are destroyed even while the activity isn't resumed.
+ mediaItemsRepo.getBrowsingState().observeForever(mMediaBrowsingObserver);
+
+ rootMediaItems.observe(activity, this::onRootMediaItemsUpdate);
+ mViewModel.getMiniControlsVisible().observe(activity, this::onPlaybackControlsChanged);
+ }
+
+ void onDestroy() {
+ mMediaItemsRepository.getBrowsingState().removeObserver(mMediaBrowsingObserver);
+ }
+
+ private AppBarController.AppBarListener mAppBarListener = new BasicAppBarListener() {
+ @Override
+ public void onTabSelected(MediaItemMetadata item) {
+ if (mAcceptTabSelection && (item != null) && (item != mViewModel.getSelectedTab())) {
+ clearStack(mBrowseStack);
+ mBrowseStack.push(item);
+ showCurrentNode(true);
+ }
+ }
+
+ @Override
+ public void onSearchSelection() {
+ if (mViewModel.isSearching()) {
+ reopenSearch();
+ } else {
+ showSearchMode(true);
+ updateAppBar();
+ }
+ }
+
+ @Override
+ public void onSearch(String query) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "onSearch: " + query);
+ }
+ mViewModel.setSearchQuery(query);
+ updateSearchQuery(query);
+ }
+ };
+
+ private final BrowseViewController.Callbacks mBrowseCallbacks =
+ new BrowseViewController.Callbacks() {
+ @Override
+ public void onPlayableItemClicked(@NonNull MediaItemMetadata item) {
+ hideKeyboard();
+ mCallbacks.onPlayableItemClicked(item);
+ }
+
+ @Override
+ public void onBrowsableItemClicked(@NonNull MediaItemMetadata item) {
+ hideKeyboard();
+ navigateInto(item);
+ }
+
+ @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.
+ }
+ }
+ }
+ }
+
+ @Override
+ public FragmentActivity getActivity() {
+ return mCallbacks.getActivity();
+ }
+ };
+
+ private final ViewAnimEndListener mViewAnimEndListener = view -> {
+ BrowseViewController toDestroy = mBrowseViewControllersToDestroy.remove(view);
+ if (toDestroy != null) {
+ toDestroy.destroy();
+ }
+ };
+
+ boolean onBackPressed() {
+ boolean success = navigateBack();
+ if (!success && mViewModel.isSearching()) {
+ showSearchMode(false);
+ updateAppBar();
+ return true;
+ }
+ return success;
+ }
+
+ boolean browseTreeHasChildren() {
+ return mBrowseTreeHasChildren;
+ }
+
+ private void navigateInto(@NonNull MediaItemMetadata item) {
+ showSearchResults(false);
+
+ // Hide the current node (parent)
+ showCurrentNode(false);
+
+ // Make item the current node
+ getStack().push(item);
+
+ // Show the current node (item)
+ showCurrentNode(true);
+
+ updateAppBar();
+ }
+
+ private BrowseViewController getControllerForItem(@NonNull MediaItemMetadata item) {
+ BrowseViewController controller = mBrowseViewControllersByNode.get(item);
+ if (controller == null) {
+ controller = BrowseViewController.newBrowseController(mBrowseCallbacks, mBrowseArea,
+ item, mMediaItemsRepository.getMediaChildren(item.getId()), mRootBrowsableHint,
+ mRootPlayableHint);
+
+ if (mCarUiInsets != null) {
+ controller.onCarUiInsetsChanged(mCarUiInsets);
+ }
+ controller.onPlaybackControlsChanged(mPlaybackControlsVisible);
+
+ mBrowseViewControllersByNode.put(item, controller);
+ }
+ return controller;
+ }
+
+ private void showCurrentNode(boolean show) {
+ MediaItemMetadata currentNode = getCurrentMediaItem();
+ if (currentNode == null) {
+ return;
+ }
+ // Only create a controller to show it.
+ BrowseViewController controller = show ? getControllerForItem(currentNode) :
+ mBrowseViewControllersByNode.get(currentNode);
+
+ if (controller != null) {
+ showHideViewAnimated(show, controller.getContent(), mFadeDuration,
+ mViewAnimEndListener);
+ }
+ }
+
+ private void showSearchResults(boolean show) {
+ if (mViewModel.isShowingSearchResults() != show) {
+ mViewModel.setShowingSearchResults(show);
+ showHideViewAnimated(show, mSearchResultsController.getContent(), mFadeDuration, null);
+ }
+ }
+
+ private void showSearchMode(boolean show) {
+ if (mViewModel.isSearching() != show) {
+ if (show) {
+ showCurrentNode(false);
+ }
+
+ mViewModel.setSearching(show);
+ showSearchResults(show);
+
+ if (!show) {
+ showCurrentNode(true);
+ }
+ }
+ }
+
+ /**
+ * @return the current item being displayed
+ */
+ @Nullable
+ private MediaItemMetadata getCurrentMediaItem() {
+ Stack<MediaItemMetadata> stack = getStack();
+ return stack.isEmpty() ? null : stack.lastElement();
+ }
+
+ @Override
+ public void onCarUiInsetsChanged(@NonNull Insets insets) {
+ mCarUiInsets = insets;
+ for (BrowseViewController controller : mBrowseViewControllersByNode.values()) {
+ controller.onCarUiInsetsChanged(mCarUiInsets);
+ }
+ mRootLoadingController.onCarUiInsetsChanged(mCarUiInsets);
+ mSearchResultsController.onCarUiInsetsChanged(mCarUiInsets);
+ }
+
+ void onPlaybackControlsChanged(boolean visible) {
+ mPlaybackControlsVisible = visible;
+ for (BrowseViewController controller : mBrowseViewControllersByNode.values()) {
+ controller.onPlaybackControlsChanged(mPlaybackControlsVisible);
+ }
+ mRootLoadingController.onPlaybackControlsChanged(mPlaybackControlsVisible);
+ mSearchResultsController.onPlaybackControlsChanged(mPlaybackControlsVisible);
+ }
+
+ private void hideKeyboard() {
+ InputMethodManager in =
+ (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
+ in.hideSoftInputFromWindow(mContent.getWindowToken(), 0);
+ }
+
+ private void hideAndDestroyControllerForItem(@Nullable MediaItemMetadata item) {
+ if (item == null) {
+ return;
+ }
+ BrowseViewController controller = mBrowseViewControllersByNode.get(item);
+ if (controller == null) {
+ return;
+ }
+
+ if (controller.getContent().getVisibility() == View.VISIBLE) {
+ View view = controller.getContent();
+ mBrowseViewControllersToDestroy.put(view, controller);
+ showHideViewAnimated(false, view, mFadeDuration, mViewAnimEndListener);
+ } else {
+ controller.destroy();
+ }
+ mBrowseViewControllersByNode.remove(item);
+ }
+
+ /**
+ * Clears the given stack (or a portion of a stack) and destroys the old controllers (after
+ * their view is hidden).
+ */
+ private void clearStack(List<MediaItemMetadata> stack) {
+ for (MediaItemMetadata item : stack) {
+ hideAndDestroyControllerForItem(item);
+ }
+ stack.clear();
+ }
+
+ /**
+ * Updates the tabs displayed on the app bar, based on the top level items on the browse tree.
+ * If there is at least one browsable item, we show the browse content of that node. If there
+ * are only playable items, then we show those items. If there are not items at all, we show the
+ * empty message. If we receive null, we show the error message.
+ *
+ * @param items top level items, null if the items are still being loaded, or empty list if
+ * items couldn't be loaded.
+ */
+ private void updateTabs(@Nullable List<MediaItemMetadata> items) {
+ if (Objects.equals(mTopItems, items)) {
+ // When coming back to the app, the live data sends an update even if the list hasn't
+ // changed. Updating the tabs then recreates the browse view, which produces jank
+ // (b/131830876), and also resets the navigation to the top of the first tab...
+ return;
+ }
+ mTopItems = items;
+
+ if (mTopItems == null || mTopItems.isEmpty()) {
+ mAppBarController.setItems(null);
+ mAppBarController.setActiveItem(null);
+ if (items != null) {
+ // Only do this when not loading the tabs or we loose the saved one.
+ clearStack(mBrowseStack);
+ }
+ updateAppBar();
+ return;
+ }
+
+ MediaItemMetadata oldTab = mViewModel.getSelectedTab();
+ MediaItemMetadata newTab = items.contains(oldTab) ? oldTab : items.get(0);
+
+ try {
+ mAcceptTabSelection = false;
+ mAppBarController.setItems(mTopItems.size() == 1 ? null : mTopItems);
+ mAppBarController.setActiveItem(newTab);
+
+ if (oldTab != newTab) {
+ // Tabs belong to the browse stack.
+ clearStack(mBrowseStack);
+ mBrowseStack.push(newTab);
+ }
+
+ if (!mViewModel.isShowingSearchResults()) {
+ // Needed when coming back to an app after a config change or from another app,
+ // or when the tab actually changes.
+ showCurrentNode(true);
+ }
+ } finally {
+ mAcceptTabSelection = true;
+ }
+ updateAppBar();
+ }
+
+ private void updateAppBarTitle() {
+ boolean isStacked = !isAtTopStack();
+
+ final CharSequence title;
+ if (isStacked) {
+ // If not at top level, show the current item as title
+ title = getCurrentMediaItem().getTitle();
+ } else if (mTopItems == null) {
+ // If still loading the tabs, force to show an empty bar.
+ title = "";
+ } else if (mTopItems.size() == 1) {
+ // If we finished loading tabs and there is only one, use that as title.
+ title = mTopItems.get(0).getTitle();
+ } else {
+ // Otherwise (no tabs or more than 1 tabs), show the current media source title.
+ MediaSource mediaSource = mMediaSourceVM.getPrimaryMediaSource().getValue();
+ title = getAppBarDefaultTitle(mediaSource);
+ }
+
+ mAppBarController.setTitle(title);
+ }
+
+ /**
+ * 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();
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "App bar is in stacked state: " + isStacked);
+ }
+ Toolbar.State unstackedState = isSearching ? Toolbar.State.SEARCH : Toolbar.State.HOME;
+ updateAppBarTitle();
+ mAppBarController.setState(isStacked ? Toolbar.State.SUBPAGE : unstackedState);
+ mAppBarController.showSearchIfSupported(!isSearching || isStacked);
+ }
+
+ private void onRootMediaItemsUpdate(FutureData<List<MediaItemMetadata>> data) {
+ if (data.isLoading()) {
+ if (Log.isLoggable(TAG, Log.INFO)) {
+ Log.i(TAG, "Loading browse tree...");
+ }
+ mBrowseTreeHasChildren = false;
+ updateTabs(null);
+ return;
+ }
+
+ List<MediaItemMetadata> items =
+ MediaBrowserViewModelImpl.filterItems(/*forRoot*/ true, data.getData());
+
+ boolean browseTreeHasChildren = items != null && !items.isEmpty();
+ if (Log.isLoggable(TAG, Log.INFO)) {
+ Log.i(TAG, "Browse tree loaded, status (has children or not) changed: "
+ + mBrowseTreeHasChildren + " -> " + browseTreeHasChildren);
+ }
+ mBrowseTreeHasChildren = browseTreeHasChildren;
+ mCallbacks.onRootLoaded();
+ updateTabs(items != null ? items : new ArrayList<>());
+ }
+
+}
diff --git a/src/com/android/car/media/MediaDispatcherActivity.java b/src/com/android/car/media/MediaDispatcherActivity.java
index b44feb9..973937b 100644
--- a/src/com/android/car/media/MediaDispatcherActivity.java
+++ b/src/com/android/car/media/MediaDispatcherActivity.java
@@ -67,6 +67,8 @@
// Launch custom app (e.g. Radio)
String srcPackage = mediaSrc.getPackageName();
newIntent = getPackageManager().getLaunchIntentForPackage(srcPackage);
+ newIntent.putExtra(Car.CAR_EXTRA_MEDIA_COMPONENT,
+ mediaSrc.getBrowseServiceComponentName().flattenToString());
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Getting launch intent for package : " + srcPackage + (newIntent != null
? " succeeded" : " failed"));
diff --git a/src/com/android/car/media/browse/BrowseAdapter.java b/src/com/android/car/media/browse/BrowseAdapter.java
index abf770d..847d270 100644
--- a/src/com/android/car/media/browse/BrowseAdapter.java
+++ b/src/com/android/car/media/browse/BrowseAdapter.java
@@ -33,7 +33,6 @@
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.Objects;
@@ -100,13 +99,13 @@
/**
* Callback invoked when a user clicks on a playable item.
*/
- protected void onPlayableItemClicked(MediaItemMetadata item) {
+ protected void onPlayableItemClicked(@NonNull MediaItemMetadata item) {
}
/**
* Callback invoked when a user clicks on a browsable item.
*/
- protected void onBrowsableItemClicked(MediaItemMetadata item) {
+ protected void onBrowsableItemClicked(@NonNull MediaItemMetadata item) {
}
/**
diff --git a/src/com/android/car/media/browse/LimitedBrowseAdapter.java b/src/com/android/car/media/browse/LimitedBrowseAdapter.java
index bbca980..6d3c6a7 100644
--- a/src/com/android/car/media/browse/LimitedBrowseAdapter.java
+++ b/src/com/android/car/media/browse/LimitedBrowseAdapter.java
@@ -65,10 +65,6 @@
}
};
- public BrowseAdapter getBrowseAdapter() {
- return mBrowseAdapter;
- }
-
/**
* @see BrowseAdapter#submitItems(MediaItemMetadata, List)
*/
diff --git a/src/com/android/car/media/widgets/AppBarController.java b/src/com/android/car/media/widgets/AppBarController.java
index 3c174a3..962f2ac 100644
--- a/src/com/android/car/media/widgets/AppBarController.java
+++ b/src/com/android/car/media/widgets/AppBarController.java
@@ -4,6 +4,7 @@
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
+import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -34,6 +35,8 @@
private int mMaxTabs;
private final ToolbarController mToolbarController;
+ private final boolean mUseSourceLogoForAppSelector;
+
@NonNull
private AppBarListener mListener = new AppBarListener();
private MenuItem mSearch;
@@ -43,6 +46,7 @@
private boolean mSearchSupported;
private boolean mShowSearchIfSupported;
+ private String mSearchQuery;
private Intent mAppSelectorIntent;
@@ -80,11 +84,19 @@
mToolbarController = controller;
mMaxTabs = context.getResources().getInteger(R.integer.max_tabs);
+ mUseSourceLogoForAppSelector =
+ context.getResources().getBoolean(R.bool.use_media_source_logo_for_app_selector);
+
mAppSelectorIntent = MediaSource.getSourceSelectorIntent(context, false);
mToolbarController.registerOnTabSelectedListener(tab ->
mListener.onTabSelected(((MediaItemTab) tab).getItem()));
- mToolbarController.registerOnSearchListener(query -> mListener.onSearch(query));
+ mToolbarController.registerOnSearchListener(query -> {
+ mSearchQuery = query;
+ mListener.onSearch(query);
+ });
+ mToolbarController.registerOnSearchCompletedListener(
+ () -> mListener.onSearch(mSearchQuery));
mSearch = MenuItem.builder(context)
.setToSearch()
.setOnClickListener(v -> mListener.onSearchSelection())
@@ -101,7 +113,9 @@
.build();
mAppSelector = MenuItem.builder(context)
.setTitle(R.string.menu_item_app_selector_title)
- .setIcon(R.drawable.ic_app_switch)
+ .setTinted(!mUseSourceLogoForAppSelector)
+ .setIcon(mUseSourceLogoForAppSelector
+ ? null : context.getDrawable(R.drawable.ic_app_switch))
.setOnClickListener(m -> context.startActivity(mAppSelectorIntent))
.build();
mToolbarController.setMenuItems(
@@ -202,7 +216,11 @@
}
public void setLogo(Drawable drawable) {
- mToolbarController.setLogo(drawable);
+ if (mUseSourceLogoForAppSelector) {
+ mAppSelector.setIcon(drawable);
+ } else {
+ mToolbarController.setLogo(drawable);
+ }
}
public void setSearchIcon(Drawable drawable) {
@@ -229,11 +247,17 @@
mToolbarController.setBackgroundShown(shown);
}
- public void registerOnBackListener(Toolbar.OnBackListener listener) {
- mToolbarController.registerOnBackListener(listener);
- }
-
public void setNavButtonMode(Toolbar.NavButtonMode mode) {
mToolbarController.setNavButtonMode(mode);
}
+
+ /** See {@link ToolbarController#canShowSearchResultItems}. */
+ public boolean canShowSearchResultsView() {
+ return mToolbarController.canShowSearchResultsView();
+ }
+
+ /** See {@link ToolbarController#setSearchResultsView}. */
+ public void setSearchResultsView(View view) {
+ mToolbarController.setSearchResultsView(view);
+ }
}