1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17 package android.view.textservice; 18 19 import android.annotation.CallbackExecutor; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.annotation.SystemService; 23 import android.annotation.UserHandleAware; 24 import android.annotation.UserIdInt; 25 import android.compat.annotation.UnsupportedAppUsage; 26 import android.content.Context; 27 import android.os.Build; 28 import android.os.Bundle; 29 import android.os.Handler; 30 import android.os.HandlerExecutor; 31 import android.os.RemoteException; 32 import android.os.ServiceManager; 33 import android.os.ServiceManager.ServiceNotFoundException; 34 import android.os.UserHandle; 35 import android.util.Log; 36 import android.view.inputmethod.InputMethodManager; 37 import android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener; 38 import android.view.textservice.SpellCheckerSession.SpellCheckerSessionParams; 39 40 import com.android.internal.textservice.ISpellCheckerSessionListener; 41 import com.android.internal.textservice.ITextServicesManager; 42 43 import java.util.Arrays; 44 import java.util.Collections; 45 import java.util.List; 46 import java.util.Locale; 47 import java.util.Objects; 48 import java.util.concurrent.Executor; 49 50 /** 51 * System API to the overall text services, which arbitrates interaction between applications 52 * and text services. 53 * 54 * The user can change the current text services in Settings. And also applications can specify 55 * the target text services. 56 * 57 * <h3>Architecture Overview</h3> 58 * 59 * <p>There are three primary parties involved in the text services 60 * framework (TSF) architecture:</p> 61 * 62 * <ul> 63 * <li> The <strong>text services manager</strong> as expressed by this class 64 * is the central point of the system that manages interaction between all 65 * other parts. It is expressed as the client-side API here which exists 66 * in each application context and communicates with a global system service 67 * that manages the interaction across all processes. 68 * <li> A <strong>text service</strong> implements a particular 69 * interaction model allowing the client application to retrieve information of text. 70 * The system binds to the current text service that is in use, causing it to be created and run. 71 * <li> Multiple <strong>client applications</strong> arbitrate with the text service 72 * manager for connections to text services. 73 * </ul> 74 * 75 * <h3>Text services sessions</h3> 76 * <ul> 77 * <li>The <strong>spell checker session</strong> is one of the text services. 78 * {@link android.view.textservice.SpellCheckerSession}</li> 79 * </ul> 80 * 81 */ 82 @SystemService(Context.TEXT_SERVICES_MANAGER_SERVICE) 83 public final class TextServicesManager { 84 private static final String TAG = TextServicesManager.class.getSimpleName(); 85 private static final boolean DBG = false; 86 87 /** 88 * @deprecated Do not use. Just kept because of {@link UnsupportedAppUsage} in 89 * {@link #getInstance()}. 90 */ 91 @Deprecated 92 private static TextServicesManager sInstance; 93 94 private final ITextServicesManager mService; 95 96 @UserIdInt 97 private final int mUserId; 98 99 @Nullable 100 private final InputMethodManager mInputMethodManager; 101 TextServicesManager(@serIdInt int userId, @Nullable InputMethodManager inputMethodManager)102 private TextServicesManager(@UserIdInt int userId, 103 @Nullable InputMethodManager inputMethodManager) throws ServiceNotFoundException { 104 mService = ITextServicesManager.Stub.asInterface( 105 ServiceManager.getServiceOrThrow(Context.TEXT_SERVICES_MANAGER_SERVICE)); 106 mUserId = userId; 107 mInputMethodManager = inputMethodManager; 108 } 109 110 /** 111 * The factory method of {@link TextServicesManager}. 112 * 113 * @param context {@link Context} from which {@link TextServicesManager} should be instantiated. 114 * @return {@link TextServicesManager} that is associated with {@link Context#getUserId()}. 115 * @throws ServiceNotFoundException When {@link TextServicesManager} is not available. 116 * @hide 117 */ 118 @UserHandleAware 119 @NonNull createInstance(@onNull Context context)120 public static TextServicesManager createInstance(@NonNull Context context) 121 throws ServiceNotFoundException { 122 return new TextServicesManager(context.getUserId(), context.getSystemService( 123 InputMethodManager.class)); 124 } 125 126 /** 127 * @deprecated Do not use. Just kept because of {@link UnsupportedAppUsage} in 128 * {@link #getInstance()}. 129 * @hide 130 */ 131 @UnsupportedAppUsage getInstance()132 public static TextServicesManager getInstance() { 133 synchronized (TextServicesManager.class) { 134 if (sInstance == null) { 135 try { 136 sInstance = new TextServicesManager(UserHandle.myUserId(), null); 137 } catch (ServiceNotFoundException e) { 138 throw new IllegalStateException(e); 139 } 140 } 141 return sInstance; 142 } 143 } 144 145 /** @hide */ 146 @Nullable getInputMethodManager()147 public InputMethodManager getInputMethodManager() { 148 return mInputMethodManager; 149 } 150 151 /** 152 * Returns the language component of a given locale string. 153 */ parseLanguageFromLocaleString(String locale)154 private static String parseLanguageFromLocaleString(String locale) { 155 final int idx = locale.indexOf('_'); 156 if (idx < 0) { 157 return locale; 158 } else { 159 return locale.substring(0, idx); 160 } 161 } 162 163 /** 164 * Get a spell checker session from the spell checker. 165 * 166 * <p>{@link SuggestionsInfo#RESULT_ATTR_IN_THE_DICTIONARY}, 167 * {@link SuggestionsInfo#RESULT_ATTR_LOOKS_LIKE_TYPO}, and 168 * {@link SuggestionsInfo#RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS} will be passed to the spell 169 * checker as supported attributes. 170 * 171 * @param locale the locale for the spell checker. If {@code locale} is null and 172 * referToSpellCheckerLanguageSettings is true, the locale specified in Settings will be 173 * returned. If {@code locale} is not null and referToSpellCheckerLanguageSettings is true, 174 * the locale specified in Settings will be returned only when it is same as {@code locale}. 175 * Exceptionally, when referToSpellCheckerLanguageSettings is true and {@code locale} is 176 * only language (e.g. "en"), the specified locale in Settings (e.g. "en_US") will be 177 * selected. 178 * @param listener a spell checker session lister for getting results from the spell checker. 179 * @param referToSpellCheckerLanguageSettings if true, the session for one of enabled 180 * languages in settings will be returned. 181 * @return a spell checker session of the spell checker 182 */ 183 @UserHandleAware 184 @Nullable newSpellCheckerSession(@ullable Bundle bundle, @Nullable Locale locale, @NonNull SpellCheckerSessionListener listener, boolean referToSpellCheckerLanguageSettings)185 public SpellCheckerSession newSpellCheckerSession(@Nullable Bundle bundle, 186 @Nullable Locale locale, 187 @NonNull SpellCheckerSessionListener listener, 188 boolean referToSpellCheckerLanguageSettings) { 189 // Attributes existed before {@link #newSpellCheckerSession(Locale, boolean, int, Bundle, 190 // Executor, SpellCheckerSessionListener)} was introduced. 191 int supportedAttributes = SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY 192 | SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO 193 | SuggestionsInfo.RESULT_ATTR_HAS_RECOMMENDED_SUGGESTIONS; 194 SpellCheckerSessionParams.Builder paramsBuilder = new SpellCheckerSessionParams.Builder() 195 .setLocale(locale) 196 .setShouldReferToSpellCheckerLanguageSettings(referToSpellCheckerLanguageSettings) 197 .setSupportedAttributes(supportedAttributes); 198 if (bundle != null) { 199 paramsBuilder.setExtras(bundle); 200 } 201 // Using the implicit looper to preserve the old behavior. 202 Executor executor = new HandlerExecutor(new Handler()); 203 return newSpellCheckerSession(paramsBuilder.build(), executor, listener); 204 } 205 206 /** 207 * Get a spell checker session from the spell checker. 208 * 209 * @param params The parameters passed to the spell checker. 210 * @param executor The executor on which {@code listener} will be called back. 211 * @param listener a spell checker session lister for getting results from the spell checker. 212 * @return The spell checker session of the spell checker. 213 */ 214 @UserHandleAware 215 @Nullable newSpellCheckerSession( @onNull SpellCheckerSessionParams params, @NonNull @CallbackExecutor Executor executor, @NonNull SpellCheckerSessionListener listener)216 public SpellCheckerSession newSpellCheckerSession( 217 @NonNull SpellCheckerSessionParams params, 218 @NonNull @CallbackExecutor Executor executor, 219 @NonNull SpellCheckerSessionListener listener) { 220 Objects.requireNonNull(executor); 221 Objects.requireNonNull(listener); 222 Locale locale = params.getLocale(); 223 if (!params.shouldReferToSpellCheckerLanguageSettings() && locale == null) { 224 throw new IllegalArgumentException("Locale should not be null if you don't refer" 225 + " settings."); 226 } 227 228 if (params.shouldReferToSpellCheckerLanguageSettings() && !isSpellCheckerEnabled()) { 229 return null; 230 } 231 232 final SpellCheckerInfo sci; 233 try { 234 sci = mService.getCurrentSpellChecker(mUserId, null); 235 } catch (RemoteException e) { 236 return null; 237 } 238 if (sci == null) { 239 return null; 240 } 241 SpellCheckerSubtype subtypeInUse = null; 242 if (params.shouldReferToSpellCheckerLanguageSettings()) { 243 subtypeInUse = getCurrentSpellCheckerSubtype(true); 244 if (subtypeInUse == null) { 245 return null; 246 } 247 if (locale != null) { 248 final String subtypeLocale = subtypeInUse.getLocale(); 249 final String subtypeLanguage = parseLanguageFromLocaleString(subtypeLocale); 250 if (subtypeLanguage.length() < 2 || !locale.getLanguage().equals(subtypeLanguage)) { 251 return null; 252 } 253 } 254 } else { 255 final String localeStr = locale.toString(); 256 for (int i = 0; i < sci.getSubtypeCount(); ++i) { 257 final SpellCheckerSubtype subtype = sci.getSubtypeAt(i); 258 final String tempSubtypeLocale = subtype.getLocale(); 259 final String tempSubtypeLanguage = parseLanguageFromLocaleString(tempSubtypeLocale); 260 if (tempSubtypeLocale.equals(localeStr)) { 261 subtypeInUse = subtype; 262 break; 263 } else if (tempSubtypeLanguage.length() >= 2 && 264 locale.getLanguage().equals(tempSubtypeLanguage)) { 265 subtypeInUse = subtype; 266 } 267 } 268 } 269 if (subtypeInUse == null) { 270 return null; 271 } 272 final SpellCheckerSession session = new SpellCheckerSession(sci, this, listener, executor); 273 try { 274 mService.getSpellCheckerService(mUserId, sci.getId(), subtypeInUse.getLocale(), 275 session.getTextServicesSessionListener(), 276 session.getSpellCheckerSessionListener(), 277 params.getExtras(), params.getSupportedAttributes()); 278 } catch (RemoteException e) { 279 throw e.rethrowFromSystemServer(); 280 } 281 return session; 282 } 283 284 /** 285 * Deprecated. Use {@link #getEnabledSpellCheckerInfos()} instead. 286 * @hide 287 */ 288 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553, 289 publicAlternatives = "Use {@link #getEnabledSpellCheckerInfos()} instead.") 290 @UserHandleAware getEnabledSpellCheckers()291 public SpellCheckerInfo[] getEnabledSpellCheckers() { 292 try { 293 final SpellCheckerInfo[] retval = mService.getEnabledSpellCheckers(mUserId); 294 if (DBG) { 295 Log.d(TAG, "getEnabledSpellCheckers: " + (retval != null ? retval.length : "null")); 296 } 297 return retval; 298 } catch (RemoteException e) { 299 throw e.rethrowFromSystemServer(); 300 } 301 } 302 303 /** 304 * Retrieve the list of currently enabled spell checkers. 305 * 306 * <p> Note: The results are filtered by the rules of 307 * <a href="/training/basics/intents/package-visibility">package visibility</a>, except for 308 * the currently active spell checker. 309 * 310 * @return The list of currently enabled spell checkers. 311 */ 312 @UserHandleAware 313 @NonNull getEnabledSpellCheckerInfos()314 public List<SpellCheckerInfo> getEnabledSpellCheckerInfos() { 315 final SpellCheckerInfo[] enabledSpellCheckers = getEnabledSpellCheckers(); 316 return enabledSpellCheckers != null 317 ? Arrays.asList(enabledSpellCheckers) : Collections.emptyList(); 318 } 319 320 /** 321 * Retrieve the currently active spell checker, or null if there is none. 322 * 323 * @return The current active spell checker info. 324 */ 325 @UserHandleAware 326 @Nullable getCurrentSpellCheckerInfo()327 public SpellCheckerInfo getCurrentSpellCheckerInfo() { 328 try { 329 // Passing null as a locale for ICS 330 return mService.getCurrentSpellChecker(mUserId, null); 331 } catch (RemoteException e) { 332 throw e.rethrowFromSystemServer(); 333 } 334 } 335 336 /** 337 * Deprecated. Use {@link #getCurrentSpellCheckerInfo()} instead. 338 * @hide 339 */ 340 @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, 341 publicAlternatives = "Use {@link #getCurrentSpellCheckerInfo()} instead.") 342 @UserHandleAware 343 @Nullable getCurrentSpellChecker()344 public SpellCheckerInfo getCurrentSpellChecker() { 345 return getCurrentSpellCheckerInfo(); 346 } 347 348 /** 349 * Retrieve the selected subtype of the selected spell checker, or null if there is none. 350 * 351 * @param allowImplicitlySelectedSubtype {@code true} to return the default language matching 352 * system locale if there's no subtype selected explicitly, otherwise, returns null. 353 * @return The meta information of the selected subtype of the selected spell checker. 354 * 355 * @hide 356 */ 357 @UnsupportedAppUsage 358 @UserHandleAware 359 @Nullable getCurrentSpellCheckerSubtype( boolean allowImplicitlySelectedSubtype)360 public SpellCheckerSubtype getCurrentSpellCheckerSubtype( 361 boolean allowImplicitlySelectedSubtype) { 362 try { 363 return mService.getCurrentSpellCheckerSubtype(mUserId, allowImplicitlySelectedSubtype); 364 } catch (RemoteException e) { 365 throw e.rethrowFromSystemServer(); 366 } 367 } 368 369 /** 370 * Return whether the spell checker is enabled or not. 371 * 372 * @return {@code true} if spell checker is enabled, {@code false} otherwise. 373 */ 374 @UserHandleAware isSpellCheckerEnabled()375 public boolean isSpellCheckerEnabled() { 376 try { 377 return mService.isSpellCheckerEnabled(mUserId); 378 } catch (RemoteException e) { 379 throw e.rethrowFromSystemServer(); 380 } 381 } 382 383 @UserHandleAware finishSpellCheckerService(ISpellCheckerSessionListener listener)384 void finishSpellCheckerService(ISpellCheckerSessionListener listener) { 385 try { 386 mService.finishSpellCheckerService(mUserId, listener); 387 } catch (RemoteException e) { 388 throw e.rethrowFromSystemServer(); 389 } 390 } 391 } 392