TextClassificationSession.java revision 88be5a6cee59868eaee6f7b52fd8b2e6f6f28429
1/*
2 * Copyright (C) 2018 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.view.textclassifier;
18
19import android.annotation.WorkerThread;
20import android.view.textclassifier.DefaultLogger.SignatureParser;
21import android.view.textclassifier.SelectionEvent.InvocationMethod;
22
23import com.android.internal.util.Preconditions;
24
25/**
26 * Session-aware TextClassifier.
27 */
28@WorkerThread
29final class TextClassificationSession implements TextClassifier {
30
31    /* package */ static final boolean DEBUG_LOG_ENABLED = true;
32    private static final String LOG_TAG = "TextClassificationSession";
33
34    private final TextClassifier mDelegate;
35    private final SelectionEventHelper mEventHelper;
36
37    private boolean mDestroyed;
38
39    TextClassificationSession(TextClassificationContext context, TextClassifier delegate) {
40        mDelegate = Preconditions.checkNotNull(delegate);
41        mEventHelper = new SelectionEventHelper(new TextClassificationSessionId(), context);
42    }
43
44    @Override
45    public TextSelection suggestSelection(CharSequence text, int selectionStartIndex,
46            int selectionEndIndex, TextSelection.Options options) {
47        checkDestroyed();
48        return mDelegate.suggestSelection(text, selectionStartIndex, selectionEndIndex, options);
49    }
50
51    @Override
52    public TextClassification classifyText(CharSequence text, int startIndex, int endIndex,
53            TextClassification.Options options) {
54        checkDestroyed();
55        return mDelegate.classifyText(text, startIndex, endIndex, options);
56    }
57
58    @Override
59    public TextLinks generateLinks(CharSequence text, TextLinks.Options options) {
60        checkDestroyed();
61        return mDelegate.generateLinks(text, options);
62    }
63
64    @Override
65    public void onSelectionEvent(SelectionEvent event) {
66        checkDestroyed();
67        Preconditions.checkNotNull(event);
68        if (mEventHelper.sanitizeEvent(event)) {
69            mDelegate.onSelectionEvent(event);
70        }
71    }
72
73    @Override
74    public void destroy() {
75        mEventHelper.endSession();
76        mDestroyed = true;
77    }
78
79    @Override
80    public boolean isDestroyed() {
81        return mDestroyed;
82    }
83
84    /**
85     * @throws IllegalStateException if this TextClassification session has been destroyed.
86     * @see #isDestroyed()
87     * @see #destroy()
88     */
89    private void checkDestroyed() {
90        if (mDestroyed) {
91            throw new IllegalStateException("This TextClassification session has been destroyed");
92        }
93    }
94
95    /**
96     * Helper class for updating SelectionEvent fields.
97     */
98    private static final class SelectionEventHelper {
99
100        private final TextClassificationSessionId mSessionId;
101        private final TextClassificationContext mContext;
102
103        @InvocationMethod
104        private int mInvocationMethod = SelectionEvent.INVOCATION_UNKNOWN;
105        private SelectionEvent mPrevEvent;
106        private SelectionEvent mSmartEvent;
107        private SelectionEvent mStartEvent;
108
109        SelectionEventHelper(
110                TextClassificationSessionId sessionId, TextClassificationContext context) {
111            mSessionId = Preconditions.checkNotNull(sessionId);
112            mContext = Preconditions.checkNotNull(context);
113        }
114
115        /**
116         * Updates the necessary fields in the event for the current session.
117         *
118         * @return true if the event should be reported. false if the event should be ignored
119         */
120        boolean sanitizeEvent(SelectionEvent event) {
121            updateInvocationMethod(event);
122            modifyAutoSelectionEventType(event);
123
124            if (event.getEventType() != SelectionEvent.EVENT_SELECTION_STARTED
125                    && mStartEvent == null) {
126                if (DEBUG_LOG_ENABLED) {
127                    Log.d(LOG_TAG, "Selection session not yet started. Ignoring event");
128                }
129                return false;
130            }
131
132            final long now = System.currentTimeMillis();
133            switch (event.getEventType()) {
134                case SelectionEvent.EVENT_SELECTION_STARTED:
135                    Preconditions.checkArgument(
136                            event.getAbsoluteEnd() == event.getAbsoluteStart() + 1);
137                    event.setSessionId(mSessionId);
138                    mStartEvent = event;
139                    break;
140                case SelectionEvent.EVENT_SMART_SELECTION_SINGLE:  // fall through
141                case SelectionEvent.EVENT_SMART_SELECTION_MULTI:
142                    mSmartEvent = event;
143                    break;
144                case SelectionEvent.EVENT_SELECTION_MODIFIED:  // fall through
145                case SelectionEvent.EVENT_AUTO_SELECTION:
146                    if (mPrevEvent != null
147                            && mPrevEvent.getAbsoluteStart() == event.getAbsoluteStart()
148                            && mPrevEvent.getAbsoluteEnd() == event.getAbsoluteEnd()) {
149                        // Selection did not change. Ignore event.
150                        return false;
151                    }
152                    break;
153                default:
154                    // do nothing.
155            }
156
157            event.setEventTime(now);
158            if (mStartEvent != null) {
159                event.setSessionId(mStartEvent.getSessionId())
160                        .setDurationSinceSessionStart(now - mStartEvent.getEventTime())
161                        .setStart(event.getAbsoluteStart() - mStartEvent.getAbsoluteStart())
162                        .setEnd(event.getAbsoluteEnd() - mStartEvent.getAbsoluteStart());
163            }
164            if (mSmartEvent != null) {
165                event.setSignature(mSmartEvent.getSignature())
166                        .setSmartStart(
167                                mSmartEvent.getAbsoluteStart() - mStartEvent.getAbsoluteStart())
168                        .setSmartEnd(mSmartEvent.getAbsoluteEnd() - mStartEvent.getAbsoluteStart());
169            }
170            if (mPrevEvent != null) {
171                event.setDurationSincePreviousEvent(now - mPrevEvent.getEventTime())
172                        .setEventIndex(mPrevEvent.getEventIndex() + 1);
173            }
174            mPrevEvent = event;
175            return true;
176        }
177
178        void endSession() {
179            mPrevEvent = null;
180            mSmartEvent = null;
181            mStartEvent = null;
182        }
183
184        private void updateInvocationMethod(SelectionEvent event) {
185            event.setTextClassificationSessionContext(mContext);
186            if (event.getInvocationMethod() == SelectionEvent.INVOCATION_UNKNOWN) {
187                event.setInvocationMethod(mInvocationMethod);
188            } else {
189                mInvocationMethod = event.getInvocationMethod();
190            }
191        }
192
193        private void modifyAutoSelectionEventType(SelectionEvent event) {
194            switch (event.getEventType()) {
195                case SelectionEvent.EVENT_SMART_SELECTION_SINGLE:  // fall through
196                case SelectionEvent.EVENT_SMART_SELECTION_MULTI:  // fall through
197                case SelectionEvent.EVENT_AUTO_SELECTION:
198                    if (isPlatformLocalTextClassifierSmartSelection(event.getSignature())) {
199                        if (event.getAbsoluteEnd() - event.getAbsoluteStart() > 1) {
200                            event.setEventType(SelectionEvent.EVENT_SMART_SELECTION_MULTI);
201                        } else {
202                            event.setEventType(SelectionEvent.EVENT_SMART_SELECTION_SINGLE);
203                        }
204                    } else {
205                        event.setEventType(SelectionEvent.EVENT_AUTO_SELECTION);
206                    }
207                    return;
208                default:
209                    return;
210            }
211        }
212
213        private static boolean isPlatformLocalTextClassifierSmartSelection(String signature) {
214            return DefaultLogger.CLASSIFIER_ID.equals(SignatureParser.getClassifierId(signature));
215        }
216    }
217}
218