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