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