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