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