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