diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index f14d0d128e673..ad985c7e5f34d 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -928,4 +928,18 @@ [CHAR LIMIT=35] --> Use system languages + + Failed to open settings for %1$s + + + This input method may be able to collect + all the text you type, including personal data like passwords and credit + card numbers. It comes from the app + %1$s. + Use this input method? + + + Note: After a reboot, this app can\'t start until you unlock your phone + diff --git a/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodPreference.java b/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodPreference.java new file mode 100755 index 0000000000000..1bbc878b56c95 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/inputmethod/InputMethodPreference.java @@ -0,0 +1,300 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.settingslib.inputmethod; + +import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; + +import android.app.AlertDialog; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.os.UserHandle; +import android.support.v7.preference.Preference; +import android.support.v7.preference.Preference.OnPreferenceChangeListener; +import android.support.v7.preference.Preference.OnPreferenceClickListener; +import android.text.TextUtils; +import android.util.Log; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodManager; +import android.view.inputmethod.InputMethodSubtype; +import android.widget.Toast; + +import com.android.internal.inputmethod.InputMethodUtils; +import com.android.settingslib.R; +import com.android.settingslib.RestrictedLockUtils; +import com.android.settingslib.RestrictedSwitchPreference; + +import java.text.Collator; +import java.util.List; + +/** + * Input method preference. + * + * This preference represents an IME. It is used for two purposes. 1) An instance with a switch + * is used to enable or disable the IME. 2) An instance without a switch is used to invoke the + * setting activity of the IME. + */ +public class InputMethodPreference extends RestrictedSwitchPreference implements OnPreferenceClickListener, + OnPreferenceChangeListener { + private static final String TAG = InputMethodPreference.class.getSimpleName(); + private static final String EMPTY_TEXT = ""; + private static final int NO_WIDGET = 0; + + public interface OnSavePreferenceListener { + /** + * Called when this preference needs to be saved its state. + * + * Note that this preference is non-persistent and needs explicitly to be saved its state. + * Because changing one IME state may change other IMEs' state, this is a place to update + * other IMEs' state as well. + * + * @param pref This preference. + */ + void onSaveInputMethodPreference(InputMethodPreference pref); + } + + private final InputMethodInfo mImi; + private final boolean mHasPriorityInSorting; + private final OnSavePreferenceListener mOnSaveListener; + private final InputMethodSettingValuesWrapper mInputMethodSettingValues; + private final boolean mIsAllowedByOrganization; + + private AlertDialog mDialog = null; + + /** + * A preference entry of an input method. + * + * @param context The Context this is associated with. + * @param imi The {@link InputMethodInfo} of this preference. + * @param isImeEnabler true if this preference is the IME enabler that has enable/disable + * switches for all available IMEs, not the list of enabled IMEs. + * @param isAllowedByOrganization false if the IME has been disabled by a device or profile + * owner. + * @param onSaveListener The listener called when this preference has been changed and needs + * to save the state to shared preference. + */ + public InputMethodPreference(final Context context, final InputMethodInfo imi, + final boolean isImeEnabler, final boolean isAllowedByOrganization, + final OnSavePreferenceListener onSaveListener) { + super(context); + setPersistent(false); + mImi = imi; + mIsAllowedByOrganization = isAllowedByOrganization; + mOnSaveListener = onSaveListener; + if (!isImeEnabler) { + // Remove switch widget. + setWidgetLayoutResource(NO_WIDGET); + } + // Disable on/off switch texts. + setSwitchTextOn(EMPTY_TEXT); + setSwitchTextOff(EMPTY_TEXT); + setKey(imi.getId()); + setTitle(imi.loadLabel(context.getPackageManager())); + final String settingsActivity = imi.getSettingsActivity(); + if (TextUtils.isEmpty(settingsActivity)) { + setIntent(null); + } else { + // Set an intent to invoke settings activity of an input method. + final Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setClassName(imi.getPackageName(), settingsActivity); + setIntent(intent); + } + mInputMethodSettingValues = InputMethodSettingValuesWrapper.getInstance(context); + mHasPriorityInSorting = InputMethodUtils.isSystemIme(imi) + && mInputMethodSettingValues.isValidSystemNonAuxAsciiCapableIme(imi, context); + setOnPreferenceClickListener(this); + setOnPreferenceChangeListener(this); + } + + public InputMethodInfo getInputMethodInfo() { + return mImi; + } + + private boolean isImeEnabler() { + // If this {@link SwitchPreference} doesn't have a widget layout, we explicitly hide the + // switch widget at constructor. + return getWidgetLayoutResource() != NO_WIDGET; + } + + @Override + public boolean onPreferenceChange(final Preference preference, final Object newValue) { + // Always returns false to prevent default behavior. + // See {@link TwoStatePreference#onClick()}. + if (!isImeEnabler()) { + // Prevent disabling an IME because this preference is for invoking a settings activity. + return false; + } + if (isChecked()) { + // Disable this IME. + setCheckedInternal(false); + return false; + } + if (InputMethodUtils.isSystemIme(mImi)) { + // Enable a system IME. No need to show a security warning dialog, + // but we might need to prompt if it's not Direct Boot aware. + // TV doesn't doesn't need to worry about this, but other platforms should show + // a warning. + if (mImi.getServiceInfo().directBootAware || isTv()) { + setCheckedInternal(true); + } else if (!isTv()){ + showDirectBootWarnDialog(); + } + } else { + // Once security is confirmed, we might prompt if the IME isn't + // Direct Boot aware. + showSecurityWarnDialog(); + } + return false; + } + + @Override + public boolean onPreferenceClick(final Preference preference) { + // Always returns true to prevent invoking an intent without catching exceptions. + // See {@link Preference#performClick(PreferenceScreen)}/ + if (isImeEnabler()) { + // Prevent invoking a settings activity because this preference is for enabling and + // disabling an input method. + return true; + } + final Context context = getContext(); + try { + final Intent intent = getIntent(); + if (intent != null) { + // Invoke a settings activity of an input method. + context.startActivity(intent); + } + } catch (final ActivityNotFoundException e) { + Log.d(TAG, "IME's Settings Activity Not Found", e); + final String message = context.getString( + R.string.failed_to_open_app_settings_toast, + mImi.loadLabel(context.getPackageManager())); + Toast.makeText(context, message, Toast.LENGTH_LONG).show(); + } + return true; + } + + public void updatePreferenceViews() { + final boolean isAlwaysChecked = mInputMethodSettingValues.isAlwaysCheckedIme( + mImi, getContext()); + // When this preference has a switch and an input method should be always enabled, + // this preference should be disabled to prevent accidentally disabling an input method. + // This preference should also be disabled in case the admin does not allow this input + // method. + if (isAlwaysChecked && isImeEnabler()) { + setDisabledByAdmin(null); + setEnabled(false); + } else if (!mIsAllowedByOrganization) { + EnforcedAdmin admin = + RestrictedLockUtils.checkIfInputMethodDisallowed(getContext(), + mImi.getPackageName(), UserHandle.myUserId()); + setDisabledByAdmin(admin); + } else { + setEnabled(true); + } + setChecked(mInputMethodSettingValues.isEnabledImi(mImi)); + if (!isDisabledByAdmin()) { + setSummary(getSummaryString()); + } + } + + private InputMethodManager getInputMethodManager() { + return (InputMethodManager)getContext().getSystemService(Context.INPUT_METHOD_SERVICE); + } + + private String getSummaryString() { + final InputMethodManager imm = getInputMethodManager(); + final List subtypes = imm.getEnabledInputMethodSubtypeList(mImi, true); + return InputMethodAndSubtypeUtil.getSubtypeLocaleNameListAsSentence( + subtypes, getContext(), mImi); + } + + private void setCheckedInternal(boolean checked) { + super.setChecked(checked); + mOnSaveListener.onSaveInputMethodPreference(InputMethodPreference.this); + notifyChanged(); + } + + private void showSecurityWarnDialog() { + if (mDialog != null && mDialog.isShowing()) { + mDialog.dismiss(); + } + final Context context = getContext(); + final AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setCancelable(true /* cancelable */); + builder.setTitle(android.R.string.dialog_alert_title); + final CharSequence label = mImi.getServiceInfo().applicationInfo.loadLabel( + context.getPackageManager()); + builder.setMessage(context.getString(R.string.ime_security_warning, label)); + builder.setPositiveButton(android.R.string.ok, (dialog, which) -> { + // The user confirmed to enable a 3rd party IME, but we might + // need to prompt if it's not Direct Boot aware. + // TV doesn't doesn't need to worry about this, but other platforms should show + // a warning. + if (mImi.getServiceInfo().directBootAware || isTv()) { + setCheckedInternal(true); + } else { + showDirectBootWarnDialog(); + } + }); + builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> { + // The user canceled to enable a 3rd party IME. + setCheckedInternal(false); + }); + mDialog = builder.create(); + mDialog.show(); + } + + private boolean isTv() { + return (getContext().getResources().getConfiguration().uiMode + & Configuration.UI_MODE_TYPE_MASK) == Configuration.UI_MODE_TYPE_TELEVISION; + } + + private void showDirectBootWarnDialog() { + if (mDialog != null && mDialog.isShowing()) { + mDialog.dismiss(); + } + final Context context = getContext(); + final AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setCancelable(true /* cancelable */); + builder.setMessage(context.getText(R.string.direct_boot_unaware_dialog_message)); + builder.setPositiveButton(android.R.string.ok, (dialog, which) -> setCheckedInternal(true)); + builder.setNegativeButton(android.R.string.cancel, + (dialog, which) -> setCheckedInternal(false)); + mDialog = builder.create(); + mDialog.show(); + } + + public int compareTo(final InputMethodPreference rhs, final Collator collator) { + if (this == rhs) { + return 0; + } + if (mHasPriorityInSorting == rhs.mHasPriorityInSorting) { + final CharSequence t0 = getTitle(); + final CharSequence t1 = rhs.getTitle(); + if (TextUtils.isEmpty(t0)) { + return 1; + } + if (TextUtils.isEmpty(t1)) { + return -1; + } + return collator.compare(t0.toString(), t1.toString()); + } + // Prefer always checked system IMEs + return mHasPriorityInSorting ? -1 : 1; + } +}