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 */ 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 */ 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 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 = imi.isSystem() 128 && InputMethodAndSubtypeUtil.isValidNonAuxAsciiCapableIme(imi); 129 setOnPreferenceClickListener(this); 130 setOnPreferenceChangeListener(this); 131 } 132 133 public InputMethodInfo getInputMethodInfo() { 134 return mImi; 135 } 136 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 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 (mImi.isSystem()) { 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 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 200 public void updatePreferenceViews() { 201 final boolean isAlwaysChecked = mInputMethodSettingValues.isAlwaysCheckedIme(mImi); 202 // When this preference has a switch and an input method should be always enabled, 203 // this preference should be disabled to prevent accidentally disabling an input method. 204 // This preference should also be disabled in case the admin does not allow this input 205 // method. 206 if (isAlwaysChecked && isImeEnabler()) { 207 setDisabledByAdmin(null); 208 setEnabled(false); 209 } else if (!mIsAllowedByOrganization) { 210 EnforcedAdmin admin = 211 RestrictedLockUtilsInternal.checkIfInputMethodDisallowed(getContext(), 212 mImi.getPackageName(), UserHandle.myUserId()); 213 setDisabledByAdmin(admin); 214 } else { 215 setEnabled(true); 216 } 217 setChecked(mInputMethodSettingValues.isEnabledImi(mImi)); 218 if (!isDisabledByAdmin()) { 219 setSummary(getSummaryString()); 220 } 221 } 222 223 private InputMethodManager getInputMethodManager() { 224 return (InputMethodManager)getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 225 } 226 227 private String getSummaryString() { 228 final InputMethodManager imm = getInputMethodManager(); 229 final List<InputMethodSubtype> subtypes = imm.getEnabledInputMethodSubtypeList(mImi, true); 230 return InputMethodAndSubtypeUtil.getSubtypeLocaleNameListAsSentence( 231 subtypes, getContext(), mImi); 232 } 233 234 private void setCheckedInternal(boolean checked) { 235 super.setChecked(checked); 236 mOnSaveListener.onSaveInputMethodPreference(InputMethodPreference.this); 237 notifyChanged(); 238 } 239 240 private void showSecurityWarnDialog() { 241 if (mDialog != null && mDialog.isShowing()) { 242 mDialog.dismiss(); 243 } 244 final Context context = getContext(); 245 final AlertDialog.Builder builder = new AlertDialog.Builder(context); 246 builder.setCancelable(true /* cancelable */); 247 builder.setTitle(android.R.string.dialog_alert_title); 248 final CharSequence label = mImi.getServiceInfo().applicationInfo.loadLabel( 249 context.getPackageManager()); 250 builder.setMessage(context.getString(R.string.ime_security_warning, label)); 251 builder.setPositiveButton(android.R.string.ok, (dialog, which) -> { 252 // The user confirmed to enable a 3rd party IME, but we might 253 // need to prompt if it's not Direct Boot aware. 254 // TV doesn't doesn't need to worry about this, but other platforms should show 255 // a warning. 256 if (mImi.getServiceInfo().directBootAware || isTv()) { 257 setCheckedInternal(true); 258 } else { 259 showDirectBootWarnDialog(); 260 } 261 }); 262 builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> { 263 // The user canceled to enable a 3rd party IME. 264 setCheckedInternal(false); 265 }); 266 builder.setOnCancelListener((dialog) -> { 267 // The user canceled to enable a 3rd party IME. 268 setCheckedInternal(false); 269 }); 270 mDialog = builder.create(); 271 mDialog.show(); 272 } 273 274 private boolean isTv() { 275 return (getContext().getResources().getConfiguration().uiMode 276 & Configuration.UI_MODE_TYPE_MASK) == Configuration.UI_MODE_TYPE_TELEVISION; 277 } 278 279 private void showDirectBootWarnDialog() { 280 if (mDialog != null && mDialog.isShowing()) { 281 mDialog.dismiss(); 282 } 283 final Context context = getContext(); 284 final AlertDialog.Builder builder = new AlertDialog.Builder(context); 285 builder.setCancelable(true /* cancelable */); 286 builder.setMessage(context.getText(R.string.direct_boot_unaware_dialog_message)); 287 builder.setPositiveButton(android.R.string.ok, (dialog, which) -> setCheckedInternal(true)); 288 builder.setNegativeButton(android.R.string.cancel, 289 (dialog, which) -> setCheckedInternal(false)); 290 mDialog = builder.create(); 291 mDialog.show(); 292 } 293 294 public int compareTo(final InputMethodPreference rhs, final Collator collator) { 295 if (this == rhs) { 296 return 0; 297 } 298 if (mHasPriorityInSorting != rhs.mHasPriorityInSorting) { 299 // Prefer always checked system IMEs 300 return mHasPriorityInSorting ? -1 : 1; 301 } 302 final CharSequence title = getTitle(); 303 final CharSequence rhsTitle = rhs.getTitle(); 304 final boolean emptyTitle = TextUtils.isEmpty(title); 305 final boolean rhsEmptyTitle = TextUtils.isEmpty(rhsTitle); 306 if (!emptyTitle && !rhsEmptyTitle) { 307 return collator.compare(title.toString(), rhsTitle.toString()); 308 } 309 // For historical reasons, an empty text needs to be put at the first. 310 return (emptyTitle ? -1 : 0) - (rhsEmptyTitle ? -1 : 0); 311 } 312 } 313