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