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