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.settings;
18 
19 import android.app.AlertDialog;
20 import android.app.Dialog;
21 import android.content.Context;
22 import android.content.DialogInterface;
23 import android.os.Parcel;
24 import android.os.Parcelable;
25 import android.preference.DialogPreference;
26 import android.preference.Preference;
27 import android.util.Log;
28 import android.view.View;
29 import android.view.inputmethod.InputMethodInfo;
30 import android.view.inputmethod.InputMethodSubtype;
31 import android.widget.ArrayAdapter;
32 import android.widget.Spinner;
33 import android.widget.SpinnerAdapter;
34 
35 import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils;
36 import com.android.inputmethod.compat.ViewCompatUtils;
37 import com.android.inputmethod.latin.R;
38 import com.android.inputmethod.latin.RichInputMethodManager;
39 import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils;
40 import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
41 
42 import java.util.TreeSet;
43 
44 final class CustomInputStylePreference extends DialogPreference
45         implements DialogInterface.OnCancelListener {
46     private static final boolean DEBUG_SUBTYPE_ID = false;
47 
48     interface Listener {
onRemoveCustomInputStyle(CustomInputStylePreference stylePref)49         public void onRemoveCustomInputStyle(CustomInputStylePreference stylePref);
onSaveCustomInputStyle(CustomInputStylePreference stylePref)50         public void onSaveCustomInputStyle(CustomInputStylePreference stylePref);
onAddCustomInputStyle(CustomInputStylePreference stylePref)51         public void onAddCustomInputStyle(CustomInputStylePreference stylePref);
getSubtypeLocaleAdapter()52         public SubtypeLocaleAdapter getSubtypeLocaleAdapter();
getKeyboardLayoutSetAdapter()53         public KeyboardLayoutSetAdapter getKeyboardLayoutSetAdapter();
54     }
55 
56     private static final String KEY_PREFIX = "subtype_pref_";
57     private static final String KEY_NEW_SUBTYPE = KEY_PREFIX + "new";
58 
59     private InputMethodSubtype mSubtype;
60     private InputMethodSubtype mPreviousSubtype;
61 
62     private final Listener mProxy;
63     private Spinner mSubtypeLocaleSpinner;
64     private Spinner mKeyboardLayoutSetSpinner;
65 
newIncompleteSubtypePreference( final Context context, final Listener proxy)66     public static CustomInputStylePreference newIncompleteSubtypePreference(
67             final Context context, final Listener proxy) {
68         return new CustomInputStylePreference(context, null, proxy);
69     }
70 
CustomInputStylePreference(final Context context, final InputMethodSubtype subtype, final Listener proxy)71     public CustomInputStylePreference(final Context context, final InputMethodSubtype subtype,
72             final Listener proxy) {
73         super(context, null);
74         setDialogLayoutResource(R.layout.additional_subtype_dialog);
75         setPersistent(false);
76         mProxy = proxy;
77         setSubtype(subtype);
78     }
79 
show()80     public void show() {
81         showDialog(null);
82     }
83 
isIncomplete()84     public final boolean isIncomplete() {
85         return mSubtype == null;
86     }
87 
getSubtype()88     public InputMethodSubtype getSubtype() {
89         return mSubtype;
90     }
91 
setSubtype(final InputMethodSubtype subtype)92     public void setSubtype(final InputMethodSubtype subtype) {
93         mPreviousSubtype = mSubtype;
94         mSubtype = subtype;
95         if (isIncomplete()) {
96             setTitle(null);
97             setDialogTitle(R.string.add_style);
98             setKey(KEY_NEW_SUBTYPE);
99         } else {
100             final String displayName =
101                     SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(subtype);
102             setTitle(displayName);
103             setDialogTitle(displayName);
104             setKey(KEY_PREFIX + subtype.getLocale() + "_"
105                     + SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype));
106         }
107     }
108 
revert()109     public void revert() {
110         setSubtype(mPreviousSubtype);
111     }
112 
hasBeenModified()113     public boolean hasBeenModified() {
114         return mSubtype != null && !mSubtype.equals(mPreviousSubtype);
115     }
116 
117     @Override
onCreateDialogView()118     protected View onCreateDialogView() {
119         final View v = super.onCreateDialogView();
120         mSubtypeLocaleSpinner = (Spinner) v.findViewById(R.id.subtype_locale_spinner);
121         mSubtypeLocaleSpinner.setAdapter(mProxy.getSubtypeLocaleAdapter());
122         mKeyboardLayoutSetSpinner = (Spinner) v.findViewById(R.id.keyboard_layout_set_spinner);
123         mKeyboardLayoutSetSpinner.setAdapter(mProxy.getKeyboardLayoutSetAdapter());
124         // All keyboard layout names are in the Latin script and thus left to right. That means
125         // the view would align them to the left even if the system locale is RTL, but that
126         // would look strange. To fix this, we align them to the view's start, which will be
127         // natural for any direction.
128         ViewCompatUtils.setTextAlignment(
129                 mKeyboardLayoutSetSpinner, ViewCompatUtils.TEXT_ALIGNMENT_VIEW_START);
130         return v;
131     }
132 
133     @Override
onPrepareDialogBuilder(final AlertDialog.Builder builder)134     protected void onPrepareDialogBuilder(final AlertDialog.Builder builder) {
135         builder.setCancelable(true).setOnCancelListener(this);
136         if (isIncomplete()) {
137             builder.setPositiveButton(R.string.add, this)
138                     .setNegativeButton(android.R.string.cancel, this);
139         } else {
140             builder.setPositiveButton(R.string.save, this)
141                     .setNeutralButton(android.R.string.cancel, this)
142                     .setNegativeButton(R.string.remove, this);
143             final SubtypeLocaleItem localeItem = new SubtypeLocaleItem(mSubtype);
144             final KeyboardLayoutSetItem layoutItem = new KeyboardLayoutSetItem(mSubtype);
145             setSpinnerPosition(mSubtypeLocaleSpinner, localeItem);
146             setSpinnerPosition(mKeyboardLayoutSetSpinner, layoutItem);
147         }
148     }
149 
setSpinnerPosition(final Spinner spinner, final Object itemToSelect)150     private static void setSpinnerPosition(final Spinner spinner, final Object itemToSelect) {
151         final SpinnerAdapter adapter = spinner.getAdapter();
152         final int count = adapter.getCount();
153         for (int i = 0; i < count; i++) {
154             final Object item = spinner.getItemAtPosition(i);
155             if (item.equals(itemToSelect)) {
156                 spinner.setSelection(i);
157                 return;
158             }
159         }
160     }
161 
162     @Override
onCancel(final DialogInterface dialog)163     public void onCancel(final DialogInterface dialog) {
164         if (isIncomplete()) {
165             mProxy.onRemoveCustomInputStyle(this);
166         }
167     }
168 
169     @Override
onClick(final DialogInterface dialog, final int which)170     public void onClick(final DialogInterface dialog, final int which) {
171         super.onClick(dialog, which);
172         switch (which) {
173         case DialogInterface.BUTTON_POSITIVE:
174             final boolean isEditing = !isIncomplete();
175             final SubtypeLocaleItem locale =
176                     (SubtypeLocaleItem) mSubtypeLocaleSpinner.getSelectedItem();
177             final KeyboardLayoutSetItem layout =
178                     (KeyboardLayoutSetItem) mKeyboardLayoutSetSpinner.getSelectedItem();
179             final InputMethodSubtype subtype =
180                     AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(
181                             locale.mLocaleString, layout.mLayoutName);
182             setSubtype(subtype);
183             notifyChanged();
184             if (isEditing) {
185                 mProxy.onSaveCustomInputStyle(this);
186             } else {
187                 mProxy.onAddCustomInputStyle(this);
188             }
189             break;
190         case DialogInterface.BUTTON_NEUTRAL:
191             // Nothing to do
192             break;
193         case DialogInterface.BUTTON_NEGATIVE:
194             mProxy.onRemoveCustomInputStyle(this);
195             break;
196         }
197     }
198 
199     @Override
onSaveInstanceState()200     protected Parcelable onSaveInstanceState() {
201         final Parcelable superState = super.onSaveInstanceState();
202         final Dialog dialog = getDialog();
203         if (dialog == null || !dialog.isShowing()) {
204             return superState;
205         }
206 
207         final SavedState myState = new SavedState(superState);
208         myState.mSubtype = mSubtype;
209         return myState;
210     }
211 
212     @Override
onRestoreInstanceState(final Parcelable state)213     protected void onRestoreInstanceState(final Parcelable state) {
214         if (!(state instanceof SavedState)) {
215             super.onRestoreInstanceState(state);
216             return;
217         }
218 
219         final SavedState myState = (SavedState) state;
220         super.onRestoreInstanceState(myState.getSuperState());
221         setSubtype(myState.mSubtype);
222     }
223 
224     static final class SavedState extends Preference.BaseSavedState {
225         InputMethodSubtype mSubtype;
226 
SavedState(final Parcelable superState)227         public SavedState(final Parcelable superState) {
228             super(superState);
229         }
230 
231         @Override
writeToParcel(final Parcel dest, final int flags)232         public void writeToParcel(final Parcel dest, final int flags) {
233             super.writeToParcel(dest, flags);
234             dest.writeParcelable(mSubtype, 0);
235         }
236 
SavedState(final Parcel source)237         public SavedState(final Parcel source) {
238             super(source);
239             mSubtype = (InputMethodSubtype)source.readParcelable(null);
240         }
241 
242         @SuppressWarnings("hiding")
243         public static final Parcelable.Creator<SavedState> CREATOR =
244                 new Parcelable.Creator<SavedState>() {
245                     @Override
246                     public SavedState createFromParcel(final Parcel source) {
247                         return new SavedState(source);
248                     }
249 
250                     @Override
251                     public SavedState[] newArray(final int size) {
252                         return new SavedState[size];
253                     }
254                 };
255     }
256 
257     static final class SubtypeLocaleItem implements Comparable<SubtypeLocaleItem> {
258         public final String mLocaleString;
259         private final String mDisplayName;
260 
SubtypeLocaleItem(final InputMethodSubtype subtype)261         public SubtypeLocaleItem(final InputMethodSubtype subtype) {
262             mLocaleString = subtype.getLocale();
263             mDisplayName = SubtypeLocaleUtils.getSubtypeLocaleDisplayNameInSystemLocale(
264                     mLocaleString);
265         }
266 
267         // {@link ArrayAdapter<T>} that hosts the instance of this class needs {@link #toString()}
268         // to get display name.
269         @Override
toString()270         public String toString() {
271             return mDisplayName;
272         }
273 
274         @Override
compareTo(final SubtypeLocaleItem o)275         public int compareTo(final SubtypeLocaleItem o) {
276             return mLocaleString.compareTo(o.mLocaleString);
277         }
278     }
279 
280     static final class SubtypeLocaleAdapter extends ArrayAdapter<SubtypeLocaleItem> {
281         private static final String TAG_SUBTYPE = SubtypeLocaleAdapter.class.getSimpleName();
282 
SubtypeLocaleAdapter(final Context context)283         public SubtypeLocaleAdapter(final Context context) {
284             super(context, android.R.layout.simple_spinner_item);
285             setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
286 
287             final TreeSet<SubtypeLocaleItem> items = new TreeSet<>();
288             final InputMethodInfo imi = RichInputMethodManager.getInstance()
289                     .getInputMethodInfoOfThisIme();
290             final int count = imi.getSubtypeCount();
291             for (int i = 0; i < count; i++) {
292                 final InputMethodSubtype subtype = imi.getSubtypeAt(i);
293                 if (DEBUG_SUBTYPE_ID) {
294                     Log.d(TAG_SUBTYPE, String.format("%-6s 0x%08x %11d %s",
295                             subtype.getLocale(), subtype.hashCode(), subtype.hashCode(),
296                             SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(subtype)));
297                 }
298                 if (InputMethodSubtypeCompatUtils.isAsciiCapable(subtype)) {
299                     items.add(new SubtypeLocaleItem(subtype));
300                 }
301             }
302             // TODO: Should filter out already existing combinations of locale and layout.
303             addAll(items);
304         }
305     }
306 
307     static final class KeyboardLayoutSetItem {
308         public final String mLayoutName;
309         private final String mDisplayName;
310 
KeyboardLayoutSetItem(final InputMethodSubtype subtype)311         public KeyboardLayoutSetItem(final InputMethodSubtype subtype) {
312             mLayoutName = SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype);
313             mDisplayName = SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(subtype);
314         }
315 
316         // {@link ArrayAdapter<T>} that hosts the instance of this class needs {@link #toString()}
317         // to get display name.
318         @Override
toString()319         public String toString() {
320             return mDisplayName;
321         }
322     }
323 
324     static final class KeyboardLayoutSetAdapter extends ArrayAdapter<KeyboardLayoutSetItem> {
KeyboardLayoutSetAdapter(final Context context)325         public KeyboardLayoutSetAdapter(final Context context) {
326             super(context, android.R.layout.simple_spinner_item);
327             setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
328 
329             final String[] predefinedKeyboardLayoutSet = context.getResources().getStringArray(
330                     R.array.predefined_layouts);
331             // TODO: Should filter out already existing combinations of locale and layout.
332             for (final String layout : predefinedKeyboardLayoutSet) {
333                 // This is a dummy subtype with NO_LANGUAGE, only for display.
334                 final InputMethodSubtype subtype =
335                         AdditionalSubtypeUtils.createDummyAdditionalSubtype(
336                                 SubtypeLocaleUtils.NO_LANGUAGE, layout);
337                 add(new KeyboardLayoutSetItem(subtype));
338             }
339         }
340     }
341 }
342