1 /*
2  * Copyright (C) 2012 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.content.Context;
21 import android.content.DialogInterface;
22 import android.content.Intent;
23 import android.content.SharedPreferences;
24 import android.content.res.Resources;
25 import android.os.Bundle;
26 import android.preference.Preference;
27 import android.preference.PreferenceFragment;
28 import android.preference.PreferenceGroup;
29 import androidx.core.view.ViewCompat;
30 import android.text.TextUtils;
31 import android.util.Log;
32 import android.view.LayoutInflater;
33 import android.view.Menu;
34 import android.view.MenuInflater;
35 import android.view.MenuItem;
36 import android.view.View;
37 import android.view.ViewGroup;
38 import android.view.inputmethod.InputMethodSubtype;
39 import android.widget.Toast;
40 
41 import com.android.inputmethod.latin.R;
42 import com.android.inputmethod.latin.RichInputMethodManager;
43 import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils;
44 import com.android.inputmethod.latin.utils.DialogUtils;
45 import com.android.inputmethod.latin.utils.IntentUtils;
46 import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
47 
48 import java.util.ArrayList;
49 
50 public final class CustomInputStyleSettingsFragment extends PreferenceFragment
51         implements CustomInputStylePreference.Listener {
52     private static final String TAG = CustomInputStyleSettingsFragment.class.getSimpleName();
53     // Note: We would like to turn this debug flag true in order to see what input styles are
54     // defined in a bug-report.
55     private static final boolean DEBUG_CUSTOM_INPUT_STYLES = true;
56 
57     private RichInputMethodManager mRichImm;
58     private SharedPreferences mPrefs;
59     private CustomInputStylePreference.SubtypeLocaleAdapter mSubtypeLocaleAdapter;
60     private CustomInputStylePreference.KeyboardLayoutSetAdapter mKeyboardLayoutSetAdapter;
61 
62     private boolean mIsAddingNewSubtype;
63     private AlertDialog mSubtypeEnablerNotificationDialog;
64     private String mSubtypePreferenceKeyForSubtypeEnabler;
65 
66     private static final String KEY_IS_ADDING_NEW_SUBTYPE = "is_adding_new_subtype";
67     private static final String KEY_IS_SUBTYPE_ENABLER_NOTIFICATION_DIALOG_OPEN =
68             "is_subtype_enabler_notification_dialog_open";
69     private static final String KEY_SUBTYPE_FOR_SUBTYPE_ENABLER = "subtype_for_subtype_enabler";
70 
CustomInputStyleSettingsFragment()71     public CustomInputStyleSettingsFragment() {
72         // Empty constructor for fragment generation.
73     }
74 
updateCustomInputStylesSummary(final Preference pref)75     static void updateCustomInputStylesSummary(final Preference pref) {
76         // When we are called from the Settings application but we are not already running, some
77         // singleton and utility classes may not have been initialized.  We have to call
78         // initialization method of these classes here. See {@link LatinIME#onCreate()}.
79         SubtypeLocaleUtils.init(pref.getContext());
80 
81         final Resources res = pref.getContext().getResources();
82         final SharedPreferences prefs = pref.getSharedPreferences();
83         final String prefSubtype = Settings.readPrefAdditionalSubtypes(prefs, res);
84         final InputMethodSubtype[] subtypes =
85                 AdditionalSubtypeUtils.createAdditionalSubtypesArray(prefSubtype);
86         final ArrayList<String> subtypeNames = new ArrayList<>();
87         for (final InputMethodSubtype subtype : subtypes) {
88             subtypeNames.add(SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(subtype));
89         }
90         // TODO: A delimiter of custom input styles should be localized.
91         pref.setSummary(TextUtils.join(", ", subtypeNames));
92     }
93 
94     @Override
onCreate(final Bundle savedInstanceState)95     public void onCreate(final Bundle savedInstanceState) {
96         super.onCreate(savedInstanceState);
97 
98         mPrefs = getPreferenceManager().getSharedPreferences();
99         RichInputMethodManager.init(getActivity());
100         mRichImm = RichInputMethodManager.getInstance();
101         addPreferencesFromResource(R.xml.additional_subtype_settings);
102         setHasOptionsMenu(true);
103     }
104 
105     @Override
onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState)106     public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
107             final Bundle savedInstanceState) {
108         final View view = super.onCreateView(inflater, container, savedInstanceState);
109         // For correct display in RTL locales, we need to set the layout direction of the
110         // fragment's top view.
111         ViewCompat.setLayoutDirection(view, ViewCompat.LAYOUT_DIRECTION_LOCALE);
112         return view;
113     }
114 
115     @Override
onActivityCreated(final Bundle savedInstanceState)116     public void onActivityCreated(final Bundle savedInstanceState) {
117         final Context context = getActivity();
118         mSubtypeLocaleAdapter = new CustomInputStylePreference.SubtypeLocaleAdapter(context);
119         mKeyboardLayoutSetAdapter =
120                 new CustomInputStylePreference.KeyboardLayoutSetAdapter(context);
121 
122         final String prefSubtypes =
123                 Settings.readPrefAdditionalSubtypes(mPrefs, getResources());
124         if (DEBUG_CUSTOM_INPUT_STYLES) {
125             Log.i(TAG, "Load custom input styles: " + prefSubtypes);
126         }
127         setPrefSubtypes(prefSubtypes, context);
128 
129         mIsAddingNewSubtype = (savedInstanceState != null)
130                 && savedInstanceState.containsKey(KEY_IS_ADDING_NEW_SUBTYPE);
131         if (mIsAddingNewSubtype) {
132             getPreferenceScreen().addPreference(
133                     CustomInputStylePreference.newIncompleteSubtypePreference(context, this));
134         }
135 
136         super.onActivityCreated(savedInstanceState);
137 
138         if (savedInstanceState != null && savedInstanceState.containsKey(
139                 KEY_IS_SUBTYPE_ENABLER_NOTIFICATION_DIALOG_OPEN)) {
140             mSubtypePreferenceKeyForSubtypeEnabler = savedInstanceState.getString(
141                     KEY_SUBTYPE_FOR_SUBTYPE_ENABLER);
142             mSubtypeEnablerNotificationDialog = createDialog();
143             mSubtypeEnablerNotificationDialog.show();
144         }
145     }
146 
147     @Override
onSaveInstanceState(final Bundle outState)148     public void onSaveInstanceState(final Bundle outState) {
149         super.onSaveInstanceState(outState);
150         if (mIsAddingNewSubtype) {
151             outState.putBoolean(KEY_IS_ADDING_NEW_SUBTYPE, true);
152         }
153         if (mSubtypeEnablerNotificationDialog != null
154                 && mSubtypeEnablerNotificationDialog.isShowing()) {
155             outState.putBoolean(KEY_IS_SUBTYPE_ENABLER_NOTIFICATION_DIALOG_OPEN, true);
156             outState.putString(
157                     KEY_SUBTYPE_FOR_SUBTYPE_ENABLER, mSubtypePreferenceKeyForSubtypeEnabler);
158         }
159     }
160 
161     @Override
onRemoveCustomInputStyle(final CustomInputStylePreference stylePref)162     public void onRemoveCustomInputStyle(final CustomInputStylePreference stylePref) {
163         mIsAddingNewSubtype = false;
164         final PreferenceGroup group = getPreferenceScreen();
165         group.removePreference(stylePref);
166         mRichImm.setAdditionalInputMethodSubtypes(getSubtypes());
167     }
168 
169     @Override
onSaveCustomInputStyle(final CustomInputStylePreference stylePref)170     public void onSaveCustomInputStyle(final CustomInputStylePreference stylePref) {
171         final InputMethodSubtype subtype = stylePref.getSubtype();
172         if (!stylePref.hasBeenModified()) {
173             return;
174         }
175         if (findDuplicatedSubtype(subtype) == null) {
176             mRichImm.setAdditionalInputMethodSubtypes(getSubtypes());
177             return;
178         }
179 
180         // Saved subtype is duplicated.
181         final PreferenceGroup group = getPreferenceScreen();
182         group.removePreference(stylePref);
183         stylePref.revert();
184         group.addPreference(stylePref);
185         showSubtypeAlreadyExistsToast(subtype);
186     }
187 
188     @Override
onAddCustomInputStyle(final CustomInputStylePreference stylePref)189     public void onAddCustomInputStyle(final CustomInputStylePreference stylePref) {
190         mIsAddingNewSubtype = false;
191         final InputMethodSubtype subtype = stylePref.getSubtype();
192         if (findDuplicatedSubtype(subtype) == null) {
193             mRichImm.setAdditionalInputMethodSubtypes(getSubtypes());
194             mSubtypePreferenceKeyForSubtypeEnabler = stylePref.getKey();
195             mSubtypeEnablerNotificationDialog = createDialog();
196             mSubtypeEnablerNotificationDialog.show();
197             return;
198         }
199 
200         // Newly added subtype is duplicated.
201         final PreferenceGroup group = getPreferenceScreen();
202         group.removePreference(stylePref);
203         showSubtypeAlreadyExistsToast(subtype);
204     }
205 
206     @Override
getSubtypeLocaleAdapter()207     public CustomInputStylePreference.SubtypeLocaleAdapter getSubtypeLocaleAdapter() {
208         return mSubtypeLocaleAdapter;
209     }
210 
211     @Override
getKeyboardLayoutSetAdapter()212     public CustomInputStylePreference.KeyboardLayoutSetAdapter getKeyboardLayoutSetAdapter() {
213         return mKeyboardLayoutSetAdapter;
214     }
215 
showSubtypeAlreadyExistsToast(final InputMethodSubtype subtype)216     private void showSubtypeAlreadyExistsToast(final InputMethodSubtype subtype) {
217         final Context context = getActivity();
218         final Resources res = context.getResources();
219         final String message = res.getString(R.string.custom_input_style_already_exists,
220                 SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(subtype));
221         Toast.makeText(context, message, Toast.LENGTH_SHORT).show();
222     }
223 
findDuplicatedSubtype(final InputMethodSubtype subtype)224     private InputMethodSubtype findDuplicatedSubtype(final InputMethodSubtype subtype) {
225         final String localeString = subtype.getLocale();
226         final String keyboardLayoutSetName = SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype);
227         return mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
228                 localeString, keyboardLayoutSetName);
229     }
230 
createDialog()231     private AlertDialog createDialog() {
232         final String imeId = mRichImm.getInputMethodIdOfThisIme();
233         final AlertDialog.Builder builder = new AlertDialog.Builder(
234                 DialogUtils.getPlatformDialogThemeContext(getActivity()));
235         builder.setTitle(R.string.custom_input_styles_title)
236                 .setMessage(R.string.custom_input_style_note_message)
237                 .setNegativeButton(R.string.not_now, null)
238                 .setPositiveButton(R.string.enable, new DialogInterface.OnClickListener() {
239                     @Override
240                     public void onClick(DialogInterface dialog, int which) {
241                         final Intent intent = IntentUtils.getInputLanguageSelectionIntent(
242                                 imeId,
243                                 Intent.FLAG_ACTIVITY_NEW_TASK
244                                 | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
245                                 | Intent.FLAG_ACTIVITY_CLEAR_TOP);
246                         // TODO: Add newly adding subtype to extra value of the intent as a hint
247                         // for the input language selection activity.
248                         // intent.putExtra("newlyAddedSubtype", subtypePref.getSubtype());
249                         startActivity(intent);
250                     }
251                 });
252 
253         return builder.create();
254     }
255 
setPrefSubtypes(final String prefSubtypes, final Context context)256     private void setPrefSubtypes(final String prefSubtypes, final Context context) {
257         final PreferenceGroup group = getPreferenceScreen();
258         group.removeAll();
259         final InputMethodSubtype[] subtypesArray =
260                 AdditionalSubtypeUtils.createAdditionalSubtypesArray(prefSubtypes);
261         for (final InputMethodSubtype subtype : subtypesArray) {
262             final CustomInputStylePreference pref =
263                     new CustomInputStylePreference(context, subtype, this);
264             group.addPreference(pref);
265         }
266     }
267 
getSubtypes()268     private InputMethodSubtype[] getSubtypes() {
269         final PreferenceGroup group = getPreferenceScreen();
270         final ArrayList<InputMethodSubtype> subtypes = new ArrayList<>();
271         final int count = group.getPreferenceCount();
272         for (int i = 0; i < count; i++) {
273             final Preference pref = group.getPreference(i);
274             if (pref instanceof CustomInputStylePreference) {
275                 final CustomInputStylePreference subtypePref = (CustomInputStylePreference)pref;
276                 // We should not save newly adding subtype to preference because it is incomplete.
277                 if (subtypePref.isIncomplete()) continue;
278                 subtypes.add(subtypePref.getSubtype());
279             }
280         }
281         return subtypes.toArray(new InputMethodSubtype[subtypes.size()]);
282     }
283 
284     @Override
onPause()285     public void onPause() {
286         super.onPause();
287         final String oldSubtypes = Settings.readPrefAdditionalSubtypes(mPrefs, getResources());
288         final InputMethodSubtype[] subtypes = getSubtypes();
289         final String prefSubtypes = AdditionalSubtypeUtils.createPrefSubtypes(subtypes);
290         if (DEBUG_CUSTOM_INPUT_STYLES) {
291             Log.i(TAG, "Save custom input styles: " + prefSubtypes);
292         }
293         if (prefSubtypes.equals(oldSubtypes)) {
294             return;
295         }
296         Settings.writePrefAdditionalSubtypes(mPrefs, prefSubtypes);
297         mRichImm.setAdditionalInputMethodSubtypes(subtypes);
298     }
299 
300     @Override
onCreateOptionsMenu(final Menu menu, final MenuInflater inflater)301     public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
302         inflater.inflate(R.menu.add_style, menu);
303     }
304 
305     @Override
onOptionsItemSelected(final MenuItem item)306     public boolean onOptionsItemSelected(final MenuItem item) {
307         final int itemId = item.getItemId();
308         if (itemId == R.id.action_add_style) {
309             final CustomInputStylePreference newSubtype =
310                     CustomInputStylePreference.newIncompleteSubtypePreference(getActivity(), this);
311             getPreferenceScreen().addPreference(newSubtype);
312             newSubtype.show();
313             mIsAddingNewSubtype = true;
314             return true;
315         }
316         return super.onOptionsItemSelected(item);
317     }
318 }
319