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