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