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