1 /*
2  * Copyright (C) 2019 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 package com.android.car.ui.toolbar;
17 
18 import static com.android.car.ui.core.CarUi.MIN_TARGET_API;
19 
20 import android.annotation.TargetApi;
21 import android.car.drivingstate.CarUxRestrictions;
22 import android.content.Context;
23 import android.graphics.drawable.Drawable;
24 import android.view.View;
25 import android.widget.Toast;
26 
27 import androidx.annotation.Nullable;
28 import androidx.annotation.VisibleForTesting;
29 
30 import com.android.car.ui.R;
31 import com.android.car.ui.utils.CarUxRestrictionsUtil;
32 
33 import java.lang.ref.WeakReference;
34 
35 /**
36  * Represents a button to display in the {@link Toolbar}.
37  *
38  * <p>There are currently 3 types of buttons: icon, text, and switch. Using
39  * {@link Builder#setCheckable()} will ensure that you get a switch, after that
40  * {@link Builder#setIcon(int)} will ensure an icon, and anything left just requires
41  * {@link Builder#setTitle(int)}.
42  *
43  * <p>Each MenuItem has a {@link DisplayBehavior} that controls if it appears on the {@link Toolbar}
44  * itself, or it's overflow menu.
45  *
46  * <p>If you require a search or settings button, you should use
47  * {@link Builder#setToSearch()} or
48  * {@link Builder#setToSettings()}.
49  *
50  * <p>Some properties can be changed after the creating a MenuItem, but others require being set
51  * with a {@link Builder}.
52  */
53 @TargetApi(MIN_TARGET_API)
54 public class MenuItem {
55 
56     private final Context mContext;
57     private final boolean mIsCheckable;
58     private final boolean mIsActivatable;
59     private final boolean mIsSearch;
60     private final boolean mShowIconAndTitle;
61     private final boolean mIsTinted;
62     private final boolean mIsPrimary;
63     @CarUxRestrictions.CarUxRestrictionsInfo
64 
65     private int mId;
66     private CarUxRestrictions mCurrentRestrictions;
67     // This is a WeakReference to allow the Toolbar (and by extension, the whole screen
68     // the toolbar is on) to be garbage-collected if the MenuItem is held past the
69     // lifecycle of the toolbar.
70     private WeakReference<Listener> mListener = new WeakReference<>(null);
71     private CharSequence mTitle;
72     private Drawable mIcon;
73     private OnClickListener mOnClickListener;
74     private final DisplayBehavior mDisplayBehavior;
75     private int mUxRestrictions;
76     private boolean mIsEnabled;
77     private boolean mIsChecked;
78     private boolean mIsVisible;
79     private boolean mIsActivated;
80 
81     @SuppressWarnings("FieldCanBeLocal") // Used with weak references
82     private final CarUxRestrictionsUtil.OnUxRestrictionsChangedListener mUxRestrictionsListener =
83             uxRestrictions -> {
84                 boolean wasRestricted = isRestricted();
85                 mCurrentRestrictions = uxRestrictions;
86 
87                 if (isRestricted() != wasRestricted) {
88                     update();
89                 }
90             };
91 
MenuItem(Builder builder)92     private MenuItem(Builder builder) {
93         mContext = builder.mContext;
94         mId = builder.mId;
95         mIsCheckable = builder.mIsCheckable;
96         mIsActivatable = builder.mIsActivatable;
97         mTitle = builder.mTitle;
98         mIcon = builder.mIcon;
99         mOnClickListener = builder.mOnClickListener;
100         mDisplayBehavior = builder.mDisplayBehavior;
101         mIsEnabled = builder.mIsEnabled;
102         mIsChecked = builder.mIsChecked;
103         mIsVisible = builder.mIsVisible;
104         mIsActivated = builder.mIsActivated;
105         mIsSearch = builder.mIsSearch;
106         mShowIconAndTitle = builder.mShowIconAndTitle;
107         mIsTinted = builder.mIsTinted;
108         mIsPrimary = builder.mIsPrimary;
109         mUxRestrictions = builder.mUxRestrictions;
110 
111         CarUxRestrictionsUtil.getInstance(mContext).register(mUxRestrictionsListener);
112     }
113 
update()114     private void update() {
115         Listener listener = mListener.get();
116         if (listener != null) {
117             listener.onMenuItemChanged(this);
118         }
119     }
120 
121     /** Sets the id, which is purely for the client to distinguish MenuItems with.  */
setId(int id)122     public void setId(int id) {
123         mId = id;
124         update();
125     }
126 
127     /** Gets the id, which is purely for the client to distinguish MenuItems with. */
getId()128     public int getId() {
129         return mId;
130     }
131 
132     /** Returns whether the MenuItem is enabled */
isEnabled()133     public boolean isEnabled() {
134         return mIsEnabled;
135     }
136 
137     /** Sets whether the MenuItem is enabled */
setEnabled(boolean enabled)138     public void setEnabled(boolean enabled) {
139         mIsEnabled = enabled;
140 
141         update();
142     }
143 
144     /** Returns whether the MenuItem is checkable. If it is, it will be displayed as a switch. */
isCheckable()145     public boolean isCheckable() {
146         return mIsCheckable;
147     }
148 
149     /**
150      * Returns whether the MenuItem is currently checked. Only valid if {@link #isCheckable()}
151      * is true.
152      */
isChecked()153     public boolean isChecked() {
154         return mIsChecked;
155     }
156 
157     /**
158      * Sets whether or not the MenuItem is checked.
159      * @throws IllegalStateException When {@link #isCheckable()} is false.
160      */
setChecked(boolean checked)161     public void setChecked(boolean checked) {
162         if (!isCheckable()) {
163             throw new IllegalStateException("Cannot call setChecked() on a non-checkable MenuItem");
164         }
165 
166         mIsChecked = checked;
167 
168         update();
169     }
170 
isTinted()171     public boolean isTinted() {
172         return mIsTinted;
173     }
174 
175     /** Returns whether or not the MenuItem is visible */
isVisible()176     public boolean isVisible() {
177         return mIsVisible;
178     }
179 
180     /** Sets whether or not the MenuItem is visible */
setVisible(boolean visible)181     public void setVisible(boolean visible) {
182         mIsVisible = visible;
183 
184         update();
185     }
186 
187     /**
188      * Returns whether the MenuItem is activatable. If it is, it's every click will toggle
189      * the MenuItem's View to appear activated or not.
190      */
isActivatable()191     public boolean isActivatable() {
192         return mIsActivatable;
193     }
194 
195     /** Returns whether or not this view is selected. Toggles after every click */
isActivated()196     public boolean isActivated() {
197         return mIsActivated;
198     }
199 
200     /** Sets the MenuItem as activated and updates it's View to the activated state */
setActivated(boolean activated)201     public void setActivated(boolean activated) {
202         if (!isActivatable()) {
203             throw new IllegalStateException(
204                     "Cannot call setActivated() on a non-activatable MenuItem");
205         }
206 
207         mIsActivated = activated;
208 
209         update();
210     }
211 
212     /** Gets the title of this MenuItem. */
getTitle()213     public CharSequence getTitle() {
214         return mTitle;
215     }
216 
217     /** Sets the title of this MenuItem. */
setTitle(CharSequence title)218     public void setTitle(CharSequence title) {
219         mTitle = title;
220 
221         update();
222     }
223 
224     /** Sets the title of this MenuItem to a string resource. */
setTitle(int resId)225     public void setTitle(int resId) {
226         setTitle(mContext.getString(resId));
227     }
228 
229     /** Sets the UxRestrictions of this MenuItem. */
setUxRestrictions(@arUxRestrictions.CarUxRestrictionsInfo int uxRestrictions)230     public void setUxRestrictions(@CarUxRestrictions.CarUxRestrictionsInfo int uxRestrictions) {
231         if (mUxRestrictions != uxRestrictions) {
232             mUxRestrictions = uxRestrictions;
233             update();
234         }
235     }
236 
237     @CarUxRestrictions.CarUxRestrictionsInfo
getUxRestrictions()238     public int getUxRestrictions() {
239         return mUxRestrictions;
240     }
241 
242     /** Gets the current {@link OnClickListener} */
getOnClickListener()243     public OnClickListener getOnClickListener() {
244         return mOnClickListener;
245     }
246 
isShowingIconAndTitle()247     public boolean isShowingIconAndTitle() {
248         return mShowIconAndTitle;
249     }
250 
251     /** Sets the {@link OnClickListener} */
setOnClickListener(OnClickListener listener)252     public void setOnClickListener(OnClickListener listener) {
253         mOnClickListener = listener;
254 
255         update();
256     }
257 
isRestricted()258     public boolean isRestricted() {
259         return CarUxRestrictionsUtil.isRestricted(mUxRestrictions, mCurrentRestrictions);
260     }
261 
262     /** Calls the {@link OnClickListener}. */
263     @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
performClick()264     public void performClick() {
265         if (!isEnabled() || !isVisible()) {
266             return;
267         }
268 
269         if (isRestricted()) {
270             Toast.makeText(mContext,
271                     R.string.car_ui_restricted_while_driving, Toast.LENGTH_LONG).show();
272             return;
273         }
274 
275         if (isActivatable()) {
276             setActivated(!isActivated());
277         }
278 
279         if (isCheckable()) {
280             setChecked(!isChecked());
281         }
282 
283         if (mOnClickListener != null) {
284             mOnClickListener.onClick(this);
285         }
286     }
287 
288     /** Gets the current {@link DisplayBehavior} */
getDisplayBehavior()289     public DisplayBehavior getDisplayBehavior() {
290         return mDisplayBehavior;
291     }
292 
293     /** Gets the current Icon */
getIcon()294     public Drawable getIcon() {
295         return mIcon;
296     }
297 
298     /** Sets the Icon of this MenuItem. */
setIcon(Drawable icon)299     public void setIcon(Drawable icon) {
300         mIcon = icon;
301 
302         update();
303     }
304 
305     /** Sets the Icon of this MenuItem to a drawable resource. */
setIcon(int resId)306     public void setIcon(int resId) {
307         setIcon(resId == 0
308                 ? null
309                 : mContext.getDrawable(resId));
310     }
311 
312     /**
313      * Returns if this MenuItem is a primary MenuItem, which means it should be visually
314      * distinct to indicate that.
315      */
isPrimary()316     public boolean isPrimary() {
317         return mIsPrimary;
318     }
319 
320     /** Returns if this is the search MenuItem, which is not shown while searching */
isSearch()321     public boolean isSearch() {
322         return mIsSearch;
323     }
324 
325     /** Builder class */
326     public static final class Builder {
327         private final Context mContext;
328 
329         private String mSearchTitle;
330         private String mSettingsTitle;
331         private Drawable mSearchIcon;
332         private Drawable mSettingsIcon;
333 
334         private int mId = View.NO_ID;
335         private CharSequence mTitle;
336         private Drawable mIcon;
337         private OnClickListener mOnClickListener;
338         private DisplayBehavior mDisplayBehavior = DisplayBehavior.ALWAYS;
339         private boolean mIsTinted = true;
340         private boolean mShowIconAndTitle = false;
341         private boolean mIsEnabled = true;
342         private boolean mIsCheckable = false;
343         private boolean mIsChecked = false;
344         private boolean mIsVisible = true;
345         private boolean mIsActivatable = false;
346         private boolean mIsActivated = false;
347         private boolean mIsSearch = false;
348         private boolean mIsSettings = false;
349         private boolean mIsPrimary = false;
350         @CarUxRestrictions.CarUxRestrictionsInfo
351         private int mUxRestrictions = CarUxRestrictions.UX_RESTRICTIONS_BASELINE;
352 
Builder(Context c)353         public Builder(Context c) {
354             // Must use getApplicationContext to avoid leaking activities when the MenuItem
355             // is held onto for longer than the Activity's lifecycle
356             mContext = c.getApplicationContext();
357         }
358 
359         /** Builds a {@link MenuItem} from the current state of the Builder */
build()360         public MenuItem build() {
361             if (mIsActivatable && (mShowIconAndTitle || mIcon == null)) {
362                 throw new IllegalStateException("Only simple icons can be activatable");
363             }
364             if (mIsCheckable && (mShowIconAndTitle || mIsActivatable)) {
365                 throw new IllegalStateException("Unsupported options for a checkable MenuItem");
366             }
367             if (mIsSearch && mIsSettings) {
368                 throw new IllegalStateException("Can't have both a search and settings MenuItem");
369             }
370             if (mIsActivatable && mDisplayBehavior == DisplayBehavior.NEVER) {
371                 throw new IllegalStateException("Activatable MenuItems not supported as Overflow");
372             }
373 
374             if (mIsSearch && (!mSearchTitle.contentEquals(mTitle)
375                     || !mSearchIcon.equals(mIcon)
376                     || mIsCheckable
377                     || mIsActivatable
378                     || !mIsTinted
379                     || mShowIconAndTitle
380                     || mDisplayBehavior != DisplayBehavior.ALWAYS)) {
381                 throw new IllegalStateException("Invalid search MenuItem");
382             }
383 
384             if (mIsSettings && (!mSettingsTitle.contentEquals(mTitle)
385                     || !mSettingsIcon.equals(mIcon)
386                     || mIsCheckable
387                     || mIsActivatable
388                     || !mIsTinted
389                     || mShowIconAndTitle
390                     || mDisplayBehavior != DisplayBehavior.ALWAYS)) {
391                 throw new IllegalStateException("Invalid settings MenuItem");
392             }
393 
394             return new MenuItem(this);
395         }
396 
397         /** Sets the id, which is purely for the client to distinguish MenuItems with. */
setId(int id)398         public Builder setId(int id) {
399             mId = id;
400             return this;
401         }
402 
403         /** Sets the title to a string resource id */
setTitle(int resId)404         public Builder setTitle(int resId) {
405             setTitle(mContext.getString(resId));
406             return this;
407         }
408 
409         /** Sets the title */
setTitle(CharSequence title)410         public Builder setTitle(CharSequence title) {
411             mTitle = title;
412             return this;
413         }
414 
415         /**
416          * Sets the icon to a drawable resource id.
417          *
418          * <p>The icon's color and size will be changed to match the other MenuItems.
419          */
setIcon(int resId)420         public Builder setIcon(int resId) {
421             mIcon = resId == 0
422                     ? null
423                     : mContext.getDrawable(resId);
424             return this;
425         }
426 
427         /**
428          * Sets the icon to a drawable.
429          *
430          * <p>The icon's color and size will be changed to match the other MenuItems.
431          */
setIcon(Drawable icon)432         public Builder setIcon(Drawable icon) {
433             mIcon = icon;
434             return this;
435         }
436 
437         /**
438          * Sets whether to tint the icon, true by default.
439          *
440          * <p>Try not to use this, it should only be used if the MenuItem is displaying some
441          * kind of logo or avatar and should be colored.
442          */
setTinted(boolean tinted)443         public Builder setTinted(boolean tinted) {
444             mIsTinted = tinted;
445             return this;
446         }
447 
448         /** Sets whether the MenuItem is visible or not. Default true. */
setVisible(boolean visible)449         public Builder setVisible(boolean visible) {
450             mIsVisible = visible;
451             return this;
452         }
453 
454         /**
455          * Makes the MenuItem activatable, which means it will toggle it's visual state after
456          * every click.
457          */
setActivatable()458         public Builder setActivatable() {
459             mIsActivatable = true;
460             return this;
461         }
462 
463         /**
464          * Sets whether or not the MenuItem is selected. If it is,
465          * {@link View#setSelected(boolean)} will be called on its View.
466          */
setActivated(boolean activated)467         public Builder setActivated(boolean activated) {
468             setActivatable();
469             mIsActivated = activated;
470             return this;
471         }
472 
473         /** Sets the {@link OnClickListener} */
setOnClickListener(OnClickListener listener)474         public Builder setOnClickListener(OnClickListener listener) {
475             mOnClickListener = listener;
476             return this;
477         }
478 
479         /**
480          * Used to show both the icon and title when displayed on the toolbar. If this
481          * is false, only the icon while be displayed when the MenuItem is in the toolbar
482          * and only the title will be displayed when the MenuItem is in the overflow menu.
483          *
484          * <p>Defaults to false.
485          */
setShowIconAndTitle(boolean showIconAndTitle)486         public Builder setShowIconAndTitle(boolean showIconAndTitle) {
487             mShowIconAndTitle = showIconAndTitle;
488             return this;
489         }
490 
491         /**
492          * Sets the {@link DisplayBehavior}.
493          *
494          * <p>If the DisplayBehavior is {@link DisplayBehavior#NEVER}, the MenuItem must not be
495          * {@link #setCheckable() checkable}.
496          */
setDisplayBehavior(DisplayBehavior behavior)497         public Builder setDisplayBehavior(DisplayBehavior behavior) {
498             mDisplayBehavior = behavior;
499             return this;
500         }
501 
502         /** Sets whether the MenuItem is enabled or not. Default true. */
setEnabled(boolean enabled)503         public Builder setEnabled(boolean enabled) {
504             mIsEnabled = enabled;
505             return this;
506         }
507 
508         /**
509          * Makes the MenuItem checkable, meaning it will be displayed as a
510          * switch.
511          *
512          * <p>The MenuItem is not checkable by default.
513          */
setCheckable()514         public Builder setCheckable() {
515             mIsCheckable = true;
516             return this;
517         }
518 
519         /**
520          * Sets whether the MenuItem is checked or not. This will imply {@link #setCheckable()}.
521          */
setChecked(boolean checked)522         public Builder setChecked(boolean checked) {
523             setCheckable();
524             mIsChecked = checked;
525             return this;
526         }
527 
528         /**
529          * Sets whether the MenuItem is primary. This is just a visual change.
530          */
setPrimary(boolean primary)531         public Builder setPrimary(boolean primary) {
532             mIsPrimary = primary;
533             return this;
534         }
535 
536         /**
537          * Sets under what {@link android.car.drivingstate.CarUxRestrictions.CarUxRestrictionsInfo}
538          * the MenuItem should be restricted.
539          */
setUxRestrictions( @arUxRestrictions.CarUxRestrictionsInfo int restrictions)540         public Builder setUxRestrictions(
541                 @CarUxRestrictions.CarUxRestrictionsInfo int restrictions) {
542             mUxRestrictions = restrictions;
543             return this;
544         }
545 
546         /**
547          * Creates a search MenuItem.
548          *
549          * <p>The advantage of using this over creating your own is getting an OEM-styled search
550          * icon, and this button will always disappear while searching, even when the
551          * {@link Toolbar Toolbar's} showMenuItemsWhileSearching is true.
552          *
553          * <p>If using this, you should only change the id, visibility, or onClickListener.
554          */
setToSearch()555         public Builder setToSearch() {
556             mSearchTitle = mContext.getString(R.string.car_ui_toolbar_menu_item_search_title);
557             mSearchIcon = mContext.getDrawable(R.drawable.car_ui_icon_search);
558             mIsSearch = true;
559             setTitle(mSearchTitle);
560             setIcon(mSearchIcon);
561             return this;
562         }
563 
564         /**
565          * Creates a settings MenuItem.
566          *
567          * <p>The advantage of this over creating your own is getting an OEM-styled settings icon,
568          * and that the MenuItem will be restricted based on
569          * {@link CarUxRestrictions#UX_RESTRICTIONS_NO_SETUP}
570          *
571          * <p>If using this, you should only change the id, visibility, or onClickListener.
572          */
setToSettings()573         public Builder setToSettings() {
574             mSettingsTitle = mContext.getString(R.string.car_ui_toolbar_menu_item_settings_title);
575             mSettingsIcon = mContext.getDrawable(R.drawable.car_ui_icon_settings);
576             mIsSettings = true;
577             setTitle(mSettingsTitle);
578             setIcon(mSettingsIcon);
579             setUxRestrictions(CarUxRestrictions.UX_RESTRICTIONS_NO_SETUP);
580             return this;
581         }
582 
583         /** @deprecated Use {@link #setToSearch()} instead. */
584         @Deprecated
createSearch(Context c, OnClickListener listener)585         public static MenuItem createSearch(Context c, OnClickListener listener) {
586             return MenuItem.builder(c)
587                     .setToSearch()
588                     .setOnClickListener(listener)
589                     .build();
590         }
591 
592         /** @deprecated Use {@link #setToSettings()} instead. */
593         @Deprecated
createSettings(Context c, OnClickListener listener)594         public static MenuItem createSettings(Context c, OnClickListener listener) {
595             return MenuItem.builder(c)
596                     .setToSettings()
597                     .setOnClickListener(listener)
598                     .build();
599         }
600     }
601 
602     /** Get a new {@link Builder}. */
builder(Context context)603     public static Builder builder(Context context) {
604         return new Builder(context);
605     }
606 
607     /**
608      * OnClickListener for a MenuItem.
609      */
610     public interface OnClickListener {
611         /** Called when the MenuItem is clicked */
onClick(MenuItem item)612         void onClick(MenuItem item);
613     }
614 
615     /**
616      * DisplayBehavior controls how the MenuItem is presented in the Toolbar
617      */
618     public enum DisplayBehavior {
619         /** Always show the MenuItem on the toolbar instead of the overflow menu */
620         ALWAYS,
621         /** Never show the MenuItem in the toolbar, always put it in the overflow menu */
622         NEVER
623     }
624 
625     /**
626      * Listener for {@link Toolbar} to update when this MenuItem changes.
627      *
628      * Do not use from client apps, for car-ui-lib internal use only.
629      */
630     //TODO(b/179092760) Find a way to prevent apps from using this
631     public interface Listener {
632         /** Called when the MenuItem is changed. For use only by {@link Toolbar} */
onMenuItemChanged(MenuItem item)633         void onMenuItemChanged(MenuItem item);
634     }
635 
636     /**
637      * Sets a listener for changes to this MenuItem. Note that the MenuItem will only hold
638      * weak references to the Listener, so that the listener is not held if the MenuItem
639      * outlives the toolbar.
640      *
641      * Do not use from client apps, for car-ui-lib internal use only.
642      */
643     //TODO(b/179092760) Find a way to prevent apps from using this
setListener(@ullable Listener listener)644     public void setListener(@Nullable Listener listener) {
645         mListener = new WeakReference<>(listener);
646     }
647 }
648