Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit eaed646c authored by Vaibhav Devmurari's avatar Vaibhav Devmurari
Browse files

Maintain keyboard configuration and reload layouts if changed

Virtual keyboard creation logic involves 2 steps:
- Create a uinput device
- Updating associated layout info
It is possible that when onInputDeviceAdded() occurs layout info is
not correctly set. So, everytime we get onInputDeviceChanged()
callback and onImeInfoChanged() callback there is a possibility that
keyboard layout needs to be reconfigured. So, keep track of current
configuration and call reload() when a new layouts needs to be set.

Test: atest VirtualKeyboardLayoutTest
Bug: 277778640
Change-Id: I998009d394a2103bf5afb37a622094dd771162d8
parent 77ea277f
Loading
Loading
Loading
Loading
+154 −70
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.server.input;

import android.annotation.AnyThread;
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
@@ -99,6 +101,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
    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 static final int MSG_CURRENT_IME_INFO_CHANGED = 5;

    private final Context mContext;
    private final NativeInputManagerService mNative;
@@ -108,16 +111,17 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
    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<>();
    // selected layout).
    private final SparseArray<KeyboardConfiguration> 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<>();
    private final Object mImeInfoLock = new Object();
    @Nullable
    @GuardedBy("mImeInfoLock")
    private ImeInfo mCurrentImeInfo;

    KeyboardLayoutManager(Context context, NativeInputManagerService nativeService,
@@ -155,26 +159,32 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
    }

    @Override
    @MainThread
    public void onInputDeviceAdded(int deviceId) {
        onInputDeviceChanged(deviceId);
        if (useNewSettingsUi()) {
            // Force native callback to set up keyboard layout overlay for newly added keyboards
            reloadKeyboardLayouts();
        }
    }

    @Override
    @MainThread
    public void onInputDeviceRemoved(int deviceId) {
        mConfiguredKeyboards.remove(deviceId);
        maybeUpdateNotification();
    }

    @Override
    @MainThread
    public void onInputDeviceChanged(int deviceId) {
        final InputDevice inputDevice = getInputDevice(deviceId);
        if (inputDevice == null || inputDevice.isVirtual() || !inputDevice.isFullKeyboard()) {
            return;
        }
        KeyboardConfiguration config = mConfiguredKeyboards.get(deviceId);
        if (config == null) {
            config = new KeyboardConfiguration();
            mConfiguredKeyboards.put(deviceId, config);
        }

        boolean needToShowNotification = false;
        if (!useNewSettingsUi()) {
            synchronized (mDataStore) {
                String layout = getCurrentKeyboardLayoutForInputDevice(inputDevice.getIdentifier());
@@ -182,55 +192,67 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
                    layout = getDefaultKeyboardLayout(inputDevice);
                    if (layout != null) {
                        setCurrentKeyboardLayoutForInputDevice(inputDevice.getIdentifier(), layout);
                    } else {
                        mConfiguredKeyboards.put(inputDevice.getId(), new HashSet<>());
                    }
                }
                config.setCurrentLayout(layout);
                if (layout == null) {
                    // In old settings show notification always until user manually selects a
                    // layout in the settings.
                    needToShowNotification = true;
                }
            }
        } 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;
                    // If even one layout not configured properly, we need to ask user to configure
                    // the keyboard properly from the Settings.
                    selectedLayouts.clear();
                    break;
                }
                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);

            config.setConfiguredLayouts(selectedLayouts);

            // Update current layout: If there is a change then need to reload.
            synchronized (mImeInfoLock) {
                String layout = getKeyboardLayoutForInputDeviceInternal(
                        inputDevice.getIdentifier(), mCurrentImeInfo);
                if (!Objects.equals(layout, config.getCurrentLayout())) {
                    config.setCurrentLayout(layout);
                    mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS);
                }
            }

            synchronized (mDataStore) {
                try {
                    if (!mDataStore.setSelectedKeyboardLayouts(key, selectedLayouts)) {
                        // No need to show the notification only if layout selection didn't change
                    if (mDataStore.setSelectedKeyboardLayouts(key, selectedLayouts)) {
                        // Need to show the notification only if layout selection changed
                        // from the previous configuration
                        return;
                        needToShowNotification = true;
                    }
                } finally {
                    mDataStore.saveIfNeeded();
                }
            }
        }
        if (needToShowNotification) {
            maybeUpdateNotification();
        }
    }

    private String getDefaultKeyboardLayout(final InputDevice inputDevice) {
        final Locale systemLocale = mContext.getResources().getConfiguration().locale;
@@ -323,12 +345,14 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
        reloadKeyboardLayouts();
    }

    @AnyThread
    public KeyboardLayout[] getKeyboardLayouts() {
        final ArrayList<KeyboardLayout> list = new ArrayList<>();
        visitAllKeyboardLayouts((resources, keyboardLayoutResId, layout) -> list.add(layout));
        return list.toArray(new KeyboardLayout[0]);
    }

    @AnyThread
    public KeyboardLayout[] getKeyboardLayoutsForInputDevice(
            final InputDeviceIdentifier identifier) {
        if (useNewSettingsUi()) {
@@ -375,6 +399,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
                KeyboardLayout[]::new);
    }

    @AnyThread
    @Nullable
    public KeyboardLayout getKeyboardLayout(String keyboardLayoutDescriptor) {
        Objects.requireNonNull(keyboardLayoutDescriptor,
@@ -543,6 +568,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
        return key.toString();
    }

    @AnyThread
    @Nullable
    public String getCurrentKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier) {
        if (useNewSettingsUi()) {
@@ -566,6 +592,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
        }
    }

    @AnyThread
    public void setCurrentKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
            String keyboardLayoutDescriptor) {
        if (useNewSettingsUi()) {
@@ -592,6 +619,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
        }
    }

    @AnyThread
    public String[] getEnabledKeyboardLayoutsForInputDevice(InputDeviceIdentifier identifier) {
        if (useNewSettingsUi()) {
            Slog.e(TAG, "getEnabledKeyboardLayoutsForInputDevice API not supported");
@@ -608,6 +636,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
        }
    }

    @AnyThread
    public void addKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
            String keyboardLayoutDescriptor) {
        if (useNewSettingsUi()) {
@@ -635,6 +664,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
        }
    }

    @AnyThread
    public void removeKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
            String keyboardLayoutDescriptor) {
        if (useNewSettingsUi()) {
@@ -667,6 +697,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
        }
    }

    @AnyThread
    public void switchKeyboardLayout(int deviceId, int direction) {
        if (useNewSettingsUi()) {
            Slog.e(TAG, "switchKeyboardLayout API not supported");
@@ -675,7 +706,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
        mHandler.obtainMessage(MSG_SWITCH_KEYBOARD_LAYOUT, deviceId, direction).sendToTarget();
    }

    // Must be called on handler.
    @MainThread
    private void handleSwitchKeyboardLayout(int deviceId, int direction) {
        final InputDevice device = getInputDevice(deviceId);
        if (device != null) {
@@ -713,23 +744,14 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
    }

    @Nullable
    @AnyThread
    public String[] getKeyboardLayoutOverlay(InputDeviceIdentifier identifier) {
        String keyboardLayoutDescriptor;
        if (useNewSettingsUi()) {
            InputDevice inputDevice = getInputDevice(identifier);
            if (inputDevice == null) {
                // getKeyboardLayoutOverlay() called before input device added completely. Need
                // to wait till the device is added which will call reloadKeyboardLayouts()
                return null;
            }
            if (mCurrentImeInfo == null) {
                // Haven't received onInputMethodSubtypeChanged() callback from IMMS. Will reload
                // keyboard layouts once we receive the callback.
                return null;
            }

            synchronized (mImeInfoLock) {
                keyboardLayoutDescriptor = getKeyboardLayoutForInputDeviceInternal(identifier,
                        mCurrentImeInfo);
            }
        } else {
            keyboardLayoutDescriptor = getCurrentKeyboardLayoutForInputDevice(identifier);
        }
@@ -755,6 +777,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
        return result;
    }

    @AnyThread
    @Nullable
    public String getKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
            @UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
@@ -773,6 +796,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
        return layout;
    }

    @AnyThread
    public void setKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
            @UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
            @Nullable InputMethodSubtype imeSubtype,
@@ -783,8 +807,8 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
        }
        Objects.requireNonNull(keyboardLayoutDescriptor,
                "keyboardLayoutDescriptor must not be null");
        String key = createLayoutKey(identifier, userId,
                InputMethodSubtypeHandle.of(imeInfo, imeSubtype));
        String key = createLayoutKey(identifier,
                new ImeInfo(userId, InputMethodSubtypeHandle.of(imeInfo, imeSubtype), imeSubtype));
        synchronized (mDataStore) {
            try {
                // Key for storing into data store = <device descriptor>,<userId>,<subtypeHandle>
@@ -803,6 +827,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
        }
    }

    @AnyThread
    public KeyboardLayout[] getKeyboardLayoutListForInputDevice(InputDeviceIdentifier identifier,
            @UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
            @Nullable InputMethodSubtype imeSubtype) {
@@ -815,8 +840,8 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
    }

    private KeyboardLayout[] getKeyboardLayoutListForInputDeviceInternal(
            InputDeviceIdentifier identifier, ImeInfo imeInfo) {
        String key = createLayoutKey(identifier, imeInfo.mUserId, imeInfo.mImeSubtypeHandle);
            InputDeviceIdentifier identifier, @Nullable ImeInfo imeInfo) {
        String key = createLayoutKey(identifier, imeInfo);

        // Fetch user selected layout and always include it in layout list.
        String userSelectedLayout;
@@ -826,7 +851,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {

        final ArrayList<KeyboardLayout> potentialLayouts = new ArrayList<>();
        String imeLanguageTag;
        if (imeInfo.mImeSubtype == null) {
        if (imeInfo == null || imeInfo.mImeSubtype == null) {
            imeLanguageTag = "";
        } else {
            ULocale imeLocale = imeInfo.mImeSubtype.getPhysicalKeyboardHintLanguageTag();
@@ -866,6 +891,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
        return potentialLayouts.toArray(new KeyboardLayout[0]);
    }

    @AnyThread
    public void onInputMethodSubtypeChanged(@UserIdInt int userId,
            @Nullable InputMethodSubtypeHandle subtypeHandle,
            @Nullable InputMethodSubtype subtype) {
@@ -879,25 +905,45 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
            }
            return;
        }
        synchronized (mImeInfoLock) {
            if (mCurrentImeInfo == null || !subtypeHandle.equals(mCurrentImeInfo.mImeSubtypeHandle)
                    || mCurrentImeInfo.mUserId != userId) {
                mCurrentImeInfo = new ImeInfo(userId, subtypeHandle, subtype);
            mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS);
                mHandler.sendEmptyMessage(MSG_CURRENT_IME_INFO_CHANGED);
                if (DEBUG) {
                    Slog.d(TAG, "InputMethodSubtype changed: userId=" + userId
                            + " subtypeHandle=" + subtypeHandle);
                }
            }
        }
    }

    @MainThread
    private void onCurrentImeInfoChanged() {
        synchronized (mImeInfoLock) {
            for (int i = 0; i < mConfiguredKeyboards.size(); i++) {
                InputDevice inputDevice = Objects.requireNonNull(
                        getInputDevice(mConfiguredKeyboards.keyAt(i)));
                String layout = getKeyboardLayoutForInputDeviceInternal(inputDevice.getIdentifier(),
                        mCurrentImeInfo);
                KeyboardConfiguration config = mConfiguredKeyboards.valueAt(i);
                if (!Objects.equals(layout, config.getCurrentLayout())) {
                    config.setCurrentLayout(layout);
                    mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS);
                    return;
                }
            }
        }
    }

    @Nullable
    private String getKeyboardLayoutForInputDeviceInternal(InputDeviceIdentifier identifier,
            ImeInfo imeInfo) {
            @Nullable ImeInfo imeInfo) {
        InputDevice inputDevice = getInputDevice(identifier);
        if (inputDevice == null || inputDevice.isVirtual() || !inputDevice.isFullKeyboard()) {
            return null;
        }
        String key = createLayoutKey(identifier, imeInfo.mUserId, imeInfo.mImeSubtypeHandle);
        String key = createLayoutKey(identifier, imeInfo);
        String layout;
        synchronized (mDataStore) {
            layout = mDataStore.getKeyboardLayout(getLayoutDescriptor(identifier), key);
@@ -923,11 +969,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {

    @Nullable
    private static String getDefaultKeyboardLayoutBasedOnImeInfo(InputDevice inputDevice,
            ImeInfo imeInfo, KeyboardLayout[] layoutList) {
        if (imeInfo.mImeSubtypeHandle == null) {
            return null;
        }

            @Nullable ImeInfo imeInfo, KeyboardLayout[] layoutList) {
        Arrays.sort(layoutList);

        // Check <VendorID, ProductID> matching for explicitly declared custom KCM files.
@@ -961,12 +1003,12 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
            }
        }

        InputMethodSubtype subtype = imeInfo.mImeSubtype;
        // Can't auto select layout based on IME if subtype or language tag is null
        if (subtype == null) {
        if (imeInfo == null || imeInfo.mImeSubtypeHandle == null || imeInfo.mImeSubtype == null) {
            // Can't auto select layout based on IME info is null
            return null;
        }

        InputMethodSubtype subtype = imeInfo.mImeSubtype;
        // Check layout type, language tag information from IME for matching
        ULocale pkLocale = subtype.getPhysicalKeyboardHintLanguageTag();
        String pkLanguageTag =
@@ -1043,6 +1085,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
        mNative.reloadKeyboardLayouts();
    }

    @MainThread
    private void maybeUpdateNotification() {
        if (mConfiguredKeyboards.size() == 0) {
            hideKeyboardLayoutNotification();
@@ -1051,7 +1094,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
        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()) {
            if (!mConfiguredKeyboards.valueAt(i).hasConfiguredLayouts()) {
                showMissingKeyboardLayoutNotification();
                return;
            }
@@ -1059,7 +1102,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
        showConfiguredKeyboardLayoutNotification();
    }

    // Must be called on handler.
    @MainThread
    private void showMissingKeyboardLayoutNotification() {
        final Resources r = mContext.getResources();
        final String missingKeyboardLayoutNotificationContent = r.getString(
@@ -1084,6 +1127,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
        }
    }

    @MainThread
    private void showKeyboardLayoutNotification(@NonNull String intentTitle,
            @NonNull String intentContent, @Nullable InputDevice targetDevice) {
        final NotificationManager notificationManager = mContext.getSystemService(
@@ -1119,7 +1163,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
                notification, UserHandle.ALL);
    }

    // Must be called on handler.
    @MainThread
    private void hideKeyboardLayoutNotification() {
        NotificationManager notificationManager = mContext.getSystemService(
                NotificationManager.class);
@@ -1132,6 +1176,7 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
                UserHandle.ALL);
    }

    @MainThread
    private void showConfiguredKeyboardLayoutNotification() {
        final Resources r = mContext.getResources();

@@ -1144,8 +1189,8 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
        }

        final InputDevice inputDevice = getInputDevice(mConfiguredKeyboards.keyAt(0));
        final Set<String> selectedLayouts = mConfiguredKeyboards.valueAt(0);
        if (inputDevice == null || selectedLayouts == null || selectedLayouts.isEmpty()) {
        final KeyboardConfiguration config = mConfiguredKeyboards.valueAt(0);
        if (inputDevice == null || !config.hasConfiguredLayouts()) {
            return;
        }

@@ -1153,10 +1198,11 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
                r.getString(
                        R.string.keyboard_layout_notification_selected_title,
                        inputDevice.getName()),
                createConfiguredNotificationText(mContext, selectedLayouts),
                createConfiguredNotificationText(mContext, config.getConfiguredLayouts()),
                inputDevice);
    }

    @MainThread
    private String createConfiguredNotificationText(@NonNull Context context,
            @NonNull Set<String> selectedLayouts) {
        final Resources r = context.getResources();
@@ -1199,6 +1245,9 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
            case MSG_UPDATE_KEYBOARD_LAYOUTS:
                updateKeyboardLayouts();
                return true;
            case MSG_CURRENT_IME_INFO_CHANGED:
                onCurrentImeInfoChanged();
                return true;
            default:
                return false;
        }
@@ -1252,11 +1301,13 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
        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 String createLayoutKey(InputDeviceIdentifier identifier, @Nullable ImeInfo imeInfo) {
        if (imeInfo == null) {
            return getLayoutDescriptor(identifier);
        }
        Objects.requireNonNull(imeInfo.mImeSubtypeHandle, "subtypeHandle must not be null");
        return "layoutDescriptor:" + getLayoutDescriptor(identifier) + ",userId:" + imeInfo.mUserId
                + ",subtypeHandle:" + imeInfo.mImeSubtypeHandle.toStringHandle();
    }

    private static boolean isLayoutCompatibleWithLanguageTag(KeyboardLayout layout,
@@ -1350,6 +1401,39 @@ final class KeyboardLayoutManager implements InputManager.InputDeviceListener {
        }
    }

    private static class KeyboardConfiguration {
        // If null or empty, it means no layout is configured for the device. And user needs to
        // manually set up the device.
        @Nullable
        private Set<String> mConfiguredLayouts;

        // If null, it means no layout is selected for the device.
        @Nullable
        private String mCurrentLayout;

        private boolean hasConfiguredLayouts() {
            return mConfiguredLayouts != null && !mConfiguredLayouts.isEmpty();
        }

        @Nullable
        private Set<String> getConfiguredLayouts() {
            return mConfiguredLayouts;
        }

        private void setConfiguredLayouts(Set<String> configuredLayouts) {
            mConfiguredLayouts = configuredLayouts;
        }

        @Nullable
        private String getCurrentLayout() {
            return mCurrentLayout;
        }

        private void setCurrentLayout(String currentLayout) {
            mCurrentLayout = currentLayout;
        }
    }

    private interface KeyboardLayoutVisitor {
        void visitKeyboardLayout(Resources resources,
                int keyboardLayoutResId, KeyboardLayout layout);