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