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