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