Loading core/java/android/view/textclassifier/TextClassifier.java +10 −0 Original line number Diff line number Diff line Loading @@ -140,4 +140,14 @@ public interface TextClassifier { @WorkerThread LinksInfo getLinks( @NonNull CharSequence text, int linkMask, @Nullable LocaleList defaultLocales); /** * Logs a TextClassifier event. * * @param source the text classifier used to generate this event * @param event the text classifier related event * @hide */ @WorkerThread default void logEvent(String source, String event) {} } core/java/android/view/textclassifier/TextClassifierImpl.java +12 −1 Original line number Diff line number Diff line Loading @@ -40,6 +40,7 @@ import android.view.View; import android.widget.TextViewMetrics; import com.android.internal.annotations.GuardedBy; import com.android.internal.logging.MetricsLogger; import com.android.internal.util.Preconditions; import java.io.File; Loading Loading @@ -77,6 +78,8 @@ final class TextClassifierImpl implements TextClassifier { private final Context mContext; private final MetricsLogger mMetricsLogger = new MetricsLogger(); private final Object mSmartSelectionLock = new Object(); @GuardedBy("mSmartSelectionLock") // Do not access outside this lock. private Map<Locale, String> mModelFilePaths; Loading Loading @@ -105,7 +108,8 @@ final class TextClassifierImpl implements TextClassifier { if (start <= end && start >= 0 && end <= string.length() && start <= selectionStartIndex && end >= selectionEndIndex) { final TextSelection.Builder tsBuilder = new TextSelection.Builder(start, end); final TextSelection.Builder tsBuilder = new TextSelection.Builder(start, end) .setLogSource(LOG_TAG); final SmartSelection.ClassificationResult[] results = smartSelection.classifyText( string, start, end, Loading Loading @@ -173,6 +177,13 @@ final class TextClassifierImpl implements TextClassifier { return TextClassifier.NO_OP.getLinks(text, linkMask, defaultLocales); } @Override public void logEvent(String source, String event) { if (LOG_TAG.equals(source)) { mMetricsLogger.count(event, 1); } } private SmartSelection getSmartSelection(LocaleList localeList) throws FileNotFoundException { synchronized (mSmartSelectionLock) { localeList = localeList == null ? LocaleList.getEmptyLocaleList() : localeList; Loading core/java/android/view/textclassifier/TextSelection.java +23 −2 Original line number Diff line number Diff line Loading @@ -34,13 +34,16 @@ public final class TextSelection { private final int mEndIndex; @NonNull private final EntityConfidence<String> mEntityConfidence; @NonNull private final List<String> mEntities; @NonNull private final String mLogSource; private TextSelection( int startIndex, int endIndex, @NonNull EntityConfidence<String> entityConfidence) { int startIndex, int endIndex, @NonNull EntityConfidence<String> entityConfidence, @NonNull String logSource) { mStartIndex = startIndex; mEndIndex = endIndex; mEntityConfidence = new EntityConfidence<>(entityConfidence); mEntities = mEntityConfidence.getEntities(); mLogSource = logSource; } /** Loading Loading @@ -87,6 +90,14 @@ public final class TextSelection { return mEntityConfidence.getConfidenceScore(entity); } /** * Returns a tag for the source classifier used to generate this result. * @hide */ public String getSourceClassifier() { return mLogSource; } @Override public String toString() { return String.format("TextSelection {%d, %d, %s}", Loading @@ -102,6 +113,7 @@ public final class TextSelection { private final int mEndIndex; @NonNull private final EntityConfidence<String> mEntityConfidence = new EntityConfidence<>(); @NonNull private String mLogSource = ""; /** * Creates a builder used to build {@link TextSelection} objects. Loading Loading @@ -130,11 +142,20 @@ public final class TextSelection { return this; } /** * Sets a tag for the source classifier used to generate this result. * @hide */ Builder setLogSource(@NonNull String logSource) { mLogSource = Preconditions.checkNotNull(logSource); return this; } /** * Builds and returns {@link TextSelection} object. */ public TextSelection build() { return new TextSelection(mStartIndex, mEndIndex, mEntityConfidence); return new TextSelection(mStartIndex, mEndIndex, mEntityConfidence, mLogSource); } } } core/java/android/widget/Editor.java +2 −0 Original line number Diff line number Diff line Loading @@ -3925,6 +3925,8 @@ public class Editor { @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { getSelectionActionModeHelper().onSelectionAction(); if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) { return true; } Loading core/java/android/widget/SelectionActionModeHelper.java +100 −23 Original line number Diff line number Diff line Loading @@ -56,13 +56,14 @@ final class SelectionActionModeHelper { private TextClassification mTextClassification; private AsyncTask mTextClassificationAsyncTask; private final SelectionInfo mSelectionInfo = new SelectionInfo(); private final SelectionTracker mSelectionTracker; SelectionActionModeHelper(@NonNull Editor editor) { mEditor = Preconditions.checkNotNull(editor); final TextView textView = mEditor.getTextView(); mTextClassificationHelper = new TextClassificationHelper( textView.getTextClassifier(), textView.getText(), 0, 1, textView.getTextLocales()); mSelectionTracker = new SelectionTracker(textView.getTextClassifier()); } public void startActionModeAsync(boolean adjustSelection) { Loading Loading @@ -99,8 +100,13 @@ final class SelectionActionModeHelper { } } public void onSelectionAction() { mSelectionTracker.onSelectionAction(mTextClassificationHelper.getClassifierTag()); } public boolean resetSelection(int textIndex) { if (mSelectionInfo.resetSelection(textIndex, mEditor)) { if (mSelectionTracker.resetSelection( textIndex, mEditor, mTextClassificationHelper.getClassifierTag())) { invalidateActionModeAsync(); return true; } Loading @@ -113,7 +119,7 @@ final class SelectionActionModeHelper { } public void onDestroyActionMode() { mSelectionInfo.onSelectionDestroyed(); mSelectionTracker.onSelectionDestroyed(); cancelAsyncTask(); } Loading @@ -137,7 +143,7 @@ final class SelectionActionModeHelper { private void startActionMode(@Nullable SelectionResult result) { final TextView textView = mEditor.getTextView(); final CharSequence text = textView.getText(); mSelectionInfo.setOriginalSelection( mSelectionTracker.setOriginalSelection( textView.getSelectionStart(), textView.getSelectionEnd()); if (result != null && text instanceof Spannable) { Selection.setSelection((Spannable) text, result.mStart, result.mEnd); Loading @@ -151,7 +157,8 @@ final class SelectionActionModeHelper { controller.show(); } if (result != null) { mSelectionInfo.onSelectionStarted(result.mStart, result.mEnd); mSelectionTracker.onSelectionStarted( result.mStart, result.mEnd, mTextClassificationHelper.getClassifierTag()); } } mEditor.setRestartActionModeOnNextRefresh(false); Loading @@ -165,7 +172,9 @@ final class SelectionActionModeHelper { actionMode.invalidate(); } final TextView textView = mEditor.getTextView(); mSelectionInfo.onSelectionUpdated(textView.getSelectionStart(), textView.getSelectionEnd()); mSelectionTracker.onSelectionUpdated( textView.getSelectionStart(), textView.getSelectionEnd(), mTextClassificationHelper.getClassifierTag()); mTextClassificationAsyncTask = null; } Loading @@ -177,49 +186,111 @@ final class SelectionActionModeHelper { } /** * Holds information about the selection and uses it to decide on whether or not to update * the selection when resetSelection is called. * The expected UX here is to allow the user to select a word inside of the "smart selection" on * a single tap. * Tracks and logs smart selection changes. * It is important to trigger this object's methods at the appropriate event so that it tracks * smart selection events appropriately. */ private static final class SelectionInfo { private static final class SelectionTracker { // Log event: Smart selection happened. private static final String LOG_EVENT_MULTI_SELECTION = "textClassifier_multiSelection"; // Log event: Smart selection acted upon. private static final String LOG_EVENT_MULTI_SELECTION_ACTION = "textClassifier_multiSelection_action"; // Log event: Smart selection was reset to original selection. private static final String LOG_EVENT_MULTI_SELECTION_RESET = "textClassifier_multiSelection_reset"; // Log event: Smart selection was user modified. private static final String LOG_EVENT_MULTI_SELECTION_MODIFIED = "textClassifier_multiSelection_modified"; private final TextClassifier mClassifier; private int mOriginalStart; private int mOriginalEnd; private int mSelectionStart; private int mSelectionEnd; private boolean mResetOriginal; private boolean mSmartSelectionActive; SelectionTracker(TextClassifier classifier) { mClassifier = classifier; } /** * Called to initialize the original selection before smart selection is triggered. */ public void setOriginalSelection(int selectionStart, int selectionEnd) { mOriginalStart = selectionStart; mOriginalEnd = selectionEnd; mResetOriginal = false; mSmartSelectionActive = false; } public void onSelectionStarted(int selectionStart, int selectionEnd) { // Set the reset flag to true if the selection changed. /** * Called when selection action mode is started. * If the selection indices are different from the original selection indices, we have a * smart selection. */ public void onSelectionStarted(int selectionStart, int selectionEnd, String logTag) { mSelectionStart = selectionStart; mSelectionEnd = selectionEnd; mResetOriginal = mSelectionStart != mOriginalStart || mSelectionEnd != mOriginalEnd; // If the started selection is different from the original selection, we have a // smart selection. mSmartSelectionActive = mSelectionStart != mOriginalStart || mSelectionEnd != mOriginalEnd; if (mSmartSelectionActive) { mClassifier.logEvent(logTag, LOG_EVENT_MULTI_SELECTION); } } public void onSelectionUpdated(int selectionStart, int selectionEnd) { // If the selection did not change, maintain the reset state. Otherwise, disable reset. mResetOriginal &= selectionStart == mSelectionStart && selectionEnd == mSelectionEnd; /** * Called when selection bounds change. */ public void onSelectionUpdated(int selectionStart, int selectionEnd, String logTag) { final boolean selectionChanged = selectionStart != mSelectionStart || selectionEnd != mSelectionEnd; if (selectionChanged) { if (mSmartSelectionActive) { mClassifier.logEvent(logTag, LOG_EVENT_MULTI_SELECTION_MODIFIED); } mSmartSelectionActive = false; } } /** * Called when the selection action mode is destroyed. */ public void onSelectionDestroyed() { mResetOriginal = false; mSmartSelectionActive = false; } public boolean resetSelection(int textIndex, Editor editor) { /** * Logs if the action was taken on a smart selection. */ public void onSelectionAction(String logTag) { if (mSmartSelectionActive) { mClassifier.logEvent(logTag, LOG_EVENT_MULTI_SELECTION_ACTION); } } /** * Returns true if the current smart selection should be reset to normal selection based on * information that has been recorded about the original selection and the smart selection. * The expected UX here is to allow the user to select a word inside of the smart selection * on a single tap. */ public boolean resetSelection(int textIndex, Editor editor, String logTag) { final CharSequence text = editor.getTextView().getText(); if (mResetOriginal if (mSmartSelectionActive && textIndex >= mSelectionStart && textIndex <= mSelectionEnd && text instanceof Spannable) { // Only allow a reset once. mResetOriginal = false; mSmartSelectionActive = false; mClassifier.logEvent(logTag, LOG_EVENT_MULTI_SELECTION_RESET); return editor.selectCurrentWord(); } return false; Loading Loading @@ -301,6 +372,7 @@ final class SelectionActionModeHelper { /** End index relative to mText. */ private int mSelectionEnd; private LocaleList mLocales; private String mClassifierTag = ""; /** Trimmed text starting from mTrimStart in mText. */ private CharSequence mTrimmedText; Loading Loading @@ -364,9 +436,14 @@ final class SelectionActionModeHelper { mTrimmedText, mRelativeStart, mRelativeEnd, mLocales); mSelectionStart = Math.max(0, sel.getSelectionStartIndex() + mTrimStart); mSelectionEnd = Math.min(mText.length(), sel.getSelectionEndIndex() + mTrimStart); mClassifierTag = sel.getSourceClassifier(); return classifyText(); } String getClassifierTag() { return mClassifierTag; } private void trimText() { mTrimStart = Math.max(0, mSelectionStart - TRIM_DELTA); final int referenceEnd = Math.min(mText.length(), mSelectionEnd + TRIM_DELTA); Loading Loading
core/java/android/view/textclassifier/TextClassifier.java +10 −0 Original line number Diff line number Diff line Loading @@ -140,4 +140,14 @@ public interface TextClassifier { @WorkerThread LinksInfo getLinks( @NonNull CharSequence text, int linkMask, @Nullable LocaleList defaultLocales); /** * Logs a TextClassifier event. * * @param source the text classifier used to generate this event * @param event the text classifier related event * @hide */ @WorkerThread default void logEvent(String source, String event) {} }
core/java/android/view/textclassifier/TextClassifierImpl.java +12 −1 Original line number Diff line number Diff line Loading @@ -40,6 +40,7 @@ import android.view.View; import android.widget.TextViewMetrics; import com.android.internal.annotations.GuardedBy; import com.android.internal.logging.MetricsLogger; import com.android.internal.util.Preconditions; import java.io.File; Loading Loading @@ -77,6 +78,8 @@ final class TextClassifierImpl implements TextClassifier { private final Context mContext; private final MetricsLogger mMetricsLogger = new MetricsLogger(); private final Object mSmartSelectionLock = new Object(); @GuardedBy("mSmartSelectionLock") // Do not access outside this lock. private Map<Locale, String> mModelFilePaths; Loading Loading @@ -105,7 +108,8 @@ final class TextClassifierImpl implements TextClassifier { if (start <= end && start >= 0 && end <= string.length() && start <= selectionStartIndex && end >= selectionEndIndex) { final TextSelection.Builder tsBuilder = new TextSelection.Builder(start, end); final TextSelection.Builder tsBuilder = new TextSelection.Builder(start, end) .setLogSource(LOG_TAG); final SmartSelection.ClassificationResult[] results = smartSelection.classifyText( string, start, end, Loading Loading @@ -173,6 +177,13 @@ final class TextClassifierImpl implements TextClassifier { return TextClassifier.NO_OP.getLinks(text, linkMask, defaultLocales); } @Override public void logEvent(String source, String event) { if (LOG_TAG.equals(source)) { mMetricsLogger.count(event, 1); } } private SmartSelection getSmartSelection(LocaleList localeList) throws FileNotFoundException { synchronized (mSmartSelectionLock) { localeList = localeList == null ? LocaleList.getEmptyLocaleList() : localeList; Loading
core/java/android/view/textclassifier/TextSelection.java +23 −2 Original line number Diff line number Diff line Loading @@ -34,13 +34,16 @@ public final class TextSelection { private final int mEndIndex; @NonNull private final EntityConfidence<String> mEntityConfidence; @NonNull private final List<String> mEntities; @NonNull private final String mLogSource; private TextSelection( int startIndex, int endIndex, @NonNull EntityConfidence<String> entityConfidence) { int startIndex, int endIndex, @NonNull EntityConfidence<String> entityConfidence, @NonNull String logSource) { mStartIndex = startIndex; mEndIndex = endIndex; mEntityConfidence = new EntityConfidence<>(entityConfidence); mEntities = mEntityConfidence.getEntities(); mLogSource = logSource; } /** Loading Loading @@ -87,6 +90,14 @@ public final class TextSelection { return mEntityConfidence.getConfidenceScore(entity); } /** * Returns a tag for the source classifier used to generate this result. * @hide */ public String getSourceClassifier() { return mLogSource; } @Override public String toString() { return String.format("TextSelection {%d, %d, %s}", Loading @@ -102,6 +113,7 @@ public final class TextSelection { private final int mEndIndex; @NonNull private final EntityConfidence<String> mEntityConfidence = new EntityConfidence<>(); @NonNull private String mLogSource = ""; /** * Creates a builder used to build {@link TextSelection} objects. Loading Loading @@ -130,11 +142,20 @@ public final class TextSelection { return this; } /** * Sets a tag for the source classifier used to generate this result. * @hide */ Builder setLogSource(@NonNull String logSource) { mLogSource = Preconditions.checkNotNull(logSource); return this; } /** * Builds and returns {@link TextSelection} object. */ public TextSelection build() { return new TextSelection(mStartIndex, mEndIndex, mEntityConfidence); return new TextSelection(mStartIndex, mEndIndex, mEntityConfidence, mLogSource); } } }
core/java/android/widget/Editor.java +2 −0 Original line number Diff line number Diff line Loading @@ -3925,6 +3925,8 @@ public class Editor { @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { getSelectionActionModeHelper().onSelectionAction(); if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) { return true; } Loading
core/java/android/widget/SelectionActionModeHelper.java +100 −23 Original line number Diff line number Diff line Loading @@ -56,13 +56,14 @@ final class SelectionActionModeHelper { private TextClassification mTextClassification; private AsyncTask mTextClassificationAsyncTask; private final SelectionInfo mSelectionInfo = new SelectionInfo(); private final SelectionTracker mSelectionTracker; SelectionActionModeHelper(@NonNull Editor editor) { mEditor = Preconditions.checkNotNull(editor); final TextView textView = mEditor.getTextView(); mTextClassificationHelper = new TextClassificationHelper( textView.getTextClassifier(), textView.getText(), 0, 1, textView.getTextLocales()); mSelectionTracker = new SelectionTracker(textView.getTextClassifier()); } public void startActionModeAsync(boolean adjustSelection) { Loading Loading @@ -99,8 +100,13 @@ final class SelectionActionModeHelper { } } public void onSelectionAction() { mSelectionTracker.onSelectionAction(mTextClassificationHelper.getClassifierTag()); } public boolean resetSelection(int textIndex) { if (mSelectionInfo.resetSelection(textIndex, mEditor)) { if (mSelectionTracker.resetSelection( textIndex, mEditor, mTextClassificationHelper.getClassifierTag())) { invalidateActionModeAsync(); return true; } Loading @@ -113,7 +119,7 @@ final class SelectionActionModeHelper { } public void onDestroyActionMode() { mSelectionInfo.onSelectionDestroyed(); mSelectionTracker.onSelectionDestroyed(); cancelAsyncTask(); } Loading @@ -137,7 +143,7 @@ final class SelectionActionModeHelper { private void startActionMode(@Nullable SelectionResult result) { final TextView textView = mEditor.getTextView(); final CharSequence text = textView.getText(); mSelectionInfo.setOriginalSelection( mSelectionTracker.setOriginalSelection( textView.getSelectionStart(), textView.getSelectionEnd()); if (result != null && text instanceof Spannable) { Selection.setSelection((Spannable) text, result.mStart, result.mEnd); Loading @@ -151,7 +157,8 @@ final class SelectionActionModeHelper { controller.show(); } if (result != null) { mSelectionInfo.onSelectionStarted(result.mStart, result.mEnd); mSelectionTracker.onSelectionStarted( result.mStart, result.mEnd, mTextClassificationHelper.getClassifierTag()); } } mEditor.setRestartActionModeOnNextRefresh(false); Loading @@ -165,7 +172,9 @@ final class SelectionActionModeHelper { actionMode.invalidate(); } final TextView textView = mEditor.getTextView(); mSelectionInfo.onSelectionUpdated(textView.getSelectionStart(), textView.getSelectionEnd()); mSelectionTracker.onSelectionUpdated( textView.getSelectionStart(), textView.getSelectionEnd(), mTextClassificationHelper.getClassifierTag()); mTextClassificationAsyncTask = null; } Loading @@ -177,49 +186,111 @@ final class SelectionActionModeHelper { } /** * Holds information about the selection and uses it to decide on whether or not to update * the selection when resetSelection is called. * The expected UX here is to allow the user to select a word inside of the "smart selection" on * a single tap. * Tracks and logs smart selection changes. * It is important to trigger this object's methods at the appropriate event so that it tracks * smart selection events appropriately. */ private static final class SelectionInfo { private static final class SelectionTracker { // Log event: Smart selection happened. private static final String LOG_EVENT_MULTI_SELECTION = "textClassifier_multiSelection"; // Log event: Smart selection acted upon. private static final String LOG_EVENT_MULTI_SELECTION_ACTION = "textClassifier_multiSelection_action"; // Log event: Smart selection was reset to original selection. private static final String LOG_EVENT_MULTI_SELECTION_RESET = "textClassifier_multiSelection_reset"; // Log event: Smart selection was user modified. private static final String LOG_EVENT_MULTI_SELECTION_MODIFIED = "textClassifier_multiSelection_modified"; private final TextClassifier mClassifier; private int mOriginalStart; private int mOriginalEnd; private int mSelectionStart; private int mSelectionEnd; private boolean mResetOriginal; private boolean mSmartSelectionActive; SelectionTracker(TextClassifier classifier) { mClassifier = classifier; } /** * Called to initialize the original selection before smart selection is triggered. */ public void setOriginalSelection(int selectionStart, int selectionEnd) { mOriginalStart = selectionStart; mOriginalEnd = selectionEnd; mResetOriginal = false; mSmartSelectionActive = false; } public void onSelectionStarted(int selectionStart, int selectionEnd) { // Set the reset flag to true if the selection changed. /** * Called when selection action mode is started. * If the selection indices are different from the original selection indices, we have a * smart selection. */ public void onSelectionStarted(int selectionStart, int selectionEnd, String logTag) { mSelectionStart = selectionStart; mSelectionEnd = selectionEnd; mResetOriginal = mSelectionStart != mOriginalStart || mSelectionEnd != mOriginalEnd; // If the started selection is different from the original selection, we have a // smart selection. mSmartSelectionActive = mSelectionStart != mOriginalStart || mSelectionEnd != mOriginalEnd; if (mSmartSelectionActive) { mClassifier.logEvent(logTag, LOG_EVENT_MULTI_SELECTION); } } public void onSelectionUpdated(int selectionStart, int selectionEnd) { // If the selection did not change, maintain the reset state. Otherwise, disable reset. mResetOriginal &= selectionStart == mSelectionStart && selectionEnd == mSelectionEnd; /** * Called when selection bounds change. */ public void onSelectionUpdated(int selectionStart, int selectionEnd, String logTag) { final boolean selectionChanged = selectionStart != mSelectionStart || selectionEnd != mSelectionEnd; if (selectionChanged) { if (mSmartSelectionActive) { mClassifier.logEvent(logTag, LOG_EVENT_MULTI_SELECTION_MODIFIED); } mSmartSelectionActive = false; } } /** * Called when the selection action mode is destroyed. */ public void onSelectionDestroyed() { mResetOriginal = false; mSmartSelectionActive = false; } public boolean resetSelection(int textIndex, Editor editor) { /** * Logs if the action was taken on a smart selection. */ public void onSelectionAction(String logTag) { if (mSmartSelectionActive) { mClassifier.logEvent(logTag, LOG_EVENT_MULTI_SELECTION_ACTION); } } /** * Returns true if the current smart selection should be reset to normal selection based on * information that has been recorded about the original selection and the smart selection. * The expected UX here is to allow the user to select a word inside of the smart selection * on a single tap. */ public boolean resetSelection(int textIndex, Editor editor, String logTag) { final CharSequence text = editor.getTextView().getText(); if (mResetOriginal if (mSmartSelectionActive && textIndex >= mSelectionStart && textIndex <= mSelectionEnd && text instanceof Spannable) { // Only allow a reset once. mResetOriginal = false; mSmartSelectionActive = false; mClassifier.logEvent(logTag, LOG_EVENT_MULTI_SELECTION_RESET); return editor.selectCurrentWord(); } return false; Loading Loading @@ -301,6 +372,7 @@ final class SelectionActionModeHelper { /** End index relative to mText. */ private int mSelectionEnd; private LocaleList mLocales; private String mClassifierTag = ""; /** Trimmed text starting from mTrimStart in mText. */ private CharSequence mTrimmedText; Loading Loading @@ -364,9 +436,14 @@ final class SelectionActionModeHelper { mTrimmedText, mRelativeStart, mRelativeEnd, mLocales); mSelectionStart = Math.max(0, sel.getSelectionStartIndex() + mTrimStart); mSelectionEnd = Math.min(mText.length(), sel.getSelectionEndIndex() + mTrimStart); mClassifierTag = sel.getSourceClassifier(); return classifyText(); } String getClassifierTag() { return mClassifierTag; } private void trimText() { mTrimStart = Math.max(0, mSelectionStart - TRIM_DELTA); final int referenceEnd = Math.min(mText.length(), mSelectionEnd + TRIM_DELTA); Loading