1 /* 2 * Copyright (C) 2022 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.server.input; 18 19 import static android.hardware.input.KeyboardLayoutSelectionResult.FAILED; 20 import static android.hardware.input.KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_USER; 21 import static android.hardware.input.KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_DEVICE; 22 import static android.hardware.input.KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD; 23 import static android.hardware.input.KeyboardLayoutSelectionResult.LAYOUT_SELECTION_CRITERIA_DEFAULT; 24 25 import android.annotation.AnyThread; 26 import android.annotation.MainThread; 27 import android.annotation.NonNull; 28 import android.annotation.Nullable; 29 import android.annotation.SuppressLint; 30 import android.annotation.UserIdInt; 31 import android.app.Notification; 32 import android.app.NotificationManager; 33 import android.app.PendingIntent; 34 import android.app.settings.SettingsEnums; 35 import android.content.BroadcastReceiver; 36 import android.content.ComponentName; 37 import android.content.Context; 38 import android.content.Intent; 39 import android.content.IntentFilter; 40 import android.content.pm.ActivityInfo; 41 import android.content.pm.ApplicationInfo; 42 import android.content.pm.PackageManager; 43 import android.content.pm.ResolveInfo; 44 import android.content.res.Resources; 45 import android.content.res.TypedArray; 46 import android.content.res.XmlResourceParser; 47 import android.hardware.input.InputDeviceIdentifier; 48 import android.hardware.input.InputManager; 49 import android.hardware.input.KeyboardLayout; 50 import android.hardware.input.KeyboardLayoutSelectionResult; 51 import android.icu.lang.UScript; 52 import android.icu.util.ULocale; 53 import android.os.Bundle; 54 import android.os.Handler; 55 import android.os.LocaleList; 56 import android.os.Looper; 57 import android.os.Message; 58 import android.os.UserHandle; 59 import android.os.UserManager; 60 import android.provider.Settings; 61 import android.text.TextUtils; 62 import android.util.ArrayMap; 63 import android.util.Log; 64 import android.util.Slog; 65 import android.util.SparseArray; 66 import android.view.InputDevice; 67 import android.view.KeyCharacterMap; 68 import android.view.inputmethod.InputMethodInfo; 69 import android.view.inputmethod.InputMethodManager; 70 import android.view.inputmethod.InputMethodSubtype; 71 72 import com.android.internal.R; 73 import com.android.internal.annotations.GuardedBy; 74 import com.android.internal.annotations.VisibleForTesting; 75 import com.android.internal.inputmethod.InputMethodSubtypeHandle; 76 import com.android.internal.messages.nano.SystemMessageProto; 77 import com.android.internal.notification.SystemNotificationChannels; 78 import com.android.internal.util.XmlUtils; 79 import com.android.server.LocalServices; 80 import com.android.server.companion.virtual.VirtualDeviceManagerInternal; 81 import com.android.server.input.KeyboardMetricsCollector.KeyboardConfigurationEvent; 82 import com.android.server.inputmethod.InputMethodManagerInternal; 83 84 import libcore.io.Streams; 85 86 import java.io.IOException; 87 import java.io.InputStreamReader; 88 import java.util.ArrayList; 89 import java.util.Arrays; 90 import java.util.Collections; 91 import java.util.HashSet; 92 import java.util.List; 93 import java.util.Locale; 94 import java.util.Map; 95 import java.util.Objects; 96 import java.util.Set; 97 98 /** 99 * A component of {@link InputManagerService} responsible for managing Physical Keyboard layouts. 100 * 101 * @hide 102 */ 103 class KeyboardLayoutManager implements InputManager.InputDeviceListener { 104 105 private static final String TAG = "KeyboardLayoutManager"; 106 107 // To enable these logs, run: 'adb shell setprop log.tag.KeyboardLayoutManager DEBUG' 108 // (requires restart) 109 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 110 111 private static final int MSG_UPDATE_EXISTING_DEVICES = 1; 112 private static final int MSG_RELOAD_KEYBOARD_LAYOUTS = 2; 113 private static final int MSG_UPDATE_KEYBOARD_LAYOUTS = 3; 114 115 private final Context mContext; 116 private final NativeInputManagerService mNative; 117 // The PersistentDataStore should be locked before use. 118 @GuardedBy("mDataStore") 119 private final PersistentDataStore mDataStore; 120 private final Handler mHandler; 121 122 // Connected keyboards with associated keyboard layouts (either auto-detected or manually 123 // selected layout). 124 private final SparseArray<KeyboardConfiguration> mConfiguredKeyboards = new SparseArray<>(); 125 126 // This cache stores "best-matched" layouts so that we don't need to run the matching 127 // algorithm repeatedly. 128 @GuardedBy("mKeyboardLayoutCache") 129 private final Map<String, KeyboardLayoutSelectionResult> mKeyboardLayoutCache = 130 new ArrayMap<>(); 131 132 private HashSet<String> mAvailableLayouts = new HashSet<>(); 133 private final Object mImeInfoLock = new Object(); 134 @Nullable 135 @GuardedBy("mImeInfoLock") 136 private ImeInfo mCurrentImeInfo; 137 KeyboardLayoutManager(Context context, NativeInputManagerService nativeService, PersistentDataStore dataStore, Looper looper)138 KeyboardLayoutManager(Context context, NativeInputManagerService nativeService, 139 PersistentDataStore dataStore, Looper looper) { 140 mContext = context; 141 mNative = nativeService; 142 mDataStore = dataStore; 143 mHandler = new Handler(looper, this::handleMessage, true /* async */); 144 } 145 systemRunning()146 public void systemRunning() { 147 // Listen to new Package installations to fetch new Keyboard layouts 148 IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); 149 filter.addAction(Intent.ACTION_PACKAGE_REMOVED); 150 filter.addAction(Intent.ACTION_PACKAGE_CHANGED); 151 filter.addAction(Intent.ACTION_PACKAGE_REPLACED); 152 filter.addDataScheme("package"); 153 mContext.registerReceiver(new BroadcastReceiver() { 154 @Override 155 public void onReceive(Context context, Intent intent) { 156 updateKeyboardLayouts(); 157 } 158 }, filter, null, mHandler); 159 160 mHandler.sendEmptyMessage(MSG_UPDATE_KEYBOARD_LAYOUTS); 161 162 // Listen to new InputDevice changes 163 InputManager inputManager = Objects.requireNonNull( 164 mContext.getSystemService(InputManager.class)); 165 inputManager.registerInputDeviceListener(this, mHandler); 166 167 Message msg = Message.obtain(mHandler, MSG_UPDATE_EXISTING_DEVICES, 168 inputManager.getInputDeviceIds()); 169 mHandler.sendMessage(msg); 170 } 171 172 @Override 173 @MainThread onInputDeviceAdded(int deviceId)174 public void onInputDeviceAdded(int deviceId) { 175 // Logging keyboard configuration data to statsd whenever input device is added. Currently 176 // only logging for New Settings UI where we are using IME to decide the layout information. 177 onInputDeviceChangedInternal(deviceId, true /* shouldLogConfiguration */); 178 } 179 180 @Override 181 @MainThread onInputDeviceRemoved(int deviceId)182 public void onInputDeviceRemoved(int deviceId) { 183 mConfiguredKeyboards.remove(deviceId); 184 maybeUpdateNotification(); 185 } 186 187 @Override 188 @MainThread onInputDeviceChanged(int deviceId)189 public void onInputDeviceChanged(int deviceId) { 190 onInputDeviceChangedInternal(deviceId, false /* shouldLogConfiguration */); 191 } 192 onInputDeviceChangedInternal(int deviceId, boolean shouldLogConfiguration)193 private void onInputDeviceChangedInternal(int deviceId, boolean shouldLogConfiguration) { 194 final InputDevice inputDevice = getInputDevice(deviceId); 195 if (inputDevice == null || inputDevice.isVirtual() || !inputDevice.isFullKeyboard()) { 196 return; 197 } 198 final KeyboardIdentifier keyboardIdentifier = new KeyboardIdentifier(inputDevice); 199 KeyboardConfiguration config = mConfiguredKeyboards.get(deviceId); 200 if (config == null) { 201 config = new KeyboardConfiguration(deviceId); 202 mConfiguredKeyboards.put(deviceId, config); 203 } 204 205 boolean needToShowNotification = false; 206 Set<String> selectedLayouts = new HashSet<>(); 207 List<ImeInfo> imeInfoList = getImeInfoListForLayoutMapping(); 208 List<KeyboardLayoutSelectionResult> resultList = new ArrayList<>(); 209 boolean hasMissingLayout = false; 210 for (ImeInfo imeInfo : imeInfoList) { 211 // Check if the layout has been previously configured 212 KeyboardLayoutSelectionResult result = getKeyboardLayoutForInputDeviceInternal( 213 keyboardIdentifier, imeInfo); 214 if (result.getLayoutDescriptor() != null) { 215 selectedLayouts.add(result.getLayoutDescriptor()); 216 } else { 217 hasMissingLayout = true; 218 } 219 resultList.add(result); 220 } 221 222 if (DEBUG) { 223 Slog.d(TAG, 224 "Layouts selected for input device: " + keyboardIdentifier 225 + " -> selectedLayouts: " + selectedLayouts); 226 } 227 228 // If even one layout not configured properly, we need to ask user to configure 229 // the keyboard properly from the Settings. 230 if (hasMissingLayout) { 231 selectedLayouts.clear(); 232 } 233 234 config.setConfiguredLayouts(selectedLayouts); 235 236 synchronized (mDataStore) { 237 try { 238 final String key = keyboardIdentifier.toString(); 239 if (mDataStore.setSelectedKeyboardLayouts(key, selectedLayouts)) { 240 // Need to show the notification only if layout selection changed 241 // from the previous configuration 242 needToShowNotification = true; 243 } 244 245 if (shouldLogConfiguration) { 246 logKeyboardConfigurationEvent(inputDevice, imeInfoList, resultList, 247 !mDataStore.hasInputDeviceEntry(key)); 248 } 249 } finally { 250 mDataStore.saveIfNeeded(); 251 } 252 } 253 if (needToShowNotification) { 254 maybeUpdateNotification(); 255 } 256 } 257 isCompatibleLocale(Locale systemLocale, Locale keyboardLocale)258 private static boolean isCompatibleLocale(Locale systemLocale, Locale keyboardLocale) { 259 // Different languages are never compatible 260 if (!systemLocale.getLanguage().equals(keyboardLocale.getLanguage())) { 261 return false; 262 } 263 // If both the system and the keyboard layout have a country specifier, they must be equal. 264 return TextUtils.isEmpty(systemLocale.getCountry()) 265 || TextUtils.isEmpty(keyboardLocale.getCountry()) 266 || systemLocale.getCountry().equals(keyboardLocale.getCountry()); 267 } 268 269 @MainThread updateKeyboardLayouts()270 private void updateKeyboardLayouts() { 271 // Scan all input devices state for keyboard layouts that have been uninstalled. 272 final HashSet<String> availableKeyboardLayouts = new HashSet<>(); 273 visitAllKeyboardLayouts((resources, keyboardLayoutResId, layout) -> 274 availableKeyboardLayouts.add(layout.getDescriptor())); 275 276 // If available layouts don't change, there is no need to reload layouts. 277 if (mAvailableLayouts.equals(availableKeyboardLayouts)) { 278 return; 279 } 280 mAvailableLayouts = availableKeyboardLayouts; 281 282 synchronized (mDataStore) { 283 try { 284 mDataStore.removeUninstalledKeyboardLayouts(availableKeyboardLayouts); 285 } finally { 286 mDataStore.saveIfNeeded(); 287 } 288 } 289 290 synchronized (mKeyboardLayoutCache) { 291 // Invalidate the cache: With packages being installed/removed, existing cache of 292 // auto-selected layout might not be the best layouts anymore. 293 mKeyboardLayoutCache.clear(); 294 } 295 296 // Reload keyboard layouts. 297 reloadKeyboardLayouts(); 298 } 299 300 @AnyThread getKeyboardLayouts()301 public KeyboardLayout[] getKeyboardLayouts() { 302 final ArrayList<KeyboardLayout> list = new ArrayList<>(); 303 visitAllKeyboardLayouts((resources, keyboardLayoutResId, layout) -> list.add(layout)); 304 return list.toArray(new KeyboardLayout[0]); 305 } 306 307 @AnyThread 308 @Nullable getKeyboardLayout(@onNull String keyboardLayoutDescriptor)309 public KeyboardLayout getKeyboardLayout(@NonNull String keyboardLayoutDescriptor) { 310 Objects.requireNonNull(keyboardLayoutDescriptor, 311 "keyboardLayoutDescriptor must not be null"); 312 313 final KeyboardLayout[] result = new KeyboardLayout[1]; 314 visitKeyboardLayout(keyboardLayoutDescriptor, 315 (resources, keyboardLayoutResId, layout) -> result[0] = layout); 316 if (result[0] == null) { 317 Slog.w(TAG, "Could not get keyboard layout with descriptor '" 318 + keyboardLayoutDescriptor + "'."); 319 } 320 return result[0]; 321 } 322 323 @AnyThread getKeyCharacterMap(@onNull String layoutDescriptor)324 public KeyCharacterMap getKeyCharacterMap(@NonNull String layoutDescriptor) { 325 final String[] overlay = new String[1]; 326 visitKeyboardLayout(layoutDescriptor, 327 (resources, keyboardLayoutResId, layout) -> { 328 try (InputStreamReader stream = new InputStreamReader( 329 resources.openRawResource(keyboardLayoutResId))) { 330 overlay[0] = Streams.readFully(stream); 331 } catch (IOException | Resources.NotFoundException ignored) { 332 } 333 }); 334 if (TextUtils.isEmpty(overlay[0])) { 335 return KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); 336 } 337 return KeyCharacterMap.load(layoutDescriptor, overlay[0]); 338 } 339 visitAllKeyboardLayouts(KeyboardLayoutVisitor visitor)340 private void visitAllKeyboardLayouts(KeyboardLayoutVisitor visitor) { 341 final PackageManager pm = mContext.getPackageManager(); 342 Intent intent = new Intent(InputManager.ACTION_QUERY_KEYBOARD_LAYOUTS); 343 for (ResolveInfo resolveInfo : pm.queryBroadcastReceiversAsUser(intent, 344 PackageManager.GET_META_DATA | PackageManager.MATCH_DIRECT_BOOT_AWARE 345 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, UserHandle.USER_SYSTEM)) { 346 if (resolveInfo == null || resolveInfo.activityInfo == null) { 347 continue; 348 } 349 final ActivityInfo activityInfo = resolveInfo.activityInfo; 350 final int priority = resolveInfo.priority; 351 visitKeyboardLayoutsInPackage(pm, activityInfo, null, priority, visitor); 352 } 353 } 354 visitKeyboardLayout(@onNull String keyboardLayoutDescriptor, KeyboardLayoutVisitor visitor)355 private void visitKeyboardLayout(@NonNull String keyboardLayoutDescriptor, 356 KeyboardLayoutVisitor visitor) { 357 KeyboardLayoutDescriptor d = KeyboardLayoutDescriptor.parse(keyboardLayoutDescriptor); 358 if (d != null) { 359 final PackageManager pm = mContext.getPackageManager(); 360 try { 361 ActivityInfo receiver = pm.getReceiverInfo( 362 new ComponentName(d.packageName, d.receiverName), 363 PackageManager.GET_META_DATA 364 | PackageManager.MATCH_DIRECT_BOOT_AWARE 365 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE); 366 visitKeyboardLayoutsInPackage(pm, receiver, d.keyboardLayoutName, 0, visitor); 367 } catch (PackageManager.NameNotFoundException ignored) { 368 } 369 } 370 } 371 visitKeyboardLayoutsInPackage(PackageManager pm, @NonNull ActivityInfo receiver, @Nullable String keyboardName, int requestedPriority, KeyboardLayoutVisitor visitor)372 private void visitKeyboardLayoutsInPackage(PackageManager pm, @NonNull ActivityInfo receiver, 373 @Nullable String keyboardName, int requestedPriority, KeyboardLayoutVisitor visitor) { 374 Bundle metaData = receiver.metaData; 375 if (metaData == null) { 376 return; 377 } 378 379 int configResId = metaData.getInt(InputManager.META_DATA_KEYBOARD_LAYOUTS); 380 if (configResId == 0) { 381 Slog.w(TAG, "Missing meta-data '" + InputManager.META_DATA_KEYBOARD_LAYOUTS 382 + "' on receiver " + receiver.packageName + "/" + receiver.name); 383 return; 384 } 385 386 CharSequence receiverLabel = receiver.loadLabel(pm); 387 String collection = receiverLabel != null ? receiverLabel.toString() : ""; 388 int priority; 389 if ((receiver.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) { 390 priority = requestedPriority; 391 } else { 392 priority = 0; 393 } 394 395 try { 396 Resources resources = pm.getResourcesForApplication(receiver.applicationInfo); 397 try (XmlResourceParser parser = resources.getXml(configResId)) { 398 XmlUtils.beginDocument(parser, "keyboard-layouts"); 399 400 while (true) { 401 XmlUtils.nextElement(parser); 402 String element = parser.getName(); 403 if (element == null) { 404 break; 405 } 406 if (element.equals("keyboard-layout")) { 407 TypedArray a = resources.obtainAttributes( 408 parser, R.styleable.KeyboardLayout); 409 try { 410 String name = a.getString( 411 R.styleable.KeyboardLayout_name); 412 String label = a.getString( 413 R.styleable.KeyboardLayout_label); 414 int keyboardLayoutResId = a.getResourceId( 415 R.styleable.KeyboardLayout_keyboardLayout, 416 0); 417 String languageTags = a.getString( 418 R.styleable.KeyboardLayout_keyboardLocale); 419 LocaleList locales = getLocalesFromLanguageTags(languageTags); 420 int layoutType = a.getInt(R.styleable.KeyboardLayout_keyboardLayoutType, 421 0); 422 int vid = a.getInt( 423 R.styleable.KeyboardLayout_vendorId, -1); 424 int pid = a.getInt( 425 R.styleable.KeyboardLayout_productId, -1); 426 427 if (name == null || label == null || keyboardLayoutResId == 0) { 428 Slog.w(TAG, "Missing required 'name', 'label' or 'keyboardLayout' " 429 + "attributes in keyboard layout " 430 + "resource from receiver " 431 + receiver.packageName + "/" + receiver.name); 432 } else { 433 String descriptor = KeyboardLayoutDescriptor.format( 434 receiver.packageName, receiver.name, name); 435 if (keyboardName == null || name.equals(keyboardName)) { 436 KeyboardLayout layout = new KeyboardLayout( 437 descriptor, label, collection, priority, 438 locales, layoutType, vid, pid); 439 visitor.visitKeyboardLayout( 440 resources, keyboardLayoutResId, layout); 441 } 442 } 443 } finally { 444 a.recycle(); 445 } 446 } else { 447 Slog.w(TAG, "Skipping unrecognized element '" + element 448 + "' in keyboard layout resource from receiver " 449 + receiver.packageName + "/" + receiver.name); 450 } 451 } 452 } 453 } catch (Exception ex) { 454 Slog.w(TAG, "Could not parse keyboard layout resource from receiver " 455 + receiver.packageName + "/" + receiver.name, ex); 456 } 457 } 458 459 @NonNull getLocalesFromLanguageTags(String languageTags)460 private static LocaleList getLocalesFromLanguageTags(String languageTags) { 461 if (TextUtils.isEmpty(languageTags)) { 462 return LocaleList.getEmptyLocaleList(); 463 } 464 return LocaleList.forLanguageTags(languageTags.replace('|', ',')); 465 } 466 467 @Nullable 468 @AnyThread getKeyboardLayoutOverlay(InputDeviceIdentifier identifier, String languageTag, String layoutType)469 public String[] getKeyboardLayoutOverlay(InputDeviceIdentifier identifier, String languageTag, 470 String layoutType) { 471 String keyboardLayoutDescriptor; 472 synchronized (mImeInfoLock) { 473 KeyboardLayoutSelectionResult result = getKeyboardLayoutForInputDeviceInternal( 474 new KeyboardIdentifier(identifier, languageTag, layoutType), 475 mCurrentImeInfo); 476 keyboardLayoutDescriptor = result.getLayoutDescriptor(); 477 } 478 if (keyboardLayoutDescriptor == null) { 479 return null; 480 } 481 482 final String[] result = new String[2]; 483 visitKeyboardLayout(keyboardLayoutDescriptor, 484 (resources, keyboardLayoutResId, layout) -> { 485 try (InputStreamReader stream = new InputStreamReader( 486 resources.openRawResource(keyboardLayoutResId))) { 487 result[0] = layout.getDescriptor(); 488 result[1] = Streams.readFully(stream); 489 } catch (IOException | Resources.NotFoundException ignored) { 490 } 491 }); 492 if (result[0] == null) { 493 Slog.w(TAG, "Could not get keyboard layout with descriptor '" 494 + keyboardLayoutDescriptor + "'."); 495 return null; 496 } 497 return result; 498 } 499 500 @AnyThread 501 @NonNull getKeyboardLayoutForInputDevice( InputDeviceIdentifier identifier, @UserIdInt int userId, @NonNull InputMethodInfo imeInfo, @Nullable InputMethodSubtype imeSubtype)502 public KeyboardLayoutSelectionResult getKeyboardLayoutForInputDevice( 503 InputDeviceIdentifier identifier, @UserIdInt int userId, 504 @NonNull InputMethodInfo imeInfo, @Nullable InputMethodSubtype imeSubtype) { 505 InputDevice inputDevice = getInputDevice(identifier); 506 if (inputDevice == null || inputDevice.isVirtual() || !inputDevice.isFullKeyboard()) { 507 return FAILED; 508 } 509 KeyboardIdentifier keyboardIdentifier = new KeyboardIdentifier(inputDevice); 510 KeyboardLayoutSelectionResult result = getKeyboardLayoutForInputDeviceInternal( 511 keyboardIdentifier, new ImeInfo(userId, imeInfo, imeSubtype)); 512 if (DEBUG) { 513 Slog.d(TAG, "getKeyboardLayoutForInputDevice() " + identifier.toString() + ", userId : " 514 + userId + ", subtype = " + imeSubtype + " -> " + result); 515 } 516 return result; 517 } 518 519 @AnyThread setKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier, @UserIdInt int userId, @NonNull InputMethodInfo imeInfo, @Nullable InputMethodSubtype imeSubtype, String keyboardLayoutDescriptor)520 public void setKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier, 521 @UserIdInt int userId, @NonNull InputMethodInfo imeInfo, 522 @Nullable InputMethodSubtype imeSubtype, 523 String keyboardLayoutDescriptor) { 524 Objects.requireNonNull(keyboardLayoutDescriptor, 525 "keyboardLayoutDescriptor must not be null"); 526 InputDevice inputDevice = getInputDevice(identifier); 527 if (inputDevice == null || inputDevice.isVirtual() || !inputDevice.isFullKeyboard()) { 528 return; 529 } 530 KeyboardIdentifier keyboardIdentifier = new KeyboardIdentifier(inputDevice); 531 String layoutKey = new LayoutKey(keyboardIdentifier, 532 new ImeInfo(userId, imeInfo, imeSubtype)).toString(); 533 synchronized (mDataStore) { 534 try { 535 if (mDataStore.setKeyboardLayout(keyboardIdentifier.toString(), layoutKey, 536 keyboardLayoutDescriptor)) { 537 if (DEBUG) { 538 Slog.d(TAG, "setKeyboardLayoutForInputDevice() " + identifier 539 + " key: " + layoutKey 540 + " keyboardLayoutDescriptor: " + keyboardLayoutDescriptor); 541 } 542 mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS); 543 } 544 } finally { 545 mDataStore.saveIfNeeded(); 546 } 547 } 548 } 549 550 @AnyThread getKeyboardLayoutListForInputDevice(InputDeviceIdentifier identifier, @UserIdInt int userId, @NonNull InputMethodInfo imeInfo, @Nullable InputMethodSubtype imeSubtype)551 public KeyboardLayout[] getKeyboardLayoutListForInputDevice(InputDeviceIdentifier identifier, 552 @UserIdInt int userId, @NonNull InputMethodInfo imeInfo, 553 @Nullable InputMethodSubtype imeSubtype) { 554 InputDevice inputDevice = getInputDevice(identifier); 555 if (inputDevice == null || inputDevice.isVirtual() || !inputDevice.isFullKeyboard()) { 556 return new KeyboardLayout[0]; 557 } 558 return getKeyboardLayoutListForInputDeviceInternal(new KeyboardIdentifier(inputDevice), 559 new ImeInfo(userId, imeInfo, imeSubtype)); 560 } 561 getKeyboardLayoutListForInputDeviceInternal( KeyboardIdentifier keyboardIdentifier, @Nullable ImeInfo imeInfo)562 private KeyboardLayout[] getKeyboardLayoutListForInputDeviceInternal( 563 KeyboardIdentifier keyboardIdentifier, @Nullable ImeInfo imeInfo) { 564 String layoutKey = new LayoutKey(keyboardIdentifier, imeInfo).toString(); 565 566 // Fetch user selected layout and always include it in layout list. 567 String userSelectedLayout; 568 synchronized (mDataStore) { 569 userSelectedLayout = mDataStore.getKeyboardLayout(keyboardIdentifier.toString(), 570 layoutKey); 571 } 572 573 final ArrayList<KeyboardLayout> potentialLayouts = new ArrayList<>(); 574 String imeLanguageTag; 575 if (imeInfo == null || imeInfo.mImeSubtype == null) { 576 imeLanguageTag = ""; 577 } else { 578 ULocale imeLocale = imeInfo.mImeSubtype.getPhysicalKeyboardHintLanguageTag(); 579 imeLanguageTag = imeLocale != null ? imeLocale.toLanguageTag() 580 : imeInfo.mImeSubtype.getCanonicalizedLanguageTag(); 581 } 582 583 visitAllKeyboardLayouts(new KeyboardLayoutVisitor() { 584 boolean mDeviceSpecificLayoutAvailable; 585 586 @Override 587 public void visitKeyboardLayout(Resources resources, 588 int keyboardLayoutResId, KeyboardLayout layout) { 589 // Next find any potential layouts that aren't yet enabled for the device. For 590 // devices that have special layouts we assume there's a reason that the generic 591 // layouts don't work for them, so we don't want to return them since it's likely 592 // to result in a poor user experience. 593 if (layout.getVendorId() == keyboardIdentifier.mIdentifier.getVendorId() 594 && layout.getProductId() == keyboardIdentifier.mIdentifier.getProductId()) { 595 if (!mDeviceSpecificLayoutAvailable) { 596 mDeviceSpecificLayoutAvailable = true; 597 potentialLayouts.clear(); 598 } 599 potentialLayouts.add(layout); 600 } else if (layout.getVendorId() == -1 && layout.getProductId() == -1 601 && !mDeviceSpecificLayoutAvailable && isLayoutCompatibleWithLanguageTag( 602 layout, imeLanguageTag)) { 603 potentialLayouts.add(layout); 604 } else if (layout.getDescriptor().equals(userSelectedLayout)) { 605 potentialLayouts.add(layout); 606 } 607 } 608 }); 609 // Sort the Keyboard layouts. This is done first by priority then by label. So, system 610 // layouts will come above 3rd party layouts. 611 Collections.sort(potentialLayouts); 612 return potentialLayouts.toArray(new KeyboardLayout[0]); 613 } 614 615 @AnyThread onInputMethodSubtypeChanged(@serIdInt int userId, @Nullable InputMethodSubtypeHandle subtypeHandle, @Nullable InputMethodSubtype subtype)616 public void onInputMethodSubtypeChanged(@UserIdInt int userId, 617 @Nullable InputMethodSubtypeHandle subtypeHandle, 618 @Nullable InputMethodSubtype subtype) { 619 if (subtypeHandle == null) { 620 if (DEBUG) { 621 Slog.d(TAG, "No InputMethod is running, ignoring change"); 622 } 623 return; 624 } 625 synchronized (mImeInfoLock) { 626 if (mCurrentImeInfo == null || !subtypeHandle.equals(mCurrentImeInfo.mImeSubtypeHandle) 627 || mCurrentImeInfo.mUserId != userId) { 628 mCurrentImeInfo = new ImeInfo(userId, subtypeHandle, subtype); 629 mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS); 630 if (DEBUG) { 631 Slog.d(TAG, "InputMethodSubtype changed: userId=" + userId 632 + " subtypeHandle=" + subtypeHandle); 633 } 634 } 635 } 636 } 637 638 @Nullable getKeyboardLayoutForInputDeviceInternal( KeyboardIdentifier keyboardIdentifier, @Nullable ImeInfo imeInfo)639 private KeyboardLayoutSelectionResult getKeyboardLayoutForInputDeviceInternal( 640 KeyboardIdentifier keyboardIdentifier, @Nullable ImeInfo imeInfo) { 641 String layoutKey = new LayoutKey(keyboardIdentifier, imeInfo).toString(); 642 synchronized (mDataStore) { 643 String layout = mDataStore.getKeyboardLayout(keyboardIdentifier.toString(), layoutKey); 644 if (layout != null) { 645 return new KeyboardLayoutSelectionResult(layout, LAYOUT_SELECTION_CRITERIA_USER); 646 } 647 } 648 649 synchronized (mKeyboardLayoutCache) { 650 // Check Auto-selected layout cache to see if layout had been previously selected 651 if (mKeyboardLayoutCache.containsKey(layoutKey)) { 652 return mKeyboardLayoutCache.get(layoutKey); 653 } else { 654 // NOTE: This list is already filtered based on IME Script code 655 KeyboardLayout[] layoutList = getKeyboardLayoutListForInputDeviceInternal( 656 keyboardIdentifier, imeInfo); 657 // Call auto-matching algorithm to find the best matching layout 658 KeyboardLayoutSelectionResult result = 659 getDefaultKeyboardLayoutBasedOnImeInfo(keyboardIdentifier, imeInfo, 660 layoutList); 661 mKeyboardLayoutCache.put(layoutKey, result); 662 return result; 663 } 664 } 665 } 666 667 /** 668 * <ol> 669 * <li> Layout selection Algorithm: 670 * <ul> 671 * <li> Choose product specific layout(KCM file with matching vendor ID and product 672 * ID) </li> 673 * <li> If none, then find layout based on PK layout info (based on country code 674 * provided by the HID descriptor of the keyboard) </li> 675 * <li> If none, then find layout based on IME layout info associated with the IME 676 * subtype </li> 677 * <li> If none, return null (Generic.kcm is the default) </li> 678 * </ul> 679 * </li> 680 * <li> Finding correct layout corresponding to provided layout info: 681 * <ul> 682 * <li> Filter all available layouts based on the IME subtype script code </li> 683 * <li> Derive locale from the provided layout info </li> 684 * <li> If layoutType i.e. qwerty, azerty, etc. is provided, filter layouts by 685 * layoutType and try to find matching layout to the derived locale. </li> 686 * <li> If none found or layoutType not provided, then ignore the layoutType and try 687 * to find matching layout to the derived locale. </li> 688 * </ul> 689 * </li> 690 * <li> Finding matching layout for the derived locale: 691 * <ul> 692 * <li> If language code doesn't match, ignore the layout (We can never match a 693 * layout if language code isn't matching) </li> 694 * <li> If country code matches, layout score +1 </li> 695 * <li> Else if country code of layout is empty, layout score +0.5 (empty country 696 * code is a semi match with derived locale with country code, this is to prioritize 697 * empty country code layouts over fully mismatching layouts) </li> 698 * <li> If variant matches, layout score +1 </li> 699 * <li> Else if variant of layout is empty, layout score +0.5 (empty variant is a 700 * semi match with derive locale with country code, this is to prioritize empty 701 * variant layouts over fully mismatching layouts) </li> 702 * <li> Choose the layout with the best score. </li> 703 * </ul> 704 * </li> 705 * </ol> 706 */ 707 @NonNull getDefaultKeyboardLayoutBasedOnImeInfo( KeyboardIdentifier keyboardIdentifier, @Nullable ImeInfo imeInfo, KeyboardLayout[] layoutList)708 private static KeyboardLayoutSelectionResult getDefaultKeyboardLayoutBasedOnImeInfo( 709 KeyboardIdentifier keyboardIdentifier, @Nullable ImeInfo imeInfo, 710 KeyboardLayout[] layoutList) { 711 Arrays.sort(layoutList); 712 713 // Check <VendorID, ProductID> matching for explicitly declared custom KCM files. 714 for (KeyboardLayout layout : layoutList) { 715 if (layout.getVendorId() == keyboardIdentifier.mIdentifier.getVendorId() 716 && layout.getProductId() == keyboardIdentifier.mIdentifier.getProductId()) { 717 if (DEBUG) { 718 Slog.d(TAG, 719 "getDefaultKeyboardLayoutBasedOnImeInfo() : Layout found based on " 720 + "vendor and product Ids. " + keyboardIdentifier 721 + " : " + layout.getDescriptor()); 722 } 723 return new KeyboardLayoutSelectionResult(layout.getDescriptor(), 724 LAYOUT_SELECTION_CRITERIA_DEVICE); 725 } 726 } 727 728 // Check layout type, language tag information from InputDevice for matching 729 String inputLanguageTag = keyboardIdentifier.mLanguageTag; 730 if (inputLanguageTag != null) { 731 String layoutDesc = getMatchingLayoutForProvidedLanguageTagAndLayoutType(layoutList, 732 inputLanguageTag, keyboardIdentifier.mLayoutType); 733 734 if (layoutDesc != null) { 735 if (DEBUG) { 736 Slog.d(TAG, 737 "getDefaultKeyboardLayoutBasedOnImeInfo() : Layout found based on " 738 + "HW information (Language tag and Layout type). " 739 + keyboardIdentifier + " : " + layoutDesc); 740 } 741 return new KeyboardLayoutSelectionResult(layoutDesc, 742 LAYOUT_SELECTION_CRITERIA_DEVICE); 743 } 744 } 745 746 if (imeInfo == null || imeInfo.mImeSubtypeHandle == null || imeInfo.mImeSubtype == null) { 747 // Can't auto select layout based on IME info is null 748 return FAILED; 749 } 750 751 InputMethodSubtype subtype = imeInfo.mImeSubtype; 752 // Check layout type, language tag information from IME for matching 753 ULocale pkLocale = subtype.getPhysicalKeyboardHintLanguageTag(); 754 String pkLanguageTag = 755 pkLocale != null ? pkLocale.toLanguageTag() : subtype.getCanonicalizedLanguageTag(); 756 String layoutDesc = getMatchingLayoutForProvidedLanguageTagAndLayoutType(layoutList, 757 pkLanguageTag, subtype.getPhysicalKeyboardHintLayoutType()); 758 if (DEBUG) { 759 Slog.d(TAG, 760 "getDefaultKeyboardLayoutBasedOnImeInfo() : Layout found based on " 761 + "IME locale matching. " + keyboardIdentifier + " : " 762 + layoutDesc); 763 } 764 if (layoutDesc != null) { 765 return new KeyboardLayoutSelectionResult(layoutDesc, 766 LAYOUT_SELECTION_CRITERIA_VIRTUAL_KEYBOARD); 767 } 768 return FAILED; 769 } 770 771 @Nullable getMatchingLayoutForProvidedLanguageTagAndLayoutType( KeyboardLayout[] layoutList, @NonNull String languageTag, @Nullable String layoutType)772 private static String getMatchingLayoutForProvidedLanguageTagAndLayoutType( 773 KeyboardLayout[] layoutList, @NonNull String languageTag, @Nullable String layoutType) { 774 if (layoutType == null || !KeyboardLayout.isLayoutTypeValid(layoutType)) { 775 layoutType = KeyboardLayout.LAYOUT_TYPE_UNDEFINED; 776 } 777 List<KeyboardLayout> layoutsFilteredByLayoutType = new ArrayList<>(); 778 for (KeyboardLayout layout : layoutList) { 779 if (layout.getLayoutType().equals(layoutType)) { 780 layoutsFilteredByLayoutType.add(layout); 781 } 782 } 783 String layoutDesc = getMatchingLayoutForProvidedLanguageTag(layoutsFilteredByLayoutType, 784 languageTag); 785 if (layoutDesc != null) { 786 return layoutDesc; 787 } 788 789 return getMatchingLayoutForProvidedLanguageTag(Arrays.asList(layoutList), languageTag); 790 } 791 792 @Nullable getMatchingLayoutForProvidedLanguageTag(List<KeyboardLayout> layoutList, @NonNull String languageTag)793 private static String getMatchingLayoutForProvidedLanguageTag(List<KeyboardLayout> layoutList, 794 @NonNull String languageTag) { 795 Locale locale = Locale.forLanguageTag(languageTag); 796 String bestMatchingLayout = null; 797 float bestMatchingLayoutScore = 0; 798 799 for (KeyboardLayout layout : layoutList) { 800 final LocaleList locales = layout.getLocales(); 801 for (int i = 0; i < locales.size(); i++) { 802 final Locale l = locales.get(i); 803 if (l == null) { 804 continue; 805 } 806 if (!l.getLanguage().equals(locale.getLanguage())) { 807 // If language mismatches: NEVER choose that layout 808 continue; 809 } 810 float layoutScore = 1; // If language matches then score +1 811 if (l.getCountry().equals(locale.getCountry())) { 812 layoutScore += 1; // If country matches then score +1 813 } else if (TextUtils.isEmpty(l.getCountry())) { 814 layoutScore += 0.5; // Consider empty country as semi-match 815 } 816 if (l.getVariant().equals(locale.getVariant())) { 817 layoutScore += 1; // If variant matches then score +1 818 } else if (TextUtils.isEmpty(l.getVariant())) { 819 layoutScore += 0.5; // Consider empty variant as semi-match 820 } 821 if (layoutScore > bestMatchingLayoutScore) { 822 bestMatchingLayoutScore = layoutScore; 823 bestMatchingLayout = layout.getDescriptor(); 824 } 825 } 826 } 827 return bestMatchingLayout; 828 } 829 reloadKeyboardLayouts()830 private void reloadKeyboardLayouts() { 831 if (DEBUG) { 832 Slog.d(TAG, "Reloading keyboard layouts."); 833 } 834 mNative.reloadKeyboardLayouts(); 835 } 836 837 @MainThread maybeUpdateNotification()838 private void maybeUpdateNotification() { 839 List<KeyboardConfiguration> configurations = new ArrayList<>(); 840 for (int i = 0; i < mConfiguredKeyboards.size(); i++) { 841 int deviceId = mConfiguredKeyboards.keyAt(i); 842 KeyboardConfiguration config = mConfiguredKeyboards.valueAt(i); 843 if (isVirtualDevice(deviceId)) { 844 continue; 845 } 846 // If we have a keyboard with no selected layouts, we should always show missing 847 // layout notification even if there are other keyboards that are configured properly. 848 if (!config.hasConfiguredLayouts()) { 849 showMissingKeyboardLayoutNotification(); 850 return; 851 } 852 configurations.add(config); 853 } 854 if (configurations.size() == 0) { 855 hideKeyboardLayoutNotification(); 856 return; 857 } 858 showConfiguredKeyboardLayoutNotification(configurations); 859 } 860 861 @MainThread showMissingKeyboardLayoutNotification()862 private void showMissingKeyboardLayoutNotification() { 863 final Resources r = mContext.getResources(); 864 final String missingKeyboardLayoutNotificationContent = r.getString( 865 R.string.select_keyboard_layout_notification_message); 866 867 if (mConfiguredKeyboards.size() == 1) { 868 final InputDevice device = getInputDevice(mConfiguredKeyboards.keyAt(0)); 869 if (device == null) { 870 return; 871 } 872 showKeyboardLayoutNotification( 873 r.getString( 874 R.string.select_keyboard_layout_notification_title, 875 device.getName()), 876 missingKeyboardLayoutNotificationContent, 877 device); 878 } else { 879 showKeyboardLayoutNotification( 880 r.getString(R.string.select_multiple_keyboards_layout_notification_title), 881 missingKeyboardLayoutNotificationContent, 882 null); 883 } 884 } 885 886 @MainThread showKeyboardLayoutNotification(@onNull String intentTitle, @NonNull String intentContent, @Nullable InputDevice targetDevice)887 private void showKeyboardLayoutNotification(@NonNull String intentTitle, 888 @NonNull String intentContent, @Nullable InputDevice targetDevice) { 889 final NotificationManager notificationManager = mContext.getSystemService( 890 NotificationManager.class); 891 if (notificationManager == null) { 892 return; 893 } 894 895 final Intent intent = new Intent(Settings.ACTION_HARD_KEYBOARD_SETTINGS); 896 897 if (targetDevice != null) { 898 intent.putExtra(Settings.EXTRA_INPUT_DEVICE_IDENTIFIER, targetDevice.getIdentifier()); 899 intent.putExtra( 900 Settings.EXTRA_ENTRYPOINT, SettingsEnums.KEYBOARD_CONFIGURED_NOTIFICATION); 901 } 902 903 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK 904 | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED 905 | Intent.FLAG_ACTIVITY_CLEAR_TOP); 906 final PendingIntent keyboardLayoutIntent = PendingIntent.getActivityAsUser(mContext, 0, 907 intent, PendingIntent.FLAG_IMMUTABLE, null, UserHandle.CURRENT); 908 909 Notification notification = 910 new Notification.Builder(mContext, SystemNotificationChannels.PHYSICAL_KEYBOARD) 911 .setContentTitle(intentTitle) 912 .setContentText(intentContent) 913 .setContentIntent(keyboardLayoutIntent) 914 .setSmallIcon(R.drawable.ic_settings_language) 915 .setColor(mContext.getColor( 916 com.android.internal.R.color.system_notification_accent_color)) 917 .setAutoCancel(true) 918 .build(); 919 notificationManager.notifyAsUser(null, 920 SystemMessageProto.SystemMessage.NOTE_SELECT_KEYBOARD_LAYOUT, 921 notification, UserHandle.ALL); 922 } 923 924 @MainThread hideKeyboardLayoutNotification()925 private void hideKeyboardLayoutNotification() { 926 NotificationManager notificationManager = mContext.getSystemService( 927 NotificationManager.class); 928 if (notificationManager == null) { 929 return; 930 } 931 932 notificationManager.cancelAsUser(null, 933 SystemMessageProto.SystemMessage.NOTE_SELECT_KEYBOARD_LAYOUT, 934 UserHandle.ALL); 935 } 936 937 @MainThread showConfiguredKeyboardLayoutNotification( List<KeyboardConfiguration> configurations)938 private void showConfiguredKeyboardLayoutNotification( 939 List<KeyboardConfiguration> configurations) { 940 final Resources r = mContext.getResources(); 941 942 if (configurations.size() != 1) { 943 showKeyboardLayoutNotification( 944 r.getString(R.string.keyboard_layout_notification_multiple_selected_title), 945 r.getString(R.string.keyboard_layout_notification_multiple_selected_message), 946 null); 947 return; 948 } 949 950 final KeyboardConfiguration config = configurations.get(0); 951 final InputDevice inputDevice = getInputDevice(config.getDeviceId()); 952 if (inputDevice == null || !config.hasConfiguredLayouts()) { 953 return; 954 } 955 956 showKeyboardLayoutNotification( 957 r.getString( 958 R.string.keyboard_layout_notification_selected_title, 959 inputDevice.getName()), 960 createConfiguredNotificationText(mContext, config.getConfiguredLayouts()), 961 inputDevice); 962 } 963 964 @MainThread createConfiguredNotificationText(@onNull Context context, @NonNull Set<String> selectedLayouts)965 private String createConfiguredNotificationText(@NonNull Context context, 966 @NonNull Set<String> selectedLayouts) { 967 final Resources r = context.getResources(); 968 List<String> layoutNames = new ArrayList<>(); 969 selectedLayouts.forEach( 970 (layoutDesc) -> layoutNames.add(getKeyboardLayout(layoutDesc).getLabel())); 971 Collections.sort(layoutNames); 972 switch (layoutNames.size()) { 973 case 1: 974 return r.getString(R.string.keyboard_layout_notification_one_selected_message, 975 layoutNames.get(0)); 976 case 2: 977 return r.getString(R.string.keyboard_layout_notification_two_selected_message, 978 layoutNames.get(0), layoutNames.get(1)); 979 case 3: 980 return r.getString(R.string.keyboard_layout_notification_three_selected_message, 981 layoutNames.get(0), layoutNames.get(1), layoutNames.get(2)); 982 default: 983 return r.getString( 984 R.string.keyboard_layout_notification_more_than_three_selected_message, 985 layoutNames.get(0), layoutNames.get(1), layoutNames.get(2)); 986 } 987 } 988 logKeyboardConfigurationEvent(@onNull InputDevice inputDevice, @NonNull List<ImeInfo> imeInfoList, @NonNull List<KeyboardLayoutSelectionResult> resultList, boolean isFirstConfiguration)989 private void logKeyboardConfigurationEvent(@NonNull InputDevice inputDevice, 990 @NonNull List<ImeInfo> imeInfoList, 991 @NonNull List<KeyboardLayoutSelectionResult> resultList, 992 boolean isFirstConfiguration) { 993 if (imeInfoList.isEmpty() || resultList.isEmpty()) { 994 return; 995 } 996 KeyboardConfigurationEvent.Builder configurationEventBuilder = 997 new KeyboardConfigurationEvent.Builder(inputDevice).setIsFirstTimeConfiguration( 998 isFirstConfiguration); 999 for (int i = 0; i < imeInfoList.size(); i++) { 1000 KeyboardLayoutSelectionResult result = resultList.get(i); 1001 String layoutName = null; 1002 int layoutSelectionCriteria = LAYOUT_SELECTION_CRITERIA_DEFAULT; 1003 if (result != null && result.getLayoutDescriptor() != null) { 1004 layoutSelectionCriteria = result.getSelectionCriteria(); 1005 KeyboardLayoutDescriptor d = KeyboardLayoutDescriptor.parse( 1006 result.getLayoutDescriptor()); 1007 if (d != null) { 1008 layoutName = d.keyboardLayoutName; 1009 } 1010 } 1011 configurationEventBuilder.addLayoutSelection(imeInfoList.get(i).mImeSubtype, layoutName, 1012 layoutSelectionCriteria); 1013 } 1014 KeyboardMetricsCollector.logKeyboardConfiguredAtom(configurationEventBuilder.build()); 1015 } 1016 handleMessage(Message msg)1017 private boolean handleMessage(Message msg) { 1018 switch (msg.what) { 1019 case MSG_UPDATE_EXISTING_DEVICES: 1020 // Circle through all the already added input devices 1021 // Need to do it on handler thread and not block IMS thread 1022 for (int deviceId : (int[]) msg.obj) { 1023 onInputDeviceAdded(deviceId); 1024 } 1025 return true; 1026 case MSG_RELOAD_KEYBOARD_LAYOUTS: 1027 reloadKeyboardLayouts(); 1028 return true; 1029 case MSG_UPDATE_KEYBOARD_LAYOUTS: 1030 updateKeyboardLayouts(); 1031 return true; 1032 default: 1033 return false; 1034 } 1035 } 1036 1037 @Nullable getInputDevice(int deviceId)1038 private InputDevice getInputDevice(int deviceId) { 1039 InputManager inputManager = mContext.getSystemService(InputManager.class); 1040 return inputManager != null ? inputManager.getInputDevice(deviceId) : null; 1041 } 1042 1043 @Nullable getInputDevice(InputDeviceIdentifier identifier)1044 private InputDevice getInputDevice(InputDeviceIdentifier identifier) { 1045 InputManager inputManager = mContext.getSystemService(InputManager.class); 1046 return inputManager != null ? inputManager.getInputDeviceByDescriptor( 1047 identifier.getDescriptor()) : null; 1048 } 1049 1050 @SuppressLint("MissingPermission") 1051 @VisibleForTesting getImeInfoListForLayoutMapping()1052 public List<ImeInfo> getImeInfoListForLayoutMapping() { 1053 List<ImeInfo> imeInfoList = new ArrayList<>(); 1054 UserManager userManager = Objects.requireNonNull( 1055 mContext.getSystemService(UserManager.class)); 1056 InputMethodManager inputMethodManager = Objects.requireNonNull( 1057 mContext.getSystemService(InputMethodManager.class)); 1058 // Need to use InputMethodManagerInternal to call getEnabledInputMethodListAsUser() 1059 // instead of using InputMethodManager which uses enforceCallingPermissions() that 1060 // breaks when we are calling the method for work profile user ID since it doesn't check 1061 // self permissions. 1062 InputMethodManagerInternal inputMethodManagerInternal = InputMethodManagerInternal.get(); 1063 for (UserHandle userHandle : userManager.getUserHandles(true /* excludeDying */)) { 1064 int userId = userHandle.getIdentifier(); 1065 for (InputMethodInfo imeInfo : 1066 inputMethodManagerInternal.getEnabledInputMethodListAsUser( 1067 userId)) { 1068 for (InputMethodSubtype imeSubtype : 1069 inputMethodManager.getEnabledInputMethodSubtypeList( 1070 imeInfo, true /* allowsImplicitlyEnabledSubtypes */)) { 1071 if (!imeSubtype.isSuitableForPhysicalKeyboardLayoutMapping()) { 1072 continue; 1073 } 1074 imeInfoList.add(new ImeInfo(userId, imeInfo, imeSubtype)); 1075 } 1076 } 1077 } 1078 return imeInfoList; 1079 } 1080 isLayoutCompatibleWithLanguageTag(KeyboardLayout layout, @NonNull String languageTag)1081 private static boolean isLayoutCompatibleWithLanguageTag(KeyboardLayout layout, 1082 @NonNull String languageTag) { 1083 LocaleList layoutLocales = layout.getLocales(); 1084 if (layoutLocales.isEmpty() || TextUtils.isEmpty(languageTag)) { 1085 // KCM file doesn't have an associated language tag. This can be from 1086 // a 3rd party app so need to include it as a potential layout. 1087 return true; 1088 } 1089 // Match derived Script codes 1090 final int[] scriptsFromLanguageTag = getScriptCodes(Locale.forLanguageTag(languageTag)); 1091 if (scriptsFromLanguageTag.length == 0) { 1092 // If no scripts inferred from languageTag then allowing the layout 1093 return true; 1094 } 1095 for (int i = 0; i < layoutLocales.size(); i++) { 1096 final Locale locale = layoutLocales.get(i); 1097 int[] scripts = getScriptCodes(locale); 1098 if (haveCommonValue(scripts, scriptsFromLanguageTag)) { 1099 return true; 1100 } 1101 } 1102 return false; 1103 } 1104 1105 @VisibleForTesting isVirtualDevice(int deviceId)1106 public boolean isVirtualDevice(int deviceId) { 1107 VirtualDeviceManagerInternal vdm = LocalServices.getService( 1108 VirtualDeviceManagerInternal.class); 1109 return vdm != null && vdm.isInputDeviceOwnedByVirtualDevice(deviceId); 1110 } 1111 getScriptCodes(@ullable Locale locale)1112 private static int[] getScriptCodes(@Nullable Locale locale) { 1113 if (locale == null) { 1114 return new int[0]; 1115 } 1116 if (!TextUtils.isEmpty(locale.getScript())) { 1117 int scriptCode = UScript.getCodeFromName(locale.getScript()); 1118 if (scriptCode != UScript.INVALID_CODE) { 1119 return new int[]{scriptCode}; 1120 } 1121 } 1122 int[] scripts = UScript.getCode(locale); 1123 if (scripts != null) { 1124 return scripts; 1125 } 1126 return new int[0]; 1127 } 1128 haveCommonValue(int[] arr1, int[] arr2)1129 private static boolean haveCommonValue(int[] arr1, int[] arr2) { 1130 for (int a1 : arr1) { 1131 for (int a2 : arr2) { 1132 if (a1 == a2) return true; 1133 } 1134 } 1135 return false; 1136 } 1137 1138 private static final class KeyboardLayoutDescriptor { 1139 public String packageName; 1140 public String receiverName; 1141 public String keyboardLayoutName; 1142 format(String packageName, String receiverName, String keyboardName)1143 public static String format(String packageName, 1144 String receiverName, String keyboardName) { 1145 return packageName + "/" + receiverName + "/" + keyboardName; 1146 } 1147 parse(@onNull String descriptor)1148 public static KeyboardLayoutDescriptor parse(@NonNull String descriptor) { 1149 int pos = descriptor.indexOf('/'); 1150 if (pos < 0 || pos + 1 == descriptor.length()) { 1151 return null; 1152 } 1153 int pos2 = descriptor.indexOf('/', pos + 1); 1154 if (pos2 < pos + 2 || pos2 + 1 == descriptor.length()) { 1155 return null; 1156 } 1157 1158 KeyboardLayoutDescriptor result = new KeyboardLayoutDescriptor(); 1159 result.packageName = descriptor.substring(0, pos); 1160 result.receiverName = descriptor.substring(pos + 1, pos2); 1161 result.keyboardLayoutName = descriptor.substring(pos2 + 1); 1162 return result; 1163 } 1164 } 1165 1166 @VisibleForTesting 1167 public static class ImeInfo { 1168 @UserIdInt int mUserId; 1169 @NonNull InputMethodSubtypeHandle mImeSubtypeHandle; 1170 @Nullable InputMethodSubtype mImeSubtype; 1171 ImeInfo(@serIdInt int userId, @NonNull InputMethodSubtypeHandle imeSubtypeHandle, @Nullable InputMethodSubtype imeSubtype)1172 ImeInfo(@UserIdInt int userId, @NonNull InputMethodSubtypeHandle imeSubtypeHandle, 1173 @Nullable InputMethodSubtype imeSubtype) { 1174 mUserId = userId; 1175 mImeSubtypeHandle = imeSubtypeHandle; 1176 mImeSubtype = imeSubtype; 1177 } 1178 ImeInfo(@serIdInt int userId, @NonNull InputMethodInfo imeInfo, @Nullable InputMethodSubtype imeSubtype)1179 ImeInfo(@UserIdInt int userId, @NonNull InputMethodInfo imeInfo, 1180 @Nullable InputMethodSubtype imeSubtype) { 1181 this(userId, InputMethodSubtypeHandle.of(imeInfo, imeSubtype), imeSubtype); 1182 } 1183 } 1184 1185 private static class KeyboardConfiguration { 1186 1187 // If null or empty, it means no layout is configured for the device. And user needs to 1188 // manually set up the device. 1189 @Nullable 1190 private Set<String> mConfiguredLayouts; 1191 1192 private final int mDeviceId; 1193 KeyboardConfiguration(int deviceId)1194 private KeyboardConfiguration(int deviceId) { 1195 mDeviceId = deviceId; 1196 } 1197 getDeviceId()1198 private int getDeviceId() { 1199 return mDeviceId; 1200 } 1201 hasConfiguredLayouts()1202 private boolean hasConfiguredLayouts() { 1203 return mConfiguredLayouts != null && !mConfiguredLayouts.isEmpty(); 1204 } 1205 1206 @Nullable getConfiguredLayouts()1207 private Set<String> getConfiguredLayouts() { 1208 return mConfiguredLayouts; 1209 } 1210 setConfiguredLayouts(Set<String> configuredLayouts)1211 private void setConfiguredLayouts(Set<String> configuredLayouts) { 1212 mConfiguredLayouts = configuredLayouts; 1213 } 1214 } 1215 1216 private interface KeyboardLayoutVisitor { visitKeyboardLayout(Resources resources, int keyboardLayoutResId, KeyboardLayout layout)1217 void visitKeyboardLayout(Resources resources, 1218 int keyboardLayoutResId, KeyboardLayout layout); 1219 } 1220 1221 private static class KeyboardIdentifier { 1222 @NonNull 1223 private final InputDeviceIdentifier mIdentifier; 1224 @Nullable 1225 private final String mLanguageTag; 1226 @Nullable 1227 private final String mLayoutType; 1228 1229 // NOTE: Use this only for old settings UI where we don't use language tag and layout 1230 // type to determine the KCM file. KeyboardIdentifier(@onNull InputDeviceIdentifier inputDeviceIdentifier)1231 private KeyboardIdentifier(@NonNull InputDeviceIdentifier inputDeviceIdentifier) { 1232 this(inputDeviceIdentifier, null, null); 1233 } 1234 KeyboardIdentifier(@onNull InputDevice inputDevice)1235 private KeyboardIdentifier(@NonNull InputDevice inputDevice) { 1236 this(inputDevice.getIdentifier(), inputDevice.getKeyboardLanguageTag(), 1237 inputDevice.getKeyboardLayoutType()); 1238 } 1239 KeyboardIdentifier(@onNull InputDeviceIdentifier identifier, @Nullable String languageTag, @Nullable String layoutType)1240 private KeyboardIdentifier(@NonNull InputDeviceIdentifier identifier, 1241 @Nullable String languageTag, @Nullable String layoutType) { 1242 Objects.requireNonNull(identifier, "identifier must not be null"); 1243 Objects.requireNonNull(identifier.getDescriptor(), "descriptor must not be null"); 1244 mIdentifier = identifier; 1245 mLanguageTag = languageTag; 1246 mLayoutType = layoutType; 1247 } 1248 1249 @Override hashCode()1250 public int hashCode() { 1251 return Objects.hashCode(toString()); 1252 } 1253 1254 @Override toString()1255 public String toString() { 1256 if (mIdentifier.getVendorId() == 0 && mIdentifier.getProductId() == 0) { 1257 return mIdentifier.getDescriptor(); 1258 } 1259 // If vendor id and product id is available, use it as keys. This allows us to have the 1260 // same setup for all keyboards with same product and vendor id. i.e. User can swap 2 1261 // identical keyboards and still get the same setup. 1262 StringBuilder key = new StringBuilder(); 1263 key.append("vendor:").append(mIdentifier.getVendorId()).append(",product:").append( 1264 mIdentifier.getProductId()); 1265 1266 // Some keyboards can have same product ID and vendor ID but different Keyboard info 1267 // like language tag and layout type. 1268 if (!TextUtils.isEmpty(mLanguageTag)) { 1269 key.append(",languageTag:").append(mLanguageTag); 1270 } 1271 if (!TextUtils.isEmpty(mLayoutType)) { 1272 key.append(",layoutType:").append(mLayoutType); 1273 } 1274 return key.toString(); 1275 } 1276 } 1277 1278 private static class LayoutKey { 1279 1280 private final KeyboardIdentifier mKeyboardIdentifier; 1281 @Nullable 1282 private final ImeInfo mImeInfo; 1283 LayoutKey(KeyboardIdentifier keyboardIdentifier, @Nullable ImeInfo imeInfo)1284 private LayoutKey(KeyboardIdentifier keyboardIdentifier, @Nullable ImeInfo imeInfo) { 1285 mKeyboardIdentifier = keyboardIdentifier; 1286 mImeInfo = imeInfo; 1287 } 1288 1289 @Override hashCode()1290 public int hashCode() { 1291 return Objects.hashCode(toString()); 1292 } 1293 1294 @Override toString()1295 public String toString() { 1296 if (mImeInfo == null) { 1297 return mKeyboardIdentifier.toString(); 1298 } 1299 Objects.requireNonNull(mImeInfo.mImeSubtypeHandle, "subtypeHandle must not be null"); 1300 return "layoutDescriptor:" + mKeyboardIdentifier + ",userId:" + mImeInfo.mUserId 1301 + ",subtypeHandle:" + mImeInfo.mImeSubtypeHandle.toStringHandle(); 1302 } 1303 } 1304 } 1305