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