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
17package com.android.inputmethod.latin.settings;
18
19import android.app.AlertDialog;
20import android.app.Dialog;
21import android.content.Context;
22import android.content.DialogInterface;
23import android.os.Parcel;
24import android.os.Parcelable;
25import android.preference.DialogPreference;
26import android.preference.Preference;
27import android.util.Log;
28import android.view.View;
29import android.view.inputmethod.InputMethodInfo;
30import android.view.inputmethod.InputMethodSubtype;
31import android.widget.ArrayAdapter;
32import android.widget.Spinner;
33import android.widget.SpinnerAdapter;
34
35import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils;
36import com.android.inputmethod.compat.ViewCompatUtils;
37import com.android.inputmethod.latin.R;
38import com.android.inputmethod.latin.RichInputMethodManager;
39import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils;
40import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
41
42import java.util.TreeSet;
43
44final class CustomInputStylePreference extends DialogPreference
45        implements DialogInterface.OnCancelListener {
46    private static final boolean DEBUG_SUBTYPE_ID = false;
47
48    interface Listener {
49        public void onRemoveCustomInputStyle(CustomInputStylePreference stylePref);
50        public void onSaveCustomInputStyle(CustomInputStylePreference stylePref);
51        public void onAddCustomInputStyle(CustomInputStylePreference stylePref);
52        public SubtypeLocaleAdapter getSubtypeLocaleAdapter();
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
66    public static CustomInputStylePreference newIncompleteSubtypePreference(
67            final Context context, final Listener proxy) {
68        return new CustomInputStylePreference(context, null, proxy);
69    }
70
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
80    public void show() {
81        showDialog(null);
82    }
83
84    public final boolean isIncomplete() {
85        return mSubtype == null;
86    }
87
88    public InputMethodSubtype getSubtype() {
89        return mSubtype;
90    }
91
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
109    public void revert() {
110        setSubtype(mPreviousSubtype);
111    }
112
113    public boolean hasBeenModified() {
114        return mSubtype != null && !mSubtype.equals(mPreviousSubtype);
115    }
116
117    @Override
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
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
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
163    public void onCancel(final DialogInterface dialog) {
164        if (isIncomplete()) {
165            mProxy.onRemoveCustomInputStyle(this);
166        }
167    }
168
169    @Override
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
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
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
227        public SavedState(final Parcelable superState) {
228            super(superState);
229        }
230
231        @Override
232        public void writeToParcel(final Parcel dest, final int flags) {
233            super.writeToParcel(dest, flags);
234            dest.writeParcelable(mSubtype, 0);
235        }
236
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
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
270        public String toString() {
271            return mDisplayName;
272        }
273
274        @Override
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
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
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
319        public String toString() {
320            return mDisplayName;
321        }
322    }
323
324    static final class KeyboardLayoutSetAdapter extends ArrayAdapter<KeyboardLayoutSetItem> {
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