Chromium Code Reviews| 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 f992a243f5afdc511bb038a0f0ce1dd264618ae7..6b5f79c415434cc9199c9ed58be3a3f6a975e82e 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 |
| @@ -54,7 +54,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 +70,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 |
|
Ted C
2017/03/29 17:43:51
ok, I think this is the relevant comment for why w
Tima Vaisburd
2017/03/29 22:56:37
Yes.
|
| + // 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; |
| @@ -114,6 +122,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. |
| @@ -144,6 +162,13 @@ 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|. |
| + mAssistMenuItemId = mContext.getResources().getIdentifier("textAssist", "id", "android"); |
|
Ted C
2017/03/29 17:43:51
are some of these things only applicable in O? Sh
Tima Vaisburd
2017/03/29 22:56:37
Done.
|
| } |
| /** |
| @@ -173,9 +198,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 |
| @@ -188,19 +213,19 @@ 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()) { |
| invalidateActionMode(); |
| - return true; |
| + return; |
| } |
| if (mView.getParent() != null) { |
| - // On ICS, startActionMode throws an NPE when getParent() is null. |
| + // On ICS, startActionMode throws an NPE when getParent() is null. |
| assert mWebContents != null; |
| ActionMode actionMode = supportsFloatingActionMode() |
| ? startFloatingActionMode() |
| @@ -212,7 +237,7 @@ public class SelectionPopupController extends ActionModeCallbackHelper { |
| mActionMode = actionMode; |
| } |
| mUnselectAllOnDismiss = true; |
| - return isActionModeValid(); |
| + if (!isActionModeValid()) clearSelection(); |
| } |
| @TargetApi(Build.VERSION_CODES.M) |
| @@ -287,6 +312,10 @@ public class SelectionPopupController extends ActionModeCallbackHelper { |
| */ |
| @Override |
| public void finishActionMode() { |
| + mPendingClassificationRequest = false; |
| + mHidden = false; |
| + if (mView != null) mView.removeCallbacks(mRepeatingHideRunnable); |
| + |
| if (isActionModeValid()) { |
| mActionMode.finish(); |
| @@ -305,7 +334,7 @@ public class SelectionPopupController extends ActionModeCallbackHelper { |
| if (mHidden) { |
| assert canHideActionMode(); |
| mHidden = false; |
| - mView.removeCallbacks(mRepeatingHideRunnable); |
| + unhideActionMode(); |
| } |
| // Try/catch necessary for framework bug, crbug.com/446717. |
| @@ -347,11 +376,16 @@ public class SelectionPopupController extends ActionModeCallbackHelper { |
| mRepeatingHideRunnable.run(); |
| } else { |
| mHidden = false; |
| - mView.removeCallbacks(mRepeatingHideRunnable); |
| - hideActionModeTemporarily(SHOW_DELAY_MS); |
| + unhideActionMode(); |
| } |
| } |
| + private void unhideActionMode() { |
| + mView.removeCallbacks(mRepeatingHideRunnable); |
| + // To show the action mode that is being hidden call hide() again with a short delay. |
| + hideActionModeTemporarily(SHOW_DELAY_MS); |
| + } |
| + |
| /** |
| * @see ActionMode#hide(long) |
| */ |
| @@ -413,6 +447,7 @@ public class SelectionPopupController extends ActionModeCallbackHelper { |
| private void createActionMenu(ActionMode mode, Menu menu) { |
| mNeedsPrepare = false; |
| initializeMenu(mContext, mode, menu); |
| + updateAssistMenuItem(menu); |
| if (!isSelectionEditable() || !canPaste()) { |
| menu.removeItem(R.id.select_action_menu_paste); |
| @@ -455,6 +490,21 @@ 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 |
|
Ted C
2017/03/29 17:43:51
I think here we should again early out if not O to
Tima Vaisburd
2017/03/29 22:56:37
Done.
|
| + // 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); |
| + if (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. |
| */ |
| @@ -470,7 +520,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) |
|
Ted C
2017/03/29 17:43:51
is this the indenting that clang format wants to d
Tima Vaisburd
2017/03/29 22:56:37
Yes. I fixed it back.
|
| .setIntent(createProcessTextIntentForResolveInfo(resolveInfo)) |
| .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); |
| } |
| @@ -496,7 +547,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(); |
| @@ -555,11 +609,38 @@ 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; |
|
amaralp
2017/03/29 01:18:26
I think it makes more sense to have the reset in s
Tima Vaisburd
2017/03/29 01:52:29
This would break the rotation, I think? When rotat
amaralp
2017/03/29 03:20:58
Oh right I forgot about rotation. Wouldn't you als
Tima Vaisburd
2017/03/29 06:09:26
Yes, this is great catch, but I still need to figu
|
| + mNeedsPrepare = true; |
| + invalidateActionMode(); |
|
amaralp
2017/03/29 01:18:26
Once you rebase this should be showActionMode()
Tima Vaisburd
2017/03/29 01:52:29
Yes. In the current CL it is showActionModeOrClear
|
| // 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()) { |
| @@ -732,7 +813,7 @@ public class SelectionPopupController extends ActionModeCallbackHelper { |
| void restoreSelectionPopupsIfNecessary() { |
| if (mHasSelection && !isActionModeValid()) { |
| - if (!showActionMode()) clearSelection(); |
| + showActionModeOrClearOnFailure(); |
| } |
| } |
| @@ -749,7 +830,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: |
| @@ -769,7 +856,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: |
| @@ -830,8 +923,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) { |
| @@ -844,6 +938,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) { |
| @@ -898,4 +997,46 @@ 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; |
| + } |
| + |
| + // Determine whether we need to recreate the menu in case we are doing invalidate(). |
| + final boolean hadOldResult = |
| + mClassificationResult != null && mClassificationResult.hasNamedAction(); |
| + final boolean hasNewResult = result != null && result.hasNamedAction(); |
| + |
| + mClassificationResult = result; |
|
amaralp
2017/03/29 01:18:25
I still don't understand why you'd want to keep th
Tima Vaisburd
2017/03/29 01:52:29
I'm not sure I understood your question. I save it
Tima Vaisburd
2017/03/29 06:09:26
I think I got your question: there is a case where
|
| + |
| + // Do not recreate the action mode if it has been cancelled (by ActionMode.finish()) |
| + // and not recreated after that. |
| + if (!pendingClassificationRequest && !isActionModeValid()) { |
|
amaralp
2017/03/29 01:18:26
Why would you want to showActionMode if pendingCla
Tima Vaisburd
2017/03/29 01:52:29
I think the following scenario is possible:
1. Re
amaralp
2017/03/29 03:20:58
That makes sense. Thanks for the clarification!
|
| + 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); |
| + } |
| + |
| + // For simplicity always recreate the menu if the new result exists. |
| + mNeedsPrepare = hasNewResult || hadOldResult; |
| + |
| + // Rely on this method to clear mHidden and unhide the action mode. |
| + showActionModeOrClearOnFailure(); |
| + } |
| + }; |
| } |