• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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