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