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