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;
31
32import java.util.Collections;
33import java.util.List;
34
35/**
36 * Enrichment class for InputMethodManager to simplify interaction and add functionality.
37 */
38public final class RichInputMethodManager {
39    private static final String TAG = RichInputMethodManager.class.getSimpleName();
40
41    private RichInputMethodManager() {
42        // This utility class is not publicly instantiable.
43    }
44
45    private static final RichInputMethodManager sInstance = new RichInputMethodManager();
46
47    private InputMethodManagerCompatWrapper mImmWrapper;
48    private InputMethodInfo mInputMethodInfoOfThisIme;
49
50    private static final int INDEX_NOT_FOUND = -1;
51
52    public static RichInputMethodManager getInstance() {
53        sInstance.checkInitialized();
54        return sInstance;
55    }
56
57    // Caveat: This may cause IPC
58    public static boolean isInputMethodManagerValidForUserOfThisProcess(final Context context) {
59        // Basically called to check whether this IME has been triggered by the current user or not
60        return !((InputMethodManager)context.getSystemService(Context.INPUT_METHOD_SERVICE)).
61                getInputMethodList().isEmpty();
62    }
63
64    public static void init(final Context context) {
65        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
66        sInstance.initInternal(context, prefs);
67    }
68
69    private boolean isInitialized() {
70        return mImmWrapper != null;
71    }
72
73    private void checkInitialized() {
74        if (!isInitialized()) {
75            throw new RuntimeException(TAG + " is used before initialization");
76        }
77    }
78
79    private void initInternal(final Context context, final SharedPreferences prefs) {
80        if (isInitialized()) {
81            return;
82        }
83        mImmWrapper = new InputMethodManagerCompatWrapper(context);
84        mInputMethodInfoOfThisIme = getInputMethodInfoOfThisIme(context);
85
86        // Initialize additional subtypes.
87        SubtypeLocale.init(context);
88        final String prefAdditionalSubtypes = Settings.readPrefAdditionalSubtypes(
89                prefs, context.getResources());
90        final InputMethodSubtype[] additionalSubtypes =
91                AdditionalSubtype.createAdditionalSubtypesArray(prefAdditionalSubtypes);
92        setAdditionalInputMethodSubtypes(additionalSubtypes);
93    }
94
95    public InputMethodManager getInputMethodManager() {
96        checkInitialized();
97        return mImmWrapper.mImm;
98    }
99
100    private InputMethodInfo getInputMethodInfoOfThisIme(final Context context) {
101        final String packageName = context.getPackageName();
102        for (final InputMethodInfo imi : mImmWrapper.mImm.getInputMethodList()) {
103            if (imi.getPackageName().equals(packageName)) {
104                return imi;
105            }
106        }
107        throw new RuntimeException("Input method id for " + packageName + " not found.");
108    }
109
110    public List<InputMethodSubtype> getMyEnabledInputMethodSubtypeList(
111            boolean allowsImplicitlySelectedSubtypes) {
112        return mImmWrapper.mImm.getEnabledInputMethodSubtypeList(
113                mInputMethodInfoOfThisIme, allowsImplicitlySelectedSubtypes);
114    }
115
116    public boolean switchToNextInputMethod(final IBinder token, final boolean onlyCurrentIme) {
117        if (mImmWrapper.switchToNextInputMethod(token, onlyCurrentIme)) {
118            return true;
119        }
120        // Was not able to call {@link InputMethodManager#switchToNextInputMethodIBinder,boolean)}
121        // because the current device is running ICS or previous and lacks the API.
122        if (switchToNextInputSubtypeInThisIme(token, onlyCurrentIme)) {
123            return true;
124        }
125        return switchToNextInputMethodAndSubtype(token);
126    }
127
128    private boolean switchToNextInputSubtypeInThisIme(final IBinder token,
129            final boolean onlyCurrentIme) {
130        final InputMethodManager imm = mImmWrapper.mImm;
131        final InputMethodSubtype currentSubtype = imm.getCurrentInputMethodSubtype();
132        final List<InputMethodSubtype> enabledSubtypes = getMyEnabledInputMethodSubtypeList(
133                true /* allowsImplicitlySelectedSubtypes */);
134        final int currentIndex = getSubtypeIndexInList(currentSubtype, enabledSubtypes);
135        if (currentIndex == INDEX_NOT_FOUND) {
136            Log.w(TAG, "Can't find current subtype in enabled subtypes: subtype="
137                    + SubtypeLocale.getSubtypeDisplayName(currentSubtype));
138            return false;
139        }
140        final int nextIndex = (currentIndex + 1) % enabledSubtypes.size();
141        if (nextIndex <= currentIndex && !onlyCurrentIme) {
142            // The current subtype is the last or only enabled one and it needs to switch to
143            // next IME.
144            return false;
145        }
146        final InputMethodSubtype nextSubtype = enabledSubtypes.get(nextIndex);
147        setInputMethodAndSubtype(token, nextSubtype);
148        return true;
149    }
150
151    private boolean switchToNextInputMethodAndSubtype(final IBinder token) {
152        final InputMethodManager imm = mImmWrapper.mImm;
153        final List<InputMethodInfo> enabledImis = imm.getEnabledInputMethodList();
154        final int currentIndex = getImiIndexInList(mInputMethodInfoOfThisIme, enabledImis);
155        if (currentIndex == INDEX_NOT_FOUND) {
156            Log.w(TAG, "Can't find current IME in enabled IMEs: IME package="
157                    + mInputMethodInfoOfThisIme.getPackageName());
158            return false;
159        }
160        final InputMethodInfo nextImi = getNextNonAuxiliaryIme(currentIndex, enabledImis);
161        final List<InputMethodSubtype> enabledSubtypes = imm.getEnabledInputMethodSubtypeList(
162                nextImi, true /* allowsImplicitlySelectedSubtypes */);
163        if (enabledSubtypes.isEmpty()) {
164            // The next IME has no subtype.
165            imm.setInputMethod(token, nextImi.getId());
166            return true;
167        }
168        final InputMethodSubtype firstSubtype = enabledSubtypes.get(0);
169        imm.setInputMethodAndSubtype(token, nextImi.getId(), firstSubtype);
170        return true;
171    }
172
173    private static int getImiIndexInList(final InputMethodInfo inputMethodInfo,
174            final List<InputMethodInfo> imiList) {
175        final int count = imiList.size();
176        for (int index = 0; index < count; index++) {
177            final InputMethodInfo imi = imiList.get(index);
178            if (imi.equals(inputMethodInfo)) {
179                return index;
180            }
181        }
182        return INDEX_NOT_FOUND;
183    }
184
185    // This method mimics {@link InputMethodManager#switchToNextInputMethod(IBinder,boolean)}.
186    private static InputMethodInfo getNextNonAuxiliaryIme(final int currentIndex,
187            final List<InputMethodInfo> imiList) {
188        final int count = imiList.size();
189        for (int i = 1; i < count; i++) {
190            final int nextIndex = (currentIndex + i) % count;
191            final InputMethodInfo nextImi = imiList.get(nextIndex);
192            if (!isAuxiliaryIme(nextImi)) {
193                return nextImi;
194            }
195        }
196        return imiList.get(currentIndex);
197    }
198
199    // Copied from {@link InputMethodInfo}. See how auxiliary of IME is determined.
200    private static boolean isAuxiliaryIme(final InputMethodInfo imi) {
201        final int count = imi.getSubtypeCount();
202        if (count == 0) {
203            return false;
204        }
205        for (int index = 0; index < count; index++) {
206            final InputMethodSubtype subtype = imi.getSubtypeAt(index);
207            if (!subtype.isAuxiliary()) {
208                return false;
209            }
210        }
211        return true;
212    }
213
214    public InputMethodInfo getInputMethodInfoOfThisIme() {
215        return mInputMethodInfoOfThisIme;
216    }
217
218    public String getInputMethodIdOfThisIme() {
219        return mInputMethodInfoOfThisIme.getId();
220    }
221
222    public boolean checkIfSubtypeBelongsToThisImeAndEnabled(final InputMethodSubtype subtype) {
223        return checkIfSubtypeBelongsToImeAndEnabled(mInputMethodInfoOfThisIme, subtype);
224    }
225
226    public boolean checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled(
227            final InputMethodSubtype subtype) {
228        final boolean subtypeEnabled = checkIfSubtypeBelongsToThisImeAndEnabled(subtype);
229        final boolean subtypeExplicitlyEnabled = checkIfSubtypeBelongsToList(
230                subtype, getMyEnabledInputMethodSubtypeList(
231                        false /* allowsImplicitlySelectedSubtypes */));
232        return subtypeEnabled && !subtypeExplicitlyEnabled;
233    }
234
235    public boolean checkIfSubtypeBelongsToImeAndEnabled(final InputMethodInfo imi,
236            final InputMethodSubtype subtype) {
237        return checkIfSubtypeBelongsToList(
238                subtype, mImmWrapper.mImm.getEnabledInputMethodSubtypeList(
239                        imi, true /* allowsImplicitlySelectedSubtypes */));
240    }
241
242    private static boolean checkIfSubtypeBelongsToList(final InputMethodSubtype subtype,
243            final List<InputMethodSubtype> subtypes) {
244        return getSubtypeIndexInList(subtype, subtypes) != INDEX_NOT_FOUND;
245    }
246
247    private static int getSubtypeIndexInList(final InputMethodSubtype subtype,
248            final List<InputMethodSubtype> subtypes) {
249        final int count = subtypes.size();
250        for (int index = 0; index < count; index++) {
251            final InputMethodSubtype ims = subtypes.get(index);
252            if (ims.equals(subtype)) {
253                return index;
254            }
255        }
256        return INDEX_NOT_FOUND;
257    }
258
259    public boolean checkIfSubtypeBelongsToThisIme(final InputMethodSubtype subtype) {
260        return getSubtypeIndexInIme(subtype, mInputMethodInfoOfThisIme) != INDEX_NOT_FOUND;
261    }
262
263    private static int getSubtypeIndexInIme(final InputMethodSubtype subtype,
264            final InputMethodInfo imi) {
265        final int count = imi.getSubtypeCount();
266        for (int index = 0; index < count; index++) {
267            final InputMethodSubtype ims = imi.getSubtypeAt(index);
268            if (ims.equals(subtype)) {
269                return index;
270            }
271        }
272        return INDEX_NOT_FOUND;
273    }
274
275    public InputMethodSubtype getCurrentInputMethodSubtype(
276            final InputMethodSubtype defaultSubtype) {
277        final InputMethodSubtype currentSubtype = mImmWrapper.mImm.getCurrentInputMethodSubtype();
278        return (currentSubtype != null) ? currentSubtype : defaultSubtype;
279    }
280
281    public boolean hasMultipleEnabledIMEsOrSubtypes(final boolean shouldIncludeAuxiliarySubtypes) {
282        final List<InputMethodInfo> enabledImis = mImmWrapper.mImm.getEnabledInputMethodList();
283        return hasMultipleEnabledSubtypes(shouldIncludeAuxiliarySubtypes, enabledImis);
284    }
285
286    public boolean hasMultipleEnabledSubtypesInThisIme(
287            final boolean shouldIncludeAuxiliarySubtypes) {
288        final List<InputMethodInfo> imiList = Collections.singletonList(mInputMethodInfoOfThisIme);
289        return hasMultipleEnabledSubtypes(shouldIncludeAuxiliarySubtypes, imiList);
290    }
291
292    private boolean hasMultipleEnabledSubtypes(final boolean shouldIncludeAuxiliarySubtypes,
293            final List<InputMethodInfo> imiList) {
294        // Number of the filtered IMEs
295        int filteredImisCount = 0;
296
297        for (InputMethodInfo imi : imiList) {
298            // We can return true immediately after we find two or more filtered IMEs.
299            if (filteredImisCount > 1) return true;
300            final List<InputMethodSubtype> subtypes =
301                    mImmWrapper.mImm.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 = SubtypeLocale.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    }
365}
366