blob: 61c0fb1aae01409660c05dcbf1aef6d935806007 [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.input;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
import android.hardware.input.InputDeviceIdentifier;
import android.hardware.input.InputManager;
import android.hardware.input.KeyboardLayout;
import android.icu.lang.UScript;
import android.icu.util.ULocale;
import android.os.Bundle;
import android.os.Handler;
import android.os.LocaleList;
import android.os.Looper;
import android.os.Message;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.FeatureFlagUtils;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
import android.view.InputDevice;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.InputMethodSubtype;
import android.widget.Toast;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.inputmethod.InputMethodSubtypeHandle;
import com.android.internal.messages.nano.SystemMessageProto;
import com.android.internal.notification.SystemNotificationChannels;
import com.android.internal.util.XmlUtils;
import libcore.io.Streams;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Stream;
/**
* A component of {@link InputManagerService} responsible for managing Physical Keyboard layouts.
*
* @hide
*/
final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
private static final String TAG = "KeyboardLayoutManager";
// To enable these logs, run: 'adb shell setprop log.tag.KeyboardLayoutManager DEBUG'
// (requires restart)
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
private static final int MSG_UPDATE_EXISTING_DEVICES = 1;
private static final int MSG_SWITCH_KEYBOARD_LAYOUT = 2;
private static final int MSG_RELOAD_KEYBOARD_LAYOUTS = 3;
private static final int MSG_UPDATE_KEYBOARD_LAYOUTS = 4;
private final Context mContext;
private final NativeInputManagerService mNative;
// The PersistentDataStore should be locked before use.
@GuardedBy("mDataStore")
private final PersistentDataStore mDataStore;
private final Handler mHandler;
// Connected keyboards with associated keyboard layouts (either auto-detected or manually
// selected layout). If the mapped value is null/empty, it means that no layout has been
// configured for the keyboard and user might need to manually configure it from the Settings.
private final SparseArray<Set<String>> mConfiguredKeyboards = new SparseArray<>();
private Toast mSwitchedKeyboardLayoutToast;
// This cache stores "best-matched" layouts so that we don't need to run the matching
// algorithm repeatedly.
@GuardedBy("mKeyboardLayoutCache")
private final Map<String, String> mKeyboardLayoutCache = new ArrayMap<>();
@Nullable
private ImeInfo mCurrentImeInfo;
KeyboardLayoutManager(Context context, NativeInputManagerService nativeService,
PersistentDataStore dataStore, Looper looper) {
mContext = context;
mNative = nativeService;
mDataStore = dataStore;
mHandler = new Handler(looper, this::handleMessage, true /* async */);
}
public void systemRunning() {
// Listen to new Package installations to fetch new Keyboard layouts
IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
filter.addDataScheme("package");
mContext.registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
updateKeyboardLayouts();
}
}, filter, null, mHandler);
mHandler.sendEmptyMessage(MSG_UPDATE_KEYBOARD_LAYOUTS);
// Listen to new InputDevice changes
InputManager inputManager = Objects.requireNonNull(
mContext.getSystemService(InputManager.class));
inputManager.registerInputDeviceListener(this, mHandler);
Message msg = Message.obtain(mHandler, MSG_UPDATE_EXISTING_DEVICES,
inputManager.getInputDeviceIds());
mHandler.sendMessage(msg);
}
@Override
public void onInputDeviceAdded(int deviceId) {
onInputDeviceChanged(deviceId);
if (useNewSettingsUi()) {
// Force native callback to set up keyboard layout overlay for newly added keyboards
reloadKeyboardLayouts();
}
}
@Override
public void onInputDeviceRemoved(int deviceId) {
mConfiguredKeyboards.remove(deviceId);
maybeUpdateNotification();
}
@Override
public void onInputDeviceChanged(int deviceId) {
final InputDevice inputDevice = getInputDevice(deviceId);
if (inputDevice == null || inputDevice.isVirtual() || !inputDevice.isFullKeyboard()) {
return;
}
if (!useNewSettingsUi()) {
synchronized (mDataStore) {
String layout = getCurrentKeyboardLayoutForInputDevice(inputDevice.getIdentifier());
if (layout == null) {
layout = getDefaultKeyboardLayout(inputDevice);
if (layout != null) {
setCurrentKeyboardLayoutForInputDevice(inputDevice.getIdentifier(), layout);
} else {
mConfiguredKeyboards.put(inputDevice.getId(), new HashSet<>());
}
}
}
} else {
final InputDeviceIdentifier identifier = inputDevice.getIdentifier();
final String key = getLayoutDescriptor(identifier);
Set<String> selectedLayouts = new HashSet<>();
boolean needToShowMissingLayoutNotification = false;
for (ImeInfo imeInfo : getImeInfoListForLayoutMapping()) {
// Check if the layout has been previously configured
String layout = getKeyboardLayoutForInputDeviceInternal(identifier,
new ImeInfo(imeInfo.mUserId, imeInfo.mImeSubtypeHandle,
imeInfo.mImeSubtype));
if (layout == null) {
needToShowMissingLayoutNotification = true;
continue;
}
selectedLayouts.add(layout);
}
if (needToShowMissingLayoutNotification) {
// If even one layout not configured properly we will show configuration
// notification allowing user to set the keyboard layout.
selectedLayouts.clear();
}
if (DEBUG) {
Slog.d(TAG,
"Layouts selected for input device: " + identifier + " -> selectedLayouts: "
+ selectedLayouts);
}
mConfiguredKeyboards.set(inputDevice.getId(), selectedLayouts);
synchronized (mDataStore) {
try {
if (!mDataStore.setSelectedKeyboardLayouts(key, selectedLayouts)) {
// No need to show the notification only if layout selection didn't change
// from the previous configuration
return;
}
} finally {
mDataStore.saveIfNeeded();
}
}
}
maybeUpdateNotification();
}
private String getDefaultKeyboardLayout(final InputDevice inputDevice) {
final Locale systemLocale = mContext.getResources().getConfiguration().locale;
// If our locale doesn't have a language for some reason, then we don't really have a
// reasonable default.
if (TextUtils.isEmpty(systemLocale.getLanguage())) {
return null;
}
final List<KeyboardLayout> layouts = new ArrayList<>();
visitAllKeyboardLayouts((resources, keyboardLayoutResId, layout) -> {
// Only select a default when we know the layout is appropriate. For now, this
// means it's a custom layout for a specific keyboard.
if (layout.getVendorId() != inputDevice.getVendorId()
|| layout.getProductId() != inputDevice.getProductId()) {
return;
}
final LocaleList locales = layout.getLocales();
for (int localeIndex = 0; localeIndex < locales.size(); ++localeIndex) {
final Locale locale = locales.get(localeIndex);
if (locale != null && isCompatibleLocale(systemLocale, locale)) {
layouts.add(layout);
break;
}
}
});
if (layouts.isEmpty()) {
return null;
}
// First sort so that ones with higher priority are listed at the top
Collections.sort(layouts);
// Next we want to try to find an exact match of language, country and variant.
for (KeyboardLayout layout : layouts) {
final LocaleList locales = layout.getLocales();
for (int localeIndex = 0; localeIndex < locales.size(); ++localeIndex) {
final Locale locale = locales.get(localeIndex);
if (locale != null && locale.getCountry().equals(systemLocale.getCountry())
&& locale.getVariant().equals(systemLocale.getVariant())) {
return layout.getDescriptor();
}
}
}
// Then try an exact match of language and country
for (KeyboardLayout layout : layouts) {
final LocaleList locales = layout.getLocales();
for (int localeIndex = 0; localeIndex < locales.size(); ++localeIndex) {
final Locale locale = locales.get(localeIndex);
if (locale != null && locale.getCountry().equals(systemLocale.getCountry())) {
return layout.getDescriptor();
}
}
}
// Give up and just use the highest priority layout with matching language
return layouts.get(0).getDescriptor();
}
private static boolean isCompatibleLocale(Locale systemLocale, Locale keyboardLocale) {
// Different languages are never compatible
if (!systemLocale.getLanguage().equals(keyboardLocale.getLanguage())) {
return false;
}
// If both the system and the keyboard layout have a country specifier, they must be equal.
return TextUtils.isEmpty(systemLocale.getCountry())
|| TextUtils.isEmpty(keyboardLocale.getCountry())
|| systemLocale.getCountry().equals(keyboardLocale.getCountry());
}
private void updateKeyboardLayouts() {
// Scan all input devices state for keyboard layouts that have been uninstalled.
final HashSet<String> availableKeyboardLayouts = new HashSet<String>();
visitAllKeyboardLayouts((resources, keyboardLayoutResId, layout) ->
availableKeyboardLayouts.add(layout.getDescriptor()));
synchronized (mDataStore) {
try {
mDataStore.removeUninstalledKeyboardLayouts(availableKeyboardLayouts);
} finally {
mDataStore.saveIfNeeded();
}
}
synchronized (mKeyboardLayoutCache) {
// Invalidate the cache: With packages being installed/removed, existing cache of
// auto-selected layout might not be the best layouts anymore.
mKeyboardLayoutCache.clear();
}
// Reload keyboard layouts.
reloadKeyboardLayouts();
}
public KeyboardLayout[] getKeyboardLayouts() {
final ArrayList<KeyboardLayout> list = new ArrayList<>();
visitAllKeyboardLayouts((resources, keyboardLayoutResId, layout) -> list.add(layout));
return list.toArray(new KeyboardLayout[0]);
}
public KeyboardLayout[] getKeyboardLayoutsForInputDevice(
final InputDeviceIdentifier identifier) {
if (useNewSettingsUi()) {
// Provide all supported keyboard layouts since Ime info is not provided
return getKeyboardLayouts();
}
final String[] enabledLayoutDescriptors =
getEnabledKeyboardLayoutsForInputDevice(identifier);
final ArrayList<KeyboardLayout> enabledLayouts =
new ArrayList<>(enabledLayoutDescriptors.length);
final ArrayList<KeyboardLayout> potentialLayouts = new ArrayList<>();
visitAllKeyboardLayouts(new KeyboardLayoutVisitor() {
boolean mHasSeenDeviceSpecificLayout;
@Override
public void visitKeyboardLayout(Resources resources,
int keyboardLayoutResId, KeyboardLayout layout) {
// First check if it's enabled. If the keyboard layout is enabled then we always
// want to return it as a possible layout for the device.
for (String s : enabledLayoutDescriptors) {
if (s != null && s.equals(layout.getDescriptor())) {
enabledLayouts.add(layout);
return;
}
}
// Next find any potential layouts that aren't yet enabled for the device. For
// devices that have special layouts we assume there's a reason that the generic
// layouts don't work for them so we don't want to return them since it's likely
// to result in a poor user experience.
if (layout.getVendorId() == identifier.getVendorId()
&& layout.getProductId() == identifier.getProductId()) {
if (!mHasSeenDeviceSpecificLayout) {
mHasSeenDeviceSpecificLayout = true;
potentialLayouts.clear();
}
potentialLayouts.add(layout);
} else if (layout.getVendorId() == -1 && layout.getProductId() == -1
&& !mHasSeenDeviceSpecificLayout) {
potentialLayouts.add(layout);
}
}
});
return Stream.concat(enabledLayouts.stream(), potentialLayouts.stream()).toArray(
KeyboardLayout[]::new);
}
@Nullable
public KeyboardLayout getKeyboardLayout(String keyboardLayoutDescriptor) {
Objects.requireNonNull(keyboardLayoutDescriptor,
"keyboardLayoutDescriptor must not be null");
final KeyboardLayout[] result = new KeyboardLayout[1];
visitKeyboardLayout(keyboardLayoutDescriptor,
(resources, keyboardLayoutResId, layout) -> result[0] = layout);
if (result[0] == null) {
Slog.w(TAG, "Could not get keyboard layout with descriptor '"
+ keyboardLayoutDescriptor + "'.");
}
return result[0];
}
private void visitAllKeyboardLayouts(KeyboardLayoutVisitor visitor) {
final PackageManager pm = mContext.getPackageManager();
Intent intent = new Intent(InputManager.ACTION_QUERY_KEYBOARD_LAYOUTS);
for (ResolveInfo resolveInfo : pm.queryBroadcastReceiversAsUser(intent,
PackageManager.GET_META_DATA | PackageManager.MATCH_DIRECT_BOOT_AWARE
| PackageManager.MATCH_DIRECT_BOOT_UNAWARE, UserHandle.USER_SYSTEM)) {
final ActivityInfo activityInfo = resolveInfo.activityInfo;
final int priority = resolveInfo.priority;
visitKeyboardLayoutsInPackage(pm, activityInfo, null, priority, visitor);
}
}
private void visitKeyboardLayout(String keyboardLayoutDescriptor,
KeyboardLayoutVisitor visitor) {
KeyboardLayoutDescriptor d = KeyboardLayoutDescriptor.parse(keyboardLayoutDescriptor);
if (d != null) {
final PackageManager pm = mContext.getPackageManager();
try {
ActivityInfo receiver = pm.getReceiverInfo(
new ComponentName(d.packageName, d.receiverName),
PackageManager.GET_META_DATA
| PackageManager.MATCH_DIRECT_BOOT_AWARE
| PackageManager.MATCH_DIRECT_BOOT_UNAWARE);
visitKeyboardLayoutsInPackage(pm, receiver, d.keyboardLayoutName, 0, visitor);
} catch (PackageManager.NameNotFoundException ignored) {
}
}
}
private void visitKeyboardLayoutsInPackage(PackageManager pm, ActivityInfo receiver,
String keyboardName, int requestedPriority, KeyboardLayoutVisitor visitor) {
Bundle metaData = receiver.metaData;
if (metaData == null) {
return;
}
int configResId = metaData.getInt(InputManager.META_DATA_KEYBOARD_LAYOUTS);
if (configResId == 0) {
Slog.w(TAG, "Missing meta-data '" + InputManager.META_DATA_KEYBOARD_LAYOUTS
+ "' on receiver " + receiver.packageName + "/" + receiver.name);
return;
}
CharSequence receiverLabel = receiver.loadLabel(pm);
String collection = receiverLabel != null ? receiverLabel.toString() : "";
int priority;
if ((receiver.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
priority = requestedPriority;
} else {
priority = 0;
}
try {
Resources resources = pm.getResourcesForApplication(receiver.applicationInfo);
try (XmlResourceParser parser = resources.getXml(configResId)) {
XmlUtils.beginDocument(parser, "keyboard-layouts");
while (true) {
XmlUtils.nextElement(parser);
String element = parser.getName();
if (element == null) {
break;
}
if (element.equals("keyboard-layout")) {
TypedArray a = resources.obtainAttributes(
parser, R.styleable.KeyboardLayout);
try {
String name = a.getString(
R.styleable.KeyboardLayout_name);
String label = a.getString(
R.styleable.KeyboardLayout_label);
int keyboardLayoutResId = a.getResourceId(
R.styleable.KeyboardLayout_keyboardLayout,
0);
String languageTags = a.getString(
R.styleable.KeyboardLayout_keyboardLocale);
LocaleList locales = getLocalesFromLanguageTags(languageTags);
int layoutType = a.getInt(R.styleable.KeyboardLayout_keyboardLayoutType,
0);
int vid = a.getInt(
R.styleable.KeyboardLayout_vendorId, -1);
int pid = a.getInt(
R.styleable.KeyboardLayout_productId, -1);
if (name == null || label == null || keyboardLayoutResId == 0) {
Slog.w(TAG, "Missing required 'name', 'label' or 'keyboardLayout' "
+ "attributes in keyboard layout "
+ "resource from receiver "
+ receiver.packageName + "/" + receiver.name);
} else {
String descriptor = KeyboardLayoutDescriptor.format(
receiver.packageName, receiver.name, name);
if (keyboardName == null || name.equals(keyboardName)) {
KeyboardLayout layout = new KeyboardLayout(
descriptor, label, collection, priority,
locales, layoutType, vid, pid);
visitor.visitKeyboardLayout(
resources, keyboardLayoutResId, layout);
}
}
} finally {
a.recycle();
}
} else {
Slog.w(TAG, "Skipping unrecognized element '" + element
+ "' in keyboard layout resource from receiver "
+ receiver.packageName + "/" + receiver.name);
}
}
}
} catch (Exception ex) {
Slog.w(TAG, "Could not parse keyboard layout resource from receiver "
+ receiver.packageName + "/" + receiver.name, ex);
}
}
@NonNull
private static LocaleList getLocalesFromLanguageTags(String languageTags) {
if (TextUtils.isEmpty(languageTags)) {
return LocaleList.getEmptyLocaleList();
}
return LocaleList.forLanguageTags(languageTags.replace('|', ','));
}
private String getLayoutDescriptor(@NonNull InputDeviceIdentifier identifier) {
Objects.requireNonNull(identifier, "identifier must not be null");
Objects.requireNonNull(identifier.getDescriptor(), "descriptor must not be null");
if (identifier.getVendorId() == 0 && identifier.getProductId() == 0) {
return identifier.getDescriptor();
}
// If vendor id and product id is available, use it as keys. This allows us to have the
// same setup for all keyboards with same product and vendor id. i.e. User can swap 2
// identical keyboards and still get the same setup.
StringBuilder key = new StringBuilder();
key.append("vendor:").append(identifier.getVendorId()).append(",product:").append(
identifier.getProductId());
InputDevice inputDevice = getInputDevice(identifier);
Objects.requireNonNull(inputDevice, "Input device must not be null");
// Some keyboards can have same product ID and vendor ID but different Keyboard info like
// language tag and layout type.
if (!TextUtils.isEmpty(inputDevice.getKeyboardLanguageTag())) {
key.append(",languageTag:").append(inputDevice.getKeyboardLanguageTag());
}
if (!TextUtils.isEmpty(inputDevice.getKeyboardLayoutType())) {
key.append(",layoutType:").append(inputDevice.getKeyboardLanguageTag());
}
return key.toString();
}
@Nullable
public String getCurrentKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier) {
if (useNewSettingsUi()) {
Slog.e(TAG, "getCurrentKeyboardLayoutForInputDevice API not supported");
return null;
}
String key = getLayoutDescriptor(identifier);
synchronized (mDataStore) {
String layout;
// try loading it using the layout descriptor if we have it
layout = mDataStore.getCurrentKeyboardLayout(key);
if (layout == null && !key.equals(identifier.getDescriptor())) {
// if it doesn't exist fall back to the device descriptor
layout = mDataStore.getCurrentKeyboardLayout(identifier.getDescriptor());
}
if (DEBUG) {
Slog.d(TAG, "getCurrentKeyboardLayoutForInputDevice() "
+ identifier.toString() + ": " + layout);
}
return layout;
}
}
public void setCurrentKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
String keyboardLayoutDescriptor) {
if (useNewSettingsUi()) {
Slog.e(TAG, "setCurrentKeyboardLayoutForInputDevice API not supported");
return;
}
Objects.requireNonNull(keyboardLayoutDescriptor,
"keyboardLayoutDescriptor must not be null");
String key = getLayoutDescriptor(identifier);
synchronized (mDataStore) {
try {
if (mDataStore.setCurrentKeyboardLayout(key, keyboardLayoutDescriptor)) {
if (DEBUG) {
Slog.d(TAG, "setCurrentKeyboardLayoutForInputDevice() " + identifier
+ " key: " + key
+ " keyboardLayoutDescriptor: " + keyboardLayoutDescriptor);
}
mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS);
}
} finally {
mDataStore.saveIfNeeded();
}
}
}
public String[] getEnabledKeyboardLayoutsForInputDevice(InputDeviceIdentifier identifier) {
if (useNewSettingsUi()) {
Slog.e(TAG, "getEnabledKeyboardLayoutsForInputDevice API not supported");
return new String[0];
}
String key = getLayoutDescriptor(identifier);
synchronized (mDataStore) {
String[] layouts = mDataStore.getKeyboardLayouts(key);
if ((layouts == null || layouts.length == 0)
&& !key.equals(identifier.getDescriptor())) {
layouts = mDataStore.getKeyboardLayouts(identifier.getDescriptor());
}
return layouts;
}
}
public void addKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
String keyboardLayoutDescriptor) {
if (useNewSettingsUi()) {
Slog.e(TAG, "addKeyboardLayoutForInputDevice API not supported");
return;
}
Objects.requireNonNull(keyboardLayoutDescriptor,
"keyboardLayoutDescriptor must not be null");
String key = getLayoutDescriptor(identifier);
synchronized (mDataStore) {
try {
String oldLayout = mDataStore.getCurrentKeyboardLayout(key);
if (oldLayout == null && !key.equals(identifier.getDescriptor())) {
oldLayout = mDataStore.getCurrentKeyboardLayout(identifier.getDescriptor());
}
if (mDataStore.addKeyboardLayout(key, keyboardLayoutDescriptor)
&& !Objects.equals(oldLayout,
mDataStore.getCurrentKeyboardLayout(key))) {
mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS);
}
} finally {
mDataStore.saveIfNeeded();
}
}
}
public void removeKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
String keyboardLayoutDescriptor) {
if (useNewSettingsUi()) {
Slog.e(TAG, "removeKeyboardLayoutForInputDevice API not supported");
return;
}
Objects.requireNonNull(keyboardLayoutDescriptor,
"keyboardLayoutDescriptor must not be null");
String key = getLayoutDescriptor(identifier);
synchronized (mDataStore) {
try {
String oldLayout = mDataStore.getCurrentKeyboardLayout(key);
if (oldLayout == null && !key.equals(identifier.getDescriptor())) {
oldLayout = mDataStore.getCurrentKeyboardLayout(identifier.getDescriptor());
}
boolean removed = mDataStore.removeKeyboardLayout(key, keyboardLayoutDescriptor);
if (!key.equals(identifier.getDescriptor())) {
// We need to remove from both places to ensure it is gone
removed |= mDataStore.removeKeyboardLayout(identifier.getDescriptor(),
keyboardLayoutDescriptor);
}
if (removed && !Objects.equals(oldLayout,
mDataStore.getCurrentKeyboardLayout(key))) {
mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS);
}
} finally {
mDataStore.saveIfNeeded();
}
}
}
public void switchKeyboardLayout(int deviceId, int direction) {
if (useNewSettingsUi()) {
Slog.e(TAG, "switchKeyboardLayout API not supported");
return;
}
mHandler.obtainMessage(MSG_SWITCH_KEYBOARD_LAYOUT, deviceId, direction).sendToTarget();
}
// Must be called on handler.
private void handleSwitchKeyboardLayout(int deviceId, int direction) {
final InputDevice device = getInputDevice(deviceId);
if (device != null) {
final boolean changed;
final String keyboardLayoutDescriptor;
String key = getLayoutDescriptor(device.getIdentifier());
synchronized (mDataStore) {
try {
changed = mDataStore.switchKeyboardLayout(key, direction);
keyboardLayoutDescriptor = mDataStore.getCurrentKeyboardLayout(
key);
} finally {
mDataStore.saveIfNeeded();
}
}
if (changed) {
if (mSwitchedKeyboardLayoutToast != null) {
mSwitchedKeyboardLayoutToast.cancel();
mSwitchedKeyboardLayoutToast = null;
}
if (keyboardLayoutDescriptor != null) {
KeyboardLayout keyboardLayout = getKeyboardLayout(keyboardLayoutDescriptor);
if (keyboardLayout != null) {
mSwitchedKeyboardLayoutToast = Toast.makeText(
mContext, keyboardLayout.getLabel(), Toast.LENGTH_SHORT);
mSwitchedKeyboardLayoutToast.show();
}
}
reloadKeyboardLayouts();
}
}
}
@Nullable
public String[] getKeyboardLayoutOverlay(InputDeviceIdentifier identifier) {
String keyboardLayoutDescriptor;
if (useNewSettingsUi()) {
if (mCurrentImeInfo == null) {
// Haven't received onInputMethodSubtypeChanged() callback from IMMS. Will reload
// keyboard layouts once we receive the callback.
return null;
}
keyboardLayoutDescriptor = getKeyboardLayoutForInputDeviceInternal(identifier,
mCurrentImeInfo);
} else {
keyboardLayoutDescriptor = getCurrentKeyboardLayoutForInputDevice(identifier);
}
if (keyboardLayoutDescriptor == null) {
return null;
}
final String[] result = new String[2];
visitKeyboardLayout(keyboardLayoutDescriptor,
(resources, keyboardLayoutResId, layout) -> {
try (InputStreamReader stream = new InputStreamReader(
resources.openRawResource(keyboardLayoutResId))) {
result[0] = layout.getDescriptor();
result[1] = Streams.readFully(stream);
} catch (IOException | Resources.NotFoundException ignored) {
}
});
if (result[0] == null) {
Slog.w(TAG, "Could not get keyboard layout with descriptor '"
+ keyboardLayoutDescriptor + "'.");
return null;
}
return result;
}
@Nullable
public String getKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
@UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
@Nullable InputMethodSubtype imeSubtype) {
if (!useNewSettingsUi()) {
Slog.e(TAG, "getKeyboardLayoutForInputDevice() API not supported");
return null;
}
InputMethodSubtypeHandle subtypeHandle = InputMethodSubtypeHandle.of(imeInfo, imeSubtype);
String layout = getKeyboardLayoutForInputDeviceInternal(identifier,
new ImeInfo(userId, subtypeHandle, imeSubtype));
if (DEBUG) {
Slog.d(TAG, "getKeyboardLayoutForInputDevice() " + identifier.toString() + ", userId : "
+ userId + ", subtypeHandle = " + subtypeHandle + " -> " + layout);
}
return layout;
}
public void setKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
@UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
@Nullable InputMethodSubtype imeSubtype,
String keyboardLayoutDescriptor) {
if (!useNewSettingsUi()) {
Slog.e(TAG, "setKeyboardLayoutForInputDevice() API not supported");
return;
}
Objects.requireNonNull(keyboardLayoutDescriptor,
"keyboardLayoutDescriptor must not be null");
String key = createLayoutKey(identifier, userId,
InputMethodSubtypeHandle.of(imeInfo, imeSubtype));
synchronized (mDataStore) {
try {
// Key for storing into data store = <device descriptor>,<userId>,<subtypeHandle>
if (mDataStore.setKeyboardLayout(getLayoutDescriptor(identifier), key,
keyboardLayoutDescriptor)) {
if (DEBUG) {
Slog.d(TAG, "setKeyboardLayoutForInputDevice() " + identifier
+ " key: " + key
+ " keyboardLayoutDescriptor: " + keyboardLayoutDescriptor);
}
mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS);
}
} finally {
mDataStore.saveIfNeeded();
}
}
}
public KeyboardLayout[] getKeyboardLayoutListForInputDevice(InputDeviceIdentifier identifier,
@UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
@Nullable InputMethodSubtype imeSubtype) {
if (!useNewSettingsUi()) {
Slog.e(TAG, "getKeyboardLayoutListForInputDevice() API not supported");
return new KeyboardLayout[0];
}
return getKeyboardLayoutListForInputDeviceInternal(identifier, new ImeInfo(userId,
InputMethodSubtypeHandle.of(imeInfo, imeSubtype), imeSubtype));
}
private KeyboardLayout[] getKeyboardLayoutListForInputDeviceInternal(
InputDeviceIdentifier identifier, ImeInfo imeInfo) {
String key = createLayoutKey(identifier, imeInfo.mUserId, imeInfo.mImeSubtypeHandle);
// Fetch user selected layout and always include it in layout list.
String userSelectedLayout;
synchronized (mDataStore) {
userSelectedLayout = mDataStore.getKeyboardLayout(getLayoutDescriptor(identifier), key);
}
final ArrayList<KeyboardLayout> potentialLayouts = new ArrayList<>();
String imeLanguageTag;
if (imeInfo.mImeSubtype == null) {
imeLanguageTag = "";
} else {
ULocale imeLocale = imeInfo.mImeSubtype.getPhysicalKeyboardHintLanguageTag();
imeLanguageTag = imeLocale != null ? imeLocale.toLanguageTag()
: imeInfo.mImeSubtype.getCanonicalizedLanguageTag();
}
visitAllKeyboardLayouts(new KeyboardLayoutVisitor() {
boolean mDeviceSpecificLayoutAvailable;
@Override
public void visitKeyboardLayout(Resources resources,
int keyboardLayoutResId, KeyboardLayout layout) {
// Next find any potential layouts that aren't yet enabled for the device. For
// devices that have special layouts we assume there's a reason that the generic
// layouts don't work for them, so we don't want to return them since it's likely
// to result in a poor user experience.
if (layout.getVendorId() == identifier.getVendorId()
&& layout.getProductId() == identifier.getProductId()) {
if (!mDeviceSpecificLayoutAvailable) {
mDeviceSpecificLayoutAvailable = true;
potentialLayouts.clear();
}
potentialLayouts.add(layout);
} else if (layout.getVendorId() == -1 && layout.getProductId() == -1
&& !mDeviceSpecificLayoutAvailable && isLayoutCompatibleWithLanguageTag(
layout, imeLanguageTag)) {
potentialLayouts.add(layout);
} else if (layout.getDescriptor().equals(userSelectedLayout)) {
potentialLayouts.add(layout);
}
}
});
// Sort the Keyboard layouts. This is done first by priority then by label. So, system
// layouts will come above 3rd party layouts.
Collections.sort(potentialLayouts);
return potentialLayouts.toArray(new KeyboardLayout[0]);
}
public void onInputMethodSubtypeChanged(@UserIdInt int userId,
@Nullable InputMethodSubtypeHandle subtypeHandle,
@Nullable InputMethodSubtype subtype) {
if (!useNewSettingsUi()) {
Slog.e(TAG, "onInputMethodSubtypeChanged() API not supported");
return;
}
if (subtypeHandle == null) {
if (DEBUG) {
Slog.d(TAG, "No InputMethod is running, ignoring change");
}
return;
}
if (mCurrentImeInfo == null || !subtypeHandle.equals(mCurrentImeInfo.mImeSubtypeHandle)
|| mCurrentImeInfo.mUserId != userId) {
mCurrentImeInfo = new ImeInfo(userId, subtypeHandle, subtype);
mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS);
if (DEBUG) {
Slog.d(TAG, "InputMethodSubtype changed: userId=" + userId
+ " subtypeHandle=" + subtypeHandle);
}
}
}
@Nullable
private String getKeyboardLayoutForInputDeviceInternal(InputDeviceIdentifier identifier,
ImeInfo imeInfo) {
InputDevice inputDevice = getInputDevice(identifier);
if (inputDevice == null || inputDevice.isVirtual() || !inputDevice.isFullKeyboard()) {
return null;
}
String key = createLayoutKey(identifier, imeInfo.mUserId, imeInfo.mImeSubtypeHandle);
String layout;
synchronized (mDataStore) {
layout = mDataStore.getKeyboardLayout(getLayoutDescriptor(identifier), key);
}
if (layout == null) {
synchronized (mKeyboardLayoutCache) {
// Check Auto-selected layout cache to see if layout had been previously selected
if (mKeyboardLayoutCache.containsKey(key)) {
layout = mKeyboardLayoutCache.get(key);
} else {
// NOTE: This list is already filtered based on IME Script code
KeyboardLayout[] layoutList = getKeyboardLayoutListForInputDeviceInternal(
identifier, imeInfo);
// Call auto-matching algorithm to find the best matching layout
layout = getDefaultKeyboardLayoutBasedOnImeInfo(inputDevice, imeInfo,
layoutList);
mKeyboardLayoutCache.put(key, layout);
}
}
}
return layout;
}
@Nullable
private static String getDefaultKeyboardLayoutBasedOnImeInfo(InputDevice inputDevice,
ImeInfo imeInfo, KeyboardLayout[] layoutList) {
if (imeInfo.mImeSubtypeHandle == null) {
return null;
}
Arrays.sort(layoutList);
// Check <VendorID, ProductID> matching for explicitly declared custom KCM files.
for (KeyboardLayout layout : layoutList) {
if (layout.getVendorId() == inputDevice.getVendorId()
&& layout.getProductId() == inputDevice.getProductId()) {
if (DEBUG) {
Slog.d(TAG,
"getDefaultKeyboardLayoutBasedOnImeInfo() : Layout found based on "
+ "vendor and product Ids. " + inputDevice.getIdentifier()
+ " : " + layout.getDescriptor());
}
return layout.getDescriptor();
}
}
// Check layout type, language tag information from InputDevice for matching
String inputLanguageTag = inputDevice.getKeyboardLanguageTag();
if (inputLanguageTag != null) {
String layoutDesc = getMatchingLayoutForProvidedLanguageTagAndLayoutType(layoutList,
inputLanguageTag, inputDevice.getKeyboardLayoutType());
if (layoutDesc != null) {
if (DEBUG) {
Slog.d(TAG,
"getDefaultKeyboardLayoutBasedOnImeInfo() : Layout found based on "
+ "HW information (Language tag and Layout type). "
+ inputDevice.getIdentifier() + " : " + layoutDesc);
}
return layoutDesc;
}
}
InputMethodSubtype subtype = imeInfo.mImeSubtype;
// Can't auto select layout based on IME if subtype or language tag is null
if (subtype == null) {
return null;
}
// Check layout type, language tag information from IME for matching
ULocale pkLocale = subtype.getPhysicalKeyboardHintLanguageTag();
String pkLanguageTag =
pkLocale != null ? pkLocale.toLanguageTag() : subtype.getCanonicalizedLanguageTag();
String layoutDesc = getMatchingLayoutForProvidedLanguageTagAndLayoutType(layoutList,
pkLanguageTag, subtype.getPhysicalKeyboardHintLayoutType());
if (DEBUG) {
Slog.d(TAG,
"getDefaultKeyboardLayoutBasedOnImeInfo() : Layout found based on "
+ "IME locale matching. " + inputDevice.getIdentifier() + " : "
+ layoutDesc);
}
return layoutDesc;
}
@Nullable
private static String getMatchingLayoutForProvidedLanguageTagAndLayoutType(
KeyboardLayout[] layoutList, @NonNull String languageTag, @Nullable String layoutType) {
if (layoutType == null || !KeyboardLayout.isLayoutTypeValid(layoutType)) {
layoutType = KeyboardLayout.LAYOUT_TYPE_UNDEFINED;
}
List<KeyboardLayout> layoutsFilteredByLayoutType = new ArrayList<>();
for (KeyboardLayout layout : layoutList) {
if (layout.getLayoutType().equals(layoutType)) {
layoutsFilteredByLayoutType.add(layout);
}
}
String layoutDesc = getMatchingLayoutForProvidedLanguageTag(layoutsFilteredByLayoutType,
languageTag);
if (layoutDesc != null) {
return layoutDesc;
}
return getMatchingLayoutForProvidedLanguageTag(Arrays.asList(layoutList), languageTag);
}
@Nullable
private static String getMatchingLayoutForProvidedLanguageTag(List<KeyboardLayout> layoutList,
@NonNull String languageTag) {
Locale locale = Locale.forLanguageTag(languageTag);
String layoutMatchingLanguage = null;
String layoutMatchingLanguageAndCountry = null;
for (KeyboardLayout layout : layoutList) {
final LocaleList locales = layout.getLocales();
for (int i = 0; i < locales.size(); i++) {
final Locale l = locales.get(i);
if (l == null) {
continue;
}
if (l.getLanguage().equals(locale.getLanguage())) {
if (layoutMatchingLanguage == null) {
layoutMatchingLanguage = layout.getDescriptor();
}
if (l.getCountry().equals(locale.getCountry())) {
if (layoutMatchingLanguageAndCountry == null) {
layoutMatchingLanguageAndCountry = layout.getDescriptor();
}
if (l.getVariant().equals(locale.getVariant())) {
return layout.getDescriptor();
}
}
}
}
}
return layoutMatchingLanguageAndCountry != null
? layoutMatchingLanguageAndCountry : layoutMatchingLanguage;
}
private void reloadKeyboardLayouts() {
if (DEBUG) {
Slog.d(TAG, "Reloading keyboard layouts.");
}
mNative.reloadKeyboardLayouts();
}
private void maybeUpdateNotification() {
if (mConfiguredKeyboards.size() == 0) {
hideKeyboardLayoutNotification();
return;
}
for (int i = 0; i < mConfiguredKeyboards.size(); i++) {
// If we have a keyboard with no selected layouts, we should always show missing
// layout notification even if there are other keyboards that are configured properly.
if (mConfiguredKeyboards.valueAt(i).isEmpty()) {
showMissingKeyboardLayoutNotification();
return;
}
}
showConfiguredKeyboardLayoutNotification();
}
// Must be called on handler.
private void showMissingKeyboardLayoutNotification() {
final Resources r = mContext.getResources();
final String missingKeyboardLayoutNotificationContent = r.getString(
R.string.select_keyboard_layout_notification_message);
if (mConfiguredKeyboards.size() == 1) {
final InputDevice device = getInputDevice(mConfiguredKeyboards.keyAt(0));
if (device == null) {
return;
}
showKeyboardLayoutNotification(
r.getString(
R.string.select_keyboard_layout_notification_title,
device.getName()),
missingKeyboardLayoutNotificationContent,
device);
} else {
showKeyboardLayoutNotification(
r.getString(R.string.select_multiple_keyboards_layout_notification_title),
missingKeyboardLayoutNotificationContent,
null);
}
}
private void showKeyboardLayoutNotification(@NonNull String intentTitle,
@NonNull String intentContent, @Nullable InputDevice targetDevice) {
final NotificationManager notificationManager = mContext.getSystemService(
NotificationManager.class);
if (notificationManager == null) {
return;
}
final Intent intent = new Intent(Settings.ACTION_HARD_KEYBOARD_SETTINGS);
if (targetDevice != null) {
intent.putExtra(Settings.EXTRA_INPUT_DEVICE_IDENTIFIER, targetDevice.getIdentifier());
}
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
| Intent.FLAG_ACTIVITY_CLEAR_TOP);
final PendingIntent keyboardLayoutIntent = PendingIntent.getActivityAsUser(mContext, 0,
intent, PendingIntent.FLAG_IMMUTABLE, null, UserHandle.CURRENT);
Notification notification =
new Notification.Builder(mContext, SystemNotificationChannels.PHYSICAL_KEYBOARD)
.setContentTitle(intentTitle)
.setContentText(intentContent)
.setContentIntent(keyboardLayoutIntent)
.setSmallIcon(R.drawable.ic_settings_language)
.setColor(mContext.getColor(
com.android.internal.R.color.system_notification_accent_color))
.setAutoCancel(true)
.build();
notificationManager.notifyAsUser(null,
SystemMessageProto.SystemMessage.NOTE_SELECT_KEYBOARD_LAYOUT,
notification, UserHandle.ALL);
}
// Must be called on handler.
private void hideKeyboardLayoutNotification() {
NotificationManager notificationManager = mContext.getSystemService(
NotificationManager.class);
if (notificationManager == null) {
return;
}
notificationManager.cancelAsUser(null,
SystemMessageProto.SystemMessage.NOTE_SELECT_KEYBOARD_LAYOUT,
UserHandle.ALL);
}
private void showConfiguredKeyboardLayoutNotification() {
final Resources r = mContext.getResources();
if (mConfiguredKeyboards.size() != 1) {
showKeyboardLayoutNotification(
r.getString(R.string.keyboard_layout_notification_multiple_selected_title),
r.getString(R.string.keyboard_layout_notification_multiple_selected_message),
null);
return;
}
final InputDevice inputDevice = getInputDevice(mConfiguredKeyboards.keyAt(0));
final Set<String> selectedLayouts = mConfiguredKeyboards.valueAt(0);
if (inputDevice == null || selectedLayouts == null || selectedLayouts.isEmpty()) {
return;
}
showKeyboardLayoutNotification(
r.getString(
R.string.keyboard_layout_notification_selected_title,
inputDevice.getName()),
createConfiguredNotificationText(mContext, selectedLayouts),
inputDevice);
}
private String createConfiguredNotificationText(@NonNull Context context,
@NonNull Set<String> selectedLayouts) {
final Resources r = context.getResources();
List<String> layoutNames = new ArrayList<>();
selectedLayouts.forEach(
(layoutDesc) -> layoutNames.add(getKeyboardLayout(layoutDesc).getLabel()));
Collections.sort(layoutNames);
switch (layoutNames.size()) {
case 1:
return r.getString(R.string.keyboard_layout_notification_one_selected_message,
layoutNames.get(0));
case 2:
return r.getString(R.string.keyboard_layout_notification_two_selected_message,
layoutNames.get(0), layoutNames.get(1));
case 3:
return r.getString(R.string.keyboard_layout_notification_three_selected_message,
layoutNames.get(0), layoutNames.get(1), layoutNames.get(2));
default:
return r.getString(
R.string.keyboard_layout_notification_more_than_three_selected_message,
layoutNames.get(0), layoutNames.get(1), layoutNames.get(2));
}
}
private boolean handleMessage(Message msg) {
switch (msg.what) {
case MSG_UPDATE_EXISTING_DEVICES:
// Circle through all the already added input devices
// Need to do it on handler thread and not block IMS thread
for (int deviceId : (int[]) msg.obj) {
onInputDeviceAdded(deviceId);
}
return true;
case MSG_SWITCH_KEYBOARD_LAYOUT:
handleSwitchKeyboardLayout(msg.arg1, msg.arg2);
return true;
case MSG_RELOAD_KEYBOARD_LAYOUTS:
reloadKeyboardLayouts();
return true;
case MSG_UPDATE_KEYBOARD_LAYOUTS:
updateKeyboardLayouts();
return true;
default:
return false;
}
}
private boolean useNewSettingsUi() {
return FeatureFlagUtils.isEnabled(mContext, FeatureFlagUtils.SETTINGS_NEW_KEYBOARD_UI);
}
@Nullable
private InputDevice getInputDevice(int deviceId) {
InputManager inputManager = mContext.getSystemService(InputManager.class);
return inputManager != null ? inputManager.getInputDevice(deviceId) : null;
}
@Nullable
private InputDevice getInputDevice(InputDeviceIdentifier identifier) {
InputManager inputManager = mContext.getSystemService(InputManager.class);
return inputManager != null ? inputManager.getInputDeviceByDescriptor(
identifier.getDescriptor()) : null;
}
private List<ImeInfo> getImeInfoListForLayoutMapping() {
List<ImeInfo> imeInfoList = new ArrayList<>();
UserManager userManager = Objects.requireNonNull(
mContext.getSystemService(UserManager.class));
InputMethodManager inputMethodManager = Objects.requireNonNull(
mContext.getSystemService(InputMethodManager.class));
for (UserHandle userHandle : userManager.getUserHandles(true /* excludeDying */)) {
int userId = userHandle.getIdentifier();
for (InputMethodInfo imeInfo : inputMethodManager.getEnabledInputMethodListAsUser(
userId)) {
for (InputMethodSubtype imeSubtype :
inputMethodManager.getEnabledInputMethodSubtypeList(
imeInfo, true /* allowsImplicitlyEnabledSubtypes */)) {
if (!imeSubtype.isSuitableForPhysicalKeyboardLayoutMapping()) {
continue;
}
imeInfoList.add(
new ImeInfo(userId, InputMethodSubtypeHandle.of(imeInfo, imeSubtype),
imeSubtype));
}
}
}
return imeInfoList;
}
private String createLayoutKey(InputDeviceIdentifier identifier, int userId,
@NonNull InputMethodSubtypeHandle subtypeHandle) {
Objects.requireNonNull(subtypeHandle, "subtypeHandle must not be null");
return "layoutDescriptor:" + getLayoutDescriptor(identifier) + ",userId:" + userId
+ ",subtypeHandle:" + subtypeHandle.toStringHandle();
}
private static boolean isLayoutCompatibleWithLanguageTag(KeyboardLayout layout,
@NonNull String languageTag) {
final int[] scriptsFromLanguageTag = UScript.getCode(Locale.forLanguageTag(languageTag));
if (scriptsFromLanguageTag.length == 0) {
// If no scripts inferred from languageTag then allowing the layout
return true;
}
LocaleList locales = layout.getLocales();
if (locales.isEmpty()) {
// KCM file doesn't have an associated language tag. This can be from
// a 3rd party app so need to include it as a potential layout.
return true;
}
for (int i = 0; i < locales.size(); i++) {
final Locale locale = locales.get(i);
if (locale == null) {
continue;
}
int[] scripts = UScript.getCode(locale);
if (scripts != null && haveCommonValue(scripts, scriptsFromLanguageTag)) {
return true;
}
}
return false;
}
private static boolean haveCommonValue(int[] arr1, int[] arr2) {
for (int a1 : arr1) {
for (int a2 : arr2) {
if (a1 == a2) return true;
}
}
return false;
}
private static final class KeyboardLayoutDescriptor {
public String packageName;
public String receiverName;
public String keyboardLayoutName;
public static String format(String packageName,
String receiverName, String keyboardName) {
return packageName + "/" + receiverName + "/" + keyboardName;
}
public static KeyboardLayoutDescriptor parse(String descriptor) {
int pos = descriptor.indexOf('/');
if (pos < 0 || pos + 1 == descriptor.length()) {
return null;
}
int pos2 = descriptor.indexOf('/', pos + 1);
if (pos2 < pos + 2 || pos2 + 1 == descriptor.length()) {
return null;
}
KeyboardLayoutDescriptor result = new KeyboardLayoutDescriptor();
result.packageName = descriptor.substring(0, pos);
result.receiverName = descriptor.substring(pos + 1, pos2);
result.keyboardLayoutName = descriptor.substring(pos2 + 1);
return result;
}
}
private static class ImeInfo {
@UserIdInt int mUserId;
@NonNull InputMethodSubtypeHandle mImeSubtypeHandle;
@Nullable InputMethodSubtype mImeSubtype;
ImeInfo(@UserIdInt int userId, @NonNull InputMethodSubtypeHandle imeSubtypeHandle,
@Nullable InputMethodSubtype imeSubtype) {
mUserId = userId;
mImeSubtypeHandle = imeSubtypeHandle;
mImeSubtype = imeSubtype;
}
}
private interface KeyboardLayoutVisitor {
void visitKeyboardLayout(Resources resources,
int keyboardLayoutResId, KeyboardLayout layout);
}
}