RichInputMethodManager.java revision ad5795a89117dbb5ebe4f1f308bc7e8a685ebf46
1/*
2 * Copyright (C) 2012 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
17package com.android.inputmethod.latin;
18
19import static com.android.inputmethod.latin.Constants.Subtype.KEYBOARD_MODE;
20
21import android.content.Context;
22import android.content.SharedPreferences;
23import android.os.IBinder;
24import android.preference.PreferenceManager;
25import android.util.Log;
26import android.view.inputmethod.InputMethodInfo;
27import android.view.inputmethod.InputMethodManager;
28import android.view.inputmethod.InputMethodSubtype;
29
30import com.android.inputmethod.compat.InputMethodManagerCompatWrapper;
31import com.android.inputmethod.latin.settings.Settings;
32import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils;
33import com.android.inputmethod.latin.utils.CollectionUtils;
34import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
35
36import java.util.Collections;
37import java.util.HashMap;
38import java.util.List;
39
40/**
41 * Enrichment class for InputMethodManager to simplify interaction and add functionality.
42 */
43public final class RichInputMethodManager {
44    private static final String TAG = RichInputMethodManager.class.getSimpleName();
45
46    private RichInputMethodManager() {
47        // This utility class is not publicly instantiable.
48    }
49
50    private static final RichInputMethodManager sInstance = new RichInputMethodManager();
51
52    private InputMethodManagerCompatWrapper mImmWrapper;
53    private InputMethodInfoCache mInputMethodInfoCache;
54    final HashMap<InputMethodInfo, List<InputMethodSubtype>>
55            mSubtypeListCacheWithImplicitlySelectedSubtypes = CollectionUtils.newHashMap();
56    final HashMap<InputMethodInfo, List<InputMethodSubtype>>
57            mSubtypeListCacheWithoutImplicitlySelectedSubtypes = CollectionUtils.newHashMap();
58
59    private static final int INDEX_NOT_FOUND = -1;
60
61    public static RichInputMethodManager getInstance() {
62        sInstance.checkInitialized();
63        return sInstance;
64    }
65
66    public static void init(final Context context) {
67        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
68        sInstance.initInternal(context, prefs);
69    }
70
71    private boolean isInitialized() {
72        return mImmWrapper != null;
73    }
74
75    private void checkInitialized() {
76        if (!isInitialized()) {
77            throw new RuntimeException(TAG + " is used before initialization");
78        }
79    }
80
81    private void initInternal(final Context context, final SharedPreferences prefs) {
82        if (isInitialized()) {
83            return;
84        }
85        mImmWrapper = new InputMethodManagerCompatWrapper(context);
86        mInputMethodInfoCache = new InputMethodInfoCache(
87                mImmWrapper.mImm, context.getPackageName());
88
89        // Initialize additional subtypes.
90        SubtypeLocaleUtils.init(context);
91        final String prefAdditionalSubtypes = Settings.readPrefAdditionalSubtypes(
92                prefs, context.getResources());
93        final InputMethodSubtype[] additionalSubtypes =
94                AdditionalSubtypeUtils.createAdditionalSubtypesArray(prefAdditionalSubtypes);
95        setAdditionalInputMethodSubtypes(additionalSubtypes);
96    }
97
98    public InputMethodManager getInputMethodManager() {
99        checkInitialized();
100        return mImmWrapper.mImm;
101    }
102
103    public List<InputMethodSubtype> getMyEnabledInputMethodSubtypeList(
104            boolean allowsImplicitlySelectedSubtypes) {
105        return getEnabledInputMethodSubtypeList(
106                getInputMethodInfoOfThisIme(), allowsImplicitlySelectedSubtypes);
107    }
108
109    public boolean switchToNextInputMethod(final IBinder token, final boolean onlyCurrentIme) {
110        if (mImmWrapper.switchToNextInputMethod(token, onlyCurrentIme)) {
111            return true;
112        }
113        // Was not able to call {@link InputMethodManager#switchToNextInputMethodIBinder,boolean)}
114        // because the current device is running ICS or previous and lacks the API.
115        if (switchToNextInputSubtypeInThisIme(token, onlyCurrentIme)) {
116            return true;
117        }
118        return switchToNextInputMethodAndSubtype(token);
119    }
120
121    private boolean switchToNextInputSubtypeInThisIme(final IBinder token,
122            final boolean onlyCurrentIme) {
123        final InputMethodManager imm = mImmWrapper.mImm;
124        final InputMethodSubtype currentSubtype = imm.getCurrentInputMethodSubtype();
125        final List<InputMethodSubtype> enabledSubtypes = getMyEnabledInputMethodSubtypeList(
126                true /* allowsImplicitlySelectedSubtypes */);
127        final int currentIndex = getSubtypeIndexInList(currentSubtype, enabledSubtypes);
128        if (currentIndex == INDEX_NOT_FOUND) {
129            Log.w(TAG, "Can't find current subtype in enabled subtypes: subtype="
130                    + SubtypeLocaleUtils.getSubtypeNameForLogging(currentSubtype));
131            return false;
132        }
133        final int nextIndex = (currentIndex + 1) % enabledSubtypes.size();
134        if (nextIndex <= currentIndex && !onlyCurrentIme) {
135            // The current subtype is the last or only enabled one and it needs to switch to
136            // next IME.
137            return false;
138        }
139        final InputMethodSubtype nextSubtype = enabledSubtypes.get(nextIndex);
140        setInputMethodAndSubtype(token, nextSubtype);
141        return true;
142    }
143
144    private boolean switchToNextInputMethodAndSubtype(final IBinder token) {
145        final InputMethodManager imm = mImmWrapper.mImm;
146        final List<InputMethodInfo> enabledImis = imm.getEnabledInputMethodList();
147        final int currentIndex = getImiIndexInList(getInputMethodInfoOfThisIme(), enabledImis);
148        if (currentIndex == INDEX_NOT_FOUND) {
149            Log.w(TAG, "Can't find current IME in enabled IMEs: IME package="
150                    + getInputMethodInfoOfThisIme().getPackageName());
151            return false;
152        }
153        final InputMethodInfo nextImi = getNextNonAuxiliaryIme(currentIndex, enabledImis);
154        final List<InputMethodSubtype> enabledSubtypes = getEnabledInputMethodSubtypeList(nextImi,
155                true /* allowsImplicitlySelectedSubtypes */);
156        if (enabledSubtypes.isEmpty()) {
157            // The next IME has no subtype.
158            imm.setInputMethod(token, nextImi.getId());
159            return true;
160        }
161        final InputMethodSubtype firstSubtype = enabledSubtypes.get(0);
162        imm.setInputMethodAndSubtype(token, nextImi.getId(), firstSubtype);
163        return true;
164    }
165
166    private static int getImiIndexInList(final InputMethodInfo inputMethodInfo,
167            final List<InputMethodInfo> imiList) {
168        final int count = imiList.size();
169        for (int index = 0; index < count; index++) {
170            final InputMethodInfo imi = imiList.get(index);
171            if (imi.equals(inputMethodInfo)) {
172                return index;
173            }
174        }
175        return INDEX_NOT_FOUND;
176    }
177
178    // This method mimics {@link InputMethodManager#switchToNextInputMethod(IBinder,boolean)}.
179    private static InputMethodInfo getNextNonAuxiliaryIme(final int currentIndex,
180            final List<InputMethodInfo> imiList) {
181        final int count = imiList.size();
182        for (int i = 1; i < count; i++) {
183            final int nextIndex = (currentIndex + i) % count;
184            final InputMethodInfo nextImi = imiList.get(nextIndex);
185            if (!isAuxiliaryIme(nextImi)) {
186                return nextImi;
187            }
188        }
189        return imiList.get(currentIndex);
190    }
191
192    // Copied from {@link InputMethodInfo}. See how auxiliary of IME is determined.
193    private static boolean isAuxiliaryIme(final InputMethodInfo imi) {
194        final int count = imi.getSubtypeCount();
195        if (count == 0) {
196            return false;
197        }
198        for (int index = 0; index < count; index++) {
199            final InputMethodSubtype subtype = imi.getSubtypeAt(index);
200            if (!subtype.isAuxiliary()) {
201                return false;
202            }
203        }
204        return true;
205    }
206
207    private static class InputMethodInfoCache {
208        private final InputMethodManager mImm;
209        private final String mImePackageName;
210
211        private InputMethodInfo mCachedValue;
212
213        public InputMethodInfoCache(final InputMethodManager imm, final String imePackageName) {
214            mImm = imm;
215            mImePackageName = imePackageName;
216        }
217
218        public synchronized InputMethodInfo get() {
219            if (mCachedValue != null) {
220                return mCachedValue;
221            }
222            for (final InputMethodInfo imi : mImm.getInputMethodList()) {
223                if (imi.getPackageName().equals(mImePackageName)) {
224                    mCachedValue = imi;
225                    return imi;
226                }
227            }
228            throw new RuntimeException("Input method id for " + mImePackageName + " not found.");
229        }
230
231        public synchronized void clear() {
232            mCachedValue = null;
233        }
234    }
235
236    public InputMethodInfo getInputMethodInfoOfThisIme() {
237        return mInputMethodInfoCache.get();
238    }
239
240    public String getInputMethodIdOfThisIme() {
241        return getInputMethodInfoOfThisIme().getId();
242    }
243
244    public boolean checkIfSubtypeBelongsToThisImeAndEnabled(final InputMethodSubtype subtype) {
245        return checkIfSubtypeBelongsToImeAndEnabled(getInputMethodInfoOfThisIme(), subtype);
246    }
247
248    public boolean checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled(
249            final InputMethodSubtype subtype) {
250        final boolean subtypeEnabled = checkIfSubtypeBelongsToThisImeAndEnabled(subtype);
251        final boolean subtypeExplicitlyEnabled = checkIfSubtypeBelongsToList(
252                subtype, getMyEnabledInputMethodSubtypeList(
253                        false /* allowsImplicitlySelectedSubtypes */));
254        return subtypeEnabled && !subtypeExplicitlyEnabled;
255    }
256
257    public boolean checkIfSubtypeBelongsToImeAndEnabled(final InputMethodInfo imi,
258            final InputMethodSubtype subtype) {
259        return checkIfSubtypeBelongsToList(subtype, getEnabledInputMethodSubtypeList(imi,
260                true /* allowsImplicitlySelectedSubtypes */));
261    }
262
263    private static boolean checkIfSubtypeBelongsToList(final InputMethodSubtype subtype,
264            final List<InputMethodSubtype> subtypes) {
265        return getSubtypeIndexInList(subtype, subtypes) != INDEX_NOT_FOUND;
266    }
267
268    private static int getSubtypeIndexInList(final InputMethodSubtype subtype,
269            final List<InputMethodSubtype> subtypes) {
270        final int count = subtypes.size();
271        for (int index = 0; index < count; index++) {
272            final InputMethodSubtype ims = subtypes.get(index);
273            if (ims.equals(subtype)) {
274                return index;
275            }
276        }
277        return INDEX_NOT_FOUND;
278    }
279
280    public boolean checkIfSubtypeBelongsToThisIme(final InputMethodSubtype subtype) {
281        return getSubtypeIndexInIme(subtype, getInputMethodInfoOfThisIme()) != INDEX_NOT_FOUND;
282    }
283
284    private static int getSubtypeIndexInIme(final InputMethodSubtype subtype,
285            final InputMethodInfo imi) {
286        final int count = imi.getSubtypeCount();
287        for (int index = 0; index < count; index++) {
288            final InputMethodSubtype ims = imi.getSubtypeAt(index);
289            if (ims.equals(subtype)) {
290                return index;
291            }
292        }
293        return INDEX_NOT_FOUND;
294    }
295
296    public InputMethodSubtype getCurrentInputMethodSubtype(
297            final InputMethodSubtype defaultSubtype) {
298        final InputMethodSubtype currentSubtype = mImmWrapper.mImm.getCurrentInputMethodSubtype();
299        return (currentSubtype != null) ? currentSubtype : defaultSubtype;
300    }
301
302    public boolean hasMultipleEnabledIMEsOrSubtypes(final boolean shouldIncludeAuxiliarySubtypes) {
303        final List<InputMethodInfo> enabledImis = mImmWrapper.mImm.getEnabledInputMethodList();
304        return hasMultipleEnabledSubtypes(shouldIncludeAuxiliarySubtypes, enabledImis);
305    }
306
307    public boolean hasMultipleEnabledSubtypesInThisIme(
308            final boolean shouldIncludeAuxiliarySubtypes) {
309        final List<InputMethodInfo> imiList = Collections.singletonList(
310                getInputMethodInfoOfThisIme());
311        return hasMultipleEnabledSubtypes(shouldIncludeAuxiliarySubtypes, imiList);
312    }
313
314    private boolean hasMultipleEnabledSubtypes(final boolean shouldIncludeAuxiliarySubtypes,
315            final List<InputMethodInfo> imiList) {
316        // Number of the filtered IMEs
317        int filteredImisCount = 0;
318
319        for (InputMethodInfo imi : imiList) {
320            // We can return true immediately after we find two or more filtered IMEs.
321            if (filteredImisCount > 1) return true;
322            final List<InputMethodSubtype> subtypes = getEnabledInputMethodSubtypeList(imi, true);
323            // IMEs that have no subtypes should be counted.
324            if (subtypes.isEmpty()) {
325                ++filteredImisCount;
326                continue;
327            }
328
329            int auxCount = 0;
330            for (InputMethodSubtype subtype : subtypes) {
331                if (subtype.isAuxiliary()) {
332                    ++auxCount;
333                }
334            }
335            final int nonAuxCount = subtypes.size() - auxCount;
336
337            // IMEs that have one or more non-auxiliary subtypes should be counted.
338            // If shouldIncludeAuxiliarySubtypes is true, IMEs that have two or more auxiliary
339            // subtypes should be counted as well.
340            if (nonAuxCount > 0 || (shouldIncludeAuxiliarySubtypes && auxCount > 1)) {
341                ++filteredImisCount;
342                continue;
343            }
344        }
345
346        if (filteredImisCount > 1) {
347            return true;
348        }
349        final List<InputMethodSubtype> subtypes = getMyEnabledInputMethodSubtypeList(true);
350        int keyboardCount = 0;
351        // imm.getEnabledInputMethodSubtypeList(null, true) will return the current IME's
352        // both explicitly and implicitly enabled input method subtype.
353        // (The current IME should be LatinIME.)
354        for (InputMethodSubtype subtype : subtypes) {
355            if (KEYBOARD_MODE.equals(subtype.getMode())) {
356                ++keyboardCount;
357            }
358        }
359        return keyboardCount > 1;
360    }
361
362    public InputMethodSubtype findSubtypeByLocaleAndKeyboardLayoutSet(final String localeString,
363            final String keyboardLayoutSetName) {
364        final InputMethodInfo myImi = getInputMethodInfoOfThisIme();
365        final int count = myImi.getSubtypeCount();
366        for (int i = 0; i < count; i++) {
367            final InputMethodSubtype subtype = myImi.getSubtypeAt(i);
368            final String layoutName = SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype);
369            if (localeString.equals(subtype.getLocale())
370                    && keyboardLayoutSetName.equals(layoutName)) {
371                return subtype;
372            }
373        }
374        return null;
375    }
376
377    public void setInputMethodAndSubtype(final IBinder token, final InputMethodSubtype subtype) {
378        mImmWrapper.mImm.setInputMethodAndSubtype(
379                token, getInputMethodIdOfThisIme(), subtype);
380    }
381
382    public void setAdditionalInputMethodSubtypes(final InputMethodSubtype[] subtypes) {
383        mImmWrapper.mImm.setAdditionalInputMethodSubtypes(
384                getInputMethodIdOfThisIme(), subtypes);
385        // Clear the cache so that we go read the {@link InputMethodInfo} of this IME and list of
386        // subtypes again next time.
387        clearSubtypeCaches();
388    }
389
390    private List<InputMethodSubtype> getEnabledInputMethodSubtypeList(final InputMethodInfo imi,
391            final boolean allowsImplicitlySelectedSubtypes) {
392        final HashMap<InputMethodInfo, List<InputMethodSubtype>> cache =
393                allowsImplicitlySelectedSubtypes
394                ? mSubtypeListCacheWithImplicitlySelectedSubtypes
395                : mSubtypeListCacheWithoutImplicitlySelectedSubtypes;
396        final List<InputMethodSubtype> cachedList = cache.get(imi);
397        if (null != cachedList) return cachedList;
398        final List<InputMethodSubtype> result = mImmWrapper.mImm.getEnabledInputMethodSubtypeList(
399                imi, allowsImplicitlySelectedSubtypes);
400        cache.put(imi, result);
401        return result;
402    }
403
404    public void clearSubtypeCaches() {
405        mSubtypeListCacheWithImplicitlySelectedSubtypes.clear();
406        mSubtypeListCacheWithoutImplicitlySelectedSubtypes.clear();
407        mInputMethodInfoCache.clear();
408    }
409}
410