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