From c6a56a31b73622133df52ff3905321d26b96e481 Mon Sep 17 00:00:00 2001 From: Abodunrinwa Toki Date: Tue, 6 Feb 2018 23:32:41 +0000 Subject: [PATCH] TextClassifierService.onSelectionEvent Bug: 67609167 Test: bit FrameworksCoreTests:android.view.textclassifier.TextClassificationManagerTest Test: bit FrameworksCoreTests:android.view.textclassifier.logging.SelectionEventTest Change-Id: Ib5af1ec80a38432d1201fbc913acdc3597d6ba82 --- api/current.txt | 5 +- api/system-current.txt | 1 + .../ITextClassifierService.aidl | 5 +- .../textclassifier/TextClassifierService.java | 16 +++ .../android/view/textclassifier/Logger.java | 5 +- .../view/textclassifier/SelectionEvent.aidl | 19 +++ .../view/textclassifier/SelectionEvent.java | 128 ++++++++++++++++-- .../textclassifier/SystemTextClassifier.java | 32 +++++ .../view/textclassifier/TextClassifier.java | 1 + .../textclassifier/TextClassifierImpl.java | 17 ++- .../widget/SelectionActionModeHelper.java | 3 + .../textclassifier/SelectionEventTest.java | 50 +++++++ .../TextClassificationManagerService.java | 71 +++++++--- 13 files changed, 313 insertions(+), 40 deletions(-) create mode 100644 core/java/android/view/textclassifier/SelectionEvent.aidl create mode 100644 core/tests/coretests/src/android/view/textclassifier/SelectionEventTest.java diff --git a/api/current.txt b/api/current.txt index 8880f2764e71d..46d26582cf71a 100644 --- a/api/current.txt +++ b/api/current.txt @@ -51015,7 +51015,8 @@ package android.view.textclassifier { 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(); @@ -51032,6 +51033,7 @@ package android.view.textclassifier { 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 @@ -51043,6 +51045,7 @@ package android.view.textclassifier { 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 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 628f7f0d2da76..1c237f3255c1d 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -4693,6 +4693,7 @@ package android.service.textclassifier { 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); method public abstract void onGenerateLinks(java.lang.CharSequence, android.view.textclassifier.TextLinks.Options, android.os.CancellationSignal, android.service.textclassifier.TextClassifierService.Callback); + 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); 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 d2ffe345ae381..25e9d454efb58 100644 --- a/core/java/android/service/textclassifier/ITextClassifierService.aidl +++ b/core/java/android/service/textclassifier/ITextClassifierService.aidl @@ -19,13 +19,14 @@ package android.service.textclassifier; 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 @@ oneway interface ITextClassifierService { 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 2c8c4ecaa6104..d1c4bab6f3952 100644 --- a/core/java/android/service/textclassifier/TextClassifierService.java +++ b/core/java/android/service/textclassifier/TextClassifierService.java @@ -33,6 +33,7 @@ import android.os.IBinder; 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.TextClassifier; import android.view.textclassifier.TextLinks; @@ -170,6 +171,12 @@ public abstract class TextClassifierService extends Service { } }); } + + /** {@inheritDoc} */ + @Override + public void onSelectionEvent(SelectionEvent event) throws RemoteException { + TextClassifierService.this.onSelectionEvent(event); + } }; @Nullable @@ -236,6 +243,15 @@ public abstract class TextClassifierService extends Service { @NonNull CancellationSignal cancellationSignal, @NonNull Callback callback); + /** + * Writes the selection event. + * This is called when a selection event occurs. e.g. user changed selection; or smart selection + * happened. + * + *

The default implementation ignores the event. + */ + public void onSelectionEvent(@NonNull SelectionEvent event) {} + /** * Callbacks for TextClassifierService results. * diff --git a/core/java/android/view/textclassifier/Logger.java b/core/java/android/view/textclassifier/Logger.java index a4f5bf17066c9..9c92fd4543e29 100644 --- a/core/java/android/view/textclassifier/Logger.java +++ b/core/java/android/view/textclassifier/Logger.java @@ -94,10 +94,7 @@ public abstract class Logger { } /** - * Writes the selection event. - * - *

NOTE: 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 0000000000000..10ed16e8df1fb --- /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 90fd921b024e2..7ac094eda031f 100644 --- a/core/java/android/view/textclassifier/SelectionEvent.java +++ b/core/java/android/view/textclassifier/SelectionEvent.java @@ -18,6 +18,8 @@ package android.view.textclassifier; 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 com.android.internal.util.Preconditions; 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 @@ public final class SelectionEvent { 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 @@ public final class SelectionEvent { 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 @@ public final class SelectionEvent { * 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; } @@ -341,16 +398,67 @@ public final class SelectionEvent { } } + @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 CREATOR = new Creator() { + @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 2b335fb09c614..c783cae4c2342 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.ITextClassifierService; 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 @@ final class SystemTextClassifier implements TextClassifier { 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 @@ final class SystemTextClassifier implements TextClassifier { /** * @inheritDoc */ + @Override @WorkerThread public TextSelection suggestSelection( @NonNull CharSequence text, @@ -84,6 +92,7 @@ final class SystemTextClassifier implements TextClassifier { /** * @inheritDoc */ + @Override @WorkerThread public TextClassification classifyText( @NonNull CharSequence text, @@ -109,6 +118,7 @@ final class SystemTextClassifier implements TextClassifier { /** * @inheritDoc */ + @Override @WorkerThread public TextLinks generateLinks( @NonNull CharSequence text, @Nullable TextLinks.Options options) { @@ -142,11 +152,33 @@ final class SystemTextClassifier implements TextClassifier { * @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 mReceiver = new ResponseReceiver<>(); diff --git a/core/java/android/view/textclassifier/TextClassifier.java b/core/java/android/view/textclassifier/TextClassifier.java index ebd2ff983bcb4..887bebbd97acd 100644 --- a/core/java/android/view/textclassifier/TextClassifier.java +++ b/core/java/android/view/textclassifier/TextClassifier.java @@ -323,6 +323,7 @@ public interface TextClassifier { * @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 0a052699590a8..a09982053091b 100644 --- a/core/java/android/view/textclassifier/TextClassifierImpl.java +++ b/core/java/android/view/textclassifier/TextClassifierImpl.java @@ -18,6 +18,7 @@ package android.view.textclassifier; 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.File; 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 @@ public final class TextClassifierImpl implements TextClassifier { private final Context mContext; private final TextClassifier mFallback; - private final GenerateLinksLogger mGenerateLinksLogger; private final Object mLock = new Object(); @@ -91,9 +90,9 @@ public final class TextClassifierImpl implements TextClassifier { private final Object mLoggerLock = new Object(); @GuardedBy("mLoggerLock") // Do not access outside this lock. - private WeakReference 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 @@ public final class TextClassifierImpl implements TextClassifier { /** @inheritDoc */ @Override + @WorkerThread public TextSelection suggestSelection( @NonNull CharSequence text, int selectionStartIndex, int selectionEndIndex, @Nullable TextSelection.Options options) { @@ -169,6 +169,7 @@ public final class TextClassifierImpl implements TextClassifier { /** @inheritDoc */ @Override + @WorkerThread public TextClassification classifyText( @NonNull CharSequence text, int startIndex, int endIndex, @Nullable TextClassification.Options options) { @@ -204,6 +205,7 @@ public final class TextClassifierImpl implements TextClassifier { /** @inheritDoc */ @Override + @WorkerThread public TextLinks generateLinks( @NonNull CharSequence text, @Nullable TextLinks.Options options) { Utils.validate(text, getMaxGenerateLinksTextLength(), false /* allowInMainThread */); @@ -282,16 +284,17 @@ public final class TextClassifierImpl implements TextClassifier { } } + /** @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 629f531928139..be8c34c53ae69 100644 --- a/core/java/android/widget/SelectionActionModeHelper.java +++ b/core/java/android/widget/SelectionActionModeHelper.java @@ -648,6 +648,9 @@ public final class SelectionActionModeHelper { * 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 0000000000000..b77982b6d1379 --- /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 0ac853b030e12..60535122929e6 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.ITextClassificationCallback; 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 @@ public final class TextClassificationManagerService extends ITextClassifierServi } } + @Override + public void onSelectionEvent(SelectionEvent event) throws RemoteException { + validateInput(event, mContext); + + synchronized (mLock) { + if (isBoundLocked()) { + mService.onSelectionEvent(event); + } else { + final Callable 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 @@ public final class TextClassificationManagerService extends ITextClassifierServi private final class PendingRequest implements IBinder.DeathRecipient { private final Callable mRequest; - private final Callable mOnServiceFailure; - private final IBinder mBinder; + @Nullable private final Callable mOnServiceFailure; + @Nullable private final IBinder mBinder; /** * Initializes a new pending request. @@ -292,15 +311,17 @@ public final class TextClassificationManagerService extends ITextClassifierServi * @param binder binder to the process that made this pending request */ PendingRequest( - @NonNull Callable request, @NonNull Callable onServiceFailure, - @NonNull IBinder binder) { + Callable request, @Nullable Callable 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 @@ public final class TextClassificationManagerService extends ITextClassifierServi @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 @@ public final class TextClassificationManagerService extends ITextClassifierServi @GuardedBy("mLock") private void removeLocked() { mPendingRequests.remove(this); - mBinder.unlinkToDeath(this, 0); + if (mBinder != null) { + mBinder.unlinkToDeath(this, 0); + } } } @@ -359,4 +384,16 @@ public final class TextClassificationManagerService extends ITextClassifierServi 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()); + } + } }