| Index: content/public/android/java/src/org/chromium/content/browser/SelectionPopupController.java
|
| diff --git a/content/public/android/java/src/org/chromium/content/browser/SelectionPopupController.java b/content/public/android/java/src/org/chromium/content/browser/SelectionPopupController.java
|
| index 612d69dc6c39fe2c4eb6c90f223081a3ed6542a3..2f77bdb533ee9301cb4ebddfe917086a852a58fe 100644
|
| --- a/content/public/android/java/src/org/chromium/content/browser/SelectionPopupController.java
|
| +++ b/content/public/android/java/src/org/chromium/content/browser/SelectionPopupController.java
|
| @@ -25,6 +25,7 @@ import android.view.View;
|
| import android.view.ViewConfiguration;
|
| import android.view.WindowManager;
|
|
|
| +import org.chromium.base.BuildInfo;
|
| import org.chromium.base.Log;
|
| import org.chromium.base.VisibleForTesting;
|
| import org.chromium.base.metrics.RecordUserAction;
|
| @@ -54,7 +55,7 @@ import java.util.List;
|
| */
|
| @TargetApi(Build.VERSION_CODES.M)
|
| public class SelectionPopupController extends ActionModeCallbackHelper {
|
| - private static final String TAG = "cr.SelectionPopCtlr"; // 20 char limit
|
| + private static final String TAG = "SelectionPopupCtlr"; // 20 char limit
|
|
|
| /**
|
| * Android Intent size limitations prevent sending over a megabyte of data. Limit
|
| @@ -70,6 +71,14 @@ public class SelectionPopupController extends ActionModeCallbackHelper {
|
| // most such trailing, async delays.
|
| private static final int SHOW_DELAY_MS = 300;
|
|
|
| + // A large value to force text processing menu items to be at the end of the
|
| + // context menu. Chosen to be bigger than the order of possible items in the
|
| + // XML template.
|
| + // TODO(timav): remove this constant and use show/hide for Assist item instead
|
| + // of adding and removing it once we switch to Android O SDK. The show/hide method
|
| + // does not require ordering information.
|
| + private static final int MENU_ITEM_ORDER_TEXT_PROCESS_START = 100;
|
| +
|
| private final Context mContext;
|
| private final WindowAndroid mWindowAndroid;
|
| private final WebContents mWebContents;
|
| @@ -111,6 +120,16 @@ public class SelectionPopupController extends ActionModeCallbackHelper {
|
| // The client that processes textual selection, or null if none exists.
|
| private SelectionClient mSelectionClient;
|
|
|
| + // The classificaton result of the selected text if the selection exists and
|
| + // ContextSelectionProvider was able to classify it, otherwise null.
|
| + private ContextSelectionProvider.Result mClassificationResult;
|
| +
|
| + // The resource ID for Assist menu item.
|
| + private int mAssistMenuItemId;
|
| +
|
| + // This variable is set to true when the classification request is in progress.
|
| + private boolean mPendingClassificationRequest;
|
| +
|
| /**
|
| * Create {@link SelectionPopupController} instance.
|
| * @param context Context for action mode.
|
| @@ -141,6 +160,16 @@ public class SelectionPopupController extends ActionModeCallbackHelper {
|
| hideActionModeTemporarily(hideDuration);
|
| }
|
| };
|
| +
|
| + mSelectionClient =
|
| + ContextSelectionClient.create(new ContextSelectionCallback(), window, webContents);
|
| +
|
| + // TODO(timav): Use android.R.id.textAssist for the Assist item id once we switch to
|
| + // Android O SDK and remove |mAssistMenuItemId|.
|
| + if (BuildInfo.isAtLeastO()) {
|
| + mAssistMenuItemId =
|
| + mContext.getResources().getIdentifier("textAssist", "id", "android");
|
| + }
|
| }
|
|
|
| /**
|
| @@ -170,9 +199,9 @@ public class SelectionPopupController extends ActionModeCallbackHelper {
|
| return mActionMode != null;
|
| }
|
|
|
| - // True if action mode is not yet initialized or set to no-op mode.
|
| - private boolean isEmpty() {
|
| - return mCallback == EMPTY_CALLBACK;
|
| + // True if action mode is initialized to a working (not a no-op) mode.
|
| + private boolean isActionModeSupported() {
|
| + return mCallback != EMPTY_CALLBACK;
|
| }
|
|
|
| @Override
|
| @@ -185,12 +214,12 @@ public class SelectionPopupController extends ActionModeCallbackHelper {
|
| *
|
| * <p>Action mode in floating mode is tried first, and then falls back to
|
| * a normal one.
|
| - * @return {@code true} if the action mode started successfully or is already on.
|
| + * <p> If the action mode cannot be created the selection is cleared.
|
| */
|
| - public boolean showActionMode() {
|
| - if (isEmpty()) return false;
|
| + public void showActionModeOrClearOnFailure() {
|
| + if (!isActionModeSupported()) return;
|
|
|
| - // Just refreshes the view if it is already showing.
|
| + // Just refresh the view if action mode already exists.
|
| if (isActionModeValid()) {
|
| // Try/catch necessary for framework bug, crbug.com/446717.
|
| try {
|
| @@ -199,8 +228,9 @@ public class SelectionPopupController extends ActionModeCallbackHelper {
|
| Log.w(TAG, "Ignoring NPE from ActionMode.invalidate() as workaround for L", e);
|
| }
|
| hideActionMode(false);
|
| - return true;
|
| + return;
|
| }
|
| +
|
| assert mWebContents != null;
|
| ActionMode actionMode = supportsFloatingActionMode()
|
| ? startFloatingActionMode()
|
| @@ -211,7 +241,8 @@ public class SelectionPopupController extends ActionModeCallbackHelper {
|
| }
|
| mActionMode = actionMode;
|
| mUnselectAllOnDismiss = true;
|
| - return isActionModeValid();
|
| +
|
| + if (!isActionModeValid()) clearSelection();
|
| }
|
|
|
| @TargetApi(Build.VERSION_CODES.M)
|
| @@ -286,6 +317,10 @@ public class SelectionPopupController extends ActionModeCallbackHelper {
|
| */
|
| @Override
|
| public void finishActionMode() {
|
| + mPendingClassificationRequest = false;
|
| + mHidden = false;
|
| + if (mView != null) mView.removeCallbacks(mRepeatingHideRunnable);
|
| +
|
| if (isActionModeValid()) {
|
| mActionMode.finish();
|
|
|
| @@ -324,8 +359,8 @@ public class SelectionPopupController extends ActionModeCallbackHelper {
|
| if (mHidden) {
|
| mRepeatingHideRunnable.run();
|
| } else {
|
| - mHidden = false;
|
| mView.removeCallbacks(mRepeatingHideRunnable);
|
| + // To show the action mode that is being hidden call hide() again with a short delay.
|
| hideActionModeTemporarily(SHOW_DELAY_MS);
|
| }
|
| }
|
| @@ -389,6 +424,7 @@ public class SelectionPopupController extends ActionModeCallbackHelper {
|
|
|
| private void createActionMenu(ActionMode mode, Menu menu) {
|
| initializeMenu(mContext, mode, menu);
|
| + updateAssistMenuItem(menu);
|
|
|
| if (!isSelectionEditable() || !canPaste()) {
|
| menu.removeItem(R.id.select_action_menu_paste);
|
| @@ -431,6 +467,23 @@ public class SelectionPopupController extends ActionModeCallbackHelper {
|
| return clipMgr.hasPrimaryClip();
|
| }
|
|
|
| + private void updateAssistMenuItem(Menu menu) {
|
| + // The assist menu item ID has to be equal to android.R.id.textAssist. Until we compile
|
| + // with Android O SDK where this ID is defined we replace the corresponding inflated
|
| + // item with an item with the proper ID.
|
| + // TODO(timav): Use android.R.id.textAssist for the Assist item id once we switch to
|
| + // Android O SDK and remove |mAssistMenuItemId|.
|
| + menu.removeItem(R.id.select_action_menu_assist);
|
| +
|
| + // There is no Assist functionality before Android O.
|
| + if (!BuildInfo.isAtLeastO() || mAssistMenuItemId == 0) return;
|
| +
|
| + if (mClassificationResult != null && mClassificationResult.hasNamedAction()) {
|
| + menu.add(mAssistMenuItemId, mAssistMenuItemId, 1, mClassificationResult.label)
|
| + .setIcon(mClassificationResult.icon);
|
| + }
|
| + }
|
| +
|
| /**
|
| * Intialize the menu items for processing text, if there is any.
|
| */
|
| @@ -446,7 +499,8 @@ public class SelectionPopupController extends ActionModeCallbackHelper {
|
| for (int i = 0; i < supportedActivities.size(); i++) {
|
| ResolveInfo resolveInfo = supportedActivities.get(i);
|
| CharSequence label = resolveInfo.loadLabel(mContext.getPackageManager());
|
| - menu.add(R.id.select_action_menu_text_processing_menus, Menu.NONE, i, label)
|
| + menu.add(R.id.select_action_menu_text_processing_menus, Menu.NONE,
|
| + MENU_ITEM_ORDER_TEXT_PROCESS_START + i, label)
|
| .setIntent(createProcessTextIntentForResolveInfo(resolveInfo))
|
| .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
|
| }
|
| @@ -472,7 +526,10 @@ public class SelectionPopupController extends ActionModeCallbackHelper {
|
| int id = item.getItemId();
|
| int groupId = item.getGroupId();
|
|
|
| - if (id == R.id.select_action_menu_select_all) {
|
| + if (id == mAssistMenuItemId) {
|
| + doAssistAction();
|
| + mode.finish();
|
| + } else if (id == R.id.select_action_menu_select_all) {
|
| selectAll();
|
| } else if (id == R.id.select_action_menu_cut) {
|
| cut();
|
| @@ -531,11 +588,37 @@ public class SelectionPopupController extends ActionModeCallbackHelper {
|
| }
|
|
|
| /**
|
| + * Perform an action that depends on the semantics of the selected text.
|
| + */
|
| + @VisibleForTesting
|
| + void doAssistAction() {
|
| + if (mClassificationResult == null || !mClassificationResult.hasNamedAction()) return;
|
| +
|
| + assert mClassificationResult.onClickListener != null
|
| + || mClassificationResult.intent != null;
|
| +
|
| + if (mClassificationResult.onClickListener != null) {
|
| + mClassificationResult.onClickListener.onClick(mView);
|
| + return;
|
| + }
|
| +
|
| + if (mClassificationResult.intent != null) {
|
| + Context context = mWindowAndroid.getContext().get();
|
| + if (context == null) return;
|
| +
|
| + context.startActivity(mClassificationResult.intent);
|
| + return;
|
| + }
|
| + }
|
| +
|
| + /**
|
| * Perform a select all action.
|
| */
|
| @VisibleForTesting
|
| void selectAll() {
|
| mWebContents.selectAll();
|
| + mClassificationResult = null;
|
| + showActionModeOrClearOnFailure();
|
| // 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()) {
|
| @@ -708,7 +791,7 @@ public class SelectionPopupController extends ActionModeCallbackHelper {
|
|
|
| void restoreSelectionPopupsIfNecessary() {
|
| if (mHasSelection && !isActionModeValid()) {
|
| - if (!showActionMode()) clearSelection();
|
| + showActionModeOrClearOnFailure();
|
| }
|
| }
|
|
|
| @@ -725,7 +808,13 @@ public class SelectionPopupController extends ActionModeCallbackHelper {
|
| mSelectionRect.set(left, top, right, bottom);
|
| mHasSelection = true;
|
| mUnselectAllOnDismiss = true;
|
| - if (!showActionMode()) clearSelection();
|
| + if (mSelectionClient != null && mSelectionClient.sendsSelectionPopupUpdates()) {
|
| + // Rely on |mSelectionClient| sending a classification request and the request
|
| + // always calling onClassified() callback.
|
| + mPendingClassificationRequest = true;
|
| + } else {
|
| + showActionModeOrClearOnFailure();
|
| + }
|
| break;
|
|
|
| case SelectionEventType.SELECTION_HANDLES_MOVED:
|
| @@ -745,7 +834,13 @@ public class SelectionPopupController extends ActionModeCallbackHelper {
|
| break;
|
|
|
| case SelectionEventType.SELECTION_HANDLE_DRAG_STOPPED:
|
| - hideActionMode(false);
|
| + if (mSelectionClient != null && mSelectionClient.sendsSelectionPopupUpdates()) {
|
| + // Rely on |mSelectionClient| sending a classification request and the request
|
| + // always calling onClassified() callback.
|
| + mPendingClassificationRequest = true;
|
| + } else {
|
| + hideActionMode(false);
|
| + }
|
| break;
|
|
|
| case SelectionEventType.INSERTION_HANDLE_SHOWN:
|
| @@ -806,8 +901,9 @@ public class SelectionPopupController extends ActionModeCallbackHelper {
|
| * end if applicable.
|
| */
|
| void clearSelection() {
|
| - if (mWebContents == null || isEmpty()) return;
|
| + if (mWebContents == null || !isActionModeSupported()) return;
|
| mWebContents.collapseSelection();
|
| + mClassificationResult = null;
|
| }
|
|
|
| void onSelectionChanged(String text) {
|
| @@ -820,6 +916,11 @@ public class SelectionPopupController extends ActionModeCallbackHelper {
|
| // The client that implements selection augmenting functionality, or null if none exists.
|
| void setSelectionClient(SelectionClient selectionClient) {
|
| mSelectionClient = selectionClient;
|
| +
|
| + mClassificationResult = null;
|
| +
|
| + assert !mPendingClassificationRequest;
|
| + assert !mHidden;
|
| }
|
|
|
| void onShowUnhandledTapUIIfNeeded(int x, int y) {
|
| @@ -866,4 +967,40 @@ public class SelectionPopupController extends ActionModeCallbackHelper {
|
| return mContext.getPackageManager().queryIntentActivities(intent,
|
| PackageManager.MATCH_DEFAULT_ONLY).size() > 0;
|
| }
|
| +
|
| + // The callback class that delivers result from a ContextSelectionClient.
|
| + private class ContextSelectionCallback implements ContextSelectionProvider.ResultCallback {
|
| + @Override
|
| + public void onClassified(ContextSelectionProvider.Result result) {
|
| + boolean pendingClassificationRequest = mPendingClassificationRequest;
|
| + mPendingClassificationRequest = false;
|
| +
|
| + // If the selection does not exist any more, discard |result|.
|
| + if (!mHasSelection) {
|
| + assert !mHidden;
|
| + assert mClassificationResult == null;
|
| + return;
|
| + }
|
| +
|
| + // The classificationresult is a property of the selection. Keep it even the action
|
| + // mode has been dismissed.
|
| + mClassificationResult = result;
|
| +
|
| + // Do not recreate the action mode if it has been cancelled (by ActionMode.finish())
|
| + // and not recreated after that.
|
| + if (!pendingClassificationRequest && !isActionModeValid()) {
|
| + assert !mHidden;
|
| + return;
|
| + }
|
| +
|
| + // Update the selection range if needed.
|
| + if (!(result.startAdjust == 0 && result.endAdjust == 0)) {
|
| + // This call causes SELECTION_HANDLES_MOVED event
|
| + mWebContents.adjustSelectionByCharacterOffset(result.startAdjust, result.endAdjust);
|
| + }
|
| +
|
| + // Rely on this method to clear |mHidden| and unhide the action mode.
|
| + showActionModeOrClearOnFailure();
|
| + }
|
| + };
|
| }
|
|
|