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.text.TextUtils;
28 import android.util.Log;
29 import android.view.inputmethod.InputMethodInfo;
30 import android.view.inputmethod.InputMethodManager;
31 import android.view.inputmethod.InputMethodSubtype;
32 import android.widget.Toast;
33 
34 import androidx.preference.Preference;
35 import androidx.preference.Preference.OnPreferenceChangeListener;
36 import androidx.preference.Preference.OnPreferenceClickListener;
37 
38 import com.android.internal.annotations.VisibleForTesting;
39 import com.android.settingslib.R;
40 import com.android.settingslib.RestrictedLockUtilsInternal;
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         setIconSize(context.getResources().getDimensionPixelSize(R.dimen.secondary_app_icon_size));
102     }
103 
104     @VisibleForTesting
InputMethodPreference(final Context context, final InputMethodInfo imi, final CharSequence title, final boolean isAllowedByOrganization, final OnSavePreferenceListener onSaveListener)105     InputMethodPreference(final Context context, final InputMethodInfo imi,
106             final CharSequence title, final boolean isAllowedByOrganization,
107             final OnSavePreferenceListener onSaveListener) {
108         super(context);
109         setPersistent(false);
110         mImi = imi;
111         mIsAllowedByOrganization = isAllowedByOrganization;
112         mOnSaveListener = onSaveListener;
113         // Disable on/off switch texts.
114         setSwitchTextOn(EMPTY_TEXT);
115         setSwitchTextOff(EMPTY_TEXT);
116         setKey(imi.getId());
117         setTitle(title);
118         final String settingsActivity = imi.getSettingsActivity();
119         if (TextUtils.isEmpty(settingsActivity)) {
120             setIntent(null);
121         } else {
122             // Set an intent to invoke settings activity of an input method.
123             final Intent intent = new Intent(Intent.ACTION_MAIN);
124             intent.setClassName(imi.getPackageName(), settingsActivity);
125             setIntent(intent);
126         }
127         mInputMethodSettingValues = InputMethodSettingValuesWrapper.getInstance(context);
128         mHasPriorityInSorting = imi.isSystem()
129                 && InputMethodAndSubtypeUtil.isValidNonAuxAsciiCapableIme(imi);
130         setOnPreferenceClickListener(this);
131         setOnPreferenceChangeListener(this);
132     }
133 
getInputMethodInfo()134     public InputMethodInfo getInputMethodInfo() {
135         return mImi;
136     }
137 
isImeEnabler()138     private boolean isImeEnabler() {
139         // If this {@link SwitchPreference} doesn't have a widget layout, we explicitly hide the
140         // switch widget at constructor.
141         return getWidgetLayoutResource() != NO_WIDGET;
142     }
143 
144     @Override
onPreferenceChange(final Preference preference, final Object newValue)145     public boolean onPreferenceChange(final Preference preference, final Object newValue) {
146         // Always returns false to prevent default behavior.
147         // See {@link TwoStatePreference#onClick()}.
148         if (!isImeEnabler()) {
149             // Prevent disabling an IME because this preference is for invoking a settings activity.
150             return false;
151         }
152         if (isChecked()) {
153             // Disable this IME.
154             setCheckedInternal(false);
155             return false;
156         }
157         if (mImi.isSystem()) {
158             // Enable a system IME. No need to show a security warning dialog,
159             // but we might need to prompt if it's not Direct Boot aware.
160             // TV doesn't doesn't need to worry about this, but other platforms should show
161             // a warning.
162             if (mImi.getServiceInfo().directBootAware || isTv()) {
163                 setCheckedInternal(true);
164             } else if (!isTv()){
165                 showDirectBootWarnDialog();
166             }
167         } else {
168             // Once security is confirmed, we might prompt if the IME isn't
169             // Direct Boot aware.
170             showSecurityWarnDialog();
171         }
172         return false;
173     }
174 
175     @Override
onPreferenceClick(final Preference preference)176     public boolean onPreferenceClick(final Preference preference) {
177         // Always returns true to prevent invoking an intent without catching exceptions.
178         // See {@link Preference#performClick(PreferenceScreen)}/
179         if (isImeEnabler()) {
180             // Prevent invoking a settings activity because this preference is for enabling and
181             // disabling an input method.
182             return true;
183         }
184         final Context context = getContext();
185         try {
186             final Intent intent = getIntent();
187             if (intent != null) {
188                 // Invoke a settings activity of an input method.
189                 context.startActivity(intent);
190             }
191         } catch (final ActivityNotFoundException e) {
192             Log.d(TAG, "IME's Settings Activity Not Found", e);
193             final String message = context.getString(
194                     R.string.failed_to_open_app_settings_toast,
195                     mImi.loadLabel(context.getPackageManager()));
196             Toast.makeText(context, message, Toast.LENGTH_LONG).show();
197         }
198         return true;
199     }
200 
updatePreferenceViews()201     public void updatePreferenceViews() {
202         final boolean isAlwaysChecked = mInputMethodSettingValues.isAlwaysCheckedIme(mImi);
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                     RestrictedLockUtilsInternal.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         builder.setOnCancelListener((dialog) -> {
268             // The user canceled to enable a 3rd party IME.
269             setCheckedInternal(false);
270         });
271         mDialog = builder.create();
272         mDialog.show();
273     }
274 
isTv()275     private boolean isTv() {
276         return (getContext().getResources().getConfiguration().uiMode
277                 & Configuration.UI_MODE_TYPE_MASK) == Configuration.UI_MODE_TYPE_TELEVISION;
278     }
279 
showDirectBootWarnDialog()280     private void showDirectBootWarnDialog() {
281         if (mDialog != null && mDialog.isShowing()) {
282             mDialog.dismiss();
283         }
284         final Context context = getContext();
285         final AlertDialog.Builder builder = new AlertDialog.Builder(context);
286         builder.setCancelable(true /* cancelable */);
287         builder.setMessage(context.getText(R.string.direct_boot_unaware_dialog_message));
288         builder.setPositiveButton(android.R.string.ok, (dialog, which) -> setCheckedInternal(true));
289         builder.setNegativeButton(android.R.string.cancel,
290                 (dialog, which) -> setCheckedInternal(false));
291         mDialog = builder.create();
292         mDialog.show();
293     }
294 
compareTo(final InputMethodPreference rhs, final Collator collator)295     public int compareTo(final InputMethodPreference rhs, final Collator collator) {
296         if (this == rhs) {
297             return 0;
298         }
299         if (mHasPriorityInSorting != rhs.mHasPriorityInSorting) {
300             // Prefer always checked system IMEs
301             return mHasPriorityInSorting ? -1 : 1;
302         }
303         final CharSequence title = getTitle();
304         final CharSequence rhsTitle = rhs.getTitle();
305         final boolean emptyTitle = TextUtils.isEmpty(title);
306         final boolean rhsEmptyTitle = TextUtils.isEmpty(rhsTitle);
307         if (!emptyTitle && !rhsEmptyTitle) {
308             return collator.compare(title.toString(), rhsTitle.toString());
309         }
310         // For historical reasons, an empty text needs to be put at the first.
311         return (emptyTitle ? -1 : 0) - (rhsEmptyTitle ? -1 : 0);
312     }
313 }
314