1 /* 2 * Copyright (C) 2019 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.car.settings.inputmethod; 18 19 import static com.android.car.settings.enterprise.ActionDisabledByAdminDialogFragment.DISABLED_BY_ADMIN_CONFIRM_DIALOG_TAG; 20 21 import android.app.admin.DevicePolicyManager; 22 import android.car.drivingstate.CarUxRestrictions; 23 import android.content.Context; 24 import android.content.pm.PackageManager; 25 import android.view.inputmethod.InputMethodInfo; 26 import android.view.inputmethod.InputMethodManager; 27 28 import androidx.annotation.VisibleForTesting; 29 import androidx.preference.PreferenceGroup; 30 31 import com.android.car.settings.R; 32 import com.android.car.settings.common.ConfirmationDialogFragment; 33 import com.android.car.settings.common.FragmentController; 34 import com.android.car.settings.common.PreferenceController; 35 import com.android.car.settings.enterprise.EnterpriseUtils; 36 import com.android.car.ui.preference.CarUiSwitchPreference; 37 38 import java.util.Collections; 39 import java.util.Comparator; 40 import java.util.HashSet; 41 import java.util.List; 42 import java.util.Set; 43 44 /** Updates the available keyboard list. */ 45 public class KeyboardManagementPreferenceController extends 46 PreferenceController<PreferenceGroup> { 47 @VisibleForTesting 48 static final String DIRECT_BOOT_WARN_DIALOG_TAG = "DirectBootWarnDialog"; 49 @VisibleForTesting 50 static final String SECURITY_WARN_DIALOG_TAG = "SecurityWarnDialog"; 51 private static final String KEY_INPUT_METHOD_INFO = "INPUT_METHOD_INFO"; 52 private final InputMethodManager mInputMethodManager; 53 private final DevicePolicyManager mDevicePolicyManager; 54 private final PackageManager mPackageManager; 55 private final ConfirmationDialogFragment.ConfirmListener mDirectBootWarnConfirmListener = 56 args -> { 57 InputMethodInfo inputMethodInfo = args.getParcelable(KEY_INPUT_METHOD_INFO); 58 InputMethodUtil.enableInputMethod(getContext().getContentResolver(), 59 inputMethodInfo); 60 refreshUi(); 61 }; 62 private final ConfirmationDialogFragment.RejectListener mRejectListener = args -> 63 refreshUi(); 64 private final ConfirmationDialogFragment.ConfirmListener mSecurityWarnDialogConfirmListener = 65 args -> { 66 InputMethodInfo inputMethodInfo = args.getParcelable(KEY_INPUT_METHOD_INFO); 67 // The user confirmed to enable a 3rd party IME, but we might need to prompt if 68 // it's not 69 // Direct Boot aware. 70 if (inputMethodInfo.getServiceInfo().directBootAware) { 71 InputMethodUtil.enableInputMethod(getContext().getContentResolver(), 72 inputMethodInfo); 73 refreshUi(); 74 } else { 75 showDirectBootWarnDialog(inputMethodInfo); 76 } 77 }; 78 KeyboardManagementPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions)79 public KeyboardManagementPreferenceController(Context context, String preferenceKey, 80 FragmentController fragmentController, CarUxRestrictions uxRestrictions) { 81 super(context, preferenceKey, fragmentController, uxRestrictions); 82 mPackageManager = context.getPackageManager(); 83 mDevicePolicyManager = context.getSystemService(DevicePolicyManager.class); 84 mInputMethodManager = context.getSystemService(InputMethodManager.class); 85 } 86 87 @Override onCreateInternal()88 protected void onCreateInternal() { 89 super.onCreateInternal(); 90 91 ConfirmationDialogFragment dialogFragment = (ConfirmationDialogFragment) 92 getFragmentController().findDialogByTag(DIRECT_BOOT_WARN_DIALOG_TAG); 93 ConfirmationDialogFragment.resetListeners(dialogFragment, 94 mDirectBootWarnConfirmListener, 95 mRejectListener, 96 /* neutralListener= */ null); 97 98 dialogFragment = (ConfirmationDialogFragment) getFragmentController() 99 .findDialogByTag(SECURITY_WARN_DIALOG_TAG); 100 ConfirmationDialogFragment.resetListeners(dialogFragment, 101 mSecurityWarnDialogConfirmListener, 102 mRejectListener, 103 /* neutralListener= */ null); 104 } 105 106 @Override getPreferenceType()107 protected Class<PreferenceGroup> getPreferenceType() { 108 return PreferenceGroup.class; 109 } 110 111 @Override updateState(PreferenceGroup preferenceGroup)112 protected void updateState(PreferenceGroup preferenceGroup) { 113 List<String> permittedInputMethods = mDevicePolicyManager 114 .getPermittedInputMethodsForCurrentUser(); 115 Set<String> permittedInputMethodsSet = permittedInputMethods == null ? null : new HashSet<>( 116 permittedInputMethods); 117 118 preferenceGroup.removeAll(); 119 120 List<InputMethodInfo> inputMethodInfos = mInputMethodManager.getInputMethodList(); 121 if (inputMethodInfos == null || inputMethodInfos.size() == 0) { 122 return; 123 } 124 125 Collections.sort(inputMethodInfos, Comparator.comparing( 126 (InputMethodInfo a) -> InputMethodUtil.getPackageLabel(mPackageManager, a)) 127 .thenComparing((InputMethodInfo a) -> InputMethodUtil.getSummaryString(getContext(), 128 mInputMethodManager, a))); 129 130 for (InputMethodInfo inputMethodInfo : inputMethodInfos) { 131 // Hide "Google voice typing" IME. 132 if (InputMethodUtil.GVT_PACKAGE_NAMES.contains(inputMethodInfo.getPackageName())) { 133 continue; 134 } 135 136 preferenceGroup 137 .addPreference(createSwitchPreference(permittedInputMethodsSet, inputMethodInfo)); 138 } 139 } 140 isInputMethodAllowedByOrganization(Set<String> permittedList, InputMethodInfo inputMethodInfo)141 private boolean isInputMethodAllowedByOrganization(Set<String> permittedList, 142 InputMethodInfo inputMethodInfo) { 143 // If an input method is enabled but not included in the permitted list, then set it as 144 // allowed by organization. Doing so will allow the user to disable the input method and 145 // remain complaint with the organization's policy. Once disabled, the input method 146 // cannot be re-enabled because it is not in the permitted list. Note: permittedList 147 // is null means that all input methods are allowed. 148 return (permittedList == null) 149 || permittedList.contains(inputMethodInfo.getPackageName()) 150 || isInputMethodEnabled(inputMethodInfo); 151 } 152 isInputMethodEnabled(InputMethodInfo inputMethodInfo)153 private boolean isInputMethodEnabled(InputMethodInfo inputMethodInfo) { 154 return InputMethodUtil.isInputMethodEnabled( 155 getContext().getContentResolver(), inputMethodInfo); 156 } 157 158 /** 159 * Check if given input method is the only enabled input method that can be a default system 160 * input method. 161 * 162 * @return {@code true} if input method is the only input method that can be a default system 163 * input method. 164 */ isOnlyEnabledDefaultInputMethod(InputMethodInfo inputMethodInfo)165 private boolean isOnlyEnabledDefaultInputMethod(InputMethodInfo inputMethodInfo) { 166 if (!inputMethodInfo.isDefault(getContext())) { 167 return false; 168 } 169 170 List<InputMethodInfo> inputMethodInfos = mInputMethodManager.getEnabledInputMethodList(); 171 172 for (InputMethodInfo imi : inputMethodInfos) { 173 if (!imi.isDefault(getContext())) { 174 continue; 175 } 176 177 if (!imi.getId().equals(inputMethodInfo.getId())) { 178 return false; 179 } 180 } 181 182 return true; 183 } 184 185 /** 186 * Create a CarUiSwitchPreference to enable/disable an input method. 187 * 188 * @return {@code CarUiSwitchPreference} which allows a user to enable/disable an input method. 189 */ createSwitchPreference(Set<String> permittedInputMethodsSet, InputMethodInfo inputMethodInfo)190 private CarUiSwitchPreference createSwitchPreference(Set<String> permittedInputMethodsSet, 191 InputMethodInfo inputMethodInfo) { 192 CarUiSwitchPreference switchPreference = new CarUiSwitchPreference(getContext()); 193 switchPreference.setKey(String.valueOf(inputMethodInfo.getId())); 194 switchPreference.setIcon(InputMethodUtil.getPackageIcon(mPackageManager, inputMethodInfo)); 195 switchPreference.setTitle(InputMethodUtil.getPackageLabel(mPackageManager, 196 inputMethodInfo)); 197 switchPreference.setChecked(InputMethodUtil.isInputMethodEnabled(getContext() 198 .getContentResolver(), inputMethodInfo)); 199 switchPreference.setSummary(InputMethodUtil.getSummaryString(getContext(), 200 mInputMethodManager, inputMethodInfo)); 201 202 // A switch preference for any disabled IME should be enabled. This is due to the 203 // possibility of having only one default IME that is disabled, which would prevent the IME 204 // from being enabled without another default input method that is enabled being present. 205 if (!isInputMethodEnabled(inputMethodInfo)) { 206 switchPreference.setEnabled(true); 207 } else { 208 switchPreference.setEnabled(!isOnlyEnabledDefaultInputMethod(inputMethodInfo)); 209 } 210 211 if (!isInputMethodAllowedByOrganization(permittedInputMethodsSet, inputMethodInfo)) { 212 switchPreference.setEnabled(false); 213 setClickableWhileDisabled(switchPreference, /* clickable= */ true, p -> 214 showActionDisabledByAdminDialog(inputMethodInfo.getPackageName())); 215 return switchPreference; 216 } 217 switchPreference.setOnPreferenceChangeListener((switchPref, newValue) -> { 218 boolean enable = (boolean) newValue; 219 if (enable) { 220 showSecurityWarnDialog(inputMethodInfo); 221 } else { 222 InputMethodUtil.disableInputMethod(getContext(), mInputMethodManager, 223 inputMethodInfo); 224 refreshUi(); 225 } 226 return false; 227 }); 228 return switchPreference; 229 } 230 showDirectBootWarnDialog(InputMethodInfo inputMethodInfo)231 private void showDirectBootWarnDialog(InputMethodInfo inputMethodInfo) { 232 ConfirmationDialogFragment dialog = new ConfirmationDialogFragment.Builder(getContext()) 233 .setMessage(getContext().getString(R.string.direct_boot_unaware_dialog_message_car)) 234 .setPositiveButton(android.R.string.ok, mDirectBootWarnConfirmListener) 235 .setNegativeButton(android.R.string.cancel, mRejectListener) 236 .addArgumentParcelable(KEY_INPUT_METHOD_INFO, inputMethodInfo) 237 .build(); 238 239 getFragmentController().showDialog(dialog, DIRECT_BOOT_WARN_DIALOG_TAG); 240 } 241 showSecurityWarnDialog(InputMethodInfo inputMethodInfo)242 private void showSecurityWarnDialog(InputMethodInfo inputMethodInfo) { 243 CharSequence label = inputMethodInfo.loadLabel(mPackageManager); 244 245 ConfirmationDialogFragment dialog = new ConfirmationDialogFragment.Builder(getContext()) 246 .setTitle(android.R.string.dialog_alert_title) 247 .setMessage(getContext().getString(R.string.ime_security_warning, label)) 248 .setPositiveButton(android.R.string.ok, mSecurityWarnDialogConfirmListener) 249 .setNegativeButton(android.R.string.cancel, mRejectListener) 250 .addArgumentParcelable(KEY_INPUT_METHOD_INFO, inputMethodInfo) 251 .build(); 252 253 getFragmentController().showDialog(dialog, SECURITY_WARN_DIALOG_TAG); 254 } 255 256 // TODO: ideally we need to refactor this method. See reasoning on 257 // EnterpriseUtils.DISABLED_INPUT_METHOD constant. showActionDisabledByAdminDialog(String inputMethodPkg)258 private void showActionDisabledByAdminDialog(String inputMethodPkg) { 259 getFragmentController().showDialog( 260 EnterpriseUtils.getActionDisabledByAdminDialog(getContext(), 261 EnterpriseUtils.DISABLED_INPUT_METHOD, inputMethodPkg), 262 DISABLED_BY_ADMIN_CONFIRM_DIALOG_TAG); 263 } 264 } 265