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 = context.getSystemService(DevicePolicyManager.class); 82 mInputMethodManager = context.getSystemService(InputMethodManager.class); 83 } 84 85 @Override onCreateInternal()86 protected void onCreateInternal() { 87 super.onCreateInternal(); 88 89 ConfirmationDialogFragment dialogFragment = (ConfirmationDialogFragment) 90 getFragmentController().findDialogByTag(DIRECT_BOOT_WARN_DIALOG_TAG); 91 ConfirmationDialogFragment.resetListeners(dialogFragment, 92 mDirectBootWarnConfirmListener, 93 mRejectListener, 94 /* neutralListener= */ null); 95 96 dialogFragment = (ConfirmationDialogFragment) getFragmentController() 97 .findDialogByTag(SECURITY_WARN_DIALOG_TAG); 98 ConfirmationDialogFragment.resetListeners(dialogFragment, 99 mSecurityWarnDialogConfirmListener, 100 mRejectListener, 101 /* neutralListener= */ null); 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 preferenceGroup.addPreference(createSwitchPreference(inputMethodInfo)); 138 } 139 } 140 isInputMethodAllowedByOrganization(Set<String> permittedList, InputMethodInfo inputMethodInfo)141 private boolean isInputMethodAllowedByOrganization(Set<String> permittedList, 142 InputMethodInfo inputMethodInfo) { 143 // permittedList is null means that all input methods are allowed. 144 return (permittedList == null) || permittedList.contains(inputMethodInfo.getPackageName()); 145 } 146 isInputMethodEnabled(InputMethodInfo inputMethodInfo)147 private boolean isInputMethodEnabled(InputMethodInfo inputMethodInfo) { 148 return InputMethodUtil.isInputMethodEnabled( 149 getContext().getContentResolver(), inputMethodInfo); 150 } 151 152 /** 153 * Check if given input method is the only enabled input method that can be a default system 154 * input method. 155 * 156 * @return {@code true} if input method is the only input method that can be a default system 157 * input method. 158 */ isOnlyEnabledDefaultInputMethod(InputMethodInfo inputMethodInfo)159 private boolean isOnlyEnabledDefaultInputMethod(InputMethodInfo inputMethodInfo) { 160 if (!inputMethodInfo.isDefault(getContext())) { 161 return false; 162 } 163 164 List<InputMethodInfo> inputMethodInfos = mInputMethodManager.getEnabledInputMethodList(); 165 166 for (InputMethodInfo imi : inputMethodInfos) { 167 if (!imi.isDefault(getContext())) { 168 continue; 169 } 170 171 if (!imi.getId().equals(inputMethodInfo.getId())) { 172 return false; 173 } 174 } 175 176 return true; 177 } 178 179 /** 180 * Create a SwitchPreference to enable/disable an input method. 181 * 182 * @return {@code SwitchPreference} which allows a user to enable/disable an input method. 183 */ createSwitchPreference(InputMethodInfo inputMethodInfo)184 private SwitchPreference createSwitchPreference(InputMethodInfo inputMethodInfo) { 185 SwitchPreference switchPreference = new CarUiSwitchPreference(getContext()); 186 switchPreference.setKey(String.valueOf(inputMethodInfo.getId())); 187 switchPreference.setIcon(InputMethodUtil.getPackageIcon(mPackageManager, inputMethodInfo)); 188 switchPreference.setTitle(InputMethodUtil.getPackageLabel(mPackageManager, 189 inputMethodInfo)); 190 switchPreference.setChecked(InputMethodUtil.isInputMethodEnabled(getContext() 191 .getContentResolver(), inputMethodInfo)); 192 switchPreference.setSummary(InputMethodUtil.getSummaryString(getContext(), 193 mInputMethodManager, inputMethodInfo)); 194 195 // A switch preference for any disabled IME should be enabled. This is due to the 196 // possibility of having only one default IME that is disabled, which would prevent the IME 197 // from being enabled without another default input method that is enabled being present. 198 if (!isInputMethodEnabled(inputMethodInfo)) { 199 switchPreference.setEnabled(true); 200 } else { 201 switchPreference.setEnabled(!isOnlyEnabledDefaultInputMethod(inputMethodInfo)); 202 } 203 204 switchPreference.setOnPreferenceChangeListener((switchPref, newValue) -> { 205 boolean enable = (boolean) newValue; 206 if (enable) { 207 showSecurityWarnDialog(inputMethodInfo); 208 } else { 209 InputMethodUtil.disableInputMethod(getContext(), mInputMethodManager, 210 inputMethodInfo); 211 refreshUi(); 212 } 213 return false; 214 }); 215 return switchPreference; 216 } 217 showDirectBootWarnDialog(InputMethodInfo inputMethodInfo)218 private void showDirectBootWarnDialog(InputMethodInfo inputMethodInfo) { 219 ConfirmationDialogFragment dialog = new ConfirmationDialogFragment.Builder(getContext()) 220 .setMessage(getContext().getString(R.string.direct_boot_unaware_dialog_message_car)) 221 .setPositiveButton(android.R.string.ok, mDirectBootWarnConfirmListener) 222 .setNegativeButton(android.R.string.cancel, mRejectListener) 223 .addArgumentParcelable(KEY_INPUT_METHOD_INFO, inputMethodInfo) 224 .build(); 225 226 getFragmentController().showDialog(dialog, DIRECT_BOOT_WARN_DIALOG_TAG); 227 } 228 showSecurityWarnDialog(InputMethodInfo inputMethodInfo)229 private void showSecurityWarnDialog(InputMethodInfo inputMethodInfo) { 230 CharSequence label = inputMethodInfo.loadLabel(mPackageManager); 231 232 ConfirmationDialogFragment dialog = new ConfirmationDialogFragment.Builder(getContext()) 233 .setTitle(android.R.string.dialog_alert_title) 234 .setMessage(getContext().getString(R.string.ime_security_warning, label)) 235 .setPositiveButton(android.R.string.ok, mSecurityWarnDialogConfirmListener) 236 .setNegativeButton(android.R.string.cancel, mRejectListener) 237 .addArgumentParcelable(KEY_INPUT_METHOD_INFO, inputMethodInfo) 238 .build(); 239 240 getFragmentController().showDialog(dialog, SECURITY_WARN_DIALOG_TAG); 241 } 242 } 243