1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.car.media; 18 19 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_FOCUS; 20 21 import static com.android.car.apps.common.util.ViewUtils.showHideViewAnimated; 22 import static com.android.car.ui.utils.ViewUtils.LazyLayoutView; 23 24 import android.car.content.pm.CarPackageManager; 25 import android.content.Context; 26 import android.support.v4.media.MediaBrowserCompat; 27 import android.util.Log; 28 import android.view.View; 29 import android.view.ViewGroup; 30 import android.view.inputmethod.InputMethodManager; 31 32 import androidx.annotation.NonNull; 33 import androidx.annotation.Nullable; 34 import androidx.fragment.app.FragmentActivity; 35 import androidx.lifecycle.Observer; 36 import androidx.lifecycle.ViewModelProviders; 37 import androidx.recyclerview.widget.LinearLayoutManager; 38 import androidx.recyclerview.widget.RecyclerView; 39 40 import com.android.car.apps.common.util.FutureData; 41 import com.android.car.apps.common.util.ViewUtils; 42 import com.android.car.apps.common.util.ViewUtils.ViewAnimEndListener; 43 import com.android.car.media.common.MediaItemMetadata; 44 import com.android.car.media.common.browse.MediaBrowserViewModelImpl; 45 import com.android.car.media.common.browse.MediaItemsRepository; 46 import com.android.car.media.common.browse.MediaItemsRepository.MediaItemsLiveData; 47 import com.android.car.media.common.source.MediaBrowserConnector.BrowsingState; 48 import com.android.car.media.common.source.MediaSource; 49 import com.android.car.media.widgets.AppBarController; 50 import com.android.car.ui.FocusParkingView; 51 import com.android.car.ui.baselayout.Insets; 52 import com.android.car.ui.recyclerview.CarUiRecyclerView; 53 import com.android.car.ui.toolbar.NavButtonMode; 54 import com.android.car.ui.toolbar.SearchConfig; 55 import com.android.car.ui.toolbar.SearchMode; 56 57 import java.util.ArrayList; 58 import java.util.Collection; 59 import java.util.HashMap; 60 import java.util.List; 61 import java.util.Map; 62 import java.util.Objects; 63 import java.util.Stack; 64 65 /** 66 * Controls the views of the {@link MediaActivity}. 67 * TODO: finish moving control code out of MediaActivity (b/179292809). 68 */ 69 public class MediaActivityController extends ViewControllerBase { 70 71 private static final String TAG = "MediaActivityCtr"; 72 73 private final MediaItemsRepository mMediaItemsRepository; 74 private final Callbacks mCallbacks; 75 private final ViewGroup mBrowseArea; 76 private final FocusParkingView mFpv; 77 private Insets mCarUiInsets; 78 private boolean mPlaybackControlsVisible; 79 80 private final Map<MediaItemMetadata, BrowseViewController> mBrowseViewControllersByNode = 81 new HashMap<>(); 82 83 // Controllers that should be destroyed once their view is hidden. 84 private final Map<View, BrowseViewController> mBrowseViewControllersToDestroy = new HashMap<>(); 85 86 private final BrowseViewController mRootLoadingController; 87 private final BrowseViewController mSearchResultsController; 88 89 /** 90 * Stores the reference to {@link MediaActivity.ViewModel#getBrowseStack}. 91 * Updated in {@link #onMediaSourceChanged}. 92 */ 93 private Stack<MediaItemMetadata> mBrowseStack; 94 /** 95 * Stores the reference to {@link MediaActivity.ViewModel#getSearchStack}. 96 * Updated in {@link #onMediaSourceChanged}. 97 */ 98 private Stack<MediaItemMetadata> mSearchStack; 99 private final MediaActivity.ViewModel mViewModel; 100 101 private int mRootBrowsableHint; 102 private int mRootPlayableHint; 103 private boolean mBrowseTreeHasChildren; 104 private boolean mAcceptTabSelection = true; 105 106 /** 107 * Media items to display as tabs. If null, it means we haven't finished loading them yet. If 108 * empty, it means there are no tabs to show 109 */ 110 @Nullable 111 private List<MediaItemMetadata> mTopItems; 112 113 private final Observer<BrowsingState> mMediaBrowsingObserver = 114 this::onMediaBrowsingStateChanged; 115 116 /** 117 * Callbacks (implemented by the hosting Activity) 118 */ 119 public interface Callbacks { 120 121 /** Invoked when the user clicks on a browsable item. */ onPlayableItemClicked(@onNull MediaItemMetadata item)122 void onPlayableItemClicked(@NonNull MediaItemMetadata item); 123 124 /** Called once the list of the root node's children has been loaded. */ onRootLoaded()125 void onRootLoaded(); 126 127 /** Returns the activity. */ getActivity()128 FragmentActivity getActivity(); 129 } 130 131 /** 132 * Moves the user one level up in the browse/search tree. Returns whether that was possible. 133 */ navigateBack()134 private boolean navigateBack() { 135 boolean result = false; 136 if (!isAtTopStack()) { 137 hideAndDestroyControllerForItem(getStack().pop()); 138 139 // Show the parent (if any) 140 showCurrentNode(true); 141 142 if (isAtTopStack() && mViewModel.isSearching()) { 143 showSearchResults(true); 144 } 145 146 updateAppBar(); 147 result = true; 148 } 149 return result; 150 } 151 reopenSearch()152 private void reopenSearch() { 153 clearStack(mSearchStack); 154 showSearchResults(true); 155 updateAppBar(); 156 } 157 getActivity()158 private FragmentActivity getActivity() { 159 return mCallbacks.getActivity(); 160 } 161 162 /** Returns the browse or search stack. */ getStack()163 private Stack<MediaItemMetadata> getStack() { 164 return mViewModel.isSearching() ? mSearchStack : mBrowseStack; 165 } 166 167 /** 168 * @return whether the user is at the top of the browsing stack. 169 */ isAtTopStack()170 private boolean isAtTopStack() { 171 if (mViewModel.isSearching()) { 172 return mSearchStack.isEmpty(); 173 } else { 174 // The mBrowseStack stack includes the tab... 175 return mBrowseStack.size() <= 1; 176 } 177 } 178 clearMediaSource()179 private void clearMediaSource() { 180 showSearchMode(false); 181 for (BrowseViewController controller : mBrowseViewControllersByNode.values()) { 182 controller.destroy(); 183 } 184 mBrowseViewControllersByNode.clear(); 185 mBrowseTreeHasChildren = false; 186 } 187 updateSearchQuery(@ullable String query)188 private void updateSearchQuery(@Nullable String query) { 189 mMediaItemsRepository.setSearchQuery(query); 190 } 191 192 /** 193 * Clears search state, removes any UI elements from previous results. 194 */ 195 @Override onMediaSourceChanged(@ullable MediaSource mediaSource)196 void onMediaSourceChanged(@Nullable MediaSource mediaSource) { 197 super.onMediaSourceChanged(mediaSource); 198 199 updateTabs((mediaSource != null) ? null : new ArrayList<>()); 200 201 mSearchStack = mViewModel.getSearchStack(); 202 mBrowseStack = mViewModel.getBrowseStack(); 203 204 updateAppBar(); 205 } 206 onMediaBrowsingStateChanged(BrowsingState newBrowsingState)207 private void onMediaBrowsingStateChanged(BrowsingState newBrowsingState) { 208 if (newBrowsingState == null) { 209 Log.e(TAG, "Null browsing state (no media source!)"); 210 return; 211 } 212 switch (newBrowsingState.mConnectionStatus) { 213 case CONNECTING: 214 break; 215 case CONNECTED: 216 MediaBrowserCompat browser = newBrowsingState.mBrowser; 217 mRootBrowsableHint = MediaBrowserViewModelImpl.getRootBrowsableHint(browser); 218 mRootPlayableHint = MediaBrowserViewModelImpl.getRootPlayableHint(browser); 219 220 boolean canSearch = MediaBrowserViewModelImpl.getSupportsSearch(browser); 221 mAppBarController.setSearchSupported(canSearch); 222 break; 223 224 case DISCONNECTING: 225 case REJECTED: 226 case SUSPENDED: 227 clearMediaSource(); 228 break; 229 } 230 231 mViewModel.saveBrowsedMediaSource(newBrowsingState.mMediaSource); 232 } 233 234 MediaActivityController(Callbacks callbacks, MediaItemsRepository mediaItemsRepo, CarPackageManager carPackageManager, ViewGroup container)235 MediaActivityController(Callbacks callbacks, MediaItemsRepository mediaItemsRepo, 236 CarPackageManager carPackageManager, ViewGroup container) { 237 super(callbacks.getActivity(), carPackageManager, container, R.layout.fragment_browse); 238 239 FragmentActivity activity = callbacks.getActivity(); 240 mCallbacks = callbacks; 241 mMediaItemsRepository = mediaItemsRepo; 242 mViewModel = ViewModelProviders.of(activity).get(MediaActivity.ViewModel.class); 243 mSearchStack = mViewModel.getSearchStack(); 244 mBrowseStack = mViewModel.getBrowseStack(); 245 mBrowseArea = mContent.requireViewById(R.id.browse_content_area); 246 mFpv = activity.requireViewById(R.id.fpv); 247 248 MediaItemsLiveData rootMediaItems = mediaItemsRepo.getRootMediaItems(); 249 mRootLoadingController = BrowseViewController.newRootController( 250 mBrowseCallbacks, mBrowseArea, rootMediaItems); 251 mRootLoadingController.getContent().setAlpha(1f); 252 253 mSearchResultsController = BrowseViewController.newSearchResultsController( 254 mBrowseCallbacks, mBrowseArea, mMediaItemsRepository.getSearchMediaItems()); 255 256 boolean showingSearch = mViewModel.isShowingSearchResults(); 257 ViewUtils.setVisible(mSearchResultsController.getContent(), showingSearch); 258 if (showingSearch) { 259 mSearchResultsController.getContent().setAlpha(1f); 260 } 261 262 mAppBarController.setListener(mAppBarListener); 263 mAppBarController.setSearchQuery(mViewModel.getSearchQuery()); 264 if (mAppBarController.getSearchCapabilities().canShowSearchResultsView()) { 265 // TODO(b/180441965) eliminate the need to create a different view and use 266 // mSearchResultsController.getContent() instead. 267 RecyclerView toolbarSearchResultsView = new RecyclerView(activity); 268 mSearchResultsController.shareBrowseAdapterWith(toolbarSearchResultsView); 269 270 ViewGroup.LayoutParams params = new ViewGroup.LayoutParams( 271 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); 272 toolbarSearchResultsView.setLayoutParams(params); 273 toolbarSearchResultsView.setLayoutManager(new LinearLayoutManager(activity)); 274 toolbarSearchResultsView.setBackground( 275 activity.getDrawable(R.drawable.car_ui_ime_wide_screen_background)); 276 277 mAppBarController.setSearchConfig(SearchConfig.builder() 278 .setSearchResultsView(toolbarSearchResultsView) 279 .build()); 280 } 281 282 updateAppBar(); 283 284 // Observe forever ensures the caches are destroyed even while the activity isn't resumed. 285 mediaItemsRepo.getBrowsingState().observeForever(mMediaBrowsingObserver); 286 287 mViewModel.getBrowsedMediaSource().observeForever(future -> { 288 onMediaSourceChanged(future.isLoading() ? null : future.getData()); 289 }); 290 291 rootMediaItems.observe(activity, this::onRootMediaItemsUpdate); 292 mViewModel.getMiniControlsVisible().observe(activity, this::onPlaybackControlsChanged); 293 } 294 onDestroy()295 void onDestroy() { 296 mMediaItemsRepository.getBrowsingState().removeObserver(mMediaBrowsingObserver); 297 } 298 299 private AppBarController.AppBarListener mAppBarListener = new BasicAppBarListener() { 300 @Override 301 public void onTabSelected(MediaItemMetadata item) { 302 if (mAcceptTabSelection && (item != null) && (item != mViewModel.getSelectedTab())) { 303 clearStack(mBrowseStack); 304 mBrowseStack.push(item); 305 showCurrentNode(true); 306 } 307 } 308 309 @Override 310 public void onSearchSelection() { 311 if (mViewModel.isSearching()) { 312 reopenSearch(); 313 } else { 314 showSearchMode(true); 315 updateAppBar(); 316 } 317 } 318 319 @Override 320 public void onSearch(String query) { 321 if (Log.isLoggable(TAG, Log.DEBUG)) { 322 Log.d(TAG, "onSearch: " + query); 323 } 324 mViewModel.setSearchQuery(query); 325 updateSearchQuery(query); 326 } 327 }; 328 329 private final BrowseViewController.Callbacks mBrowseCallbacks = 330 new BrowseViewController.Callbacks() { 331 @Override 332 public void onPlayableItemClicked(@NonNull MediaItemMetadata item) { 333 hideKeyboard(); 334 mCallbacks.onPlayableItemClicked(item); 335 } 336 337 @Override 338 public void onBrowsableItemClicked(@NonNull MediaItemMetadata item) { 339 hideKeyboard(); 340 navigateInto(item); 341 } 342 343 @Override 344 public void onChildrenNodesRemoved(@NonNull BrowseViewController controller, 345 @NonNull Collection<MediaItemMetadata> removedNodes) { 346 347 if (mBrowseStack.contains(controller.getParentItem())) { 348 for (MediaItemMetadata node : removedNodes) { 349 int indexOfNode = mBrowseStack.indexOf(node); 350 if (indexOfNode >= 0) { 351 clearStack(mBrowseStack.subList(indexOfNode, mBrowseStack.size())); 352 if (!mViewModel.isShowingSearchResults()) { 353 showCurrentNode(true); 354 updateAppBar(); 355 } 356 break; // The stack contains at most one of the removed nodes. 357 } 358 } 359 } 360 } 361 362 @Override 363 public FragmentActivity getActivity() { 364 return mCallbacks.getActivity(); 365 } 366 }; 367 368 private final ViewAnimEndListener mViewAnimEndListener = view -> { 369 BrowseViewController toDestroy = mBrowseViewControllersToDestroy.remove(view); 370 if (toDestroy != null) { 371 toDestroy.destroy(); 372 } 373 }; 374 onBackPressed()375 boolean onBackPressed() { 376 boolean success = navigateBack(); 377 if (!success && mViewModel.isSearching()) { 378 showSearchMode(false); 379 updateAppBar(); 380 success = true; 381 } 382 if (success) { 383 // When the back button is pressed, if a CarUiRecyclerView shows up and it's in rotary 384 // mode, restore focus in the CarUiRecyclerView. 385 restoreFocusInCurrentNode(); 386 } 387 return success; 388 } 389 browseTreeHasChildren()390 boolean browseTreeHasChildren() { 391 return mBrowseTreeHasChildren; 392 } 393 navigateInto(@onNull MediaItemMetadata item)394 private void navigateInto(@NonNull MediaItemMetadata item) { 395 showSearchResults(false); 396 397 // Hide the current node (parent) 398 showCurrentNode(false); 399 400 // Make item the current node 401 getStack().push(item); 402 403 // Show the current node (item) 404 showCurrentNode(true); 405 406 updateAppBar(); 407 } 408 getControllerForItem(@onNull MediaItemMetadata item)409 private BrowseViewController getControllerForItem(@NonNull MediaItemMetadata item) { 410 BrowseViewController controller = mBrowseViewControllersByNode.get(item); 411 if (controller == null) { 412 controller = BrowseViewController.newBrowseController(mBrowseCallbacks, mBrowseArea, 413 item, mMediaItemsRepository.getMediaChildren(item.getId()), mRootBrowsableHint, 414 mRootPlayableHint); 415 416 if (mCarUiInsets != null) { 417 controller.onCarUiInsetsChanged(mCarUiInsets); 418 } 419 controller.onPlaybackControlsChanged(mPlaybackControlsVisible); 420 421 mBrowseViewControllersByNode.put(item, controller); 422 } 423 return controller; 424 } 425 showCurrentNode(boolean show)426 private void showCurrentNode(boolean show) { 427 MediaItemMetadata currentNode = getCurrentMediaItem(); 428 if (currentNode == null) { 429 return; 430 } 431 // Only create a controller to show it. 432 BrowseViewController controller = show ? getControllerForItem(currentNode) : 433 mBrowseViewControllersByNode.get(currentNode); 434 435 if (controller != null) { 436 showHideContentAnimated(show, controller.getContent(), mViewAnimEndListener); 437 } 438 } 439 440 // If the current node has a CarUiRecyclerView and it's in rotary mode, restore focus in it. restoreFocusInCurrentNode()441 void restoreFocusInCurrentNode() { 442 MediaItemMetadata currentNode = getCurrentMediaItem(); 443 if (currentNode == null) { 444 return; 445 } 446 BrowseViewController controller = getControllerForItem(currentNode); 447 if (controller == null) { 448 return; 449 } 450 CarUiRecyclerView carUiRecyclerView = 451 controller.getContent().findViewById(R.id.browse_list); 452 if (carUiRecyclerView != null && carUiRecyclerView instanceof LazyLayoutView 453 && !carUiRecyclerView.getView().hasFocus() 454 && !carUiRecyclerView.getView().isInTouchMode()) { 455 // Park the focus on the FocusParkingView to ensure that it can restore focus inside 456 // the LazyLayoutView successfully later. 457 mFpv.performAccessibilityAction(ACTION_FOCUS, null); 458 459 LazyLayoutView lazyLayoutView = (LazyLayoutView) carUiRecyclerView; 460 com.android.car.ui.utils.ViewUtils.initFocus(lazyLayoutView); 461 } 462 } 463 showHideContentAnimated(boolean show, @NonNull View content, @Nullable ViewAnimEndListener listener)464 private void showHideContentAnimated(boolean show, @NonNull View content, 465 @Nullable ViewAnimEndListener listener) { 466 CarUiRecyclerView carUiRecyclerView = content.findViewById(R.id.browse_list); 467 if (carUiRecyclerView != null && carUiRecyclerView instanceof LazyLayoutView 468 && !carUiRecyclerView.getView().isInTouchMode()) { 469 // If a CarUiRecyclerView is about to hide and it has focus, park the focus on the 470 // FocusParkingView before hiding the CarUiRecyclerView. Otherwise hiding the focused 471 // view will cause the Android framework to move focus to another view, causing visual 472 // jank. 473 if (!show && carUiRecyclerView.getView().hasFocus()) { 474 mFpv.performAccessibilityAction(ACTION_FOCUS, null); 475 } 476 // If a new CarUiRecyclerView is about to show and there is no view focused or the 477 // FocusParkingView is focused, restore focus in the new CarUiRecyclerView. 478 if (show) { 479 View focusedView = carUiRecyclerView.getView().getRootView().findFocus(); 480 if (focusedView == null || focusedView instanceof FocusParkingView) { 481 LazyLayoutView lazyLayoutView = (LazyLayoutView) carUiRecyclerView; 482 com.android.car.ui.utils.ViewUtils.initFocus(lazyLayoutView); 483 } 484 } 485 } 486 487 showHideViewAnimated(show, content, mFadeDuration, listener); 488 } 489 490 491 showSearchResults(boolean show)492 private void showSearchResults(boolean show) { 493 if (mViewModel.isShowingSearchResults() != show) { 494 mViewModel.setShowingSearchResults(show); 495 showHideContentAnimated(show, mSearchResultsController.getContent(), null); 496 } 497 } 498 showSearchMode(boolean show)499 private void showSearchMode(boolean show) { 500 if (mViewModel.isSearching() != show) { 501 if (show) { 502 showCurrentNode(false); 503 } 504 505 mViewModel.setSearching(show); 506 showSearchResults(show); 507 508 if (!show) { 509 showCurrentNode(true); 510 } 511 } 512 } 513 514 /** 515 * @return the current item being displayed 516 */ 517 @Nullable getCurrentMediaItem()518 private MediaItemMetadata getCurrentMediaItem() { 519 Stack<MediaItemMetadata> stack = getStack(); 520 return stack.isEmpty() ? null : stack.lastElement(); 521 } 522 523 @Override onCarUiInsetsChanged(@onNull Insets insets)524 public void onCarUiInsetsChanged(@NonNull Insets insets) { 525 mCarUiInsets = insets; 526 for (BrowseViewController controller : mBrowseViewControllersByNode.values()) { 527 controller.onCarUiInsetsChanged(mCarUiInsets); 528 } 529 mRootLoadingController.onCarUiInsetsChanged(mCarUiInsets); 530 mSearchResultsController.onCarUiInsetsChanged(mCarUiInsets); 531 } 532 onPlaybackControlsChanged(boolean visible)533 void onPlaybackControlsChanged(boolean visible) { 534 mPlaybackControlsVisible = visible; 535 for (BrowseViewController controller : mBrowseViewControllersByNode.values()) { 536 controller.onPlaybackControlsChanged(mPlaybackControlsVisible); 537 } 538 mRootLoadingController.onPlaybackControlsChanged(mPlaybackControlsVisible); 539 mSearchResultsController.onPlaybackControlsChanged(mPlaybackControlsVisible); 540 } 541 hideKeyboard()542 private void hideKeyboard() { 543 InputMethodManager in = 544 (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); 545 in.hideSoftInputFromWindow(mContent.getWindowToken(), 0); 546 } 547 hideAndDestroyControllerForItem(@ullable MediaItemMetadata item)548 private void hideAndDestroyControllerForItem(@Nullable MediaItemMetadata item) { 549 if (item == null) { 550 return; 551 } 552 BrowseViewController controller = mBrowseViewControllersByNode.get(item); 553 if (controller == null) { 554 return; 555 } 556 557 if (controller.getContent().getVisibility() == View.VISIBLE) { 558 View view = controller.getContent(); 559 mBrowseViewControllersToDestroy.put(view, controller); 560 showHideContentAnimated(false, view, mViewAnimEndListener); 561 } else { 562 controller.destroy(); 563 } 564 mBrowseViewControllersByNode.remove(item); 565 } 566 567 /** 568 * Clears the given stack (or a portion of a stack) and destroys the old controllers (after 569 * their view is hidden). 570 */ clearStack(List<MediaItemMetadata> stack)571 private void clearStack(List<MediaItemMetadata> stack) { 572 for (MediaItemMetadata item : stack) { 573 hideAndDestroyControllerForItem(item); 574 } 575 stack.clear(); 576 } 577 578 /** 579 * Updates the tabs displayed on the app bar, based on the top level items on the browse tree. 580 * If there is at least one browsable item, we show the browse content of that node. If there 581 * are only playable items, then we show those items. If there are not items at all, we show the 582 * empty message. If we receive null, we show the error message. 583 * 584 * @param items top level items, null if the items are still being loaded, or empty list if 585 * items couldn't be loaded. 586 */ updateTabs(@ullable List<MediaItemMetadata> items)587 private void updateTabs(@Nullable List<MediaItemMetadata> items) { 588 if (Objects.equals(mTopItems, items)) { 589 // When coming back to the app, the live data sends an update even if the list hasn't 590 // changed. Updating the tabs then recreates the browse view, which produces jank 591 // (b/131830876), and also resets the navigation to the top of the first tab... 592 return; 593 } 594 mTopItems = items; 595 596 if (mTopItems == null || mTopItems.isEmpty()) { 597 mAppBarController.setItems(null); 598 mAppBarController.setActiveItem(null); 599 if (items != null) { 600 // Only do this when not loading the tabs or we loose the saved one. 601 clearStack(mBrowseStack); 602 } 603 updateAppBar(); 604 return; 605 } 606 607 MediaItemMetadata oldTab = mViewModel.getSelectedTab(); 608 MediaItemMetadata newTab = items.contains(oldTab) ? oldTab : items.get(0); 609 610 try { 611 mAcceptTabSelection = false; 612 mAppBarController.setItems(mTopItems.size() == 1 ? null : mTopItems); 613 mAppBarController.setActiveItem(newTab); 614 615 if (oldTab != newTab) { 616 // Tabs belong to the browse stack. 617 clearStack(mBrowseStack); 618 mBrowseStack.push(newTab); 619 } 620 621 if (!mViewModel.isShowingSearchResults()) { 622 // Needed when coming back to an app after a config change or from another app, 623 // or when the tab actually changes. 624 showCurrentNode(true); 625 } 626 } finally { 627 mAcceptTabSelection = true; 628 } 629 updateAppBar(); 630 } 631 getAppBarTitle()632 private CharSequence getAppBarTitle() { 633 boolean isStacked = !isAtTopStack(); 634 635 final CharSequence title; 636 if (isStacked) { 637 // If not at top level, show the current item as title 638 MediaItemMetadata item = getCurrentMediaItem(); 639 title = item != null ? item.getTitle() : ""; 640 } else if (mTopItems == null) { 641 // If still loading the tabs, force to show an empty bar. 642 title = ""; 643 } else if (mTopItems.size() == 1) { 644 // If we finished loading tabs and there is only one, use that as title. 645 title = mTopItems.get(0).getTitle(); 646 } else { 647 // Otherwise (no tabs or more than 1 tabs), show the current media source title. 648 MediaSource mediaSource = mMediaSourceVM.getPrimaryMediaSource().getValue(); 649 title = getAppBarDefaultTitle(mediaSource); 650 } 651 652 return title; 653 } 654 655 /** 656 * Update elements of the appbar that change depending on where we are in the browse. 657 */ updateAppBar()658 private void updateAppBar() { 659 boolean isSearching = mViewModel.isSearching(); 660 boolean isStacked = !isAtTopStack(); 661 if (Log.isLoggable(TAG, Log.DEBUG)) { 662 Log.d(TAG, "App bar is in stacked state: " + isStacked); 663 } 664 665 mAppBarController.setSearchMode(isSearching ? SearchMode.SEARCH : SearchMode.DISABLED); 666 mAppBarController.setNavButtonMode(isStacked || isSearching 667 ? NavButtonMode.BACK : NavButtonMode.DISABLED); 668 mAppBarController.setTitle(!isSearching ? getAppBarTitle() : null); 669 mAppBarController.showSearchIfSupported(!isSearching || isStacked); 670 } 671 onRootMediaItemsUpdate(FutureData<List<MediaItemMetadata>> data)672 private void onRootMediaItemsUpdate(FutureData<List<MediaItemMetadata>> data) { 673 if (data.isLoading()) { 674 if (Log.isLoggable(TAG, Log.INFO)) { 675 Log.i(TAG, "Loading browse tree..."); 676 } 677 mBrowseTreeHasChildren = false; 678 updateTabs(null); 679 return; 680 } 681 682 List<MediaItemMetadata> items = 683 MediaBrowserViewModelImpl.filterItems(/*forRoot*/ true, data.getData()); 684 685 boolean browseTreeHasChildren = items != null && !items.isEmpty(); 686 if (Log.isLoggable(TAG, Log.INFO)) { 687 Log.i(TAG, "Browse tree loaded, status (has children or not) changed: " 688 + mBrowseTreeHasChildren + " -> " + browseTreeHasChildren); 689 } 690 mBrowseTreeHasChildren = browseTreeHasChildren; 691 mCallbacks.onRootLoaded(); 692 updateTabs(items != null ? items : new ArrayList<>()); 693 } 694 695 } 696