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.support.v7.preference.Preference; 28 import android.support.v7.preference.Preference.OnPreferenceChangeListener; 29 import android.support.v7.preference.Preference.OnPreferenceClickListener; 30 import android.text.TextUtils; 31 import android.util.Log; 32 import android.view.inputmethod.InputMethodInfo; 33 import android.view.inputmethod.InputMethodManager; 34 import android.view.inputmethod.InputMethodSubtype; 35 import android.widget.Toast; 36 37 import com.android.internal.annotations.VisibleForTesting; 38 import com.android.internal.inputmethod.InputMethodUtils; 39 import com.android.settingslib.R; 40 import com.android.settingslib.RestrictedLockUtils; 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 } 102 103 @VisibleForTesting InputMethodPreference(final Context context, final InputMethodInfo imi, final CharSequence title, final boolean isAllowedByOrganization, final OnSavePreferenceListener onSaveListener)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 = InputMethodUtils.isSystemIme(imi) 128 && mInputMethodSettingValues.isValidSystemNonAuxAsciiCapableIme(imi, context); 129 setOnPreferenceClickListener(this); 130 setOnPreferenceChangeListener(this); 131 } 132 getInputMethodInfo()133 public InputMethodInfo getInputMethodInfo() { 134 return mImi; 135 } 136 isImeEnabler()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 onPreferenceChange(final Preference preference, final Object newValue)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 (InputMethodUtils.isSystemIme(mImi)) { 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 onPreferenceClick(final Preference preference)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 updatePreferenceViews()200 public void updatePreferenceViews() { 201 final boolean isAlwaysChecked = mInputMethodSettingValues.isAlwaysCheckedIme( 202 mImi, getContext()); 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 RestrictedLockUtils.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 mDialog = builder.create(); 268 mDialog.show(); 269 } 270 isTv()271 private boolean isTv() { 272 return (getContext().getResources().getConfiguration().uiMode 273 & Configuration.UI_MODE_TYPE_MASK) == Configuration.UI_MODE_TYPE_TELEVISION; 274 } 275 showDirectBootWarnDialog()276 private void showDirectBootWarnDialog() { 277 if (mDialog != null && mDialog.isShowing()) { 278 mDialog.dismiss(); 279 } 280 final Context context = getContext(); 281 final AlertDialog.Builder builder = new AlertDialog.Builder(context); 282 builder.setCancelable(true /* cancelable */); 283 builder.setMessage(context.getText(R.string.direct_boot_unaware_dialog_message)); 284 builder.setPositiveButton(android.R.string.ok, (dialog, which) -> setCheckedInternal(true)); 285 builder.setNegativeButton(android.R.string.cancel, 286 (dialog, which) -> setCheckedInternal(false)); 287 mDialog = builder.create(); 288 mDialog.show(); 289 } 290 compareTo(final InputMethodPreference rhs, final Collator collator)291 public int compareTo(final InputMethodPreference rhs, final Collator collator) { 292 if (this == rhs) { 293 return 0; 294 } 295 if (mHasPriorityInSorting != rhs.mHasPriorityInSorting) { 296 // Prefer always checked system IMEs 297 return mHasPriorityInSorting ? -1 : 1; 298 } 299 final CharSequence title = getTitle(); 300 final CharSequence rhsTitle = rhs.getTitle(); 301 final boolean emptyTitle = TextUtils.isEmpty(title); 302 final boolean rhsEmptyTitle = TextUtils.isEmpty(rhsTitle); 303 if (!emptyTitle && !rhsEmptyTitle) { 304 return collator.compare(title.toString(), rhsTitle.toString()); 305 } 306 // For historical reasons, an empty text needs to be put at the first. 307 return (emptyTitle ? -1 : 0) - (rhsEmptyTitle ? -1 : 0); 308 } 309 } 310