| /* |
| * 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 android.view.accessibility.AccessibilityNodeInfo.ACTION_FOCUS; |
| |
| import static com.android.car.apps.common.util.ViewUtils.showHideViewAnimated; |
| import static com.android.car.ui.utils.ViewUtils.LazyLayoutView; |
| |
| 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.FutureData; |
| import com.android.car.apps.common.util.ViewUtils; |
| import com.android.car.apps.common.util.ViewUtils.ViewAnimEndListener; |
| 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.FocusParkingView; |
| import com.android.car.ui.baselayout.Insets; |
| import com.android.car.ui.recyclerview.CarUiRecyclerView; |
| import com.android.car.ui.toolbar.NavButtonMode; |
| import com.android.car.ui.toolbar.SearchConfig; |
| import com.android.car.ui.toolbar.SearchMode; |
| |
| 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 final FocusParkingView mFpv; |
| 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) { |
| if (newBrowsingState == null) { |
| Log.e(TAG, "Null browsing state (no media source!)"); |
| return; |
| } |
| 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; |
| } |
| |
| mViewModel.saveBrowsedMediaSource(newBrowsingState.mMediaSource); |
| } |
| |
| |
| 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); |
| mFpv = activity.requireViewById(R.id.fpv); |
| |
| 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.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); |
| |
| 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.setSearchConfig(SearchConfig.builder() |
| .setSearchResultsView(toolbarSearchResultsView) |
| .build()); |
| } |
| |
| updateAppBar(); |
| |
| // Observe forever ensures the caches are destroyed even while the activity isn't resumed. |
| mediaItemsRepo.getBrowsingState().observeForever(mMediaBrowsingObserver); |
| |
| mViewModel.getBrowsedMediaSource().observeForever(future -> { |
| onMediaSourceChanged(future.isLoading() ? null : future.getData()); |
| }); |
| |
| 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(); |
| 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. |
| restoreFocusInCurrentNode(); |
| } |
| 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(); |
| } |
| |
| @NonNull |
| 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) { |
| showHideContentAnimated(show, controller.getContent(), mViewAnimEndListener); |
| } |
| } |
| |
| // If the current node has a CarUiRecyclerView and it's in rotary mode, restore focus in it. |
| // Should remain private and definitely NOT be called from MediaActivity#changeModeInternal |
| // 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) { |
| return; |
| } |
| BrowseViewController controller = getControllerForItem(currentNode); |
| CarUiRecyclerView carUiRecyclerView = |
| controller.getContent().findViewById(R.id.browse_list); |
| if (carUiRecyclerView != null && carUiRecyclerView instanceof LazyLayoutView |
| && !carUiRecyclerView.getView().hasFocus() |
| && !carUiRecyclerView.getView().isInTouchMode()) { |
| // Park the focus on the FocusParkingView to ensure that it can restore focus inside |
| // the LazyLayoutView successfully later. |
| mFpv.performAccessibilityAction(ACTION_FOCUS, null); |
| |
| LazyLayoutView lazyLayoutView = (LazyLayoutView) carUiRecyclerView; |
| com.android.car.ui.utils.ViewUtils.initFocus(lazyLayoutView); |
| } |
| } |
| |
| 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 |
| && !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 |
| // view will cause the Android framework to move focus to another view, causing visual |
| // jank. |
| if (!show && carUiRecyclerView.getView().hasFocus()) { |
| mFpv.performAccessibilityAction(ACTION_FOCUS, null); |
| } |
| // If a new CarUiRecyclerView is about to show and there is no view focused or the |
| // FocusParkingView is focused, restore focus in the new CarUiRecyclerView. |
| if (show) { |
| View focusedView = carUiRecyclerView.getView().getRootView().findFocus(); |
| if (focusedView == null || focusedView instanceof FocusParkingView) { |
| LazyLayoutView lazyLayoutView = (LazyLayoutView) carUiRecyclerView; |
| com.android.car.ui.utils.ViewUtils.initFocus(lazyLayoutView); |
| } |
| } |
| } |
| |
| showHideViewAnimated(show, content, mFadeDuration, listener); |
| } |
| |
| |
| |
| private void showSearchResults(boolean show) { |
| if (mViewModel.isShowingSearchResults() != show) { |
| mViewModel.setShowingSearchResults(show); |
| showHideContentAnimated(show, mSearchResultsController.getContent(), 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); |
| showHideContentAnimated(false, view, 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 CharSequence getAppBarTitle() { |
| boolean isStacked = !isAtTopStack(); |
| |
| final CharSequence title; |
| if (isStacked) { |
| // If not at top level, show the current item as title |
| MediaItemMetadata item = getCurrentMediaItem(); |
| title = item != null ? item.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); |
| } |
| |
| return 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); |
| } |
| |
| mAppBarController.setSearchMode(isSearching ? SearchMode.SEARCH : SearchMode.DISABLED); |
| mAppBarController.setNavButtonMode(isStacked || isSearching |
| ? NavButtonMode.BACK : NavButtonMode.DISABLED); |
| mAppBarController.setTitle(!isSearching ? getAppBarTitle() : null); |
| 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<>()); |
| } |
| |
| } |