• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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