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.LoaderManager; 23 import android.content.AsyncTaskLoader; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.Loader; 27 import android.database.ContentObserver; 28 import android.hardware.input.InputDeviceIdentifier; 29 import android.hardware.input.InputManager; 30 import android.hardware.input.KeyboardLayout; 31 import android.os.Bundle; 32 import android.os.Handler; 33 import android.os.UserHandle; 34 import android.provider.Settings.Secure; 35 import android.support.v14.preference.SwitchPreference; 36 import android.support.v7.preference.Preference; 37 import android.support.v7.preference.Preference.OnPreferenceChangeListener; 38 import android.support.v7.preference.PreferenceCategory; 39 import android.support.v7.preference.PreferenceScreen; 40 import android.text.TextUtils; 41 import android.view.InputDevice; 42 import android.view.inputmethod.InputMethodInfo; 43 import android.view.inputmethod.InputMethodManager; 44 import android.view.inputmethod.InputMethodSubtype; 45 46 import com.android.internal.inputmethod.InputMethodUtils; 47 import com.android.internal.logging.nano.MetricsProto.MetricsEvent; 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.search.BaseSearchIndexProvider; 53 import com.android.settings.search.Indexable; 54 import com.android.settings.search.SearchIndexableRaw; 55 import com.android.settingslib.inputmethod.InputMethodAndSubtypeUtil; 56 57 import java.text.Collator; 58 import java.util.ArrayList; 59 import java.util.Collections; 60 import java.util.HashMap; 61 import java.util.HashSet; 62 import java.util.List; 63 import java.util.Objects; 64 65 public final class PhysicalKeyboardFragment extends SettingsPreferenceFragment 66 implements InputManager.InputDeviceListener, Indexable { 67 68 private static final String KEYBOARD_ASSISTANCE_CATEGORY = "keyboard_assistance_category"; 69 private static final String SHOW_VIRTUAL_KEYBOARD_SWITCH = "show_virtual_keyboard_switch"; 70 private static final String KEYBOARD_SHORTCUTS_HELPER = "keyboard_shortcuts_helper"; 71 private static final String IM_SUBTYPE_MODE_KEYBOARD = "keyboard"; 72 73 @NonNull 74 private final List<HardKeyboardDeviceInfo> mLastHardKeyboards = new ArrayList<>(); 75 @NonNull 76 private final List<KeyboardInfoPreference> mTempKeyboardInfoList = new ArrayList<>(); 77 78 @NonNull 79 private final HashSet<Integer> mLoaderIDs = new HashSet<>(); 80 private int mNextLoaderId = 0; 81 82 private InputManager mIm; 83 @NonNull 84 private PreferenceCategory mKeyboardAssistanceCategory; 85 @NonNull 86 private SwitchPreference mShowVirtualKeyboardSwitch; 87 @NonNull 88 private InputMethodUtils.InputMethodSettings mSettings; 89 90 @Override 91 public void onCreatePreferences(Bundle bundle, String s) { 92 Activity activity = Preconditions.checkNotNull(getActivity()); 93 addPreferencesFromResource(R.xml.physical_keyboard_settings); 94 mIm = Preconditions.checkNotNull(activity.getSystemService(InputManager.class)); 95 mSettings = new InputMethodUtils.InputMethodSettings( 96 activity.getResources(), 97 getContentResolver(), 98 new HashMap<>(), 99 new ArrayList<>(), 100 UserHandle.myUserId(), 101 false /* copyOnWrite */); 102 mKeyboardAssistanceCategory = Preconditions.checkNotNull( 103 (PreferenceCategory) findPreference(KEYBOARD_ASSISTANCE_CATEGORY)); 104 mShowVirtualKeyboardSwitch = Preconditions.checkNotNull( 105 (SwitchPreference) mKeyboardAssistanceCategory.findPreference( 106 SHOW_VIRTUAL_KEYBOARD_SWITCH)); 107 findPreference(KEYBOARD_SHORTCUTS_HELPER).setOnPreferenceClickListener( 108 new Preference.OnPreferenceClickListener() { 109 @Override 110 public boolean onPreferenceClick(Preference preference) { 111 toggleKeyboardShortcutsMenu(); 112 return true; 113 } 114 }); 115 } 116 117 @Override 118 public void onResume() { 119 super.onResume(); 120 clearLoader(); 121 mLastHardKeyboards.clear(); 122 updateHardKeyboards(); 123 mIm.registerInputDeviceListener(this, null); 124 mShowVirtualKeyboardSwitch.setOnPreferenceChangeListener( 125 mShowVirtualKeyboardSwitchPreferenceChangeListener); 126 registerShowVirtualKeyboardSettingsObserver(); 127 } 128 129 @Override 130 public void onPause() { 131 super.onPause(); 132 clearLoader(); 133 mLastHardKeyboards.clear(); 134 mIm.unregisterInputDeviceListener(this); 135 mShowVirtualKeyboardSwitch.setOnPreferenceChangeListener(null); 136 unregisterShowVirtualKeyboardSettingsObserver(); 137 } 138 139 private void onLoadFinishedInternal( 140 final int loaderId, @NonNull final List<Keyboards> keyboardsList) { 141 if (!mLoaderIDs.remove(loaderId)) { 142 // Already destroyed loader. Ignore. 143 return; 144 } 145 146 Collections.sort(keyboardsList); 147 final PreferenceScreen preferenceScreen = getPreferenceScreen(); 148 preferenceScreen.removeAll(); 149 for (Keyboards keyboards : keyboardsList) { 150 final PreferenceCategory category = new PreferenceCategory(getPrefContext(), null); 151 category.setTitle(keyboards.mDeviceInfo.mDeviceName); 152 category.setOrder(0); 153 preferenceScreen.addPreference(category); 154 for (Keyboards.KeyboardInfo info : keyboards.mKeyboardInfoList) { 155 mTempKeyboardInfoList.clear(); 156 final InputMethodInfo imi = info.mImi; 157 final InputMethodSubtype imSubtype = info.mImSubtype; 158 if (imi != null) { 159 KeyboardInfoPreference pref = 160 new KeyboardInfoPreference(getPrefContext(), info); 161 pref.setOnPreferenceClickListener(preference -> { 162 showKeyboardLayoutScreen( 163 keyboards.mDeviceInfo.mDeviceIdentifier, imi, imSubtype); 164 return true; 165 }); 166 mTempKeyboardInfoList.add(pref); 167 Collections.sort(mTempKeyboardInfoList); 168 } 169 for (KeyboardInfoPreference pref : mTempKeyboardInfoList) { 170 category.addPreference(pref); 171 } 172 } 173 } 174 mTempKeyboardInfoList.clear(); 175 mKeyboardAssistanceCategory.setOrder(1); 176 preferenceScreen.addPreference(mKeyboardAssistanceCategory); 177 updateShowVirtualKeyboardSwitch(); 178 } 179 180 @Override 181 public void onInputDeviceAdded(int deviceId) { 182 updateHardKeyboards(); 183 } 184 185 @Override 186 public void onInputDeviceRemoved(int deviceId) { 187 updateHardKeyboards(); 188 } 189 190 @Override 191 public void onInputDeviceChanged(int deviceId) { 192 updateHardKeyboards(); 193 } 194 195 @Override 196 public int getMetricsCategory() { 197 return MetricsEvent.PHYSICAL_KEYBOARDS; 198 } 199 200 @NonNull 201 public static List<HardKeyboardDeviceInfo> getHardKeyboards() { 202 final List<HardKeyboardDeviceInfo> keyboards = new ArrayList<>(); 203 final int[] devicesIds = InputDevice.getDeviceIds(); 204 for (int deviceId : devicesIds) { 205 final InputDevice device = InputDevice.getDevice(deviceId); 206 if (device != null && !device.isVirtual() && device.isFullKeyboard()) { 207 keyboards.add(new HardKeyboardDeviceInfo(device.getName(), device.getIdentifier())); 208 } 209 } 210 return keyboards; 211 } 212 213 private void updateHardKeyboards() { 214 final List<HardKeyboardDeviceInfo> newHardKeyboards = getHardKeyboards(); 215 if (!Objects.equals(newHardKeyboards, mLastHardKeyboards)) { 216 clearLoader(); 217 mLastHardKeyboards.clear(); 218 mLastHardKeyboards.addAll(newHardKeyboards); 219 mLoaderIDs.add(mNextLoaderId); 220 getLoaderManager().initLoader(mNextLoaderId, null, 221 new Callbacks(getContext(), this, mLastHardKeyboards)); 222 ++mNextLoaderId; 223 } 224 } 225 226 private void showKeyboardLayoutScreen( 227 @NonNull InputDeviceIdentifier inputDeviceIdentifier, 228 @NonNull InputMethodInfo imi, 229 @Nullable InputMethodSubtype imSubtype) { 230 final Intent intent = new Intent(Intent.ACTION_MAIN); 231 intent.setClass(getActivity(), Settings.KeyboardLayoutPickerActivity.class); 232 intent.putExtra(KeyboardLayoutPickerFragment.EXTRA_INPUT_DEVICE_IDENTIFIER, 233 inputDeviceIdentifier); 234 intent.putExtra(KeyboardLayoutPickerFragment.EXTRA_INPUT_METHOD_INFO, imi); 235 intent.putExtra(KeyboardLayoutPickerFragment.EXTRA_INPUT_METHOD_SUBTYPE, imSubtype); 236 startActivity(intent); 237 } 238 239 private void clearLoader() { 240 for (final int loaderId : mLoaderIDs) { 241 getLoaderManager().destroyLoader(loaderId); 242 } 243 mLoaderIDs.clear(); 244 } 245 246 private void registerShowVirtualKeyboardSettingsObserver() { 247 unregisterShowVirtualKeyboardSettingsObserver(); 248 getActivity().getContentResolver().registerContentObserver( 249 Secure.getUriFor(Secure.SHOW_IME_WITH_HARD_KEYBOARD), 250 false, 251 mContentObserver, 252 UserHandle.myUserId()); 253 updateShowVirtualKeyboardSwitch(); 254 } 255 256 private void unregisterShowVirtualKeyboardSettingsObserver() { 257 getActivity().getContentResolver().unregisterContentObserver(mContentObserver); 258 } 259 260 private void updateShowVirtualKeyboardSwitch() { 261 mShowVirtualKeyboardSwitch.setChecked(mSettings.isShowImeWithHardKeyboardEnabled()); 262 } 263 264 private void toggleKeyboardShortcutsMenu() { 265 getActivity().requestShowKeyboardShortcuts(); 266 } 267 268 private final OnPreferenceChangeListener mShowVirtualKeyboardSwitchPreferenceChangeListener = 269 new OnPreferenceChangeListener() { 270 @Override 271 public boolean onPreferenceChange(Preference preference, Object newValue) { 272 mSettings.setShowImeWithHardKeyboard((Boolean) newValue); 273 return true; 274 } 275 }; 276 277 private final ContentObserver mContentObserver = new ContentObserver(new Handler(true)) { 278 @Override 279 public void onChange(boolean selfChange) { 280 updateShowVirtualKeyboardSwitch(); 281 } 282 }; 283 284 private static final class Callbacks implements LoaderManager.LoaderCallbacks<List<Keyboards>> { 285 @NonNull 286 final Context mContext; 287 @NonNull 288 final PhysicalKeyboardFragment mPhysicalKeyboardFragment; 289 @NonNull 290 final List<HardKeyboardDeviceInfo> mHardKeyboards; 291 public Callbacks( 292 @NonNull Context context, 293 @NonNull PhysicalKeyboardFragment physicalKeyboardFragment, 294 @NonNull List<HardKeyboardDeviceInfo> hardKeyboards) { 295 mContext = context; 296 mPhysicalKeyboardFragment = physicalKeyboardFragment; 297 mHardKeyboards = hardKeyboards; 298 } 299 300 @Override 301 public Loader<List<Keyboards>> onCreateLoader(int id, Bundle args) { 302 return new KeyboardLayoutLoader(mContext, mHardKeyboards); 303 } 304 305 @Override 306 public void onLoadFinished(Loader<List<Keyboards>> loader, List<Keyboards> data) { 307 mPhysicalKeyboardFragment.onLoadFinishedInternal(loader.getId(), data); 308 } 309 310 @Override 311 public void onLoaderReset(Loader<List<Keyboards>> loader) { 312 } 313 } 314 315 private static final class KeyboardLayoutLoader extends AsyncTaskLoader<List<Keyboards>> { 316 @NonNull 317 private final List<HardKeyboardDeviceInfo> mHardKeyboards; 318 319 public KeyboardLayoutLoader( 320 @NonNull Context context, 321 @NonNull List<HardKeyboardDeviceInfo> hardKeyboards) { 322 super(context); 323 mHardKeyboards = Preconditions.checkNotNull(hardKeyboards); 324 } 325 326 private Keyboards loadInBackground(HardKeyboardDeviceInfo deviceInfo) { 327 final ArrayList<Keyboards.KeyboardInfo> keyboardInfoList = new ArrayList<>(); 328 final InputMethodManager imm = getContext().getSystemService(InputMethodManager.class); 329 final InputManager im = getContext().getSystemService(InputManager.class); 330 if (imm != null && im != null) { 331 for (InputMethodInfo imi : imm.getEnabledInputMethodList()) { 332 final List<InputMethodSubtype> subtypes = imm.getEnabledInputMethodSubtypeList( 333 imi, true /* allowsImplicitlySelectedSubtypes */); 334 if (subtypes.isEmpty()) { 335 // Here we use null to indicate that this IME has no subtype. 336 final InputMethodSubtype nullSubtype = null; 337 final KeyboardLayout layout = im.getKeyboardLayoutForInputDevice( 338 deviceInfo.mDeviceIdentifier, imi, nullSubtype); 339 keyboardInfoList.add(new Keyboards.KeyboardInfo(imi, nullSubtype, layout)); 340 continue; 341 } 342 343 // If the IME supports subtypes, we pick up "keyboard" subtypes only. 344 final int N = subtypes.size(); 345 for (int i = 0; i < N; ++i) { 346 final InputMethodSubtype subtype = subtypes.get(i); 347 if (!IM_SUBTYPE_MODE_KEYBOARD.equalsIgnoreCase(subtype.getMode())) { 348 continue; 349 } 350 final KeyboardLayout layout = im.getKeyboardLayoutForInputDevice( 351 deviceInfo.mDeviceIdentifier, imi, subtype); 352 keyboardInfoList.add(new Keyboards.KeyboardInfo(imi, subtype, layout)); 353 } 354 } 355 } 356 return new Keyboards(deviceInfo, keyboardInfoList); 357 } 358 359 @Override 360 public List<Keyboards> loadInBackground() { 361 List<Keyboards> keyboardsList = new ArrayList<>(mHardKeyboards.size()); 362 for (HardKeyboardDeviceInfo deviceInfo : mHardKeyboards) { 363 keyboardsList.add(loadInBackground(deviceInfo)); 364 } 365 return keyboardsList; 366 } 367 368 @Override 369 protected void onStartLoading() { 370 super.onStartLoading(); 371 forceLoad(); 372 } 373 374 @Override 375 protected void onStopLoading() { 376 super.onStopLoading(); 377 cancelLoad(); 378 } 379 } 380 381 public static final class HardKeyboardDeviceInfo { 382 @NonNull 383 public final String mDeviceName; 384 @NonNull 385 public final InputDeviceIdentifier mDeviceIdentifier; 386 387 public HardKeyboardDeviceInfo( 388 @Nullable final String deviceName, 389 @NonNull final InputDeviceIdentifier deviceIdentifier) { 390 mDeviceName = deviceName != null ? deviceName : ""; 391 mDeviceIdentifier = deviceIdentifier; 392 } 393 394 @Override 395 public boolean equals(Object o) { 396 if (o == this) return true; 397 if (o == null) return false; 398 399 if (!(o instanceof HardKeyboardDeviceInfo)) return false; 400 401 final HardKeyboardDeviceInfo that = (HardKeyboardDeviceInfo) o; 402 if (!TextUtils.equals(mDeviceName, that.mDeviceName)) { 403 return false; 404 } 405 if (mDeviceIdentifier.getVendorId() != that.mDeviceIdentifier.getVendorId()) { 406 return false; 407 } 408 if (mDeviceIdentifier.getProductId() != that.mDeviceIdentifier.getProductId()) { 409 return false; 410 } 411 if (!TextUtils.equals(mDeviceIdentifier.getDescriptor(), 412 that.mDeviceIdentifier.getDescriptor())) { 413 return false; 414 } 415 416 return true; 417 } 418 } 419 420 public static final class Keyboards implements Comparable<Keyboards> { 421 @NonNull 422 public final HardKeyboardDeviceInfo mDeviceInfo; 423 @NonNull 424 public final ArrayList<KeyboardInfo> mKeyboardInfoList; 425 @NonNull 426 public final Collator mCollator = Collator.getInstance(); 427 428 public Keyboards( 429 @NonNull final HardKeyboardDeviceInfo deviceInfo, 430 @NonNull final ArrayList<KeyboardInfo> keyboardInfoList) { 431 mDeviceInfo = deviceInfo; 432 mKeyboardInfoList = keyboardInfoList; 433 } 434 435 @Override 436 public int compareTo(@NonNull Keyboards another) { 437 return mCollator.compare(mDeviceInfo.mDeviceName, another.mDeviceInfo.mDeviceName); 438 } 439 440 public static final class KeyboardInfo { 441 @NonNull 442 public final InputMethodInfo mImi; 443 @Nullable 444 public final InputMethodSubtype mImSubtype; 445 @NonNull 446 public final KeyboardLayout mLayout; 447 448 public KeyboardInfo( 449 @NonNull final InputMethodInfo imi, 450 @Nullable final InputMethodSubtype imSubtype, 451 @NonNull final KeyboardLayout layout) { 452 mImi = imi; 453 mImSubtype = imSubtype; 454 mLayout = layout; 455 } 456 } 457 } 458 459 static final class KeyboardInfoPreference extends Preference { 460 461 @NonNull 462 private final CharSequence mImeName; 463 @Nullable 464 private final CharSequence mImSubtypeName; 465 @NonNull 466 private final Collator collator = Collator.getInstance(); 467 468 private KeyboardInfoPreference( 469 @NonNull Context context, @NonNull Keyboards.KeyboardInfo info) { 470 super(context); 471 mImeName = info.mImi.loadLabel(context.getPackageManager()); 472 mImSubtypeName = getImSubtypeName(context, info.mImi, info.mImSubtype); 473 setTitle(formatDisplayName(context, mImeName, mImSubtypeName)); 474 if (info.mLayout != null) { 475 setSummary(info.mLayout.getLabel()); 476 } 477 } 478 479 @NonNull 480 static CharSequence getDisplayName( 481 @NonNull Context context, @NonNull InputMethodInfo imi, 482 @Nullable InputMethodSubtype imSubtype) { 483 final CharSequence imeName = imi.loadLabel(context.getPackageManager()); 484 final CharSequence imSubtypeName = getImSubtypeName(context, imi, imSubtype); 485 return formatDisplayName(context, imeName, imSubtypeName); 486 } 487 488 private static CharSequence formatDisplayName( 489 @NonNull Context context, 490 @NonNull CharSequence imeName, @Nullable CharSequence imSubtypeName) { 491 if (imSubtypeName == null) { 492 return imeName; 493 } 494 return String.format( 495 context.getString(R.string.physical_device_title), imeName, imSubtypeName); 496 } 497 498 @Nullable 499 private static CharSequence getImSubtypeName( 500 @NonNull Context context, @NonNull InputMethodInfo imi, 501 @Nullable InputMethodSubtype imSubtype) { 502 if (imSubtype != null) { 503 return InputMethodAndSubtypeUtil.getSubtypeLocaleNameAsSentence( 504 imSubtype, context, imi); 505 } 506 return null; 507 } 508 509 @Override 510 public int compareTo(@NonNull Preference object) { 511 if (!(object instanceof KeyboardInfoPreference)) { 512 return super.compareTo(object); 513 } 514 KeyboardInfoPreference another = (KeyboardInfoPreference) object; 515 int result = compare(mImeName, another.mImeName); 516 if (result == 0) { 517 result = compare(mImSubtypeName, another.mImSubtypeName); 518 } 519 return result; 520 } 521 522 private int compare(@Nullable CharSequence lhs, @Nullable CharSequence rhs) { 523 if (!TextUtils.isEmpty(lhs) && !TextUtils.isEmpty(rhs)) { 524 return collator.compare(lhs.toString(), rhs.toString()); 525 } else if (TextUtils.isEmpty(lhs) && TextUtils.isEmpty(rhs)) { 526 return 0; 527 } else if (!TextUtils.isEmpty(lhs)) { 528 return -1; 529 } else { 530 return 1; 531 } 532 } 533 } 534 535 public static List<InputDevice> getPhysicalFullKeyboards() { 536 List<InputDevice> keyboards = null; 537 for (final int deviceId : InputDevice.getDeviceIds()) { 538 final InputDevice device = InputDevice.getDevice(deviceId); 539 if (device != null && !device.isVirtual() && device.isFullKeyboard()) { 540 if (keyboards == null) keyboards = new ArrayList<>(); 541 keyboards.add(device); 542 } 543 } 544 return (keyboards == null) ? Collections.emptyList() : keyboards; 545 } 546 547 public static final Indexable.SearchIndexProvider SEARCH_INDEX_DATA_PROVIDER = 548 new BaseSearchIndexProvider() { 549 @Override 550 public List<SearchIndexableRaw> getRawDataToIndex(Context context, boolean enabled) { 551 final InputManager inputManager = (InputManager) context.getSystemService( 552 Context.INPUT_SERVICE); 553 final String screenTitle = context.getString(R.string.physical_keyboard_title); 554 final List<SearchIndexableRaw> indexes = new ArrayList<>(); 555 for (final InputDevice device : getPhysicalFullKeyboards()) { 556 final String keyboardLayoutDescriptor = inputManager 557 .getCurrentKeyboardLayoutForInputDevice(device.getIdentifier()); 558 final KeyboardLayout keyboardLayout = (keyboardLayoutDescriptor != null) 559 ? inputManager.getKeyboardLayout(keyboardLayoutDescriptor) : null; 560 final String summary = (keyboardLayout != null) 561 ? keyboardLayout.toString() 562 : context.getString(R.string.keyboard_layout_default_label); 563 final SearchIndexableRaw index = new SearchIndexableRaw(context); 564 index.key = device.getName(); 565 index.title = device.getName(); 566 index.summaryOn = summary; 567 index.summaryOff = summary; 568 index.screenTitle = screenTitle; 569 indexes.add(index); 570 } 571 return indexes; 572 } 573 }; 574 } 575