1 /*
2  * Copyright (C) 2011 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.settings.inputmethod;
18 
19 import android.app.AlertDialog;
20 import android.content.ActivityNotFoundException;
21 import android.content.Context;
22 import android.content.DialogInterface;
23 import android.content.Intent;
24 import android.preference.Preference;
25 import android.preference.Preference.OnPreferenceChangeListener;
26 import android.preference.Preference.OnPreferenceClickListener;
27 import android.preference.SwitchPreference;
28 import android.text.TextUtils;
29 import android.util.Log;
30 import android.view.inputmethod.InputMethodInfo;
31 import android.view.inputmethod.InputMethodManager;
32 import android.view.inputmethod.InputMethodSubtype;
33 import android.widget.Toast;
34 
35 import com.android.internal.inputmethod.InputMethodUtils;
36 import com.android.settings.R;
37 
38 import java.text.Collator;
39 import java.util.ArrayList;
40 import java.util.List;
41 
42 /**
43  * Input method preference.
44  *
45  * This preference represents an IME. It is used for two purposes. 1) An instance with a switch
46  * is used to enable or disable the IME. 2) An instance without a switch is used to invoke the
47  * setting activity of the IME.
48  */
49 class InputMethodPreference extends SwitchPreference implements OnPreferenceClickListener,
50         OnPreferenceChangeListener {
51     private static final String TAG = InputMethodPreference.class.getSimpleName();
52     private static final String EMPTY_TEXT = "";
53 
54     interface OnSavePreferenceListener {
55         /**
56          * Called when this preference needs to be saved its state.
57          *
58          * Note that this preference is non-persistent and needs explicitly to be saved its state.
59          * Because changing one IME state may change other IMEs' state, this is a place to update
60          * other IMEs' state as well.
61          *
62          * @param pref This preference.
63          */
onSaveInputMethodPreference(InputMethodPreference pref)64         public void onSaveInputMethodPreference(InputMethodPreference pref);
65     }
66 
67     private final InputMethodInfo mImi;
68     private final boolean mHasPriorityInSorting;
69     private final OnSavePreferenceListener mOnSaveListener;
70     private final InputMethodSettingValuesWrapper mInputMethodSettingValues;
71     private final boolean mIsAllowedByOrganization;
72 
73     private AlertDialog mDialog = null;
74 
75     /**
76      * A preference entry of an input method.
77      *
78      * @param context The Context this is associated with.
79      * @param imi The {@link InputMethodInfo} of this preference.
80      * @param isImeEnabler true if this preference is the IME enabler that has enable/disable
81      *     switches for all available IMEs, not the list of enabled IMEs.
82      * @param isAllowedByOrganization false if the IME has been disabled by a device or profile
83            owner.
84      * @param onSaveListener The listener called when this preference has been changed and needs
85      *     to save the state to shared preference.
86      */
InputMethodPreference(final Context context, final InputMethodInfo imi, final boolean isImeEnabler, final boolean isAllowedByOrganization, final OnSavePreferenceListener onSaveListener)87     InputMethodPreference(final Context context, final InputMethodInfo imi,
88             final boolean isImeEnabler, final boolean isAllowedByOrganization,
89             final OnSavePreferenceListener onSaveListener) {
90         super(context);
91         setPersistent(false);
92         mImi = imi;
93         mIsAllowedByOrganization = isAllowedByOrganization;
94         mOnSaveListener = onSaveListener;
95         if (!isImeEnabler) {
96             // Hide switch widget.
97             setWidgetLayoutResource(0 /* widgetLayoutResId */);
98         }
99         // Disable on/off switch texts.
100         setSwitchTextOn(EMPTY_TEXT);
101         setSwitchTextOff(EMPTY_TEXT);
102         setKey(imi.getId());
103         setTitle(imi.loadLabel(context.getPackageManager()));
104         final String settingsActivity = imi.getSettingsActivity();
105         if (TextUtils.isEmpty(settingsActivity)) {
106             setIntent(null);
107         } else {
108             // Set an intent to invoke settings activity of an input method.
109             final Intent intent = new Intent(Intent.ACTION_MAIN);
110             intent.setClassName(imi.getPackageName(), settingsActivity);
111             setIntent(intent);
112         }
113         mInputMethodSettingValues = InputMethodSettingValuesWrapper.getInstance(context);
114         mHasPriorityInSorting = InputMethodUtils.isSystemIme(imi)
115                 && mInputMethodSettingValues.isValidSystemNonAuxAsciiCapableIme(imi, context);
116         setOnPreferenceClickListener(this);
117         setOnPreferenceChangeListener(this);
118     }
119 
getInputMethodInfo()120     public InputMethodInfo getInputMethodInfo() {
121         return mImi;
122     }
123 
isImeEnabler()124     private boolean isImeEnabler() {
125         // If this {@link SwitchPreference} doesn't have a widget layout, we explicitly hide the
126         // switch widget at constructor.
127         return getWidgetLayoutResource() != 0;
128     }
129 
130     @Override
onPreferenceChange(final Preference preference, final Object newValue)131     public boolean onPreferenceChange(final Preference preference, final Object newValue) {
132         // Always returns false to prevent default behavior.
133         // See {@link TwoStatePreference#onClick()}.
134         if (!isImeEnabler()) {
135             // Prevent disabling an IME because this preference is for invoking a settings activity.
136             return false;
137         }
138         if (isChecked()) {
139             // Disable this IME.
140             setChecked(false);
141             mOnSaveListener.onSaveInputMethodPreference(this);
142             return false;
143         }
144         if (InputMethodUtils.isSystemIme(mImi)) {
145             // Enable a system IME. No need to show a security warning dialog.
146             setChecked(true);
147             mOnSaveListener.onSaveInputMethodPreference(this);
148             return false;
149         }
150         // Enable a 3rd party IME.
151         showSecurityWarnDialog(mImi);
152         return false;
153     }
154 
155     @Override
onPreferenceClick(final Preference preference)156     public boolean onPreferenceClick(final Preference preference) {
157         // Always returns true to prevent invoking an intent without catching exceptions.
158         // See {@link Preference#performClick(PreferenceScreen)}/
159         if (isImeEnabler()) {
160             // Prevent invoking a settings activity because this preference is for enabling and
161             // disabling an input method.
162             return true;
163         }
164         final Context context = getContext();
165         try {
166             final Intent intent = getIntent();
167             if (intent != null) {
168                 // Invoke a settings activity of an input method.
169                 context.startActivity(intent);
170             }
171         } catch (final ActivityNotFoundException e) {
172             Log.d(TAG, "IME's Settings Activity Not Found", e);
173             final String message = context.getString(
174                     R.string.failed_to_open_app_settings_toast,
175                     mImi.loadLabel(context.getPackageManager()));
176             Toast.makeText(context, message, Toast.LENGTH_LONG).show();
177         }
178         return true;
179     }
180 
updatePreferenceViews()181     void updatePreferenceViews() {
182         final boolean isAlwaysChecked = mInputMethodSettingValues.isAlwaysCheckedIme(
183                 mImi, getContext());
184         // Only when this preference has a switch and an input method should be always enabled,
185         // this preference should be disabled to prevent accidentally disabling an input method.
186         setEnabled(!((isAlwaysChecked && isImeEnabler()) || (!mIsAllowedByOrganization)));
187         setChecked(mInputMethodSettingValues.isEnabledImi(mImi));
188         setSummary(getSummaryString());
189     }
190 
getInputMethodManager()191     private InputMethodManager getInputMethodManager() {
192         return (InputMethodManager)getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
193     }
194 
getSummaryString()195     private String getSummaryString() {
196         final Context context = getContext();
197         if (!mIsAllowedByOrganization) {
198             return context.getString(R.string.accessibility_feature_or_input_method_not_allowed);
199         }
200         final InputMethodManager imm = getInputMethodManager();
201         final List<InputMethodSubtype> subtypes = imm.getEnabledInputMethodSubtypeList(mImi, true);
202         final ArrayList<CharSequence> subtypeLabels = new ArrayList<>();
203         for (final InputMethodSubtype subtype : subtypes) {
204             final CharSequence label = subtype.getDisplayName(
205                   context, mImi.getPackageName(), mImi.getServiceInfo().applicationInfo);
206             subtypeLabels.add(label);
207         }
208         // TODO: A delimiter of subtype labels should be localized.
209         return TextUtils.join(", ", subtypeLabels);
210     }
211 
showSecurityWarnDialog(final InputMethodInfo imi)212     private void showSecurityWarnDialog(final InputMethodInfo imi) {
213         if (mDialog != null && mDialog.isShowing()) {
214             mDialog.dismiss();
215         }
216         final Context context = getContext();
217         final AlertDialog.Builder builder = new AlertDialog.Builder(context);
218         builder.setCancelable(true /* cancelable */);
219         builder.setTitle(android.R.string.dialog_alert_title);
220         final CharSequence label = imi.getServiceInfo().applicationInfo.loadLabel(
221                 context.getPackageManager());
222         builder.setMessage(context.getString(R.string.ime_security_warning, label));
223         builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
224             @Override
225             public void onClick(final DialogInterface dialog, final int which) {
226                 // The user confirmed to enable a 3rd party IME.
227                 setChecked(true);
228                 mOnSaveListener.onSaveInputMethodPreference(InputMethodPreference.this);
229                 notifyChanged();
230             }
231         });
232         builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
233             @Override
234             public void onClick(final DialogInterface dialog, final int which) {
235                 // The user canceled to enable a 3rd party IME.
236                 setChecked(false);
237                 mOnSaveListener.onSaveInputMethodPreference(InputMethodPreference.this);
238                 notifyChanged();
239             }
240         });
241         mDialog = builder.create();
242         mDialog.show();
243     }
244 
compareTo(final InputMethodPreference rhs, final Collator collator)245     int compareTo(final InputMethodPreference rhs, final Collator collator) {
246         if (this == rhs) {
247             return 0;
248         }
249         if (mHasPriorityInSorting == rhs.mHasPriorityInSorting) {
250             final CharSequence t0 = getTitle();
251             final CharSequence t1 = rhs.getTitle();
252             if (TextUtils.isEmpty(t0)) {
253                 return 1;
254             }
255             if (TextUtils.isEmpty(t1)) {
256                 return -1;
257             }
258             return collator.compare(t0.toString(), t1.toString());
259         }
260         // Prefer always checked system IMEs
261         return mHasPriorityInSorting ? -1 : 1;
262     }
263 }
264