1 /* 2 * Copyright (C) 2006 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 17 package com.android.internal.view.menu; 18 19 20 import android.annotation.NonNull; 21 import android.content.ComponentName; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.pm.PackageManager; 25 import android.content.pm.ResolveInfo; 26 import android.content.res.Configuration; 27 import android.content.res.Resources; 28 import android.graphics.drawable.Drawable; 29 import android.os.Bundle; 30 import android.os.Parcelable; 31 import android.util.SparseArray; 32 import android.view.ActionProvider; 33 import android.view.ContextMenu.ContextMenuInfo; 34 import android.view.KeyCharacterMap; 35 import android.view.KeyEvent; 36 import android.view.Menu; 37 import android.view.MenuItem; 38 import android.view.SubMenu; 39 import android.view.View; 40 41 import java.lang.ref.WeakReference; 42 import java.util.ArrayList; 43 import java.util.List; 44 import java.util.concurrent.CopyOnWriteArrayList; 45 46 /** 47 * Implementation of the {@link android.view.Menu} interface for creating a 48 * standard menu UI. 49 */ 50 public class MenuBuilder implements Menu { 51 private static final String TAG = "MenuBuilder"; 52 53 private static final String PRESENTER_KEY = "android:menu:presenters"; 54 private static final String ACTION_VIEW_STATES_KEY = "android:menu:actionviewstates"; 55 private static final String EXPANDED_ACTION_VIEW_ID = "android:menu:expandedactionview"; 56 57 private static final int[] sCategoryToOrder = new int[] { 58 1, /* No category */ 59 4, /* CONTAINER */ 60 5, /* SYSTEM */ 61 3, /* SECONDARY */ 62 2, /* ALTERNATIVE */ 63 0, /* SELECTED_ALTERNATIVE */ 64 }; 65 66 private final Context mContext; 67 private final Resources mResources; 68 69 /** 70 * Whether the shortcuts should be qwerty-accessible. Use isQwertyMode() 71 * instead of accessing this directly. 72 */ 73 private boolean mQwertyMode; 74 75 /** 76 * Whether the shortcuts should be visible on menus. Use isShortcutsVisible() 77 * instead of accessing this directly. 78 */ 79 private boolean mShortcutsVisible; 80 81 /** 82 * Callback that will receive the various menu-related events generated by 83 * this class. Use getCallback to get a reference to the callback. 84 */ 85 private Callback mCallback; 86 87 /** Contains all of the items for this menu */ 88 private ArrayList<MenuItemImpl> mItems; 89 90 /** Contains only the items that are currently visible. This will be created/refreshed from 91 * {@link #getVisibleItems()} */ 92 private ArrayList<MenuItemImpl> mVisibleItems; 93 /** 94 * Whether or not the items (or any one item's shown state) has changed since it was last 95 * fetched from {@link #getVisibleItems()} 96 */ 97 private boolean mIsVisibleItemsStale; 98 99 /** 100 * Contains only the items that should appear in the Action Bar, if present. 101 */ 102 private ArrayList<MenuItemImpl> mActionItems; 103 /** 104 * Contains items that should NOT appear in the Action Bar, if present. 105 */ 106 private ArrayList<MenuItemImpl> mNonActionItems; 107 108 /** 109 * Whether or not the items (or any one item's action state) has changed since it was 110 * last fetched. 111 */ 112 private boolean mIsActionItemsStale; 113 114 /** 115 * Default value for how added items should show in the action list. 116 */ 117 private int mDefaultShowAsAction = MenuItem.SHOW_AS_ACTION_NEVER; 118 119 /** 120 * Current use case is Context Menus: As Views populate the context menu, each one has 121 * extra information that should be passed along. This is the current menu info that 122 * should be set on all items added to this menu. 123 */ 124 private ContextMenuInfo mCurrentMenuInfo; 125 126 /** Header title for menu types that have a header (context and submenus) */ 127 CharSequence mHeaderTitle; 128 /** Header icon for menu types that have a header and support icons (context) */ 129 Drawable mHeaderIcon; 130 /** Header custom view for menu types that have a header and support custom views (context) */ 131 View mHeaderView; 132 133 /** 134 * Contains the state of the View hierarchy for all menu views when the menu 135 * was frozen. 136 */ 137 private SparseArray<Parcelable> mFrozenViewStates; 138 139 /** 140 * Prevents onItemsChanged from doing its junk, useful for batching commands 141 * that may individually call onItemsChanged. 142 */ 143 private boolean mPreventDispatchingItemsChanged = false; 144 private boolean mItemsChangedWhileDispatchPrevented = false; 145 146 private boolean mOptionalIconsVisible = false; 147 148 private boolean mIsClosing = false; 149 150 private ArrayList<MenuItemImpl> mTempShortcutItemList = new ArrayList<MenuItemImpl>(); 151 152 private CopyOnWriteArrayList<WeakReference<MenuPresenter>> mPresenters = 153 new CopyOnWriteArrayList<WeakReference<MenuPresenter>>(); 154 155 /** 156 * Currently expanded menu item; must be collapsed when we clear. 157 */ 158 private MenuItemImpl mExpandedItem; 159 160 /** 161 * Called by menu to notify of close and selection changes. 162 */ 163 public interface Callback { 164 /** 165 * Called when a menu item is selected. 166 * @param menu The menu that is the parent of the item 167 * @param item The menu item that is selected 168 * @return whether the menu item selection was handled 169 */ onMenuItemSelected(MenuBuilder menu, MenuItem item)170 public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item); 171 172 /** 173 * Called when the mode of the menu changes (for example, from icon to expanded). 174 * 175 * @param menu the menu that has changed modes 176 */ onMenuModeChange(MenuBuilder menu)177 public void onMenuModeChange(MenuBuilder menu); 178 } 179 180 /** 181 * Called by menu items to execute their associated action 182 */ 183 public interface ItemInvoker { invokeItem(MenuItemImpl item)184 public boolean invokeItem(MenuItemImpl item); 185 } 186 MenuBuilder(Context context)187 public MenuBuilder(Context context) { 188 mContext = context; 189 mResources = context.getResources(); 190 mItems = new ArrayList<MenuItemImpl>(); 191 192 mVisibleItems = new ArrayList<MenuItemImpl>(); 193 mIsVisibleItemsStale = true; 194 195 mActionItems = new ArrayList<MenuItemImpl>(); 196 mNonActionItems = new ArrayList<MenuItemImpl>(); 197 mIsActionItemsStale = true; 198 199 setShortcutsVisibleInner(true); 200 } 201 setDefaultShowAsAction(int defaultShowAsAction)202 public MenuBuilder setDefaultShowAsAction(int defaultShowAsAction) { 203 mDefaultShowAsAction = defaultShowAsAction; 204 return this; 205 } 206 207 /** 208 * Add a presenter to this menu. This will only hold a WeakReference; 209 * you do not need to explicitly remove a presenter, but you can using 210 * {@link #removeMenuPresenter(MenuPresenter)}. 211 * 212 * @param presenter The presenter to add 213 */ addMenuPresenter(MenuPresenter presenter)214 public void addMenuPresenter(MenuPresenter presenter) { 215 addMenuPresenter(presenter, mContext); 216 } 217 218 /** 219 * Add a presenter to this menu that uses an alternate context for 220 * inflating menu items. This will only hold a WeakReference; you do not 221 * need to explicitly remove a presenter, but you can using 222 * {@link #removeMenuPresenter(MenuPresenter)}. 223 * 224 * @param presenter The presenter to add 225 * @param menuContext The context used to inflate menu items 226 */ addMenuPresenter(MenuPresenter presenter, Context menuContext)227 public void addMenuPresenter(MenuPresenter presenter, Context menuContext) { 228 mPresenters.add(new WeakReference<MenuPresenter>(presenter)); 229 presenter.initForMenu(menuContext, this); 230 mIsActionItemsStale = true; 231 } 232 233 /** 234 * Remove a presenter from this menu. That presenter will no longer 235 * receive notifications of updates to this menu's data. 236 * 237 * @param presenter The presenter to remove 238 */ removeMenuPresenter(MenuPresenter presenter)239 public void removeMenuPresenter(MenuPresenter presenter) { 240 for (WeakReference<MenuPresenter> ref : mPresenters) { 241 final MenuPresenter item = ref.get(); 242 if (item == null || item == presenter) { 243 mPresenters.remove(ref); 244 } 245 } 246 } 247 dispatchPresenterUpdate(boolean cleared)248 private void dispatchPresenterUpdate(boolean cleared) { 249 if (mPresenters.isEmpty()) return; 250 251 stopDispatchingItemsChanged(); 252 for (WeakReference<MenuPresenter> ref : mPresenters) { 253 final MenuPresenter presenter = ref.get(); 254 if (presenter == null) { 255 mPresenters.remove(ref); 256 } else { 257 presenter.updateMenuView(cleared); 258 } 259 } 260 startDispatchingItemsChanged(); 261 } 262 dispatchSubMenuSelected(SubMenuBuilder subMenu, MenuPresenter preferredPresenter)263 private boolean dispatchSubMenuSelected(SubMenuBuilder subMenu, 264 MenuPresenter preferredPresenter) { 265 if (mPresenters.isEmpty()) return false; 266 267 boolean result = false; 268 269 // Try the preferred presenter first. 270 if (preferredPresenter != null) { 271 result = preferredPresenter.onSubMenuSelected(subMenu); 272 } 273 274 for (WeakReference<MenuPresenter> ref : mPresenters) { 275 final MenuPresenter presenter = ref.get(); 276 if (presenter == null) { 277 mPresenters.remove(ref); 278 } else if (!result) { 279 result = presenter.onSubMenuSelected(subMenu); 280 } 281 } 282 return result; 283 } 284 dispatchSaveInstanceState(Bundle outState)285 private void dispatchSaveInstanceState(Bundle outState) { 286 if (mPresenters.isEmpty()) return; 287 288 SparseArray<Parcelable> presenterStates = new SparseArray<Parcelable>(); 289 290 for (WeakReference<MenuPresenter> ref : mPresenters) { 291 final MenuPresenter presenter = ref.get(); 292 if (presenter == null) { 293 mPresenters.remove(ref); 294 } else { 295 final int id = presenter.getId(); 296 if (id > 0) { 297 final Parcelable state = presenter.onSaveInstanceState(); 298 if (state != null) { 299 presenterStates.put(id, state); 300 } 301 } 302 } 303 } 304 305 outState.putSparseParcelableArray(PRESENTER_KEY, presenterStates); 306 } 307 dispatchRestoreInstanceState(Bundle state)308 private void dispatchRestoreInstanceState(Bundle state) { 309 SparseArray<Parcelable> presenterStates = state.getSparseParcelableArray(PRESENTER_KEY); 310 311 if (presenterStates == null || mPresenters.isEmpty()) return; 312 313 for (WeakReference<MenuPresenter> ref : mPresenters) { 314 final MenuPresenter presenter = ref.get(); 315 if (presenter == null) { 316 mPresenters.remove(ref); 317 } else { 318 final int id = presenter.getId(); 319 if (id > 0) { 320 Parcelable parcel = presenterStates.get(id); 321 if (parcel != null) { 322 presenter.onRestoreInstanceState(parcel); 323 } 324 } 325 } 326 } 327 } 328 savePresenterStates(Bundle outState)329 public void savePresenterStates(Bundle outState) { 330 dispatchSaveInstanceState(outState); 331 } 332 restorePresenterStates(Bundle state)333 public void restorePresenterStates(Bundle state) { 334 dispatchRestoreInstanceState(state); 335 } 336 saveActionViewStates(Bundle outStates)337 public void saveActionViewStates(Bundle outStates) { 338 SparseArray<Parcelable> viewStates = null; 339 340 final int itemCount = size(); 341 for (int i = 0; i < itemCount; i++) { 342 final MenuItem item = getItem(i); 343 final View v = item.getActionView(); 344 if (v != null && v.getId() != View.NO_ID) { 345 if (viewStates == null) { 346 viewStates = new SparseArray<Parcelable>(); 347 } 348 v.saveHierarchyState(viewStates); 349 if (item.isActionViewExpanded()) { 350 outStates.putInt(EXPANDED_ACTION_VIEW_ID, item.getItemId()); 351 } 352 } 353 if (item.hasSubMenu()) { 354 final SubMenuBuilder subMenu = (SubMenuBuilder) item.getSubMenu(); 355 subMenu.saveActionViewStates(outStates); 356 } 357 } 358 359 if (viewStates != null) { 360 outStates.putSparseParcelableArray(getActionViewStatesKey(), viewStates); 361 } 362 } 363 restoreActionViewStates(Bundle states)364 public void restoreActionViewStates(Bundle states) { 365 if (states == null) { 366 return; 367 } 368 369 SparseArray<Parcelable> viewStates = states.getSparseParcelableArray( 370 getActionViewStatesKey()); 371 372 final int itemCount = size(); 373 for (int i = 0; i < itemCount; i++) { 374 final MenuItem item = getItem(i); 375 final View v = item.getActionView(); 376 if (v != null && v.getId() != View.NO_ID) { 377 v.restoreHierarchyState(viewStates); 378 } 379 if (item.hasSubMenu()) { 380 final SubMenuBuilder subMenu = (SubMenuBuilder) item.getSubMenu(); 381 subMenu.restoreActionViewStates(states); 382 } 383 } 384 385 final int expandedId = states.getInt(EXPANDED_ACTION_VIEW_ID); 386 if (expandedId > 0) { 387 MenuItem itemToExpand = findItem(expandedId); 388 if (itemToExpand != null) { 389 itemToExpand.expandActionView(); 390 } 391 } 392 } 393 getActionViewStatesKey()394 protected String getActionViewStatesKey() { 395 return ACTION_VIEW_STATES_KEY; 396 } 397 setCallback(Callback cb)398 public void setCallback(Callback cb) { 399 mCallback = cb; 400 } 401 402 /** 403 * Adds an item to the menu. The other add methods funnel to this. 404 */ addInternal(int group, int id, int categoryOrder, CharSequence title)405 private MenuItem addInternal(int group, int id, int categoryOrder, CharSequence title) { 406 final int ordering = getOrdering(categoryOrder); 407 408 final MenuItemImpl item = createNewMenuItem(group, id, categoryOrder, ordering, title, 409 mDefaultShowAsAction); 410 411 if (mCurrentMenuInfo != null) { 412 // Pass along the current menu info 413 item.setMenuInfo(mCurrentMenuInfo); 414 } 415 416 mItems.add(findInsertIndex(mItems, ordering), item); 417 onItemsChanged(true); 418 419 return item; 420 } 421 422 // Layoutlib overrides this method to return its custom implementation of MenuItemImpl createNewMenuItem(int group, int id, int categoryOrder, int ordering, CharSequence title, int defaultShowAsAction)423 private MenuItemImpl createNewMenuItem(int group, int id, int categoryOrder, int ordering, 424 CharSequence title, int defaultShowAsAction) { 425 return new MenuItemImpl(this, group, id, categoryOrder, ordering, title, 426 defaultShowAsAction); 427 } 428 add(CharSequence title)429 public MenuItem add(CharSequence title) { 430 return addInternal(0, 0, 0, title); 431 } 432 add(int titleRes)433 public MenuItem add(int titleRes) { 434 return addInternal(0, 0, 0, mResources.getString(titleRes)); 435 } 436 add(int group, int id, int categoryOrder, CharSequence title)437 public MenuItem add(int group, int id, int categoryOrder, CharSequence title) { 438 return addInternal(group, id, categoryOrder, title); 439 } 440 add(int group, int id, int categoryOrder, int title)441 public MenuItem add(int group, int id, int categoryOrder, int title) { 442 return addInternal(group, id, categoryOrder, mResources.getString(title)); 443 } 444 addSubMenu(CharSequence title)445 public SubMenu addSubMenu(CharSequence title) { 446 return addSubMenu(0, 0, 0, title); 447 } 448 addSubMenu(int titleRes)449 public SubMenu addSubMenu(int titleRes) { 450 return addSubMenu(0, 0, 0, mResources.getString(titleRes)); 451 } 452 addSubMenu(int group, int id, int categoryOrder, CharSequence title)453 public SubMenu addSubMenu(int group, int id, int categoryOrder, CharSequence title) { 454 final MenuItemImpl item = (MenuItemImpl) addInternal(group, id, categoryOrder, title); 455 final SubMenuBuilder subMenu = new SubMenuBuilder(mContext, this, item); 456 item.setSubMenu(subMenu); 457 458 return subMenu; 459 } 460 addSubMenu(int group, int id, int categoryOrder, int title)461 public SubMenu addSubMenu(int group, int id, int categoryOrder, int title) { 462 return addSubMenu(group, id, categoryOrder, mResources.getString(title)); 463 } 464 addIntentOptions(int group, int id, int categoryOrder, ComponentName caller, Intent[] specifics, Intent intent, int flags, MenuItem[] outSpecificItems)465 public int addIntentOptions(int group, int id, int categoryOrder, ComponentName caller, 466 Intent[] specifics, Intent intent, int flags, MenuItem[] outSpecificItems) { 467 PackageManager pm = mContext.getPackageManager(); 468 final List<ResolveInfo> lri = 469 pm.queryIntentActivityOptions(caller, specifics, intent, 0); 470 final int N = lri != null ? lri.size() : 0; 471 472 if ((flags & FLAG_APPEND_TO_GROUP) == 0) { 473 removeGroup(group); 474 } 475 476 for (int i=0; i<N; i++) { 477 final ResolveInfo ri = lri.get(i); 478 Intent rintent = new Intent( 479 ri.specificIndex < 0 ? intent : specifics[ri.specificIndex]); 480 rintent.setComponent(new ComponentName( 481 ri.activityInfo.applicationInfo.packageName, 482 ri.activityInfo.name)); 483 final MenuItem item = add(group, id, categoryOrder, ri.loadLabel(pm)) 484 .setIcon(ri.loadIcon(pm)) 485 .setIntent(rintent); 486 if (outSpecificItems != null && ri.specificIndex >= 0) { 487 outSpecificItems[ri.specificIndex] = item; 488 } 489 } 490 491 return N; 492 } 493 removeItem(int id)494 public void removeItem(int id) { 495 removeItemAtInt(findItemIndex(id), true); 496 } 497 removeGroup(int group)498 public void removeGroup(int group) { 499 final int i = findGroupIndex(group); 500 501 if (i >= 0) { 502 final int maxRemovable = mItems.size() - i; 503 int numRemoved = 0; 504 while ((numRemoved++ < maxRemovable) && (mItems.get(i).getGroupId() == group)) { 505 // Don't force update for each one, this method will do it at the end 506 removeItemAtInt(i, false); 507 } 508 509 // Notify menu views 510 onItemsChanged(true); 511 } 512 } 513 514 /** 515 * Remove the item at the given index and optionally forces menu views to 516 * update. 517 * 518 * @param index The index of the item to be removed. If this index is 519 * invalid an exception is thrown. 520 * @param updateChildrenOnMenuViews Whether to force update on menu views. 521 * Please make sure you eventually call this after your batch of 522 * removals. 523 */ removeItemAtInt(int index, boolean updateChildrenOnMenuViews)524 private void removeItemAtInt(int index, boolean updateChildrenOnMenuViews) { 525 if ((index < 0) || (index >= mItems.size())) return; 526 527 mItems.remove(index); 528 529 if (updateChildrenOnMenuViews) onItemsChanged(true); 530 } 531 removeItemAt(int index)532 public void removeItemAt(int index) { 533 removeItemAtInt(index, true); 534 } 535 clearAll()536 public void clearAll() { 537 mPreventDispatchingItemsChanged = true; 538 clear(); 539 clearHeader(); 540 mPreventDispatchingItemsChanged = false; 541 mItemsChangedWhileDispatchPrevented = false; 542 onItemsChanged(true); 543 } 544 clear()545 public void clear() { 546 if (mExpandedItem != null) { 547 collapseItemActionView(mExpandedItem); 548 } 549 mItems.clear(); 550 551 onItemsChanged(true); 552 } 553 setExclusiveItemChecked(MenuItem item)554 void setExclusiveItemChecked(MenuItem item) { 555 final int group = item.getGroupId(); 556 557 final int N = mItems.size(); 558 for (int i = 0; i < N; i++) { 559 MenuItemImpl curItem = mItems.get(i); 560 if (curItem.getGroupId() == group) { 561 if (!curItem.isExclusiveCheckable()) continue; 562 if (!curItem.isCheckable()) continue; 563 564 // Check the item meant to be checked, uncheck the others (that are in the group) 565 curItem.setCheckedInt(curItem == item); 566 } 567 } 568 } 569 setGroupCheckable(int group, boolean checkable, boolean exclusive)570 public void setGroupCheckable(int group, boolean checkable, boolean exclusive) { 571 final int N = mItems.size(); 572 573 for (int i = 0; i < N; i++) { 574 MenuItemImpl item = mItems.get(i); 575 if (item.getGroupId() == group) { 576 item.setExclusiveCheckable(exclusive); 577 item.setCheckable(checkable); 578 } 579 } 580 } 581 setGroupVisible(int group, boolean visible)582 public void setGroupVisible(int group, boolean visible) { 583 final int N = mItems.size(); 584 585 // We handle the notification of items being changed ourselves, so we use setVisibleInt rather 586 // than setVisible and at the end notify of items being changed 587 588 boolean changedAtLeastOneItem = false; 589 for (int i = 0; i < N; i++) { 590 MenuItemImpl item = mItems.get(i); 591 if (item.getGroupId() == group) { 592 if (item.setVisibleInt(visible)) changedAtLeastOneItem = true; 593 } 594 } 595 596 if (changedAtLeastOneItem) onItemsChanged(true); 597 } 598 setGroupEnabled(int group, boolean enabled)599 public void setGroupEnabled(int group, boolean enabled) { 600 final int N = mItems.size(); 601 602 for (int i = 0; i < N; i++) { 603 MenuItemImpl item = mItems.get(i); 604 if (item.getGroupId() == group) { 605 item.setEnabled(enabled); 606 } 607 } 608 } 609 hasVisibleItems()610 public boolean hasVisibleItems() { 611 final int size = size(); 612 613 for (int i = 0; i < size; i++) { 614 MenuItemImpl item = mItems.get(i); 615 if (item.isVisible()) { 616 return true; 617 } 618 } 619 620 return false; 621 } 622 findItem(int id)623 public MenuItem findItem(int id) { 624 final int size = size(); 625 for (int i = 0; i < size; i++) { 626 MenuItemImpl item = mItems.get(i); 627 if (item.getItemId() == id) { 628 return item; 629 } else if (item.hasSubMenu()) { 630 MenuItem possibleItem = item.getSubMenu().findItem(id); 631 632 if (possibleItem != null) { 633 return possibleItem; 634 } 635 } 636 } 637 638 return null; 639 } 640 findItemIndex(int id)641 public int findItemIndex(int id) { 642 final int size = size(); 643 644 for (int i = 0; i < size; i++) { 645 MenuItemImpl item = mItems.get(i); 646 if (item.getItemId() == id) { 647 return i; 648 } 649 } 650 651 return -1; 652 } 653 findGroupIndex(int group)654 public int findGroupIndex(int group) { 655 return findGroupIndex(group, 0); 656 } 657 findGroupIndex(int group, int start)658 public int findGroupIndex(int group, int start) { 659 final int size = size(); 660 661 if (start < 0) { 662 start = 0; 663 } 664 665 for (int i = start; i < size; i++) { 666 final MenuItemImpl item = mItems.get(i); 667 668 if (item.getGroupId() == group) { 669 return i; 670 } 671 } 672 673 return -1; 674 } 675 size()676 public int size() { 677 return mItems.size(); 678 } 679 680 /** {@inheritDoc} */ getItem(int index)681 public MenuItem getItem(int index) { 682 return mItems.get(index); 683 } 684 isShortcutKey(int keyCode, KeyEvent event)685 public boolean isShortcutKey(int keyCode, KeyEvent event) { 686 return findItemWithShortcutForKey(keyCode, event) != null; 687 } 688 setQwertyMode(boolean isQwerty)689 public void setQwertyMode(boolean isQwerty) { 690 mQwertyMode = isQwerty; 691 692 onItemsChanged(false); 693 } 694 695 /** 696 * Returns the ordering across all items. This will grab the category from 697 * the upper bits, find out how to order the category with respect to other 698 * categories, and combine it with the lower bits. 699 * 700 * @param categoryOrder The category order for a particular item (if it has 701 * not been or/add with a category, the default category is 702 * assumed). 703 * @return An ordering integer that can be used to order this item across 704 * all the items (even from other categories). 705 */ getOrdering(int categoryOrder)706 private static int getOrdering(int categoryOrder) { 707 final int index = (categoryOrder & CATEGORY_MASK) >> CATEGORY_SHIFT; 708 709 if (index < 0 || index >= sCategoryToOrder.length) { 710 throw new IllegalArgumentException("order does not contain a valid category."); 711 } 712 713 return (sCategoryToOrder[index] << CATEGORY_SHIFT) | (categoryOrder & USER_MASK); 714 } 715 716 /** 717 * @return whether the menu shortcuts are in qwerty mode or not 718 */ isQwertyMode()719 boolean isQwertyMode() { 720 return mQwertyMode; 721 } 722 723 /** 724 * Sets whether the shortcuts should be visible on menus. Devices without hardware 725 * key input will never make shortcuts visible even if this method is passed 'true'. 726 * 727 * @param shortcutsVisible Whether shortcuts should be visible (if true and a 728 * menu item does not have a shortcut defined, that item will 729 * still NOT show a shortcut) 730 */ setShortcutsVisible(boolean shortcutsVisible)731 public void setShortcutsVisible(boolean shortcutsVisible) { 732 if (mShortcutsVisible == shortcutsVisible) return; 733 734 setShortcutsVisibleInner(shortcutsVisible); 735 onItemsChanged(false); 736 } 737 setShortcutsVisibleInner(boolean shortcutsVisible)738 private void setShortcutsVisibleInner(boolean shortcutsVisible) { 739 mShortcutsVisible = shortcutsVisible 740 && mResources.getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS 741 && mResources.getBoolean( 742 com.android.internal.R.bool.config_showMenuShortcutsWhenKeyboardPresent); 743 } 744 745 /** 746 * @return Whether shortcuts should be visible on menus. 747 */ isShortcutsVisible()748 public boolean isShortcutsVisible() { 749 return mShortcutsVisible; 750 } 751 getResources()752 Resources getResources() { 753 return mResources; 754 } 755 getContext()756 public Context getContext() { 757 return mContext; 758 } 759 dispatchMenuItemSelected(MenuBuilder menu, MenuItem item)760 boolean dispatchMenuItemSelected(MenuBuilder menu, MenuItem item) { 761 return mCallback != null && mCallback.onMenuItemSelected(menu, item); 762 } 763 764 /** 765 * Dispatch a mode change event to this menu's callback. 766 */ changeMenuMode()767 public void changeMenuMode() { 768 if (mCallback != null) { 769 mCallback.onMenuModeChange(this); 770 } 771 } 772 findInsertIndex(ArrayList<MenuItemImpl> items, int ordering)773 private static int findInsertIndex(ArrayList<MenuItemImpl> items, int ordering) { 774 for (int i = items.size() - 1; i >= 0; i--) { 775 MenuItemImpl item = items.get(i); 776 if (item.getOrdering() <= ordering) { 777 return i + 1; 778 } 779 } 780 781 return 0; 782 } 783 performShortcut(int keyCode, KeyEvent event, int flags)784 public boolean performShortcut(int keyCode, KeyEvent event, int flags) { 785 final MenuItemImpl item = findItemWithShortcutForKey(keyCode, event); 786 787 boolean handled = false; 788 789 if (item != null) { 790 handled = performItemAction(item, flags); 791 } 792 793 if ((flags & FLAG_ALWAYS_PERFORM_CLOSE) != 0) { 794 close(true /* closeAllMenus */); 795 } 796 797 return handled; 798 } 799 800 /* 801 * This function will return all the menu and sub-menu items that can 802 * be directly (the shortcut directly corresponds) and indirectly 803 * (the ALT-enabled char corresponds to the shortcut) associated 804 * with the keyCode. 805 */ findItemsWithShortcutForKey(List<MenuItemImpl> items, int keyCode, KeyEvent event)806 void findItemsWithShortcutForKey(List<MenuItemImpl> items, int keyCode, KeyEvent event) { 807 final boolean qwerty = isQwertyMode(); 808 final int modifierState = event.getModifiers(); 809 final KeyCharacterMap.KeyData possibleChars = new KeyCharacterMap.KeyData(); 810 // Get the chars associated with the keyCode (i.e using any chording combo) 811 final boolean isKeyCodeMapped = event.getKeyData(possibleChars); 812 // The delete key is not mapped to '\b' so we treat it specially 813 if (!isKeyCodeMapped && (keyCode != KeyEvent.KEYCODE_DEL)) { 814 return; 815 } 816 817 // Look for an item whose shortcut is this key. 818 final int N = mItems.size(); 819 for (int i = 0; i < N; i++) { 820 MenuItemImpl item = mItems.get(i); 821 if (item.hasSubMenu()) { 822 ((MenuBuilder)item.getSubMenu()).findItemsWithShortcutForKey(items, keyCode, event); 823 } 824 final char shortcutChar = 825 qwerty ? item.getAlphabeticShortcut() : item.getNumericShortcut(); 826 final int shortcutModifiers = 827 qwerty ? item.getAlphabeticModifiers() : item.getNumericModifiers(); 828 final boolean isModifiersExactMatch = (modifierState & SUPPORTED_MODIFIERS_MASK) 829 == (shortcutModifiers & SUPPORTED_MODIFIERS_MASK); 830 if (isModifiersExactMatch && (shortcutChar != 0) && 831 (shortcutChar == possibleChars.meta[0] 832 || shortcutChar == possibleChars.meta[2] 833 || (qwerty && shortcutChar == '\b' && 834 keyCode == KeyEvent.KEYCODE_DEL)) && 835 item.isEnabled()) { 836 items.add(item); 837 } 838 } 839 } 840 841 /* 842 * We want to return the menu item associated with the key, but if there is no 843 * ambiguity (i.e. there is only one menu item corresponding to the key) we want 844 * to return it even if it's not an exact match; this allow the user to 845 * _not_ use the ALT key for example, making the use of shortcuts slightly more 846 * user-friendly. An example is on the G1, '!' and '1' are on the same key, and 847 * in Gmail, Menu+1 will trigger Menu+! (the actual shortcut). 848 * 849 * On the other hand, if two (or more) shortcuts corresponds to the same key, 850 * we have to only return the exact match. 851 */ findItemWithShortcutForKey(int keyCode, KeyEvent event)852 MenuItemImpl findItemWithShortcutForKey(int keyCode, KeyEvent event) { 853 // Get all items that can be associated directly or indirectly with the keyCode 854 ArrayList<MenuItemImpl> items = mTempShortcutItemList; 855 items.clear(); 856 findItemsWithShortcutForKey(items, keyCode, event); 857 858 if (items.isEmpty()) { 859 return null; 860 } 861 862 final int metaState = event.getMetaState(); 863 final KeyCharacterMap.KeyData possibleChars = new KeyCharacterMap.KeyData(); 864 // Get the chars associated with the keyCode (i.e using any chording combo) 865 event.getKeyData(possibleChars); 866 867 // If we have only one element, we can safely returns it 868 final int size = items.size(); 869 if (size == 1) { 870 return items.get(0); 871 } 872 873 final boolean qwerty = isQwertyMode(); 874 // If we found more than one item associated with the key, 875 // we have to return the exact match 876 for (int i = 0; i < size; i++) { 877 final MenuItemImpl item = items.get(i); 878 final char shortcutChar = qwerty ? item.getAlphabeticShortcut() : 879 item.getNumericShortcut(); 880 if ((shortcutChar == possibleChars.meta[0] && 881 (metaState & KeyEvent.META_ALT_ON) == 0) 882 || (shortcutChar == possibleChars.meta[2] && 883 (metaState & KeyEvent.META_ALT_ON) != 0) 884 || (qwerty && shortcutChar == '\b' && 885 keyCode == KeyEvent.KEYCODE_DEL)) { 886 return item; 887 } 888 } 889 return null; 890 } 891 performIdentifierAction(int id, int flags)892 public boolean performIdentifierAction(int id, int flags) { 893 // Look for an item whose identifier is the id. 894 return performItemAction(findItem(id), flags); 895 } 896 performItemAction(MenuItem item, int flags)897 public boolean performItemAction(MenuItem item, int flags) { 898 return performItemAction(item, null, flags); 899 } 900 performItemAction(MenuItem item, MenuPresenter preferredPresenter, int flags)901 public boolean performItemAction(MenuItem item, MenuPresenter preferredPresenter, int flags) { 902 MenuItemImpl itemImpl = (MenuItemImpl) item; 903 904 if (itemImpl == null || !itemImpl.isEnabled()) { 905 return false; 906 } 907 908 boolean invoked = itemImpl.invoke(); 909 910 final ActionProvider provider = item.getActionProvider(); 911 final boolean providerHasSubMenu = provider != null && provider.hasSubMenu(); 912 if (itemImpl.hasCollapsibleActionView()) { 913 invoked |= itemImpl.expandActionView(); 914 if (invoked) { 915 close(true /* closeAllMenus */); 916 } 917 } else if (itemImpl.hasSubMenu() || providerHasSubMenu) { 918 if (!itemImpl.hasSubMenu()) { 919 itemImpl.setSubMenu(new SubMenuBuilder(getContext(), this, itemImpl)); 920 } 921 922 final SubMenuBuilder subMenu = (SubMenuBuilder) itemImpl.getSubMenu(); 923 if (providerHasSubMenu) { 924 provider.onPrepareSubMenu(subMenu); 925 } 926 invoked |= dispatchSubMenuSelected(subMenu, preferredPresenter); 927 if (!invoked) { 928 close(true /* closeAllMenus */); 929 } 930 } else { 931 if ((flags & FLAG_PERFORM_NO_CLOSE) == 0) { 932 close(true /* closeAllMenus */); 933 } 934 } 935 936 return invoked; 937 } 938 939 /** 940 * Closes the menu. 941 * 942 * @param closeAllMenus {@code true} if all displayed menus and submenus 943 * should be completely closed (as when a menu item is 944 * selected) or {@code false} if only this menu should 945 * be closed 946 */ close(boolean closeAllMenus)947 public final void close(boolean closeAllMenus) { 948 if (mIsClosing) return; 949 950 mIsClosing = true; 951 for (WeakReference<MenuPresenter> ref : mPresenters) { 952 final MenuPresenter presenter = ref.get(); 953 if (presenter == null) { 954 mPresenters.remove(ref); 955 } else { 956 presenter.onCloseMenu(this, closeAllMenus); 957 } 958 } 959 mIsClosing = false; 960 } 961 962 /** {@inheritDoc} */ close()963 public void close() { 964 close(true /* closeAllMenus */); 965 } 966 967 /** 968 * Called when an item is added or removed. 969 * 970 * @param structureChanged true if the menu structure changed, 971 * false if only item properties changed. 972 * (Visibility is a structural property since it affects layout.) 973 */ onItemsChanged(boolean structureChanged)974 public void onItemsChanged(boolean structureChanged) { 975 if (!mPreventDispatchingItemsChanged) { 976 if (structureChanged) { 977 mIsVisibleItemsStale = true; 978 mIsActionItemsStale = true; 979 } 980 981 dispatchPresenterUpdate(structureChanged); 982 } else { 983 mItemsChangedWhileDispatchPrevented = true; 984 } 985 } 986 987 /** 988 * Stop dispatching item changed events to presenters until 989 * {@link #startDispatchingItemsChanged()} is called. Useful when 990 * many menu operations are going to be performed as a batch. 991 */ stopDispatchingItemsChanged()992 public void stopDispatchingItemsChanged() { 993 if (!mPreventDispatchingItemsChanged) { 994 mPreventDispatchingItemsChanged = true; 995 mItemsChangedWhileDispatchPrevented = false; 996 } 997 } 998 startDispatchingItemsChanged()999 public void startDispatchingItemsChanged() { 1000 mPreventDispatchingItemsChanged = false; 1001 1002 if (mItemsChangedWhileDispatchPrevented) { 1003 mItemsChangedWhileDispatchPrevented = false; 1004 onItemsChanged(true); 1005 } 1006 } 1007 1008 /** 1009 * Called by {@link MenuItemImpl} when its visible flag is changed. 1010 * @param item The item that has gone through a visibility change. 1011 */ onItemVisibleChanged(MenuItemImpl item)1012 void onItemVisibleChanged(MenuItemImpl item) { 1013 // Notify of items being changed 1014 mIsVisibleItemsStale = true; 1015 onItemsChanged(true); 1016 } 1017 1018 /** 1019 * Called by {@link MenuItemImpl} when its action request status is changed. 1020 * @param item The item that has gone through a change in action request status. 1021 */ onItemActionRequestChanged(MenuItemImpl item)1022 void onItemActionRequestChanged(MenuItemImpl item) { 1023 // Notify of items being changed 1024 mIsActionItemsStale = true; 1025 onItemsChanged(true); 1026 } 1027 1028 @NonNull getVisibleItems()1029 public ArrayList<MenuItemImpl> getVisibleItems() { 1030 if (!mIsVisibleItemsStale) return mVisibleItems; 1031 1032 // Refresh the visible items 1033 mVisibleItems.clear(); 1034 1035 final int itemsSize = mItems.size(); 1036 MenuItemImpl item; 1037 for (int i = 0; i < itemsSize; i++) { 1038 item = mItems.get(i); 1039 if (item.isVisible()) mVisibleItems.add(item); 1040 } 1041 1042 mIsVisibleItemsStale = false; 1043 mIsActionItemsStale = true; 1044 1045 return mVisibleItems; 1046 } 1047 1048 /** 1049 * This method determines which menu items get to be 'action items' that will appear 1050 * in an action bar and which items should be 'overflow items' in a secondary menu. 1051 * The rules are as follows: 1052 * 1053 * <p>Items are considered for inclusion in the order specified within the menu. 1054 * There is a limit of mMaxActionItems as a total count, optionally including the overflow 1055 * menu button itself. This is a soft limit; if an item shares a group ID with an item 1056 * previously included as an action item, the new item will stay with its group and become 1057 * an action item itself even if it breaks the max item count limit. This is done to 1058 * limit the conceptual complexity of the items presented within an action bar. Only a few 1059 * unrelated concepts should be presented to the user in this space, and groups are treated 1060 * as a single concept. 1061 * 1062 * <p>There is also a hard limit of consumed measurable space: mActionWidthLimit. This 1063 * limit may be broken by a single item that exceeds the remaining space, but no further 1064 * items may be added. If an item that is part of a group cannot fit within the remaining 1065 * measured width, the entire group will be demoted to overflow. This is done to ensure room 1066 * for navigation and other affordances in the action bar as well as reduce general UI clutter. 1067 * 1068 * <p>The space freed by demoting a full group cannot be consumed by future menu items. 1069 * Once items begin to overflow, all future items become overflow items as well. This is 1070 * to avoid inadvertent reordering that may break the app's intended design. 1071 */ flagActionItems()1072 public void flagActionItems() { 1073 // Important side effect: if getVisibleItems is stale it may refresh, 1074 // which can affect action items staleness. 1075 final ArrayList<MenuItemImpl> visibleItems = getVisibleItems(); 1076 1077 if (!mIsActionItemsStale) { 1078 return; 1079 } 1080 1081 // Presenters flag action items as needed. 1082 boolean flagged = false; 1083 for (WeakReference<MenuPresenter> ref : mPresenters) { 1084 final MenuPresenter presenter = ref.get(); 1085 if (presenter == null) { 1086 mPresenters.remove(ref); 1087 } else { 1088 flagged |= presenter.flagActionItems(); 1089 } 1090 } 1091 1092 if (flagged) { 1093 mActionItems.clear(); 1094 mNonActionItems.clear(); 1095 final int itemsSize = visibleItems.size(); 1096 for (int i = 0; i < itemsSize; i++) { 1097 MenuItemImpl item = visibleItems.get(i); 1098 if (item.isActionButton()) { 1099 mActionItems.add(item); 1100 } else { 1101 mNonActionItems.add(item); 1102 } 1103 } 1104 } else { 1105 // Nobody flagged anything, everything is a non-action item. 1106 // (This happens during a first pass with no action-item presenters.) 1107 mActionItems.clear(); 1108 mNonActionItems.clear(); 1109 mNonActionItems.addAll(getVisibleItems()); 1110 } 1111 mIsActionItemsStale = false; 1112 } 1113 getActionItems()1114 public ArrayList<MenuItemImpl> getActionItems() { 1115 flagActionItems(); 1116 return mActionItems; 1117 } 1118 getNonActionItems()1119 public ArrayList<MenuItemImpl> getNonActionItems() { 1120 flagActionItems(); 1121 return mNonActionItems; 1122 } 1123 clearHeader()1124 public void clearHeader() { 1125 mHeaderIcon = null; 1126 mHeaderTitle = null; 1127 mHeaderView = null; 1128 1129 onItemsChanged(false); 1130 } 1131 setHeaderInternal(final int titleRes, final CharSequence title, final int iconRes, final Drawable icon, final View view)1132 private void setHeaderInternal(final int titleRes, final CharSequence title, final int iconRes, 1133 final Drawable icon, final View view) { 1134 final Resources r = getResources(); 1135 1136 if (view != null) { 1137 mHeaderView = view; 1138 1139 // If using a custom view, then the title and icon aren't used 1140 mHeaderTitle = null; 1141 mHeaderIcon = null; 1142 } else { 1143 if (titleRes > 0) { 1144 mHeaderTitle = r.getText(titleRes); 1145 } else if (title != null) { 1146 mHeaderTitle = title; 1147 } 1148 1149 if (iconRes > 0) { 1150 mHeaderIcon = getContext().getDrawable(iconRes); 1151 } else if (icon != null) { 1152 mHeaderIcon = icon; 1153 } 1154 1155 // If using the title or icon, then a custom view isn't used 1156 mHeaderView = null; 1157 } 1158 1159 // Notify of change 1160 onItemsChanged(false); 1161 } 1162 1163 /** 1164 * Sets the header's title. This replaces the header view. Called by the 1165 * builder-style methods of subclasses. 1166 * 1167 * @param title The new title. 1168 * @return This MenuBuilder so additional setters can be called. 1169 */ setHeaderTitleInt(CharSequence title)1170 protected MenuBuilder setHeaderTitleInt(CharSequence title) { 1171 setHeaderInternal(0, title, 0, null, null); 1172 return this; 1173 } 1174 1175 /** 1176 * Sets the header's title. This replaces the header view. Called by the 1177 * builder-style methods of subclasses. 1178 * 1179 * @param titleRes The new title (as a resource ID). 1180 * @return This MenuBuilder so additional setters can be called. 1181 */ setHeaderTitleInt(int titleRes)1182 protected MenuBuilder setHeaderTitleInt(int titleRes) { 1183 setHeaderInternal(titleRes, null, 0, null, null); 1184 return this; 1185 } 1186 1187 /** 1188 * Sets the header's icon. This replaces the header view. Called by the 1189 * builder-style methods of subclasses. 1190 * 1191 * @param icon The new icon. 1192 * @return This MenuBuilder so additional setters can be called. 1193 */ setHeaderIconInt(Drawable icon)1194 protected MenuBuilder setHeaderIconInt(Drawable icon) { 1195 setHeaderInternal(0, null, 0, icon, null); 1196 return this; 1197 } 1198 1199 /** 1200 * Sets the header's icon. This replaces the header view. Called by the 1201 * builder-style methods of subclasses. 1202 * 1203 * @param iconRes The new icon (as a resource ID). 1204 * @return This MenuBuilder so additional setters can be called. 1205 */ setHeaderIconInt(int iconRes)1206 protected MenuBuilder setHeaderIconInt(int iconRes) { 1207 setHeaderInternal(0, null, iconRes, null, null); 1208 return this; 1209 } 1210 1211 /** 1212 * Sets the header's view. This replaces the title and icon. Called by the 1213 * builder-style methods of subclasses. 1214 * 1215 * @param view The new view. 1216 * @return This MenuBuilder so additional setters can be called. 1217 */ setHeaderViewInt(View view)1218 protected MenuBuilder setHeaderViewInt(View view) { 1219 setHeaderInternal(0, null, 0, null, view); 1220 return this; 1221 } 1222 getHeaderTitle()1223 public CharSequence getHeaderTitle() { 1224 return mHeaderTitle; 1225 } 1226 getHeaderIcon()1227 public Drawable getHeaderIcon() { 1228 return mHeaderIcon; 1229 } 1230 getHeaderView()1231 public View getHeaderView() { 1232 return mHeaderView; 1233 } 1234 1235 /** 1236 * Gets the root menu (if this is a submenu, find its root menu). 1237 * @return The root menu. 1238 */ getRootMenu()1239 public MenuBuilder getRootMenu() { 1240 return this; 1241 } 1242 1243 /** 1244 * Sets the current menu info that is set on all items added to this menu 1245 * (until this is called again with different menu info, in which case that 1246 * one will be added to all subsequent item additions). 1247 * 1248 * @param menuInfo The extra menu information to add. 1249 */ setCurrentMenuInfo(ContextMenuInfo menuInfo)1250 public void setCurrentMenuInfo(ContextMenuInfo menuInfo) { 1251 mCurrentMenuInfo = menuInfo; 1252 } 1253 setOptionalIconsVisible(boolean visible)1254 void setOptionalIconsVisible(boolean visible) { 1255 mOptionalIconsVisible = visible; 1256 } 1257 getOptionalIconsVisible()1258 boolean getOptionalIconsVisible() { 1259 return mOptionalIconsVisible; 1260 } 1261 expandItemActionView(MenuItemImpl item)1262 public boolean expandItemActionView(MenuItemImpl item) { 1263 if (mPresenters.isEmpty()) return false; 1264 1265 boolean expanded = false; 1266 1267 stopDispatchingItemsChanged(); 1268 for (WeakReference<MenuPresenter> ref : mPresenters) { 1269 final MenuPresenter presenter = ref.get(); 1270 if (presenter == null) { 1271 mPresenters.remove(ref); 1272 } else if ((expanded = presenter.expandItemActionView(this, item))) { 1273 break; 1274 } 1275 } 1276 startDispatchingItemsChanged(); 1277 1278 if (expanded) { 1279 mExpandedItem = item; 1280 } 1281 return expanded; 1282 } 1283 collapseItemActionView(MenuItemImpl item)1284 public boolean collapseItemActionView(MenuItemImpl item) { 1285 if (mPresenters.isEmpty() || mExpandedItem != item) return false; 1286 1287 boolean collapsed = false; 1288 1289 stopDispatchingItemsChanged(); 1290 for (WeakReference<MenuPresenter> ref : mPresenters) { 1291 final MenuPresenter presenter = ref.get(); 1292 if (presenter == null) { 1293 mPresenters.remove(ref); 1294 } else if ((collapsed = presenter.collapseItemActionView(this, item))) { 1295 break; 1296 } 1297 } 1298 startDispatchingItemsChanged(); 1299 1300 if (collapsed) { 1301 mExpandedItem = null; 1302 } 1303 return collapsed; 1304 } 1305 getExpandedItem()1306 public MenuItemImpl getExpandedItem() { 1307 return mExpandedItem; 1308 } 1309 } 1310