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

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

Issue 2740103006: Implement SmartText selection. (Closed)
Patch Set: Use getMaximumOrderItem() for text processing menu order Created 3 years, 9 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/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..9aeadf65ae39c7851c76431cc3735cb37daa6e03 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
@@ -114,6 +114,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 +154,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");
}
/**
@@ -173,9 +190,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 +205,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 +229,7 @@ public class SelectionPopupController extends ActionModeCallbackHelper {
mActionMode = actionMode;
}
mUnselectAllOnDismiss = true;
- return isActionModeValid();
+ if (!isActionModeValid()) clearSelection();
}
@TargetApi(Build.VERSION_CODES.M)
@@ -287,6 +304,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 +326,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 +368,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 +439,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 +482,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
+ // 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.
*/
@@ -467,15 +509,37 @@ public class SelectionPopupController extends ActionModeCallbackHelper {
PackageManager packageManager = mContext.getPackageManager();
List<ResolveInfo> supportedActivities =
packageManager.queryIntentActivities(createProcessTextIntent(), 0);
+ if (supportedActivities.size() == 0) return;
+
+ // Force text processing menu at the end.
+ final int order = getMaximumItemOrder(menu) + 1;
+
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, order + i, label)
.setIntent(createProcessTextIntentForResolveInfo(resolveInfo))
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
}
}
+ /**
+ * Returns largest order among the menu items or 0 if no item contains the ordering
+ * information (android:orderInCategory). The categories are not considered, the
+ * return value would only make sense if all items belong to the same category.
+ */
+ private static int getMaximumItemOrder(Menu menu) {
Tima Vaisburd 2017/03/28 23:16:01 Determine the maximum value of the order. We canno
boliu 2017/03/28 23:24:15 Err, this is worse than the the constant, because
Tima Vaisburd 2017/03/29 00:08:15 I like the constant better, too.
+ // The mask to remove all documented categories.
+ int mask = ~(Menu.CATEGORY_ALTERNATIVE | Menu.CATEGORY_CONTAINER | Menu.CATEGORY_SECONDARY
+ | Menu.CATEGORY_SYSTEM);
+
+ int result = 0;
+ for (int i = 0; i < menu.size(); ++i) {
+ result = Math.max(result, menu.getItem(i).getOrder() & mask);
+ }
+ return result;
+ }
+
@TargetApi(Build.VERSION_CODES.M)
private static Intent createProcessTextIntent() {
return new Intent().setAction(Intent.ACTION_PROCESS_TEXT).setType("text/plain");
@@ -496,7 +560,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 +622,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;
+ mNeedsPrepare = true;
+ invalidateActionMode();
// 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 +826,7 @@ public class SelectionPopupController extends ActionModeCallbackHelper {
void restoreSelectionPopupsIfNecessary() {
if (mHasSelection && !isActionModeValid()) {
- if (!showActionMode()) clearSelection();
+ showActionModeOrClearOnFailure();
}
}
@@ -749,7 +843,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 +869,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 +936,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 +951,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 +1010,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;
+
+ // 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);
+ }
+
+ // 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();
+ }
+ };
}

Powered by Google App Engine
This is Rietveld 408576698