TextClassifierService.onSelectionEvent

Bug: 74466564
Bug: 67609167
Test: bit FrameworksCoreTests:android.view.textclassifier.TextClassificationManagerTest
Test: bit FrameworksCoreTests:android.view.textclassifier.logging.SelectionEventTest
Merged-In: Ib5af1ec80a38432d1201fbc913acdc3597d6ba82
Change-Id: Ib5af1ec80a38432d1201fbc913acdc3597d6ba82
diff --git a/api/current.txt b/api/current.txt
index 3193d30..037dba9 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -51067,7 +51067,8 @@
     method public java.lang.String getWidgetVersion();
   }
 
-  public final class SelectionEvent {
+  public final class SelectionEvent implements android.os.Parcelable {
+    method public int describeContents();
     method public long getDurationSincePreviousEvent();
     method public long getDurationSinceSessionStart();
     method public int getEnd();
@@ -51084,6 +51085,7 @@
     method public int getStart();
     method public java.lang.String getWidgetType();
     method public java.lang.String getWidgetVersion();
+    method public void writeToParcel(android.os.Parcel, int);
     field public static final int ACTION_ABANDON = 107; // 0x6b
     field public static final int ACTION_COPY = 101; // 0x65
     field public static final int ACTION_CUT = 103; // 0x67
@@ -51095,6 +51097,7 @@
     field public static final int ACTION_SELECT_ALL = 200; // 0xc8
     field public static final int ACTION_SHARE = 104; // 0x68
     field public static final int ACTION_SMART_SHARE = 105; // 0x69
+    field public static final android.os.Parcelable.Creator<android.view.textclassifier.SelectionEvent> CREATOR;
     field public static final int EVENT_AUTO_SELECTION = 5; // 0x5
     field public static final int EVENT_SELECTION_MODIFIED = 2; // 0x2
     field public static final int EVENT_SELECTION_STARTED = 1; // 0x1
diff --git a/api/system-current.txt b/api/system-current.txt
index c80f239b..e205331 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -4707,6 +4707,7 @@
     method public final android.os.IBinder onBind(android.content.Intent);
     method public abstract void onClassifyText(java.lang.CharSequence, int, int, android.view.textclassifier.TextClassification.Options, android.os.CancellationSignal, android.service.textclassifier.TextClassifierService.Callback<android.view.textclassifier.TextClassification>);
     method public abstract void onGenerateLinks(java.lang.CharSequence, android.view.textclassifier.TextLinks.Options, android.os.CancellationSignal, android.service.textclassifier.TextClassifierService.Callback<android.view.textclassifier.TextLinks>);
+    method public void onSelectionEvent(android.view.textclassifier.SelectionEvent);
     method public abstract void onSuggestSelection(java.lang.CharSequence, int, int, android.view.textclassifier.TextSelection.Options, android.os.CancellationSignal, android.service.textclassifier.TextClassifierService.Callback<android.view.textclassifier.TextSelection>);
     field public static final java.lang.String SERVICE_INTERFACE = "android.service.textclassifier.TextClassifierService";
   }
diff --git a/core/java/android/service/textclassifier/ITextClassifierService.aidl b/core/java/android/service/textclassifier/ITextClassifierService.aidl
index d2ffe34..25e9d45 100644
--- a/core/java/android/service/textclassifier/ITextClassifierService.aidl
+++ b/core/java/android/service/textclassifier/ITextClassifierService.aidl
@@ -19,13 +19,14 @@
 import android.service.textclassifier.ITextClassificationCallback;
 import android.service.textclassifier.ITextLinksCallback;
 import android.service.textclassifier.ITextSelectionCallback;
+import android.view.textclassifier.SelectionEvent;
 import android.view.textclassifier.TextClassification;
 import android.view.textclassifier.TextLinks;
 import android.view.textclassifier.TextSelection;
 
 /**
  * TextClassifierService binder interface.
- * See TextClassifier for interface documentation.
+ * See TextClassifier (and TextClassifier.Logger) for interface documentation.
  * {@hide}
  */
 oneway interface ITextClassifierService {
@@ -44,4 +45,6 @@
             in CharSequence text,
             in TextLinks.Options options,
             in ITextLinksCallback c);
+
+    void onSelectionEvent(in SelectionEvent event);
 }
diff --git a/core/java/android/service/textclassifier/TextClassifierService.java b/core/java/android/service/textclassifier/TextClassifierService.java
index 57c8def..88e29b0 100644
--- a/core/java/android/service/textclassifier/TextClassifierService.java
+++ b/core/java/android/service/textclassifier/TextClassifierService.java
@@ -33,6 +33,7 @@
 import android.os.RemoteException;
 import android.text.TextUtils;
 import android.util.Slog;
+import android.view.textclassifier.SelectionEvent;
 import android.view.textclassifier.TextClassification;
 import android.view.textclassifier.TextClassificationManager;
 import android.view.textclassifier.TextClassifier;
@@ -171,6 +172,12 @@
                         }
                     });
         }
+
+        /** {@inheritDoc} */
+        @Override
+        public void onSelectionEvent(SelectionEvent event) throws RemoteException {
+            TextClassifierService.this.onSelectionEvent(event);
+        }
     };
 
     @Nullable
@@ -238,6 +245,15 @@
             @NonNull Callback<TextLinks> callback);
 
     /**
+     * Writes the selection event.
+     * This is called when a selection event occurs. e.g. user changed selection; or smart selection
+     * happened.
+     *
+     * <p>The default implementation ignores the event.
+     */
+    public void onSelectionEvent(@NonNull SelectionEvent event) {}
+
+    /**
      * Returns a TextClassifier that runs in this service's process.
      * If the local TextClassifier is disabled, this returns {@link TextClassifier#NO_OP}.
      */
diff --git a/core/java/android/view/textclassifier/Logger.java b/core/java/android/view/textclassifier/Logger.java
index a4f5bf1..9c92fd4 100644
--- a/core/java/android/view/textclassifier/Logger.java
+++ b/core/java/android/view/textclassifier/Logger.java
@@ -94,10 +94,7 @@
     }
 
     /**
-     * Writes the selection event.
-     *
-     * <p><strong>NOTE: </strong>This method is designed for subclasses.
-     * Apps should not call it directly.
+     * Writes the selection event to a log.
      */
     public abstract void writeEvent(@NonNull SelectionEvent event);
 
diff --git a/core/java/android/view/textclassifier/SelectionEvent.aidl b/core/java/android/view/textclassifier/SelectionEvent.aidl
new file mode 100644
index 0000000..10ed16e
--- /dev/null
+++ b/core/java/android/view/textclassifier/SelectionEvent.aidl
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2018 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;
+
+parcelable SelectionEvent;
diff --git a/core/java/android/view/textclassifier/SelectionEvent.java b/core/java/android/view/textclassifier/SelectionEvent.java
index 90fd921..7ac094e 100644
--- a/core/java/android/view/textclassifier/SelectionEvent.java
+++ b/core/java/android/view/textclassifier/SelectionEvent.java
@@ -18,6 +18,8 @@
 
 import android.annotation.IntDef;
 import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
 import android.view.textclassifier.TextClassifier.EntityType;
 
 import com.android.internal.util.Preconditions;
@@ -25,12 +27,13 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.Locale;
+import java.util.Objects;
 
 /**
  * A selection event.
  * Specify index parameters as word token indices.
  */
-public final class SelectionEvent {
+public final class SelectionEvent implements Parcelable {
 
     /** @hide */
     @Retention(RetentionPolicy.SOURCE)
@@ -121,9 +124,9 @@
     private String mSignature;
     private long mEventTime;
     private long mDurationSinceSessionStart;
-    private long mDurationSinceLastEvent;
+    private long mDurationSincePreviousEvent;
     private int mEventIndex;
-    private String mSessionId;
+    @Nullable private String mSessionId;
     private int mStart;
     private int mEnd;
     private int mSmartStart;
@@ -146,6 +149,60 @@
         mInvocationMethod = invocationMethod;
     }
 
+    private SelectionEvent(Parcel in) {
+        mAbsoluteStart = in.readInt();
+        mAbsoluteEnd = in.readInt();
+        mEventType = in.readInt();
+        mEntityType = in.readString();
+        mWidgetVersion = in.readInt() > 0 ? in.readString() : null;
+        mPackageName = in.readString();
+        mWidgetType = in.readString();
+        mInvocationMethod = in.readInt();
+        mSignature = in.readString();
+        mEventTime = in.readLong();
+        mDurationSinceSessionStart = in.readLong();
+        mDurationSincePreviousEvent = in.readLong();
+        mEventIndex = in.readInt();
+        mSessionId = in.readInt() > 0 ? in.readString() : null;
+        mStart = in.readInt();
+        mEnd = in.readInt();
+        mSmartStart = in.readInt();
+        mSmartEnd = in.readInt();
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(mAbsoluteStart);
+        dest.writeInt(mAbsoluteEnd);
+        dest.writeInt(mEventType);
+        dest.writeString(mEntityType);
+        dest.writeInt(mWidgetVersion != null ? 1 : 0);
+        if (mWidgetVersion != null) {
+            dest.writeString(mWidgetVersion);
+        }
+        dest.writeString(mPackageName);
+        dest.writeString(mWidgetType);
+        dest.writeInt(mInvocationMethod);
+        dest.writeString(mSignature);
+        dest.writeLong(mEventTime);
+        dest.writeLong(mDurationSinceSessionStart);
+        dest.writeLong(mDurationSincePreviousEvent);
+        dest.writeInt(mEventIndex);
+        dest.writeInt(mSessionId != null ? 1 : 0);
+        if (mSessionId != null) {
+            dest.writeString(mSessionId);
+        }
+        dest.writeInt(mStart);
+        dest.writeInt(mEnd);
+        dest.writeInt(mSmartStart);
+        dest.writeInt(mSmartEnd);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
     int getAbsoluteStart() {
         return mAbsoluteStart;
     }
@@ -240,11 +297,11 @@
      * in the selection session was triggered.
      */
     public long getDurationSincePreviousEvent() {
-        return mDurationSinceLastEvent;
+        return mDurationSincePreviousEvent;
     }
 
     SelectionEvent setDurationSincePreviousEvent(long durationMs) {
-        this.mDurationSinceLastEvent = durationMs;
+        this.mDurationSincePreviousEvent = durationMs;
         return this;
     }
 
@@ -342,15 +399,66 @@
     }
 
     @Override
+    public int hashCode() {
+        return Objects.hash(mAbsoluteStart, mAbsoluteEnd, mEventType, mEntityType,
+                mWidgetVersion, mPackageName, mWidgetType, mInvocationMethod, mSignature,
+                mEventTime, mDurationSinceSessionStart, mDurationSincePreviousEvent,
+                mEventIndex, mSessionId, mStart, mEnd, mSmartStart, mSmartEnd);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!(obj instanceof SelectionEvent)) {
+            return false;
+        }
+
+        final SelectionEvent other = (SelectionEvent) obj;
+        return mAbsoluteStart == other.mAbsoluteStart
+                && mAbsoluteEnd == other.mAbsoluteEnd
+                && mEventType == other.mEventType
+                && Objects.equals(mEntityType, other.mEntityType)
+                && Objects.equals(mWidgetVersion, other.mWidgetVersion)
+                && Objects.equals(mPackageName, other.mPackageName)
+                && Objects.equals(mWidgetType, other.mWidgetType)
+                && mInvocationMethod == other.mInvocationMethod
+                && Objects.equals(mSignature, other.mSignature)
+                && mEventTime == other.mEventTime
+                && mDurationSinceSessionStart == other.mDurationSinceSessionStart
+                && mDurationSincePreviousEvent == other.mDurationSincePreviousEvent
+                && mEventIndex == other.mEventIndex
+                && Objects.equals(mSessionId, other.mSessionId)
+                && mStart == other.mStart
+                && mEnd == other.mEnd
+                && mSmartStart == other.mSmartStart
+                && mSmartEnd == other.mSmartEnd;
+    }
+
+    @Override
     public String toString() {
         return String.format(Locale.US,
         "SelectionEvent {absoluteStart=%d, absoluteEnd=%d, eventType=%d, entityType=%s, "
-                + "widgetVersion=%s, packageName=%s, widgetType=%s, signature=%s, "
-                + "eventTime=%d, durationSinceSessionStart=%d, durationSinceLastEvent=%d, "
-                + "eventIndex=%d, sessionId=%s, start=%d, end=%d, smartStart=%d, smartEnd=%d}",
+                + "widgetVersion=%s, packageName=%s, widgetType=%s, invocationMethod=%s, "
+                + "signature=%s, eventTime=%d, durationSinceSessionStart=%d, "
+                + "durationSincePreviousEvent=%d, eventIndex=%d, sessionId=%s, start=%d, end=%d, "
+                + "smartStart=%d, smartEnd=%d}",
                 mAbsoluteStart, mAbsoluteEnd, mEventType, mEntityType,
-                mWidgetVersion, mPackageName, mWidgetType, mSignature,
-                mEventTime, mDurationSinceSessionStart, mDurationSinceLastEvent,
+                mWidgetVersion, mPackageName, mWidgetType, mInvocationMethod, mSignature,
+                mEventTime, mDurationSinceSessionStart, mDurationSincePreviousEvent,
                 mEventIndex, mSessionId, mStart, mEnd, mSmartStart, mSmartEnd);
     }
+
+    public static final Creator<SelectionEvent> CREATOR = new Creator<SelectionEvent>() {
+        @Override
+        public SelectionEvent createFromParcel(Parcel in) {
+            return new SelectionEvent(in);
+        }
+
+        @Override
+        public SelectionEvent[] newArray(int size) {
+            return new SelectionEvent[size];
+        }
+    };
 }
diff --git a/core/java/android/view/textclassifier/SystemTextClassifier.java b/core/java/android/view/textclassifier/SystemTextClassifier.java
index 2b335fb..c783cae 100644
--- a/core/java/android/view/textclassifier/SystemTextClassifier.java
+++ b/core/java/android/view/textclassifier/SystemTextClassifier.java
@@ -29,6 +29,7 @@
 import android.service.textclassifier.ITextLinksCallback;
 import android.service.textclassifier.ITextSelectionCallback;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.Preconditions;
 
 import java.util.concurrent.CountDownLatch;
@@ -46,6 +47,12 @@
     private final TextClassifier mFallback;
     private final String mPackageName;
 
+    private final Object mLoggerLock = new Object();
+    @GuardedBy("mLoggerLock")
+    private Logger.Config mLoggerConfig;
+    @GuardedBy("mLoggerLock")
+    private Logger mLogger;
+
     SystemTextClassifier(Context context, TextClassificationConstants settings)
                 throws ServiceManager.ServiceNotFoundException {
         mManagerService = ITextClassifierService.Stub.asInterface(
@@ -58,6 +65,7 @@
     /**
      * @inheritDoc
      */
+    @Override
     @WorkerThread
     public TextSelection suggestSelection(
             @NonNull CharSequence text,
@@ -84,6 +92,7 @@
     /**
      * @inheritDoc
      */
+    @Override
     @WorkerThread
     public TextClassification classifyText(
             @NonNull CharSequence text,
@@ -109,6 +118,7 @@
     /**
      * @inheritDoc
      */
+    @Override
     @WorkerThread
     public TextLinks generateLinks(
             @NonNull CharSequence text, @Nullable TextLinks.Options options) {
@@ -142,11 +152,33 @@
      * @inheritDoc
      */
     @Override
+    @WorkerThread
     public int getMaxGenerateLinksTextLength() {
         // TODO: retrieve this from the bound service.
         return mFallback.getMaxGenerateLinksTextLength();
     }
 
+    @Override
+    public Logger getLogger(@NonNull Logger.Config config) {
+        Preconditions.checkNotNull(config);
+        synchronized (mLoggerLock) {
+            if (mLogger == null || !config.equals(mLoggerConfig)) {
+                mLoggerConfig = config;
+                mLogger = new Logger(config) {
+                    @Override
+                    public void writeEvent(SelectionEvent event) {
+                        try {
+                            mManagerService.onSelectionEvent(event);
+                        } catch (RemoteException e) {
+                            e.rethrowAsRuntimeException();
+                        }
+                    }
+                };
+            }
+        }
+        return mLogger;
+    }
+
     private static final class TextSelectionCallback extends ITextSelectionCallback.Stub {
 
         final ResponseReceiver<TextSelection> mReceiver = new ResponseReceiver<>();
diff --git a/core/java/android/view/textclassifier/TextClassifier.java b/core/java/android/view/textclassifier/TextClassifier.java
index ebd2ff9..887bebb 100644
--- a/core/java/android/view/textclassifier/TextClassifier.java
+++ b/core/java/android/view/textclassifier/TextClassifier.java
@@ -323,6 +323,7 @@
      * @see #generateLinks(CharSequence)
      * @see #generateLinks(CharSequence, TextLinks.Options)
      */
+    @WorkerThread
     default int getMaxGenerateLinksTextLength() {
         return Integer.MAX_VALUE;
     }
diff --git a/core/java/android/view/textclassifier/TextClassifierImpl.java b/core/java/android/view/textclassifier/TextClassifierImpl.java
index 0a05269..a099820 100644
--- a/core/java/android/view/textclassifier/TextClassifierImpl.java
+++ b/core/java/android/view/textclassifier/TextClassifierImpl.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.WorkerThread;
 import android.app.SearchManager;
 import android.content.ComponentName;
 import android.content.ContentUris;
@@ -42,7 +43,6 @@
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
-import java.lang.ref.WeakReference;
 import java.net.URLEncoder;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -78,7 +78,6 @@
 
     private final Context mContext;
     private final TextClassifier mFallback;
-
     private final GenerateLinksLogger mGenerateLinksLogger;
 
     private final Object mLock = new Object();
@@ -91,9 +90,9 @@
 
     private final Object mLoggerLock = new Object();
     @GuardedBy("mLoggerLock") // Do not access outside this lock.
-    private WeakReference<Logger.Config> mLoggerConfig = new WeakReference<>(null);
+    private Logger.Config mLoggerConfig;
     @GuardedBy("mLoggerLock") // Do not access outside this lock.
-    private Logger mLogger;  // Should never be null if mLoggerConfig.get() is not null.
+    private Logger mLogger;
 
     private final TextClassificationConstants mSettings;
 
@@ -106,6 +105,7 @@
 
     /** @inheritDoc */
     @Override
+    @WorkerThread
     public TextSelection suggestSelection(
             @NonNull CharSequence text, int selectionStartIndex, int selectionEndIndex,
             @Nullable TextSelection.Options options) {
@@ -169,6 +169,7 @@
 
     /** @inheritDoc */
     @Override
+    @WorkerThread
     public TextClassification classifyText(
             @NonNull CharSequence text, int startIndex, int endIndex,
             @Nullable TextClassification.Options options) {
@@ -204,6 +205,7 @@
 
     /** @inheritDoc */
     @Override
+    @WorkerThread
     public TextLinks generateLinks(
             @NonNull CharSequence text, @Nullable TextLinks.Options options) {
         Utils.validate(text, getMaxGenerateLinksTextLength(), false /* allowInMainThread */);
@@ -282,16 +284,17 @@
         }
     }
 
+    /** @inheritDoc */
     @Override
     public Logger getLogger(@NonNull Logger.Config config) {
         Preconditions.checkNotNull(config);
         synchronized (mLoggerLock) {
-            if (mLoggerConfig.get() == null || !mLoggerConfig.get().equals(config)) {
-                mLoggerConfig = new WeakReference<>(config);
+            if (mLogger == null || !config.equals(mLoggerConfig)) {
+                mLoggerConfig = config;
                 mLogger = new DefaultLogger(config);
             }
-            return mLogger;
         }
+        return mLogger;
     }
 
     private TextClassifierImplNative getNative(LocaleList localeList)
diff --git a/core/java/android/widget/SelectionActionModeHelper.java b/core/java/android/widget/SelectionActionModeHelper.java
index 629f531..be8c34c 100644
--- a/core/java/android/widget/SelectionActionModeHelper.java
+++ b/core/java/android/widget/SelectionActionModeHelper.java
@@ -648,6 +648,9 @@
      * Part selection of a word e.g. "or" is counted as selecting the
      * entire word i.e. equivalent to "York", and each special character is counted as a word, e.g.
      * "," is at [2, 3). Whitespaces are ignored.
+     *
+     * NOTE that the definition of a word is defined by the TextClassifier's Logger's token
+     * iterator.
      */
     private static final class SelectionMetricsLogger {
 
diff --git a/core/tests/coretests/src/android/view/textclassifier/SelectionEventTest.java b/core/tests/coretests/src/android/view/textclassifier/SelectionEventTest.java
new file mode 100644
index 0000000..b77982b
--- /dev/null
+++ b/core/tests/coretests/src/android/view/textclassifier/SelectionEventTest.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2018 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 org.junit.Assert.assertEquals;
+
+import android.os.Parcel;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class SelectionEventTest {
+
+    @Test
+    public void testParcel() {
+        final SelectionEvent[] captured = new SelectionEvent[1];
+        final Logger logger = new Logger(new Logger.Config(
+                InstrumentationRegistry.getTargetContext(), Logger.WIDGET_TEXTVIEW, null)) {
+            @Override
+            public void writeEvent(SelectionEvent event) {
+                captured[0] = event;
+            }
+        };
+        logger.logSelectionStartedEvent(SelectionEvent.INVOCATION_MANUAL, 0);
+        final SelectionEvent event = captured[0];
+        final Parcel parcel = Parcel.obtain();
+        event.writeToParcel(parcel, event.describeContents());
+        parcel.setDataPosition(0);
+        assertEquals(event, SelectionEvent.CREATOR.createFromParcel(parcel));
+    }
+}
diff --git a/services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java b/services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java
index 0ac853b..6053512 100644
--- a/services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java
+++ b/services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java
@@ -16,11 +16,12 @@
 
 package com.android.server.textclassifier;
 
-import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
 import android.os.Binder;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -30,6 +31,7 @@
 import android.service.textclassifier.ITextLinksCallback;
 import android.service.textclassifier.ITextSelectionCallback;
 import android.service.textclassifier.TextClassifierService;
+import android.view.textclassifier.SelectionEvent;
 import android.view.textclassifier.TextClassification;
 import android.view.textclassifier.TextClassifier;
 import android.view.textclassifier.TextLinks;
@@ -216,6 +218,23 @@
         }
     }
 
+    @Override
+    public void onSelectionEvent(SelectionEvent event) throws RemoteException {
+        validateInput(event, mContext);
+
+        synchronized (mLock) {
+            if (isBoundLocked()) {
+                mService.onSelectionEvent(event);
+            } else {
+                final Callable<Void> request = () -> {
+                    onSelectionEvent(event);
+                    return null;
+                };
+                enqueueRequestLocked(request, null /* onServiceFailure */, null /* binder */);
+            }
+        }
+    }
+
     /**
      * @return true if the service is bound or in the process of being bound.
      *      Returns false otherwise.
@@ -281,8 +300,8 @@
     private final class PendingRequest implements IBinder.DeathRecipient {
 
         private final Callable<Void> mRequest;
-        private final Callable<Void> mOnServiceFailure;
-        private final IBinder mBinder;
+        @Nullable private final Callable<Void> mOnServiceFailure;
+        @Nullable private final IBinder mBinder;
 
         /**
          * Initializes a new pending request.
@@ -292,15 +311,17 @@
          * @param binder binder to the process that made this pending request
          */
         PendingRequest(
-                @NonNull Callable<Void> request, @NonNull Callable<Void> onServiceFailure,
-                @NonNull IBinder binder) {
+                Callable<Void> request, @Nullable Callable<Void> onServiceFailure,
+                @Nullable IBinder binder) {
             mRequest = Preconditions.checkNotNull(request);
-            mOnServiceFailure = Preconditions.checkNotNull(onServiceFailure);
-            mBinder = Preconditions.checkNotNull(binder);
-            try {
-                mBinder.linkToDeath(this, 0);
-            } catch (RemoteException e) {
-                e.printStackTrace();
+            mOnServiceFailure = onServiceFailure;
+            mBinder = binder;
+            if (mBinder != null) {
+                try {
+                    mBinder.linkToDeath(this, 0);
+                } catch (RemoteException e) {
+                    e.printStackTrace();
+                }
             }
         }
 
@@ -317,11 +338,13 @@
         @GuardedBy("mLock")
         void notifyServiceFailureLocked() {
             removeLocked();
-            try {
-                mOnServiceFailure.call();
-            } catch (Exception e) {
-                Slog.d(LOG_TAG, "Error notifying callback of service failure: "
-                        + e.getMessage());
+            if (mOnServiceFailure != null) {
+                try {
+                    mOnServiceFailure.call();
+                } catch (Exception e) {
+                    Slog.d(LOG_TAG, "Error notifying callback of service failure: "
+                            + e.getMessage());
+                }
             }
         }
 
@@ -336,7 +359,9 @@
         @GuardedBy("mLock")
         private void removeLocked() {
             mPendingRequests.remove(this);
-            mBinder.unlinkToDeath(this, 0);
+            if (mBinder != null) {
+                mBinder.unlinkToDeath(this, 0);
+            }
         }
     }
 
@@ -359,4 +384,16 @@
             throw new RemoteException(e.getMessage());
         }
     }
+
+    private static void validateInput(SelectionEvent event, Context context)
+            throws RemoteException {
+        try {
+            final int uid = context.getPackageManager()
+                    .getPackageUid(event.getPackageName(), 0);
+            Preconditions.checkArgument(Binder.getCallingUid() == uid);
+        } catch (IllegalArgumentException | NullPointerException |
+                PackageManager.NameNotFoundException e) {
+            throw new RemoteException(e.getMessage());
+        }
+    }
 }