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