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
17package android.view.textservice;
18
19import android.annotation.SystemService;
20import android.content.Context;
21import android.os.Bundle;
22import android.os.RemoteException;
23import android.os.ServiceManager;
24import android.os.ServiceManager.ServiceNotFoundException;
25import android.util.Log;
26import android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener;
27
28import com.android.internal.textservice.ITextServicesManager;
29
30import java.util.Locale;
31
32/**
33 * System API to the overall text services, which arbitrates interaction between applications
34 * and text services.
35 *
36 * The user can change the current text services in Settings. And also applications can specify
37 * the target text services.
38 *
39 * <h3>Architecture Overview</h3>
40 *
41 * <p>There are three primary parties involved in the text services
42 * framework (TSF) architecture:</p>
43 *
44 * <ul>
45 * <li> The <strong>text services manager</strong> as expressed by this class
46 * is the central point of the system that manages interaction between all
47 * other parts.  It is expressed as the client-side API here which exists
48 * in each application context and communicates with a global system service
49 * that manages the interaction across all processes.
50 * <li> A <strong>text service</strong> implements a particular
51 * interaction model allowing the client application to retrieve information of text.
52 * The system binds to the current text service that is in use, causing it to be created and run.
53 * <li> Multiple <strong>client applications</strong> arbitrate with the text service
54 * manager for connections to text services.
55 * </ul>
56 *
57 * <h3>Text services sessions</h3>
58 * <ul>
59 * <li>The <strong>spell checker session</strong> is one of the text services.
60 * {@link android.view.textservice.SpellCheckerSession}</li>
61 * </ul>
62 *
63 */
64@SystemService(Context.TEXT_SERVICES_MANAGER_SERVICE)
65public final class TextServicesManager {
66    private static final String TAG = TextServicesManager.class.getSimpleName();
67    private static final boolean DBG = false;
68
69    /**
70     * A compile time switch to control per-profile spell checker, which is not yet ready.
71     * @hide
72     */
73    public static final boolean DISABLE_PER_PROFILE_SPELL_CHECKER = true;
74
75    private static TextServicesManager sInstance;
76
77    private final ITextServicesManager mService;
78
79    private TextServicesManager() throws ServiceNotFoundException {
80        mService = ITextServicesManager.Stub.asInterface(
81                ServiceManager.getServiceOrThrow(Context.TEXT_SERVICES_MANAGER_SERVICE));
82    }
83
84    /**
85     * Retrieve the global TextServicesManager instance, creating it if it doesn't already exist.
86     * @hide
87     */
88    public static TextServicesManager getInstance() {
89        synchronized (TextServicesManager.class) {
90            if (sInstance == null) {
91                try {
92                    sInstance = new TextServicesManager();
93                } catch (ServiceNotFoundException e) {
94                    throw new IllegalStateException(e);
95                }
96            }
97            return sInstance;
98        }
99    }
100
101    /**
102     * Returns the language component of a given locale string.
103     */
104    private static String parseLanguageFromLocaleString(String locale) {
105        final int idx = locale.indexOf('_');
106        if (idx < 0) {
107            return locale;
108        } else {
109            return locale.substring(0, idx);
110        }
111    }
112
113    /**
114     * Get a spell checker session for the specified spell checker
115     * @param locale the locale for the spell checker. If {@code locale} is null and
116     * referToSpellCheckerLanguageSettings is true, the locale specified in Settings will be
117     * returned. If {@code locale} is not null and referToSpellCheckerLanguageSettings is true,
118     * the locale specified in Settings will be returned only when it is same as {@code locale}.
119     * Exceptionally, when referToSpellCheckerLanguageSettings is true and {@code locale} is
120     * only language (e.g. "en"), the specified locale in Settings (e.g. "en_US") will be
121     * selected.
122     * @param listener a spell checker session lister for getting results from a spell checker.
123     * @param referToSpellCheckerLanguageSettings if true, the session for one of enabled
124     * languages in settings will be returned.
125     * @return the spell checker session of the spell checker
126     */
127    public SpellCheckerSession newSpellCheckerSession(Bundle bundle, Locale locale,
128            SpellCheckerSessionListener listener, boolean referToSpellCheckerLanguageSettings) {
129        if (listener == null) {
130            throw new NullPointerException();
131        }
132        if (!referToSpellCheckerLanguageSettings && locale == null) {
133            throw new IllegalArgumentException("Locale should not be null if you don't refer"
134                    + " settings.");
135        }
136
137        if (referToSpellCheckerLanguageSettings && !isSpellCheckerEnabled()) {
138            return null;
139        }
140
141        final SpellCheckerInfo sci;
142        try {
143            sci = mService.getCurrentSpellChecker(null);
144        } catch (RemoteException e) {
145            return null;
146        }
147        if (sci == null) {
148            return null;
149        }
150        SpellCheckerSubtype subtypeInUse = null;
151        if (referToSpellCheckerLanguageSettings) {
152            subtypeInUse = getCurrentSpellCheckerSubtype(true);
153            if (subtypeInUse == null) {
154                return null;
155            }
156            if (locale != null) {
157                final String subtypeLocale = subtypeInUse.getLocale();
158                final String subtypeLanguage = parseLanguageFromLocaleString(subtypeLocale);
159                if (subtypeLanguage.length() < 2 || !locale.getLanguage().equals(subtypeLanguage)) {
160                    return null;
161                }
162            }
163        } else {
164            final String localeStr = locale.toString();
165            for (int i = 0; i < sci.getSubtypeCount(); ++i) {
166                final SpellCheckerSubtype subtype = sci.getSubtypeAt(i);
167                final String tempSubtypeLocale = subtype.getLocale();
168                final String tempSubtypeLanguage = parseLanguageFromLocaleString(tempSubtypeLocale);
169                if (tempSubtypeLocale.equals(localeStr)) {
170                    subtypeInUse = subtype;
171                    break;
172                } else if (tempSubtypeLanguage.length() >= 2 &&
173                        locale.getLanguage().equals(tempSubtypeLanguage)) {
174                    subtypeInUse = subtype;
175                }
176            }
177        }
178        if (subtypeInUse == null) {
179            return null;
180        }
181        final SpellCheckerSession session = new SpellCheckerSession(sci, mService, listener);
182        try {
183            mService.getSpellCheckerService(sci.getId(), subtypeInUse.getLocale(),
184                    session.getTextServicesSessionListener(),
185                    session.getSpellCheckerSessionListener(), bundle);
186        } catch (RemoteException e) {
187            throw e.rethrowFromSystemServer();
188        }
189        return session;
190    }
191
192    /**
193     * @hide
194     */
195    public SpellCheckerInfo[] getEnabledSpellCheckers() {
196        try {
197            final SpellCheckerInfo[] retval = mService.getEnabledSpellCheckers();
198            if (DBG) {
199                Log.d(TAG, "getEnabledSpellCheckers: " + (retval != null ? retval.length : "null"));
200            }
201            return retval;
202        } catch (RemoteException e) {
203            throw e.rethrowFromSystemServer();
204        }
205    }
206
207    /**
208     * @hide
209     */
210    public SpellCheckerInfo getCurrentSpellChecker() {
211        try {
212            // Passing null as a locale for ICS
213            return mService.getCurrentSpellChecker(null);
214        } catch (RemoteException e) {
215            throw e.rethrowFromSystemServer();
216        }
217    }
218
219    /**
220     * @hide
221     */
222    public SpellCheckerSubtype getCurrentSpellCheckerSubtype(
223            boolean allowImplicitlySelectedSubtype) {
224        try {
225            // Passing null as a locale until we support multiple enabled spell checker subtypes.
226            return mService.getCurrentSpellCheckerSubtype(null, allowImplicitlySelectedSubtype);
227        } catch (RemoteException e) {
228            throw e.rethrowFromSystemServer();
229        }
230    }
231
232    /**
233     * @hide
234     */
235    public boolean isSpellCheckerEnabled() {
236        try {
237            return mService.isSpellCheckerEnabled();
238        } catch (RemoteException e) {
239            throw e.rethrowFromSystemServer();
240        }
241    }
242}
243