/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.sugree.inputmethod.latin; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.AsyncTask; import android.os.IBinder; import android.text.TextUtils; import android.util.Log; import com.android.inputmethod.compat.InputMethodInfoCompatWrapper; import com.android.inputmethod.compat.InputMethodManagerCompatWrapper; import com.android.inputmethod.compat.InputMethodSubtypeCompatWrapper; import com.android.inputmethod.deprecated.VoiceProxy; import com.android.inputmethod.keyboard.KeyboardSwitcher; import com.android.inputmethod.keyboard.LatinKeyboard; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.Map; public class SubtypeSwitcher { private static boolean DBG = LatinImeLogger.sDBG; private static final String TAG = SubtypeSwitcher.class.getSimpleName(); private static final char LOCALE_SEPARATER = '_'; private static final String KEYBOARD_MODE = "keyboard"; private static final String VOICE_MODE = "voice"; private static final String SUBTYPE_EXTRAVALUE_REQUIRE_NETWORK_CONNECTIVITY = "requireNetworkConnectivity"; private final TextUtils.SimpleStringSplitter mLocaleSplitter = new TextUtils.SimpleStringSplitter(LOCALE_SEPARATER); private static final SubtypeSwitcher sInstance = new SubtypeSwitcher(); private /* final */ LatinIME mService; private /* final */ InputMethodManagerCompatWrapper mImm; private /* final */ Resources mResources; private /* final */ ConnectivityManager mConnectivityManager; private final ArrayList<InputMethodSubtypeCompatWrapper> mEnabledKeyboardSubtypesOfCurrentInputMethod = new ArrayList<InputMethodSubtypeCompatWrapper>(); private final ArrayList<String> mEnabledLanguagesOfCurrentInputMethod = new ArrayList<String>(); /*-----------------------------------------------------------*/ // Variants which should be changed only by reload functions. private boolean mNeedsToDisplayLanguage; private boolean mIsSystemLanguageSameAsInputLanguage; private InputMethodInfoCompatWrapper mShortcutInputMethodInfo; private InputMethodSubtypeCompatWrapper mShortcutSubtype; private List<InputMethodSubtypeCompatWrapper> mAllEnabledSubtypesOfCurrentInputMethod; private InputMethodSubtypeCompatWrapper mCurrentSubtype; private Locale mSystemLocale; private Locale mInputLocale; private String mInputLocaleStr; private VoiceProxy.VoiceInputWrapper mVoiceInputWrapper; /*-----------------------------------------------------------*/ private boolean mIsNetworkConnected; public static SubtypeSwitcher getInstance() { return sInstance; } public static void init(LatinIME service) { SubtypeLocale.init(service); sInstance.initialize(service); sInstance.updateAllParameters(); } private SubtypeSwitcher() { // Intentional empty constructor for singleton. } private void initialize(LatinIME service) { mService = service; mResources = service.getResources(); mImm = InputMethodManagerCompatWrapper.getInstance(); mConnectivityManager = (ConnectivityManager) service.getSystemService( Context.CONNECTIVITY_SERVICE); mEnabledKeyboardSubtypesOfCurrentInputMethod.clear(); mEnabledLanguagesOfCurrentInputMethod.clear(); mSystemLocale = null; mInputLocale = null; mInputLocaleStr = null; mCurrentSubtype = null; mAllEnabledSubtypesOfCurrentInputMethod = null; mVoiceInputWrapper = null; final NetworkInfo info = mConnectivityManager.getActiveNetworkInfo(); mIsNetworkConnected = (info != null && info.isConnected()); } // Update all parameters stored in SubtypeSwitcher. // Only configuration changed event is allowed to call this because this is heavy. private void updateAllParameters() { mSystemLocale = mResources.getConfiguration().locale; updateSubtype(mImm.getCurrentInputMethodSubtype()); updateParametersOnStartInputView(); } // Update parameters which are changed outside LatinIME. This parameters affect UI so they // should be updated every time onStartInputview. public void updateParametersOnStartInputView() { updateEnabledSubtypes(); updateShortcutIME(); } // Reload enabledSubtypes from the framework. private void updateEnabledSubtypes() { final String currentMode = getCurrentSubtypeMode(); boolean foundCurrentSubtypeBecameDisabled = true; mAllEnabledSubtypesOfCurrentInputMethod = mImm.getEnabledInputMethodSubtypeList( null, true); mEnabledLanguagesOfCurrentInputMethod.clear(); mEnabledKeyboardSubtypesOfCurrentInputMethod.clear(); for (InputMethodSubtypeCompatWrapper ims : mAllEnabledSubtypesOfCurrentInputMethod) { final String locale = getSubtypeLocale(ims); final String mode = ims.getMode(); mLocaleSplitter.setString(locale); if (mLocaleSplitter.hasNext()) { mEnabledLanguagesOfCurrentInputMethod.add(mLocaleSplitter.next()); } if (locale.equals(mInputLocaleStr) && mode.equals(currentMode)) { foundCurrentSubtypeBecameDisabled = false; } if (KEYBOARD_MODE.equals(ims.getMode())) { mEnabledKeyboardSubtypesOfCurrentInputMethod.add(ims); } } mNeedsToDisplayLanguage = !(getEnabledKeyboardLocaleCount() <= 1 && mIsSystemLanguageSameAsInputLanguage); if (foundCurrentSubtypeBecameDisabled) { if (DBG) { Log.w(TAG, "Current subtype: " + mInputLocaleStr + ", " + currentMode); Log.w(TAG, "Last subtype was disabled. Update to the current one."); } updateSubtype(mImm.getCurrentInputMethodSubtype()); } } private void updateShortcutIME() { if (DBG) { Log.d(TAG, "Update shortcut IME from : " + (mShortcutInputMethodInfo == null ? "<null>" : mShortcutInputMethodInfo.getId()) + ", " + (mShortcutSubtype == null ? "<null>" : (getSubtypeLocale(mShortcutSubtype) + ", " + mShortcutSubtype.getMode()))); } // TODO: Update an icon for shortcut IME final Map<InputMethodInfoCompatWrapper, List<InputMethodSubtypeCompatWrapper>> shortcuts = mImm.getShortcutInputMethodsAndSubtypes(); mShortcutInputMethodInfo = null; mShortcutSubtype = null; for (InputMethodInfoCompatWrapper imi : shortcuts.keySet()) { List<InputMethodSubtypeCompatWrapper> subtypes = shortcuts.get(imi); // TODO: Returns the first found IMI for now. Should handle all shortcuts as // appropriate. mShortcutInputMethodInfo = imi; // TODO: Pick up the first found subtype for now. Should handle all subtypes // as appropriate. mShortcutSubtype = subtypes.size() > 0 ? subtypes.get(0) : null; break; } if (DBG) { Log.d(TAG, "Update shortcut IME to : " + (mShortcutInputMethodInfo == null ? "<null>" : mShortcutInputMethodInfo.getId()) + ", " + (mShortcutSubtype == null ? "<null>" : (getSubtypeLocale(mShortcutSubtype) + ", " + mShortcutSubtype.getMode()))); } } private static String getSubtypeLocale(InputMethodSubtypeCompatWrapper subtype) { final String keyboardLocale = subtype.getExtraValueOf( LatinIME.SUBTYPE_EXTRA_VALUE_KEYBOARD_LOCALE); return keyboardLocale != null ? keyboardLocale : subtype.getLocale(); } // Update the current subtype. LatinIME.onCurrentInputMethodSubtypeChanged calls this function. public void updateSubtype(InputMethodSubtypeCompatWrapper newSubtype) { final String newLocale; final String newMode; final String oldMode = getCurrentSubtypeMode(); if (newSubtype == null) { // Normally, newSubtype shouldn't be null. But just in case newSubtype was null, // fallback to the default locale. Log.w(TAG, "Couldn't get the current subtype."); newLocale = "en_US"; newMode = KEYBOARD_MODE; } else { newLocale = getSubtypeLocale(newSubtype); newMode = newSubtype.getMode(); } if (DBG) { Log.w(TAG, "Update subtype to:" + newLocale + "," + newMode + ", from: " + mInputLocaleStr + ", " + oldMode); } boolean languageChanged = false; if (!newLocale.equals(mInputLocaleStr)) { if (mInputLocaleStr != null) { languageChanged = true; } updateInputLocale(newLocale); } boolean modeChanged = false; if (!newMode.equals(oldMode)) { if (oldMode != null) { modeChanged = true; } } mCurrentSubtype = newSubtype; // If the old mode is voice input, we need to reset or cancel its status. // We cancel its status when we change mode, while we reset otherwise. if (isKeyboardMode()) { if (modeChanged) { if (VOICE_MODE.equals(oldMode) && mVoiceInputWrapper != null) { mVoiceInputWrapper.cancel(); } } if (modeChanged || languageChanged) { updateShortcutIME(); mService.onRefreshKeyboard(); } } else if (isVoiceMode() && mVoiceInputWrapper != null) { if (VOICE_MODE.equals(oldMode)) { mVoiceInputWrapper.reset(); } // If needsToShowWarningDialog is true, voice input need to show warning before // show recognition view. if (languageChanged || modeChanged || VoiceProxy.getInstance().needsToShowWarningDialog()) { triggerVoiceIME(); } } else { if (VOICE_MODE.equals(oldMode) && mVoiceInputWrapper != null) { // We need to reset the voice input to release the resources and to reset its status // as it is not the current input mode. mVoiceInputWrapper.reset(); } final String packageName = mService.getPackageName(); int version = -1; try { version = mService.getPackageManager().getPackageInfo( packageName, 0).versionCode; } catch (NameNotFoundException e) { } Log.w(TAG, "Unknown subtype mode: " + newMode + "," + version + ", " + packageName + ", " + mVoiceInputWrapper + ". IME is already changed to other IME."); if (newSubtype != null) { Log.w(TAG, "Subtype mode:" + newSubtype.getMode()); Log.w(TAG, "Subtype locale:" + newSubtype.getLocale()); Log.w(TAG, "Subtype extra value:" + newSubtype.getExtraValue()); Log.w(TAG, "Subtype is auxiliary:" + newSubtype.isAuxiliary()); } } } // Update the current input locale from Locale string. private void updateInputLocale(String inputLocaleStr) { // example: inputLocaleStr = "en_US" "en" "" // "en_US" --> language: en & country: US // "en" --> language: en // "" --> the system locale if (!TextUtils.isEmpty(inputLocaleStr)) { mInputLocale = LocaleUtils.constructLocaleFromString(inputLocaleStr); mInputLocaleStr = inputLocaleStr; } else { mInputLocale = mSystemLocale; String country = mSystemLocale.getCountry(); mInputLocaleStr = mSystemLocale.getLanguage() + (TextUtils.isEmpty(country) ? "" : "_" + mSystemLocale.getLanguage()); } mIsSystemLanguageSameAsInputLanguage = getSystemLocale().getLanguage().equalsIgnoreCase( getInputLocale().getLanguage()); mNeedsToDisplayLanguage = !(getEnabledKeyboardLocaleCount() <= 1 && mIsSystemLanguageSameAsInputLanguage); } //////////////////////////// // Shortcut IME functions // //////////////////////////// public void switchToShortcutIME() { if (mShortcutInputMethodInfo == null) { return; } final String imiId = mShortcutInputMethodInfo.getId(); final InputMethodSubtypeCompatWrapper subtype = mShortcutSubtype; switchToTargetIME(imiId, subtype); } private void switchToTargetIME( final String imiId, final InputMethodSubtypeCompatWrapper subtype) { final IBinder token = mService.getWindow().getWindow().getAttributes().token; if (token == null) { return; } new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { mImm.setInputMethodAndSubtype(token, imiId, subtype); return null; } @Override protected void onPostExecute(Void result) { // Calls in this method need to be done in the same thread as the thread which // called switchToShortcutIME(). // Notify an event that the current subtype was changed. This event will be // handled if "onCurrentInputMethodSubtypeChanged" can't be implemented // when the API level is 10 or previous. mService.notifyOnCurrentInputMethodSubtypeChanged(subtype); } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); } public Drawable getShortcutIcon() { return getSubtypeIcon(mShortcutInputMethodInfo, mShortcutSubtype); } private Drawable getSubtypeIcon( InputMethodInfoCompatWrapper imi, InputMethodSubtypeCompatWrapper subtype) { final PackageManager pm = mService.getPackageManager(); if (imi != null) { final String imiPackageName = imi.getPackageName(); if (DBG) { Log.d(TAG, "Update icons of IME: " + imiPackageName + "," + getSubtypeLocale(subtype) + "," + subtype.getMode()); } if (subtype != null) { return pm.getDrawable(imiPackageName, subtype.getIconResId(), imi.getServiceInfo().applicationInfo); } else if (imi.getSubtypeCount() > 0 && imi.getSubtypeAt(0) != null) { return pm.getDrawable(imiPackageName, imi.getSubtypeAt(0).getIconResId(), imi.getServiceInfo().applicationInfo); } else { try { return pm.getApplicationInfo(imiPackageName, 0).loadIcon(pm); } catch (PackageManager.NameNotFoundException e) { Log.w(TAG, "IME can't be found: " + imiPackageName); } } } return null; } private static boolean contains(String[] hay, String needle) { for (String element : hay) { if (element.equals(needle)) return true; } return false; } public boolean isShortcutImeEnabled() { if (mShortcutInputMethodInfo == null) { return false; } if (mShortcutSubtype == null) { return true; } // For compatibility, if the shortcut subtype is dummy, we assume the shortcut IME // (built-in voice dummy subtype) is available. if (!mShortcutSubtype.hasOriginalObject()) { return true; } final boolean allowsImplicitlySelectedSubtypes = true; for (final InputMethodSubtypeCompatWrapper enabledSubtype : mImm.getEnabledInputMethodSubtypeList( mShortcutInputMethodInfo, allowsImplicitlySelectedSubtypes)) { if (enabledSubtype.equals(mShortcutSubtype)) { return true; } } return false; } public boolean isShortcutImeReady() { if (mShortcutInputMethodInfo == null) return false; if (mShortcutSubtype == null) return true; if (contains(mShortcutSubtype.getExtraValue().split(","), SUBTYPE_EXTRAVALUE_REQUIRE_NETWORK_CONNECTIVITY)) { return mIsNetworkConnected; } return true; } public void onNetworkStateChanged(Intent intent) { final boolean noConnection = intent.getBooleanExtra( ConnectivityManager.EXTRA_NO_CONNECTIVITY, false); mIsNetworkConnected = !noConnection; final KeyboardSwitcher switcher = KeyboardSwitcher.getInstance(); final LatinKeyboard keyboard = switcher.getLatinKeyboard(); if (keyboard != null) { keyboard.updateShortcutKey(isShortcutImeReady(), switcher.getKeyboardView()); } } ////////////////////////////////// // Language Switching functions // ////////////////////////////////// public int getEnabledKeyboardLocaleCount() { return mEnabledKeyboardSubtypesOfCurrentInputMethod.size(); } public boolean needsToDisplayLanguage(Locale keyboardLocale) { if (!keyboardLocale.equals(mInputLocale)) { return false; } return mNeedsToDisplayLanguage; } public Locale getInputLocale() { return mInputLocale; } public String getInputLocaleStr() { return mInputLocaleStr; } public String[] getEnabledLanguages() { int enabledLanguageCount = mEnabledLanguagesOfCurrentInputMethod.size(); // Workaround for explicitly specifying the voice language if (enabledLanguageCount == 1) { mEnabledLanguagesOfCurrentInputMethod.add(mEnabledLanguagesOfCurrentInputMethod .get(0)); ++enabledLanguageCount; } return mEnabledLanguagesOfCurrentInputMethod.toArray(new String[enabledLanguageCount]); } public Locale getSystemLocale() { return mSystemLocale; } public boolean isSystemLanguageSameAsInputLanguage() { return mIsSystemLanguageSameAsInputLanguage; } public void onConfigurationChanged(Configuration conf) { final Locale systemLocale = conf.locale; // If system configuration was changed, update all parameters. if (!TextUtils.equals(systemLocale.toString(), mSystemLocale.toString())) { updateAllParameters(); } } public boolean isKeyboardMode() { return KEYBOARD_MODE.equals(getCurrentSubtypeMode()); } /////////////////////////// // Voice Input functions // /////////////////////////// public boolean setVoiceInputWrapper(VoiceProxy.VoiceInputWrapper vi) { if (mVoiceInputWrapper == null && vi != null) { mVoiceInputWrapper = vi; if (isVoiceMode()) { if (DBG) { Log.d(TAG, "Set and call voice input.: " + getInputLocaleStr()); } triggerVoiceIME(); return true; } } return false; } public boolean isVoiceMode() { return null == mCurrentSubtype ? false : VOICE_MODE.equals(getCurrentSubtypeMode()); } public boolean isDummyVoiceMode() { return mCurrentSubtype != null && mCurrentSubtype.getOriginalObject() == null && VOICE_MODE.equals(getCurrentSubtypeMode()); } private void triggerVoiceIME() { if (!mService.isInputViewShown()) return; VoiceProxy.getInstance().startListening(false, KeyboardSwitcher.getInstance().getKeyboardView().getWindowToken()); } public String getInputLanguageName() { return Utils.getDisplayLanguage(getInputLocale()); } ///////////////////////////// // Other utility functions // ///////////////////////////// public String getCurrentSubtypeExtraValue() { // If null, return what an empty ExtraValue would return : the empty string. return null != mCurrentSubtype ? mCurrentSubtype.getExtraValue() : ""; } public boolean currentSubtypeContainsExtraValueKey(String key) { // If null, return what an empty ExtraValue would return : false. return null != mCurrentSubtype ? mCurrentSubtype.containsExtraValueKey(key) : false; } public String getCurrentSubtypeExtraValueOf(String key) { // If null, return what an empty ExtraValue would return : null. return null != mCurrentSubtype ? mCurrentSubtype.getExtraValueOf(key) : null; } public String getCurrentSubtypeMode() { return null != mCurrentSubtype ? mCurrentSubtype.getMode() : KEYBOARD_MODE; } public static boolean isVoiceSupported(Context context, String locale) { // Get the current list of supported locales and check the current locale against that // list. We cache this value so as not to check it every time the user starts a voice // input. Because this method is called by onStartInputView, this should mean that as // long as the locale doesn't change while the user is keeping the IME open, the // value should never be stale. String supportedLocalesString = VoiceProxy.getSupportedLocalesString( context.getContentResolver()); List<String> voiceInputSupportedLocales = Arrays.asList( supportedLocalesString.split("\\s+")); return voiceInputSupportedLocales.contains(locale); } }