1/*
2 * Copyright (C) 2010 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.ExtraValue.REQ_NETWORK_CONNECTIVITY;
20
21import android.content.Context;
22import android.content.Intent;
23import android.content.res.Resources;
24import android.inputmethodservice.InputMethodService;
25import android.net.ConnectivityManager;
26import android.net.NetworkInfo;
27import android.os.AsyncTask;
28import android.os.IBinder;
29import android.util.Log;
30import android.view.inputmethod.InputMethodInfo;
31import android.view.inputmethod.InputMethodManager;
32import android.view.inputmethod.InputMethodSubtype;
33
34import com.android.inputmethod.annotations.UsedForTesting;
35import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils;
36import com.android.inputmethod.keyboard.KeyboardSwitcher;
37import com.android.inputmethod.keyboard.internal.LanguageOnSpacebarHelper;
38import com.android.inputmethod.latin.define.DebugFlags;
39import com.android.inputmethod.latin.utils.LocaleUtils;
40import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
41
42import java.util.HashSet;
43import java.util.List;
44import java.util.Locale;
45import java.util.Map;
46import java.util.Set;
47
48public final class SubtypeSwitcher {
49    private static boolean DBG = DebugFlags.DEBUG_ENABLED;
50    private static final String TAG = SubtypeSwitcher.class.getSimpleName();
51
52    private static final SubtypeSwitcher sInstance = new SubtypeSwitcher();
53
54    private /* final */ RichInputMethodManager mRichImm;
55    private /* final */ Resources mResources;
56
57    private final LanguageOnSpacebarHelper mLanguageOnSpacebarHelper =
58            new LanguageOnSpacebarHelper();
59    private InputMethodInfo mShortcutInputMethodInfo;
60    private InputMethodSubtype mShortcutSubtype;
61    private InputMethodSubtype mNoLanguageSubtype;
62    private InputMethodSubtype mEmojiSubtype;
63    private boolean mIsNetworkConnected;
64
65    private static final String KEYBOARD_MODE = "keyboard";
66    // Dummy no language QWERTY subtype. See {@link R.xml.method}.
67    private static final int SUBTYPE_ID_OF_DUMMY_NO_LANGUAGE_SUBTYPE = 0xdde0bfd3;
68    private static final String EXTRA_VALUE_OF_DUMMY_NO_LANGUAGE_SUBTYPE =
69            "KeyboardLayoutSet=" + SubtypeLocaleUtils.QWERTY
70            + "," + Constants.Subtype.ExtraValue.ASCII_CAPABLE
71            + "," + Constants.Subtype.ExtraValue.ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE
72            + "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE;
73    private static final InputMethodSubtype DUMMY_NO_LANGUAGE_SUBTYPE =
74            InputMethodSubtypeCompatUtils.newInputMethodSubtype(
75                    R.string.subtype_no_language_qwerty, R.drawable.ic_ime_switcher_dark,
76                    SubtypeLocaleUtils.NO_LANGUAGE, KEYBOARD_MODE,
77                    EXTRA_VALUE_OF_DUMMY_NO_LANGUAGE_SUBTYPE,
78                    false /* isAuxiliary */, false /* overridesImplicitlyEnabledSubtype */,
79                    SUBTYPE_ID_OF_DUMMY_NO_LANGUAGE_SUBTYPE);
80    // Caveat: We probably should remove this when we add an Emoji subtype in {@link R.xml.method}.
81    // Dummy Emoji subtype. See {@link R.xml.method}.
82    private static final int SUBTYPE_ID_OF_DUMMY_EMOJI_SUBTYPE = 0xd78b2ed0;
83    private static final String EXTRA_VALUE_OF_DUMMY_EMOJI_SUBTYPE =
84            "KeyboardLayoutSet=" + SubtypeLocaleUtils.EMOJI
85            + "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE;
86    private static final InputMethodSubtype DUMMY_EMOJI_SUBTYPE =
87            InputMethodSubtypeCompatUtils.newInputMethodSubtype(
88                    R.string.subtype_emoji, R.drawable.ic_ime_switcher_dark,
89                    SubtypeLocaleUtils.NO_LANGUAGE, KEYBOARD_MODE,
90                    EXTRA_VALUE_OF_DUMMY_EMOJI_SUBTYPE,
91                    false /* isAuxiliary */, false /* overridesImplicitlyEnabledSubtype */,
92                    SUBTYPE_ID_OF_DUMMY_EMOJI_SUBTYPE);
93
94    public static SubtypeSwitcher getInstance() {
95        return sInstance;
96    }
97
98    public static void init(final Context context) {
99        SubtypeLocaleUtils.init(context);
100        RichInputMethodManager.init(context);
101        sInstance.initialize(context);
102    }
103
104    private SubtypeSwitcher() {
105        // Intentional empty constructor for singleton.
106    }
107
108    private void initialize(final Context context) {
109        if (mResources != null) {
110            return;
111        }
112        mResources = context.getResources();
113        mRichImm = RichInputMethodManager.getInstance();
114        ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(
115                Context.CONNECTIVITY_SERVICE);
116
117        final NetworkInfo info = connectivityManager.getActiveNetworkInfo();
118        mIsNetworkConnected = (info != null && info.isConnected());
119
120        onSubtypeChanged(getCurrentSubtype());
121        updateParametersOnStartInputView();
122    }
123
124    /**
125     * Update parameters which are changed outside LatinIME. This parameters affect UI so that they
126     * should be updated every time onStartInputView is called.
127     */
128    public void updateParametersOnStartInputView() {
129        final List<InputMethodSubtype> enabledSubtypesOfThisIme =
130                mRichImm.getMyEnabledInputMethodSubtypeList(true);
131        mLanguageOnSpacebarHelper.updateEnabledSubtypes(enabledSubtypesOfThisIme);
132        updateShortcutIME();
133    }
134
135    private void updateShortcutIME() {
136        if (DBG) {
137            Log.d(TAG, "Update shortcut IME from : "
138                    + (mShortcutInputMethodInfo == null
139                            ? "<null>" : mShortcutInputMethodInfo.getId()) + ", "
140                    + (mShortcutSubtype == null ? "<null>" : (
141                            mShortcutSubtype.getLocale() + ", " + mShortcutSubtype.getMode())));
142        }
143        // TODO: Update an icon for shortcut IME
144        final Map<InputMethodInfo, List<InputMethodSubtype>> shortcuts =
145                mRichImm.getInputMethodManager().getShortcutInputMethodsAndSubtypes();
146        mShortcutInputMethodInfo = null;
147        mShortcutSubtype = null;
148        for (final InputMethodInfo imi : shortcuts.keySet()) {
149            final List<InputMethodSubtype> subtypes = shortcuts.get(imi);
150            // TODO: Returns the first found IMI for now. Should handle all shortcuts as
151            // appropriate.
152            mShortcutInputMethodInfo = imi;
153            // TODO: Pick up the first found subtype for now. Should handle all subtypes
154            // as appropriate.
155            mShortcutSubtype = subtypes.size() > 0 ? subtypes.get(0) : null;
156            break;
157        }
158        if (DBG) {
159            Log.d(TAG, "Update shortcut IME to : "
160                    + (mShortcutInputMethodInfo == null
161                            ? "<null>" : mShortcutInputMethodInfo.getId()) + ", "
162                    + (mShortcutSubtype == null ? "<null>" : (
163                            mShortcutSubtype.getLocale() + ", " + mShortcutSubtype.getMode())));
164        }
165    }
166
167    // Update the current subtype. LatinIME.onCurrentInputMethodSubtypeChanged calls this function.
168    public void onSubtypeChanged(final InputMethodSubtype newSubtype) {
169        if (DBG) {
170            Log.w(TAG, "onSubtypeChanged: "
171                    + SubtypeLocaleUtils.getSubtypeNameForLogging(newSubtype));
172        }
173
174        final Locale newLocale = SubtypeLocaleUtils.getSubtypeLocale(newSubtype);
175        final Locale systemLocale = mResources.getConfiguration().locale;
176        final boolean sameLocale = systemLocale.equals(newLocale);
177        final boolean sameLanguage = systemLocale.getLanguage().equals(newLocale.getLanguage());
178        final boolean implicitlyEnabled =
179                mRichImm.checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled(newSubtype);
180        mLanguageOnSpacebarHelper.updateIsSystemLanguageSameAsInputLanguage(
181                sameLocale || (sameLanguage && implicitlyEnabled));
182
183        updateShortcutIME();
184    }
185
186    ////////////////////////////
187    // Shortcut IME functions //
188    ////////////////////////////
189
190    public void switchToShortcutIME(final InputMethodService context) {
191        if (mShortcutInputMethodInfo == null) {
192            return;
193        }
194
195        final String imiId = mShortcutInputMethodInfo.getId();
196        switchToTargetIME(imiId, mShortcutSubtype, context);
197    }
198
199    private void switchToTargetIME(final String imiId, final InputMethodSubtype subtype,
200            final InputMethodService context) {
201        final IBinder token = context.getWindow().getWindow().getAttributes().token;
202        if (token == null) {
203            return;
204        }
205        final InputMethodManager imm = mRichImm.getInputMethodManager();
206        new AsyncTask<Void, Void, Void>() {
207            @Override
208            protected Void doInBackground(Void... params) {
209                imm.setInputMethodAndSubtype(token, imiId, subtype);
210                return null;
211            }
212        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
213    }
214
215    public boolean isShortcutImeEnabled() {
216        updateShortcutIME();
217        if (mShortcutInputMethodInfo == null) {
218            return false;
219        }
220        if (mShortcutSubtype == null) {
221            return true;
222        }
223        return mRichImm.checkIfSubtypeBelongsToImeAndEnabled(
224                mShortcutInputMethodInfo, mShortcutSubtype);
225    }
226
227    public boolean isShortcutImeReady() {
228        updateShortcutIME();
229        if (mShortcutInputMethodInfo == null) {
230            return false;
231        }
232        if (mShortcutSubtype == null) {
233            return true;
234        }
235        if (mShortcutSubtype.containsExtraValueKey(REQ_NETWORK_CONNECTIVITY)) {
236            return mIsNetworkConnected;
237        }
238        return true;
239    }
240
241    public void onNetworkStateChanged(final Intent intent) {
242        final boolean noConnection = intent.getBooleanExtra(
243                ConnectivityManager.EXTRA_NO_CONNECTIVITY, false);
244        mIsNetworkConnected = !noConnection;
245
246        KeyboardSwitcher.getInstance().onNetworkStateChanged();
247    }
248
249    //////////////////////////////////
250    // Subtype Switching functions //
251    //////////////////////////////////
252
253    public int getLanguageOnSpacebarFormatType(final InputMethodSubtype subtype) {
254        return mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(subtype);
255    }
256
257    public boolean isSystemLocaleSameAsLocaleOfAllEnabledSubtypesOfEnabledImes() {
258        final Locale systemLocale = mResources.getConfiguration().locale;
259        final Set<InputMethodSubtype> enabledSubtypesOfEnabledImes = new HashSet<>();
260        final InputMethodManager inputMethodManager = mRichImm.getInputMethodManager();
261        final List<InputMethodInfo> enabledInputMethodInfoList =
262                inputMethodManager.getEnabledInputMethodList();
263        for (final InputMethodInfo info : enabledInputMethodInfoList) {
264            final List<InputMethodSubtype> enabledSubtypes =
265                    inputMethodManager.getEnabledInputMethodSubtypeList(
266                            info, true /* allowsImplicitlySelectedSubtypes */);
267            if (enabledSubtypes.isEmpty()) {
268                // An IME with no subtypes is found.
269                return false;
270            }
271            enabledSubtypesOfEnabledImes.addAll(enabledSubtypes);
272        }
273        for (final InputMethodSubtype subtype : enabledSubtypesOfEnabledImes) {
274            if (!subtype.isAuxiliary() && !subtype.getLocale().isEmpty()
275                    && !systemLocale.equals(SubtypeLocaleUtils.getSubtypeLocale(subtype))) {
276                return false;
277            }
278        }
279        return true;
280    }
281
282    private static InputMethodSubtype sForcedSubtypeForTesting = null;
283    @UsedForTesting
284    void forceSubtype(final InputMethodSubtype subtype) {
285        sForcedSubtypeForTesting = subtype;
286    }
287
288    public Locale getCurrentSubtypeLocale() {
289        if (null != sForcedSubtypeForTesting) {
290            return LocaleUtils.constructLocaleFromString(sForcedSubtypeForTesting.getLocale());
291        }
292        return SubtypeLocaleUtils.getSubtypeLocale(getCurrentSubtype());
293    }
294
295    public InputMethodSubtype getCurrentSubtype() {
296        if (null != sForcedSubtypeForTesting) {
297            return sForcedSubtypeForTesting;
298        }
299        return mRichImm.getCurrentInputMethodSubtype(getNoLanguageSubtype());
300    }
301
302    public InputMethodSubtype getNoLanguageSubtype() {
303        if (mNoLanguageSubtype == null) {
304            mNoLanguageSubtype = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
305                    SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.QWERTY);
306        }
307        if (mNoLanguageSubtype != null) {
308            return mNoLanguageSubtype;
309        }
310        Log.w(TAG, "Can't find any language with QWERTY subtype");
311        Log.w(TAG, "No input method subtype found; returning dummy subtype: "
312                + DUMMY_NO_LANGUAGE_SUBTYPE);
313        return DUMMY_NO_LANGUAGE_SUBTYPE;
314    }
315
316    public InputMethodSubtype getEmojiSubtype() {
317        if (mEmojiSubtype == null) {
318            mEmojiSubtype = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
319                    SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.EMOJI);
320        }
321        if (mEmojiSubtype != null) {
322            return mEmojiSubtype;
323        }
324        Log.w(TAG, "Can't find emoji subtype");
325        Log.w(TAG, "No input method subtype found; returning dummy subtype: "
326                + DUMMY_EMOJI_SUBTYPE);
327        return DUMMY_EMOJI_SUBTYPE;
328    }
329
330    public String getCombiningRulesExtraValueOfCurrentSubtype() {
331        return SubtypeLocaleUtils.getCombiningRulesExtraValue(getCurrentSubtype());
332    }
333}
334