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