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