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 */ onSaveInputMethodPreference(InputMethodPreference pref)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 */ InputMethodPreference(final Context context, final InputMethodInfo imi, final boolean isImeEnabler, final boolean isAllowedByOrganization, final OnSavePreferenceListener onSaveListener)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 getInputMethodInfo()124 public InputMethodInfo getInputMethodInfo() { 125 return mImi; 126 } 127 isImeEnabler()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 onPreferenceChange(final Preference preference, final Object newValue)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 onPreferenceClick(final Preference preference)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 updatePreferenceViews()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 getInputMethodManager()215 private InputMethodManager getInputMethodManager() { 216 return (InputMethodManager)getContext().getSystemService(Context.INPUT_METHOD_SERVICE); 217 } 218 getSummaryString()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 setCheckedInternal(boolean checked)226 private void setCheckedInternal(boolean checked) { 227 super.setChecked(checked); 228 mOnSaveListener.onSaveInputMethodPreference(InputMethodPreference.this); 229 notifyChanged(); 230 } 231 showSecurityWarnDialog()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 isTv()262 private boolean isTv() { 263 return (getContext().getResources().getConfiguration().uiMode 264 & Configuration.UI_MODE_TYPE_MASK) == Configuration.UI_MODE_TYPE_TELEVISION; 265 } 266 showDirectBootWarnDialog()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 compareTo(final InputMethodPreference rhs, final Collator collator)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