1 /*
2  * Copyright (C) 2021 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.tv.settings.library.inputmethod;
18 
19 import android.app.AlertDialog;
20 import android.content.ActivityNotFoundException;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.os.UserHandle;
24 import android.text.TextUtils;
25 import android.util.Log;
26 import android.view.inputmethod.InputMethodInfo;
27 import android.view.inputmethod.InputMethodManager;
28 import android.view.inputmethod.InputMethodSubtype;
29 import android.widget.Toast;
30 
31 import com.android.tv.settings.library.PreferenceCompat;
32 import com.android.tv.settings.library.UIUpdateCallback;
33 import com.android.tv.settings.library.data.PreferenceCompatManager;
34 import com.android.tv.settings.library.settingslib.InputMethodAndSubtypeUtil;
35 import com.android.tv.settings.library.settingslib.InputMethodSettingValuesWrapper;
36 import com.android.tv.settings.library.settingslib.RestrictedLockUtils;
37 import com.android.tv.settings.library.settingslib.RestrictedLockUtilsInternal;
38 import com.android.tv.settings.library.util.ResourcesUtil;
39 import com.android.tv.settings.library.util.RestrictedPreferenceController;
40 
41 import java.util.List;
42 
43 /**
44  * Use InputMethodPreferenceController to handle the logic of InputMethodPreference from
45  * SettingsLib.
46  */
47 public class InputMethodPreferenceController extends RestrictedPreferenceController {
48     private static final String TAG = InputMethodPreferenceController.class.getSimpleName();
49     private static final String EMPTY_TEXT = "";
50     private static final int NO_WIDGET = 0;
51 
52     public interface OnSavePreferenceListener {
53         /**
54          * Called when this preference needs to be saved its state.
55          *
56          * Note that this preference is non-persistent and needs explicitly to be saved its state.
57          * Because changing one IME state may change other IMEs' state, this is a place to update
58          * other IMEs' state as well.
59          *
60          * @param pref This preference.
61          */
onSaveInputMethodPreference(InputMethodPreferenceController pref)62         void onSaveInputMethodPreference(InputMethodPreferenceController pref);
63     }
64 
65     private final InputMethodInfo mImi;
66     private final String mTitle;
67     private final boolean mHasPriorityInSorting;
68     private final OnSavePreferenceListener mOnSaveListener;
69     private final InputMethodSettingValuesWrapper mInputMethodSettingValues;
70     private final boolean mIsAllowedByOrganization;
71     private AlertDialog mDialog = null;
72     private boolean mHasSwitch;
73 
InputMethodPreferenceController(Context context, UIUpdateCallback callback, int stateIdentifier, PreferenceCompatManager preferenceCompatManager, InputMethodInfo imi, CharSequence title, boolean isAllowedByOrganization, OnSavePreferenceListener onSaveListener)74     public InputMethodPreferenceController(Context context,
75             UIUpdateCallback callback, int stateIdentifier,
76             PreferenceCompatManager preferenceCompatManager, InputMethodInfo imi,
77             CharSequence title,
78             boolean isAllowedByOrganization, OnSavePreferenceListener onSaveListener) {
79         super(context, callback, stateIdentifier, preferenceCompatManager);
80         mImi = imi;
81         mTitle = title.toString();
82         mIsAllowedByOrganization = isAllowedByOrganization;
83         mOnSaveListener = onSaveListener;
84         mInputMethodSettingValues = InputMethodSettingValuesWrapper.getInstance(context);
85         mHasPriorityInSorting = imi.isSystem()
86                 && InputMethodAndSubtypeUtil.isValidNonAuxAsciiCapableIme(imi);
87         mHasSwitch = true;
88     }
89 
InputMethodPreferenceController(final Context context, UIUpdateCallback callback, int stateIdentifier, PreferenceCompatManager preferenceCompatManager, final InputMethodInfo imi, final boolean isImeEnabler, final boolean isAllowedByOrganization, final InputMethodPreferenceController.OnSavePreferenceListener onSaveListener)90     public InputMethodPreferenceController(final Context context, UIUpdateCallback callback,
91             int stateIdentifier, PreferenceCompatManager preferenceCompatManager,
92             final InputMethodInfo imi,
93             final boolean isImeEnabler, final boolean isAllowedByOrganization,
94             final InputMethodPreferenceController.OnSavePreferenceListener onSaveListener) {
95         this(context, callback, stateIdentifier, preferenceCompatManager, imi,
96                 imi.loadLabel(context.getPackageManager()),
97                 isAllowedByOrganization,
98                 onSaveListener);
99         mHasSwitch = isImeEnabler;
100 
101     }
102 
103 
104     @Override
handlePreferenceChange(Object newValue)105     public boolean handlePreferenceChange(Object newValue) {
106         if (!mHasSwitch) {
107             // Prevent disabling an IME because this preference is for invoking a settings activity.
108             return true;
109         }
110         if (mPreferenceCompat.getChecked() == PreferenceCompat.STATUS_ON) {
111             // Disable this IME.
112             setCheckedInternal(false);
113             return true;
114         }
115         if (mImi.isSystem()) {
116             setCheckedInternal(true);
117         } else {
118             // Once security is confirmed, we might prompt if the IME isn't
119             // Direct Boot aware.
120             showSecurityWarnDialog();
121         }
122         return false;
123     }
124 
setCheckedInternal(boolean checked)125     private void setCheckedInternal(boolean checked) {
126         mOnSaveListener.onSaveInputMethodPreference(this);
127         mUIUpdateCallback.notifyUpdate(mStateIdentifier, mPreferenceCompat);
128     }
129 
showSecurityWarnDialog()130     private void showSecurityWarnDialog() {
131         if (mDialog != null && mDialog.isShowing()) {
132             mDialog.dismiss();
133         }
134         final AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
135         builder.setCancelable(true /* cancelable */);
136         builder.setTitle(android.R.string.dialog_alert_title);
137         final CharSequence label = mImi.getServiceInfo().applicationInfo.loadLabel(
138                 mContext.getPackageManager());
139         builder.setMessage(ResourcesUtil.getString(mContext, "ime_security_warning", label));
140         builder.setPositiveButton(ResourcesUtil.getString(mContext, "ok"),
141                 (dialog, which) -> setCheckedInternal(true));
142         builder.setNegativeButton(ResourcesUtil.getString(mContext, "cancel"), (dialog, which) -> {
143             // The user canceled to enable a 3rd party IME.
144             setCheckedInternal(false);
145         });
146         builder.setOnCancelListener((dialog) -> {
147             // The user canceled to enable a 3rd party IME.
148             setCheckedInternal(false);
149         });
150         mDialog = builder.create();
151         mDialog.show();
152     }
153 
154     @Override
handlePreferenceTreeClick(boolean status)155     public boolean handlePreferenceTreeClick(boolean status) {
156         if (!mDisabledByAdmin) {
157             // Always returns true to prevent invoking an intent without catching exceptions.
158             if (mHasSwitch) {
159                 // Prevent invoking a settings activity because this preference is for enabling and
160                 // disabling an input method.
161                 return true;
162             }
163             try {
164                 final Intent intent = mPreferenceCompat.getIntent();
165                 if (intent != null) {
166                     // Invoke a settings activity of an input method.
167                     mContext.startActivity(intent);
168                 }
169             } catch (final ActivityNotFoundException e) {
170                 Log.d(TAG, "IME's Settings Activity Not Found", e);
171                 final String message = ResourcesUtil.getString(
172                         mContext, "failed_to_open_app_settings_toast",
173                         mImi.loadLabel(mContext.getPackageManager()));
174                 Toast.makeText(mContext, message, Toast.LENGTH_LONG).show();
175             }
176             return true;
177         }
178         return super.handlePreferenceTreeClick(status);
179     }
180 
181     @Override
isAvailable()182     public boolean isAvailable() {
183         return true;
184     }
185 
186     @Override
update()187     public void update() {
188         mPreferenceCompat.setTitle(mTitle);
189         mPreferenceCompat.setType(PreferenceCompat.TYPE_SWITCH);
190         final String settingsActivity = mImi.getSettingsActivity();
191         if (TextUtils.isEmpty(settingsActivity)) {
192             mPreferenceCompat.setIntent(null);
193         } else {
194             // Set an intent to invoke settings activity of an input method.
195             final Intent intent = new Intent(Intent.ACTION_MAIN);
196             intent.setClassName(mImi.getPackageName(), settingsActivity);
197             mPreferenceCompat.setIntent(intent);
198         }
199     }
200 
201     @Override
init()202     public void init() {
203         super.init();
204     }
205 
isImeEnabler()206     private boolean isImeEnabler() {
207         return mHasSwitch;
208     }
209 
updatePreferenceViews()210     public void updatePreferenceViews() {
211         final boolean isAlwaysChecked = mInputMethodSettingValues.isAlwaysCheckedIme(mImi);
212         // When this preference has a switch and an input method should be always enabled,
213         // this preference should be disabled to prevent accidentally disabling an input method.
214         // This preference should also be disabled in case the admin does not allow this input
215         // method.
216         if (isAlwaysChecked && isImeEnabler()) {
217             setDisabledByAdmin(null);
218             setEnabled(false);
219         } else if (!mIsAllowedByOrganization) {
220             RestrictedLockUtils.EnforcedAdmin admin =
221                     RestrictedLockUtilsInternal.checkIfInputMethodDisallowed(mContext,
222                             mImi.getPackageName(), UserHandle.myUserId());
223             setDisabledByAdmin(admin);
224         } else {
225             setEnabled(true);
226         }
227 
228         mPreferenceCompat.setChecked(mInputMethodSettingValues.isEnabledImi(mImi));
229         if (!isDisabledByAdmin()) {
230             mPreferenceCompat.setSummary(getSummaryString());
231         }
232     }
233 
getSummaryString()234     private String getSummaryString() {
235         final InputMethodManager imm = getInputMethodManager();
236         final List<InputMethodSubtype> subtypes = imm.getEnabledInputMethodSubtypeList(mImi, true);
237         return InputMethodAndSubtypeUtil.getSubtypeLocaleNameListAsSentence(
238                 subtypes, mContext, mImi);
239     }
240 
getInputMethodManager()241     private InputMethodManager getInputMethodManager() {
242         return (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
243     }
244 
245     @Override
useAdminDisabledSummary()246     public boolean useAdminDisabledSummary() {
247         return false;
248     }
249 
250     @Override
getAttrUserRestriction()251     public String getAttrUserRestriction() {
252         return null;
253     }
254 
255     @Override
getPreferenceKey()256     public String[] getPreferenceKey() {
257         return new String[]{mImi.getId()};
258     }
259 
getPrefCompat()260     public PreferenceCompat getPrefCompat() {
261         return mPreferenceCompat;
262     }
263 }
264