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.Nullable; 21 import android.annotation.WorkerThread; 22 import android.content.Context; 23 import android.os.Bundle; 24 import android.os.Looper; 25 import android.os.Parcelable; 26 import android.os.RemoteException; 27 import android.os.ServiceManager; 28 import android.service.textclassifier.ITextClassifierCallback; 29 import android.service.textclassifier.ITextClassifierService; 30 import android.service.textclassifier.TextClassifierService; 31 32 import com.android.internal.annotations.VisibleForTesting; 33 import com.android.internal.annotations.VisibleForTesting.Visibility; 34 import com.android.internal.util.IndentingPrintWriter; 35 36 import java.util.Objects; 37 import java.util.concurrent.CountDownLatch; 38 import java.util.concurrent.TimeUnit; 39 40 /** 41 * proxy to the request to TextClassifierService via the TextClassificationManagerService. 42 * 43 * @hide 44 */ 45 @VisibleForTesting(visibility = Visibility.PACKAGE) 46 public final class SystemTextClassifier implements TextClassifier { 47 48 private static final String LOG_TAG = TextClassifier.LOG_TAG; 49 50 private final ITextClassifierService mManagerService; 51 private final TextClassificationConstants mSettings; 52 private final TextClassifier mFallback; 53 private TextClassificationSessionId mSessionId; 54 // NOTE: Always set this before sending a request to the manager service otherwise the 55 // manager service will throw a remote exception. 56 @NonNull 57 private final SystemTextClassifierMetadata mSystemTcMetadata; 58 59 /** 60 * Constructor of {@link SystemTextClassifier} 61 * 62 * @param context the context of the request. 63 * @param settings TextClassifier specific settings. 64 * @param useDefault whether to use the default text classifier to handle this request 65 */ SystemTextClassifier( Context context, TextClassificationConstants settings, boolean useDefault)66 public SystemTextClassifier( 67 Context context, 68 TextClassificationConstants settings, 69 boolean useDefault) throws ServiceManager.ServiceNotFoundException { 70 mManagerService = ITextClassifierService.Stub.asInterface( 71 ServiceManager.getServiceOrThrow(Context.TEXT_CLASSIFICATION_SERVICE)); 72 mSettings = Objects.requireNonNull(settings); 73 mFallback = TextClassifier.NO_OP; 74 // NOTE: Always set this before sending a request to the manager service otherwise the 75 // manager service will throw a remote exception. 76 mSystemTcMetadata = new SystemTextClassifierMetadata( 77 Objects.requireNonNull(context.getOpPackageName()), context.getUserId(), 78 useDefault); 79 } 80 81 /** 82 * @inheritDoc 83 */ 84 @Override 85 @WorkerThread suggestSelection(TextSelection.Request request)86 public TextSelection suggestSelection(TextSelection.Request request) { 87 Objects.requireNonNull(request); 88 Utils.checkMainThread(); 89 try { 90 request.setSystemTextClassifierMetadata(mSystemTcMetadata); 91 final BlockingCallback<TextSelection> callback = 92 new BlockingCallback<>("textselection", mSettings); 93 mManagerService.onSuggestSelection(mSessionId, request, callback); 94 final TextSelection selection = callback.get(); 95 if (selection != null) { 96 return selection; 97 } 98 } catch (RemoteException e) { 99 Log.e(LOG_TAG, "Error suggesting selection for text. Using fallback.", e); 100 } 101 return mFallback.suggestSelection(request); 102 } 103 104 /** 105 * @inheritDoc 106 */ 107 @Override 108 @WorkerThread classifyText(TextClassification.Request request)109 public TextClassification classifyText(TextClassification.Request request) { 110 Objects.requireNonNull(request); 111 Utils.checkMainThread(); 112 try { 113 request.setSystemTextClassifierMetadata(mSystemTcMetadata); 114 final BlockingCallback<TextClassification> callback = 115 new BlockingCallback<>("textclassification", mSettings); 116 mManagerService.onClassifyText(mSessionId, request, callback); 117 final TextClassification classification = callback.get(); 118 if (classification != null) { 119 return classification; 120 } 121 } catch (RemoteException e) { 122 Log.e(LOG_TAG, "Error classifying text. Using fallback.", e); 123 } 124 return mFallback.classifyText(request); 125 } 126 127 /** 128 * @inheritDoc 129 */ 130 @Override 131 @WorkerThread generateLinks(@onNull TextLinks.Request request)132 public TextLinks generateLinks(@NonNull TextLinks.Request request) { 133 Objects.requireNonNull(request); 134 Utils.checkMainThread(); 135 if (!Utils.checkTextLength(request.getText(), getMaxGenerateLinksTextLength())) { 136 return mFallback.generateLinks(request); 137 } 138 if (!mSettings.isSmartLinkifyEnabled() && request.isLegacyFallback()) { 139 return Utils.generateLegacyLinks(request); 140 } 141 142 try { 143 request.setSystemTextClassifierMetadata(mSystemTcMetadata); 144 final BlockingCallback<TextLinks> callback = 145 new BlockingCallback<>("textlinks", mSettings); 146 mManagerService.onGenerateLinks(mSessionId, request, callback); 147 final TextLinks links = callback.get(); 148 if (links != null) { 149 return links; 150 } 151 } catch (RemoteException e) { 152 Log.e(LOG_TAG, "Error generating links. Using fallback.", e); 153 } 154 return mFallback.generateLinks(request); 155 } 156 157 @Override onSelectionEvent(SelectionEvent event)158 public void onSelectionEvent(SelectionEvent event) { 159 Objects.requireNonNull(event); 160 Utils.checkMainThread(); 161 162 try { 163 event.setSystemTextClassifierMetadata(mSystemTcMetadata); 164 mManagerService.onSelectionEvent(mSessionId, event); 165 } catch (RemoteException e) { 166 Log.e(LOG_TAG, "Error reporting selection event.", e); 167 } 168 } 169 170 @Override onTextClassifierEvent(@onNull TextClassifierEvent event)171 public void onTextClassifierEvent(@NonNull TextClassifierEvent event) { 172 Objects.requireNonNull(event); 173 Utils.checkMainThread(); 174 175 try { 176 final TextClassificationContext tcContext = 177 event.getEventContext() == null ? new TextClassificationContext.Builder( 178 mSystemTcMetadata.getCallingPackageName(), WIDGET_TYPE_UNKNOWN).build() 179 : event.getEventContext(); 180 tcContext.setSystemTextClassifierMetadata(mSystemTcMetadata); 181 event.setEventContext(tcContext); 182 mManagerService.onTextClassifierEvent(mSessionId, event); 183 } catch (RemoteException e) { 184 Log.e(LOG_TAG, "Error reporting textclassifier event.", e); 185 } 186 } 187 188 @Override detectLanguage(TextLanguage.Request request)189 public TextLanguage detectLanguage(TextLanguage.Request request) { 190 Objects.requireNonNull(request); 191 Utils.checkMainThread(); 192 193 try { 194 request.setSystemTextClassifierMetadata(mSystemTcMetadata); 195 final BlockingCallback<TextLanguage> callback = 196 new BlockingCallback<>("textlanguage", mSettings); 197 mManagerService.onDetectLanguage(mSessionId, request, callback); 198 final TextLanguage textLanguage = callback.get(); 199 if (textLanguage != null) { 200 return textLanguage; 201 } 202 } catch (RemoteException e) { 203 Log.e(LOG_TAG, "Error detecting language.", e); 204 } 205 return mFallback.detectLanguage(request); 206 } 207 208 @Override suggestConversationActions(ConversationActions.Request request)209 public ConversationActions suggestConversationActions(ConversationActions.Request request) { 210 Objects.requireNonNull(request); 211 Utils.checkMainThread(); 212 213 try { 214 request.setSystemTextClassifierMetadata(mSystemTcMetadata); 215 final BlockingCallback<ConversationActions> callback = 216 new BlockingCallback<>("conversation-actions", mSettings); 217 mManagerService.onSuggestConversationActions(mSessionId, request, callback); 218 final ConversationActions conversationActions = callback.get(); 219 if (conversationActions != null) { 220 return conversationActions; 221 } 222 } catch (RemoteException e) { 223 Log.e(LOG_TAG, "Error reporting selection event.", e); 224 } 225 return mFallback.suggestConversationActions(request); 226 } 227 228 /** 229 * @inheritDoc 230 */ 231 @Override 232 @WorkerThread getMaxGenerateLinksTextLength()233 public int getMaxGenerateLinksTextLength() { 234 // TODO: retrieve this from the bound service. 235 return mSettings.getGenerateLinksMaxTextLength(); 236 } 237 238 @Override destroy()239 public void destroy() { 240 try { 241 if (mSessionId != null) { 242 mManagerService.onDestroyTextClassificationSession(mSessionId); 243 } 244 } catch (RemoteException e) { 245 Log.e(LOG_TAG, "Error destroying classification session.", e); 246 } 247 } 248 249 @Override dump(@onNull IndentingPrintWriter printWriter)250 public void dump(@NonNull IndentingPrintWriter printWriter) { 251 printWriter.println("SystemTextClassifier:"); 252 printWriter.increaseIndent(); 253 printWriter.printPair("mFallback", mFallback); 254 printWriter.printPair("mSessionId", mSessionId); 255 printWriter.printPair("mSystemTcMetadata", mSystemTcMetadata); 256 printWriter.decreaseIndent(); 257 printWriter.println(); 258 } 259 260 /** 261 * Attempts to initialize a new classification session. 262 * 263 * @param classificationContext the classification context 264 * @param sessionId the session's id 265 */ initializeRemoteSession( @onNull TextClassificationContext classificationContext, @NonNull TextClassificationSessionId sessionId)266 void initializeRemoteSession( 267 @NonNull TextClassificationContext classificationContext, 268 @NonNull TextClassificationSessionId sessionId) { 269 mSessionId = Objects.requireNonNull(sessionId); 270 try { 271 classificationContext.setSystemTextClassifierMetadata(mSystemTcMetadata); 272 mManagerService.onCreateTextClassificationSession(classificationContext, mSessionId); 273 } catch (RemoteException e) { 274 Log.e(LOG_TAG, "Error starting a new classification session.", e); 275 } 276 } 277 278 private static final class BlockingCallback<T extends Parcelable> 279 extends ITextClassifierCallback.Stub { 280 private final ResponseReceiver<T> mReceiver; 281 BlockingCallback(String name, TextClassificationConstants settings)282 BlockingCallback(String name, TextClassificationConstants settings) { 283 mReceiver = new ResponseReceiver<>(name, settings); 284 } 285 286 @Override onSuccess(Bundle result)287 public void onSuccess(Bundle result) { 288 mReceiver.onSuccess(TextClassifierService.getResponse(result)); 289 } 290 291 @Override onFailure()292 public void onFailure() { 293 mReceiver.onFailure(); 294 } 295 get()296 public T get() { 297 return mReceiver.get(); 298 } 299 300 } 301 302 private static final class ResponseReceiver<T> { 303 304 private final CountDownLatch mLatch = new CountDownLatch(1); 305 private final String mName; 306 private final TextClassificationConstants mSettings; 307 private T mResponse; 308 ResponseReceiver(String name, TextClassificationConstants settings)309 private ResponseReceiver(String name, TextClassificationConstants settings) { 310 mName = name; 311 mSettings = settings; 312 } 313 onSuccess(T response)314 public void onSuccess(T response) { 315 mResponse = response; 316 mLatch.countDown(); 317 } 318 onFailure()319 public void onFailure() { 320 Log.e(LOG_TAG, "Request failed at " + mName, null); 321 mLatch.countDown(); 322 } 323 324 @Nullable get()325 public T get() { 326 // If this is running on the main thread, do not block for a response. 327 // The response will unfortunately be null and the TextClassifier should depend on its 328 // fallback. 329 // NOTE that TextClassifier calls should preferably always be called on a worker thread. 330 if (Looper.myLooper() != Looper.getMainLooper()) { 331 try { 332 boolean success = mLatch.await( 333 mSettings.getSystemTextClassifierApiTimeoutInSecond(), 334 TimeUnit.SECONDS); 335 if (!success) { 336 Log.w(LOG_TAG, "Timeout in ResponseReceiver.get(): " + mName); 337 } 338 } catch (InterruptedException e) { 339 Thread.currentThread().interrupt(); 340 Log.e(LOG_TAG, "Interrupted during ResponseReceiver.get(): " + mName, e); 341 } 342 } 343 return mResponse; 344 } 345 } 346 } 347