1 /*
2  * Copyright (C) 2017 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.tv.settings.inputmethod;
18 
19 import static com.android.tv.settings.util.InstrumentationUtils.logEntrySelected;
20 
21 import android.app.tvsettings.TvSettingsEnums;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.pm.PackageManager;
25 import android.hardware.input.InputManager;
26 import android.os.Bundle;
27 import android.os.UserHandle;
28 import android.text.TextUtils;
29 import android.util.ArraySet;
30 import android.util.Log;
31 import android.view.inputmethod.InputMethodInfo;
32 
33 import androidx.annotation.Keep;
34 import androidx.annotation.NonNull;
35 import androidx.annotation.VisibleForTesting;
36 import androidx.preference.ListPreference;
37 import androidx.preference.Preference;
38 import androidx.preference.PreferenceCategory;
39 import androidx.preference.PreferenceScreen;
40 
41 import com.android.settingslib.applications.DefaultAppInfo;
42 import com.android.tv.settings.R;
43 import com.android.tv.settings.SettingsPreferenceFragment;
44 import com.android.tv.settings.autofill.AutofillHelper;
45 import com.android.tv.settings.library.util.ThreadUtils;
46 import com.android.tv.settings.overlay.FlavorUtils;
47 import com.android.tv.settings.util.SliceUtils;
48 import com.android.tv.twopanelsettings.slices.SlicePreference;
49 
50 import java.util.ArrayList;
51 import java.util.List;
52 import java.util.Objects;
53 import java.util.Set;
54 
55 /**
56  * Fragment for managing IMEs, Autofills and physical keyboards
57  */
58 @Keep
59 public class KeyboardFragment extends SettingsPreferenceFragment implements
60         InputManager.InputDeviceListener {
61     private static final String TAG = "KeyboardFragment";
62     private static final boolean DEBUG = false;
63 
64     // Order of input methods, make sure they are inserted between 1 (currentKeyboard) and
65     // 3 (manageKeyboards).
66     private static final int INPUT_METHOD_PREFERENCE_ORDER = 2;
67 
68     // Order of physical keyboard setting, in the end
69     private static final int PHYSICAL_KEYBOARD_PREFERENCE_ORDER = 5;
70 
71     @VisibleForTesting
72     static final String KEY_KEYBOARD_CATEGORY = "keyboardCategory";
73 
74     @VisibleForTesting
75     static final String KEY_CURRENT_KEYBOARD = "currentKeyboard";
76 
77     private static final String KEY_KEYBOARD_SETTINGS_PREFIX = "keyboardSettings:";
78 
79     private static final String KEY_PHYSICAL_KEYBOARD_SETTINGS_PREFIX = "physicalKeyboardSettings:";
80 
81     @VisibleForTesting
82     static final String KEY_AUTOFILL_CATEGORY = "autofillCategory";
83 
84     @VisibleForTesting
85     static final String KEY_CURRENT_AUTOFILL = "currentAutofill";
86 
87     private static final String KEY_AUTOFILL_SETTINGS_PREFIX = "autofillSettings:";
88 
89     private PackageManager mPm;
90 
91     private InputManager mIm;
92 
93     /**
94      * @return New fragment instance
95      */
newInstance()96     public static KeyboardFragment newInstance() {
97         return new KeyboardFragment();
98     }
99 
100     @Override
onAttach(Context context)101     public void onAttach(Context context) {
102         super.onAttach(context);
103         mPm = context.getPackageManager();
104         mIm = Objects.requireNonNull(context.getSystemService(InputManager.class));
105     }
106 
107     @Override
onCreatePreferences(Bundle savedInstanceState, String rootKey)108     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
109         setPreferencesFromResource(R.xml.keyboard, null);
110 
111         findPreference(KEY_CURRENT_KEYBOARD).setOnPreferenceChangeListener(
112                 (preference, newValue) -> {
113                     logEntrySelected(TvSettingsEnums.SYSTEM_KEYBOARD_CURRENT_KEYBOARD);
114                     InputMethodHelper.setDefaultInputMethodId(getContext(), (String) newValue);
115                     return true;
116                 });
117 
118         updateUi();
119     }
120 
121     @Override
onResume()122     public void onResume() {
123         super.onResume();
124         updateUi();
125         mIm.registerInputDeviceListener(this, null);
126     }
127 
128     @Override
onPause()129     public void onPause() {
130         super.onPause();
131         mIm.unregisterInputDeviceListener(this);
132     }
133 
134     @VisibleForTesting
updateUi()135     void updateUi() {
136         updateAutofill();
137         updateKeyboards();
138     }
139 
updateKeyboards()140     private void updateKeyboards() {
141         updateCurrentKeyboardPreference(findPreference(KEY_CURRENT_KEYBOARD));
142         updateKeyboardsSettings();
143         scheduleUpdatePhysicalKeyboards(getPreferenceContext());
144     }
145 
updateCurrentKeyboardPreference(ListPreference currentKeyboardPref)146     private void updateCurrentKeyboardPreference(ListPreference currentKeyboardPref) {
147         List<InputMethodInfo> enabledInputMethodInfos = InputMethodHelper
148                 .getEnabledSystemInputMethodList(getContext());
149         final List<CharSequence> entries = new ArrayList<>(enabledInputMethodInfos.size());
150         final List<CharSequence> values = new ArrayList<>(enabledInputMethodInfos.size());
151 
152         int defaultIndex = 0;
153         final String defaultId = InputMethodHelper.getDefaultInputMethodId(getContext());
154 
155         for (final InputMethodInfo info : enabledInputMethodInfos) {
156             entries.add(info.loadLabel(mPm));
157             final String id = info.getId();
158             values.add(id);
159             if (TextUtils.equals(id, defaultId)) {
160                 defaultIndex = values.size() - 1;
161             }
162         }
163 
164         currentKeyboardPref.setEntries(entries.toArray(new CharSequence[entries.size()]));
165         currentKeyboardPref.setEntryValues(values.toArray(new CharSequence[values.size()]));
166         if (entries.size() > 0) {
167             currentKeyboardPref.setValueIndex(defaultIndex);
168         }
169     }
170 
getPreferenceContext()171     Context getPreferenceContext() {
172         return getPreferenceManager().getContext();
173     }
174 
updateKeyboardsSettings()175     private void updateKeyboardsSettings() {
176         final Context preferenceContext = getPreferenceContext();
177         List<InputMethodInfo> enabledInputMethodInfos = InputMethodHelper
178                 .getEnabledSystemInputMethodList(getContext());
179         PreferenceScreen preferenceScreen = getPreferenceScreen();
180         final Set<String> enabledInputMethodKeys = new ArraySet<>(
181                 enabledInputMethodInfos.size());
182         // Add per-IME settings
183         for (final InputMethodInfo info : enabledInputMethodInfos) {
184             final String uri = InputMethodHelper.getInputMethodsSettingsUri(getContext(), info);
185             final Intent settingsIntent = InputMethodHelper.getInputMethodSettingsIntent(info);
186             if (uri == null && settingsIntent == null) {
187                 continue;
188             }
189             final String key = KEY_KEYBOARD_SETTINGS_PREFIX + info.getId();
190 
191             Preference preference = preferenceScreen.findPreference(key);
192             boolean useSlice = FlavorUtils.isTwoPanel(getContext()) && uri != null;
193             if (preference == null) {
194                 if (useSlice) {
195                     preference = new SlicePreference(preferenceContext);
196                 } else {
197                     preference = new Preference(preferenceContext);
198                 }
199                 preference.setOrder(INPUT_METHOD_PREFERENCE_ORDER);
200                 preferenceScreen.addPreference(preference);
201             }
202             preference.setTitle(getContext().getString(R.string.title_settings,
203                     info.loadLabel(mPm)));
204             preference.setKey(key);
205             if (useSlice) {
206                 ((SlicePreference) preference).setUri(uri);
207                 preference.setFragment(SliceUtils.PATH_SLICE_FRAGMENT);
208             } else {
209                 preference.setIntent(settingsIntent);
210             }
211             enabledInputMethodKeys.add(key);
212         }
213         removeDisabledPreferencesFromScreen(preferenceScreen, enabledInputMethodKeys,
214                 KEY_KEYBOARD_SETTINGS_PREFIX);
215     }
216 
scheduleUpdatePhysicalKeyboards(Context context)217     void scheduleUpdatePhysicalKeyboards(Context context) {
218         ThreadUtils.postOnBackgroundThread(() -> {
219             final List<PhysicalKeyboardHelper.DeviceInfo> newPhysicalKeyboards =
220                     PhysicalKeyboardHelper.getPhysicalKeyboards(context);
221             ThreadUtils.postOnMainThread(() -> updatePhysicalKeyboards(newPhysicalKeyboards));
222         });
223     }
224 
updatePhysicalKeyboards( @onNull List<PhysicalKeyboardHelper.DeviceInfo> newPhysicalKeyboards)225     private void updatePhysicalKeyboards(
226             @NonNull List<PhysicalKeyboardHelper.DeviceInfo> newPhysicalKeyboards) {
227         final PreferenceScreen preferenceScreen = getPreferenceScreen();
228         if (DEBUG) {
229             Log.d(TAG, "updatePhysicalKeyboards: " + newPhysicalKeyboards.toString());
230         }
231         final Set<String> enabledPhysicalKeyboardKeys = new ArraySet<>(newPhysicalKeyboards.size());
232         // Add a setting per physical keyboard device
233         for (PhysicalKeyboardHelper.DeviceInfo deviceInfo :
234                 newPhysicalKeyboards) {
235             String key = KEY_PHYSICAL_KEYBOARD_SETTINGS_PREFIX
236                     + deviceInfo.mDeviceIdentifier.getDescriptor();
237             Preference pref = preferenceScreen.findPreference(key);
238             if (pref == null) {
239                 pref = new Preference(getPreferenceContext());
240                 pref.setOrder(PHYSICAL_KEYBOARD_PREFERENCE_ORDER);
241                 preferenceScreen.addPreference(pref);
242             }
243             pref.setKey(key);
244             pref.setTitle(getPreferenceContext().getString(
245                     com.android.settingslib.R.string.physical_keyboard_title));
246             pref.setSummary(deviceInfo.getSummary());
247             KeyboardLayoutSelectionFragment.prepareArgs(pref.getExtras(),
248                     deviceInfo.mDeviceIdentifier,
249                     deviceInfo.mDeviceName,
250                     deviceInfo.mDeviceId,
251                     deviceInfo.mCurrentLayoutDescriptor);
252             pref.setFragment(KeyboardLayoutSelectionFragment.class.getName());
253             enabledPhysicalKeyboardKeys.add(key);
254         }
255         removeDisabledPreferencesFromScreen(preferenceScreen, enabledPhysicalKeyboardKeys,
256                 KEY_PHYSICAL_KEYBOARD_SETTINGS_PREFIX);
257     }
258 
259     /**
260      * Removes all preferences which start with the key prefix and are not among the enabled keys
261      * from the preference screen.
262      *
263      * @param preferenceScreen The preference screen.
264      * @param enabledKeys The set of enabled keys.
265      * @param keyPrefix The prefix for the keys to be removed.
266      */
removeDisabledPreferencesFromScreen(PreferenceScreen preferenceScreen, Set<String> enabledKeys, String keyPrefix)267     private void removeDisabledPreferencesFromScreen(PreferenceScreen preferenceScreen,
268             Set<String> enabledKeys, String keyPrefix) {
269         for (int i = 0; i < preferenceScreen.getPreferenceCount(); ) {
270             final Preference preference = preferenceScreen.getPreference(i);
271             final String key = preference.getKey();
272             if (!TextUtils.isEmpty(key)
273                     && key.startsWith(keyPrefix)
274                     && !enabledKeys.contains(key)) {
275                 preferenceScreen.removePreference(preference);
276             } else {
277                 i++;
278             }
279         }
280     }
281 
282     /**
283      * Update autofill related preferences.
284      */
updateAutofill()285     private void updateAutofill() {
286         final PreferenceCategory autofillCategory = findPreference(KEY_AUTOFILL_CATEGORY);
287         List<DefaultAppInfo> candidates = getAutofillCandidates();
288         if (candidates.isEmpty()) {
289             // No need to show keyboard category and autofill category.
290             // Keyboard only preference screen:
291             findPreference(KEY_KEYBOARD_CATEGORY).setVisible(false);
292             autofillCategory.setVisible(false);
293             getPreferenceScreen().setTitle(R.string.system_keyboard);
294         } else {
295             // Show both keyboard category and autofill category in keyboard & autofill screen.
296             findPreference(KEY_KEYBOARD_CATEGORY).setVisible(true);
297             autofillCategory.setVisible(true);
298             final Preference currentAutofillPref = findPreference(KEY_CURRENT_AUTOFILL);
299             updateCurrentAutofillPreference(currentAutofillPref, candidates);
300             updateAutofillSettings(candidates);
301             getPreferenceScreen().setTitle(R.string.system_keyboard_autofill);
302         }
303     }
304 
getAutofillCandidates()305     private List<DefaultAppInfo> getAutofillCandidates() {
306         return AutofillHelper.getAutofillCandidates(getContext(),
307                 mPm, UserHandle.myUserId());
308     }
309 
updateCurrentAutofillPreference(Preference currentAutofillPref, List<DefaultAppInfo> candidates)310     private void updateCurrentAutofillPreference(Preference currentAutofillPref,
311             List<DefaultAppInfo> candidates) {
312 
313         DefaultAppInfo app = AutofillHelper.getCurrentAutofill(getContext(), candidates);
314 
315         CharSequence summary = app == null ? getContext().getString(R.string.autofill_none)
316                 : app.loadLabel();
317         currentAutofillPref.setSummary(summary);
318     }
319 
updateAutofillSettings(List<DefaultAppInfo> candidates)320     private void updateAutofillSettings(List<DefaultAppInfo> candidates) {
321         final Context preferenceContext = getPreferenceContext();
322 
323         final PreferenceCategory autofillCategory = findPreference(KEY_AUTOFILL_CATEGORY);
324 
325         final Set<String> autofillServicesKeys = new ArraySet<>(candidates.size());
326         for (final DefaultAppInfo info : candidates) {
327             final Intent settingsIntent = AutofillHelper.getAutofillSettingsIntent(getContext(),
328                     mPm, info);
329             if (settingsIntent == null) {
330                 continue;
331             }
332             final String key = KEY_AUTOFILL_SETTINGS_PREFIX + info.getKey();
333 
334             Preference preference = findPreference(key);
335             if (preference == null) {
336                 preference = new Preference(preferenceContext);
337                 autofillCategory.addPreference(preference);
338             }
339             preference.setTitle(getContext().getString(R.string.title_settings, info.loadLabel()));
340             preference.setKey(key);
341             preference.setIntent(settingsIntent);
342             autofillServicesKeys.add(key);
343         }
344 
345         for (int i = 0; i < autofillCategory.getPreferenceCount(); ) {
346             final Preference preference = autofillCategory.getPreference(i);
347             final String key = preference.getKey();
348             if (!TextUtils.isEmpty(key)
349                     && key.startsWith(KEY_AUTOFILL_SETTINGS_PREFIX)
350                     && !autofillServicesKeys.contains(key)) {
351                 autofillCategory.removePreference(preference);
352             } else {
353                 i++;
354             }
355         }
356     }
357 
358     @Override
getPageId()359     protected int getPageId() {
360         return TvSettingsEnums.SYSTEM_KEYBOARD;
361     }
362 
363     @Override
onInputDeviceAdded(int deviceId)364     public void onInputDeviceAdded(int deviceId) {
365         scheduleUpdatePhysicalKeyboards(getPreferenceContext());
366     }
367 
368     @Override
onInputDeviceRemoved(int deviceId)369     public void onInputDeviceRemoved(int deviceId) {
370         scheduleUpdatePhysicalKeyboards(getPreferenceContext());
371     }
372 
373     @Override
onInputDeviceChanged(int deviceId)374     public void onInputDeviceChanged(int deviceId) {
375         scheduleUpdatePhysicalKeyboards(getPreferenceContext());
376     }
377 }
378