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