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.Preference; 28 import androidx.preference.PreferenceGroup; 29 import androidx.preference.SwitchPreference; 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 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 97 dialogFragment = (ConfirmationDialogFragment) getFragmentController() 98 .findDialogByTag(SECURITY_WARN_DIALOG_TAG); 99 ConfirmationDialogFragment.resetListeners(dialogFragment, 100 mSecurityWarnDialogConfirmListener, 101 mRejectListener); 102 } 103 104 @Override getPreferenceType()105 protected Class<PreferenceGroup> getPreferenceType() { 106 return PreferenceGroup.class; 107 } 108 109 @Override updateState(PreferenceGroup preferenceGroup)110 protected void updateState(PreferenceGroup preferenceGroup) { 111 List<String> permittedInputMethods = mDevicePolicyManager 112 .getPermittedInputMethodsForCurrentUser(); 113 Set<String> permittedInputMethodsSet = permittedInputMethods == null ? null : new HashSet<>( 114 permittedInputMethods); 115 116 preferenceGroup.removeAll(); 117 118 List<InputMethodInfo> inputMethodInfos = mInputMethodManager.getInputMethodList(); 119 if (inputMethodInfos == null || inputMethodInfos.size() == 0) { 120 return; 121 } 122 123 Collections.sort(inputMethodInfos, Comparator.comparing( 124 (InputMethodInfo a) -> InputMethodUtil.getPackageLabel(mPackageManager, a)) 125 .thenComparing((InputMethodInfo a) -> InputMethodUtil.getSummaryString(getContext(), 126 mInputMethodManager, a))); 127 128 for (InputMethodInfo inputMethodInfo : inputMethodInfos) { 129 if (!isInputMethodAllowedByOrganization(permittedInputMethodsSet, inputMethodInfo)) { 130 continue; 131 } 132 // Hide "Google voice typing" IME. 133 if (inputMethodInfo.getPackageName().equals(InputMethodUtil.GOOGLE_VOICE_TYPING)) { 134 continue; 135 } 136 137 Preference preference = createSwitchPreference(inputMethodInfo); 138 139 preference.setEnabled(!isOnlyEnabledDefaultInputMethod(inputMethodInfo)); 140 141 preferenceGroup.addPreference(preference); 142 } 143 } 144 isInputMethodAllowedByOrganization(Set<String> permittedList, InputMethodInfo inputMethodInfo)145 private boolean isInputMethodAllowedByOrganization(Set<String> permittedList, 146 InputMethodInfo inputMethodInfo) { 147 // permittedList is null means that all input methods are allowed. 148 return (permittedList == null) || permittedList.contains(inputMethodInfo.getPackageName()); 149 } 150 151 /** 152 * Check if given input method is the only enabled input method that can be a default system 153 * input method. 154 * 155 * @return {@code true} if input method is the only input method that can be a default system 156 * input method. 157 */ isOnlyEnabledDefaultInputMethod(InputMethodInfo inputMethodInfo)158 private boolean isOnlyEnabledDefaultInputMethod(InputMethodInfo inputMethodInfo) { 159 if (!inputMethodInfo.isDefault(getContext())) { 160 return false; 161 } 162 163 List<InputMethodInfo> inputMethodInfos = mInputMethodManager.getEnabledInputMethodList(); 164 165 for (InputMethodInfo imi : inputMethodInfos) { 166 if (!imi.isDefault(getContext())) { 167 continue; 168 } 169 170 if (!imi.getId().equals(inputMethodInfo.getId())) { 171 return false; 172 } 173 } 174 175 return true; 176 } 177 178 /** 179 * Create a SwitchPreference to enable/disable an input method. 180 * 181 * @return {@code SwitchPreference} which allows a user to enable/disable an input method. 182 */ createSwitchPreference(InputMethodInfo inputMethodInfo)183 private SwitchPreference createSwitchPreference(InputMethodInfo inputMethodInfo) { 184 SwitchPreference switchPreference = new SwitchPreference(getContext()); 185 switchPreference.setKey(String.valueOf(inputMethodInfo.getId())); 186 switchPreference.setIcon(InputMethodUtil.getPackageIcon(mPackageManager, inputMethodInfo)); 187 switchPreference.setTitle(InputMethodUtil.getPackageLabel(mPackageManager, 188 inputMethodInfo)); 189 switchPreference.setChecked(InputMethodUtil.isInputMethodEnabled(getContext() 190 .getContentResolver(), inputMethodInfo)); 191 switchPreference.setSummary(InputMethodUtil.getSummaryString(getContext(), 192 mInputMethodManager, inputMethodInfo)); 193 194 switchPreference.setOnPreferenceChangeListener((switchPref, newValue) -> { 195 boolean enable = (boolean) newValue; 196 if (enable) { 197 showSecurityWarnDialog(inputMethodInfo); 198 } else { 199 InputMethodUtil.disableInputMethod(getContext(), mInputMethodManager, 200 inputMethodInfo); 201 refreshUi(); 202 } 203 return false; 204 }); 205 return switchPreference; 206 } 207 showDirectBootWarnDialog(InputMethodInfo inputMethodInfo)208 private void showDirectBootWarnDialog(InputMethodInfo inputMethodInfo) { 209 ConfirmationDialogFragment dialog = new ConfirmationDialogFragment.Builder(getContext()) 210 .setMessage(getContext().getString(R.string.direct_boot_unaware_dialog_message_car)) 211 .setPositiveButton(android.R.string.ok, mDirectBootWarnConfirmListener) 212 .setNegativeButton(android.R.string.cancel, mRejectListener) 213 .addArgumentParcelable(KEY_INPUT_METHOD_INFO, inputMethodInfo) 214 .build(); 215 216 getFragmentController().showDialog(dialog, DIRECT_BOOT_WARN_DIALOG_TAG); 217 } 218 showSecurityWarnDialog(InputMethodInfo inputMethodInfo)219 private void showSecurityWarnDialog(InputMethodInfo inputMethodInfo) { 220 CharSequence label = inputMethodInfo.loadLabel(mPackageManager); 221 222 ConfirmationDialogFragment dialog = new ConfirmationDialogFragment.Builder(getContext()) 223 .setTitle(android.R.string.dialog_alert_title) 224 .setMessage(getContext().getString(R.string.ime_security_warning, label)) 225 .setPositiveButton(android.R.string.ok, mSecurityWarnDialogConfirmListener) 226 .setNegativeButton(android.R.string.cancel, mRejectListener) 227 .addArgumentParcelable(KEY_INPUT_METHOD_INFO, inputMethodInfo) 228 .build(); 229 230 getFragmentController().showDialog(dialog, SECURITY_WARN_DIALOG_TAG); 231 } 232 } 233