1 /*
2  * Copyright (C) 2017 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.SystemService;
22 import android.app.ActivityThread;
23 import android.compat.annotation.UnsupportedAppUsage;
24 import android.content.Context;
25 import android.database.ContentObserver;
26 import android.os.ServiceManager;
27 import android.provider.DeviceConfig;
28 import android.provider.DeviceConfig.Properties;
29 import android.provider.Settings;
30 import android.service.textclassifier.TextClassifierService;
31 import android.view.textclassifier.TextClassifier.TextClassifierType;
32 
33 import com.android.internal.annotations.GuardedBy;
34 import com.android.internal.annotations.VisibleForTesting;
35 import com.android.internal.util.IndentingPrintWriter;
36 import com.android.internal.util.Preconditions;
37 
38 import java.lang.ref.WeakReference;
39 
40 /**
41  * Interface to the text classification service.
42  */
43 @SystemService(Context.TEXT_CLASSIFICATION_SERVICE)
44 public final class TextClassificationManager {
45 
46     private static final String LOG_TAG = "TextClassificationManager";
47 
48     private static final TextClassificationConstants sDefaultSettings =
49             new TextClassificationConstants(() ->  null);
50 
51     private final Object mLock = new Object();
52     private final TextClassificationSessionFactory mDefaultSessionFactory =
53             classificationContext -> new TextClassificationSession(
54                     classificationContext, getTextClassifier());
55 
56     private final Context mContext;
57     private final SettingsObserver mSettingsObserver;
58 
59     @GuardedBy("mLock")
60     @Nullable
61     private TextClassifier mCustomTextClassifier;
62     @GuardedBy("mLock")
63     @Nullable
64     private TextClassifier mLocalTextClassifier;
65     @GuardedBy("mLock")
66     @Nullable
67     private TextClassifier mSystemTextClassifier;
68     @GuardedBy("mLock")
69     private TextClassificationSessionFactory mSessionFactory;
70     @GuardedBy("mLock")
71     private TextClassificationConstants mSettings;
72 
73     /** @hide */
TextClassificationManager(Context context)74     public TextClassificationManager(Context context) {
75         mContext = Preconditions.checkNotNull(context);
76         mSessionFactory = mDefaultSessionFactory;
77         mSettingsObserver = new SettingsObserver(this);
78     }
79 
80     /**
81      * Returns the text classifier that was set via {@link #setTextClassifier(TextClassifier)}.
82      * If this is null, this method returns a default text classifier (i.e. either the system text
83      * classifier if one exists, or a local text classifier running in this process.)
84      * <p>
85      * Note that requests to the TextClassifier may be handled in an OEM-provided process rather
86      * than in the calling app's process.
87      *
88      * @see #setTextClassifier(TextClassifier)
89      */
90     @NonNull
getTextClassifier()91     public TextClassifier getTextClassifier() {
92         synchronized (mLock) {
93             if (mCustomTextClassifier != null) {
94                 return mCustomTextClassifier;
95             } else if (isSystemTextClassifierEnabled()) {
96                 return getSystemTextClassifier();
97             } else {
98                 return getLocalTextClassifier();
99             }
100         }
101     }
102 
103     /**
104      * Sets the text classifier.
105      * Set to null to use the system default text classifier.
106      * Set to {@link TextClassifier#NO_OP} to disable text classifier features.
107      */
setTextClassifier(@ullable TextClassifier textClassifier)108     public void setTextClassifier(@Nullable TextClassifier textClassifier) {
109         synchronized (mLock) {
110             mCustomTextClassifier = textClassifier;
111         }
112     }
113 
114     /**
115      * Returns a specific type of text classifier.
116      * If the specified text classifier cannot be found, this returns {@link TextClassifier#NO_OP}.
117      *
118      * @see TextClassifier#LOCAL
119      * @see TextClassifier#SYSTEM
120      * @hide
121      */
122     @UnsupportedAppUsage
getTextClassifier(@extClassifierType int type)123     public TextClassifier getTextClassifier(@TextClassifierType int type) {
124         switch (type) {
125             case TextClassifier.LOCAL:
126                 return getLocalTextClassifier();
127             default:
128                 return getSystemTextClassifier();
129         }
130     }
131 
getSettings()132     private TextClassificationConstants getSettings() {
133         synchronized (mLock) {
134             if (mSettings == null) {
135                 mSettings = new TextClassificationConstants(
136                         () ->  Settings.Global.getString(
137                                 getApplicationContext().getContentResolver(),
138                                 Settings.Global.TEXT_CLASSIFIER_CONSTANTS));
139             }
140             return mSettings;
141         }
142     }
143 
144     /**
145      * Call this method to start a text classification session with the given context.
146      * A session is created with a context helping the classifier better understand
147      * what the user needs and consists of queries and feedback events. The queries
148      * are directly related to providing useful functionality to the user and the events
149      * are a feedback loop back to the classifier helping it learn and better serve
150      * future queries.
151      *
152      * <p> All interactions with the returned classifier are considered part of a single
153      * session and are logically grouped. For example, when a text widget is focused
154      * all user interactions around text editing (selection, editing, etc) can be
155      * grouped together to allow the classifier get better.
156      *
157      * @param classificationContext The context in which classification would occur
158      *
159      * @return An instance to perform classification in the given context
160      */
161     @NonNull
createTextClassificationSession( @onNull TextClassificationContext classificationContext)162     public TextClassifier createTextClassificationSession(
163             @NonNull TextClassificationContext classificationContext) {
164         Preconditions.checkNotNull(classificationContext);
165         final TextClassifier textClassifier =
166                 mSessionFactory.createTextClassificationSession(classificationContext);
167         Preconditions.checkNotNull(textClassifier, "Session Factory should never return null");
168         return textClassifier;
169     }
170 
171     /**
172      * @see #createTextClassificationSession(TextClassificationContext, TextClassifier)
173      * @hide
174      */
createTextClassificationSession( TextClassificationContext classificationContext, TextClassifier textClassifier)175     public TextClassifier createTextClassificationSession(
176             TextClassificationContext classificationContext, TextClassifier textClassifier) {
177         Preconditions.checkNotNull(classificationContext);
178         Preconditions.checkNotNull(textClassifier);
179         return new TextClassificationSession(classificationContext, textClassifier);
180     }
181 
182     /**
183      * Sets a TextClassificationSessionFactory to be used to create session-aware TextClassifiers.
184      *
185      * @param factory the textClassification session factory. If this is null, the default factory
186      *      will be used.
187      */
setTextClassificationSessionFactory( @ullable TextClassificationSessionFactory factory)188     public void setTextClassificationSessionFactory(
189             @Nullable TextClassificationSessionFactory factory) {
190         synchronized (mLock) {
191             if (factory != null) {
192                 mSessionFactory = factory;
193             } else {
194                 mSessionFactory = mDefaultSessionFactory;
195             }
196         }
197     }
198 
199     @Override
finalize()200     protected void finalize() throws Throwable {
201         try {
202             // Note that fields could be null if the constructor threw.
203             if (mSettingsObserver != null) {
204                 getApplicationContext().getContentResolver()
205                         .unregisterContentObserver(mSettingsObserver);
206                 if (ConfigParser.ENABLE_DEVICE_CONFIG) {
207                     DeviceConfig.removeOnPropertiesChangedListener(mSettingsObserver);
208                 }
209             }
210         } finally {
211             super.finalize();
212         }
213     }
214 
getSystemTextClassifier()215     private TextClassifier getSystemTextClassifier() {
216         synchronized (mLock) {
217             if (mSystemTextClassifier == null && isSystemTextClassifierEnabled()) {
218                 try {
219                     mSystemTextClassifier = new SystemTextClassifier(mContext, getSettings());
220                     Log.d(LOG_TAG, "Initialized SystemTextClassifier");
221                 } catch (ServiceManager.ServiceNotFoundException e) {
222                     Log.e(LOG_TAG, "Could not initialize SystemTextClassifier", e);
223                 }
224             }
225         }
226         if (mSystemTextClassifier != null) {
227             return mSystemTextClassifier;
228         }
229         return TextClassifier.NO_OP;
230     }
231 
232     /**
233      * Returns a local textclassifier, which is running in this process.
234      */
235     @NonNull
getLocalTextClassifier()236     private TextClassifier getLocalTextClassifier() {
237         synchronized (mLock) {
238             if (mLocalTextClassifier == null) {
239                 if (getSettings().isLocalTextClassifierEnabled()) {
240                     mLocalTextClassifier =
241                             new TextClassifierImpl(mContext, getSettings(), TextClassifier.NO_OP);
242                 } else {
243                     Log.d(LOG_TAG, "Local TextClassifier disabled");
244                     mLocalTextClassifier = TextClassifier.NO_OP;
245                 }
246             }
247             return mLocalTextClassifier;
248         }
249     }
250 
isSystemTextClassifierEnabled()251     private boolean isSystemTextClassifierEnabled() {
252         return getSettings().isSystemTextClassifierEnabled()
253                 && TextClassifierService.getServiceComponentName(mContext) != null;
254     }
255 
256     /** @hide */
257     @VisibleForTesting
invalidateForTesting()258     public void invalidateForTesting() {
259         invalidate();
260     }
261 
invalidate()262     private void invalidate() {
263         synchronized (mLock) {
264             mSettings = null;
265             mLocalTextClassifier = null;
266             mSystemTextClassifier = null;
267         }
268     }
269 
getApplicationContext()270     Context getApplicationContext() {
271         return mContext.getApplicationContext() != null
272                 ? mContext.getApplicationContext()
273                 : mContext;
274     }
275 
276     /** @hide **/
dump(IndentingPrintWriter pw)277     public void dump(IndentingPrintWriter pw) {
278         getLocalTextClassifier().dump(pw);
279         getSystemTextClassifier().dump(pw);
280         getSettings().dump(pw);
281     }
282 
283     /** @hide */
getSettings(Context context)284     public static TextClassificationConstants getSettings(Context context) {
285         Preconditions.checkNotNull(context);
286         final TextClassificationManager tcm =
287                 context.getSystemService(TextClassificationManager.class);
288         if (tcm != null) {
289             return tcm.getSettings();
290         } else {
291             // Use default settings if there is no tcm.
292             return sDefaultSettings;
293         }
294     }
295 
296     private static final class SettingsObserver extends ContentObserver
297             implements DeviceConfig.OnPropertiesChangedListener {
298 
299         private final WeakReference<TextClassificationManager> mTcm;
300 
SettingsObserver(TextClassificationManager tcm)301         SettingsObserver(TextClassificationManager tcm) {
302             super(null);
303             mTcm = new WeakReference<>(tcm);
304             tcm.getApplicationContext().getContentResolver().registerContentObserver(
305                     Settings.Global.getUriFor(Settings.Global.TEXT_CLASSIFIER_CONSTANTS),
306                     false /* notifyForDescendants */,
307                     this);
308             if (ConfigParser.ENABLE_DEVICE_CONFIG) {
309                 DeviceConfig.addOnPropertiesChangedListener(
310                         DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
311                         ActivityThread.currentApplication().getMainExecutor(),
312                         this);
313             }
314         }
315 
316         @Override
onChange(boolean selfChange)317         public void onChange(boolean selfChange) {
318             invalidateSettings();
319         }
320 
321         @Override
onPropertiesChanged(Properties properties)322         public void onPropertiesChanged(Properties properties) {
323             invalidateSettings();
324         }
325 
invalidateSettings()326         private void invalidateSettings() {
327             final TextClassificationManager tcm = mTcm.get();
328             if (tcm != null) {
329                 tcm.invalidate();
330             }
331         }
332     }
333 }
334