1 /*
2  * Copyright (C) 2014 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.common.Constants.Subtype.KEYBOARD_MODE;
20 
21 import android.os.Build;
22 import android.util.Log;
23 import android.view.inputmethod.InputMethodSubtype;
24 
25 import com.android.inputmethod.compat.BuildCompatUtils;
26 import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils;
27 import com.android.inputmethod.latin.common.Constants;
28 import com.android.inputmethod.latin.common.LocaleUtils;
29 import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
30 
31 import java.util.HashMap;
32 import java.util.Locale;
33 
34 import javax.annotation.Nonnull;
35 import javax.annotation.Nullable;
36 
37 /**
38  * Enrichment class for InputMethodSubtype to enable concurrent multi-lingual input.
39  *
40  * Right now, this returns the extra value of its primary subtype.
41  */
42 // non final for easy mocking.
43 public class RichInputMethodSubtype {
44     private static final String TAG = RichInputMethodSubtype.class.getSimpleName();
45 
46     private static final HashMap<Locale, Locale> sLocaleMap = initializeLocaleMap();
initializeLocaleMap()47     private static final HashMap<Locale, Locale> initializeLocaleMap() {
48         final HashMap<Locale, Locale> map = new HashMap<>();
49         if (BuildCompatUtils.EFFECTIVE_SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
50             // Locale#forLanguageTag is available on API Level 21+.
51             // TODO: Remove this workaround once when we become able to deal with "sr-Latn".
52             map.put(Locale.forLanguageTag("sr-Latn"), new Locale("sr_ZZ"));
53         }
54         return map;
55     }
56 
57     @Nonnull
58     private final InputMethodSubtype mSubtype;
59     @Nonnull
60     private final Locale mLocale;
61     @Nonnull
62     private final Locale mOriginalLocale;
63 
RichInputMethodSubtype(@onnull final InputMethodSubtype subtype)64     public RichInputMethodSubtype(@Nonnull final InputMethodSubtype subtype) {
65         mSubtype = subtype;
66         mOriginalLocale = InputMethodSubtypeCompatUtils.getLocaleObject(mSubtype);
67         final Locale mappedLocale = sLocaleMap.get(mOriginalLocale);
68         mLocale = mappedLocale != null ? mappedLocale : mOriginalLocale;
69     }
70 
71     // Extra values are determined by the primary subtype. This is probably right, but
72     // we may have to revisit this later.
getExtraValueOf(@onnull final String key)73     public String getExtraValueOf(@Nonnull final String key) {
74         return mSubtype.getExtraValueOf(key);
75     }
76 
77     // The mode is also determined by the primary subtype.
getMode()78     public String getMode() {
79         return mSubtype.getMode();
80     }
81 
isNoLanguage()82     public boolean isNoLanguage() {
83         return SubtypeLocaleUtils.NO_LANGUAGE.equals(mSubtype.getLocale());
84     }
85 
getNameForLogging()86     public String getNameForLogging() {
87         return toString();
88     }
89 
90     // InputMethodSubtype's display name for spacebar text in its locale.
91     //        isAdditionalSubtype (T=true, F=false)
92     // locale layout  |  Middle      Full
93     // ------ ------- - --------- ----------------------
94     //  en_US qwerty  F  English   English (US)           exception
95     //  en_GB qwerty  F  English   English (UK)           exception
96     //  es_US spanish F  Español   Español (EE.UU.)       exception
97     //  fr    azerty  F  Français  Français
98     //  fr_CA qwerty  F  Français  Français (Canada)
99     //  fr_CH swiss   F  Français  Français (Suisse)
100     //  de    qwertz  F  Deutsch   Deutsch
101     //  de_CH swiss   T  Deutsch   Deutsch (Schweiz)
102     //  zz    qwerty  F  QWERTY    QWERTY
103     //  fr    qwertz  T  Français  Français
104     //  de    qwerty  T  Deutsch   Deutsch
105     //  en_US azerty  T  English   English (US)
106     //  zz    azerty  T  AZERTY    AZERTY
107     // Get the RichInputMethodSubtype's full display name in its locale.
108     @Nonnull
getFullDisplayName()109     public String getFullDisplayName() {
110         if (isNoLanguage()) {
111             return SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(mSubtype);
112         }
113         return SubtypeLocaleUtils.getSubtypeLocaleDisplayName(mSubtype.getLocale());
114     }
115 
116     // Get the RichInputMethodSubtype's middle display name in its locale.
117     @Nonnull
getMiddleDisplayName()118     public String getMiddleDisplayName() {
119         if (isNoLanguage()) {
120             return SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(mSubtype);
121         }
122         return SubtypeLocaleUtils.getSubtypeLanguageDisplayName(mSubtype.getLocale());
123     }
124 
125     @Override
equals(final Object o)126     public boolean equals(final Object o) {
127         if (!(o instanceof RichInputMethodSubtype)) {
128             return false;
129         }
130         final RichInputMethodSubtype other = (RichInputMethodSubtype)o;
131         return mSubtype.equals(other.mSubtype) && mLocale.equals(other.mLocale);
132     }
133 
134     @Override
hashCode()135     public int hashCode() {
136         return mSubtype.hashCode() + mLocale.hashCode();
137     }
138 
139     @Override
toString()140     public String toString() {
141         return "Multi-lingual subtype: " + mSubtype + ", " + mLocale;
142     }
143 
144     @Nonnull
getLocale()145     public Locale getLocale() {
146         return mLocale;
147     }
148 
149     @Nonnull
getOriginalLocale()150     public Locale getOriginalLocale() {
151         return mOriginalLocale;
152     }
153 
isRtlSubtype()154     public boolean isRtlSubtype() {
155         // The subtype is considered RTL if the language of the main subtype is RTL.
156         return LocaleUtils.isRtlLanguage(mLocale);
157     }
158 
159     // TODO: remove this method
160     @Nonnull
getRawSubtype()161     public InputMethodSubtype getRawSubtype() { return mSubtype; }
162 
163     @Nonnull
getKeyboardLayoutSetName()164     public String getKeyboardLayoutSetName() {
165         return SubtypeLocaleUtils.getKeyboardLayoutSetName(mSubtype);
166     }
167 
getRichInputMethodSubtype( @ullable final InputMethodSubtype subtype)168     public static RichInputMethodSubtype getRichInputMethodSubtype(
169             @Nullable final InputMethodSubtype subtype) {
170         if (subtype == null) {
171             return getNoLanguageSubtype();
172         } else {
173             return new RichInputMethodSubtype(subtype);
174         }
175     }
176 
177     // Dummy no language QWERTY subtype. See {@link R.xml.method}.
178     private static final int SUBTYPE_ID_OF_DUMMY_NO_LANGUAGE_SUBTYPE = 0xdde0bfd3;
179     private static final String EXTRA_VALUE_OF_DUMMY_NO_LANGUAGE_SUBTYPE =
180             "KeyboardLayoutSet=" + SubtypeLocaleUtils.QWERTY
181             + "," + Constants.Subtype.ExtraValue.ASCII_CAPABLE
182             + "," + Constants.Subtype.ExtraValue.ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE
183             + "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE;
184     @Nonnull
185     private static final RichInputMethodSubtype DUMMY_NO_LANGUAGE_SUBTYPE =
186             new RichInputMethodSubtype(InputMethodSubtypeCompatUtils.newInputMethodSubtype(
187                     R.string.subtype_no_language_qwerty, R.drawable.ic_ime_switcher_dark,
188                     SubtypeLocaleUtils.NO_LANGUAGE, KEYBOARD_MODE,
189                     EXTRA_VALUE_OF_DUMMY_NO_LANGUAGE_SUBTYPE,
190                     false /* isAuxiliary */, false /* overridesImplicitlyEnabledSubtype */,
191                     SUBTYPE_ID_OF_DUMMY_NO_LANGUAGE_SUBTYPE));
192     // Caveat: We probably should remove this when we add an Emoji subtype in {@link R.xml.method}.
193     // Dummy Emoji subtype. See {@link R.xml.method}.
194     private static final int SUBTYPE_ID_OF_DUMMY_EMOJI_SUBTYPE = 0xd78b2ed0;
195     private static final String EXTRA_VALUE_OF_DUMMY_EMOJI_SUBTYPE =
196             "KeyboardLayoutSet=" + SubtypeLocaleUtils.EMOJI
197             + "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE;
198     @Nonnull
199     private static final RichInputMethodSubtype DUMMY_EMOJI_SUBTYPE = new RichInputMethodSubtype(
200             InputMethodSubtypeCompatUtils.newInputMethodSubtype(
201                     R.string.subtype_emoji, R.drawable.ic_ime_switcher_dark,
202                     SubtypeLocaleUtils.NO_LANGUAGE, KEYBOARD_MODE,
203                     EXTRA_VALUE_OF_DUMMY_EMOJI_SUBTYPE,
204                     false /* isAuxiliary */, false /* overridesImplicitlyEnabledSubtype */,
205                     SUBTYPE_ID_OF_DUMMY_EMOJI_SUBTYPE));
206     private static RichInputMethodSubtype sNoLanguageSubtype;
207     private static RichInputMethodSubtype sEmojiSubtype;
208 
209     @Nonnull
getNoLanguageSubtype()210     public static RichInputMethodSubtype getNoLanguageSubtype() {
211         RichInputMethodSubtype noLanguageSubtype = sNoLanguageSubtype;
212         if (noLanguageSubtype == null) {
213             final InputMethodSubtype rawNoLanguageSubtype = RichInputMethodManager.getInstance()
214                     .findSubtypeByLocaleAndKeyboardLayoutSet(
215                             SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.QWERTY);
216             if (rawNoLanguageSubtype != null) {
217                 noLanguageSubtype = new RichInputMethodSubtype(rawNoLanguageSubtype);
218             }
219         }
220         if (noLanguageSubtype != null) {
221             sNoLanguageSubtype = noLanguageSubtype;
222             return noLanguageSubtype;
223         }
224         Log.w(TAG, "Can't find any language with QWERTY subtype");
225         Log.w(TAG, "No input method subtype found; returning dummy subtype: "
226                 + DUMMY_NO_LANGUAGE_SUBTYPE);
227         return DUMMY_NO_LANGUAGE_SUBTYPE;
228     }
229 
230     @Nonnull
getEmojiSubtype()231     public static RichInputMethodSubtype getEmojiSubtype() {
232         RichInputMethodSubtype emojiSubtype = sEmojiSubtype;
233         if (emojiSubtype == null) {
234             final InputMethodSubtype rawEmojiSubtype = RichInputMethodManager.getInstance()
235                     .findSubtypeByLocaleAndKeyboardLayoutSet(
236                             SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.EMOJI);
237             if (rawEmojiSubtype != null) {
238                 emojiSubtype = new RichInputMethodSubtype(rawEmojiSubtype);
239             }
240         }
241         if (emojiSubtype != null) {
242             sEmojiSubtype = emojiSubtype;
243             return emojiSubtype;
244         }
245         Log.w(TAG, "Can't find emoji subtype");
246         Log.w(TAG, "No input method subtype found; returning dummy subtype: "
247                 + DUMMY_EMOJI_SUBTYPE);
248         return DUMMY_EMOJI_SUBTYPE;
249     }
250 }
251