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(); 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 347 && (mDisplayBehavior == DisplayBehavior.NEVER 348 || mShowIconAndTitle 349 || mIsActivatable)) { 350 throw new IllegalStateException("Unsupported options for a checkable MenuItem"); 351 } 352 if (mIsSearch && mIsSettings) { 353 throw new IllegalStateException("Can't have both a search and settings MenuItem"); 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. Currently a checkable MenuItem cannot have a {@link DisplayBehavior} of NEVER. 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()602 void onMenuItemChanged(); 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