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 InputMethodInfo mInputMethodInfoOfThisIme;
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        mInputMethodInfoOfThisIme = getInputMethodInfoOfThisIme(context);
87
88        // Initialize additional subtypes.
89        SubtypeLocaleUtils.init(context);
90        final String prefAdditionalSubtypes = Settings.readPrefAdditionalSubtypes(
91                prefs, context.getResources());
92        final InputMethodSubtype[] additionalSubtypes =
93                AdditionalSubtypeUtils.createAdditionalSubtypesArray(prefAdditionalSubtypes);
94        setAdditionalInputMethodSubtypes(additionalSubtypes);
95    }
96
97    public InputMethodManager getInputMethodManager() {
98        checkInitialized();
99        return mImmWrapper.mImm;
100    }
101
102    private InputMethodInfo getInputMethodInfoOfThisIme(final Context context) {
103        final String packageName = context.getPackageName();
104        for (final InputMethodInfo imi : mImmWrapper.mImm.getInputMethodList()) {
105            if (imi.getPackageName().equals(packageName)) {
106                return imi;
107            }
108        }
109        throw new RuntimeException("Input method id for " + packageName + " not found.");
110    }
111
112    public List<InputMethodSubtype> getMyEnabledInputMethodSubtypeList(
113            boolean allowsImplicitlySelectedSubtypes) {
114        return getEnabledInputMethodSubtypeList(mInputMethodInfoOfThisIme,
115                allowsImplicitlySelectedSubtypes);
116    }
117
118    public boolean switchToNextInputMethod(final IBinder token, final boolean onlyCurrentIme) {
119        if (mImmWrapper.switchToNextInputMethod(token, onlyCurrentIme)) {
120            return true;
121        }
122        // Was not able to call {@link InputMethodManager#switchToNextInputMethodIBinder,boolean)}
123        // because the current device is running ICS or previous and lacks the API.
124        if (switchToNextInputSubtypeInThisIme(token, onlyCurrentIme)) {
125            return true;
126        }
127        return switchToNextInputMethodAndSubtype(token);
128    }
129
130    private boolean switchToNextInputSubtypeInThisIme(final IBinder token,
131            final boolean onlyCurrentIme) {
132        final InputMethodManager imm = mImmWrapper.mImm;
133        final InputMethodSubtype currentSubtype = imm.getCurrentInputMethodSubtype();
134        final List<InputMethodSubtype> enabledSubtypes = getMyEnabledInputMethodSubtypeList(
135                true /* allowsImplicitlySelectedSubtypes */);
136        final int currentIndex = getSubtypeIndexInList(currentSubtype, enabledSubtypes);
137        if (currentIndex == INDEX_NOT_FOUND) {
138            Log.w(TAG, "Can't find current subtype in enabled subtypes: subtype="
139                    + SubtypeLocaleUtils.getSubtypeNameForLogging(currentSubtype));
140            return false;
141        }
142        final int nextIndex = (currentIndex + 1) % enabledSubtypes.size();
143        if (nextIndex <= currentIndex && !onlyCurrentIme) {
144            // The current subtype is the last or only enabled one and it needs to switch to
145            // next IME.
146            return false;
147        }
148        final InputMethodSubtype nextSubtype = enabledSubtypes.get(nextIndex);
149        setInputMethodAndSubtype(token, nextSubtype);
150        return true;
151    }
152
153    private boolean switchToNextInputMethodAndSubtype(final IBinder token) {
154        final InputMethodManager imm = mImmWrapper.mImm;
155        final List<InputMethodInfo> enabledImis = imm.getEnabledInputMethodList();
156        final int currentIndex = getImiIndexInList(mInputMethodInfoOfThisIme, enabledImis);
157        if (currentIndex == INDEX_NOT_FOUND) {
158            Log.w(TAG, "Can't find current IME in enabled IMEs: IME package="
159                    + mInputMethodInfoOfThisIme.getPackageName());
160            return false;
161        }
162        final InputMethodInfo nextImi = getNextNonAuxiliaryIme(currentIndex, enabledImis);
163        final List<InputMethodSubtype> enabledSubtypes = getEnabledInputMethodSubtypeList(nextImi,
164                true /* allowsImplicitlySelectedSubtypes */);
165        if (enabledSubtypes.isEmpty()) {
166            // The next IME has no subtype.
167            imm.setInputMethod(token, nextImi.getId());
168            return true;
169        }
170        final InputMethodSubtype firstSubtype = enabledSubtypes.get(0);
171        imm.setInputMethodAndSubtype(token, nextImi.getId(), firstSubtype);
172        return true;
173    }
174
175    private static int getImiIndexInList(final InputMethodInfo inputMethodInfo,
176            final List<InputMethodInfo> imiList) {
177        final int count = imiList.size();
178        for (int index = 0; index < count; index++) {
179            final InputMethodInfo imi = imiList.get(index);
180            if (imi.equals(inputMethodInfo)) {
181                return index;
182            }
183        }
184        return INDEX_NOT_FOUND;
185    }
186
187    // This method mimics {@link InputMethodManager#switchToNextInputMethod(IBinder,boolean)}.
188    private static InputMethodInfo getNextNonAuxiliaryIme(final int currentIndex,
189            final List<InputMethodInfo> imiList) {
190        final int count = imiList.size();
191        for (int i = 1; i < count; i++) {
192            final int nextIndex = (currentIndex + i) % count;
193            final InputMethodInfo nextImi = imiList.get(nextIndex);
194            if (!isAuxiliaryIme(nextImi)) {
195                return nextImi;
196            }
197        }
198        return imiList.get(currentIndex);
199    }
200
201    // Copied from {@link InputMethodInfo}. See how auxiliary of IME is determined.
202    private static boolean isAuxiliaryIme(final InputMethodInfo imi) {
203        final int count = imi.getSubtypeCount();
204        if (count == 0) {
205            return false;
206        }
207        for (int index = 0; index < count; index++) {
208            final InputMethodSubtype subtype = imi.getSubtypeAt(index);
209            if (!subtype.isAuxiliary()) {
210                return false;
211            }
212        }
213        return true;
214    }
215
216    public InputMethodInfo getInputMethodInfoOfThisIme() {
217        return mInputMethodInfoOfThisIme;
218    }
219
220    public String getInputMethodIdOfThisIme() {
221        return mInputMethodInfoOfThisIme.getId();
222    }
223
224    public boolean checkIfSubtypeBelongsToThisImeAndEnabled(final InputMethodSubtype subtype) {
225        return checkIfSubtypeBelongsToImeAndEnabled(mInputMethodInfoOfThisIme, subtype);
226    }
227
228    public boolean checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled(
229            final InputMethodSubtype subtype) {
230        final boolean subtypeEnabled = checkIfSubtypeBelongsToThisImeAndEnabled(subtype);
231        final boolean subtypeExplicitlyEnabled = checkIfSubtypeBelongsToList(
232                subtype, getMyEnabledInputMethodSubtypeList(
233                        false /* allowsImplicitlySelectedSubtypes */));
234        return subtypeEnabled && !subtypeExplicitlyEnabled;
235    }
236
237    public boolean checkIfSubtypeBelongsToImeAndEnabled(final InputMethodInfo imi,
238            final InputMethodSubtype subtype) {
239        return checkIfSubtypeBelongsToList(subtype, getEnabledInputMethodSubtypeList(imi,
240                true /* allowsImplicitlySelectedSubtypes */));
241    }
242
243    private static boolean checkIfSubtypeBelongsToList(final InputMethodSubtype subtype,
244            final List<InputMethodSubtype> subtypes) {
245        return getSubtypeIndexInList(subtype, subtypes) != INDEX_NOT_FOUND;
246    }
247
248    private static int getSubtypeIndexInList(final InputMethodSubtype subtype,
249            final List<InputMethodSubtype> subtypes) {
250        final int count = subtypes.size();
251        for (int index = 0; index < count; index++) {
252            final InputMethodSubtype ims = subtypes.get(index);
253            if (ims.equals(subtype)) {
254                return index;
255            }
256        }
257        return INDEX_NOT_FOUND;
258    }
259
260    public boolean checkIfSubtypeBelongsToThisIme(final InputMethodSubtype subtype) {
261        return getSubtypeIndexInIme(subtype, mInputMethodInfoOfThisIme) != INDEX_NOT_FOUND;
262    }
263
264    private static int getSubtypeIndexInIme(final InputMethodSubtype subtype,
265            final InputMethodInfo imi) {
266        final int count = imi.getSubtypeCount();
267        for (int index = 0; index < count; index++) {
268            final InputMethodSubtype ims = imi.getSubtypeAt(index);
269            if (ims.equals(subtype)) {
270                return index;
271            }
272        }
273        return INDEX_NOT_FOUND;
274    }
275
276    public InputMethodSubtype getCurrentInputMethodSubtype(
277            final InputMethodSubtype defaultSubtype) {
278        final InputMethodSubtype currentSubtype = mImmWrapper.mImm.getCurrentInputMethodSubtype();
279        return (currentSubtype != null) ? currentSubtype : defaultSubtype;
280    }
281
282    public boolean hasMultipleEnabledIMEsOrSubtypes(final boolean shouldIncludeAuxiliarySubtypes) {
283        final List<InputMethodInfo> enabledImis = mImmWrapper.mImm.getEnabledInputMethodList();
284        return hasMultipleEnabledSubtypes(shouldIncludeAuxiliarySubtypes, enabledImis);
285    }
286
287    public boolean hasMultipleEnabledSubtypesInThisIme(
288            final boolean shouldIncludeAuxiliarySubtypes) {
289        final List<InputMethodInfo> imiList = Collections.singletonList(mInputMethodInfoOfThisIme);
290        return hasMultipleEnabledSubtypes(shouldIncludeAuxiliarySubtypes, imiList);
291    }
292
293    private boolean hasMultipleEnabledSubtypes(final boolean shouldIncludeAuxiliarySubtypes,
294            final List<InputMethodInfo> imiList) {
295        // Number of the filtered IMEs
296        int filteredImisCount = 0;
297
298        for (InputMethodInfo imi : imiList) {
299            // We can return true immediately after we find two or more filtered IMEs.
300            if (filteredImisCount > 1) return true;
301            final List<InputMethodSubtype> subtypes = getEnabledInputMethodSubtypeList(imi, true);
302            // IMEs that have no subtypes should be counted.
303            if (subtypes.isEmpty()) {
304                ++filteredImisCount;
305                continue;
306            }
307
308            int auxCount = 0;
309            for (InputMethodSubtype subtype : subtypes) {
310                if (subtype.isAuxiliary()) {
311                    ++auxCount;
312                }
313            }
314            final int nonAuxCount = subtypes.size() - auxCount;
315
316            // IMEs that have one or more non-auxiliary subtypes should be counted.
317            // If shouldIncludeAuxiliarySubtypes is true, IMEs that have two or more auxiliary
318            // subtypes should be counted as well.
319            if (nonAuxCount > 0 || (shouldIncludeAuxiliarySubtypes && auxCount > 1)) {
320                ++filteredImisCount;
321                continue;
322            }
323        }
324
325        if (filteredImisCount > 1) {
326            return true;
327        }
328        final List<InputMethodSubtype> subtypes = getMyEnabledInputMethodSubtypeList(true);
329        int keyboardCount = 0;
330        // imm.getEnabledInputMethodSubtypeList(null, true) will return the current IME's
331        // both explicitly and implicitly enabled input method subtype.
332        // (The current IME should be LatinIME.)
333        for (InputMethodSubtype subtype : subtypes) {
334            if (KEYBOARD_MODE.equals(subtype.getMode())) {
335                ++keyboardCount;
336            }
337        }
338        return keyboardCount > 1;
339    }
340
341    public InputMethodSubtype findSubtypeByLocaleAndKeyboardLayoutSet(final String localeString,
342            final String keyboardLayoutSetName) {
343        final InputMethodInfo myImi = mInputMethodInfoOfThisIme;
344        final int count = myImi.getSubtypeCount();
345        for (int i = 0; i < count; i++) {
346            final InputMethodSubtype subtype = myImi.getSubtypeAt(i);
347            final String layoutName = SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype);
348            if (localeString.equals(subtype.getLocale())
349                    && keyboardLayoutSetName.equals(layoutName)) {
350                return subtype;
351            }
352        }
353        return null;
354    }
355
356    public void setInputMethodAndSubtype(final IBinder token, final InputMethodSubtype subtype) {
357        mImmWrapper.mImm.setInputMethodAndSubtype(
358                token, mInputMethodInfoOfThisIme.getId(), subtype);
359    }
360
361    public void setAdditionalInputMethodSubtypes(final InputMethodSubtype[] subtypes) {
362        mImmWrapper.mImm.setAdditionalInputMethodSubtypes(
363                mInputMethodInfoOfThisIme.getId(), subtypes);
364        // Clear the cache so that we go read the subtypes again next time.
365        clearSubtypeCaches();
366    }
367
368    private List<InputMethodSubtype> getEnabledInputMethodSubtypeList(final InputMethodInfo imi,
369            final boolean allowsImplicitlySelectedSubtypes) {
370        final HashMap<InputMethodInfo, List<InputMethodSubtype>> cache =
371                allowsImplicitlySelectedSubtypes
372                ? mSubtypeListCacheWithImplicitlySelectedSubtypes
373                : mSubtypeListCacheWithoutImplicitlySelectedSubtypes;
374        final List<InputMethodSubtype> cachedList = cache.get(imi);
375        if (null != cachedList) return cachedList;
376        final List<InputMethodSubtype> result = mImmWrapper.mImm.getEnabledInputMethodSubtypeList(
377                imi, allowsImplicitlySelectedSubtypes);
378        cache.put(imi, result);
379        return result;
380    }
381
382    public void clearSubtypeCaches() {
383        mSubtypeListCacheWithImplicitlySelectedSubtypes.clear();
384        mSubtypeListCacheWithoutImplicitlySelectedSubtypes.clear();
385    }
386}
387