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 android.app.admin.DevicePolicyManager; 20 import android.car.drivingstate.CarUxRestrictions; 21 import android.content.Context; 22 import android.content.pm.PackageManager; 23 import android.view.inputmethod.InputMethodInfo; 24 import android.view.inputmethod.InputMethodManager; 25 26 import androidx.annotation.VisibleForTesting; 27 import androidx.preference.PreferenceGroup; 28 import androidx.preference.SwitchPreference; 29 30 import com.android.car.settings.R; 31 import com.android.car.settings.common.ConfirmationDialogFragment; 32 import com.android.car.settings.common.FragmentController; 33 import com.android.car.settings.common.PreferenceController; 34 import com.android.car.ui.preference.CarUiSwitchPreference; 35 36 import java.util.Collections; 37 import java.util.Comparator; 38 import java.util.HashSet; 39 import java.util.List; 40 import java.util.Set; 41 42 /** Updates the available keyboard list. */ 43 public class KeyboardManagementPreferenceController extends 44 PreferenceController<PreferenceGroup> { 45 @VisibleForTesting 46 static final String DIRECT_BOOT_WARN_DIALOG_TAG = "DirectBootWarnDialog"; 47 @VisibleForTesting 48 static final String SECURITY_WARN_DIALOG_TAG = "SecurityWarnDialog"; 49 private static final String KEY_INPUT_METHOD_INFO = "INPUT_METHOD_INFO"; 50 private final InputMethodManager mInputMethodManager; 51 private final DevicePolicyManager mDevicePolicyManager; 52 private final PackageManager mPackageManager; 53 private final ConfirmationDialogFragment.ConfirmListener mDirectBootWarnConfirmListener = 54 args -> { 55 InputMethodInfo inputMethodInfo = args.getParcelable(KEY_INPUT_METHOD_INFO); 56 InputMethodUtil.enableInputMethod(getContext().getContentResolver(), 57 inputMethodInfo); 58 refreshUi(); 59 }; 60 private final ConfirmationDialogFragment.RejectListener mRejectListener = args -> 61 refreshUi(); 62 private final ConfirmationDialogFragment.ConfirmListener mSecurityWarnDialogConfirmListener = 63 args -> { 64 InputMethodInfo inputMethodInfo = args.getParcelable(KEY_INPUT_METHOD_INFO); 65 // The user confirmed to enable a 3rd party IME, but we might need to prompt if 66 // it's not 67 // Direct Boot aware. 68 if (inputMethodInfo.getServiceInfo().directBootAware) { 69 InputMethodUtil.enableInputMethod(getContext().getContentResolver(), 70 inputMethodInfo); 71 refreshUi(); 72 } else { 73 showDirectBootWarnDialog(inputMethodInfo); 74 } 75 }; 76 KeyboardManagementPreferenceController(Context context, String preferenceKey, FragmentController fragmentController, CarUxRestrictions uxRestrictions)77 public KeyboardManagementPreferenceController(Context context, String preferenceKey, 78 FragmentController fragmentController, CarUxRestrictions uxRestrictions) { 79 super(context, preferenceKey, fragmentController, uxRestrictions); 80 mPackageManager = context.getPackageManager(); 81 mDevicePolicyManager = 82 (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE); 83 mInputMethodManager = 84 (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); 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 if (!isInputMethodAllowedByOrganization(permittedInputMethodsSet, inputMethodInfo)) { 132 continue; 133 } 134 // Hide "Google voice typing" IME. 135 if (inputMethodInfo.getPackageName().equals(InputMethodUtil.GOOGLE_VOICE_TYPING)) { 136 continue; 137 } 138 139 preferenceGroup.addPreference(createSwitchPreference(inputMethodInfo)); 140 } 141 } 142 isInputMethodAllowedByOrganization(Set<String> permittedList, InputMethodInfo inputMethodInfo)143 private boolean isInputMethodAllowedByOrganization(Set<String> permittedList, 144 InputMethodInfo inputMethodInfo) { 145 // permittedList is null means that all input methods are allowed. 146 return (permittedList == null) || permittedList.contains(inputMethodInfo.getPackageName()); 147 } 148 isInputMethodEnabled(InputMethodInfo inputMethodInfo)149 private boolean isInputMethodEnabled(InputMethodInfo inputMethodInfo) { 150 return InputMethodUtil.isInputMethodEnabled( 151 getContext().getContentResolver(), inputMethodInfo); 152 } 153 154 /** 155 * Check if given input method is the only enabled input method that can be a default system 156 * input method. 157 * 158 * @return {@code true} if input method is the only input method that can be a default system 159 * input method. 160 */ isOnlyEnabledDefaultInputMethod(InputMethodInfo inputMethodInfo)161 private boolean isOnlyEnabledDefaultInputMethod(InputMethodInfo inputMethodInfo) { 162 if (!inputMethodInfo.isDefault(getContext())) { 163 return false; 164 } 165 166 List<InputMethodInfo> inputMethodInfos = mInputMethodManager.getEnabledInputMethodList(); 167 168 for (InputMethodInfo imi : inputMethodInfos) { 169 if (!imi.isDefault(getContext())) { 170 continue; 171 } 172 173 if (!imi.getId().equals(inputMethodInfo.getId())) { 174 return false; 175 } 176 } 177 178 return true; 179 } 180 181 /** 182 * Create a SwitchPreference to enable/disable an input method. 183 * 184 * @return {@code SwitchPreference} which allows a user to enable/disable an input method. 185 */ createSwitchPreference(InputMethodInfo inputMethodInfo)186 private SwitchPreference createSwitchPreference(InputMethodInfo inputMethodInfo) { 187 SwitchPreference switchPreference = new CarUiSwitchPreference(getContext()); 188 switchPreference.setKey(String.valueOf(inputMethodInfo.getId())); 189 switchPreference.setIcon(InputMethodUtil.getPackageIcon(mPackageManager, inputMethodInfo)); 190 switchPreference.setTitle(InputMethodUtil.getPackageLabel(mPackageManager, 191 inputMethodInfo)); 192 switchPreference.setChecked(InputMethodUtil.isInputMethodEnabled(getContext() 193 .getContentResolver(), inputMethodInfo)); 194 switchPreference.setSummary(InputMethodUtil.getSummaryString(getContext(), 195 mInputMethodManager, inputMethodInfo)); 196 197 // A switch preference for any disabled IME should be enabled. This is due to the 198 // possibility of having only one default IME that is disabled, which would prevent the IME 199 // from being enabled without another default input method that is enabled being present. 200 if (!isInputMethodEnabled(inputMethodInfo)) { 201 switchPreference.setEnabled(true); 202 } else { 203 switchPreference.setEnabled(!isOnlyEnabledDefaultInputMethod(inputMethodInfo)); 204 } 205 206 switchPreference.setOnPreferenceChangeListener((switchPref, newValue) -> { 207 boolean enable = (boolean) newValue; 208 if (enable) { 209 showSecurityWarnDialog(inputMethodInfo); 210 } else { 211 InputMethodUtil.disableInputMethod(getContext(), mInputMethodManager, 212 inputMethodInfo); 213 refreshUi(); 214 } 215 return false; 216 }); 217 return switchPreference; 218 } 219 showDirectBootWarnDialog(InputMethodInfo inputMethodInfo)220 private void showDirectBootWarnDialog(InputMethodInfo inputMethodInfo) { 221 ConfirmationDialogFragment dialog = new ConfirmationDialogFragment.Builder(getContext()) 222 .setMessage(getContext().getString(R.string.direct_boot_unaware_dialog_message_car)) 223 .setPositiveButton(android.R.string.ok, mDirectBootWarnConfirmListener) 224 .setNegativeButton(android.R.string.cancel, mRejectListener) 225 .addArgumentParcelable(KEY_INPUT_METHOD_INFO, inputMethodInfo) 226 .build(); 227 228 getFragmentController().showDialog(dialog, DIRECT_BOOT_WARN_DIALOG_TAG); 229 } 230 showSecurityWarnDialog(InputMethodInfo inputMethodInfo)231 private void showSecurityWarnDialog(InputMethodInfo inputMethodInfo) { 232 CharSequence label = inputMethodInfo.loadLabel(mPackageManager); 233 234 ConfirmationDialogFragment dialog = new ConfirmationDialogFragment.Builder(getContext()) 235 .setTitle(android.R.string.dialog_alert_title) 236 .setMessage(getContext().getString(R.string.ime_security_warning, label)) 237 .setPositiveButton(android.R.string.ok, mSecurityWarnDialogConfirmListener) 238 .setNegativeButton(android.R.string.cancel, mRejectListener) 239 .addArgumentParcelable(KEY_INPUT_METHOD_INFO, inputMethodInfo) 240 .build(); 241 242 getFragmentController().showDialog(dialog, SECURITY_WARN_DIALOG_TAG); 243 } 244 } 245