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