1 /* 2 * Copyright (C) 2011 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.settings.inputmethod; 18 19 import android.app.AlertDialog; 20 import android.content.ActivityNotFoundException; 21 import android.content.Context; 22 import android.content.DialogInterface; 23 import android.content.Intent; 24 import android.os.UserHandle; 25 import android.support.v14.preference.SwitchPreference; 26 import android.support.v7.preference.Preference; 27 import android.support.v7.preference.Preference.OnPreferenceChangeListener; 28 import android.support.v7.preference.Preference.OnPreferenceClickListener; 29 import android.text.TextUtils; 30 import android.util.Log; 31 import android.view.inputmethod.InputMethodInfo; 32 import android.view.inputmethod.InputMethodManager; 33 import android.view.inputmethod.InputMethodSubtype; 34 import android.widget.Toast; 35 36 import com.android.internal.inputmethod.InputMethodUtils; 37 import com.android.settings.R; 38 import com.android.settingslib.RestrictedLockUtils; 39 import com.android.settingslib.RestrictedSwitchPreference; 40 41 import java.text.Collator; 42 import java.util.List; 43 44 import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; 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 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 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 public 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 InputMethodPreference(final Context context, final InputMethodInfo imi, 93 final boolean isImeEnabler, final boolean isAllowedByOrganization, 94 final OnSavePreferenceListener onSaveListener) { 95 super(context); 96 setPersistent(false); 97 mImi = imi; 98 mIsAllowedByOrganization = isAllowedByOrganization; 99 mOnSaveListener = onSaveListener; 100 if (!isImeEnabler) { 101 // Remove switch widget. 102 setWidgetLayoutResource(NO_WIDGET); 103 } 104 // Disable on/off switch texts. 105 setSwitchTextOn(EMPTY_TEXT); 106 setSwitchTextOff(EMPTY_TEXT); 107 setKey(imi.getId()); 108 setTitle(imi.loadLabel(context.getPackageManager())); 109 final String settingsActivity = imi.getSettingsActivity(); 110 if (TextUtils.isEmpty(settingsActivity)) { 111 setIntent(null); 112 } else { 113 // Set an intent to invoke settings activity of an input method. 114 final Intent intent = new Intent(Intent.ACTION_MAIN); 115 intent.setClassName(imi.getPackageName(), settingsActivity); 116 setIntent(intent); 117 } 118 mInputMethodSettingValues = InputMethodSettingValuesWrapper.getInstance(context); 119 mHasPriorityInSorting = InputMethodUtils.isSystemIme(imi) 120 && mInputMethodSettingValues.isValidSystemNonAuxAsciiCapableIme(imi, context); 121 setOnPreferenceClickListener(this); 122 setOnPreferenceChangeListener(this); 123 } 124 getInputMethodInfo()125 public InputMethodInfo getInputMethodInfo() { 126 return mImi; 127 } 128 isImeEnabler()129 private boolean isImeEnabler() { 130 // If this {@link SwitchPreference} doesn't have a widget layout, we explicitly hide the 131 // switch widget at constructor. 132 return getWidgetLayoutResource() != NO_WIDGET; 133 } 134 135 @Override onPreferenceChange(final Preference preference, final Object newValue)136 public boolean onPreferenceChange(final Preference preference, final Object newValue) { 137 // Always returns false to prevent default behavior. 138 // See {@link TwoStatePreference#onClick()}. 139 if (!isImeEnabler()) { 140 // Prevent disabling an IME because this preference is for invoking a settings activity. 141 return false; 142 } 143 if (isChecked()) { 144 // Disable this IME. 145 setCheckedInternal(false); 146 return false; 147 } 148 if (InputMethodUtils.isSystemIme(mImi)) { 149 // Enable a system IME. No need to show a security warning dialog, 150 // but we might need to prompt if it's not Direct Boot aware. 151 if (mImi.getServiceInfo().directBootAware) { 152 setCheckedInternal(true); 153 } else { 154 showDirectBootWarnDialog(); 155 } 156 } else { 157 // Once security is confirmed, we might prompt if the IME isn't 158 // Direct Boot aware. 159 showSecurityWarnDialog(); 160 } 161 return false; 162 } 163 164 @Override onPreferenceClick(final Preference preference)165 public boolean onPreferenceClick(final Preference preference) { 166 // Always returns true to prevent invoking an intent without catching exceptions. 167 // See {@link Preference#performClick(PreferenceScreen)}/ 168 if (isImeEnabler()) { 169 // Prevent invoking a settings activity because this preference is for enabling and 170 // disabling an input method. 171 return true; 172 } 173 final Context context = getContext(); 174 try { 175 final Intent intent = getIntent(); 176 if (intent != null) { 177 // Invoke a settings activity of an input method. 178 context.startActivity(intent); 179 } 180 } catch (final ActivityNotFoundException e) { 181 Log.d(TAG, "IME's Settings Activity Not Found", e); 182 final String message = context.getString( 183 R.string.failed_to_open_app_settings_toast, 184 mImi.loadLabel(context.getPackageManager())); 185 Toast.makeText(context, message, Toast.LENGTH_LONG).show(); 186 } 187 return true; 188 } 189 updatePreferenceViews()190 void updatePreferenceViews() { 191 final boolean isAlwaysChecked = mInputMethodSettingValues.isAlwaysCheckedIme( 192 mImi, getContext()); 193 // When this preference has a switch and an input method should be always enabled, 194 // this preference should be disabled to prevent accidentally disabling an input method. 195 // This preference should also be disabled in case the admin does not allow this input 196 // method. 197 if (isAlwaysChecked && isImeEnabler()) { 198 setDisabledByAdmin(null); 199 setEnabled(false); 200 } else if (!mIsAllowedByOrganization) { 201 EnforcedAdmin admin = 202 RestrictedLockUtils.checkIfInputMethodDisallowed(getContext(), 203 mImi.getPackageName(), UserHandle.myUserId()); 204 setDisabledByAdmin(admin); 205 } else { 206 setEnabled(true); 207 } 208 setChecked(mInputMethodSettingValues.isEnabledImi(mImi)); 209 if (!isDisabledByAdmin()) { 210 setSummary(getSummaryString()); 211 } 212 } 213 getInputMethodManager()214 private InputMethodManager getInputMethodManager() { 215 return (InputMethodManager)getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 216 } 217 getSummaryString()218 private String getSummaryString() { 219 final InputMethodManager imm = getInputMethodManager(); 220 final List<InputMethodSubtype> subtypes = imm.getEnabledInputMethodSubtypeList(mImi, true); 221 return InputMethodAndSubtypeUtil.getSubtypeLocaleNameListAsSentence( 222 subtypes, getContext(), mImi); 223 } 224 setCheckedInternal(boolean checked)225 private void setCheckedInternal(boolean checked) { 226 super.setChecked(checked); 227 mOnSaveListener.onSaveInputMethodPreference(InputMethodPreference.this); 228 notifyChanged(); 229 } 230 showSecurityWarnDialog()231 private void showSecurityWarnDialog() { 232 if (mDialog != null && mDialog.isShowing()) { 233 mDialog.dismiss(); 234 } 235 final Context context = getContext(); 236 final AlertDialog.Builder builder = new AlertDialog.Builder(context); 237 builder.setCancelable(true /* cancelable */); 238 builder.setTitle(android.R.string.dialog_alert_title); 239 final CharSequence label = mImi.getServiceInfo().applicationInfo.loadLabel( 240 context.getPackageManager()); 241 builder.setMessage(context.getString(R.string.ime_security_warning, label)); 242 builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { 243 @Override 244 public void onClick(final DialogInterface dialog, final int which) { 245 // The user confirmed to enable a 3rd party IME, but we might 246 // need to prompt if it's not Direct Boot aware. 247 if (mImi.getServiceInfo().directBootAware) { 248 setCheckedInternal(true); 249 } else { 250 showDirectBootWarnDialog(); 251 } 252 } 253 }); 254 builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { 255 @Override 256 public void onClick(final DialogInterface dialog, final int which) { 257 // The user canceled to enable a 3rd party IME. 258 setCheckedInternal(false); 259 } 260 }); 261 mDialog = builder.create(); 262 mDialog.show(); 263 } 264 showDirectBootWarnDialog()265 private void showDirectBootWarnDialog() { 266 if (mDialog != null && mDialog.isShowing()) { 267 mDialog.dismiss(); 268 } 269 final Context context = getContext(); 270 final AlertDialog.Builder builder = new AlertDialog.Builder(context); 271 builder.setCancelable(true /* cancelable */); 272 builder.setMessage(context.getText(R.string.direct_boot_unaware_dialog_message)); 273 builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { 274 @Override 275 public void onClick(final DialogInterface dialog, final int which) { 276 setCheckedInternal(true); 277 } 278 }); 279 builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { 280 @Override 281 public void onClick(final DialogInterface dialog, final int which) { 282 setCheckedInternal(false); 283 } 284 }); 285 mDialog = builder.create(); 286 mDialog.show(); 287 } 288 compareTo(final InputMethodPreference rhs, final Collator collator)289 int compareTo(final InputMethodPreference rhs, final Collator collator) { 290 if (this == rhs) { 291 return 0; 292 } 293 if (mHasPriorityInSorting == rhs.mHasPriorityInSorting) { 294 final CharSequence t0 = getTitle(); 295 final CharSequence t1 = rhs.getTitle(); 296 if (TextUtils.isEmpty(t0)) { 297 return 1; 298 } 299 if (TextUtils.isEmpty(t1)) { 300 return -1; 301 } 302 return collator.compare(t0.toString(), t1.toString()); 303 } 304 // Prefer always checked system IMEs 305 return mHasPriorityInSorting ? -1 : 1; 306 } 307 } 308