SystemTextClassifier.java revision dfd3c653cef679b05a5e4f79b2e894b5b0e55c37
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.NonNull;
20import android.annotation.Nullable;
21import android.annotation.WorkerThread;
22import android.content.Context;
23import android.os.Looper;
24import android.os.RemoteException;
25import android.os.ServiceManager;
26import android.service.textclassifier.ITextClassificationCallback;
27import android.service.textclassifier.ITextClassifierService;
28import android.service.textclassifier.ITextLinksCallback;
29import android.service.textclassifier.ITextSelectionCallback;
30
31import com.android.internal.annotations.VisibleForTesting;
32import com.android.internal.annotations.VisibleForTesting.Visibility;
33import com.android.internal.util.Preconditions;
34
35import java.util.concurrent.CountDownLatch;
36import java.util.concurrent.TimeUnit;
37
38/**
39 * Proxy to the system's default TextClassifier.
40 * @hide
41 */
42@VisibleForTesting(visibility = Visibility.PACKAGE)
43public final class SystemTextClassifier implements TextClassifier {
44
45    private static final String LOG_TAG = "SystemTextClassifier";
46
47    private final ITextClassifierService mManagerService;
48    private final TextClassificationConstants mSettings;
49    private final TextClassifier mFallback;
50    private final String mPackageName;
51    private TextClassificationSessionId mSessionId;
52
53    public SystemTextClassifier(Context context, TextClassificationConstants settings)
54                throws ServiceManager.ServiceNotFoundException {
55        mManagerService = ITextClassifierService.Stub.asInterface(
56                ServiceManager.getServiceOrThrow(Context.TEXT_CLASSIFICATION_SERVICE));
57        mSettings = Preconditions.checkNotNull(settings);
58        mFallback = context.getSystemService(TextClassificationManager.class)
59                .getTextClassifier(TextClassifier.LOCAL);
60        mPackageName = Preconditions.checkNotNull(context.getPackageName());
61    }
62
63    /**
64     * @inheritDoc
65     */
66    @Override
67    @WorkerThread
68    public TextSelection suggestSelection(TextSelection.Request request) {
69        Preconditions.checkNotNull(request);
70        Utils.checkMainThread();
71        try {
72            final TextSelectionCallback callback = new TextSelectionCallback();
73            mManagerService.onSuggestSelection(mSessionId, request, callback);
74            final TextSelection selection = callback.mReceiver.get();
75            if (selection != null) {
76                return selection;
77            }
78        } catch (RemoteException | InterruptedException e) {
79            Log.e(LOG_TAG, "Error suggesting selection for text. Using fallback.", e);
80        }
81        return mFallback.suggestSelection(request);
82    }
83
84    /**
85     * @inheritDoc
86     */
87    @Override
88    @WorkerThread
89    public TextClassification classifyText(TextClassification.Request request) {
90        Preconditions.checkNotNull(request);
91        Utils.checkMainThread();
92        try {
93            final TextClassificationCallback callback = new TextClassificationCallback();
94            mManagerService.onClassifyText(mSessionId, request, callback);
95            final TextClassification classification = callback.mReceiver.get();
96            if (classification != null) {
97                return classification;
98            }
99        } catch (RemoteException | InterruptedException e) {
100            Log.e(LOG_TAG, "Error classifying text. Using fallback.", e);
101        }
102        return mFallback.classifyText(request);
103    }
104
105    /**
106     * @inheritDoc
107     */
108    @Override
109    @WorkerThread
110    public TextLinks generateLinks(@NonNull TextLinks.Request request) {
111        Preconditions.checkNotNull(request);
112        Utils.checkMainThread();
113
114        if (!mSettings.isSmartLinkifyEnabled() && request.isLegacyFallback()) {
115            return Utils.generateLegacyLinks(request);
116        }
117
118        try {
119            request.setCallingPackageName(mPackageName);
120            final TextLinksCallback callback = new TextLinksCallback();
121            mManagerService.onGenerateLinks(mSessionId, request, callback);
122            final TextLinks links = callback.mReceiver.get();
123            if (links != null) {
124                return links;
125            }
126        } catch (RemoteException | InterruptedException e) {
127            Log.e(LOG_TAG, "Error generating links. Using fallback.", e);
128        }
129        return mFallback.generateLinks(request);
130    }
131
132    @Override
133    public void onSelectionEvent(SelectionEvent event) {
134        Preconditions.checkNotNull(event);
135        Utils.checkMainThread();
136
137        try {
138            mManagerService.onSelectionEvent(mSessionId, event);
139        } catch (RemoteException e) {
140            Log.e(LOG_TAG, "Error reporting selection event.", e);
141        }
142    }
143
144    /**
145     * @inheritDoc
146     */
147    @Override
148    @WorkerThread
149    public int getMaxGenerateLinksTextLength() {
150        // TODO: retrieve this from the bound service.
151        return mFallback.getMaxGenerateLinksTextLength();
152    }
153
154    @Override
155    public void destroy() {
156        try {
157            if (mSessionId != null) {
158                mManagerService.onDestroyTextClassificationSession(mSessionId);
159            }
160        } catch (RemoteException e) {
161            Log.e(LOG_TAG, "Error destroying classification session.", e);
162        }
163    }
164
165    /**
166     * Attempts to initialize a new classification session.
167     *
168     * @param classificationContext the classification context
169     * @param sessionId the session's id
170     */
171    void initializeRemoteSession(
172            @NonNull TextClassificationContext classificationContext,
173            @NonNull TextClassificationSessionId sessionId) {
174        mSessionId = Preconditions.checkNotNull(sessionId);
175        try {
176            mManagerService.onCreateTextClassificationSession(classificationContext, mSessionId);
177        } catch (RemoteException e) {
178            Log.e(LOG_TAG, "Error starting a new classification session.", e);
179        }
180    }
181
182    private static final class TextSelectionCallback extends ITextSelectionCallback.Stub {
183
184        final ResponseReceiver<TextSelection> mReceiver = new ResponseReceiver<>();
185
186        @Override
187        public void onSuccess(TextSelection selection) {
188            mReceiver.onSuccess(selection);
189        }
190
191        @Override
192        public void onFailure() {
193            mReceiver.onFailure();
194        }
195    }
196
197    private static final class TextClassificationCallback extends ITextClassificationCallback.Stub {
198
199        final ResponseReceiver<TextClassification> mReceiver = new ResponseReceiver<>();
200
201        @Override
202        public void onSuccess(TextClassification classification) {
203            mReceiver.onSuccess(classification);
204        }
205
206        @Override
207        public void onFailure() {
208            mReceiver.onFailure();
209        }
210    }
211
212    private static final class TextLinksCallback extends ITextLinksCallback.Stub {
213
214        final ResponseReceiver<TextLinks> mReceiver = new ResponseReceiver<>();
215
216        @Override
217        public void onSuccess(TextLinks links) {
218            mReceiver.onSuccess(links);
219        }
220
221        @Override
222        public void onFailure() {
223            mReceiver.onFailure();
224        }
225    }
226
227    private static final class ResponseReceiver<T> {
228
229        private final CountDownLatch mLatch = new CountDownLatch(1);
230
231        private T mResponse;
232
233        public void onSuccess(T response) {
234            mResponse = response;
235            mLatch.countDown();
236        }
237
238        public void onFailure() {
239            Log.e(LOG_TAG, "Request failed.", null);
240            mLatch.countDown();
241        }
242
243        @Nullable
244        public T get() throws InterruptedException {
245            // If this is running on the main thread, do not block for a response.
246            // The response will unfortunately be null and the TextClassifier should depend on its
247            // fallback.
248            // NOTE that TextClassifier calls should preferably always be called on a worker thread.
249            if (Looper.myLooper() != Looper.getMainLooper()) {
250                mLatch.await(2, TimeUnit.SECONDS);
251            }
252            return mResponse;
253        }
254    }
255}
256