Chromium Code Reviews| Index: content/public/android/java/src/org/chromium/content/browser/WebActionMode.java |
| diff --git a/content/public/android/java/src/org/chromium/content/browser/WebActionMode.java b/content/public/android/java/src/org/chromium/content/browser/WebActionMode.java |
| index a59b0da2b7c1981509d0693e325558bbd0f940e6..35f214050c0555daef56eceaea2bb88cfd3690b4 100644 |
| --- a/content/public/android/java/src/org/chromium/content/browser/WebActionMode.java |
| +++ b/content/public/android/java/src/org/chromium/content/browser/WebActionMode.java |
| @@ -5,21 +5,74 @@ |
| package org.chromium.content.browser; |
|
boliu
2016/11/01 00:37:36
remove all imports of this class outside of conten
Jinsuk Kim
2016/11/01 04:43:56
Done.
|
| import android.annotation.TargetApi; |
| +import android.app.Activity; |
| +import android.app.SearchManager; |
| +import android.content.ClipboardManager; |
| +import android.content.ContentResolver; |
| +import android.content.Context; |
| +import android.content.Intent; |
| +import android.content.pm.PackageManager; |
| +import android.content.pm.ResolveInfo; |
| +import android.content.res.Resources; |
| +import android.graphics.Rect; |
| import android.os.Build; |
| +import android.provider.Browser; |
| +import android.text.TextUtils; |
| +import android.util.SparseBooleanArray; |
| import android.view.ActionMode; |
| +import android.view.Menu; |
| +import android.view.MenuInflater; |
| +import android.view.MenuItem; |
| import android.view.View; |
| import android.view.ViewConfiguration; |
| +import android.view.WindowManager; |
| import org.chromium.base.Log; |
| +import org.chromium.base.VisibleForTesting; |
| +import org.chromium.base.metrics.RecordUserAction; |
| +import org.chromium.content.R; |
| +import org.chromium.content.browser.input.FloatingPastePopupMenu; |
| +import org.chromium.content.browser.input.ImeAdapter; |
| +import org.chromium.content.browser.input.LGEmailActionModeWorkaround; |
| +import org.chromium.content.browser.input.LegacyPastePopupMenu; |
| +import org.chromium.content.browser.input.PastePopupMenu; |
| +import org.chromium.content.browser.input.PastePopupMenu.PastePopupMenuDelegate; |
| +import org.chromium.content_public.browser.WebContents; |
| +import org.chromium.ui.base.DeviceFormFactor; |
| +import org.chromium.ui.base.WindowAndroid; |
| +import org.chromium.ui.touch_selection.SelectionEventType; |
| + |
| +import java.util.List; |
| /** |
| * An ActionMode for in-page web content selection. This class wraps an ActionMode created |
| * by the associated View, providing modified interaction with that ActionMode. |
| + * |
| + * Embedders use {@link WebActionModeDelegate}, a delegate of this class, to build |
| + * {@link ActionMode.Callback} instance to configure the selection action mode tasks to |
| + * their requirements. |
| */ |
| @TargetApi(Build.VERSION_CODES.M) |
| -public class WebActionMode { |
| +public class WebActionMode implements ActionModeCallbackHelper { |
| private static final String TAG = "cr.WebActionMode"; |
| + /** Google search doesn't support requests slightly larger than this. */ |
| + public static final int MAX_SEARCH_QUERY_LENGTH = 1000; |
|
boliu
2016/11/01 00:37:36
this should be in the interface (I think it may ne
Jinsuk Kim
2016/11/01 04:43:56
Done.
|
| + |
| + /** |
| + * Android Intent size limitations prevent sending over a megabyte of data. Limit |
| + * query lengths to 100kB because other things may be added to the Intent. |
| + */ |
| + public static final int MAX_SHARE_QUERY_LENGTH = 100000; |
|
boliu
2016/11/01 00:37:36
private
Jinsuk Kim
2016/11/01 04:43:56
Done.
|
| + |
| + // TODO(hush): Use these constants from android.webkit.WebSettings, when they are made |
| + // available. crbug.com/546762. |
| + public static final int MENU_ITEM_SHARE = 1 << 0; |
|
boliu
2016/11/01 00:37:36
all private here
Jinsuk Kim
2016/11/01 04:43:56
Moved to the abstract class ActionModeCallbackHelp
|
| + public static final int MENU_ITEM_WEB_SEARCH = 1 << 1; |
| + public static final int MENU_ITEM_PROCESS_TEXT = 1 << 2; |
| + |
| + public static final EmptyActionCallback EMPTY_CALLBACK = new EmptyActionCallback(); |
| + |
| // Default delay for reshowing the {@link ActionMode} after it has been |
| // hidden. This avoids flickering issues if there are trailing rect |
| // invalidations after the ActionMode is shown. For example, after the user |
| @@ -28,25 +81,75 @@ public class WebActionMode { |
| // most such trailing, async delays. |
| private static final int SHOW_DELAY_MS = 300; |
| - protected final ActionMode mActionMode; |
| - private final View mView; |
| - private boolean mHidden; |
| - private boolean mPendingInvalidateContentRect; |
| + // Creation failure event can be shared among WebActionMode instances as it depends on |
| + // underlying Android platform version. |
| + private static boolean sFloatingActionModeCreationFailed; |
|
boliu
2016/11/01 00:37:36
this was not static before, any reason why was mad
Jinsuk Kim
2016/11/01 04:43:56
Previously this flag was being set for every CVC.
boliu
2016/11/01 22:24:32
Did you test that? What if failure is due to some
Jinsuk Kim
2016/11/02 03:48:43
Done. Please note that the flag is already shared
|
| + |
| + private final Context mContext; |
| + private final WindowAndroid mWindowAndroid; |
| + private final WebContents mWebContents; |
| + private final ActionMode.Callback mCallback; |
| + private final RenderCoordinates mRenderCoordinates; |
| + |
| + // Selection rectangle in DIP. |
| + private final Rect mSelectionRect = new Rect(); |
| // Self-repeating task that repeatedly hides the ActionMode. This is |
| // required because ActionMode only exposes a temporary hide routine. |
| private final Runnable mRepeatingHideRunnable; |
| + private View mView; |
| + private ActionMode mActionMode; |
| + private boolean mDraggingSelection; |
| + |
| + // Boolean array with mappings from menu item to a flag indicating it is allowed. |
| + // The menu items are allowed by default if they not contained in the array. |
| + private SparseBooleanArray mAllowedMenuItems; |
| + private boolean mMenuDefaultAllowed; |
| + |
| + private boolean mHidden; |
| + private boolean mPendingInvalidateContentRect; |
| + |
| + private boolean mEditable; |
| + private boolean mIsPasswordType; |
| + private boolean mIsInsertion; |
| + |
| + // Indicates whether the action mode needs to be redrawn since last invalidation. |
| + private boolean mNeedsPrepare; |
| + |
| + private boolean mUnselectAllOnDismiss; |
| + private String mLastSelectedText; |
| + |
| + // Tracks whether a selection is currently active. When applied to selected text, indicates |
| + // whether the last selected text is still highlighted. |
| + private boolean mHasSelection; |
| + |
| + // Lazily created paste popup menu, triggered either via long press in an |
| + // editable region or from tapping the insertion handle. |
| + private PastePopupMenu mPastePopupMenu; |
| + private boolean mWasPastePopupShowingOnInsertionDragStart; |
| + |
| + // The client that implements Contextual Search functionality, or null if none exists. |
| + private ContextualSearchClient mContextualSearchClient; |
| + |
| /** |
| - * Constructs a SelectActionMode instance wrapping a concrete ActionMode. |
| - * @param actionMode the wrapped ActionMode. |
| - * @param view the associated View. |
| + * Create {@link WebActionMode} instance. |
| + * @param context Context for action mode. |
| + * @param window WindowAndroid instance. |
| + * @param webContents WebContents instance. |
| + * @param view Container view. |
| + * @param renderCoordinates Coordinates info used to position elements. |
| + * @param callback ActionMode.Callback handling the callbacks from action mode. |
| */ |
| - public WebActionMode(ActionMode actionMode, View view) { |
| - assert actionMode != null; |
| - assert view != null; |
| - mActionMode = actionMode; |
| + public WebActionMode(Context context, WindowAndroid window, WebContents webContents, |
| + View view, RenderCoordinates renderCoordinates, ActionMode.Callback callback) { |
| + mContext = context; |
| + mWindowAndroid = window; |
| + mWebContents = webContents; |
| mView = view; |
| + mRenderCoordinates = renderCoordinates; |
| + mCallback = callback; |
| + mMenuDefaultAllowed = true; |
| mRepeatingHideRunnable = new Runnable() { |
| @Override |
| public void run() { |
| @@ -60,10 +163,166 @@ public class WebActionMode { |
| } |
| /** |
| + * Update the container view. |
| + */ |
| + void setContainerView(View view) { |
| + assert view != null; |
| + |
| + // Cleans up action mode before switching to a new container view. |
| + if (isActionModeValid()) finishActionMode(); |
| + mUnselectAllOnDismiss = true; |
| + destroyPastePopup(); |
| + |
| + mView = view; |
| + } |
| + |
| + @Override |
| + public boolean isActionModeValid() { |
| + return mActionMode != null; |
| + } |
| + |
| + // True if action mode is not yet initialized or set to no-op mode. |
| + private boolean isEmpty() { |
| + return mCallback == EMPTY_CALLBACK; |
| + } |
| + |
| + @Override |
| + public void setAllowedMenuItems(SparseBooleanArray allowedMenuItems, boolean defaultValue) { |
| + mAllowedMenuItems = allowedMenuItems; |
| + mMenuDefaultAllowed = defaultValue; |
| + } |
| + |
| + /** |
| + * Show (activate) android action mode by starting it. |
| + * |
| + * <p>Action mode in floating mode is tried first, and then falls back to |
| + * a normal one if allowed. |
| + * @param allowFallback A flag indicating if we allow for falling back to |
| + * normal action mode in case floating action mode creation fails. |
| + * @return {@code true} if the action mode was started; {@code false} otherwise due to |
| + * the condition not being met. |
| + */ |
| + public boolean showActionMode(boolean allowFallback) { |
| + if (isEmpty()) return false; |
| + |
| + // Just refreshes the view if it is already showing. |
| + if (isActionModeValid()) { |
| + invalidateActionMode(); |
| + return false; |
|
boliu
2016/11/01 00:37:36
this should return true?
Jinsuk Kim
2016/11/01 04:43:56
CVC.showSelectActionMode() uses this value to dete
boliu
2016/11/01 22:24:32
You sure? This is meant to return whether showActi
Jinsuk Kim
2016/11/02 03:48:43
This is what was being done before change:
|
| + } |
| + |
| + // On ICS, startActionMode throws an NPE when getParent() is null. |
| + ActionMode actionMode = null; |
| + if (mView.getParent() != null) { |
| + assert mWebContents != null; |
| + if (supportsFloatingActionMode()) actionMode = startFloatingActionMode(); |
| + if (actionMode == null && allowFallback) actionMode = mView.startActionMode(mCallback); |
| + } |
| + if (actionMode != null) { |
| + // This is to work around an LGE email issue. See crbug.com/651706 for more details. |
| + LGEmailActionModeWorkaround.runIfNecessary(mContext, actionMode); |
| + } |
| + mActionMode = actionMode; |
| + return true; |
| + } |
| + |
| + /** |
| + * Tell if the platform supports floating type action mode. Used not to repeatedly |
| + * attempt the creation if the request fails once at the beginning. Also check |
| + * platform version since the floating type is supported only on M or later version |
| + * of Android platform. |
| + */ |
| + public static boolean supportsFloatingActionMode() { |
| + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return false; |
| + return !sFloatingActionModeCreationFailed; |
| + } |
| + |
| + private static void setFloatingActionModeCreationFailed() { |
| + sFloatingActionModeCreationFailed = true; |
| + } |
| + |
| + @TargetApi(Build.VERSION_CODES.M) |
| + private ActionMode startFloatingActionMode() { |
| + ActionMode actionMode = mView.startActionMode( |
| + new FloatingWebActionModeCallback(this, mCallback), ActionMode.TYPE_FLOATING); |
| + if (actionMode == null) setFloatingActionModeCreationFailed(); |
| + return actionMode; |
| + } |
| + |
| + void showPastePopup(int x, int y) { |
| + if (mView.getParent() == null || mView.getVisibility() != View.VISIBLE) { |
| + return; |
| + } |
| + |
| + if (!isInsertion() || (!supportsFloatingActionMode() && !canPaste())) return; |
| + |
| + PastePopupMenu pastePopupMenu = getPastePopup(); |
| + if (pastePopupMenu == null) return; |
| + |
| + // Coordinates are in DIP. |
| + final float deviceScale = mRenderCoordinates.getDeviceScaleFactor(); |
| + final int xPix = (int) (x * deviceScale); |
| + final int yPix = (int) (y * deviceScale); |
| + final float topControlsShownPix = mRenderCoordinates.getContentOffsetYPix(); |
|
boliu
2016/11/01 00:37:36
this variable this got renamed to browserControlsS
Jinsuk Kim
2016/11/01 04:43:56
Thanks. rebased.
|
| + try { |
| + pastePopupMenu.show(xPix, (int) (yPix + topControlsShownPix)); |
| + } catch (WindowManager.BadTokenException e) { |
| + } |
| + } |
| + |
| + void hidePastePopup() { |
| + if (mPastePopupMenu != null) mPastePopupMenu.hide(); |
| + } |
| + |
| + private PastePopupMenu getPastePopup() { |
| + if (mPastePopupMenu == null) { |
| + PastePopupMenuDelegate delegate = new PastePopupMenuDelegate() { |
| + @Override |
| + public void paste() { |
| + mWebContents.paste(); |
| + mWebContents.dismissTextHandles(); |
| + } |
| + }; |
| + Context windowContext = mWindowAndroid.getContext().get(); |
| + if (windowContext == null) return null; |
| + if (supportsFloatingActionMode()) { |
| + mPastePopupMenu = new FloatingPastePopupMenu(windowContext, mView, delegate); |
| + } else { |
| + mPastePopupMenu = new LegacyPastePopupMenu(windowContext, mView, delegate); |
| + } |
| + } |
| + return mPastePopupMenu; |
| + } |
| + |
| + void destroyPastePopup() { |
| + hidePastePopup(); |
| + mPastePopupMenu = null; |
| + } |
| + |
| + @VisibleForTesting |
| + public boolean isPastePopupShowing() { |
| + return mPastePopupMenu != null && mPastePopupMenu.isShowing(); |
| + } |
| + |
| + private Context getContext() { |
| + return mContext; |
| + } |
| + |
| + // Composition methods for android.view.ActionMode |
| + |
| + /** |
| + * @see ActionMode#getType()} |
| + */ |
| + public int getType() { |
|
boliu
2016/11/01 00:37:36
package visible
Jinsuk Kim
2016/11/01 04:43:56
In fact it's not used any more. Removed.
|
| + return isActionModeValid() ? mActionMode.getType() : ActionMode.TYPE_PRIMARY; |
| + } |
| + |
| + /** |
| * @see ActionMode#finish() |
| */ |
| - public void finish() { |
| - mActionMode.finish(); |
| + @Override |
| + public void finishActionMode() { |
| + if (isActionModeValid()) mActionMode.finish(); |
| } |
| /** |
| @@ -71,7 +330,8 @@ public class WebActionMode { |
| * Note that invalidation will also reset visibility state. The caller |
| * should account for this when making subsequent visibility updates. |
| */ |
| - public void invalidate() { |
| + public void invalidateActionMode() { |
|
boliu
2016/11/01 00:37:36
private
Jinsuk Kim
2016/11/01 04:43:56
Done.
|
| + if (!isActionModeValid()) return; |
| if (mHidden) { |
| assert canHide(); |
| mHidden = false; |
| @@ -96,7 +356,7 @@ public class WebActionMode { |
| mPendingInvalidateContentRect = true; |
| } else { |
| mPendingInvalidateContentRect = false; |
| - mActionMode.invalidateContentRect(); |
| + if (isActionModeValid()) mActionMode.invalidateContentRect(); |
| } |
| } |
| } |
| @@ -106,7 +366,7 @@ public class WebActionMode { |
| */ |
| public void onWindowFocusChanged(boolean hasWindowFocus) { |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { |
| - mActionMode.onWindowFocusChanged(hasWindowFocus); |
| + if (isActionModeValid()) mActionMode.onWindowFocusChanged(hasWindowFocus); |
| } |
| } |
| @@ -115,7 +375,7 @@ public class WebActionMode { |
| * side-effects if the underlying ActionMode supports hiding. |
| * @param hide whether to hide or show the ActionMode. |
| */ |
| - public void hide(boolean hide) { |
| + void hideActionMode(boolean hide) { |
| if (!canHide()) return; |
| if (mHidden == hide) return; |
| mHidden = hide; |
| @@ -138,15 +398,14 @@ public class WebActionMode { |
| private void hideTemporarily(long duration) { |
| assert canHide(); |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { |
| - mActionMode.hide(duration); |
| + if (isActionModeValid()) mActionMode.hide(duration); |
| } |
| } |
| private boolean canHide() { |
| - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { |
| - return mActionMode.getType() == ActionMode.TYPE_FLOATING; |
| - } |
| - return false; |
| + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M |
| + && isActionModeValid() |
| + && mActionMode.getType() == ActionMode.TYPE_FLOATING; |
| } |
| private long getDefaultHideDuration() { |
| @@ -155,4 +414,567 @@ public class WebActionMode { |
| } |
| return 2000; |
| } |
| + |
| + // Default handlers for action mode callbacks. |
| + |
| + @Override |
| + public boolean onCreateActionMode(ActionMode mode, Menu menu) { |
| + mode.setTitle(DeviceFormFactor.isTablet(getContext()) |
| + ? getContext().getString(R.string.actionbar_textselection_title) |
| + : null); |
| + mode.setSubtitle(null); |
| + createActionMenu(mode, menu); |
| + mNeedsPrepare = false; |
| + return true; |
| + } |
| + |
| + @Override |
| + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { |
| + if (mNeedsPrepare) { |
| + menu.clear(); |
| + createActionMenu(mode, menu); |
| + mNeedsPrepare = false; |
| + return true; |
| + } |
| + return false; |
| + } |
| + |
| + /** |
| + * Initialize the menu by populating all the available items. Embedders should remove |
| + * the items that are not relevant to the input text being edited. |
| + */ |
| + public static void initializeMenu(Context context, ActionMode mode, Menu menu) { |
| + try { |
| + mode.getMenuInflater().inflate(R.menu.select_action_menu, menu); |
| + } catch (Resources.NotFoundException e) { |
| + // TODO(tobiasjs) by the time we get here we have already |
| + // caused a resource loading failure to be logged. WebView |
| + // resource access needs to be improved so that this |
| + // logspam can be avoided. |
| + new MenuInflater(context).inflate(R.menu.select_action_menu, menu); |
| + } |
| + } |
| + |
| + private void createActionMenu(ActionMode mode, Menu menu) { |
| + initializeMenu(mContext, mode, menu); |
| + |
| + if (!isSelectionEditable() || !canPaste()) { |
| + menu.removeItem(R.id.select_action_menu_paste); |
| + } |
| + |
| + if (isInsertion()) { |
| + menu.removeItem(R.id.select_action_menu_select_all); |
| + menu.removeItem(R.id.select_action_menu_cut); |
| + menu.removeItem(R.id.select_action_menu_copy); |
| + menu.removeItem(R.id.select_action_menu_share); |
| + menu.removeItem(R.id.select_action_menu_web_search); |
| + return; |
| + } |
| + |
| + if (!isSelectionEditable()) { |
| + menu.removeItem(R.id.select_action_menu_cut); |
| + } |
| + |
| + if (isSelectionEditable() || !isSelectActionModeAllowed(MENU_ITEM_SHARE)) { |
| + menu.removeItem(R.id.select_action_menu_share); |
| + } |
| + |
| + if (isSelectionEditable() || isIncognito() |
| + || !isSelectActionModeAllowed(MENU_ITEM_WEB_SEARCH)) { |
| + menu.removeItem(R.id.select_action_menu_web_search); |
| + } |
| + |
| + if (isSelectionPassword()) { |
| + menu.removeItem(R.id.select_action_menu_copy); |
| + menu.removeItem(R.id.select_action_menu_cut); |
| + return; |
| + } |
| + |
| + initializeTextProcessingMenu(menu); |
| + } |
| + |
| + private boolean canPaste() { |
| + ClipboardManager clipMgr = (ClipboardManager) |
| + getContext().getSystemService(Context.CLIPBOARD_SERVICE); |
| + return clipMgr.hasPrimaryClip(); |
| + } |
| + |
| + /** |
| + * Intialize the menu items for processing text, if there is any. |
| + */ |
| + private void initializeTextProcessingMenu(Menu menu) { |
| + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M |
| + || !isSelectActionModeAllowed(MENU_ITEM_PROCESS_TEXT)) { |
| + return; |
| + } |
| + |
| + PackageManager packageManager = getContext().getPackageManager(); |
| + List<ResolveInfo> supportedActivities = |
| + packageManager.queryIntentActivities(createProcessTextIntent(), 0); |
| + for (int i = 0; i < supportedActivities.size(); i++) { |
| + ResolveInfo resolveInfo = supportedActivities.get(i); |
| + CharSequence label = resolveInfo.loadLabel(getContext().getPackageManager()); |
| + menu.add(R.id.select_action_menu_text_processing_menus, Menu.NONE, i, label) |
| + .setIntent(createProcessTextIntentForResolveInfo(resolveInfo)) |
| + .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); |
| + } |
| + } |
| + |
| + @TargetApi(Build.VERSION_CODES.M) |
| + private Intent createProcessTextIntent() { |
| + return new Intent().setAction(Intent.ACTION_PROCESS_TEXT).setType("text/plain"); |
| + } |
| + |
| + @TargetApi(Build.VERSION_CODES.M) |
| + private Intent createProcessTextIntentForResolveInfo(ResolveInfo info) { |
| + boolean isReadOnly = !isSelectionEditable(); |
| + return createProcessTextIntent() |
| + .putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, isReadOnly) |
| + .setClassName(info.activityInfo.packageName, info.activityInfo.name); |
| + } |
| + |
| + @Override |
| + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { |
| + if (!isActionModeValid()) return true; |
| + |
| + int id = item.getItemId(); |
| + int groupId = item.getGroupId(); |
| + |
| + if (id == R.id.select_action_menu_select_all) { |
| + selectAll(); |
| + } else if (id == R.id.select_action_menu_cut) { |
| + cut(); |
| + mode.finish(); |
| + } else if (id == R.id.select_action_menu_copy) { |
| + copy(); |
| + mode.finish(); |
| + } else if (id == R.id.select_action_menu_paste) { |
| + paste(); |
| + mode.finish(); |
| + } else if (id == R.id.select_action_menu_share) { |
| + share(); |
| + mode.finish(); |
| + } else if (id == R.id.select_action_menu_web_search) { |
| + search(); |
| + mode.finish(); |
| + } else if (groupId == R.id.select_action_menu_text_processing_menus) { |
| + processText(item.getIntent()); |
| + // The ActionMode is not dismissed to match the behavior with |
| + // TextView in Android M. |
| + } else { |
| + return false; |
| + } |
| + return true; |
| + } |
| + |
| + @Override |
| + public void onDestroyActionMode() { |
| + mActionMode = null; |
| + if (mUnselectAllOnDismiss) { |
| + mWebContents.dismissTextHandles(); |
| + mWebContents.unselect(); |
| + } |
| + } |
| + |
| + /** |
| + * Called when an ActionMode needs to be positioned on screen, potentially occluding view |
| + * content. Note this may be called on a per-frame basis. |
| + * |
| + * @param mode The ActionMode that requires positioning. |
| + * @param view The View that originated the ActionMode, in whose coordinates the Rect should |
| + * be provided. |
| + * @param outRect The Rect to be populated with the content position. |
| + */ |
| + @Override |
| + public void onGetContentRect(ActionMode mode, View view, Rect outRect) { |
| + float deviceScale = mRenderCoordinates.getDeviceScaleFactor(); |
| + outRect.set((int) (mSelectionRect.left * deviceScale), |
| + (int) (mSelectionRect.top * deviceScale), |
| + (int) (mSelectionRect.right * deviceScale), |
| + (int) (mSelectionRect.bottom * deviceScale)); |
| + |
| + // The selection coordinates are relative to the content viewport, but we need |
| + // coordinates relative to the containing View. |
| + outRect.offset(0, (int) mRenderCoordinates.getContentOffsetYPix()); |
| + } |
| + |
| + /** |
| + * Perform a select all action. |
| + */ |
| + public void selectAll() { |
| + mWebContents.selectAll(); |
| + // Even though the above statement logged a SelectAll user action, we want to |
| + // track whether the focus was in an editable field, so log that too. |
| + if (isSelectionEditable()) { |
| + RecordUserAction.record("MobileActionMode.SelectAllWasEditable"); |
| + } else { |
| + RecordUserAction.record("MobileActionMode.SelectAllWasNonEditable"); |
| + } |
| + } |
| + |
| + /** |
| + * Perform a cut (to clipboard) action. |
| + */ |
| + public void cut() { |
| + mWebContents.cut(); |
| + } |
| + |
| + /** |
| + * Perform a copy (to clipboard) action. |
| + */ |
| + public void copy() { |
| + mWebContents.copy(); |
| + } |
| + |
| + /** |
| + * Perform a paste action. |
| + */ |
| + public void paste() { |
| + mWebContents.paste(); |
| + } |
| + |
| + /** |
| + * Perform a share action. |
| + */ |
| + public void share() { |
| + RecordUserAction.record("MobileActionMode.Share"); |
| + String query = sanitizeQuery(getSelectedText(), MAX_SHARE_QUERY_LENGTH); |
| + if (TextUtils.isEmpty(query)) return; |
| + |
| + Intent send = new Intent(Intent.ACTION_SEND); |
| + send.setType("text/plain"); |
| + send.putExtra(Intent.EXTRA_TEXT, query); |
| + try { |
| + Intent i = Intent.createChooser(send, mContext.getString(R.string.actionbar_share)); |
| + i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| + mContext.startActivity(i); |
| + } catch (android.content.ActivityNotFoundException ex) { |
| + // If no app handles it, do nothing. |
| + } |
| + } |
| + |
| + /** |
| + * Perform a processText action (translating the text, for example). |
| + */ |
| + public void processText(Intent intent) { |
| + RecordUserAction.record("MobileActionMode.ProcessTextIntent"); |
| + assert Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; |
| + |
| + String query = sanitizeQuery(getSelectedText(), MAX_SEARCH_QUERY_LENGTH); |
| + if (TextUtils.isEmpty(query)) return; |
| + |
| + intent.putExtra(Intent.EXTRA_PROCESS_TEXT, query); |
| + |
| + // Intent is sent by WindowAndroid by default. |
| + try { |
| + mWindowAndroid.showIntent(intent, new WindowAndroid.IntentCallback() { |
| + @Override |
| + public void onIntentCompleted(WindowAndroid window, |
| + int resultCode, ContentResolver contentResolver, Intent data) { |
| + onReceivedProcessTextResult(resultCode, data); |
| + } |
| + }, null); |
| + } catch (android.content.ActivityNotFoundException ex) { |
| + // If no app handles it, do nothing. |
| + } |
| + } |
| + |
| + /** |
| + * Perform a search action. |
| + */ |
| + public void search() { |
| + RecordUserAction.record("MobileActionMode.WebSearch"); |
| + String query = sanitizeQuery(getSelectedText(), MAX_SEARCH_QUERY_LENGTH); |
| + if (TextUtils.isEmpty(query)) return; |
| + |
| + Intent i = new Intent(Intent.ACTION_WEB_SEARCH); |
| + i.putExtra(SearchManager.EXTRA_NEW_SEARCH, true); |
| + i.putExtra(SearchManager.QUERY, query); |
| + i.putExtra(Browser.EXTRA_APPLICATION_ID, mContext.getPackageName()); |
| + i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
| + try { |
| + mContext.startActivity(i); |
| + } catch (android.content.ActivityNotFoundException ex) { |
| + // If no app handles it, do nothing. |
| + } |
| + } |
| + |
| + /** |
| + * @return true if the current selection is of password type. |
| + */ |
| + public boolean isSelectionPassword() { |
| + return mIsPasswordType; |
| + } |
| + |
| + /** |
| + * @return true iff the current selection is editable (e.g. text within an input field). |
| + */ |
| + public boolean isSelectionEditable() { |
| + return mEditable; |
| + } |
| + |
| + /** |
| + * @return true if the current selection is an insertion point. |
| + */ |
| + @VisibleForTesting |
| + public boolean isInsertion() { |
| + return mIsInsertion; |
| + } |
| + |
| + @Override |
| + public boolean isIncognito() { |
| + return mWebContents.isIncognito(); |
| + } |
| + |
| + /** |
| + * @param actionModeItem the flag for the action mode item in question. The valid flags are |
| + * {@link #MENU_ITEM_SHARE}, {@link #MENU_ITEM_WEB_SEARCH}, and |
| + * {@link #MENU_ITEM_PROCESS_TEXT}. |
| + * @return true if the menu item action is allowed. Otherwise, the menu item |
| + * should be removed from the menu. |
| + */ |
| + private boolean isSelectActionModeAllowed(int actionModeItem) { |
| + boolean isAllowedByClient = mAllowedMenuItems != null |
| + ? mAllowedMenuItems.get(actionModeItem, mMenuDefaultAllowed) |
| + : mMenuDefaultAllowed; |
| + if (actionModeItem == MENU_ITEM_SHARE) { |
| + return isAllowedByClient && isShareAvailable(); |
| + } |
| + if (actionModeItem == MENU_ITEM_WEB_SEARCH) { |
| + return isAllowedByClient && isWebSearchAvailable(); |
| + } |
| + return isAllowedByClient; |
| + } |
| + |
| + @Override |
| + public void onReceivedProcessTextResult(int resultCode, Intent data) { |
| + if (mWebContents == null || resultCode != Activity.RESULT_OK || data == null) return; |
| + |
| + // Do not handle the result if no text is selected or current selection is not editable. |
| + if (!mHasSelection || !isSelectionEditable()) return; |
| + |
| + CharSequence result = data.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT); |
| + if (result != null) { |
| + // TODO(hush): Use a variant of replace that re-selects the replaced text. |
| + // crbug.com/546710 |
| + mWebContents.replace(result.toString()); |
| + } |
| + } |
| + |
| + void restoreSelectionPopupsIfNecessary() { |
| + if (mHasSelection && isActionModeValid()) showActionMode(true); |
| + } |
| + |
| + // All coordinates are in DIP. |
| + void onSelectionEvent(int eventType, int xAnchor, int yAnchor, |
| + int left, int top, int right, int bottom, boolean isScrollInProgress, |
| + boolean touchScrollInProgress, ImeAdapter imeAdapter) { |
| + // Ensure the provided selection coordinates form a non-empty rect, as required by |
| + // the selection action mode. |
| + if (left == right) ++right; |
| + if (top == bottom) ++bottom; |
| + switch (eventType) { |
| + case SelectionEventType.SELECTION_HANDLES_SHOWN: |
| + mSelectionRect.set(left, top, right, bottom); |
| + mHasSelection = true; |
| + mUnselectAllOnDismiss = true; |
| + if (showActionMode(true) && !isActionModeValid()) { |
| + if (isSelectionEditable()) { |
| + imeAdapter.moveCursorToSelectionEnd(); |
| + } else { |
| + if (mWebContents != null) mWebContents.unselect(); |
| + } |
| + } |
| + break; |
| + |
| + case SelectionEventType.SELECTION_HANDLES_MOVED: |
| + mSelectionRect.set(left, top, right, bottom); |
| + invalidateContentRect(); |
| + break; |
| + |
| + case SelectionEventType.SELECTION_HANDLES_CLEARED: |
| + mHasSelection = false; |
| + mUnselectAllOnDismiss = false; |
| + mSelectionRect.setEmpty(); |
| + finishActionMode(); |
| + mDraggingSelection = false; |
| + break; |
| + |
| + case SelectionEventType.SELECTION_HANDLE_DRAG_STARTED: |
| + mDraggingSelection = true; |
| + updateActionModeVisibility(touchScrollInProgress); |
| + break; |
| + |
| + case SelectionEventType.SELECTION_HANDLE_DRAG_STOPPED: |
| + mDraggingSelection = false; |
| + updateActionModeVisibility(touchScrollInProgress); |
| + break; |
| + |
| + case SelectionEventType.INSERTION_HANDLE_SHOWN: |
| + mSelectionRect.set(left, top, right, bottom); |
| + setIsInsertion(true); |
| + break; |
| + |
| + case SelectionEventType.INSERTION_HANDLE_MOVED: |
| + mSelectionRect.set(left, top, right, bottom); |
| + if (!isScrollInProgress && isPastePopupShowing()) { |
| + showPastePopup(xAnchor, yAnchor); |
| + } else { |
| + hidePastePopup(); |
| + } |
| + break; |
| + |
| + case SelectionEventType.INSERTION_HANDLE_TAPPED: |
| + if (mWasPastePopupShowingOnInsertionDragStart) { |
| + hidePastePopup(); |
| + } else { |
| + showPastePopup(xAnchor, yAnchor); |
| + } |
| + mWasPastePopupShowingOnInsertionDragStart = false; |
| + break; |
| + |
| + case SelectionEventType.INSERTION_HANDLE_CLEARED: |
| + hidePastePopup(); |
| + setIsInsertion(false); |
| + mSelectionRect.setEmpty(); |
| + break; |
| + |
| + case SelectionEventType.INSERTION_HANDLE_DRAG_STARTED: |
| + mWasPastePopupShowingOnInsertionDragStart = isPastePopupShowing(); |
| + hidePastePopup(); |
| + break; |
| + |
| + case SelectionEventType.INSERTION_HANDLE_DRAG_STOPPED: |
| + if (mWasPastePopupShowingOnInsertionDragStart) { |
| + showPastePopup(xAnchor, yAnchor); |
| + } |
| + mWasPastePopupShowingOnInsertionDragStart = false; |
| + break; |
| + |
| + case SelectionEventType.SELECTION_ESTABLISHED: |
| + case SelectionEventType.SELECTION_DISSOLVED: |
| + break; |
| + |
| + default: |
| + assert false : "Invalid selection event type."; |
| + } |
| + |
| + if (mContextualSearchClient != null) { |
| + final float deviceScale = mRenderCoordinates.getDeviceScaleFactor(); |
| + int xAnchorPix = (int) (xAnchor * deviceScale); |
| + int yAnchorPix = (int) (yAnchor * deviceScale); |
| + mContextualSearchClient.onSelectionEvent(eventType, xAnchorPix, yAnchorPix); |
| + } |
| + } |
| + |
| + void onSelectionChanged(String text) { |
| + mLastSelectedText = text; |
| + if (mContextualSearchClient != null) { |
| + mContextualSearchClient.onSelectionChanged(text); |
| + } |
| + } |
| + |
| + // The client that implements Contextual Search functionality, or null if none exists. |
| + void setContextualSearchClient(ContextualSearchClient contextualSearchClient) { |
| + mContextualSearchClient = contextualSearchClient; |
| + } |
| + |
| + void onShowUnhandledTapUIIfNeeded(int x, int y) { |
| + if (mContextualSearchClient != null) { |
| + mContextualSearchClient.showUnhandledTapUIIfNeeded(x, y); |
| + } |
| + } |
| + |
| + void destroyActionModeAndUnselect() { |
| + mUnselectAllOnDismiss = true; |
| + } |
| + |
| + void destroyActionModeAndKeepSelection() { |
| + mUnselectAllOnDismiss = false; |
| + } |
| + |
| + void updateSelectionState(boolean editable, boolean isPassword) { |
| + if (!editable) hidePastePopup(); |
| + if (isActionModeValid() |
| + && (editable != isSelectionEditable() || isPassword != isSelectionPassword())) { |
| + mActionMode.invalidate(); |
| + mNeedsPrepare = true; |
| + } |
| + mEditable = editable; |
| + mIsPasswordType = isPassword; |
| + } |
| + |
| + /** |
| + * @return Whether the page has an active, touch-controlled selection region. |
| + */ |
| + @VisibleForTesting |
| + public boolean hasSelection() { |
| + return mHasSelection; |
| + } |
| + |
| + void updateActionModeVisibility(boolean touchScrollInProgress) { |
| + // The active fling count isn't reliable with WebView, so only use the |
| + // active touch scroll signal for hiding. The fling animation movement |
| + // will naturally hide the ActionMode by invalidating its content rect. |
| + hideActionMode(mDraggingSelection || touchScrollInProgress); |
| + } |
| + |
| + @Override |
| + public String getSelectedText() { |
| + return mHasSelection ? mLastSelectedText : ""; |
| + } |
| + |
| + private void setIsInsertion(boolean insertion) { |
| + if (isActionModeValid() && mIsInsertion != insertion) mNeedsPrepare = true; |
| + mIsInsertion = insertion; |
| + } |
| + |
| + /** |
| + * Trim a given string query to be processed safely. |
| + * |
| + * @param query a raw query to sanitize. |
| + * @param maxLength maximum length to which the query will be truncated. |
| + */ |
| + public static String sanitizeQuery(String query, int maxLength) { |
| + if (TextUtils.isEmpty(query) || query.length() < maxLength) return query; |
| + Log.w(TAG, "Truncating oversized query (" + query.length() + ")."); |
| + return query.substring(0, maxLength) + "…"; |
| + } |
| + |
| + private boolean isShareAvailable() { |
| + Intent intent = new Intent(Intent.ACTION_SEND); |
| + intent.setType("text/plain"); |
| + return mContext.getPackageManager().queryIntentActivities(intent, |
| + PackageManager.MATCH_DEFAULT_ONLY).size() > 0; |
| + } |
| + |
| + private boolean isWebSearchAvailable() { |
| + Intent intent = new Intent(Intent.ACTION_WEB_SEARCH); |
| + intent.putExtra(SearchManager.EXTRA_NEW_SEARCH, true); |
| + return mContext.getPackageManager().queryIntentActivities(intent, |
| + PackageManager.MATCH_DEFAULT_ONLY).size() > 0; |
| + } |
| + |
| + /** |
| + * Empty {@link ActionMode.Callback} that does nothing. Used for {@link #empty()}. |
| + */ |
| + private static class EmptyActionCallback implements ActionMode.Callback { |
| + @Override |
| + public boolean onCreateActionMode(ActionMode mode, Menu menu) { |
| + return false; |
| + } |
| + |
| + @Override |
| + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { |
| + return false; |
| + } |
| + |
| + @Override |
| + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { |
| + return false; |
| + } |
| + |
| + @Override |
| + public void onDestroyActionMode(ActionMode mode) {} |
| + }; |
| } |