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 com.android.internal.textservice.ITextServicesManager;
20
21import android.content.Context;
22import android.os.Bundle;
23import android.os.IBinder;
24import android.os.RemoteException;
25import android.os.ServiceManager;
26import android.util.Log;
27import android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener;
28
29import java.util.Locale;
30
31/**
32 * System API to the overall text services, which arbitrates interaction between applications
33 * and text services. You can retrieve an instance of this interface with
34 * {@link Context#getSystemService(String) Context.getSystemService()}.
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 */
64public final class TextServicesManager {
65    private static final String TAG = TextServicesManager.class.getSimpleName();
66    private static final boolean DBG = false;
67
68    private static TextServicesManager sInstance;
69    private static ITextServicesManager sService;
70
71    private TextServicesManager() {
72        if (sService == null) {
73            IBinder b = ServiceManager.getService(Context.TEXT_SERVICES_MANAGER_SERVICE);
74            sService = ITextServicesManager.Stub.asInterface(b);
75        }
76    }
77
78    /**
79     * Retrieve the global TextServicesManager instance, creating it if it doesn't already exist.
80     * @hide
81     */
82    public static TextServicesManager getInstance() {
83        synchronized (TextServicesManager.class) {
84            if (sInstance != null) {
85                return sInstance;
86            }
87            sInstance = new TextServicesManager();
88        }
89        return sInstance;
90    }
91
92    /**
93     * Get a spell checker session for the specified spell checker
94     * @param locale the locale for the spell checker. If {@code locale} is null and
95     * referToSpellCheckerLanguageSettings is true, the locale specified in Settings will be
96     * returned. If {@code locale} is not null and referToSpellCheckerLanguageSettings is true,
97     * the locale specified in Settings will be returned only when it is same as {@code locale}.
98     * Exceptionally, when referToSpellCheckerLanguageSettings is true and {@code locale} is
99     * only language (e.g. "en"), the specified locale in Settings (e.g. "en_US") will be
100     * selected.
101     * @param listener a spell checker session lister for getting results from a spell checker.
102     * @param referToSpellCheckerLanguageSettings if true, the session for one of enabled
103     * languages in settings will be returned.
104     * @return the spell checker session of the spell checker
105     */
106    public SpellCheckerSession newSpellCheckerSession(Bundle bundle, Locale locale,
107            SpellCheckerSessionListener listener, boolean referToSpellCheckerLanguageSettings) {
108        if (listener == null) {
109            throw new NullPointerException();
110        }
111        if (!referToSpellCheckerLanguageSettings && locale == null) {
112            throw new IllegalArgumentException("Locale should not be null if you don't refer"
113                    + " settings.");
114        }
115
116        if (referToSpellCheckerLanguageSettings && !isSpellCheckerEnabled()) {
117            return null;
118        }
119
120        final SpellCheckerInfo sci;
121        try {
122            sci = sService.getCurrentSpellChecker(null);
123        } catch (RemoteException e) {
124            return null;
125        }
126        if (sci == null) {
127            return null;
128        }
129        SpellCheckerSubtype subtypeInUse = null;
130        if (referToSpellCheckerLanguageSettings) {
131            subtypeInUse = getCurrentSpellCheckerSubtype(true);
132            if (subtypeInUse == null) {
133                return null;
134            }
135            if (locale != null) {
136                final String subtypeLocale = subtypeInUse.getLocale();
137                final String inputLocale = locale.toString();
138                if (subtypeLocale.length() < 2 || inputLocale.length() < 2
139                        || !subtypeLocale.substring(0, 2).equals(inputLocale.substring(0, 2))) {
140                    return null;
141                }
142            }
143        } else {
144            final String localeStr = locale.toString();
145            for (int i = 0; i < sci.getSubtypeCount(); ++i) {
146                final SpellCheckerSubtype subtype = sci.getSubtypeAt(i);
147                final String tempSubtypeLocale = subtype.getLocale();
148                if (tempSubtypeLocale.equals(localeStr)) {
149                    subtypeInUse = subtype;
150                    break;
151                } else if (localeStr.length() >= 2 && tempSubtypeLocale.length() >= 2
152                        && localeStr.startsWith(tempSubtypeLocale)) {
153                    subtypeInUse = subtype;
154                }
155            }
156        }
157        if (subtypeInUse == null) {
158            return null;
159        }
160        final SpellCheckerSession session = new SpellCheckerSession(
161                sci, sService, listener, subtypeInUse);
162        try {
163            sService.getSpellCheckerService(sci.getId(), subtypeInUse.getLocale(),
164                    session.getTextServicesSessionListener(),
165                    session.getSpellCheckerSessionListener(), bundle);
166        } catch (RemoteException e) {
167            return null;
168        }
169        return session;
170    }
171
172    /**
173     * @hide
174     */
175    public SpellCheckerInfo[] getEnabledSpellCheckers() {
176        try {
177            final SpellCheckerInfo[] retval = sService.getEnabledSpellCheckers();
178            if (DBG) {
179                Log.d(TAG, "getEnabledSpellCheckers: " + (retval != null ? retval.length : "null"));
180            }
181            return retval;
182        } catch (RemoteException e) {
183            Log.e(TAG, "Error in getEnabledSpellCheckers: " + e);
184            return null;
185        }
186    }
187
188    /**
189     * @hide
190     */
191    public SpellCheckerInfo getCurrentSpellChecker() {
192        try {
193            // Passing null as a locale for ICS
194            return sService.getCurrentSpellChecker(null);
195        } catch (RemoteException e) {
196            return null;
197        }
198    }
199
200    /**
201     * @hide
202     */
203    public void setCurrentSpellChecker(SpellCheckerInfo sci) {
204        try {
205            if (sci == null) {
206                throw new NullPointerException("SpellCheckerInfo is null.");
207            }
208            sService.setCurrentSpellChecker(null, sci.getId());
209        } catch (RemoteException e) {
210            Log.e(TAG, "Error in setCurrentSpellChecker: " + e);
211        }
212    }
213
214    /**
215     * @hide
216     */
217    public SpellCheckerSubtype getCurrentSpellCheckerSubtype(
218            boolean allowImplicitlySelectedSubtype) {
219        try {
220            if (sService == null) {
221                // TODO: This is a workaround. Needs to investigate why sService could be null
222                // here.
223                Log.e(TAG, "sService is null.");
224                return null;
225            }
226            // Passing null as a locale until we support multiple enabled spell checker subtypes.
227            return sService.getCurrentSpellCheckerSubtype(null, allowImplicitlySelectedSubtype);
228        } catch (RemoteException e) {
229            Log.e(TAG, "Error in getCurrentSpellCheckerSubtype: " + e);
230            return null;
231        }
232    }
233
234    /**
235     * @hide
236     */
237    public void setSpellCheckerSubtype(SpellCheckerSubtype subtype) {
238        try {
239            final int hashCode;
240            if (subtype == null) {
241                hashCode = 0;
242            } else {
243                hashCode = subtype.hashCode();
244            }
245            sService.setCurrentSpellCheckerSubtype(null, hashCode);
246        } catch (RemoteException e) {
247            Log.e(TAG, "Error in setSpellCheckerSubtype:" + e);
248        }
249    }
250
251    /**
252     * @hide
253     */
254    public void setSpellCheckerEnabled(boolean enabled) {
255        try {
256            sService.setSpellCheckerEnabled(enabled);
257        } catch (RemoteException e) {
258            Log.e(TAG, "Error in setSpellCheckerEnabled:" + e);
259        }
260    }
261
262    /**
263     * @hide
264     */
265    public boolean isSpellCheckerEnabled() {
266        try {
267            return sService.isSpellCheckerEnabled();
268        } catch (RemoteException e) {
269            Log.e(TAG, "Error in isSpellCheckerEnabled:" + e);
270            return false;
271        }
272    }
273}
274