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