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