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.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.app.Activity; 22 import android.app.settings.SettingsEnums; 23 import android.content.ContentResolver; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.database.ContentObserver; 27 import android.hardware.input.InputDeviceIdentifier; 28 import android.hardware.input.InputManager; 29 import android.hardware.input.KeyboardLayout; 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.preference.Preference; 41 import androidx.preference.Preference.OnPreferenceChangeListener; 42 import androidx.preference.PreferenceCategory; 43 import androidx.preference.PreferenceScreen; 44 import androidx.preference.SwitchPreference; 45 46 import com.android.internal.util.Preconditions; 47 import com.android.settings.R; 48 import com.android.settings.Settings; 49 import com.android.settings.SettingsPreferenceFragment; 50 import com.android.settings.core.SubSettingLauncher; 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 @SearchIndexable 63 public final class PhysicalKeyboardFragment extends SettingsPreferenceFragment 64 implements InputManager.InputDeviceListener, 65 KeyboardLayoutDialogFragment.OnSetupKeyboardLayoutsListener { 66 67 private static final String KEYBOARD_OPTIONS_CATEGORY = "keyboard_options_category"; 68 private static final String SHOW_VIRTUAL_KEYBOARD_SWITCH = "show_virtual_keyboard_switch"; 69 private static final String KEYBOARD_SHORTCUTS_HELPER = "keyboard_shortcuts_helper"; 70 private static final String MODIFIER_KEYS_SETTINGS = "modifier_keys_settings"; 71 private static final String EXTRA_AUTO_SELECTION = "auto_selection"; 72 73 @NonNull 74 private final ArrayList<HardKeyboardDeviceInfo> mLastHardKeyboards = new ArrayList<>(); 75 76 private InputManager mIm; 77 private InputMethodManager mImm; 78 private InputDeviceIdentifier mAutoInputDeviceIdentifier; 79 private KeyboardSettingsFeatureProvider mFeatureProvider; 80 @NonNull 81 private PreferenceCategory mKeyboardAssistanceCategory; 82 @NonNull 83 private SwitchPreference mShowVirtualKeyboardSwitch; 84 85 private Intent mIntentWaitingForResult; 86 private boolean mIsNewKeyboardSettings; 87 private boolean mSupportsFirmwareUpdate; 88 89 static final String EXTRA_BT_ADDRESS = "extra_bt_address"; 90 private String mBluetoothAddress; 91 92 @Override onSaveInstanceState(Bundle outState)93 public void onSaveInstanceState(Bundle outState) { 94 outState.putParcelable(EXTRA_AUTO_SELECTION, mAutoInputDeviceIdentifier); 95 super.onSaveInstanceState(outState); 96 } 97 98 @Override onCreatePreferences(Bundle bundle, String s)99 public void onCreatePreferences(Bundle bundle, String s) { 100 Activity activity = Preconditions.checkNotNull(getActivity()); 101 addPreferencesFromResource(R.xml.physical_keyboard_settings); 102 mIm = Preconditions.checkNotNull(activity.getSystemService(InputManager.class)); 103 mImm = Preconditions.checkNotNull(activity.getSystemService(InputMethodManager.class)); 104 mKeyboardAssistanceCategory = Preconditions.checkNotNull( 105 (PreferenceCategory) findPreference(KEYBOARD_OPTIONS_CATEGORY)); 106 mShowVirtualKeyboardSwitch = Preconditions.checkNotNull( 107 (SwitchPreference) mKeyboardAssistanceCategory.findPreference( 108 SHOW_VIRTUAL_KEYBOARD_SWITCH)); 109 110 FeatureFactory featureFactory = FeatureFactory.getFactory(getContext()); 111 mFeatureProvider = featureFactory.getKeyboardSettingsFeatureProvider(); 112 mSupportsFirmwareUpdate = mFeatureProvider.supportsFirmwareUpdate(); 113 if (mSupportsFirmwareUpdate) { 114 mFeatureProvider.addFirmwareUpdateCategory(getContext(), getPreferenceScreen()); 115 } 116 mIsNewKeyboardSettings = FeatureFlagUtils.isEnabled( 117 getContext(), FeatureFlagUtils.SETTINGS_NEW_KEYBOARD_UI); 118 boolean isModifierKeySettingsEnabled = FeatureFlagUtils 119 .isEnabled(getContext(), FeatureFlagUtils.SETTINGS_NEW_KEYBOARD_MODIFIER_KEY); 120 if (!isModifierKeySettingsEnabled) { 121 mKeyboardAssistanceCategory.removePreference(findPreference(MODIFIER_KEYS_SETTINGS)); 122 } 123 InputDeviceIdentifier inputDeviceIdentifier = activity.getIntent().getParcelableExtra( 124 KeyboardLayoutPickerFragment.EXTRA_INPUT_DEVICE_IDENTIFIER); 125 int intentFromWhere = 126 activity.getIntent().getIntExtra(android.provider.Settings.EXTRA_ENTRYPOINT, -1); 127 if (inputDeviceIdentifier != null) { 128 mAutoInputDeviceIdentifier = inputDeviceIdentifier; 129 } 130 // Don't repeat the autoselection. 131 if (isAutoSelection(bundle, inputDeviceIdentifier)) { 132 showEnabledLocalesKeyboardLayoutList(inputDeviceIdentifier); 133 } 134 } 135 isAutoSelection(Bundle bundle, InputDeviceIdentifier identifier)136 private static boolean isAutoSelection(Bundle bundle, InputDeviceIdentifier identifier) { 137 if (bundle != null && bundle.getParcelable(EXTRA_AUTO_SELECTION) != null) { 138 return false; 139 } 140 return identifier != null; 141 } 142 143 @Override onPreferenceTreeClick(Preference preference)144 public boolean onPreferenceTreeClick(Preference preference) { 145 if (KEYBOARD_SHORTCUTS_HELPER.equals(preference.getKey())) { 146 writePreferenceClickMetric(preference); 147 toggleKeyboardShortcutsMenu(); 148 return true; 149 } 150 return super.onPreferenceTreeClick(preference); 151 } 152 153 @Override onResume()154 public void onResume() { 155 super.onResume(); 156 mLastHardKeyboards.clear(); 157 scheduleUpdateHardKeyboards(); 158 mIm.registerInputDeviceListener(this, null); 159 mShowVirtualKeyboardSwitch.setOnPreferenceChangeListener( 160 mShowVirtualKeyboardSwitchPreferenceChangeListener); 161 registerShowVirtualKeyboardSettingsObserver(); 162 } 163 164 @Override onPause()165 public void onPause() { 166 super.onPause(); 167 mLastHardKeyboards.clear(); 168 mIm.unregisterInputDeviceListener(this); 169 mShowVirtualKeyboardSwitch.setOnPreferenceChangeListener(null); 170 unregisterShowVirtualKeyboardSettingsObserver(); 171 } 172 173 @Override onInputDeviceAdded(int deviceId)174 public void onInputDeviceAdded(int deviceId) { 175 scheduleUpdateHardKeyboards(); 176 } 177 178 @Override onInputDeviceRemoved(int deviceId)179 public void onInputDeviceRemoved(int deviceId) { 180 scheduleUpdateHardKeyboards(); 181 } 182 183 @Override onInputDeviceChanged(int deviceId)184 public void onInputDeviceChanged(int deviceId) { 185 scheduleUpdateHardKeyboards(); 186 } 187 188 @Override getMetricsCategory()189 public int getMetricsCategory() { 190 return SettingsEnums.PHYSICAL_KEYBOARDS; 191 } 192 scheduleUpdateHardKeyboards()193 private void scheduleUpdateHardKeyboards() { 194 final Context context = getContext(); 195 ThreadUtils.postOnBackgroundThread(() -> { 196 final List<HardKeyboardDeviceInfo> newHardKeyboards = getHardKeyboards(context); 197 if (newHardKeyboards.isEmpty()) { 198 getActivity().finish(); 199 return; 200 } 201 ThreadUtils.postOnMainThread(() -> updateHardKeyboards(newHardKeyboards)); 202 }); 203 } 204 updateHardKeyboards(@onNull List<HardKeyboardDeviceInfo> newHardKeyboards)205 private void updateHardKeyboards(@NonNull List<HardKeyboardDeviceInfo> newHardKeyboards) { 206 if (Objects.equals(mLastHardKeyboards, newHardKeyboards)) { 207 // Nothing has changed. Ignore. 208 return; 209 } 210 211 // TODO(yukawa): Maybe we should follow the style used in ConnectedDeviceDashboardFragment. 212 213 mLastHardKeyboards.clear(); 214 mLastHardKeyboards.addAll(newHardKeyboards); 215 216 final PreferenceScreen preferenceScreen = getPreferenceScreen(); 217 preferenceScreen.removeAll(); 218 final PreferenceCategory category = new PreferenceCategory(getPrefContext()); 219 category.setTitle(R.string.builtin_keyboard_settings_title); 220 category.setOrder(0); 221 preferenceScreen.addPreference(category); 222 223 for (HardKeyboardDeviceInfo hardKeyboardDeviceInfo : newHardKeyboards) { 224 // TODO(yukawa): Consider using com.android.settings.widget.GearPreference 225 final Preference pref = new Preference(getPrefContext()); 226 pref.setTitle(hardKeyboardDeviceInfo.mDeviceName); 227 if (mIsNewKeyboardSettings) { 228 List<String> suitableImes = new ArrayList<>(); 229 suitableImes.addAll( 230 NewKeyboardSettingsUtils.getSuitableImeLabels( 231 getContext(), mImm, UserHandle.myUserId())); 232 if (!suitableImes.isEmpty()) { 233 String summary = suitableImes.get(0); 234 StringBuilder result = new StringBuilder(summary); 235 for (int i = 1; i < suitableImes.size(); i++) { 236 result.append(", ").append(suitableImes.get(i)); 237 } 238 pref.setSummary(result.toString()); 239 } else { 240 pref.setSummary(hardKeyboardDeviceInfo.mLayoutLabel); 241 } 242 pref.setOnPreferenceClickListener( 243 preference -> { 244 showEnabledLocalesKeyboardLayoutList( 245 hardKeyboardDeviceInfo.mDeviceIdentifier); 246 return true; 247 }); 248 } else { 249 pref.setSummary(hardKeyboardDeviceInfo.mLayoutLabel); 250 pref.setOnPreferenceClickListener( 251 preference -> { 252 showKeyboardLayoutDialog(hardKeyboardDeviceInfo.mDeviceIdentifier); 253 return true; 254 }); 255 } 256 category.addPreference(pref); 257 } 258 mKeyboardAssistanceCategory.setOrder(1); 259 preferenceScreen.addPreference(mKeyboardAssistanceCategory); 260 if (mSupportsFirmwareUpdate) { 261 mFeatureProvider.addFirmwareUpdateCategory(getPrefContext(), preferenceScreen); 262 } 263 updateShowVirtualKeyboardSwitch(); 264 } 265 showKeyboardLayoutDialog(InputDeviceIdentifier inputDeviceIdentifier)266 private void showKeyboardLayoutDialog(InputDeviceIdentifier inputDeviceIdentifier) { 267 KeyboardLayoutDialogFragment fragment = new KeyboardLayoutDialogFragment( 268 inputDeviceIdentifier); 269 fragment.setTargetFragment(this, 0); 270 fragment.show(getActivity().getSupportFragmentManager(), "keyboardLayout"); 271 } 272 showEnabledLocalesKeyboardLayoutList(InputDeviceIdentifier inputDeviceIdentifier)273 private void showEnabledLocalesKeyboardLayoutList(InputDeviceIdentifier inputDeviceIdentifier) { 274 Bundle arguments = new Bundle(); 275 arguments.putParcelable(NewKeyboardSettingsUtils.EXTRA_INPUT_DEVICE_IDENTIFIER, 276 inputDeviceIdentifier); 277 new SubSettingLauncher(getContext()) 278 .setSourceMetricsCategory(getMetricsCategory()) 279 .setDestination(NewKeyboardLayoutEnabledLocalesFragment.class.getName()) 280 .setArguments(arguments) 281 .launch(); 282 } 283 registerShowVirtualKeyboardSettingsObserver()284 private void registerShowVirtualKeyboardSettingsObserver() { 285 unregisterShowVirtualKeyboardSettingsObserver(); 286 getActivity().getContentResolver().registerContentObserver( 287 Secure.getUriFor(Secure.SHOW_IME_WITH_HARD_KEYBOARD), 288 false, 289 mContentObserver, 290 UserHandle.myUserId()); 291 updateShowVirtualKeyboardSwitch(); 292 } 293 unregisterShowVirtualKeyboardSettingsObserver()294 private void unregisterShowVirtualKeyboardSettingsObserver() { 295 getActivity().getContentResolver().unregisterContentObserver(mContentObserver); 296 } 297 updateShowVirtualKeyboardSwitch()298 private void updateShowVirtualKeyboardSwitch() { 299 mShowVirtualKeyboardSwitch.setChecked( 300 Secure.getInt(getContentResolver(), Secure.SHOW_IME_WITH_HARD_KEYBOARD, 0) != 0); 301 } 302 toggleKeyboardShortcutsMenu()303 private void toggleKeyboardShortcutsMenu() { 304 getActivity().requestShowKeyboardShortcuts(); 305 } 306 307 private final OnPreferenceChangeListener mShowVirtualKeyboardSwitchPreferenceChangeListener = 308 (preference, newValue) -> { 309 final ContentResolver cr = getContentResolver(); 310 Secure.putInt(cr, Secure.SHOW_IME_WITH_HARD_KEYBOARD, ((Boolean) newValue) ? 1 : 0); 311 cr.notifyChange(Secure.getUriFor(Secure.SHOW_IME_WITH_HARD_KEYBOARD), 312 null /* observer */, ContentResolver.NOTIFY_NO_DELAY); 313 return true; 314 }; 315 316 private final ContentObserver mContentObserver = new ContentObserver(new Handler(true)) { 317 @Override 318 public void onChange(boolean selfChange) { 319 updateShowVirtualKeyboardSwitch(); 320 } 321 }; 322 323 @Override onSetupKeyboardLayouts(InputDeviceIdentifier inputDeviceIdentifier)324 public void onSetupKeyboardLayouts(InputDeviceIdentifier inputDeviceIdentifier) { 325 final Intent intent = new Intent(Intent.ACTION_MAIN); 326 intent.setClass(getActivity(), Settings.KeyboardLayoutPickerActivity.class); 327 intent.putExtra(KeyboardLayoutPickerFragment.EXTRA_INPUT_DEVICE_IDENTIFIER, 328 inputDeviceIdentifier); 329 mIntentWaitingForResult = intent; 330 startActivityForResult(intent, 0); 331 } 332 333 @Override onActivityResult(int requestCode, int resultCode, Intent data)334 public void onActivityResult(int requestCode, int resultCode, Intent data) { 335 super.onActivityResult(requestCode, resultCode, data); 336 337 if (mIntentWaitingForResult != null) { 338 InputDeviceIdentifier inputDeviceIdentifier = mIntentWaitingForResult 339 .getParcelableExtra(KeyboardLayoutPickerFragment.EXTRA_INPUT_DEVICE_IDENTIFIER); 340 mIntentWaitingForResult = null; 341 showKeyboardLayoutDialog(inputDeviceIdentifier); 342 } 343 } 344 getLayoutLabel(@onNull InputDevice device, @NonNull Context context, @NonNull InputManager im)345 private static String getLayoutLabel(@NonNull InputDevice device, 346 @NonNull Context context, @NonNull InputManager im) { 347 final String currentLayoutDesc = 348 im.getCurrentKeyboardLayoutForInputDevice(device.getIdentifier()); 349 if (currentLayoutDesc == null) { 350 return context.getString(R.string.keyboard_layout_default_label); 351 } 352 final KeyboardLayout currentLayout = im.getKeyboardLayout(currentLayoutDesc); 353 if (currentLayout == null) { 354 return context.getString(R.string.keyboard_layout_default_label); 355 } 356 // If current layout is specified but the layout is null, just return an empty string 357 // instead of falling back to R.string.keyboard_layout_default_label. 358 return TextUtils.emptyIfNull(currentLayout.getLabel()); 359 } 360 361 @NonNull getHardKeyboards(@onNull Context context)362 static List<HardKeyboardDeviceInfo> getHardKeyboards(@NonNull Context context) { 363 final List<HardKeyboardDeviceInfo> keyboards = new ArrayList<>(); 364 final InputManager im = context.getSystemService(InputManager.class); 365 if (im == null) { 366 return new ArrayList<>(); 367 } 368 for (int deviceId : InputDevice.getDeviceIds()) { 369 final InputDevice device = InputDevice.getDevice(deviceId); 370 if (device == null || device.isVirtual() || !device.isFullKeyboard()) { 371 continue; 372 } 373 keyboards.add(new HardKeyboardDeviceInfo( 374 device.getName(), 375 device.getIdentifier(), 376 getLayoutLabel(device, context, im), 377 device.getBluetoothAddress())); 378 } 379 380 // We intentionally don't reuse Comparator because Collator may not be thread-safe. 381 final Collator collator = Collator.getInstance(); 382 keyboards.sort((a, b) -> { 383 int result = collator.compare(a.mDeviceName, b.mDeviceName); 384 if (result != 0) { 385 return result; 386 } 387 result = a.mDeviceIdentifier.getDescriptor().compareTo( 388 b.mDeviceIdentifier.getDescriptor()); 389 if (result != 0) { 390 return result; 391 } 392 return collator.compare(a.mLayoutLabel, b.mLayoutLabel); 393 }); 394 return keyboards; 395 } 396 397 public static final class HardKeyboardDeviceInfo { 398 @NonNull 399 public final String mDeviceName; 400 @NonNull 401 public final InputDeviceIdentifier mDeviceIdentifier; 402 @NonNull 403 public final String mLayoutLabel; 404 @Nullable 405 public final String mBluetoothAddress; 406 HardKeyboardDeviceInfo( @ullable String deviceName, @NonNull InputDeviceIdentifier deviceIdentifier, @NonNull String layoutLabel, @Nullable String bluetoothAddress)407 public HardKeyboardDeviceInfo( 408 @Nullable String deviceName, 409 @NonNull InputDeviceIdentifier deviceIdentifier, 410 @NonNull String layoutLabel, 411 @Nullable String bluetoothAddress) { 412 mDeviceName = TextUtils.emptyIfNull(deviceName); 413 mDeviceIdentifier = deviceIdentifier; 414 mLayoutLabel = layoutLabel; 415 mBluetoothAddress = bluetoothAddress; 416 } 417 418 @Override equals(Object o)419 public boolean equals(Object o) { 420 if (o == this) return true; 421 if (o == null) return false; 422 423 if (!(o instanceof HardKeyboardDeviceInfo)) return false; 424 425 final HardKeyboardDeviceInfo that = (HardKeyboardDeviceInfo) o; 426 if (!TextUtils.equals(mDeviceName, that.mDeviceName)) { 427 return false; 428 } 429 if (!Objects.equals(mDeviceIdentifier, that.mDeviceIdentifier)) { 430 return false; 431 } 432 if (!TextUtils.equals(mLayoutLabel, that.mLayoutLabel)) { 433 return false; 434 } 435 if (!TextUtils.equals(mBluetoothAddress, that.mBluetoothAddress)) { 436 return false; 437 } 438 439 return true; 440 } 441 } 442 443 public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = 444 new BaseSearchIndexProvider() { 445 @Override 446 public List<SearchIndexableResource> getXmlResourcesToIndex( 447 Context context, boolean enabled) { 448 final SearchIndexableResource sir = new SearchIndexableResource(context); 449 sir.xmlResId = R.xml.physical_keyboard_settings; 450 return Arrays.asList(sir); 451 } 452 }; 453 } 454