blob: 3fc38b48c044256d322d9fbc70c715a32b2bcc25 [file] [log] [blame]
/*
* Copyright (C) 2019 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.car.settings.inputmethod;
import static com.android.car.settings.enterprise.ActionDisabledByAdminDialogFragment.DISABLED_BY_ADMIN_CONFIRM_DIALOG_TAG;
import android.app.admin.DevicePolicyManager;
import android.car.drivingstate.CarUxRestrictions;
import android.content.Context;
import android.content.pm.PackageManager;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodManager;
import androidx.annotation.VisibleForTesting;
import androidx.preference.PreferenceGroup;
import com.android.car.settings.R;
import com.android.car.settings.common.ConfirmationDialogFragment;
import com.android.car.settings.common.FragmentController;
import com.android.car.settings.common.PreferenceController;
import com.android.car.settings.enterprise.EnterpriseUtils;
import com.android.car.ui.preference.CarUiSwitchPreference;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/** Updates the available keyboard list. */
public class KeyboardManagementPreferenceController extends
PreferenceController<PreferenceGroup> {
@VisibleForTesting
static final String DIRECT_BOOT_WARN_DIALOG_TAG = "DirectBootWarnDialog";
@VisibleForTesting
static final String SECURITY_WARN_DIALOG_TAG = "SecurityWarnDialog";
private static final String KEY_INPUT_METHOD_INFO = "INPUT_METHOD_INFO";
private final InputMethodManager mInputMethodManager;
private final DevicePolicyManager mDevicePolicyManager;
private final PackageManager mPackageManager;
private final ConfirmationDialogFragment.ConfirmListener mDirectBootWarnConfirmListener =
args -> {
InputMethodInfo inputMethodInfo = args.getParcelable(KEY_INPUT_METHOD_INFO);
InputMethodUtil.enableInputMethod(getContext().getContentResolver(),
inputMethodInfo);
refreshUi();
};
private final ConfirmationDialogFragment.RejectListener mRejectListener = args ->
refreshUi();
private final ConfirmationDialogFragment.ConfirmListener mSecurityWarnDialogConfirmListener =
args -> {
InputMethodInfo inputMethodInfo = args.getParcelable(KEY_INPUT_METHOD_INFO);
// The user confirmed to enable a 3rd party IME, but we might need to prompt if
// it's not
// Direct Boot aware.
if (inputMethodInfo.getServiceInfo().directBootAware) {
InputMethodUtil.enableInputMethod(getContext().getContentResolver(),
inputMethodInfo);
refreshUi();
} else {
showDirectBootWarnDialog(inputMethodInfo);
}
};
public KeyboardManagementPreferenceController(Context context, String preferenceKey,
FragmentController fragmentController, CarUxRestrictions uxRestrictions) {
super(context, preferenceKey, fragmentController, uxRestrictions);
mPackageManager = context.getPackageManager();
mDevicePolicyManager = context.getSystemService(DevicePolicyManager.class);
mInputMethodManager = context.getSystemService(InputMethodManager.class);
}
@Override
protected void onCreateInternal() {
super.onCreateInternal();
ConfirmationDialogFragment dialogFragment = (ConfirmationDialogFragment)
getFragmentController().findDialogByTag(DIRECT_BOOT_WARN_DIALOG_TAG);
ConfirmationDialogFragment.resetListeners(dialogFragment,
mDirectBootWarnConfirmListener,
mRejectListener,
/* neutralListener= */ null);
dialogFragment = (ConfirmationDialogFragment) getFragmentController()
.findDialogByTag(SECURITY_WARN_DIALOG_TAG);
ConfirmationDialogFragment.resetListeners(dialogFragment,
mSecurityWarnDialogConfirmListener,
mRejectListener,
/* neutralListener= */ null);
}
@Override
protected Class<PreferenceGroup> getPreferenceType() {
return PreferenceGroup.class;
}
@Override
protected void updateState(PreferenceGroup preferenceGroup) {
List<String> permittedInputMethods = mDevicePolicyManager
.getPermittedInputMethodsForCurrentUser();
Set<String> permittedInputMethodsSet = permittedInputMethods == null ? null : new HashSet<>(
permittedInputMethods);
preferenceGroup.removeAll();
List<InputMethodInfo> inputMethodInfos = mInputMethodManager.getInputMethodList();
if (inputMethodInfos == null || inputMethodInfos.size() == 0) {
return;
}
Collections.sort(inputMethodInfos, Comparator.comparing(
(InputMethodInfo a) -> InputMethodUtil.getPackageLabel(mPackageManager, a))
.thenComparing((InputMethodInfo a) -> InputMethodUtil.getSummaryString(getContext(),
mInputMethodManager, a)));
for (InputMethodInfo inputMethodInfo : inputMethodInfos) {
// Hide "Google voice typing" IME.
if (inputMethodInfo.getPackageName().equals(InputMethodUtil.GOOGLE_VOICE_TYPING)) {
continue;
}
preferenceGroup
.addPreference(createSwitchPreference(permittedInputMethodsSet, inputMethodInfo));
}
}
private boolean isInputMethodAllowedByOrganization(Set<String> permittedList,
InputMethodInfo inputMethodInfo) {
// If an input method is enabled but not included in the permitted list, then set it as
// allowed by organization. Doing so will allow the user to disable the input method and
// remain complaint with the organization's policy. Once disabled, the input method
// cannot be re-enabled because it is not in the permitted list. Note: permittedList
// is null means that all input methods are allowed.
return (permittedList == null)
|| permittedList.contains(inputMethodInfo.getPackageName())
|| isInputMethodEnabled(inputMethodInfo);
}
private boolean isInputMethodEnabled(InputMethodInfo inputMethodInfo) {
return InputMethodUtil.isInputMethodEnabled(
getContext().getContentResolver(), inputMethodInfo);
}
/**
* Check if given input method is the only enabled input method that can be a default system
* input method.
*
* @return {@code true} if input method is the only input method that can be a default system
* input method.
*/
private boolean isOnlyEnabledDefaultInputMethod(InputMethodInfo inputMethodInfo) {
if (!inputMethodInfo.isDefault(getContext())) {
return false;
}
List<InputMethodInfo> inputMethodInfos = mInputMethodManager.getEnabledInputMethodList();
for (InputMethodInfo imi : inputMethodInfos) {
if (!imi.isDefault(getContext())) {
continue;
}
if (!imi.getId().equals(inputMethodInfo.getId())) {
return false;
}
}
return true;
}
/**
* Create a CarUiSwitchPreference to enable/disable an input method.
*
* @return {@code CarUiSwitchPreference} which allows a user to enable/disable an input method.
*/
private CarUiSwitchPreference createSwitchPreference(Set<String> permittedInputMethodsSet,
InputMethodInfo inputMethodInfo) {
CarUiSwitchPreference switchPreference = new CarUiSwitchPreference(getContext());
switchPreference.setKey(String.valueOf(inputMethodInfo.getId()));
switchPreference.setIcon(InputMethodUtil.getPackageIcon(mPackageManager, inputMethodInfo));
switchPreference.setTitle(InputMethodUtil.getPackageLabel(mPackageManager,
inputMethodInfo));
switchPreference.setChecked(InputMethodUtil.isInputMethodEnabled(getContext()
.getContentResolver(), inputMethodInfo));
switchPreference.setSummary(InputMethodUtil.getSummaryString(getContext(),
mInputMethodManager, inputMethodInfo));
// A switch preference for any disabled IME should be enabled. This is due to the
// possibility of having only one default IME that is disabled, which would prevent the IME
// from being enabled without another default input method that is enabled being present.
if (!isInputMethodEnabled(inputMethodInfo)) {
switchPreference.setEnabled(true);
} else {
switchPreference.setEnabled(!isOnlyEnabledDefaultInputMethod(inputMethodInfo));
}
if (!isInputMethodAllowedByOrganization(permittedInputMethodsSet, inputMethodInfo)) {
switchPreference.setEnabled(false);
setClickableWhileDisabled(switchPreference, /* clickable= */ true, p ->
showActionDisabledByAdminDialog(inputMethodInfo.getPackageName()));
return switchPreference;
}
switchPreference.setOnPreferenceChangeListener((switchPref, newValue) -> {
boolean enable = (boolean) newValue;
if (enable) {
showSecurityWarnDialog(inputMethodInfo);
} else {
InputMethodUtil.disableInputMethod(getContext(), mInputMethodManager,
inputMethodInfo);
refreshUi();
}
return false;
});
return switchPreference;
}
private void showDirectBootWarnDialog(InputMethodInfo inputMethodInfo) {
ConfirmationDialogFragment dialog = new ConfirmationDialogFragment.Builder(getContext())
.setMessage(getContext().getString(R.string.direct_boot_unaware_dialog_message_car))
.setPositiveButton(android.R.string.ok, mDirectBootWarnConfirmListener)
.setNegativeButton(android.R.string.cancel, mRejectListener)
.addArgumentParcelable(KEY_INPUT_METHOD_INFO, inputMethodInfo)
.build();
getFragmentController().showDialog(dialog, DIRECT_BOOT_WARN_DIALOG_TAG);
}
private void showSecurityWarnDialog(InputMethodInfo inputMethodInfo) {
CharSequence label = inputMethodInfo.loadLabel(mPackageManager);
ConfirmationDialogFragment dialog = new ConfirmationDialogFragment.Builder(getContext())
.setTitle(android.R.string.dialog_alert_title)
.setMessage(getContext().getString(R.string.ime_security_warning, label))
.setPositiveButton(android.R.string.ok, mSecurityWarnDialogConfirmListener)
.setNegativeButton(android.R.string.cancel, mRejectListener)
.addArgumentParcelable(KEY_INPUT_METHOD_INFO, inputMethodInfo)
.build();
getFragmentController().showDialog(dialog, SECURITY_WARN_DIALOG_TAG);
}
// TODO: ideally we need to refactor this method. See reasoning on
// EnterpriseUtils.DISABLED_INPUT_METHOD constant.
private void showActionDisabledByAdminDialog(String inputMethodPkg) {
getFragmentController().showDialog(
EnterpriseUtils.getActionDisabledByAdminDialog(getContext(),
EnterpriseUtils.DISABLED_INPUT_METHOD, inputMethodPkg),
DISABLED_BY_ADMIN_CONFIRM_DIALOG_TAG);
}
}