Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(595)

Unified Diff: content/public/android/java/src/org/chromium/content/browser/WebActionMode.java

Issue 2407303005: Let embedder provide select action mode (Closed)
Patch Set: more comments addressed Created 4 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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) {}
+ };
}

Powered by Google App Engine
This is Rietveld 408576698