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