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