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 */ 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 */ 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 125 public InputMethodInfo getInputMethodInfo() { 126 return mImi; 127 } 128 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 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 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 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 214 private InputMethodManager getInputMethodManager() { 215 return (InputMethodManager)getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 216 } 217 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 225 private void setCheckedInternal(boolean checked) { 226 super.setChecked(checked); 227 mOnSaveListener.onSaveInputMethodPreference(InputMethodPreference.this); 228 notifyChanged(); 229 } 230 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 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 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