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