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.keyboard.KeyboardSwitcher;
36import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
37
38import java.util.List;
39import java.util.Locale;
40import java.util.Map;
41
42public final class SubtypeSwitcher {
43    private static boolean DBG = LatinImeLogger.sDBG;
44    private static final String TAG = SubtypeSwitcher.class.getSimpleName();
45
46    private static final SubtypeSwitcher sInstance = new SubtypeSwitcher();
47
48    private /* final */ RichInputMethodManager mRichImm;
49    private /* final */ Resources mResources;
50    private /* final */ ConnectivityManager mConnectivityManager;
51
52    private final NeedsToDisplayLanguage mNeedsToDisplayLanguage = new NeedsToDisplayLanguage();
53    private InputMethodInfo mShortcutInputMethodInfo;
54    private InputMethodSubtype mShortcutSubtype;
55    private InputMethodSubtype mNoLanguageSubtype;
56    private InputMethodSubtype mEmojiSubtype;
57    private boolean mIsNetworkConnected;
58
59    // Dummy no language QWERTY subtype. See {@link R.xml.method}.
60    private static final InputMethodSubtype DUMMY_NO_LANGUAGE_SUBTYPE = new InputMethodSubtype(
61            R.string.subtype_no_language_qwerty, R.drawable.ic_ime_switcher_dark,
62            SubtypeLocaleUtils.NO_LANGUAGE, "keyboard", "KeyboardLayoutSet="
63                    + SubtypeLocaleUtils.QWERTY
64                    + "," + Constants.Subtype.ExtraValue.ASCII_CAPABLE
65                    + ",EnabledWhenDefaultIsNotAsciiCapable,"
66                    + Constants.Subtype.ExtraValue.EMOJI_CAPABLE,
67            false /* isAuxiliary */, false /* overridesImplicitlyEnabledSubtype */);
68    // Caveat: We probably should remove this when we add an Emoji subtype in {@link R.xml.method}.
69    // Dummy Emoji subtype. See {@link R.xml.method}.
70    private static final InputMethodSubtype DUMMY_EMOJI_SUBTYPE = new InputMethodSubtype(
71            R.string.subtype_emoji, R.drawable.ic_ime_switcher_dark,
72            SubtypeLocaleUtils.NO_LANGUAGE, "keyboard", "KeyboardLayoutSet="
73                    + SubtypeLocaleUtils.EMOJI + ","
74                    + Constants.Subtype.ExtraValue.EMOJI_CAPABLE,
75            false /* isAuxiliary */, false /* overridesImplicitlyEnabledSubtype */);
76
77    static final class NeedsToDisplayLanguage {
78        private int mEnabledSubtypeCount;
79        private boolean mIsSystemLanguageSameAsInputLanguage;
80
81        public boolean getValue() {
82            return mEnabledSubtypeCount >= 2 || !mIsSystemLanguageSameAsInputLanguage;
83        }
84
85        public void updateEnabledSubtypeCount(final int count) {
86            mEnabledSubtypeCount = count;
87        }
88
89        public void updateIsSystemLanguageSameAsInputLanguage(final boolean isSame) {
90            mIsSystemLanguageSameAsInputLanguage = isSame;
91        }
92    }
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        mConnectivityManager = (ConnectivityManager) context.getSystemService(
115                Context.CONNECTIVITY_SERVICE);
116
117        final NetworkInfo info = mConnectivityManager.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        mNeedsToDisplayLanguage.updateEnabledSubtypeCount(enabledSubtypesOfThisIme.size());
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        mNeedsToDisplayLanguage.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        if (mShortcutInputMethodInfo == null) {
217            return false;
218        }
219        if (mShortcutSubtype == null) {
220            return true;
221        }
222        return mRichImm.checkIfSubtypeBelongsToImeAndEnabled(
223                mShortcutInputMethodInfo, mShortcutSubtype);
224    }
225
226    public boolean isShortcutImeReady() {
227        if (mShortcutInputMethodInfo == null)
228            return false;
229        if (mShortcutSubtype == null)
230            return true;
231        if (mShortcutSubtype.containsExtraValueKey(REQ_NETWORK_CONNECTIVITY)) {
232            return mIsNetworkConnected;
233        }
234        return true;
235    }
236
237    public void onNetworkStateChanged(final Intent intent) {
238        final boolean noConnection = intent.getBooleanExtra(
239                ConnectivityManager.EXTRA_NO_CONNECTIVITY, false);
240        mIsNetworkConnected = !noConnection;
241
242        KeyboardSwitcher.getInstance().onNetworkStateChanged();
243    }
244
245    //////////////////////////////////
246    // Subtype Switching functions //
247    //////////////////////////////////
248
249    public boolean needsToDisplayLanguage(final Locale keyboardLocale) {
250        if (keyboardLocale.toString().equals(SubtypeLocaleUtils.NO_LANGUAGE)) {
251            return true;
252        }
253        if (!keyboardLocale.equals(getCurrentSubtypeLocale())) {
254            return false;
255        }
256        return mNeedsToDisplayLanguage.getValue();
257    }
258
259    private static Locale sForcedLocaleForTesting = null;
260    @UsedForTesting
261    void forceLocale(final Locale locale) {
262        sForcedLocaleForTesting = locale;
263    }
264
265    public Locale getCurrentSubtypeLocale() {
266        if (null != sForcedLocaleForTesting) return sForcedLocaleForTesting;
267        return SubtypeLocaleUtils.getSubtypeLocale(getCurrentSubtype());
268    }
269
270    public InputMethodSubtype getCurrentSubtype() {
271        return mRichImm.getCurrentInputMethodSubtype(getNoLanguageSubtype());
272    }
273
274    public InputMethodSubtype getNoLanguageSubtype() {
275        if (mNoLanguageSubtype == null) {
276            mNoLanguageSubtype = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
277                    SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.QWERTY);
278        }
279        if (mNoLanguageSubtype != null) {
280            return mNoLanguageSubtype;
281        }
282        Log.w(TAG, "Can't find no lanugage with QWERTY subtype");
283        Log.w(TAG, "No input method subtype found; return dummy subtype: "
284                + DUMMY_NO_LANGUAGE_SUBTYPE);
285        return DUMMY_NO_LANGUAGE_SUBTYPE;
286    }
287
288    public InputMethodSubtype getEmojiSubtype() {
289        if (mEmojiSubtype == null) {
290            mEmojiSubtype = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
291                    SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.EMOJI);
292        }
293        if (mEmojiSubtype != null) {
294            return mEmojiSubtype;
295        }
296        Log.w(TAG, "Can't find Emoji subtype");
297        Log.w(TAG, "No input method subtype found; return dummy subtype: " + DUMMY_EMOJI_SUBTYPE);
298        return DUMMY_EMOJI_SUBTYPE;
299    }
300}
301