Commit fbcbd3bc authored by Tony Mak's avatar Tony Mak Committed by Commit Bot

Call onSelectionEvent to log instead of using reflection

1. Removed reflection and instead use SelectionEvent introduced in P
   That means we won't have logging from O device anymore.
   However, P-R logs should be enough for us to conduct A/B
   experiments.
2. By calling onSelectionEvent, events would now be routed to a
   TextClassifierService if exists. Previously, TCS won't get the
   event.
3. This will help migrate to the new android metrics logger.

Bug: b/120032995

Change-Id: I4d39600cb0d029f2b311d66fb97ec6bb7a1c7a2b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1868882Reviewed-by: default avatarShimi Zhang <ctzsm@chromium.org>
Reviewed-by: default avatarBo <boliu@chromium.org>
Commit-Queue: Tony Mak <tonymak@google.com>
Auto-Submit: Tony Mak <tonymak@google.com>
Cr-Commit-Position: refs/heads/master@{#708964}
parent f501d023
...@@ -229,7 +229,6 @@ android_library("content_java") { ...@@ -229,7 +229,6 @@ android_library("content_java") {
"java/src/org/chromium/content/browser/selection/MagnifierWrapper.java", "java/src/org/chromium/content/browser/selection/MagnifierWrapper.java",
"java/src/org/chromium/content/browser/selection/MagnifierWrapperImpl.java", "java/src/org/chromium/content/browser/selection/MagnifierWrapperImpl.java",
"java/src/org/chromium/content/browser/selection/PastePopupMenu.java", "java/src/org/chromium/content/browser/selection/PastePopupMenu.java",
"java/src/org/chromium/content/browser/selection/SelectionEventProxyImpl.java",
"java/src/org/chromium/content/browser/selection/SelectionIndicesConverter.java", "java/src/org/chromium/content/browser/selection/SelectionIndicesConverter.java",
"java/src/org/chromium/content/browser/selection/SelectionInsertionHandleObserver.java", "java/src/org/chromium/content/browser/selection/SelectionInsertionHandleObserver.java",
"java/src/org/chromium/content/browser/selection/SelectionPopupControllerImpl.java", "java/src/org/chromium/content/browser/selection/SelectionPopupControllerImpl.java",
......
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.content.browser.selection;
import android.annotation.TargetApi;
import android.os.Build;
import android.view.textclassifier.TextClassification;
import android.view.textclassifier.TextSelection;
import androidx.annotation.NonNull;
import org.chromium.base.Log;
import java.lang.reflect.Method;
/**
* A wrapper class for Android SmartSelectionEventTracker.SelectionEvent.
* SmartSelectionEventTracker.SelectionEvent is a hidden class, need reflection to access to it.
*/
@TargetApi(Build.VERSION_CODES.O)
public class SelectionEventProxyImpl implements SmartSelectionMetricsLogger.SelectionEventProxy {
private static final String TAG = "SmartSelectionLogger";
private static final boolean DEBUG = false;
private static final String SELECTION_EVENT_CLASS =
"android.view.textclassifier.logging.SmartSelectionEventTracker$SelectionEvent";
// Reflection class and methods.
private static Class<?> sSelectionEventClass;
private static Method sSelectionStartedMethod;
private static Method sSelectionModifiedMethod;
private static Method sSelectionModifiedClassificationMethod;
private static Method sSelectionModifiedSelectionMethod;
private static Method sSelectionActionMethod;
private static Method sSelectionActionClassificationMethod;
private static boolean sReflectionFailed;
public static SelectionEventProxyImpl create() {
if (sReflectionFailed) return null;
// TODO(ctzsm): Remove reflection after SDK updates.
if (sSelectionEventClass == null) {
try {
sSelectionEventClass = Class.forName(SELECTION_EVENT_CLASS);
sSelectionStartedMethod =
sSelectionEventClass.getMethod("selectionStarted", Integer.TYPE);
sSelectionModifiedMethod = sSelectionEventClass.getMethod(
"selectionModified", Integer.TYPE, Integer.TYPE);
sSelectionModifiedClassificationMethod = sSelectionEventClass.getMethod(
"selectionModified", Integer.TYPE, Integer.TYPE, TextClassification.class);
sSelectionModifiedSelectionMethod = sSelectionEventClass.getMethod(
"selectionModified", Integer.TYPE, Integer.TYPE, TextSelection.class);
sSelectionActionMethod = sSelectionEventClass.getMethod(
"selectionAction", Integer.TYPE, Integer.TYPE, Integer.TYPE);
sSelectionActionClassificationMethod =
sSelectionEventClass.getMethod("selectionAction", Integer.TYPE,
Integer.TYPE, Integer.TYPE, TextClassification.class);
} catch (ClassNotFoundException | NoSuchMethodException e) {
if (DEBUG) Log.d(TAG, "Reflection failure", e);
sReflectionFailed = true;
return null;
}
}
return new SelectionEventProxyImpl();
}
private SelectionEventProxyImpl() {}
// Reflection wrapper of SelectionEvent#selectionStarted(int)
@Override
public Object createSelectionStarted(int start) {
try {
return sSelectionStartedMethod.invoke(null, start);
} catch (ReflectiveOperationException e) {
// Avoid crashes due to logging.
if (DEBUG) Log.d(TAG, "Reflection failure", e);
}
return null;
}
// Reflection wrapper of SelectionEvent#selectionModified(int, int)
@Override
public Object createSelectionModified(int start, int end) {
try {
return sSelectionModifiedMethod.invoke(null, start, end);
} catch (ReflectiveOperationException e) {
// Avoid crashes due to logging.
if (DEBUG) Log.d(TAG, "Reflection failure", e);
}
return null;
}
// Reflection wrapper of SelectionEvent#selectionModified(int, int, TextClassification)
@Override
public Object createSelectionModifiedClassification(
int start, int end, @NonNull Object classification) {
try {
return sSelectionModifiedClassificationMethod.invoke(
null, start, end, (TextClassification) classification);
} catch (ReflectiveOperationException e) {
// Avoid crashes due to logging.
if (DEBUG) Log.d(TAG, "Reflection failure", e);
}
return null;
}
// Reflection wrapper of SelectionEvent#selectionModified(int, int, TextSelection)
@Override
public Object createSelectionModifiedSelection(int start, int end, @NonNull Object selection) {
try {
return sSelectionModifiedSelectionMethod.invoke(
null, start, end, (TextSelection) selection);
} catch (ReflectiveOperationException e) {
// Avoid crashes due to logging.
if (DEBUG) Log.d(TAG, "Reflection failure", e);
}
return null;
}
// Reflection wrapper of SelectionEvent#selectionAction(int, int, int)
@Override
public Object createSelectionAction(int start, int end, int actionType) {
try {
return sSelectionActionMethod.invoke(null, start, end, actionType);
} catch (ReflectiveOperationException e) {
// Avoid crashes due to logging.
if (DEBUG) Log.d(TAG, "Reflection failure", e);
}
return null;
}
// Reflection wrapper of SelectionEvent#selectionAction(int, int, int, TextClassification)
@Override
public Object createSelectionAction(
int start, int end, int actionType, @NonNull Object classification) {
try {
return sSelectionActionClassificationMethod.invoke(
null, start, end, actionType, (TextClassification) classification);
} catch (ReflectiveOperationException e) {
// Avoid crashes due to logging.
if (DEBUG) Log.d(TAG, "Reflection failure", e);
}
return null;
}
}
...@@ -31,6 +31,7 @@ import android.view.MenuItem; ...@@ -31,6 +31,7 @@ import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.WindowManager; import android.view.WindowManager;
import android.view.textclassifier.SelectionEvent;
import android.view.textclassifier.TextClassifier; import android.view.textclassifier.TextClassifier;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
...@@ -374,7 +375,7 @@ public class SelectionPopupControllerImpl extends ActionModeCallbackHelper ...@@ -374,7 +375,7 @@ public class SelectionPopupControllerImpl extends ActionModeCallbackHelper
break; break;
case MenuSourceType.MENU_SOURCE_ADJUST_SELECTION_RESET: case MenuSourceType.MENU_SOURCE_ADJUST_SELECTION_RESET:
mSelectionMetricsLogger.logSelectionAction(mLastSelectedText, mSelectionMetricsLogger.logSelectionAction(mLastSelectedText,
mLastSelectionOffset, SmartSelectionMetricsLogger.ActionType.RESET, mLastSelectionOffset, SelectionEvent.ACTION_RESET,
/* SelectionClient.Result = */ null); /* SelectionClient.Result = */ null);
break; break;
case MenuSourceType.MENU_SOURCE_TOUCH_HANDLE: case MenuSourceType.MENU_SOURCE_TOUCH_HANDLE:
...@@ -971,28 +972,28 @@ public class SelectionPopupControllerImpl extends ActionModeCallbackHelper ...@@ -971,28 +972,28 @@ public class SelectionPopupControllerImpl extends ActionModeCallbackHelper
private int getActionType(int menuItemId, int menuItemGroupId) { private int getActionType(int menuItemId, int menuItemGroupId) {
if (menuItemGroupId == android.R.id.textAssist) { if (menuItemGroupId == android.R.id.textAssist) {
return SmartSelectionMetricsLogger.ActionType.SMART_SHARE; return SelectionEvent.ACTION_SMART_SHARE;
} }
if (menuItemId == R.id.select_action_menu_select_all) { if (menuItemId == R.id.select_action_menu_select_all) {
return SmartSelectionMetricsLogger.ActionType.SELECT_ALL; return SelectionEvent.ACTION_SELECT_ALL;
} }
if (menuItemId == R.id.select_action_menu_cut) { if (menuItemId == R.id.select_action_menu_cut) {
return SmartSelectionMetricsLogger.ActionType.CUT; return SelectionEvent.ACTION_CUT;
} }
if (menuItemId == R.id.select_action_menu_copy) { if (menuItemId == R.id.select_action_menu_copy) {
return SmartSelectionMetricsLogger.ActionType.COPY; return SelectionEvent.ACTION_COPY;
} }
if (menuItemId == R.id.select_action_menu_paste if (menuItemId == R.id.select_action_menu_paste
|| menuItemId == R.id.select_action_menu_paste_as_plain_text) { || menuItemId == R.id.select_action_menu_paste_as_plain_text) {
return SmartSelectionMetricsLogger.ActionType.PASTE; return SelectionEvent.ACTION_PASTE;
} }
if (menuItemId == R.id.select_action_menu_share) { if (menuItemId == R.id.select_action_menu_share) {
return SmartSelectionMetricsLogger.ActionType.SHARE; return SelectionEvent.ACTION_SHARE;
} }
if (menuItemId == android.R.id.textAssist) { if (menuItemId == android.R.id.textAssist) {
return SmartSelectionMetricsLogger.ActionType.SMART_SHARE; return SelectionEvent.ACTION_SMART_SHARE;
} }
return SmartSelectionMetricsLogger.ActionType.OTHER; return SelectionEvent.ACTION_OTHER;
} }
/** /**
...@@ -1396,7 +1397,7 @@ public class SelectionPopupControllerImpl extends ActionModeCallbackHelper ...@@ -1396,7 +1397,7 @@ public class SelectionPopupControllerImpl extends ActionModeCallbackHelper
if (unSelected) { if (unSelected) {
if (mSelectionMetricsLogger != null) { if (mSelectionMetricsLogger != null) {
mSelectionMetricsLogger.logSelectionAction(mLastSelectedText, mLastSelectionOffset, mSelectionMetricsLogger.logSelectionAction(mLastSelectedText, mLastSelectionOffset,
SmartSelectionMetricsLogger.ActionType.ABANDON, SelectionEvent.ACTION_ABANDON,
/* SelectionClient.Result = */ null); /* SelectionClient.Result = */ null);
} }
destroyActionModeAndKeepSelection(); destroyActionModeAndKeepSelection();
......
...@@ -7,19 +7,15 @@ package org.chromium.content.browser.selection; ...@@ -7,19 +7,15 @@ package org.chromium.content.browser.selection;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.Context; import android.content.Context;
import android.os.Build; import android.os.Build;
import android.view.textclassifier.SelectionEvent;
import androidx.annotation.IntDef; import android.view.textclassifier.TextClassificationContext;
import android.view.textclassifier.TextClassificationManager;
import android.view.textclassifier.TextClassifier;
import org.chromium.base.Log; import org.chromium.base.Log;
import org.chromium.base.VisibleForTesting;
import org.chromium.content_public.browser.SelectionClient; import org.chromium.content_public.browser.SelectionClient;
import org.chromium.content_public.browser.SelectionMetricsLogger; import org.chromium.content_public.browser.SelectionMetricsLogger;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
/** /**
* Smart Selection logger, wrapper of Android logger methods. * Smart Selection logger, wrapper of Android logger methods.
* We are logging word indices here. For one example: * We are logging word indices here. For one example:
...@@ -30,165 +26,44 @@ import java.lang.reflect.Method; ...@@ -30,165 +26,44 @@ import java.lang.reflect.Method;
* that, we single tap on "City", Smart Selection reset get triggered, we need to log [1, 2). Spaces * that, we single tap on "City", Smart Selection reset get triggered, we need to log [1, 2). Spaces
* are ignored but we count each punctuation mark as a word. * are ignored but we count each punctuation mark as a word.
*/ */
@TargetApi(Build.VERSION_CODES.O) @TargetApi(Build.VERSION_CODES.P)
public class SmartSelectionMetricsLogger implements SelectionMetricsLogger { public class SmartSelectionMetricsLogger implements SelectionMetricsLogger {
private static final String TAG = "SmartSelectionLogger"; private static final String TAG = "SmartSelectionLogger";
private static final boolean DEBUG = false; private static final boolean DEBUG = false;
// Reflection classes, constructor and method.
private static final String TRACKER_CLASS =
"android.view.textclassifier.logging.SmartSelectionEventTracker";
private static Class<?> sTrackerClass;
private static Class<?> sSelectionEventClass;
private static Constructor sTrackerConstructor;
private static Method sLogEventMethod;
private static boolean sReflectionFailed;
private Context mContext; private Context mContext;
private Object mTracker;
private SelectionEventProxy mSelectionEventProxy; private TextClassifier mSession;
private SelectionIndicesConverter mConverter;
// ActionType, from SmartSelectionEventTracker.SelectionEvent class.
@IntDef({ActionType.OVERTYPE, ActionType.COPY, ActionType.PASTE, ActionType.CUT,
ActionType.SHARE, ActionType.SMART_SHARE, ActionType.DRAG, ActionType.ABANDON,
ActionType.OTHER, ActionType.SELECT_ALL, ActionType.RESET})
@Retention(RetentionPolicy.SOURCE)
public @interface ActionType {
/** User typed over the selection. */
int OVERTYPE = 100;
/** User copied the selection. */
int COPY = 101;
/** User pasted over the selection. */
int PASTE = 102;
/** User cut the selection. */
int CUT = 103;
/** User shared the selection. */
int SHARE = 104;
/** User clicked the textAssist menu item. */
int SMART_SHARE = 105;
/** User dragged+dropped the selection. */
int DRAG = 106;
/** User abandoned the selection. */
int ABANDON = 107;
/** User performed an action on the selection. */
int OTHER = 108;
/* Non-terminal actions. */
/** User activated Select All */
int SELECT_ALL = 200;
/** User reset the smart selection. */
int RESET = 201;
}
/** private SelectionIndicesConverter mConverter;
* SmartSelectionEventTracker.SelectionEvent class proxy. Having this interface for testing
* purpose.
*/
// TODO(ctzsm): Replace Object with corresponding APIs after Robolectric updated.
public static interface SelectionEventProxy {
/**
* Creates a SelectionEvent for selection started event.
* @param start Start word index.
*/
Object createSelectionStarted(int start);
/**
* Creates a SelectionEvent for selection modified event.
* @param start Start word index.
* @param end End word index.
*/
Object createSelectionModified(int start, int end);
/**
* Creates a SelectionEvent for selection modified event.
* @param start Start word index.
* @param end End word index.
* @param classification {@link android.view.textclassifier.TextClassification object} to
* log.
*/
Object createSelectionModifiedClassification(int start, int end, Object classification);
/**
* Creates a SelectionEvent for selection modified event.
* @param start Start word index.
* @param end End word index.
* @param selection {@link android.view.textclassifier.TextSelection} object to log.
*/
Object createSelectionModifiedSelection(int start, int end, Object selection);
/**
* Creates a SelectionEvent for taking action on selection.
* @param start Start word index.
* @param end End word index.
* @param actionType The action type defined in SelectionMetricsLogger.ActionType.
*/
Object createSelectionAction(int start, int end, int actionType);
/**
* Creates a SelectionEvent for taking action on selection.
* @param start Start word index.
* @param end End word index.
* @param actionType The action type defined in SelectionMetricsLogger.ActionType.
* @param classification {@link android.view.textclassifier.TextClassification object} to
* log.
*/
Object createSelectionAction(int start, int end, int actionType, Object classification);
}
public static SmartSelectionMetricsLogger create(Context context) { public static SmartSelectionMetricsLogger create(Context context) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O || context == null if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P || context == null) {
|| sReflectionFailed) {
return null; return null;
} }
return new SmartSelectionMetricsLogger(context);
// TODO(ctzsm): Remove reflection after SDK updates.
if (sTrackerClass == null) {
try {
sTrackerClass = Class.forName(TRACKER_CLASS);
sSelectionEventClass = Class.forName(TRACKER_CLASS + "$SelectionEvent");
sTrackerConstructor = sTrackerClass.getConstructor(Context.class, Integer.TYPE);
sLogEventMethod = sTrackerClass.getMethod("logEvent", sSelectionEventClass);
} catch (ClassNotFoundException | NoSuchMethodException e) {
if (DEBUG) Log.d(TAG, "Reflection failure", e);
sReflectionFailed = true;
return null;
}
}
SelectionEventProxy selectionEventProxy = SelectionEventProxyImpl.create();
if (selectionEventProxy == null) return null;
return new SmartSelectionMetricsLogger(context, selectionEventProxy);
} }
private SmartSelectionMetricsLogger(Context context, SelectionEventProxy selectionEventProxy) { private SmartSelectionMetricsLogger(Context context) {
mContext = context; mContext = context;
mSelectionEventProxy = selectionEventProxy;
}
@VisibleForTesting
protected SmartSelectionMetricsLogger(SelectionEventProxy selectionEventProxy) {
mSelectionEventProxy = selectionEventProxy;
} }
public void logSelectionStarted(String selectionText, int startOffset, boolean editable) { public void logSelectionStarted(String selectionText, int startOffset, boolean editable) {
mTracker = createTracker(mContext, editable); mSession = createSession(mContext, editable);
mConverter = new SelectionIndicesConverter(); mConverter = new SelectionIndicesConverter();
mConverter.updateSelectionState(selectionText, startOffset); mConverter.updateSelectionState(selectionText, startOffset);
mConverter.setInitialStartOffset(startOffset); mConverter.setInitialStartOffset(startOffset);
if (DEBUG) Log.d(TAG, "logSelectionStarted"); if (DEBUG) Log.d(TAG, "logSelectionStarted");
logEvent(mSelectionEventProxy.createSelectionStarted(0)); logEvent(SelectionEvent.createSelectionStartedEvent(SelectionEvent.INVOCATION_MANUAL, 0));
} }
public void logSelectionModified( public void logSelectionModified(
String selectionText, int startOffset, SelectionClient.Result result) { String selectionText, int startOffset, SelectionClient.Result result) {
if (mTracker == null) return; if (mSession == null) return;
if (!mConverter.updateSelectionState(selectionText, startOffset)) { if (!mConverter.updateSelectionState(selectionText, startOffset)) {
// DOM change detected, end logging session. // DOM change detected, end logging session.
mTracker = null; endTextClassificationSession();
return; return;
} }
...@@ -196,28 +71,30 @@ public class SmartSelectionMetricsLogger implements SelectionMetricsLogger { ...@@ -196,28 +71,30 @@ public class SmartSelectionMetricsLogger implements SelectionMetricsLogger {
int[] indices = new int[2]; int[] indices = new int[2];
if (!mConverter.getWordDelta(startOffset, endOffset, indices)) { if (!mConverter.getWordDelta(startOffset, endOffset, indices)) {
// Invalid indices, end logging session. // Invalid indices, end logging session.
mTracker = null; endTextClassificationSession();
return; return;
} }
if (DEBUG) Log.d(TAG, "logSelectionModified [%d, %d)", indices[0], indices[1]); if (DEBUG) Log.d(TAG, "logSelectionModified [%d, %d)", indices[0], indices[1]);
if (result != null && result.textSelection != null) { if (result != null && result.textSelection != null) {
logEvent(mSelectionEventProxy.createSelectionModifiedSelection( logEvent(SelectionEvent.createSelectionModifiedEvent(
indices[0], indices[1], result.textSelection)); indices[0], indices[1], result.textSelection));
} else if (result != null && result.textClassification != null) { } else if (result != null && result.textClassification != null) {
logEvent(mSelectionEventProxy.createSelectionModifiedClassification( logEvent(SelectionEvent.createSelectionModifiedEvent(
indices[0], indices[1], result.textClassification)); indices[0], indices[1], result.textClassification));
} else { } else {
logEvent(mSelectionEventProxy.createSelectionModified(indices[0], indices[1])); logEvent(SelectionEvent.createSelectionModifiedEvent(indices[0], indices[1]));
} }
} }
public void logSelectionAction(String selectionText, int startOffset, @ActionType int action, public void logSelectionAction(
SelectionClient.Result result) { String selectionText, int startOffset, int action, SelectionClient.Result result) {
if (mTracker == null) return; if (mSession == null) {
return;
}
if (!mConverter.updateSelectionState(selectionText, startOffset)) { if (!mConverter.updateSelectionState(selectionText, startOffset)) {
// DOM change detected, end logging session. // DOM change detected, end logging session.
mTracker = null; endTextClassificationSession();
return; return;
} }
...@@ -225,7 +102,7 @@ public class SmartSelectionMetricsLogger implements SelectionMetricsLogger { ...@@ -225,7 +102,7 @@ public class SmartSelectionMetricsLogger implements SelectionMetricsLogger {
int[] indices = new int[2]; int[] indices = new int[2];
if (!mConverter.getWordDelta(startOffset, endOffset, indices)) { if (!mConverter.getWordDelta(startOffset, endOffset, indices)) {
// Invalid indices, end logging session. // Invalid indices, end logging session.
mTracker = null; endTextClassificationSession();
return; return;
} }
...@@ -235,51 +112,38 @@ public class SmartSelectionMetricsLogger implements SelectionMetricsLogger { ...@@ -235,51 +112,38 @@ public class SmartSelectionMetricsLogger implements SelectionMetricsLogger {
} }
if (result != null && result.textClassification != null) { if (result != null && result.textClassification != null) {
logEvent(mSelectionEventProxy.createSelectionAction( logEvent(SelectionEvent.createSelectionActionEvent(
indices[0], indices[1], action, result.textClassification)); indices[0], indices[1], action, result.textClassification));
} else { } else {
logEvent(mSelectionEventProxy.createSelectionAction(indices[0], indices[1], action)); logEvent(SelectionEvent.createSelectionActionEvent(indices[0], indices[1], action));
} }
if (isTerminal(action)) mTracker = null; if (SelectionEvent.isTerminal(action)) {
endTextClassificationSession();
}
} }
public Object createTracker(Context context, boolean editable) { public TextClassifier createSession(Context context, boolean editable) {
try { TextClassificationContext textClassificationContext =
return sTrackerConstructor.newInstance( new TextClassificationContext
context, editable ? /* EDIT_WEBVIEW */ 4 : /* WEBVIEW */ 2); .Builder(mContext.getPackageName(),
} catch (ReflectiveOperationException e) { editable ? TextClassifier.WIDGET_TYPE_EDIT_WEBVIEW
// Avoid crashes due to logging. : TextClassifier.WIDGET_TYPE_WEBVIEW)
if (DEBUG) Log.d(TAG, "Reflection failure", e); .build();
} TextClassificationManager tcm = (TextClassificationManager) context.getSystemService(
return null; Context.TEXT_CLASSIFICATION_SERVICE);
return tcm.createTextClassificationSession(textClassificationContext);
} }
// Reflection wrapper of SmartSelectionEventTracker#logEvent(SelectionEvent) private void endTextClassificationSession() {
public void logEvent(Object selectionEvent) { if (mSession == null || mSession.isDestroyed()) {
if (selectionEvent == null) return; return;
try {
sLogEventMethod.invoke(mTracker, sSelectionEventClass.cast(selectionEvent));
} catch (ClassCastException | ReflectiveOperationException e) {
// Avoid crashes due to logging.
if (DEBUG) Log.d(TAG, "Reflection failure", e);
} }
mSession.destroy();
mSession = null;
} }
public static boolean isTerminal(@ActionType int actionType) { public void logEvent(SelectionEvent selectionEvent) {
switch (actionType) { mSession.onSelectionEvent(selectionEvent);
case ActionType.OVERTYPE: // fall through
case ActionType.COPY: // fall through
case ActionType.PASTE: // fall through
case ActionType.CUT: // fall through
case ActionType.SHARE: // fall through
case ActionType.SMART_SHARE: // fall through
case ActionType.DRAG: // fall through
case ActionType.ABANDON: // fall through
case ActionType.OTHER: // fall through
return true;
default:
return false;
}
} }
} }
...@@ -73,8 +73,16 @@ public class SmartSelectionProvider { ...@@ -73,8 +73,16 @@ public class SmartSelectionProvider {
} }
} }
@TargetApi(Build.VERSION_CODES.O)
public void setTextClassifier(TextClassifier textClassifier) { public void setTextClassifier(TextClassifier textClassifier) {
mTextClassifier = textClassifier; mTextClassifier = textClassifier;
Context context = mWindowAndroid.getContext().get();
if (context == null) {
return;
}
((TextClassificationManager) context.getSystemService(Context.TEXT_CLASSIFICATION_SERVICE))
.setTextClassifier(textClassifier);
} }
// TODO(wnwen): Remove this suppression once the constant is added to lint. // TODO(wnwen): Remove this suppression once the constant is added to lint.
......
...@@ -8,16 +8,21 @@ import static org.junit.Assert.assertEquals; ...@@ -8,16 +8,21 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import android.content.Context; import android.view.textclassifier.SelectionEvent;
import android.view.textclassifier.TextClassificationManager;
import android.view.textclassifier.TextClassifier;
import androidx.test.core.app.ApplicationProvider;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder; import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
...@@ -25,7 +30,6 @@ import org.robolectric.shadows.ShadowLog; ...@@ -25,7 +30,6 @@ import org.robolectric.shadows.ShadowLog;
import org.chromium.base.test.BaseRobolectricTestRunner; import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Feature; import org.chromium.base.test.util.Feature;
import org.chromium.content.browser.selection.SmartSelectionMetricsLogger.ActionType;
import java.text.BreakIterator; import java.text.BreakIterator;
...@@ -35,6 +39,9 @@ import java.text.BreakIterator; ...@@ -35,6 +39,9 @@ import java.text.BreakIterator;
@RunWith(BaseRobolectricTestRunner.class) @RunWith(BaseRobolectricTestRunner.class)
@Config(manifest = Config.NONE) @Config(manifest = Config.NONE)
public class SmartSelectionMetricsLoggerTest { public class SmartSelectionMetricsLoggerTest {
@Mock
private TextClassifier mTextClassifier;
// Char index (in 10s) 0 1 2 3 4 // Char index (in 10s) 0 1 2 3 4
// Word index (thou) -7-6 -5-4 -3-2 -1 0 1 2 // Word index (thou) -7-6 -5-4 -3-2 -1 0 1 2
private static String sText = "O Romeo, Romeo! Wherefore art thou Romeo?\n" private static String sText = "O Romeo, Romeo! Wherefore art thou Romeo?\n"
...@@ -47,26 +54,16 @@ public class SmartSelectionMetricsLoggerTest { ...@@ -47,26 +54,16 @@ public class SmartSelectionMetricsLoggerTest {
// 3 4 5 // 3 4 5
// 4 567 8 9 0 1 2 34 // 4 567 8 9 0 1 2 34
+ "And I’ll no longer be a Capulet.\n"; + "And I’ll no longer be a Capulet.\n";
private static class TestSmartSelectionMetricsLogger extends SmartSelectionMetricsLogger {
public TestSmartSelectionMetricsLogger(SelectionEventProxy selectionEventProxy) {
super(selectionEventProxy);
}
@Override
public void logEvent(Object selectionEvent) {
// no-op
}
@Override
public Object createTracker(Context context, boolean editable) {
return new Object();
}
}
@Before @Before
public void setUp() { public void setUp() {
MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this);
ShadowLog.stream = System.out; ShadowLog.stream = System.out;
TextClassificationManager tcm =
ApplicationProvider.getApplicationContext().getSystemService(
TextClassificationManager.class);
tcm.setTextClassifier(mTextClassifier);
} }
@Test @Test
...@@ -306,146 +303,221 @@ public class SmartSelectionMetricsLoggerTest { ...@@ -306,146 +303,221 @@ public class SmartSelectionMetricsLoggerTest {
@Test @Test
@Feature({"TextInput", "SmartSelection"}) @Feature({"TextInput", "SmartSelection"})
public void testNormalLoggingFlow() { public void testNormalLoggingFlow() {
SmartSelectionMetricsLogger.SelectionEventProxy selectionEventProxy = SmartSelectionMetricsLogger logger =
Mockito.mock(SmartSelectionMetricsLogger.SelectionEventProxy.class); SmartSelectionMetricsLogger.create(ApplicationProvider.getApplicationContext());
TestSmartSelectionMetricsLogger logger = ArgumentCaptor<SelectionEvent> captor = ArgumentCaptor.forClass(SelectionEvent.class);
new TestSmartSelectionMetricsLogger(selectionEventProxy); InOrder inOrder = inOrder(mTextClassifier);
InOrder order = inOrder(selectionEventProxy);
// Start to select, selected "thou" in row#1. // Start to select, selected "thou" in row#1.
logger.logSelectionStarted("thou", 30, /* editable = */ false); logger.logSelectionStarted("thou", 30, /* editable = */ false);
order.verify(selectionEventProxy).createSelectionStarted(0); inOrder.verify(mTextClassifier).onSelectionEvent(captor.capture());
SelectionEvent selectionEvent = captor.getValue();
assertSelectionStartedEvent(selectionEvent);
// Smart Selection, expand to "Wherefore art thou Romeo?". // Smart Selection, expand to "Wherefore art thou Romeo?".
logger.logSelectionModified("Wherefore art thou Romeo?", 16, null); logger.logSelectionModified("Wherefore art thou Romeo?", 16, null);
order.verify(selectionEventProxy).createSelectionModified(-2, 3); inOrder.verify(mTextClassifier).onSelectionEvent(captor.capture());
selectionEvent = captor.getValue();
assertEvent(selectionEvent, SelectionEvent.EVENT_SELECTION_MODIFIED, /*expectedStart=*/-2,
/*expectedEnd=*/3);
// Smart Selection reset, to the last Romeo in row#1. // Smart Selection reset, to the last Romeo in row#1.
logger.logSelectionAction("Romeo", 35, ActionType.RESET, null); logger.logSelectionAction("Romeo", 35, SelectionEvent.ACTION_RESET, null);
order.verify(selectionEventProxy).createSelectionAction(1, 2, ActionType.RESET); inOrder.verify(mTextClassifier).onSelectionEvent(captor.capture());
selectionEvent = captor.getValue();
assertEquals(SelectionEvent.ACTION_RESET, selectionEvent.getEventType());
assertEvent(selectionEvent, SelectionEvent.ACTION_RESET, /*expectedStart=*/1,
/*expectedEnd=*/2);
// User clear selection. // User clear selection.
logger.logSelectionAction("Romeo", 35, ActionType.ABANDON, null); logger.logSelectionAction("Romeo", 35, SelectionEvent.ACTION_ABANDON, null);
order.verify(selectionEventProxy).createSelectionAction(1, 2, ActionType.ABANDON); inOrder.verify(mTextClassifier).onSelectionEvent(captor.capture());
selectionEvent = captor.getValue();
assertEvent(selectionEvent, SelectionEvent.ACTION_ABANDON, /*expectedStart=*/1,
/*expectedEnd=*/2);
// User start a new selection without abandon. // User start a new selection without abandon.
logger.logSelectionStarted("thou", 30, /* editable = */ false); logger.logSelectionStarted("thou", 30, /* editable = */ false);
order.verify(selectionEventProxy).createSelectionStarted(0); inOrder.verify(mTextClassifier).onSelectionEvent(captor.capture());
selectionEvent = captor.getValue();
assertSelectionStartedEvent(selectionEvent);
// Smart Selection, expand to "Wherefore art thou Romeo?". // Smart Selection, expand to "Wherefore art thou Romeo?".
logger.logSelectionModified("Wherefore art thou Romeo?", 16, null); logger.logSelectionModified("Wherefore art thou Romeo?", 16, null);
order.verify(selectionEventProxy).createSelectionModified(-2, 3); inOrder.verify(mTextClassifier).onSelectionEvent(captor.capture());
selectionEvent = captor.getValue();
assertEvent(selectionEvent, SelectionEvent.EVENT_SELECTION_MODIFIED, /*expectedStart=*/-2,
/*expectedEnd=*/3);
// COPY, PASTE, CUT, SHARE, SMART_SHARE are basically the same. // COPY, PASTE, CUT, SHARE, SMART_SHARE are basically the same.
logger.logSelectionAction("Wherefore art thou Romeo?", 16, ActionType.COPY, null); logger.logSelectionAction(
order.verify(selectionEventProxy).createSelectionAction(-2, 3, ActionType.COPY); "Wherefore art thou Romeo?", 16, SelectionEvent.ACTION_COPY, null);
inOrder.verify(mTextClassifier).onSelectionEvent(captor.capture());
selectionEvent = captor.getValue();
assertEvent(selectionEvent, SelectionEvent.ACTION_COPY, /*expectedStart=*/-2,
/*expectedEnd=*/3);
// SELECT_ALL // SELECT_ALL
logger.logSelectionStarted("thou", 30, /* editable = */ true); logger.logSelectionStarted("thou", 30, /* editable = */ true);
order.verify(selectionEventProxy).createSelectionStarted(0); inOrder.verify(mTextClassifier).onSelectionEvent(captor.capture());
selectionEvent = captor.getValue();
logger.logSelectionAction(sText, 0, ActionType.SELECT_ALL, null); assertSelectionStartedEvent(selectionEvent);
order.verify(selectionEventProxy).createSelectionAction(-7, 34, ActionType.SELECT_ALL);
logger.logSelectionAction(sText, 0, SelectionEvent.ACTION_SELECT_ALL, null);
inOrder.verify(mTextClassifier).onSelectionEvent(captor.capture());
selectionEvent = captor.getValue();
assertEvent(selectionEvent, SelectionEvent.ACTION_SELECT_ALL, /*expectedStart=*/-7,
/*expectedEnd=*/34);
} }
@Test @Test
@Feature({"TextInput", "SmartSelection"}) @Feature({"TextInput", "SmartSelection"})
public void testMultipleDrag() { public void testMultipleDrag() {
SmartSelectionMetricsLogger.SelectionEventProxy selectionEventProxy = SmartSelectionMetricsLogger logger =
Mockito.mock(SmartSelectionMetricsLogger.SelectionEventProxy.class); SmartSelectionMetricsLogger.create(ApplicationProvider.getApplicationContext());
TestSmartSelectionMetricsLogger logger = ArgumentCaptor<SelectionEvent> captor = ArgumentCaptor.forClass(SelectionEvent.class);
new TestSmartSelectionMetricsLogger(selectionEventProxy); InOrder inOrder = inOrder(mTextClassifier);
InOrder order = inOrder(selectionEventProxy);
// Start new selection. First "Deny" in row#2. // Start new selection. First "Deny" in row#2.
logger.logSelectionStarted("Deny", 42, /* editable = */ false); logger.logSelectionStarted("Deny", 42, /* editable = */ false);
order.verify(selectionEventProxy).createSelectionStarted(0); inOrder.verify(mTextClassifier).onSelectionEvent(captor.capture());
SelectionEvent selectionEvent = captor.getValue();
assertSelectionStartedEvent(selectionEvent);
// Drag right handle to "father". // Drag right handle to "father".
logger.logSelectionModified("Deny thy father", 42, null); logger.logSelectionModified("Deny thy father", 42, null);
order.verify(selectionEventProxy).createSelectionModified(0, 3); inOrder.verify(mTextClassifier).onSelectionEvent(captor.capture());
selectionEvent = captor.getValue();
assertEvent(selectionEvent, SelectionEvent.EVENT_SELECTION_MODIFIED, /*expectedStart=*/0,
/*expectedEnd=*/3);
// Drag left handle to " and refuse" // Drag left handle to " and refuse"
logger.logSelectionModified(" and refuse", 57, null); logger.logSelectionModified(" and refuse", 57, null);
order.verify(selectionEventProxy).createSelectionModified(3, 5); inOrder.verify(mTextClassifier).onSelectionEvent(captor.capture());
selectionEvent = captor.getValue();
assertEvent(selectionEvent, SelectionEvent.EVENT_SELECTION_MODIFIED, /*expectedStart=*/3,
/*expectedEnd=*/5);
// Drag right handle to " Romeo?\nDeny thy father". // Drag right handle to " Romeo?\nDeny thy father".
logger.logSelectionModified(" Romeo?\nDeny thy father", 34, null); logger.logSelectionModified(" Romeo?\nDeny thy father", 34, null);
order.verify(selectionEventProxy).createSelectionModified(-2, 3); inOrder.verify(mTextClassifier).onSelectionEvent(captor.capture());
selectionEvent = captor.getValue();
assertEvent(selectionEvent, SelectionEvent.EVENT_SELECTION_MODIFIED, /*expectedStart=*/-2,
/*expectedEnd=*/3);
// Dismiss the selection. // Dismiss the selection.
logger.logSelectionAction(" Romeo?\nDeny thy father", 34, ActionType.ABANDON, null); logger.logSelectionAction(
order.verify(selectionEventProxy).createSelectionAction(-2, 3, ActionType.ABANDON); " Romeo?\nDeny thy father", 34, SelectionEvent.ACTION_ABANDON, null);
inOrder.verify(mTextClassifier).onSelectionEvent(captor.capture());
selectionEvent = captor.getValue();
assertEvent(selectionEvent, SelectionEvent.ACTION_ABANDON, /*expectedStart=*/-2,
/*expectedEnd=*/3);
// Start a new selection. // Start a new selection.
logger.logSelectionStarted("Deny", 42, /* editable = */ false); logger.logSelectionStarted("Deny", 42, /* editable = */ false);
order.verify(selectionEventProxy).createSelectionStarted(0); inOrder.verify(mTextClassifier).onSelectionEvent(captor.capture());
selectionEvent = captor.getValue();
assertSelectionStartedEvent(selectionEvent);
} }
@Test @Test
@Feature({"TextInput", "SmartSelection"}) @Feature({"TextInput", "SmartSelection"})
public void testTextShift() { public void testTextShift() {
SmartSelectionMetricsLogger.SelectionEventProxy selectionEventProxy = SmartSelectionMetricsLogger logger =
Mockito.mock(SmartSelectionMetricsLogger.SelectionEventProxy.class); SmartSelectionMetricsLogger.create(ApplicationProvider.getApplicationContext());
TestSmartSelectionMetricsLogger logger = ArgumentCaptor<SelectionEvent> captor = ArgumentCaptor.forClass(SelectionEvent.class);
new TestSmartSelectionMetricsLogger(selectionEventProxy); InOrder inOrder = inOrder(mTextClassifier);
InOrder order = inOrder(selectionEventProxy);
// Start to select, selected "thou" in row#1. // Start to select, selected "thou" in row#1.
logger.logSelectionStarted("thou", 30, /* editable = */ false); logger.logSelectionStarted("thou", 30, /* editable = */ false);
order.verify(selectionEventProxy).createSelectionStarted(0); inOrder.verify(mTextClassifier).onSelectionEvent(captor.capture());
SelectionEvent selectionEvent = captor.getValue();
assertSelectionStartedEvent(selectionEvent);
// Smart Selection, expand to "Wherefore art thou Romeo?". // Smart Selection, expand to "Wherefore art thou Romeo?".
logger.logSelectionModified("Wherefore art thou Romeo?", 30, null); logger.logSelectionModified("Wherefore art thou Romeo?", 30, null);
order.verify(selectionEventProxy, never()).createSelectionModified(anyInt(), anyInt()); inOrder.verify(mTextClassifier, never())
.onSelectionEvent(Mockito.any(SelectionEvent.class));
// Start to select, selected "thou" in row#1. // Start to select, selected "thou" in row#1.
logger.logSelectionStarted("thou", 30, /* editable = */ false); logger.logSelectionStarted("thou", 30, /* editable = */ false);
order.verify(selectionEventProxy).createSelectionStarted(0); inOrder.verify(mTextClassifier).onSelectionEvent(captor.capture());
selectionEvent = captor.getValue();
assertSelectionStartedEvent(selectionEvent);
// Drag. Non-intersect case. // Drag. Non-intersect case.
logger.logSelectionModified("Wherefore art thou", 10, null); logger.logSelectionModified("Wherefore art thou", 10, null);
order.verify(selectionEventProxy, never()).createSelectionModified(anyInt(), anyInt()); inOrder.verify(mTextClassifier, never())
.onSelectionEvent(Mockito.any(SelectionEvent.class));
// Start to select, selected "thou" in row#1. // Start to select, selected "thou" in row#1.
logger.logSelectionStarted("thou", 30, /* editable = */ false); logger.logSelectionStarted("thou", 30, /* editable = */ false);
order.verify(selectionEventProxy).createSelectionStarted(0); inOrder.verify(mTextClassifier).onSelectionEvent(captor.capture());
selectionEvent = captor.getValue();
assertSelectionStartedEvent(selectionEvent);
// Drag. Adjacent case, form "Wherefore art thouthou". Wrong case. // Drag. Adjacent case, form "Wherefore art thouthou". Wrong case.
logger.logSelectionModified("Wherefore art thou", 12, null); logger.logSelectionModified("Wherefore art thou", 12, null);
order.verify(selectionEventProxy).createSelectionModified(-3, 0); inOrder.verify(mTextClassifier).onSelectionEvent(captor.capture());
selectionEvent = captor.getValue();
assertEvent(selectionEvent, SelectionEvent.EVENT_SELECTION_MODIFIED, /*expectedStart=*/-3,
/*expectedEnd=*/0);
} }
@Test @Test
@Feature({"TextInput", "SmartSelection"}) @Feature({"TextInput", "SmartSelection"})
public void testSelectionChanged() { public void testSelectionChanged() {
SmartSelectionMetricsLogger.SelectionEventProxy selectionEventProxy = SmartSelectionMetricsLogger logger =
Mockito.mock(SmartSelectionMetricsLogger.SelectionEventProxy.class); SmartSelectionMetricsLogger.create(ApplicationProvider.getApplicationContext());
TestSmartSelectionMetricsLogger logger = ArgumentCaptor<SelectionEvent> captor = ArgumentCaptor.forClass(SelectionEvent.class);
new TestSmartSelectionMetricsLogger(selectionEventProxy); InOrder inOrder = inOrder(mTextClassifier);
InOrder order = inOrder(selectionEventProxy);
// Start to select, selected "thou" in row#1. // Start to select, selected "thou" in row#1.
logger.logSelectionStarted("thou", 30, /* editable = */ false); logger.logSelectionStarted("thou", 30, /* editable = */ false);
order.verify(selectionEventProxy).createSelectionStarted(0); inOrder.verify(mTextClassifier).onSelectionEvent(captor.capture());
SelectionEvent selectionEvent = captor.getValue();
assertSelectionStartedEvent(selectionEvent);
// Change "thou" to "math". // Change "thou" to "math".
logger.logSelectionModified("Wherefore art math", 16, null); logger.logSelectionModified("Wherefore art math", 16, null);
order.verify(selectionEventProxy, never()).createSelectionModified(anyInt(), anyInt()); inOrder.verify(mTextClassifier, never())
.onSelectionEvent(Mockito.any(SelectionEvent.class));
// Start to select, selected "thou" in row#1. // Start to select, selected "thou" in row#1.
logger.logSelectionStarted("thou", 30, /* editable = */ false); logger.logSelectionStarted("thou", 30, /* editable = */ false);
order.verify(selectionEventProxy).createSelectionStarted(0); inOrder.verify(mTextClassifier).onSelectionEvent(captor.capture());
selectionEvent = captor.getValue();
assertSelectionStartedEvent(selectionEvent);
// Drag while deleting "art ". Wrong case. // Drag while deleting "art ". Wrong case.
logger.logSelectionModified("Wherefore thou", 16, null); logger.logSelectionModified("Wherefore thou", 16, null);
order.verify(selectionEventProxy).createSelectionModified(-2, 0); inOrder.verify(mTextClassifier).onSelectionEvent(captor.capture());
selectionEvent = captor.getValue();
assertEvent(selectionEvent, SelectionEvent.EVENT_SELECTION_MODIFIED, /*expectedStart=*/-2,
/*expectedEnd=*/0);
// Start to select, selected "thou" in row#1. // Start to select, selected "thou" in row#1.
logger.logSelectionStarted("thou", 30, /* editable = */ false); logger.logSelectionStarted("thou", 30, /* editable = */ false);
order.verify(selectionEventProxy).createSelectionStarted(0); inOrder.verify(mTextClassifier).onSelectionEvent(captor.capture());
selectionEvent = captor.getValue();
assertSelectionStartedEvent(selectionEvent);
// Drag while deleting "Wherefore art ". // Drag while deleting "Wherefore art ".
logger.logSelectionModified("thou", 16, null); logger.logSelectionModified("thou", 16, null);
order.verify(selectionEventProxy, never()).createSelectionModified(anyInt(), anyInt()); inOrder.verify(mTextClassifier, never())
.onSelectionEvent(Mockito.any(SelectionEvent.class));
}
private static void assertSelectionStartedEvent(SelectionEvent event) {
assertEquals(SelectionEvent.EVENT_SELECTION_STARTED, event.getEventType());
assertEquals(0, event.getStart());
assertEquals(1, event.getEnd());
}
private static void assertEvent(
SelectionEvent event, int eventType, int expectedStart, int expectedEnd) {
assertEquals(eventType, event.getEventType());
assertEquals(expectedStart, event.getStart());
assertEquals(expectedEnd, event.getEnd());
} }
} }
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment