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 17 package android.view.textclassifier; 18 19 import android.annotation.NonNull; 20 import android.annotation.WorkerThread; 21 import android.view.textclassifier.SelectionEvent.InvocationMethod; 22 23 import com.android.internal.annotations.GuardedBy; 24 import com.android.internal.util.Preconditions; 25 26 import java.util.Objects; 27 import java.util.function.Supplier; 28 29 import sun.misc.Cleaner; 30 31 /** 32 * Session-aware TextClassifier. 33 */ 34 @WorkerThread 35 final class TextClassificationSession implements TextClassifier { 36 37 private static final String LOG_TAG = "TextClassificationSession"; 38 39 private final TextClassifier mDelegate; 40 private final SelectionEventHelper mEventHelper; 41 private final TextClassificationSessionId mSessionId; 42 private final TextClassificationContext mClassificationContext; 43 private final Cleaner mCleaner; 44 45 private final Object mLock = new Object(); 46 47 @GuardedBy("mLock") 48 private boolean mDestroyed; 49 TextClassificationSession(TextClassificationContext context, TextClassifier delegate)50 TextClassificationSession(TextClassificationContext context, TextClassifier delegate) { 51 mClassificationContext = Objects.requireNonNull(context); 52 mDelegate = Objects.requireNonNull(delegate); 53 mSessionId = new TextClassificationSessionId(); 54 mEventHelper = new SelectionEventHelper(mSessionId, mClassificationContext); 55 initializeRemoteSession(); 56 // This ensures destroy() is called if the client forgot to do so. 57 mCleaner = Cleaner.create(this, new CleanerRunnable(mEventHelper, mDelegate)); 58 } 59 60 @Override suggestSelection(TextSelection.Request request)61 public TextSelection suggestSelection(TextSelection.Request request) { 62 return checkDestroyedAndRun(() -> mDelegate.suggestSelection(request)); 63 } 64 initializeRemoteSession()65 private void initializeRemoteSession() { 66 if (mDelegate instanceof SystemTextClassifier) { 67 ((SystemTextClassifier) mDelegate).initializeRemoteSession( 68 mClassificationContext, mSessionId); 69 } 70 } 71 72 @Override classifyText(TextClassification.Request request)73 public TextClassification classifyText(TextClassification.Request request) { 74 return checkDestroyedAndRun(() -> mDelegate.classifyText(request)); 75 } 76 77 @Override generateLinks(TextLinks.Request request)78 public TextLinks generateLinks(TextLinks.Request request) { 79 return checkDestroyedAndRun(() -> mDelegate.generateLinks(request)); 80 } 81 82 @Override suggestConversationActions(ConversationActions.Request request)83 public ConversationActions suggestConversationActions(ConversationActions.Request request) { 84 return checkDestroyedAndRun(() -> mDelegate.suggestConversationActions(request)); 85 } 86 87 @Override detectLanguage(TextLanguage.Request request)88 public TextLanguage detectLanguage(TextLanguage.Request request) { 89 return checkDestroyedAndRun(() -> mDelegate.detectLanguage(request)); 90 } 91 92 @Override getMaxGenerateLinksTextLength()93 public int getMaxGenerateLinksTextLength() { 94 return checkDestroyedAndRun(mDelegate::getMaxGenerateLinksTextLength); 95 } 96 97 @Override onSelectionEvent(SelectionEvent event)98 public void onSelectionEvent(SelectionEvent event) { 99 checkDestroyedAndRun(() -> { 100 try { 101 if (mEventHelper.sanitizeEvent(event)) { 102 mDelegate.onSelectionEvent(event); 103 } 104 } catch (Exception e) { 105 // Avoid crashing for event reporting. 106 Log.e(LOG_TAG, "Error reporting text classifier selection event", e); 107 } 108 return null; 109 }); 110 } 111 112 @Override onTextClassifierEvent(TextClassifierEvent event)113 public void onTextClassifierEvent(TextClassifierEvent event) { 114 checkDestroyedAndRun(() -> { 115 try { 116 event.mHiddenTempSessionId = mSessionId; 117 mDelegate.onTextClassifierEvent(event); 118 } catch (Exception e) { 119 // Avoid crashing for event reporting. 120 Log.e(LOG_TAG, "Error reporting text classifier event", e); 121 } 122 return null; 123 }); 124 } 125 126 @Override destroy()127 public void destroy() { 128 synchronized (mLock) { 129 if (!mDestroyed) { 130 mCleaner.clean(); 131 mDestroyed = true; 132 } 133 } 134 } 135 136 @Override isDestroyed()137 public boolean isDestroyed() { 138 synchronized (mLock) { 139 return mDestroyed; 140 } 141 } 142 143 /** 144 * Check whether the TextClassification Session was destroyed before and after the actual API 145 * invocation, and return response if not. 146 * 147 * @param responseSupplier a Supplier that represents a TextClassifier call 148 * @return the response of the TextClassifier call 149 * @throws IllegalStateException if this TextClassification session was destroyed before the 150 * call returned 151 * @see #isDestroyed() 152 * @see #destroy() 153 */ checkDestroyedAndRun(Supplier<T> responseSupplier)154 private <T> T checkDestroyedAndRun(Supplier<T> responseSupplier) { 155 if (!isDestroyed()) { 156 T response = responseSupplier.get(); 157 synchronized (mLock) { 158 if (!mDestroyed) { 159 return response; 160 } 161 } 162 } 163 throw new IllegalStateException( 164 "This TextClassification session has been destroyed"); 165 } 166 167 /** 168 * Helper class for updating SelectionEvent fields. 169 */ 170 private static final class SelectionEventHelper { 171 172 private final TextClassificationSessionId mSessionId; 173 private final TextClassificationContext mContext; 174 175 @InvocationMethod 176 private int mInvocationMethod = SelectionEvent.INVOCATION_UNKNOWN; 177 private SelectionEvent mPrevEvent; 178 private SelectionEvent mSmartEvent; 179 private SelectionEvent mStartEvent; 180 SelectionEventHelper( TextClassificationSessionId sessionId, TextClassificationContext context)181 SelectionEventHelper( 182 TextClassificationSessionId sessionId, TextClassificationContext context) { 183 mSessionId = Objects.requireNonNull(sessionId); 184 mContext = Objects.requireNonNull(context); 185 } 186 187 /** 188 * Updates the necessary fields in the event for the current session. 189 * 190 * @return true if the event should be reported. false if the event should be ignored 191 */ sanitizeEvent(SelectionEvent event)192 boolean sanitizeEvent(SelectionEvent event) { 193 updateInvocationMethod(event); 194 modifyAutoSelectionEventType(event); 195 196 if (event.getEventType() != SelectionEvent.EVENT_SELECTION_STARTED 197 && mStartEvent == null) { 198 Log.d(LOG_TAG, "Selection session not yet started. Ignoring event"); 199 return false; 200 } 201 202 final long now = System.currentTimeMillis(); 203 switch (event.getEventType()) { 204 case SelectionEvent.EVENT_SELECTION_STARTED: 205 Preconditions.checkArgument( 206 event.getAbsoluteEnd() == event.getAbsoluteStart() + 1); 207 event.setSessionId(mSessionId); 208 mStartEvent = event; 209 break; 210 case SelectionEvent.EVENT_SMART_SELECTION_SINGLE: // fall through 211 case SelectionEvent.EVENT_SMART_SELECTION_MULTI: // fall through 212 case SelectionEvent.EVENT_AUTO_SELECTION: 213 mSmartEvent = event; 214 break; 215 case SelectionEvent.ACTION_ABANDON: 216 case SelectionEvent.ACTION_OVERTYPE: 217 if (mPrevEvent != null) { 218 event.setEntityType(mPrevEvent.getEntityType()); 219 } 220 break; 221 case SelectionEvent.EVENT_SELECTION_MODIFIED: 222 if (mPrevEvent != null 223 && mPrevEvent.getAbsoluteStart() == event.getAbsoluteStart() 224 && mPrevEvent.getAbsoluteEnd() == event.getAbsoluteEnd()) { 225 // Selection did not change. Ignore event. 226 return false; 227 } 228 break; 229 default: 230 // do nothing. 231 } 232 233 event.setEventTime(now); 234 if (mStartEvent != null) { 235 event.setSessionId(mStartEvent.getSessionId()) 236 .setDurationSinceSessionStart(now - mStartEvent.getEventTime()) 237 .setStart(event.getAbsoluteStart() - mStartEvent.getAbsoluteStart()) 238 .setEnd(event.getAbsoluteEnd() - mStartEvent.getAbsoluteStart()); 239 } 240 if (mSmartEvent != null) { 241 event.setResultId(mSmartEvent.getResultId()) 242 .setSmartStart( 243 mSmartEvent.getAbsoluteStart() - mStartEvent.getAbsoluteStart()) 244 .setSmartEnd(mSmartEvent.getAbsoluteEnd() - mStartEvent.getAbsoluteStart()); 245 } 246 if (mPrevEvent != null) { 247 event.setDurationSincePreviousEvent(now - mPrevEvent.getEventTime()) 248 .setEventIndex(mPrevEvent.getEventIndex() + 1); 249 } 250 mPrevEvent = event; 251 return true; 252 } 253 endSession()254 void endSession() { 255 mPrevEvent = null; 256 mSmartEvent = null; 257 mStartEvent = null; 258 } 259 updateInvocationMethod(SelectionEvent event)260 private void updateInvocationMethod(SelectionEvent event) { 261 event.setTextClassificationSessionContext(mContext); 262 if (event.getInvocationMethod() == SelectionEvent.INVOCATION_UNKNOWN) { 263 event.setInvocationMethod(mInvocationMethod); 264 } else { 265 mInvocationMethod = event.getInvocationMethod(); 266 } 267 } 268 modifyAutoSelectionEventType(SelectionEvent event)269 private void modifyAutoSelectionEventType(SelectionEvent event) { 270 switch (event.getEventType()) { 271 case SelectionEvent.EVENT_SMART_SELECTION_SINGLE: // fall through 272 case SelectionEvent.EVENT_SMART_SELECTION_MULTI: // fall through 273 case SelectionEvent.EVENT_AUTO_SELECTION: 274 if (SelectionSessionLogger.isPlatformLocalTextClassifierSmartSelection( 275 event.getResultId())) { 276 if (event.getAbsoluteEnd() - event.getAbsoluteStart() > 1) { 277 event.setEventType(SelectionEvent.EVENT_SMART_SELECTION_MULTI); 278 } else { 279 event.setEventType(SelectionEvent.EVENT_SMART_SELECTION_SINGLE); 280 } 281 } else { 282 event.setEventType(SelectionEvent.EVENT_AUTO_SELECTION); 283 } 284 return; 285 default: 286 return; 287 } 288 } 289 } 290 291 // We use a static nested class here to avoid retaining the object reference of the outer 292 // class. Otherwise. the Cleaner would never be triggered. 293 private static class CleanerRunnable implements Runnable { 294 @NonNull 295 private final SelectionEventHelper mEventHelper; 296 @NonNull 297 private final TextClassifier mDelegate; 298 CleanerRunnable( @onNull SelectionEventHelper eventHelper, @NonNull TextClassifier delegate)299 CleanerRunnable( 300 @NonNull SelectionEventHelper eventHelper, @NonNull TextClassifier delegate) { 301 mEventHelper = Objects.requireNonNull(eventHelper); 302 mDelegate = Objects.requireNonNull(delegate); 303 } 304 305 @Override run()306 public void run() { 307 mEventHelper.endSession(); 308 mDelegate.destroy(); 309 } 310 } 311 } 312