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