1 /*
2  * Copyright (C) 2016 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.settings.inputmethod;
18 
19 import android.app.Activity;
20 import android.app.settings.SettingsEnums;
21 import android.content.ContentResolver;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.database.ContentObserver;
25 import android.hardware.input.InputDeviceIdentifier;
26 import android.hardware.input.InputManager;
27 import android.hardware.input.InputSettings;
28 import android.hardware.input.KeyboardLayout;
29 import android.net.Uri;
30 import android.os.Bundle;
31 import android.os.Handler;
32 import android.os.UserHandle;
33 import android.provider.SearchIndexableResource;
34 import android.provider.Settings.Secure;
35 import android.text.TextUtils;
36 import android.util.FeatureFlagUtils;
37 import android.view.InputDevice;
38 import android.view.inputmethod.InputMethodManager;
39 
40 import androidx.annotation.NonNull;
41 import androidx.annotation.Nullable;
42 import androidx.preference.Preference;
43 import androidx.preference.Preference.OnPreferenceChangeListener;
44 import androidx.preference.PreferenceCategory;
45 import androidx.preference.PreferenceScreen;
46 import androidx.preference.TwoStatePreference;
47 
48 import com.android.internal.util.Preconditions;
49 import com.android.settings.R;
50 import com.android.settings.Settings;
51 import com.android.settings.SettingsPreferenceFragment;
52 import com.android.settings.core.SubSettingLauncher;
53 import com.android.settings.overlay.FeatureFactory;
54 import com.android.settings.search.BaseSearchIndexProvider;
55 import com.android.settingslib.search.SearchIndexable;
56 import com.android.settingslib.utils.ThreadUtils;
57 
58 import java.text.Collator;
59 import java.util.ArrayList;
60 import java.util.Arrays;
61 import java.util.List;
62 import java.util.Objects;
63 
64 // TODO(b/327638540): Update implementation of preference here and reuse key preferences and
65 //  controllers between here and A11y Setting page.
66 @SearchIndexable
67 public final class PhysicalKeyboardFragment extends SettingsPreferenceFragment
68         implements InputManager.InputDeviceListener,
69         KeyboardLayoutDialogFragment.OnSetupKeyboardLayoutsListener {
70 
71     private static final String KEYBOARD_OPTIONS_CATEGORY = "keyboard_options_category";
72     private static final String KEYBOARD_A11Y_CATEGORY = "keyboard_a11y_category";
73     private static final String SHOW_VIRTUAL_KEYBOARD_SWITCH = "show_virtual_keyboard_switch";
74     private static final String ACCESSIBILITY_BOUNCE_KEYS = "accessibility_bounce_keys";
75     private static final String ACCESSIBILITY_SLOW_KEYS = "accessibility_slow_keys";
76     private static final String ACCESSIBILITY_STICKY_KEYS = "accessibility_sticky_keys";
77     private static final String KEYBOARD_SHORTCUTS_HELPER = "keyboard_shortcuts_helper";
78     private static final String MODIFIER_KEYS_SETTINGS = "modifier_keys_settings";
79     private static final String EXTRA_AUTO_SELECTION = "auto_selection";
80     private static final Uri sVirtualKeyboardSettingsUri = Secure.getUriFor(
81             Secure.SHOW_IME_WITH_HARD_KEYBOARD);
82     private static final Uri sAccessibilityBounceKeysUri = Secure.getUriFor(
83             Secure.ACCESSIBILITY_BOUNCE_KEYS);
84     private static final Uri sAccessibilitySlowKeysUri = Secure.getUriFor(
85             Secure.ACCESSIBILITY_SLOW_KEYS);
86     private static final Uri sAccessibilityStickyKeysUri = Secure.getUriFor(
87             Secure.ACCESSIBILITY_STICKY_KEYS);
88     public static final int BOUNCE_KEYS_THRESHOLD = 500;
89     public static final int SLOW_KEYS_THRESHOLD = 500;
90 
91     @NonNull
92     private final ArrayList<HardKeyboardDeviceInfo> mLastHardKeyboards = new ArrayList<>();
93 
94     private InputManager mIm;
95     private InputMethodManager mImm;
96     private InputDeviceIdentifier mAutoInputDeviceIdentifier;
97     private KeyboardSettingsFeatureProvider mFeatureProvider;
98     @NonNull
99     private PreferenceCategory mKeyboardAssistanceCategory;
100     @Nullable
101     private PreferenceCategory mKeyboardA11yCategory = null;
102     @Nullable
103     private TwoStatePreference mShowVirtualKeyboardSwitch = null;
104     @Nullable
105     private TwoStatePreference mAccessibilityBounceKeys = null;
106     @Nullable
107     private TwoStatePreference mAccessibilitySlowKeys = null;
108     @Nullable
109     private TwoStatePreference mAccessibilityStickyKeys = null;
110 
111 
112     private Intent mIntentWaitingForResult;
113     private boolean mSupportsFirmwareUpdate;
114 
115     static final String EXTRA_BT_ADDRESS = "extra_bt_address";
116     private String mBluetoothAddress;
117 
118     @Override
onSaveInstanceState(Bundle outState)119     public void onSaveInstanceState(Bundle outState) {
120         outState.putParcelable(EXTRA_AUTO_SELECTION, mAutoInputDeviceIdentifier);
121         super.onSaveInstanceState(outState);
122     }
123 
124     @Override
onCreatePreferences(Bundle bundle, String s)125     public void onCreatePreferences(Bundle bundle, String s) {
126         Activity activity = Preconditions.checkNotNull(getActivity());
127         addPreferencesFromResource(R.xml.physical_keyboard_settings);
128         mIm = Preconditions.checkNotNull(activity.getSystemService(InputManager.class));
129         mImm = Preconditions.checkNotNull(activity.getSystemService(InputMethodManager.class));
130         mKeyboardAssistanceCategory = Preconditions.checkNotNull(
131                 findPreference(KEYBOARD_OPTIONS_CATEGORY));
132         mShowVirtualKeyboardSwitch = Objects.requireNonNull(
133                 mKeyboardAssistanceCategory.findPreference(SHOW_VIRTUAL_KEYBOARD_SWITCH));
134 
135         mKeyboardA11yCategory = Objects.requireNonNull(findPreference(KEYBOARD_A11Y_CATEGORY));
136         mAccessibilityBounceKeys = Objects.requireNonNull(
137                 mKeyboardA11yCategory.findPreference(ACCESSIBILITY_BOUNCE_KEYS));
138         mAccessibilityBounceKeys.setSummary(
139                 getContext().getString(R.string.bounce_keys_summary, BOUNCE_KEYS_THRESHOLD));
140         mAccessibilitySlowKeys = Objects.requireNonNull(
141                 mKeyboardA11yCategory.findPreference(ACCESSIBILITY_SLOW_KEYS));
142         mAccessibilitySlowKeys.setSummary(
143                 getContext().getString(R.string.slow_keys_summary, SLOW_KEYS_THRESHOLD));
144         mAccessibilityStickyKeys = Objects.requireNonNull(
145                 mKeyboardA11yCategory.findPreference(ACCESSIBILITY_STICKY_KEYS));
146 
147         FeatureFactory featureFactory = FeatureFactory.getFeatureFactory();
148         mMetricsFeatureProvider = featureFactory.getMetricsFeatureProvider();
149         mFeatureProvider = featureFactory.getKeyboardSettingsFeatureProvider();
150         mSupportsFirmwareUpdate = mFeatureProvider.supportsFirmwareUpdate();
151         if (mSupportsFirmwareUpdate) {
152             mFeatureProvider.addFirmwareUpdateCategory(getContext(), getPreferenceScreen());
153         }
154         boolean isModifierKeySettingsEnabled = FeatureFlagUtils
155                 .isEnabled(getContext(), FeatureFlagUtils.SETTINGS_NEW_KEYBOARD_MODIFIER_KEY);
156         if (!isModifierKeySettingsEnabled) {
157             mKeyboardAssistanceCategory.removePreference(findPreference(MODIFIER_KEYS_SETTINGS));
158         }
159         if (!InputSettings.isAccessibilityBounceKeysFeatureEnabled()) {
160             mKeyboardA11yCategory.removePreference(mAccessibilityBounceKeys);
161         }
162         if (!InputSettings.isAccessibilitySlowKeysFeatureFlagEnabled()) {
163             mKeyboardA11yCategory.removePreference(mAccessibilitySlowKeys);
164         }
165         if (!InputSettings.isAccessibilityStickyKeysFeatureEnabled()) {
166             mKeyboardA11yCategory.removePreference(mAccessibilityStickyKeys);
167         }
168         InputDeviceIdentifier inputDeviceIdentifier = activity.getIntent().getParcelableExtra(
169                 KeyboardLayoutPickerFragment.EXTRA_INPUT_DEVICE_IDENTIFIER,
170                 InputDeviceIdentifier.class);
171         int intentFromWhere =
172                 activity.getIntent().getIntExtra(android.provider.Settings.EXTRA_ENTRYPOINT, -1);
173         if (intentFromWhere != -1) {
174             mMetricsFeatureProvider.action(
175                     getContext(), SettingsEnums.ACTION_OPEN_PK_SETTINGS_FROM, intentFromWhere);
176         }
177         if (inputDeviceIdentifier != null) {
178             mAutoInputDeviceIdentifier = inputDeviceIdentifier;
179         }
180         // Don't repeat the autoselection.
181         if (isAutoSelection(bundle, inputDeviceIdentifier)) {
182             showEnabledLocalesKeyboardLayoutList(inputDeviceIdentifier);
183         }
184     }
185 
isAutoSelection(Bundle bundle, InputDeviceIdentifier identifier)186     private static boolean isAutoSelection(Bundle bundle, InputDeviceIdentifier identifier) {
187         if (bundle != null && bundle.getParcelable(EXTRA_AUTO_SELECTION,
188                 InputDeviceIdentifier.class) != null) {
189             return false;
190         }
191         return identifier != null;
192     }
193 
194     @Override
onPreferenceTreeClick(Preference preference)195     public boolean onPreferenceTreeClick(Preference preference) {
196         if (KEYBOARD_SHORTCUTS_HELPER.equals(preference.getKey())) {
197             writePreferenceClickMetric(preference);
198             toggleKeyboardShortcutsMenu();
199             return true;
200         }
201         return super.onPreferenceTreeClick(preference);
202     }
203 
204     @Override
onResume()205     public void onResume() {
206         super.onResume();
207         mLastHardKeyboards.clear();
208         scheduleUpdateHardKeyboards();
209         mIm.registerInputDeviceListener(this, null);
210         Objects.requireNonNull(mShowVirtualKeyboardSwitch).setOnPreferenceChangeListener(
211                 mShowVirtualKeyboardSwitchPreferenceChangeListener);
212         Objects.requireNonNull(mAccessibilityBounceKeys).setOnPreferenceChangeListener(
213                 mAccessibilityBounceKeysSwitchPreferenceChangeListener);
214         Objects.requireNonNull(mAccessibilitySlowKeys).setOnPreferenceChangeListener(
215                 mAccessibilitySlowKeysSwitchPreferenceChangeListener);
216         Objects.requireNonNull(mAccessibilityStickyKeys).setOnPreferenceChangeListener(
217                 mAccessibilityStickyKeysSwitchPreferenceChangeListener);
218         registerSettingsObserver();
219     }
220 
221     @Override
onPause()222     public void onPause() {
223         super.onPause();
224         mLastHardKeyboards.clear();
225         mIm.unregisterInputDeviceListener(this);
226         Objects.requireNonNull(mShowVirtualKeyboardSwitch).setOnPreferenceChangeListener(null);
227         Objects.requireNonNull(mAccessibilityBounceKeys).setOnPreferenceChangeListener(null);
228         Objects.requireNonNull(mAccessibilitySlowKeys).setOnPreferenceChangeListener(null);
229         Objects.requireNonNull(mAccessibilityStickyKeys).setOnPreferenceChangeListener(null);
230         unregisterSettingsObserver();
231     }
232 
233     @Override
onInputDeviceAdded(int deviceId)234     public void onInputDeviceAdded(int deviceId) {
235         scheduleUpdateHardKeyboards();
236     }
237 
238     @Override
onInputDeviceRemoved(int deviceId)239     public void onInputDeviceRemoved(int deviceId) {
240         scheduleUpdateHardKeyboards();
241     }
242 
243     @Override
onInputDeviceChanged(int deviceId)244     public void onInputDeviceChanged(int deviceId) {
245         scheduleUpdateHardKeyboards();
246     }
247 
248     @Override
getMetricsCategory()249     public int getMetricsCategory() {
250         return SettingsEnums.PHYSICAL_KEYBOARDS;
251     }
252 
scheduleUpdateHardKeyboards()253     private void scheduleUpdateHardKeyboards() {
254         final Context context = getContext();
255         ThreadUtils.postOnBackgroundThread(() -> {
256             final List<HardKeyboardDeviceInfo> newHardKeyboards = getHardKeyboards(context);
257             if (newHardKeyboards.isEmpty()) {
258                 getActivity().finish();
259                 return;
260             }
261             ThreadUtils.postOnMainThread(() -> updateHardKeyboards(newHardKeyboards));
262         });
263     }
264 
updateHardKeyboards(@onNull List<HardKeyboardDeviceInfo> newHardKeyboards)265     private void updateHardKeyboards(@NonNull List<HardKeyboardDeviceInfo> newHardKeyboards) {
266         if (Objects.equals(mLastHardKeyboards, newHardKeyboards)) {
267             // Nothing has changed.  Ignore.
268             return;
269         }
270 
271         // TODO(yukawa): Maybe we should follow the style used in ConnectedDeviceDashboardFragment.
272 
273         mLastHardKeyboards.clear();
274         mLastHardKeyboards.addAll(newHardKeyboards);
275 
276         final PreferenceScreen preferenceScreen = getPreferenceScreen();
277         preferenceScreen.removeAll();
278         final PreferenceCategory category = new PreferenceCategory(getPrefContext());
279         category.setTitle(R.string.builtin_keyboard_settings_title);
280         category.setOrder(0);
281         preferenceScreen.addPreference(category);
282 
283         for (HardKeyboardDeviceInfo hardKeyboardDeviceInfo : newHardKeyboards) {
284             // TODO(yukawa): Consider using com.android.settings.widget.GearPreference
285             final Preference pref = new Preference(getPrefContext());
286             pref.setTitle(hardKeyboardDeviceInfo.mDeviceName);
287             String currentLayout =
288                     NewKeyboardSettingsUtils.getSelectedKeyboardLayoutLabelForUser(getContext(),
289                             UserHandle.myUserId(), hardKeyboardDeviceInfo.mDeviceIdentifier);
290             if (currentLayout != null) {
291                 pref.setSummary(currentLayout);
292             }
293             pref.setOnPreferenceClickListener(
294                     preference -> {
295                         showEnabledLocalesKeyboardLayoutList(
296                                 hardKeyboardDeviceInfo.mDeviceIdentifier);
297                         return true;
298                     });
299 
300             category.addPreference(pref);
301             StringBuilder vendorAndProductId = new StringBuilder();
302             String vendorId = String.valueOf(hardKeyboardDeviceInfo.mVendorId);
303             String productId = String.valueOf(hardKeyboardDeviceInfo.mProductId);
304             vendorAndProductId.append(vendorId);
305             vendorAndProductId.append("-");
306             vendorAndProductId.append(productId);
307             mMetricsFeatureProvider.action(
308                     getContext(),
309                     SettingsEnums.ACTION_USE_SPECIFIC_KEYBOARD,
310                     vendorAndProductId.toString());
311         }
312         mKeyboardAssistanceCategory.setOrder(1);
313         preferenceScreen.addPreference(mKeyboardAssistanceCategory);
314         if (mSupportsFirmwareUpdate) {
315             mFeatureProvider.addFirmwareUpdateCategory(getPrefContext(), preferenceScreen);
316         }
317         updateShowVirtualKeyboardSwitch();
318 
319         if (InputSettings.isAccessibilityBounceKeysFeatureEnabled()
320                 || InputSettings.isAccessibilityStickyKeysFeatureEnabled()
321                 || InputSettings.isAccessibilitySlowKeysFeatureFlagEnabled()) {
322             Objects.requireNonNull(mKeyboardA11yCategory).setOrder(2);
323             preferenceScreen.addPreference(mKeyboardA11yCategory);
324             updateAccessibilityBounceKeysSwitch();
325             updateAccessibilitySlowKeysSwitch();
326             updateAccessibilityStickyKeysSwitch();
327         }
328     }
329 
showKeyboardLayoutDialog(InputDeviceIdentifier inputDeviceIdentifier)330     private void showKeyboardLayoutDialog(InputDeviceIdentifier inputDeviceIdentifier) {
331         KeyboardLayoutDialogFragment fragment = new KeyboardLayoutDialogFragment(
332                 inputDeviceIdentifier);
333         fragment.setTargetFragment(this, 0);
334         fragment.show(getActivity().getSupportFragmentManager(), "keyboardLayout");
335     }
336 
showEnabledLocalesKeyboardLayoutList(InputDeviceIdentifier inputDeviceIdentifier)337     private void showEnabledLocalesKeyboardLayoutList(InputDeviceIdentifier inputDeviceIdentifier) {
338         Bundle arguments = new Bundle();
339         arguments.putParcelable(NewKeyboardSettingsUtils.EXTRA_INPUT_DEVICE_IDENTIFIER,
340                 inputDeviceIdentifier);
341         new SubSettingLauncher(getContext())
342                 .setSourceMetricsCategory(getMetricsCategory())
343                 .setDestination(NewKeyboardLayoutEnabledLocalesFragment.class.getName())
344                 .setArguments(arguments)
345                 .launch();
346     }
347 
registerSettingsObserver()348     private void registerSettingsObserver() {
349         unregisterSettingsObserver();
350         ContentResolver contentResolver = getActivity().getContentResolver();
351         contentResolver.registerContentObserver(
352                 sVirtualKeyboardSettingsUri,
353                 false,
354                 mContentObserver,
355                 UserHandle.myUserId());
356         if (InputSettings.isAccessibilityBounceKeysFeatureEnabled()) {
357             contentResolver.registerContentObserver(
358                     sAccessibilityBounceKeysUri,
359                     false,
360                     mContentObserver,
361                     UserHandle.myUserId());
362         }
363         if (InputSettings.isAccessibilitySlowKeysFeatureFlagEnabled()) {
364             contentResolver.registerContentObserver(
365                     sAccessibilitySlowKeysUri,
366                     false,
367                     mContentObserver,
368                     UserHandle.myUserId());
369         }
370         if (InputSettings.isAccessibilityStickyKeysFeatureEnabled()) {
371             contentResolver.registerContentObserver(
372                     sAccessibilityStickyKeysUri,
373                     false,
374                     mContentObserver,
375                     UserHandle.myUserId());
376         }
377         updateShowVirtualKeyboardSwitch();
378         updateAccessibilityBounceKeysSwitch();
379         updateAccessibilitySlowKeysSwitch();
380         updateAccessibilityStickyKeysSwitch();
381     }
382 
unregisterSettingsObserver()383     private void unregisterSettingsObserver() {
384         getActivity().getContentResolver().unregisterContentObserver(mContentObserver);
385     }
386 
updateShowVirtualKeyboardSwitch()387     private void updateShowVirtualKeyboardSwitch() {
388         Objects.requireNonNull(mShowVirtualKeyboardSwitch).setChecked(
389                 Secure.getInt(getContentResolver(), Secure.SHOW_IME_WITH_HARD_KEYBOARD, 0) != 0);
390     }
391 
updateAccessibilityBounceKeysSwitch()392     private void updateAccessibilityBounceKeysSwitch() {
393         if (!InputSettings.isAccessibilityBounceKeysFeatureEnabled()) {
394             return;
395         }
396         Objects.requireNonNull(mAccessibilityBounceKeys).setChecked(
397                 InputSettings.isAccessibilityBounceKeysEnabled(getContext()));
398     }
399 
updateAccessibilitySlowKeysSwitch()400     private void updateAccessibilitySlowKeysSwitch() {
401         if (!InputSettings.isAccessibilitySlowKeysFeatureFlagEnabled()) {
402             return;
403         }
404         Objects.requireNonNull(mAccessibilitySlowKeys).setChecked(
405                 InputSettings.isAccessibilitySlowKeysEnabled(getContext()));
406     }
407 
updateAccessibilityStickyKeysSwitch()408     private void updateAccessibilityStickyKeysSwitch() {
409         if (!InputSettings.isAccessibilityStickyKeysFeatureEnabled()) {
410             return;
411         }
412         Objects.requireNonNull(mAccessibilityStickyKeys).setChecked(
413                 InputSettings.isAccessibilityStickyKeysEnabled(getContext()));
414     }
415 
toggleKeyboardShortcutsMenu()416     private void toggleKeyboardShortcutsMenu() {
417         getActivity().requestShowKeyboardShortcuts();
418     }
419 
420     private final OnPreferenceChangeListener mShowVirtualKeyboardSwitchPreferenceChangeListener =
421             (preference, newValue) -> {
422                 final ContentResolver cr = getContentResolver();
423                 Secure.putInt(cr, Secure.SHOW_IME_WITH_HARD_KEYBOARD, ((Boolean) newValue) ? 1 : 0);
424                 cr.notifyChange(Secure.getUriFor(Secure.SHOW_IME_WITH_HARD_KEYBOARD),
425                         null /* observer */, ContentResolver.NOTIFY_NO_DELAY);
426                 return true;
427             };
428 
429     private final OnPreferenceChangeListener
430             mAccessibilityBounceKeysSwitchPreferenceChangeListener = (preference, newValue) -> {
431                 InputSettings.setAccessibilityBounceKeysThreshold(getContext(),
432                         ((Boolean) newValue) ? 500 : 0);
433                 return true;
434             };
435 
436     private final OnPreferenceChangeListener
437             mAccessibilitySlowKeysSwitchPreferenceChangeListener = (preference, newValue) -> {
438                 InputSettings.setAccessibilitySlowKeysThreshold(getContext(),
439                         ((Boolean) newValue) ? 500 : 0);
440                 return true;
441             };
442 
443     private final OnPreferenceChangeListener
444             mAccessibilityStickyKeysSwitchPreferenceChangeListener = (preference, newValue) -> {
445                 InputSettings.setAccessibilityStickyKeysEnabled(getContext(), (Boolean) newValue);
446                 return true;
447             };
448 
449     private final ContentObserver mContentObserver = new ContentObserver(new Handler(true)) {
450         @Override
451         public void onChange(boolean selfChange, Uri uri) {
452             if (sVirtualKeyboardSettingsUri.equals(uri)) {
453                 updateShowVirtualKeyboardSwitch();
454             } else if (sAccessibilityBounceKeysUri.equals(uri)) {
455                 updateAccessibilityBounceKeysSwitch();
456             } else if (sAccessibilitySlowKeysUri.equals(uri)) {
457                 updateAccessibilitySlowKeysSwitch();
458             } else if (sAccessibilityStickyKeysUri.equals(uri)) {
459                 updateAccessibilityStickyKeysSwitch();
460             }
461         }
462     };
463 
464     @Override
onSetupKeyboardLayouts(InputDeviceIdentifier inputDeviceIdentifier)465     public void onSetupKeyboardLayouts(InputDeviceIdentifier inputDeviceIdentifier) {
466         final Intent intent = new Intent(Intent.ACTION_MAIN);
467         intent.setClass(getActivity(), Settings.KeyboardLayoutPickerActivity.class);
468         intent.putExtra(KeyboardLayoutPickerFragment.EXTRA_INPUT_DEVICE_IDENTIFIER,
469                 inputDeviceIdentifier);
470         mIntentWaitingForResult = intent;
471         startActivityForResult(intent, 0);
472     }
473 
474     @Override
onActivityResult(int requestCode, int resultCode, Intent data)475     public void onActivityResult(int requestCode, int resultCode, Intent data) {
476         super.onActivityResult(requestCode, resultCode, data);
477 
478         if (mIntentWaitingForResult != null) {
479             InputDeviceIdentifier inputDeviceIdentifier = mIntentWaitingForResult
480                     .getParcelableExtra(KeyboardLayoutPickerFragment.EXTRA_INPUT_DEVICE_IDENTIFIER,
481                             InputDeviceIdentifier.class);
482             mIntentWaitingForResult = null;
483             showKeyboardLayoutDialog(inputDeviceIdentifier);
484         }
485     }
486 
getLayoutLabel(@onNull InputDevice device, @NonNull Context context, @NonNull InputManager im)487     private static String getLayoutLabel(@NonNull InputDevice device,
488             @NonNull Context context, @NonNull InputManager im) {
489         final String currentLayoutDesc =
490                 im.getCurrentKeyboardLayoutForInputDevice(device.getIdentifier());
491         if (currentLayoutDesc == null) {
492             return context.getString(R.string.keyboard_layout_default_label);
493         }
494         final KeyboardLayout currentLayout = im.getKeyboardLayout(currentLayoutDesc);
495         if (currentLayout == null) {
496             return context.getString(R.string.keyboard_layout_default_label);
497         }
498         // If current layout is specified but the layout is null, just return an empty string
499         // instead of falling back to R.string.keyboard_layout_default_label.
500         return TextUtils.emptyIfNull(currentLayout.getLabel());
501     }
502 
503     @NonNull
getHardKeyboards(@onNull Context context)504     static List<HardKeyboardDeviceInfo> getHardKeyboards(@NonNull Context context) {
505         final List<HardKeyboardDeviceInfo> keyboards = new ArrayList<>();
506         final InputManager im = context.getSystemService(InputManager.class);
507         if (im == null) {
508             return new ArrayList<>();
509         }
510         for (int deviceId : InputDevice.getDeviceIds()) {
511             final InputDevice device = InputDevice.getDevice(deviceId);
512             if (device == null || device.isVirtual() || !device.isFullKeyboard()) {
513                 continue;
514             }
515             keyboards.add(new HardKeyboardDeviceInfo(
516                     device.getName(),
517                     device.getIdentifier(),
518                     getLayoutLabel(device, context, im),
519                     device.getBluetoothAddress(),
520                     device.getVendorId(),
521                     device.getProductId()));
522         }
523 
524         // We intentionally don't reuse Comparator because Collator may not be thread-safe.
525         final Collator collator = Collator.getInstance();
526         keyboards.sort((a, b) -> {
527             int result = collator.compare(a.mDeviceName, b.mDeviceName);
528             if (result != 0) {
529                 return result;
530             }
531             result = a.mDeviceIdentifier.getDescriptor().compareTo(
532                     b.mDeviceIdentifier.getDescriptor());
533             if (result != 0) {
534                 return result;
535             }
536             return collator.compare(a.mLayoutLabel, b.mLayoutLabel);
537         });
538         return keyboards;
539     }
540 
541     public static final class HardKeyboardDeviceInfo {
542         @NonNull
543         public final String mDeviceName;
544         @NonNull
545         public final InputDeviceIdentifier mDeviceIdentifier;
546         @NonNull
547         public final String mLayoutLabel;
548         @Nullable
549         public final String mBluetoothAddress;
550         @NonNull
551         public final int mVendorId;
552         @NonNull
553         public final int mProductId;
554 
HardKeyboardDeviceInfo( @ullable String deviceName, @NonNull InputDeviceIdentifier deviceIdentifier, @NonNull String layoutLabel, @Nullable String bluetoothAddress, @NonNull int vendorId, @NonNull int productId)555         public HardKeyboardDeviceInfo(
556                 @Nullable String deviceName,
557                 @NonNull InputDeviceIdentifier deviceIdentifier,
558                 @NonNull String layoutLabel,
559                 @Nullable String bluetoothAddress,
560                 @NonNull int vendorId,
561                 @NonNull int productId) {
562             mDeviceName = TextUtils.emptyIfNull(deviceName);
563             mDeviceIdentifier = deviceIdentifier;
564             mLayoutLabel = layoutLabel;
565             mBluetoothAddress = bluetoothAddress;
566             mVendorId = vendorId;
567             mProductId = productId;
568         }
569 
570         @Override
equals(Object o)571         public boolean equals(Object o) {
572             if (o == this) return true;
573             if (o == null) return false;
574 
575             if (!(o instanceof HardKeyboardDeviceInfo)) return false;
576 
577             final HardKeyboardDeviceInfo that = (HardKeyboardDeviceInfo) o;
578             if (!TextUtils.equals(mDeviceName, that.mDeviceName)) {
579                 return false;
580             }
581             if (!Objects.equals(mDeviceIdentifier, that.mDeviceIdentifier)) {
582                 return false;
583             }
584             if (!TextUtils.equals(mLayoutLabel, that.mLayoutLabel)) {
585                 return false;
586             }
587             if (!TextUtils.equals(mBluetoothAddress, that.mBluetoothAddress)) {
588                 return false;
589             }
590 
591             return true;
592         }
593     }
594 
595     public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =
596             new BaseSearchIndexProvider() {
597                 @Override
598                 public List<SearchIndexableResource> getXmlResourcesToIndex(
599                         Context context, boolean enabled) {
600                     final SearchIndexableResource sir = new SearchIndexableResource(context);
601                     sir.xmlResId = R.xml.physical_keyboard_settings;
602                     return Arrays.asList(sir);
603                 }
604 
605                 @Override
606                 protected boolean isPageSearchEnabled(Context context) {
607                     return !getHardKeyboards(context).isEmpty();
608                 }
609             };
610 }
611