diff --git a/core/java/android/view/textclassifier/SelectionEvent.java b/core/java/android/view/textclassifier/SelectionEvent.java index 105cbcc06496c..9ae0c65e0cff3 100644 --- a/core/java/android/view/textclassifier/SelectionEvent.java +++ b/core/java/android/view/textclassifier/SelectionEvent.java @@ -24,6 +24,7 @@ import android.os.Parcelable; import android.view.textclassifier.TextClassifier.EntityType; import android.view.textclassifier.TextClassifier.WidgetType; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; @@ -115,7 +116,7 @@ public final class SelectionEvent implements Parcelable { /** Unknown invocation method */ public static final int INVOCATION_UNKNOWN = 0; - private static final String NO_SIGNATURE = ""; + static final String NO_SIGNATURE = ""; private final int mAbsoluteStart; private final int mAbsoluteEnd; @@ -374,8 +375,10 @@ public final class SelectionEvent implements Parcelable { /** * Sets the event type. + * @hide */ - void setEventType(@EventType int eventType) { + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + public void setEventType(@EventType int eventType) { mEventType = eventType; } @@ -416,8 +419,10 @@ public final class SelectionEvent implements Parcelable { /** * Sets the {@link TextClassificationContext} for this event. + * @hide */ - void setTextClassificationSessionContext(TextClassificationContext context) { + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + public void setTextClassificationSessionContext(TextClassificationContext context) { mPackageName = context.getPackageName(); mWidgetType = context.getWidgetType(); mWidgetVersion = context.getWidgetVersion(); @@ -432,8 +437,10 @@ public final class SelectionEvent implements Parcelable { /** * Sets the invocationMethod for this event. + * @hide */ - void setInvocationMethod(@InvocationMethod int invocationMethod) { + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + public void setInvocationMethod(@InvocationMethod int invocationMethod) { mInvocationMethod = invocationMethod; } @@ -495,7 +502,9 @@ public final class SelectionEvent implements Parcelable { return mEventIndex; } - SelectionEvent setEventIndex(int index) { + /** @hide */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + public SelectionEvent setEventIndex(int index) { mEventIndex = index; return this; } @@ -508,7 +517,9 @@ public final class SelectionEvent implements Parcelable { return mSessionId; } - SelectionEvent setSessionId(TextClassificationSessionId id) { + /** @hide */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + public SelectionEvent setSessionId(@Nullable TextClassificationSessionId id) { mSessionId = id; return this; } @@ -521,7 +532,9 @@ public final class SelectionEvent implements Parcelable { return mStart; } - SelectionEvent setStart(int start) { + /** @hide */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + public SelectionEvent setStart(int start) { mStart = start; return this; } @@ -534,7 +547,9 @@ public final class SelectionEvent implements Parcelable { return mEnd; } - SelectionEvent setEnd(int end) { + /** @hide */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + public SelectionEvent setEnd(int end) { mEnd = end; return this; } @@ -547,7 +562,9 @@ public final class SelectionEvent implements Parcelable { return mSmartStart; } - SelectionEvent setSmartStart(int start) { + /** @hide */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + public SelectionEvent setSmartStart(int start) { this.mSmartStart = start; return this; } @@ -560,7 +577,9 @@ public final class SelectionEvent implements Parcelable { return mSmartEnd; } - SelectionEvent setSmartEnd(int end) { + /** @hide */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + public SelectionEvent setSmartEnd(int end) { mSmartEnd = end; return this; } diff --git a/core/java/android/view/textclassifier/TextClassificationSession.java b/core/java/android/view/textclassifier/TextClassificationSession.java index 15f54ef2a623e..db0202aa93c08 100644 --- a/core/java/android/view/textclassifier/TextClassificationSession.java +++ b/core/java/android/view/textclassifier/TextClassificationSession.java @@ -84,6 +84,7 @@ final class TextClassificationSession implements TextClassifier { @Override public void onTextClassifierEvent(TextClassifierEvent event) { try { + event.mHiddenTempSessionId = mSessionId; mDelegate.onTextClassifierEvent(event); } catch (Exception e) { // Avoid crashing for event reporting. diff --git a/core/java/android/view/textclassifier/TextClassifierEvent.java b/core/java/android/view/textclassifier/TextClassifierEvent.java index 7b623e9b6391e..57da829b3f446 100644 --- a/core/java/android/view/textclassifier/TextClassifierEvent.java +++ b/core/java/android/view/textclassifier/TextClassifierEvent.java @@ -24,6 +24,7 @@ import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; @@ -150,6 +151,14 @@ public abstract class TextClassifierEvent implements Parcelable { private final ULocale mLocale; private final Bundle mExtras; + /** + * Session id holder to help with converting this event to the legacy SelectionEvent. + * @hide + */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + @Nullable + public TextClassificationSessionId mHiddenTempSessionId; + private TextClassifierEvent(Builder builder) { mEventCategory = builder.mEventCategory; mEventType = builder.mEventType; @@ -359,6 +368,120 @@ public abstract class TextClassifierEvent implements Parcelable { return out.toString(); } + /** + * Returns a {@link SelectionEvent} equivalent of this event; or {@code null} if it can not be + * converted to a {@link SelectionEvent}. + * @hide + */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + @Nullable + public final SelectionEvent toSelectionEvent() { + final int invocationMethod; + switch (getEventCategory()) { + case TextClassifierEvent.CATEGORY_SELECTION: + invocationMethod = SelectionEvent.INVOCATION_MANUAL; + break; + case TextClassifierEvent.CATEGORY_LINKIFY: + invocationMethod = SelectionEvent.INVOCATION_LINK; + break; + default: + // Cannot be converted to a SelectionEvent. + return null; + } + + final String entityType = getEntityTypes().length > 0 + ? getEntityTypes()[0] : TextClassifier.TYPE_UNKNOWN; + final SelectionEvent out = new SelectionEvent( + /* absoluteStart= */ 0, + /* absoluteEnd= */ 0, + /* eventType= */0, + entityType, + SelectionEvent.INVOCATION_UNKNOWN, + SelectionEvent.NO_SIGNATURE); + out.setInvocationMethod(invocationMethod); + + final TextClassificationContext eventContext = getEventContext(); + if (eventContext != null) { + out.setTextClassificationSessionContext(getEventContext()); + } + out.setSessionId(mHiddenTempSessionId); + final String resultId = getResultId(); + out.setResultId(resultId == null ? SelectionEvent.NO_SIGNATURE : resultId); + out.setEventIndex(getEventIndex()); + + + final int eventType; + switch (getEventType()) { + case TextClassifierEvent.TYPE_SELECTION_STARTED: + eventType = SelectionEvent.EVENT_SELECTION_STARTED; + break; + case TextClassifierEvent.TYPE_SELECTION_MODIFIED: + eventType = SelectionEvent.EVENT_SELECTION_MODIFIED; + break; + case TextClassifierEvent.TYPE_SMART_SELECTION_SINGLE: + eventType = SelectionEvent.EVENT_SMART_SELECTION_SINGLE; + break; + case TextClassifierEvent.TYPE_SMART_SELECTION_MULTI: + eventType = SelectionEvent.EVENT_SMART_SELECTION_MULTI; + break; + case TextClassifierEvent.TYPE_AUTO_SELECTION: + eventType = SelectionEvent.EVENT_AUTO_SELECTION; + break; + case TextClassifierEvent.TYPE_OVERTYPE: + eventType = SelectionEvent.ACTION_OVERTYPE; + break; + case TextClassifierEvent.TYPE_COPY_ACTION: + eventType = SelectionEvent.ACTION_COPY; + break; + case TextClassifierEvent.TYPE_PASTE_ACTION: + eventType = SelectionEvent.ACTION_PASTE; + break; + case TextClassifierEvent.TYPE_CUT_ACTION: + eventType = SelectionEvent.ACTION_CUT; + break; + case TextClassifierEvent.TYPE_SHARE_ACTION: + eventType = SelectionEvent.ACTION_SHARE; + break; + case TextClassifierEvent.TYPE_SMART_ACTION: + eventType = SelectionEvent.ACTION_SMART_SHARE; + break; + case TextClassifierEvent.TYPE_SELECTION_DRAG: + eventType = SelectionEvent.ACTION_DRAG; + break; + case TextClassifierEvent.TYPE_SELECTION_DESTROYED: + eventType = SelectionEvent.ACTION_ABANDON; + break; + case TextClassifierEvent.TYPE_OTHER_ACTION: + eventType = SelectionEvent.ACTION_OTHER; + break; + case TextClassifierEvent.TYPE_SELECT_ALL: + eventType = SelectionEvent.ACTION_SELECT_ALL; + break; + case TextClassifierEvent.TYPE_SELECTION_RESET: + eventType = SelectionEvent.ACTION_RESET; + break; + default: + eventType = 0; + break; + } + out.setEventType(eventType); + + if (this instanceof TextClassifierEvent.TextSelectionEvent) { + final TextClassifierEvent.TextSelectionEvent selEvent = + (TextClassifierEvent.TextSelectionEvent) this; + // TODO: Ideally, we should have these fields in events of type + // TextClassifierEvent.TextLinkifyEvent events too but we're now past the API deadline + // and will have to do with these fields being set only in TextSelectionEvent events. + // Fix this at the next API bump. + out.setStart(selEvent.getRelativeWordStartIndex()); + out.setEnd(selEvent.getRelativeWordEndIndex()); + out.setSmartStart(selEvent.getRelativeSuggestedWordStartIndex()); + out.setSmartEnd(selEvent.getRelativeSuggestedWordEndIndex()); + } + + return out; + } + /** * Builder to build a text classifier event. * diff --git a/core/java/android/view/textclassifier/TextClassifierImpl.java b/core/java/android/view/textclassifier/TextClassifierImpl.java index 3e95f1baf4bbe..024c379e01657 100644 --- a/core/java/android/view/textclassifier/TextClassifierImpl.java +++ b/core/java/android/view/textclassifier/TextClassifierImpl.java @@ -376,7 +376,6 @@ public final class TextClassifierImpl implements TextClassifier { /** @inheritDoc */ @Override public void onSelectionEvent(SelectionEvent event) { - Preconditions.checkNotNull(event); mSessionLogger.writeEvent(event); } @@ -386,7 +385,12 @@ public final class TextClassifierImpl implements TextClassifier { Log.d(DEFAULT_LOG_TAG, "onTextClassifierEvent() called with: event = [" + event + "]"); } try { - mTextClassifierEventTronLogger.writeEvent(event); + final SelectionEvent selEvent = event.toSelectionEvent(); + if (selEvent != null) { + mSessionLogger.writeEvent(selEvent); + } else { + mTextClassifierEventTronLogger.writeEvent(event); + } } catch (Exception e) { Log.e(LOG_TAG, "Error writing event", e); } diff --git a/core/tests/coretests/src/android/view/textclassifier/TextClassifierEventTest.java b/core/tests/coretests/src/android/view/textclassifier/TextClassifierEventTest.java new file mode 100644 index 0000000000000..11eb567df488c --- /dev/null +++ b/core/tests/coretests/src/android/view/textclassifier/TextClassifierEventTest.java @@ -0,0 +1,294 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.view.textclassifier; + +import static com.google.common.truth.Truth.assertWithMessage; + +import android.annotation.Nullable; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public final class TextClassifierEventTest { + + private static final TextClassificationContext TC_CONTEXT = + new TextClassificationContext.Builder("pkg", TextClassifier.WIDGET_TYPE_TEXTVIEW) + .build(); + + private static final TextSelection TEXT_SELECTION = new TextSelection.Builder(10, 20) + .setEntityType(TextClassifier.TYPE_ADDRESS, 1) + .setId("id1") + .build(); + + private static final TextClassification TEXT_CLASSIFICATION = new TextClassification.Builder() + .setEntityType(TextClassifier.TYPE_DATE, 1) + .setId("id2") + .build(); + + @Test + public void toSelectionEvent_selectionStarted() { + final TextClassificationSessionId sessionId = new TextClassificationSessionId(); + final SelectionEvent expected = SelectionEvent.createSelectionStartedEvent( + SelectionEvent.INVOCATION_MANUAL, 0); + expected.setTextClassificationSessionContext(TC_CONTEXT); + expected.setSessionId(sessionId); + + final TextClassifierEvent event = new TextClassifierEvent.TextSelectionEvent.Builder( + TextClassifierEvent.TYPE_SELECTION_STARTED) + .setEventContext(TC_CONTEXT) + .build(); + event.mHiddenTempSessionId = sessionId; + + assertEquals(expected, event.toSelectionEvent()); + } + + @Test + public void toSelectionEvent_smartSelectionMulti() { + final int start = -1; + final int end = 2; + final int eventIndex = 1; + final SelectionEvent expected = SelectionEvent.createSelectionModifiedEvent( + 0, 3, TEXT_SELECTION); + expected.setInvocationMethod(SelectionEvent.INVOCATION_MANUAL); + expected.setEventType(SelectionEvent.EVENT_SMART_SELECTION_MULTI); + expected.setStart(start); + expected.setEnd(end); + expected.setEventIndex(eventIndex); + expected.setTextClassificationSessionContext(TC_CONTEXT); + + final String entityType = TEXT_SELECTION.getEntity(0); + final TextClassifierEvent event = new TextClassifierEvent.TextSelectionEvent.Builder( + TextClassifierEvent.TYPE_SMART_SELECTION_MULTI) + .setEventContext(TC_CONTEXT) + .setResultId(TEXT_SELECTION.getId()) + .setRelativeWordStartIndex(start) + .setRelativeWordEndIndex(end) + .setEntityTypes(entityType) + .setScores(TEXT_SELECTION.getConfidenceScore(entityType)) + .setEventIndex(eventIndex) + .build(); + + assertEquals(expected, event.toSelectionEvent()); + } + + @Test + public void toSelectionEvent_smartSelectionSingle() { + final int start = 0; + final int end = 1; + final int eventIndex = 2; + final SelectionEvent expected = SelectionEvent.createSelectionModifiedEvent( + 0, 1, TEXT_SELECTION); + expected.setInvocationMethod(SelectionEvent.INVOCATION_MANUAL); + expected.setEventType(SelectionEvent.EVENT_SMART_SELECTION_SINGLE); + expected.setStart(start); + expected.setEnd(end); + expected.setEventIndex(eventIndex); + expected.setTextClassificationSessionContext(TC_CONTEXT); + + final String entityType = TEXT_SELECTION.getEntity(0); + final TextClassifierEvent event = new TextClassifierEvent.TextSelectionEvent.Builder( + TextClassifierEvent.TYPE_SMART_SELECTION_SINGLE) + .setEventContext(TC_CONTEXT) + .setResultId(TEXT_SELECTION.getId()) + .setRelativeWordStartIndex(start) + .setRelativeWordEndIndex(end) + .setEntityTypes(entityType) + .setScores(TEXT_SELECTION.getConfidenceScore(entityType)) + .setEventIndex(eventIndex) + .build(); + + assertEquals(expected, event.toSelectionEvent()); + } + + @Test + public void toSelectionEvent_resetSelection() { + final int start = 0; + final int end = 1; + final int smartStart = -1; + final int smartEnd = 2; + final int eventIndex = 3; + final SelectionEvent expected = SelectionEvent.createSelectionActionEvent( + 0, 1, SelectionEvent.ACTION_RESET, TEXT_CLASSIFICATION); + expected.setInvocationMethod(SelectionEvent.INVOCATION_MANUAL); + expected.setStart(start); + expected.setEnd(end); + expected.setSmartStart(smartStart); + expected.setSmartEnd(smartEnd); + expected.setEventIndex(eventIndex); + expected.setTextClassificationSessionContext(TC_CONTEXT); + + final String entityType = TEXT_CLASSIFICATION.getEntity(0); + final TextClassifierEvent event = new TextClassifierEvent.TextSelectionEvent.Builder( + TextClassifierEvent.TYPE_SELECTION_RESET) + .setEventContext(TC_CONTEXT) + .setResultId(TEXT_CLASSIFICATION.getId()) + .setRelativeSuggestedWordStartIndex(smartStart) + .setRelativeSuggestedWordEndIndex(smartEnd) + .setRelativeWordStartIndex(start) + .setRelativeWordEndIndex(end) + .setEntityTypes(TEXT_CLASSIFICATION.getEntity(0)) + .setScores(TEXT_CLASSIFICATION.getConfidenceScore(entityType)) + .setEventIndex(eventIndex) + .build(); + + assertEquals(expected, event.toSelectionEvent()); + } + + @Test + public void toSelectionEvent_modifySelection() { + final int start = -1; + final int end = 5; + final int eventIndex = 4; + final SelectionEvent expected = SelectionEvent.createSelectionModifiedEvent(0, 1); + expected.setInvocationMethod(SelectionEvent.INVOCATION_MANUAL); + expected.setStart(start); + expected.setEnd(end); + expected.setEventIndex(eventIndex); + expected.setTextClassificationSessionContext(TC_CONTEXT); + + final TextClassifierEvent event = new TextClassifierEvent.TextSelectionEvent.Builder( + TextClassifierEvent.TYPE_SELECTION_MODIFIED) + .setEventContext(TC_CONTEXT) + .setRelativeWordStartIndex(start) + .setRelativeWordEndIndex(end) + .setEventIndex(eventIndex) + .build(); + + assertEquals(expected, event.toSelectionEvent()); + } + + @Test + public void toSelectionEvent_copyAction() { + final int start = 3; + final int end = 4; + final int eventIndex = 5; + final SelectionEvent expected = SelectionEvent.createSelectionActionEvent( + 5, 6, SelectionEvent.ACTION_COPY); + expected.setInvocationMethod(SelectionEvent.INVOCATION_MANUAL); + expected.setStart(start); + expected.setEnd(end); + expected.setEventIndex(eventIndex); + expected.setTextClassificationSessionContext(TC_CONTEXT); + + final TextClassifierEvent event = new TextClassifierEvent.TextSelectionEvent.Builder( + TextClassifierEvent.TYPE_COPY_ACTION) + .setEventContext(TC_CONTEXT) + .setRelativeWordStartIndex(start) + .setRelativeWordEndIndex(end) + .setEventIndex(eventIndex) + .build(); + + assertEquals(expected, event.toSelectionEvent()); + } + + @Test + public void toSelectionEvent_selectionDismissed() { + final int eventIndex = 6; + final SelectionEvent expected = SelectionEvent.createSelectionActionEvent( + 0, 1, SelectionEvent.ACTION_ABANDON); + expected.setInvocationMethod(SelectionEvent.INVOCATION_MANUAL); + expected.setEventIndex(eventIndex); + + final TextClassifierEvent event = new TextClassifierEvent.TextSelectionEvent.Builder( + TextClassifierEvent.TYPE_SELECTION_DESTROYED) + .setEventIndex(eventIndex) + .build(); + + assertEquals(expected, event.toSelectionEvent()); + } + + @Test + public void toSelectionEvent_link_smartAction() { + final int eventIndex = 2; + final SelectionEvent expected = SelectionEvent.createSelectionActionEvent( + 1, 9, SelectionEvent.ACTION_SMART_SHARE, TEXT_CLASSIFICATION); + expected.setInvocationMethod(SelectionEvent.INVOCATION_LINK); + // TODO: TextLinkifyEvent API is missing APIs to set text indices. See related comment in + // TextClassifierEvent. + expected.setEventIndex(eventIndex); + expected.setTextClassificationSessionContext(TC_CONTEXT); + + final String entityType = TEXT_CLASSIFICATION.getEntity(0); + final TextClassifierEvent event = new TextClassifierEvent.TextLinkifyEvent.Builder( + TextClassifierEvent.TYPE_SMART_ACTION) + .setEventContext(TC_CONTEXT) + .setResultId(TEXT_CLASSIFICATION.getId()) + .setEntityTypes(entityType) + .setScores(TEXT_CLASSIFICATION.getConfidenceScore(entityType)) + .setActionIndices(0) + .setEventIndex(eventIndex) + .build(); + + assertEquals(expected, event.toSelectionEvent()); + } + + @Test + public void toSelectionEvent_nonSelectionOrLinkifyEvent() { + final TextClassifierEvent convActionEvent = + new TextClassifierEvent.ConversationActionsEvent.Builder( + TextClassifierEvent.TYPE_ACTIONS_GENERATED) + .build(); + assertWithMessage("conversation action event") + .that(convActionEvent.toSelectionEvent()).isNull(); + + final TextClassifierEvent langDetEvent = + new TextClassifierEvent.ConversationActionsEvent.Builder( + TextClassifierEvent.TYPE_ACTIONS_GENERATED) + .setEventContext(TC_CONTEXT) + .build(); + assertWithMessage("language detection event") + .that(langDetEvent.toSelectionEvent()).isNull(); + } + + private static void assertEquals( + @Nullable SelectionEvent expected, @Nullable SelectionEvent actual) { + if (expected == null && actual == null) return; + if (expected == actual) return; + assertWithMessage("actual").that(actual).isNotNull(); + assertWithMessage("expected").that(expected).isNotNull(); + assertWithMessage("eventType") + .that(actual.getEventType()).isEqualTo(expected.getEventType()); + assertWithMessage("packageName") + .that(actual.getPackageName()).isEqualTo(expected.getPackageName()); + assertWithMessage("widgetType") + .that(actual.getWidgetType()).isEqualTo(expected.getWidgetType()); + assertWithMessage("widgetVersion") + .that(actual.getWidgetVersion()).isEqualTo(expected.getWidgetVersion()); + assertWithMessage("invocationMethod") + .that(actual.getInvocationMethod()).isEqualTo(expected.getInvocationMethod()); + assertWithMessage("resultId") + .that(actual.getResultId()).isEqualTo(expected.getResultId()); + assertWithMessage("sessionId") + .that(actual.getSessionId()).isEqualTo(expected.getSessionId()); + assertWithMessage("entityType") + .that(actual.getEntityType()).isEqualTo(expected.getEntityType()); + assertWithMessage("eventIndex") + .that(actual.getEventIndex()).isEqualTo(expected.getEventIndex()); + assertWithMessage("start") + .that(actual.getStart()).isEqualTo(expected.getStart()); + assertWithMessage("end") + .that(actual.getEnd()).isEqualTo(expected.getEnd()); + assertWithMessage("smartStart") + .that(actual.getSmartStart()).isEqualTo(expected.getSmartStart()); + assertWithMessage("smartEnd") + .that(actual.getSmartEnd()).isEqualTo(expected.getSmartEnd()); + } +}