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