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