Ensure only one context menu is shown at a time
Refactors the menu helper classes. Both classes now implement a common
MenuHelper interface, which eliminates the need to keep separate helpers
on PhoneWindow and unifies the DecorView showContextMenuForChild()
implementations.
We now explicitly dismiss any previously shown context menu before showing
a new context menu. Previously we relied on the modal nature of the dialog
context menu to prevent multiple menus from being opened at once, but this
is no longer reliable with popup context menus.
Bug: 25656520
Change-Id: Idab3daa6d6888f803f2e33660fe1dd488e4c28d1
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index ab1943c..a1bd281 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -5505,20 +5505,26 @@
}
/**
- * Bring up the context menu for this view.
+ * Shows the context menu for this view.
*
- * @return Whether a context menu was displayed.
+ * @return {@code true} if the context menu was shown, {@code false}
+ * otherwise
+ * @see #showContextMenu(float, float)
*/
public boolean showContextMenu() {
return getParent().showContextMenuForChild(this);
}
/**
- * Bring up the context menu for this view, referring to the item under the specified point.
+ * Shows the context menu for this view anchored to the specified
+ * view-relative coordinate.
*
- * @param x The referenced x coordinate.
- * @param y The referenced y coordinate.
- * @return Whether a context menu was displayed.
+ * @param x the X coordinate in pixels relative to the view to which the
+ * menu should be anchored
+ * @param y the Y coordinate in pixels relative to the view to which the
+ * menu should be anchored
+ * @return {@code true} if the context menu was shown, {@code false}
+ * otherwise
*/
public boolean showContextMenu(float x, float y) {
return getParent().showContextMenuForChild(this, x, y);
diff --git a/core/java/android/view/ViewParent.java b/core/java/android/view/ViewParent.java
index 6ae448a..f2ab35e 100644
--- a/core/java/android/view/ViewParent.java
+++ b/core/java/android/view/ViewParent.java
@@ -174,26 +174,38 @@
public void focusableViewAvailable(View v);
/**
- * Bring up a context menu for the specified view or its ancestors.
- *
- * <p>In most cases, a subclass does not need to override this. However, if
+ * Shows the context menu for the specified view or its ancestors.
+ * <p>
+ * In most cases, a subclass does not need to override this. However, if
* the subclass is added directly to the window manager (for example,
* {@link ViewManager#addView(View, android.view.ViewGroup.LayoutParams)})
- * then it should override this and show the context menu.</p>
- *
- * @param originalView The source view where the context menu was first invoked
- * @return true if a context menu was displayed
+ * then it should override this and show the context menu.
+ *
+ * @param originalView the source view where the context menu was first
+ * invoked
+ * @return {@code true} if the context menu was shown, {@code false}
+ * otherwise
+ * @see #showContextMenuForChild(View, float, float)
*/
public boolean showContextMenuForChild(View originalView);
/**
- * Bring up a context menu for the specified view at the given x/y offset from
- * the top left corner.
+ * Shows the context menu for the specified view or its ancestors anchored
+ * to the specified view-relative coordinate.
+ * <p>
+ * In most cases, a subclass does not need to override this. However, if
+ * the subclass is added directly to the window manager (for example,
+ * {@link ViewManager#addView(View, android.view.ViewGroup.LayoutParams)})
+ * then it should override this and show the context menu.
*
- * @param originalView
- * @param x The x offset at which to open the menu
- * @param y The y offset at which to open the menu
- * @return true if a context menu was displayed
+ * @param originalView the source view where the context menu was first
+ * invoked
+ * @param x the X coordinate in pixels relative to the original view to
+ * which the menu should be anchored
+ * @param y the Y coordinate in pixels relative to the original view to
+ * which the menu should be anchored
+ * @return {@code true} if the context menu was shown, {@code false}
+ * otherwise
*/
public boolean showContextMenuForChild(View originalView, float x, float y);
diff --git a/core/java/android/widget/ActionMenuPresenter.java b/core/java/android/widget/ActionMenuPresenter.java
index 41f1ce7..0032f17 100644
--- a/core/java/android/widget/ActionMenuPresenter.java
+++ b/core/java/android/widget/ActionMenuPresenter.java
@@ -933,7 +933,7 @@
super(context, menu, anchorView, overflowOnly,
com.android.internal.R.attr.actionOverflowMenuStyle);
setGravity(Gravity.END);
- setCallback(mPopupPresenterCallback);
+ setPresenterCallback(mPopupPresenterCallback);
}
@Override
@@ -956,7 +956,7 @@
setAnchorView(mOverflowButton == null ? (View) mMenuView : mOverflowButton);
}
- setCallback(mPopupPresenterCallback);
+ setPresenterCallback(mPopupPresenterCallback);
}
@Override
diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java
index 27fe03c..a95c401 100644
--- a/core/java/com/android/internal/policy/DecorView.java
+++ b/core/java/com/android/internal/policy/DecorView.java
@@ -21,8 +21,7 @@
import com.android.internal.view.RootViewSurfaceTaker;
import com.android.internal.view.StandaloneActionMode;
import com.android.internal.view.menu.ContextMenuBuilder;
-import com.android.internal.view.menu.MenuDialogHelper;
-import com.android.internal.view.menu.MenuPopupHelper;
+import com.android.internal.view.menu.MenuHelper;
import com.android.internal.widget.ActionBarContextView;
import com.android.internal.widget.BackgroundFallback;
import com.android.internal.widget.DecorCaptionView;
@@ -659,30 +658,23 @@
@Override
public boolean showContextMenuForChild(View originalView) {
- // Reuse the context menu builder
- if (mWindow.mContextMenu == null) {
- mWindow.mContextMenu = new ContextMenuBuilder(getContext());
- mWindow.mContextMenu.setCallback(mWindow.mContextMenuCallback);
- } else {
- mWindow.mContextMenu.clearAll();
- }
-
- final MenuDialogHelper helper = mWindow.mContextMenu.show(originalView,
- originalView.getWindowToken());
- if (helper != null) {
- helper.setPresenterCallback(mWindow.mContextMenuCallback);
- } else if (mWindow.mContextMenuHelper != null) {
- // No menu to show, but if we have a menu currently showing it just became blank.
- // Close it.
- mWindow.mContextMenuHelper.dismiss();
- }
- mWindow.mContextMenuHelper = helper;
- return helper != null;
+ return showContextMenuForChildInternal(originalView, 0, 0, false);
}
@Override
public boolean showContextMenuForChild(View originalView, float x, float y) {
- // Reuse the context menu builder
+ return showContextMenuForChildInternal(originalView, x, y, true);
+ }
+
+ private boolean showContextMenuForChildInternal(View originalView,
+ float x, float y, boolean isPopup) {
+ // Only allow one context menu at a time.
+ if (mWindow.mContextMenuHelper != null) {
+ mWindow.mContextMenuHelper.dismiss();
+ mWindow.mContextMenuHelper = null;
+ }
+
+ // Reuse the context menu builder.
if (mWindow.mContextMenu == null) {
mWindow.mContextMenu = new ContextMenuBuilder(getContext());
mWindow.mContextMenu.setCallback(mWindow.mContextMenuCallback);
@@ -690,16 +682,18 @@
mWindow.mContextMenu.clearAll();
}
- final MenuPopupHelper helper = mWindow.mContextMenu.showPopup(
- getContext(), originalView, x, y);
- if (helper != null) {
- helper.setCallback(mWindow.mContextMenuCallback);
- } else if (mWindow.mContextMenuPopupHelper != null) {
- // No menu to show, but if we have a menu currently showing it just became blank.
- // Close it.
- mWindow.mContextMenuPopupHelper.dismiss();
+ final MenuHelper helper;
+ if (isPopup) {
+ helper = mWindow.mContextMenu.showPopup(getContext(), originalView, x, y);
+ } else {
+ helper = mWindow.mContextMenu.showDialog(originalView, originalView.getWindowToken());
}
- mWindow.mContextMenuPopupHelper = helper;
+
+ if (helper != null) {
+ helper.setPresenterCallback(mWindow.mContextMenuCallback);
+ }
+
+ mWindow.mContextMenuHelper = helper;
return helper != null;
}
diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java
index 6e7e5cf..04f1dc5 100644
--- a/core/java/com/android/internal/policy/PhoneWindow.java
+++ b/core/java/com/android/internal/policy/PhoneWindow.java
@@ -53,7 +53,7 @@
import com.android.internal.view.menu.ListMenuPresenter;
import com.android.internal.view.menu.MenuBuilder;
import com.android.internal.view.menu.MenuDialogHelper;
-import com.android.internal.view.menu.MenuPopupHelper;
+import com.android.internal.view.menu.MenuHelper;
import com.android.internal.view.menu.MenuPresenter;
import com.android.internal.view.menu.MenuView;
import com.android.internal.widget.DecorContentParent;
@@ -232,8 +232,7 @@
private boolean mAlwaysReadCloseOnTouchAttr = false;
ContextMenuBuilder mContextMenu;
- MenuDialogHelper mContextMenuHelper;
- MenuPopupHelper mContextMenuPopupHelper;
+ MenuHelper mContextMenuHelper;
private boolean mClosingActionMenu;
private int mVolumeControlStreamType = AudioManager.USE_DEFAULT_STREAM_TYPE;
@@ -1103,10 +1102,6 @@
mContextMenuHelper.dismiss();
mContextMenuHelper = null;
}
- if (mContextMenuPopupHelper != null) {
- mContextMenuPopupHelper.dismiss();
- mContextMenuPopupHelper = null;
- }
}
@Override
diff --git a/core/java/com/android/internal/view/menu/ContextMenuBuilder.java b/core/java/com/android/internal/view/menu/ContextMenuBuilder.java
index aaa1bf1..82f061c 100644
--- a/core/java/com/android/internal/view/menu/ContextMenuBuilder.java
+++ b/core/java/com/android/internal/view/menu/ContextMenuBuilder.java
@@ -17,7 +17,6 @@
package com.android.internal.view.menu;
import android.content.Context;
-import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.IBinder;
import android.util.EventLog;
@@ -35,7 +34,7 @@
* <p>
* To use this class, instantiate it via {@link #ContextMenuBuilder(Context)},
* and optionally populate it with any of your custom items. Finally,
- * call {@link #show(View, IBinder)} which will populate the menu
+ * call {@link #showDialog(View, IBinder)} which will populate the menu
* with a view's context menu items and show the context menu.
*/
public class ContextMenuBuilder extends MenuBuilder implements ContextMenu {
@@ -75,7 +74,7 @@
* @return If the context menu was shown, the {@link MenuDialogHelper} for
* dismissing it. Otherwise, null.
*/
- public MenuDialogHelper show(View originalView, IBinder token) {
+ public MenuDialogHelper showDialog(View originalView, IBinder token) {
if (originalView != null) {
// Let relevant views and their populate context listeners populate
// the context menu
diff --git a/core/java/com/android/internal/view/menu/MenuDialogHelper.java b/core/java/com/android/internal/view/menu/MenuDialogHelper.java
index b9e0e40..ecab29f 100644
--- a/core/java/com/android/internal/view/menu/MenuDialogHelper.java
+++ b/core/java/com/android/internal/view/menu/MenuDialogHelper.java
@@ -26,13 +26,10 @@
import android.view.WindowManager;
/**
- * Helper for menus that appear as Dialogs (context and submenus).
- *
- * @hide
+ * Presents a menu as a modal dialog.
*/
-public class MenuDialogHelper implements DialogInterface.OnKeyListener,
- DialogInterface.OnClickListener,
- DialogInterface.OnDismissListener,
+public class MenuDialogHelper implements MenuHelper, DialogInterface.OnKeyListener,
+ DialogInterface.OnClickListener, DialogInterface.OnDismissListener,
MenuPresenter.Callback {
private MenuBuilder mMenu;
private AlertDialog mDialog;
@@ -125,6 +122,7 @@
}
+ @Override
public void setPresenterCallback(MenuPresenter.Callback cb) {
mPresenterCallback = cb;
}
@@ -134,6 +132,7 @@
*
* @see Dialog#dismiss()
*/
+ @Override
public void dismiss() {
if (mDialog != null) {
mDialog.dismiss();
diff --git a/core/java/com/android/internal/view/menu/MenuHelper.java b/core/java/com/android/internal/view/menu/MenuHelper.java
new file mode 100644
index 0000000..9534722
--- /dev/null
+++ b/core/java/com/android/internal/view/menu/MenuHelper.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.view.menu;
+
+/**
+ * Interface for a helper capable of presenting a menu.
+ */
+public interface MenuHelper {
+ void setPresenterCallback(MenuPresenter.Callback cb);
+ void dismiss();
+}
diff --git a/core/java/com/android/internal/view/menu/MenuPopupHelper.java b/core/java/com/android/internal/view/menu/MenuPopupHelper.java
index 59d5f94..044ee6e 100644
--- a/core/java/com/android/internal/view/menu/MenuPopupHelper.java
+++ b/core/java/com/android/internal/view/menu/MenuPopupHelper.java
@@ -30,7 +30,7 @@
/**
* Presents a menu as a small, simple popup anchored to another view.
*/
-public class MenuPopupHelper {
+public class MenuPopupHelper implements MenuHelper {
private final Context mContext;
// Immutable cached popup menu properties.
@@ -244,6 +244,7 @@
/**
* Dismisses the popup, if showing.
*/
+ @Override
public void dismiss() {
if (isShowing()) {
mPopup.dismiss();
@@ -270,7 +271,8 @@
return mPopup != null && mPopup.isShowing();
}
- public void setCallback(@Nullable MenuPresenter.Callback cb) {
+ @Override
+ public void setPresenterCallback(@Nullable MenuPresenter.Callback cb) {
mPresenterCallback = cb;
if (mPopup != null) {
mPopup.setCallback(cb);
diff --git a/core/java/com/android/internal/view/menu/StandardMenuPopup.java b/core/java/com/android/internal/view/menu/StandardMenuPopup.java
index caee0d2..6a5f6d8 100644
--- a/core/java/com/android/internal/view/menu/StandardMenuPopup.java
+++ b/core/java/com/android/internal/view/menu/StandardMenuPopup.java
@@ -245,7 +245,7 @@
MenuPopupHelper subPopup = new MenuPopupHelper(
mContext, subMenu, mShownAnchorView, mOverflowOnly, mPopupStyleAttr,
mPopupStyleRes);
- subPopup.setCallback(mPresenterCallback);
+ subPopup.setPresenterCallback(mPresenterCallback);
subPopup.setForceShowIcon(mAdapter.getForceShowIcon());
if (subPopup.tryShow()) {