1 /* 2 * Copyright (C) 2023 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.chassis.car.ui.plugin.toolbar; 18 19 import static android.view.View.GONE; 20 import static android.view.View.VISIBLE; 21 22 import static com.android.car.ui.utils.CarUiUtils.findViewByRefId; 23 import static com.android.car.ui.utils.CarUiUtils.requireViewByRefId; 24 25 import android.app.Activity; 26 import android.app.AlertDialog; 27 import android.content.Context; 28 import android.graphics.drawable.Drawable; 29 import android.os.Bundle; 30 import android.text.TextUtils; 31 import android.util.Log; 32 import android.view.View; 33 import android.view.ViewGroup; 34 import android.widget.FrameLayout; 35 import android.widget.ImageView; 36 import android.widget.TextView; 37 38 import androidx.annotation.DrawableRes; 39 import androidx.annotation.NonNull; 40 import androidx.annotation.Nullable; 41 import androidx.annotation.StringRes; 42 import androidx.annotation.XmlRes; 43 import androidx.core.content.ContextCompat; 44 45 import com.android.car.ui.AlertDialogBuilder; 46 import com.android.car.ui.CarUiText; 47 import com.android.car.ui.imewidescreen.CarUiImeSearchListItem; 48 import com.android.car.ui.plugin.oemapis.toolbar.ImeSearchInterfaceOEMV2; 49 import com.android.car.ui.recyclerview.CarUiContentListItem; 50 import com.android.car.ui.recyclerview.CarUiListItem; 51 import com.android.car.ui.recyclerview.CarUiListItemAdapter; 52 import com.android.car.ui.toolbar.DeprecatedTabWrapper; 53 import com.android.car.ui.toolbar.MenuItem; 54 import com.android.car.ui.toolbar.MenuItemXmlParserUtil; 55 import com.android.car.ui.toolbar.NavButtonMode; 56 import com.android.car.ui.toolbar.ProgressBarController; 57 import com.android.car.ui.toolbar.SearchCapabilities; 58 import com.android.car.ui.toolbar.SearchConfig; 59 import com.android.car.ui.toolbar.SearchMode; 60 import com.android.car.ui.toolbar.SearchView; 61 import com.android.car.ui.toolbar.SearchWidescreenController; 62 import com.android.car.ui.toolbar.Tab; 63 import com.android.car.ui.toolbar.Toolbar; 64 import com.android.car.ui.toolbar.ToolbarController; 65 import com.android.car.ui.utils.CarUiUtils; 66 import com.android.car.ui.widget.CarUiTextView; 67 68 import com.chassis.car.ui.plugin.R; 69 70 import java.util.ArrayList; 71 import java.util.Collections; 72 import java.util.HashSet; 73 import java.util.List; 74 import java.util.Objects; 75 import java.util.Set; 76 import java.util.concurrent.Executor; 77 import java.util.concurrent.atomic.AtomicInteger; 78 import java.util.function.BiConsumer; 79 import java.util.function.Consumer; 80 import java.util.function.Supplier; 81 82 /** 83 * The implementation of {@link ToolbarController}. This class takes a ViewGroup, and looks in the 84 * ViewGroup to find all the toolbar-related views to control. 85 */ 86 public class ToolbarControllerImpl implements ToolbarController { 87 private static final String TAG = "CarUiPortraitToolbarController"; 88 89 @Nullable 90 private View mBackground; 91 private ImageView mNavIcon; 92 private ViewGroup mNavIconContainer; 93 private ViewGroup mTitleLogoContainer; 94 private ViewGroup mTitleContainer; 95 private Runnable mOnLogoClickListener; 96 private CarUiTextView mTitle; 97 @NonNull 98 private CarUiText mTitleText = new CarUiText.Builder("").build(); 99 private CarUiTextView mSubtitle; 100 @NonNull 101 private CarUiText mSubtitleText = new CarUiText.Builder("").build(); 102 private ImageView mLogo; 103 private TabLayout mTabLayout; 104 private ViewGroup mMenuItemsContainer; 105 private SearchConfig.SearchConfigBuilder mSearchConfigBuilder; 106 private final FrameLayout mSearchViewContainer; 107 private SearchView mSearchView; 108 109 // Cached values that we will send to views when they are inflated 110 private CharSequence mSearchHint; 111 private Drawable mSearchIcon; 112 private String mSearchQuery; 113 private final Context mContext; 114 private final Context mPluginContext; 115 private final Set<Consumer<String>> mOnSearchListeners = new HashSet<>(); 116 private final Set<Runnable> mOnSearchCompletedListeners = new HashSet<>(); 117 private final Set<Toolbar.OnSearchListener> mDeprecatedSearchListeners = new HashSet<>(); 118 private final Set<Toolbar.OnSearchCompletedListener> mDeprecatedSearchCompletedListeners = 119 new HashSet<>(); 120 121 private final Set<Toolbar.OnBackListener> mDeprecatedBackListeners = new HashSet<>(); 122 private final Set<Supplier<Boolean>> mBackListeners = new HashSet<>(); 123 private final Set<Toolbar.OnTabSelectedListener> mOnTabSelectedListeners = new HashSet<>(); 124 125 private final MenuItem mOverflowButton; 126 private boolean mShowTabsInSubpage = false; 127 private boolean mHasLogo = false; 128 private boolean mShowMenuItemsWhileSearching; 129 private Toolbar.State mState = Toolbar.State.HOME; 130 private boolean mStateSet = false; 131 private NavButtonMode mNavButtonMode = NavButtonMode.DISABLED; 132 private SearchMode mSearchMode = SearchMode.DISABLED; 133 @NonNull 134 private List<MenuItem> mMenuItems = Collections.emptyList(); 135 private List<MenuItem> mOverflowItems = new ArrayList<>(); 136 private final List<CarUiListItem> mUiOverflowItems = new ArrayList<>(); 137 private final CarUiListItemAdapter mOverflowAdapter; 138 private final List<MenuItemRenderer> mMenuItemRenderers = new ArrayList<>(); 139 private final List<DeprecatedTabWrapper> mDeprecatedTabs = new ArrayList<>(); 140 private View[] mMenuItemViews; 141 private int mMenuItemsXmlId = 0; 142 private AlertDialog mOverflowDialog; 143 private final boolean mShowLogo; 144 private SearchConfig mSearchConfigForWidescreen; 145 private final ProgressBarController mProgressBar; 146 private final View mNavIconSpacer; 147 private final View mLogoSpacer; 148 private final View mTitleSpacer; 149 private final MenuItem.Listener mOverflowItemListener = item -> { 150 updateOverflowDialog(item); 151 update(); 152 }; 153 private Consumer<TextView> mSearchTextViewConsumer = null; 154 private BiConsumer<String, Bundle> mOnPrivateImeCommandListener = null; 155 156 ToolbarControllerImpl(View view, Context pluginContext)157 public ToolbarControllerImpl(View view, Context pluginContext) { 158 mContext = view.getContext(); 159 mPluginContext = pluginContext; 160 mOverflowButton = MenuItem.builder(mPluginContext) 161 .setIcon(R.drawable.car_ui_icon_overflow_menu) 162 .setTitle(R.string.car_ui_toolbar_menu_item_overflow_title) 163 .setOnClickListener(v -> { 164 if (mOverflowDialog == null) { 165 if (Log.isLoggable(TAG, Log.ERROR)) { 166 Log.e(TAG, "Overflow dialog was null when trying to show it!"); 167 } 168 } else { 169 mOverflowDialog.show(); 170 } 171 }) 172 .build(); 173 174 mShowLogo = mPluginContext.getResources().getBoolean( 175 R.bool.car_ui_toolbar_show_logo); 176 mSearchHint = mPluginContext.getString(R.string.car_ui_toolbar_default_search_hint); 177 178 mBackground = findViewByRefId(view, R.id.car_ui_portrait_toolbar_background); 179 mLogoSpacer = requireViewByRefId(view, R.id.car_ui_portrait_toolbar_logo_spacer); 180 mLogo = requireViewByRefId(view, R.id.car_ui_portrait_toolbar_logo); 181 mOnLogoClickListener = null; 182 mTitleLogoContainer = requireViewByRefId(view, 183 R.id.car_ui_portrait_toolbar_title_logo_container); 184 mTitleSpacer = requireViewByRefId(view, R.id.car_ui_portrait_toolbar_title_spacer); 185 mTitleContainer = requireViewByRefId(view, R.id.car_ui_portrait_toolbar_title_container); 186 mTitle = requireViewByRefId(view, R.id.car_ui_portrait_toolbar_title); 187 mSubtitle = requireViewByRefId(view, R.id.car_ui_portrait_toolbar_subtitle); 188 mNavIconSpacer = requireViewByRefId(view, R.id.car_ui_portrait_toolbar_nav_icon_spacer); 189 mNavIconContainer = requireViewByRefId(view, 190 R.id.car_ui_portrait_toolbar_nav_icon_container); 191 mNavIcon = requireViewByRefId(view, R.id.car_ui_portrait_toolbar_nav_icon); 192 mSearchViewContainer = requireViewByRefId(view, R.id.car_ui_toolbar_search_view_container); 193 mMenuItemsContainer = requireViewByRefId(view, R.id.car_ui_toolbar_menu_items_container); 194 mProgressBar = new ProgressBarControllerImpl( 195 requireViewByRefId(view, R.id.car_ui_toolbar_progress_bar)); 196 mTabLayout = requireViewByRefId(view, R.id.car_ui_portrait_toolbar_tabs); 197 mSearchConfigBuilder = SearchConfig.builder(); 198 199 setBackgroundShown(true); 200 201 mOverflowAdapter = new CarUiListItemAdapter(mUiOverflowItems); 202 update(); 203 } 204 getContext()205 private Context getContext() { 206 return mContext; 207 } 208 getDrawable(@rawableRes int resId)209 private Drawable getDrawable(@DrawableRes int resId) { 210 if (resId == 0) { 211 return null; 212 } else { 213 return ContextCompat.getDrawable(getContext(), resId); 214 } 215 } 216 217 /** 218 * Sets the title of the toolbar to a string resource. 219 * 220 * <p>The title may not always be shown, for example with one row layout with tabs. 221 */ 222 @Override setTitle(@tringRes int title)223 public void setTitle(@StringRes int title) { 224 setTitle(title == 0 ? null : getContext().getString(title)); 225 } 226 227 /** 228 * Sets the title of the toolbar to a CharSequence. 229 * 230 * <p>The title may not always be shown, for example with one row layout with tabs. 231 */ 232 @Override setTitle(CharSequence title)233 public void setTitle(CharSequence title) { 234 mTitleText = title == null ? new CarUiText.Builder("").build() : new CarUiText.Builder( 235 title).build(); 236 mTitle.setText(mTitleText); 237 update(); 238 } 239 240 @Override setTitle(CarUiText title)241 public void setTitle(CarUiText title) { 242 mTitleText = title; 243 mTitle.setText(mTitleText); 244 update(); 245 } 246 247 @Override getTitle()248 public CharSequence getTitle() { 249 return mTitleText.getPreferredText(); 250 } 251 252 /** 253 * Sets the subtitle of the toolbar to a string resource. 254 * 255 * <p>The title may not always be shown, for example with one row layout with tabs. 256 */ 257 @Override setSubtitle(@tringRes int subTitle)258 public void setSubtitle(@StringRes int subTitle) { 259 setSubtitle(subTitle == 0 ? null : getContext().getString(subTitle)); 260 } 261 262 /** 263 * Sets the subtitle of the toolbar to a CharSequence. 264 * 265 * <p>The title may not always be shown, for example with one row layout with tabs. 266 */ 267 @Override setSubtitle(CharSequence subTitle)268 public void setSubtitle(CharSequence subTitle) { 269 mSubtitleText = subTitle == null ? new CarUiText.Builder("").build() 270 : new CarUiText.Builder(subTitle).build(); 271 mSubtitle.setText(mSubtitleText); 272 update(); 273 } 274 275 @Override setSubtitle(CarUiText text)276 public void setSubtitle(CarUiText text) { 277 mSubtitleText = text; 278 mSubtitle.setText(mSubtitleText); 279 update(); 280 } 281 282 @Override getSubtitle()283 public CharSequence getSubtitle() { 284 return mSubtitleText.getPreferredText(); 285 } 286 287 @Override setTabs(@ullable List<Tab> tabs)288 public void setTabs(@Nullable List<Tab> tabs) { 289 setTabs(tabs, 0); 290 } 291 292 @Override setTabs(@ullable List<Tab> tabs, int selectedTab)293 public void setTabs(@Nullable List<Tab> tabs, int selectedTab) { 294 mDeprecatedTabs.clear(); 295 setTabsInternal(tabs, selectedTab); 296 } 297 298 @Override getTabs()299 public List<Tab> getTabs() { 300 return mTabLayout.getTabs(); 301 } 302 setTabsInternal(@ullable List<Tab> tabs, int selectedTab)303 private void setTabsInternal(@Nullable List<Tab> tabs, int selectedTab) { 304 if (tabs == null || tabs.isEmpty()) { 305 selectedTab = -1; 306 } else if (selectedTab < 0 || selectedTab >= tabs.size()) { 307 throw new IllegalArgumentException("Tab position is invalid: " + selectedTab); 308 } 309 mTabLayout.setTabs(tabs, selectedTab); 310 update(); 311 } 312 313 /** 314 * Gets the number of tabs in the toolbar. The tabs can be retrieved using 315 * {@link #getTab(int)}. 316 */ 317 @Override getTabCount()318 public int getTabCount() { 319 return mDeprecatedTabs.size(); 320 } 321 322 @Override getTabPosition(com.android.car.ui.toolbar.TabLayout.Tab tab)323 public int getTabPosition(com.android.car.ui.toolbar.TabLayout.Tab tab) { 324 for (int i = 0; i < mDeprecatedTabs.size(); i++) { 325 if (mDeprecatedTabs.get(i).getDeprecatedTab() == tab) { 326 return i; 327 } 328 } 329 return -1; 330 } 331 332 @Override addTab(com.android.car.ui.toolbar.TabLayout.Tab newTab)333 public void addTab(com.android.car.ui.toolbar.TabLayout.Tab newTab) { 334 mDeprecatedTabs.add(new DeprecatedTabWrapper(getContext(), newTab, 335 this::updateModernTabsFromDeprecatedOnes, (tab) -> { 336 for (Toolbar.OnTabSelectedListener listener : mOnTabSelectedListeners) { 337 listener.onTabSelected(newTab); 338 } 339 })); 340 updateModernTabsFromDeprecatedOnes(); 341 } 342 updateModernTabsFromDeprecatedOnes()343 private void updateModernTabsFromDeprecatedOnes() { 344 List<Tab> modernTabs = new ArrayList<>(); 345 346 for (DeprecatedTabWrapper tab : mDeprecatedTabs) { 347 modernTabs.add(tab.getModernTab()); 348 } 349 350 setTabsInternal(modernTabs, 0); 351 } 352 353 @Override clearAllTabs()354 public void clearAllTabs() { 355 mDeprecatedTabs.clear(); 356 setTabs(null); 357 } 358 359 @Override getTab(int position)360 public com.android.car.ui.toolbar.TabLayout.Tab getTab(int position) { 361 return mDeprecatedTabs.get(position).getDeprecatedTab(); 362 } 363 364 /** 365 * Selects a tab added to this toolbar. See {@link #addTab(TabLayout.Tab)}. 366 */ 367 @Override selectTab(int position)368 public void selectTab(int position) { 369 mTabLayout.selectTab(position); 370 } 371 372 @Override getSelectedTab()373 public int getSelectedTab() { 374 return mTabLayout.getSelectedTab(); 375 } 376 377 /** 378 * Sets whether or not tabs should also be shown in the SUBPAGE {@link Toolbar.State}. 379 */ 380 @Override setShowTabsInSubpage(boolean showTabs)381 public void setShowTabsInSubpage(boolean showTabs) { 382 if (showTabs != mShowTabsInSubpage) { 383 mShowTabsInSubpage = showTabs; 384 update(); 385 } 386 } 387 388 /** 389 * Gets whether or not tabs should also be shown in the SUBPAGE {@link Toolbar.State}. 390 */ 391 @Override getShowTabsInSubpage()392 public boolean getShowTabsInSubpage() { 393 return mShowTabsInSubpage; 394 } 395 396 /** 397 * Sets the logo to display in this toolbar. If navigation icon is being displayed, this logo 398 * will be displayed next to the title. 399 */ 400 @Override setLogo(@rawableRes int resId)401 public void setLogo(@DrawableRes int resId) { 402 asyncSetLogo(resId, Runnable::run); 403 } 404 asyncSetLogo(int resId, Executor bgExecutor)405 private void asyncSetLogo(int resId, Executor bgExecutor) { 406 if (!mShowLogo) { 407 // If no logo should be shown then we act as if we never received one. 408 return; 409 } 410 if (resId != 0) { 411 bgExecutor.execute(() -> { 412 // load resource on background thread. 413 Drawable drawable = getDrawable(resId); 414 mLogo.post(() -> { 415 // UI thread. 416 mLogo.setImageDrawable(drawable); 417 }); 418 }); 419 mHasLogo = true; 420 } else { 421 mHasLogo = false; 422 } 423 update(); 424 } 425 426 /** 427 * Sets the logo to display in this toolbar. If navigation icon is being displayed, this logo 428 * will be displayed next to the title. 429 */ 430 @Override setLogo(Drawable drawable)431 public void setLogo(Drawable drawable) { 432 if (!mShowLogo) { 433 // If no logo should be shown then we act as if we never received one. 434 return; 435 } 436 if (drawable != null) { 437 mLogo.setImageDrawable(drawable); 438 mHasLogo = true; 439 } else { 440 mHasLogo = false; 441 } 442 443 update(); 444 } 445 446 @Override setOnLogoClickListener(@ullable Runnable listener)447 public void setOnLogoClickListener(@Nullable Runnable listener) { 448 if (mOnLogoClickListener != listener) { 449 mOnLogoClickListener = listener; 450 update(); 451 } 452 } 453 454 /** 455 * Sets the hint for the search bar. 456 */ 457 @Override setSearchHint(@tringRes int resId)458 public void setSearchHint(@StringRes int resId) { 459 setSearchHint(getContext().getString(resId)); 460 } 461 462 /** 463 * Sets the hint for the search bar. 464 */ setSearchHint(CharSequence hint)465 public void setSearchHint(CharSequence hint) { 466 mSearchHint = hint; 467 if (mSearchView != null) { 468 mSearchView.setHint(mSearchHint); 469 } 470 } 471 472 /** 473 * Gets the search hint 474 */ 475 @Override getSearchHint()476 public CharSequence getSearchHint() { 477 return mSearchHint; 478 } 479 480 /** 481 * Sets the icon to display in the search box. 482 * 483 * <p>The icon will be lost on configuration change, make sure to set it in onCreate() or 484 * a similar place. 485 */ 486 @Override setSearchIcon(@rawableRes int resId)487 public void setSearchIcon(@DrawableRes int resId) { 488 setSearchIcon(getDrawable(resId)); 489 } 490 491 /** 492 * Sets the icon to display in the search box. 493 * 494 * <p>The icon will be lost on configuration change, make sure to set it in onCreate() or 495 * a similar place. 496 */ 497 @Override setSearchIcon(Drawable d)498 public void setSearchIcon(Drawable d) { 499 if (!Objects.equals(d, mSearchIcon)) { 500 mSearchIcon = d; 501 if (mSearchView != null) { 502 mSearchView.setIcon(mSearchIcon); 503 } 504 } 505 } 506 507 508 /** 509 * Sets the {@link Toolbar.NavButtonMode} 510 */ 511 @Override setNavButtonMode(Toolbar.NavButtonMode mode)512 public void setNavButtonMode(Toolbar.NavButtonMode mode) { 513 NavButtonMode modernMode; 514 switch (mode) { 515 case BACK: 516 modernMode = NavButtonMode.BACK; 517 break; 518 case DOWN: 519 modernMode = NavButtonMode.DOWN; 520 break; 521 case CLOSE: 522 modernMode = NavButtonMode.CLOSE; 523 break; 524 case DISABLED: 525 default: 526 modernMode = NavButtonMode.DISABLED; 527 break; 528 } 529 setNavButtonMode(modernMode); 530 } 531 532 @Override setNavButtonMode(NavButtonMode mode)533 public void setNavButtonMode(NavButtonMode mode) { 534 if (mode != mNavButtonMode) { 535 mNavButtonMode = mode; 536 update(); 537 } 538 } 539 540 /** 541 * Gets the {@link Toolbar.NavButtonMode} 542 */ 543 @Override getNavButtonMode()544 public NavButtonMode getNavButtonMode() { 545 return mNavButtonMode; 546 } 547 548 /** 549 * Show/hide the background. When hidden, the toolbar is completely transparent. 550 */ 551 @Override setBackgroundShown(boolean shown)552 public void setBackgroundShown(boolean shown) { 553 if (mBackground == null) { 554 return; 555 } 556 557 if (shown) { 558 mBackground.setBackground(getDrawable(R.drawable.car_ui_portrait_toolbar_background)); 559 } else { 560 mBackground.setBackground(null); 561 } 562 } 563 564 /** 565 * Returns true is the toolbar background is shown 566 */ 567 @Override getBackgroundShown()568 public boolean getBackgroundShown() { 569 if (mBackground == null) { 570 return true; 571 } 572 573 return mBackground.getBackground() != null; 574 } 575 setMenuItemsInternal(@ullable List<MenuItem> items)576 private void setMenuItemsInternal(@Nullable List<MenuItem> items) { 577 if (items == null) { 578 items = Collections.emptyList(); 579 } 580 581 List<MenuItem> visibleMenuItems = new ArrayList<>(); 582 List<MenuItem> overflowItems = new ArrayList<>(); 583 AtomicInteger loadedMenuItems = new AtomicInteger(0); 584 585 synchronized (this) { 586 if (items.equals(mMenuItems)) { 587 return; 588 } 589 590 for (MenuItem item : items) { 591 if (item.getDisplayBehavior() == MenuItem.DisplayBehavior.NEVER) { 592 overflowItems.add(item); 593 item.setListener(mOverflowItemListener); 594 } else { 595 visibleMenuItems.add(item); 596 } 597 } 598 599 // Copy the list so that if the list is modified and setMenuItems is called again, 600 // the equals() check will fail. Note that the MenuItems are not copied here. 601 mMenuItems = new ArrayList<>(items); 602 mOverflowItems = overflowItems; 603 mMenuItemRenderers.clear(); 604 mMenuItemsContainer.removeAllViews(); 605 606 if (!overflowItems.isEmpty()) { 607 visibleMenuItems.add(mOverflowButton); 608 createOverflowDialog(); 609 } 610 611 View[] menuItemViews = new View[visibleMenuItems.size()]; 612 mMenuItemViews = menuItemViews; 613 614 for (int i = 0; i < visibleMenuItems.size(); ++i) { 615 int index = i; 616 MenuItem item = visibleMenuItems.get(i); 617 MenuItemRenderer renderer = new MenuItemRenderer(item, mMenuItemsContainer); 618 mMenuItemRenderers.add(renderer); 619 renderer.createView(view -> { 620 synchronized (ToolbarControllerImpl.this) { 621 if (menuItemViews != mMenuItemViews) { 622 return; 623 } 624 625 menuItemViews[index] = view; 626 if (loadedMenuItems.addAndGet(1) == menuItemViews.length) { 627 for (View v : menuItemViews) { 628 mMenuItemsContainer.addView(v); 629 } 630 } 631 } 632 }); 633 } 634 } 635 636 update(); 637 } 638 639 /** 640 * Sets the {@link MenuItem Menuitems} to display. 641 */ 642 @Override setMenuItems(@ullable List<MenuItem> items)643 public void setMenuItems(@Nullable List<MenuItem> items) { 644 mMenuItemsXmlId = 0; 645 setMenuItemsInternal(items); 646 } 647 648 /** 649 * Sets the {@link MenuItem Menuitems} to display to a list defined in XML. 650 * 651 * <p>If this method is called twice with the same argument (and {@link #setMenuItems(List)} 652 * wasn't called), nothing will happen the second time, even if the MenuItems were changed. 653 * 654 * <p>The XML file must have one <MenuItems> tag, with a variable number of <MenuItem> 655 * child tags. See CarUiToolbarMenuItem in CarUi's attrs.xml for a list of available 656 * attributes. 657 * <p> 658 * Example: 659 * <pre> 660 * <MenuItems> 661 * <MenuItem 662 * app:title="Foo"/> 663 * <MenuItem 664 * app:title="Bar" 665 * app:icon="@drawable/ic_tracklist" 666 * app:onClick="xmlMenuItemClicked"/> 667 * <MenuItem 668 * app:title="Bar" 669 * app:checkable="true" 670 * app:uxRestrictions="FULLY_RESTRICTED" 671 * app:onClick="xmlMenuItemClicked"/> 672 * </MenuItems> 673 * </pre> 674 * 675 * @return The MenuItems that were loaded from XML. 676 * @see #setMenuItems(List) 677 */ 678 @Override setMenuItems(@mlRes int resId)679 public List<MenuItem> setMenuItems(@XmlRes int resId) { 680 if (mMenuItemsXmlId != 0 && mMenuItemsXmlId == resId) { 681 return mMenuItems; 682 } 683 684 mMenuItemsXmlId = resId; 685 List<MenuItem> menuItems = MenuItemXmlParserUtil.readMenuItemList(getContext(), resId); 686 setMenuItemsInternal(menuItems); 687 return menuItems; 688 } 689 690 /** 691 * Gets the {@link MenuItem MenuItems} currently displayed 692 */ 693 @Override 694 @NonNull getMenuItems()695 public List<MenuItem> getMenuItems() { 696 return Collections.unmodifiableList(mMenuItems); 697 } 698 699 /** 700 * Gets a {@link MenuItem} by id. 701 */ 702 @Override 703 @Nullable findMenuItemById(int id)704 public MenuItem findMenuItemById(int id) { 705 for (MenuItem item : mMenuItems) { 706 if (item.getId() == id) { 707 return item; 708 } 709 } 710 return null; 711 } 712 713 /** 714 * Gets a {@link MenuItem} by id. Will throw an IllegalArgumentException if not found. 715 */ 716 @Override 717 @NonNull requireMenuItemById(int id)718 public MenuItem requireMenuItemById(int id) { 719 MenuItem result = findMenuItemById(id); 720 721 if (result == null) { 722 throw new IllegalArgumentException("ID does not reference a MenuItem on this Toolbar"); 723 } 724 725 return result; 726 } 727 countVisibleOverflowItems()728 private int countVisibleOverflowItems() { 729 int numVisibleItems = 0; 730 for (MenuItem item : mOverflowItems) { 731 if (item.isVisible()) { 732 numVisibleItems++; 733 } 734 } 735 return numVisibleItems; 736 } 737 createOverflowDialog()738 private void createOverflowDialog() { 739 // Need to check if overflow dialog is showing before the new AlertDialog is created 740 // because it will return false when checked after 741 boolean isShowing = mOverflowDialog == null ? false : mOverflowDialog.isShowing(); 742 743 mUiOverflowItems.clear(); 744 for (MenuItem menuItem : mOverflowItems) { 745 if (menuItem.isVisible()) { 746 mUiOverflowItems.add(toCarUiContentListItem(menuItem)); 747 } 748 } 749 mOverflowDialog = new AlertDialogBuilder(getContext()) 750 .setAdapter(mOverflowAdapter) 751 .create(); 752 753 // When show() is called on a dialog, it is created from scratch. This means the underlying 754 // list of the dialog is instantiated and the corresponding adapter is set on it. So, any 755 // changes to the data of the dialog's list's adapter prior to the call to show() will be 756 // be shown on screen. Previously, if the dialog was being shown and the data of the adapter 757 // was changed (i.e., setMenuItems -> setMenuItemsInternal -> createOverflowDialog), the 758 // data of the adapter would change without show() being called, causing the updated data to 759 // not be reflected on screen. So, call notifyDataSetChanged if the dialog is being shown. 760 if (isShowing) { 761 mOverflowAdapter.notifyDataSetChanged(); 762 } 763 } 764 updateOverflowDialog(MenuItem changedItem)765 private void updateOverflowDialog(MenuItem changedItem) { 766 int itemIndex = mOverflowItems.indexOf(changedItem); 767 if (itemIndex >= 0) { 768 mUiOverflowItems.set(itemIndex, toCarUiContentListItem(changedItem)); 769 mOverflowAdapter.notifyItemChanged(itemIndex); 770 } else { 771 createOverflowDialog(); 772 } 773 } 774 toCarUiContentListItem(MenuItem menuItem)775 private CarUiContentListItem toCarUiContentListItem(MenuItem menuItem) { 776 CarUiContentListItem carUiItem; 777 if (menuItem.isCheckable()) { 778 carUiItem = new CarUiContentListItem(CarUiContentListItem.Action.SWITCH); 779 } else { 780 carUiItem = new CarUiContentListItem(CarUiContentListItem.Action.NONE); 781 } 782 carUiItem.setIcon(menuItem.getIcon()); 783 carUiItem.setActivated(menuItem.isActivated()); 784 carUiItem.setChecked(menuItem.isChecked()); 785 carUiItem.setEnabled(menuItem.isEnabled()); 786 carUiItem.setTitle(menuItem.getTitle()); 787 carUiItem.setOnItemClickedListener(item -> { 788 menuItem.performClick(); 789 mOverflowDialog.hide(); 790 }); 791 return carUiItem; 792 } 793 794 /** 795 * Set whether or not to show the {@link MenuItem MenuItems} while searching. Default false. 796 * Even if this is set to true, the {@link MenuItem} created by 797 * {@link MenuItem.Builder#setToSearch()} will still be hidden. 798 */ 799 @Override setShowMenuItemsWhileSearching(boolean showMenuItems)800 public void setShowMenuItemsWhileSearching(boolean showMenuItems) { 801 mShowMenuItemsWhileSearching = showMenuItems; 802 update(); 803 } 804 805 /** 806 * Returns if {@link MenuItem MenuItems} are shown while searching 807 */ 808 @Override getShowMenuItemsWhileSearching()809 public boolean getShowMenuItemsWhileSearching() { 810 return mShowMenuItemsWhileSearching; 811 } 812 813 /** 814 * Sets the search query. 815 */ 816 @Override setSearchQuery(String query)817 public void setSearchQuery(String query) { 818 if (mSearchView != null) { 819 mSearchView.setSearchQuery(query); 820 } else { 821 mSearchQuery = query; 822 for (Toolbar.OnSearchListener listener : mDeprecatedSearchListeners) { 823 listener.onSearch(query); 824 } 825 for (Consumer<String> listener : mOnSearchListeners) { 826 listener.accept(query); 827 } 828 } 829 } 830 831 /** 832 * Sets the state of the toolbar. This will show/hide the appropriate elements of the toolbar 833 * for the desired state. 834 */ 835 @Override setState(Toolbar.State state)836 public void setState(Toolbar.State state) { 837 if (mState != state || !mStateSet) { 838 mState = state; 839 mStateSet = true; 840 update(); 841 } 842 } 843 844 @Override setSearchMode(SearchMode mode)845 public void setSearchMode(SearchMode mode) { 846 if (mStateSet) { 847 throw new IllegalStateException("Cannot set search mode when using setState()"); 848 } 849 if (mSearchMode != mode) { 850 mSearchMode = mode; 851 update(); 852 } 853 } 854 855 @Override getSearchMode()856 public SearchMode getSearchMode() { 857 return mSearchMode; 858 } 859 update()860 private void update() { 861 // Start by removing mState/mStateSet from the equation by incorporating them into other 862 // variables. 863 NavButtonMode navButtonMode = mNavButtonMode; 864 if (mStateSet) { 865 if (mState == Toolbar.State.HOME) { 866 navButtonMode = NavButtonMode.DISABLED; 867 } else if (navButtonMode == NavButtonMode.DISABLED) { 868 navButtonMode = NavButtonMode.BACK; 869 } 870 } 871 872 SearchMode searchMode = mSearchMode; 873 if (mStateSet) { 874 if (mState == Toolbar.State.SEARCH) { 875 searchMode = SearchMode.SEARCH; 876 } else if (mState == Toolbar.State.EDIT) { 877 searchMode = SearchMode.EDIT; 878 } else { 879 searchMode = SearchMode.DISABLED; 880 } 881 } 882 883 boolean hasLogo = mHasLogo; 884 if (mStateSet && (mState == Toolbar.State.SEARCH || mState == Toolbar.State.EDIT)) { 885 hasLogo = false; 886 } 887 888 boolean hasTabs = mTabLayout.hasTabs(); 889 if (mStateSet && mState != Toolbar.State.HOME 890 && !(mState == Toolbar.State.SUBPAGE && mShowTabsInSubpage)) { 891 hasTabs = false; 892 } 893 894 boolean isSearching = searchMode != SearchMode.DISABLED; 895 if (mSearchView == null && isSearching) { 896 inflateSearchView(); 897 } 898 899 for (MenuItemRenderer renderer : mMenuItemRenderers) { 900 renderer.setToolbarIsSearching(searchMode == SearchMode.SEARCH); 901 } 902 903 View.OnClickListener backClickListener = (v) -> { 904 boolean absorbed = false; 905 List<Toolbar.OnBackListener> deprecatedListenersCopy = 906 new ArrayList<>(mDeprecatedBackListeners); 907 List<Supplier<Boolean>> listenersCopy = new ArrayList<>(mBackListeners); 908 for (Toolbar.OnBackListener listener : deprecatedListenersCopy) { 909 absorbed = absorbed || listener.onBack(); 910 } 911 for (Supplier<Boolean> listener : listenersCopy) { 912 absorbed = absorbed || listener.get(); 913 } 914 915 if (!absorbed) { 916 Activity activity = CarUiUtils.getActivity(getContext()); 917 if (activity != null) { 918 activity.onBackPressed(); 919 } 920 } 921 }; 922 923 switch (navButtonMode) { 924 case CLOSE: 925 mNavIcon.setImageResource(R.drawable.car_ui_icon_close); 926 break; 927 case DOWN: 928 mNavIcon.setImageResource(R.drawable.car_ui_icon_down); 929 break; 930 default: 931 mNavIcon.setImageResource(R.drawable.car_ui_icon_arrow_back); 932 break; 933 } 934 935 mLogoSpacer.setVisibility( 936 !hasTabs && navButtonMode != NavButtonMode.DISABLED ? VISIBLE : GONE); 937 mLogo.setVisibility(hasLogo ? VISIBLE : GONE); 938 939 mTitleSpacer.setVisibility( 940 hasTabs && navButtonMode != NavButtonMode.DISABLED ? VISIBLE : GONE); 941 mTitleLogoContainer.setOnClickListener( 942 hasLogo && mOnLogoClickListener != null && navButtonMode != NavButtonMode.DISABLED 943 ? v -> mOnLogoClickListener.run() : null); 944 mTitleLogoContainer.setVisibility(isSearching ? GONE : VISIBLE); 945 946 // Show the nav icon container if we're not in the home space or the logo fills the nav icon 947 // container. If car_ui_toolbar_nav_icon_reserve_space is true, hiding it will still reserve 948 // its space 949 mNavIconContainer.setVisibility(navButtonMode != NavButtonMode.DISABLED ? VISIBLE : GONE); 950 mNavIconContainer.setOnClickListener( 951 navButtonMode != NavButtonMode.DISABLED ? backClickListener : null); 952 mNavIconContainer.setClickable(navButtonMode != NavButtonMode.DISABLED); 953 mNavIconContainer.setContentDescription(navButtonMode != NavButtonMode.DISABLED 954 ? getContext().getString(R.string.car_ui_toolbar_nav_icon_content_description) 955 : null); 956 mNavIconSpacer.setVisibility(hasTabs && !isSearching ? VISIBLE : GONE); 957 958 // Show the title if we're in the subpage state, or in the home state with no tabs or tabs 959 // on the second row 960 mSubtitle.setVisibility( 961 TextUtils.isEmpty(getSubtitle()) ? GONE : VISIBLE); 962 963 mTabLayout.setVisibility(hasTabs && !isSearching ? VISIBLE : GONE); 964 965 if (mSearchView != null) { 966 if (isSearching) { 967 mSearchView.setPlainText(searchMode == SearchMode.EDIT); 968 mSearchView.setVisibility(VISIBLE); 969 } else { 970 mSearchView.setVisibility(GONE); 971 } 972 } 973 974 boolean showButtons = !isSearching || mShowMenuItemsWhileSearching; 975 mMenuItemsContainer.setVisibility(showButtons ? VISIBLE : GONE); 976 mOverflowButton.setVisible(showButtons && countVisibleOverflowItems() > 0); 977 } 978 inflateSearchView()979 private void inflateSearchView() { 980 SearchView searchView = new SearchView(mPluginContext); 981 searchView.setHint(mSearchHint); 982 searchView.setIcon(mSearchIcon); 983 searchView.setSearchQuery(mSearchQuery); 984 searchView.setSearchListeners( 985 mDeprecatedSearchListeners, mOnSearchListeners); 986 searchView.setSearchCompletedListeners( 987 mDeprecatedSearchCompletedListeners, mOnSearchCompletedListeners); 988 searchView.setVisibility(GONE); 989 990 FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams( 991 ViewGroup.LayoutParams.MATCH_PARENT, 992 ViewGroup.LayoutParams.MATCH_PARENT); 993 mSearchViewContainer.addView(searchView, layoutParams); 994 995 searchView.setSearchConfig(mSearchConfigForWidescreen); 996 997 mSearchView = searchView; 998 999 // These consumers should only be set once search view has been inflated 1000 if (mSearchTextViewConsumer != null) { 1001 mSearchView.setSearchTextViewConsumer( 1002 (TextView tv) -> mSearchTextViewConsumer.accept(tv)); 1003 } 1004 if (mOnPrivateImeCommandListener != null) { 1005 mSearchView.setOnPrivateImeCommandListener( 1006 (String s, Bundle b) -> mOnPrivateImeCommandListener.accept(s, b)); 1007 } 1008 } 1009 1010 /** 1011 * Add a view within a container that will animate with the wide screen IME to display search 1012 * results. 1013 * 1014 * <p>Note: Apps can only call this method if the package name is allowed via OEM to render 1015 * their view. To check if the application have the permission to do so or not first call 1016 * {@link SearchCapabilities#canShowSearchResultsView()}. If the app is not allowed this method 1017 * will throw an {@link IllegalStateException} 1018 * 1019 * @param view to be added in the container. 1020 */ 1021 @Override setSearchResultsView(View view)1022 public void setSearchResultsView(View view) { 1023 if (!getSearchCapabilities().canShowSearchResultsView()) { 1024 throw new IllegalStateException( 1025 "not allowed to add view to wide screen IME, package name: " 1026 + getContext().getPackageName()); 1027 } 1028 1029 setSearchConfig(mSearchConfigBuilder.setSearchResultsView(view).build()); 1030 } 1031 1032 @Override setSearchResultsInputViewIcon(@onNull Drawable drawable)1033 public void setSearchResultsInputViewIcon(@NonNull Drawable drawable) { 1034 setSearchConfig(mSearchConfigBuilder.setSearchResultsInputViewIcon(drawable).build()); 1035 } 1036 1037 /** 1038 * Sets list of search item {@link CarUiListItem} to be displayed in the IMS template. This 1039 * method should be called when system is running in a wide screen mode. Apps can check that by 1040 * using {@link SearchCapabilities#canShowSearchResultItems()} Else, this method will throw an 1041 * {@link IllegalStateException} 1042 */ 1043 @Override setSearchResultItems(List<? extends CarUiImeSearchListItem> searchItems)1044 public void setSearchResultItems(List<? extends CarUiImeSearchListItem> searchItems) { 1045 if (!getSearchCapabilities().canShowSearchResultItems()) { 1046 throw new IllegalStateException( 1047 "system not in wide screen mode, not allowed to set search result items "); 1048 } 1049 1050 setSearchConfig(mSearchConfigBuilder.setSearchResultItems(searchItems).build()); 1051 } 1052 1053 @Override setSearchConfig(SearchConfig searchConfig)1054 public void setSearchConfig(SearchConfig searchConfig) { 1055 mSearchConfigForWidescreen = searchConfig; 1056 if (mSearchView != null) { 1057 mSearchView.setSearchConfig(mSearchConfigForWidescreen); 1058 } 1059 } 1060 1061 @Override getSearchCapabilities()1062 public SearchCapabilities getSearchCapabilities() { 1063 return SearchWidescreenController.getSearchCapabilities(mContext); 1064 } 1065 1066 @Override canShowSearchResultItems()1067 public boolean canShowSearchResultItems() { 1068 return getSearchCapabilities().canShowSearchResultItems(); 1069 } 1070 1071 @Override canShowSearchResultsView()1072 public boolean canShowSearchResultsView() { 1073 return getSearchCapabilities().canShowSearchResultsView(); 1074 } 1075 1076 /** 1077 * Gets the current {@link Toolbar.State} of the toolbar. 1078 */ 1079 @Override getState()1080 public Toolbar.State getState() { 1081 return mState; 1082 } 1083 1084 /** 1085 * Returns whether or not the state of the toolbar was previously set. 1086 */ 1087 @Override isStateSet()1088 public boolean isStateSet() { 1089 return mStateSet; 1090 } 1091 1092 /** 1093 * Registers a new {@link Toolbar.OnTabSelectedListener} to the list of listeners. 1094 */ 1095 @Override registerOnTabSelectedListener(Toolbar.OnTabSelectedListener listener)1096 public void registerOnTabSelectedListener(Toolbar.OnTabSelectedListener listener) { 1097 mOnTabSelectedListeners.add(listener); 1098 } 1099 1100 /** 1101 * Unregisters an existing {@link Toolbar.OnTabSelectedListener} from the list of listeners. 1102 */ 1103 @Override unregisterOnTabSelectedListener(Toolbar.OnTabSelectedListener listener)1104 public boolean unregisterOnTabSelectedListener(Toolbar.OnTabSelectedListener listener) { 1105 return mOnTabSelectedListeners.remove(listener); 1106 } 1107 1108 /** 1109 * Registers a new {@link Toolbar.OnSearchListener} to the list of listeners. 1110 */ 1111 @Override registerOnSearchListener(Toolbar.OnSearchListener listener)1112 public void registerOnSearchListener(Toolbar.OnSearchListener listener) { 1113 mDeprecatedSearchListeners.add(listener); 1114 } 1115 1116 /** 1117 * Unregisters an existing {@link Toolbar.OnSearchListener} from the list of listeners. 1118 */ 1119 @Override unregisterOnSearchListener(Toolbar.OnSearchListener listener)1120 public boolean unregisterOnSearchListener(Toolbar.OnSearchListener listener) { 1121 return mDeprecatedSearchListeners.remove(listener); 1122 } 1123 1124 @Override registerSearchListener(Consumer<String> listener)1125 public void registerSearchListener(Consumer<String> listener) { 1126 mOnSearchListeners.add(listener); 1127 } 1128 1129 @Override unregisterSearchListener(Consumer<String> listener)1130 public boolean unregisterSearchListener(Consumer<String> listener) { 1131 return mOnSearchListeners.remove(listener); 1132 } 1133 1134 /** 1135 * Registers a new {@link Toolbar.OnSearchCompletedListener} to the list of listeners. 1136 */ 1137 @Override registerOnSearchCompletedListener(Toolbar.OnSearchCompletedListener listener)1138 public void registerOnSearchCompletedListener(Toolbar.OnSearchCompletedListener listener) { 1139 mDeprecatedSearchCompletedListeners.add(listener); 1140 } 1141 1142 /** 1143 * Unregisters an existing {@link Toolbar.OnSearchCompletedListener} from the list of 1144 * listeners. 1145 */ 1146 @Override unregisterOnSearchCompletedListener(Toolbar.OnSearchCompletedListener listener)1147 public boolean unregisterOnSearchCompletedListener(Toolbar.OnSearchCompletedListener listener) { 1148 return mDeprecatedSearchCompletedListeners.remove(listener); 1149 } 1150 1151 @Override registerSearchCompletedListener(Runnable listener)1152 public void registerSearchCompletedListener(Runnable listener) { 1153 mOnSearchCompletedListeners.add(listener); 1154 } 1155 1156 @Override unregisterSearchCompletedListener(Runnable listener)1157 public boolean unregisterSearchCompletedListener(Runnable listener) { 1158 return mOnSearchCompletedListeners.remove(listener); 1159 } 1160 1161 /** 1162 * Registers a new {@link Toolbar.OnBackListener} to the list of listeners. 1163 */ 1164 @Override registerOnBackListener(Toolbar.OnBackListener listener)1165 public void registerOnBackListener(Toolbar.OnBackListener listener) { 1166 mDeprecatedBackListeners.add(listener); 1167 } 1168 1169 /** 1170 * Unregisters an existing {@link Toolbar.OnBackListener} from the list of listeners. 1171 */ 1172 @Override unregisterOnBackListener(Toolbar.OnBackListener listener)1173 public boolean unregisterOnBackListener(Toolbar.OnBackListener listener) { 1174 return mDeprecatedBackListeners.remove(listener); 1175 } 1176 1177 @Override registerBackListener(Supplier<Boolean> listener)1178 public void registerBackListener(Supplier<Boolean> listener) { 1179 mBackListeners.add(listener); 1180 } 1181 1182 @Override unregisterBackListener(Supplier<Boolean> listener)1183 public boolean unregisterBackListener(Supplier<Boolean> listener) { 1184 return mBackListeners.remove(listener); 1185 } 1186 1187 /** 1188 * Returns the progress bar. 1189 */ 1190 @Override getProgressBar()1191 public ProgressBarController getProgressBar() { 1192 return mProgressBar; 1193 } 1194 1195 /** 1196 * Returns a ImeSearchInterfaceOEMV2 implementation. 1197 */ getImeSearchInterface()1198 public ImeSearchInterfaceOEMV2 getImeSearchInterface() { 1199 return new ImeSearchInterfaceOEMV2() { 1200 @Override 1201 public void setSearchTextViewConsumer( 1202 @Nullable com.android.car.ui.plugin.oemapis.Consumer<TextView> consumer) { 1203 mSearchTextViewConsumer = (TextView tv) -> consumer.accept(tv); 1204 } 1205 1206 @Override 1207 public void setOnPrivateImeCommandListener(@Nullable 1208 com.android.car.ui.plugin.oemapis.BiConsumer<String, Bundle> biConsumer) { 1209 mOnPrivateImeCommandListener = (String s, Bundle b) -> biConsumer.accept(s, b); 1210 } 1211 }; 1212 } 1213 } 1214