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