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