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.preference.Preference; 25 import android.preference.Preference.OnPreferenceChangeListener; 26 import android.preference.Preference.OnPreferenceClickListener; 27 import android.preference.SwitchPreference; 28 import android.text.TextUtils; 29 import android.util.Log; 30 import android.view.inputmethod.InputMethodInfo; 31 import android.view.inputmethod.InputMethodManager; 32 import android.view.inputmethod.InputMethodSubtype; 33 import android.widget.Toast; 34 35 import com.android.internal.inputmethod.InputMethodUtils; 36 import com.android.settings.R; 37 38 import java.text.Collator; 39 import java.util.ArrayList; 40 import java.util.List; 41 42 /** 43 * Input method preference. 44 * 45 * This preference represents an IME. It is used for two purposes. 1) An instance with a switch 46 * is used to enable or disable the IME. 2) An instance without a switch is used to invoke the 47 * setting activity of the IME. 48 */ 49 class InputMethodPreference extends SwitchPreference implements OnPreferenceClickListener, 50 OnPreferenceChangeListener { 51 private static final String TAG = InputMethodPreference.class.getSimpleName(); 52 private static final String EMPTY_TEXT = ""; 53 54 interface OnSavePreferenceListener { 55 /** 56 * Called when this preference needs to be saved its state. 57 * 58 * Note that this preference is non-persistent and needs explicitly to be saved its state. 59 * Because changing one IME state may change other IMEs' state, this is a place to update 60 * other IMEs' state as well. 61 * 62 * @param pref This preference. 63 */ 64 public void onSaveInputMethodPreference(InputMethodPreference pref); 65 } 66 67 private final InputMethodInfo mImi; 68 private final boolean mHasPriorityInSorting; 69 private final OnSavePreferenceListener mOnSaveListener; 70 private final InputMethodSettingValuesWrapper mInputMethodSettingValues; 71 private final boolean mIsAllowedByOrganization; 72 73 private AlertDialog mDialog = null; 74 75 /** 76 * A preference entry of an input method. 77 * 78 * @param context The Context this is associated with. 79 * @param imi The {@link InputMethodInfo} of this preference. 80 * @param isImeEnabler true if this preference is the IME enabler that has enable/disable 81 * switches for all available IMEs, not the list of enabled IMEs. 82 * @param isAllowedByOrganization false if the IME has been disabled by a device or profile 83 owner. 84 * @param onSaveListener The listener called when this preference has been changed and needs 85 * to save the state to shared preference. 86 */ 87 InputMethodPreference(final Context context, final InputMethodInfo imi, 88 final boolean isImeEnabler, final boolean isAllowedByOrganization, 89 final OnSavePreferenceListener onSaveListener) { 90 super(context); 91 setPersistent(false); 92 mImi = imi; 93 mIsAllowedByOrganization = isAllowedByOrganization; 94 mOnSaveListener = onSaveListener; 95 if (!isImeEnabler) { 96 // Hide switch widget. 97 setWidgetLayoutResource(0 /* widgetLayoutResId */); 98 } 99 // Disable on/off switch texts. 100 setSwitchTextOn(EMPTY_TEXT); 101 setSwitchTextOff(EMPTY_TEXT); 102 setKey(imi.getId()); 103 setTitle(imi.loadLabel(context.getPackageManager())); 104 final String settingsActivity = imi.getSettingsActivity(); 105 if (TextUtils.isEmpty(settingsActivity)) { 106 setIntent(null); 107 } else { 108 // Set an intent to invoke settings activity of an input method. 109 final Intent intent = new Intent(Intent.ACTION_MAIN); 110 intent.setClassName(imi.getPackageName(), settingsActivity); 111 setIntent(intent); 112 } 113 mInputMethodSettingValues = InputMethodSettingValuesWrapper.getInstance(context); 114 mHasPriorityInSorting = InputMethodUtils.isSystemIme(imi) 115 && mInputMethodSettingValues.isValidSystemNonAuxAsciiCapableIme(imi, context); 116 setOnPreferenceClickListener(this); 117 setOnPreferenceChangeListener(this); 118 } 119 120 public InputMethodInfo getInputMethodInfo() { 121 return mImi; 122 } 123 124 private boolean isImeEnabler() { 125 // If this {@link SwitchPreference} doesn't have a widget layout, we explicitly hide the 126 // switch widget at constructor. 127 return getWidgetLayoutResource() != 0; 128 } 129 130 @Override 131 public boolean onPreferenceChange(final Preference preference, final Object newValue) { 132 // Always returns false to prevent default behavior. 133 // See {@link TwoStatePreference#onClick()}. 134 if (!isImeEnabler()) { 135 // Prevent disabling an IME because this preference is for invoking a settings activity. 136 return false; 137 } 138 if (isChecked()) { 139 // Disable this IME. 140 setChecked(false); 141 mOnSaveListener.onSaveInputMethodPreference(this); 142 return false; 143 } 144 if (InputMethodUtils.isSystemIme(mImi)) { 145 // Enable a system IME. No need to show a security warning dialog. 146 setChecked(true); 147 mOnSaveListener.onSaveInputMethodPreference(this); 148 return false; 149 } 150 // Enable a 3rd party IME. 151 showSecurityWarnDialog(mImi); 152 return false; 153 } 154 155 @Override 156 public boolean onPreferenceClick(final Preference preference) { 157 // Always returns true to prevent invoking an intent without catching exceptions. 158 // See {@link Preference#performClick(PreferenceScreen)}/ 159 if (isImeEnabler()) { 160 // Prevent invoking a settings activity because this preference is for enabling and 161 // disabling an input method. 162 return true; 163 } 164 final Context context = getContext(); 165 try { 166 final Intent intent = getIntent(); 167 if (intent != null) { 168 // Invoke a settings activity of an input method. 169 context.startActivity(intent); 170 } 171 } catch (final ActivityNotFoundException e) { 172 Log.d(TAG, "IME's Settings Activity Not Found", e); 173 final String message = context.getString( 174 R.string.failed_to_open_app_settings_toast, 175 mImi.loadLabel(context.getPackageManager())); 176 Toast.makeText(context, message, Toast.LENGTH_LONG).show(); 177 } 178 return true; 179 } 180 181 void updatePreferenceViews() { 182 final boolean isAlwaysChecked = mInputMethodSettingValues.isAlwaysCheckedIme( 183 mImi, getContext()); 184 // Only when this preference has a switch and an input method should be always enabled, 185 // this preference should be disabled to prevent accidentally disabling an input method. 186 setEnabled(!((isAlwaysChecked && isImeEnabler()) || (!mIsAllowedByOrganization))); 187 setChecked(mInputMethodSettingValues.isEnabledImi(mImi)); 188 setSummary(getSummaryString()); 189 } 190 191 private InputMethodManager getInputMethodManager() { 192 return (InputMethodManager)getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 193 } 194 195 private String getSummaryString() { 196 final Context context = getContext(); 197 if (!mIsAllowedByOrganization) { 198 return context.getString(R.string.accessibility_feature_or_input_method_not_allowed); 199 } 200 final InputMethodManager imm = getInputMethodManager(); 201 final List<InputMethodSubtype> subtypes = imm.getEnabledInputMethodSubtypeList(mImi, true); 202 final ArrayList<CharSequence> subtypeLabels = new ArrayList<>(); 203 for (final InputMethodSubtype subtype : subtypes) { 204 final CharSequence label = subtype.getDisplayName( 205 context, mImi.getPackageName(), mImi.getServiceInfo().applicationInfo); 206 subtypeLabels.add(label); 207 } 208 // TODO: A delimiter of subtype labels should be localized. 209 return TextUtils.join(", ", subtypeLabels); 210 } 211 212 private void showSecurityWarnDialog(final InputMethodInfo imi) { 213 if (mDialog != null && mDialog.isShowing()) { 214 mDialog.dismiss(); 215 } 216 final Context context = getContext(); 217 final AlertDialog.Builder builder = new AlertDialog.Builder(context); 218 builder.setCancelable(true /* cancelable */); 219 builder.setTitle(android.R.string.dialog_alert_title); 220 final CharSequence label = imi.getServiceInfo().applicationInfo.loadLabel( 221 context.getPackageManager()); 222 builder.setMessage(context.getString(R.string.ime_security_warning, label)); 223 builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { 224 @Override 225 public void onClick(final DialogInterface dialog, final int which) { 226 // The user confirmed to enable a 3rd party IME. 227 setChecked(true); 228 mOnSaveListener.onSaveInputMethodPreference(InputMethodPreference.this); 229 notifyChanged(); 230 } 231 }); 232 builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() { 233 @Override 234 public void onClick(final DialogInterface dialog, final int which) { 235 // The user canceled to enable a 3rd party IME. 236 setChecked(false); 237 mOnSaveListener.onSaveInputMethodPreference(InputMethodPreference.this); 238 notifyChanged(); 239 } 240 }); 241 mDialog = builder.create(); 242 mDialog.show(); 243 } 244 245 int compareTo(final InputMethodPreference rhs, final Collator collator) { 246 if (this == rhs) { 247 return 0; 248 } 249 if (mHasPriorityInSorting == rhs.mHasPriorityInSorting) { 250 final CharSequence t0 = getTitle(); 251 final CharSequence t1 = rhs.getTitle(); 252 if (TextUtils.isEmpty(t0)) { 253 return 1; 254 } 255 if (TextUtils.isEmpty(t1)) { 256 return -1; 257 } 258 return collator.compare(t0.toString(), t1.toString()); 259 } 260 // Prefer always checked system IMEs 261 return mHasPriorityInSorting ? -1 : 1; 262 } 263 } 264