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

Commit b1e17a3f authored by David Padlipsky's avatar David Padlipsky
Browse files

Persist and load input gestures from disk

Saves custom key and touchpad input gestures to disk and loads them in
future sessions. The gestures are stored in an XML file in the system_de
directory per-user.

The same XML format will be used to implement backup and restore for the
feature as well.

Test: atest InputTests:InputDataStoreTests
Test: atest InputTests:KeyGestureControllerTests
Test: Manually on device rebooting restores custom gestures
Bug: 365064144
Flag: com.android.hardware.input.enable_customizable_input_gestures
Change-Id: I55a875044367efcd3840b975cbe74520794c3ac7
parent 9492fd98
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -43,6 +43,8 @@ public final class KeyGestureEvent {
    private static final int LOG_EVENT_UNSPECIFIED =
            FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__UNSPECIFIED;

    // These values should not change and values should not be re-used as this data is persisted to
    // long term storage and must be kept backwards compatible.
    public static final int KEY_GESTURE_TYPE_UNSPECIFIED = 0;
    public static final int KEY_GESTURE_TYPE_HOME = 1;
    public static final int KEY_GESTURE_TYPE_RECENT_APPS = 2;
+309 −0
Original line number Diff line number Diff line
/*
 * Copyright 2024 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.hardware.input.InputGestureData;
import android.os.Environment;
import android.util.AtomicFile;
import android.util.Slog;
import android.util.SparseArray;
import android.util.Xml;

import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

/**
 * Manages persistent state recorded by the input manager service as a set of XML files.
 * Caller must acquire lock on the data store before accessing it.
 */
public final class InputDataStore {
    private static final String TAG = "InputDataStore";

    private static final String INPUT_MANAGER_DIRECTORY = "input";

    private static final String TAG_ROOT = "root";

    private static final String TAG_INPUT_GESTURE_LIST = "input_gesture_list";
    private static final String TAG_INPUT_GESTURE = "input_gesture";
    private static final String TAG_KEY_TRIGGER = "key_trigger";
    private static final String TAG_TOUCHPAD_TRIGGER = "touchpad_trigger";

    private static final String ATTR_KEY_TRIGGER_KEYCODE = "keycode";
    private static final String ATTR_KEY_TRIGGER_MODIFIER_STATE = "modifiers";
    private static final String ATTR_KEY_GESTURE_TYPE = "key_gesture_type";
    private static final String ATTR_TOUCHPAD_TRIGGER_GESTURE_TYPE = "touchpad_gesture_type";

    private final FileInjector mInputGestureFileInjector;

    public InputDataStore() {
        this(new FileInjector("input_gestures.xml"));
    }

    public InputDataStore(final FileInjector inputGestureFileInjector) {
        mInputGestureFileInjector = inputGestureFileInjector;
    }

    /**
     * Reads from the local disk storage the list of customized input gestures.
     *
     * @param userId The user id to fetch the gestures for.
     * @return List of {@link InputGestureData} which the user previously customized.
     */
    public List<InputGestureData> loadInputGestures(int userId) {
        List<InputGestureData> inputGestureDataList;
        try {
            final InputStream inputStream = mInputGestureFileInjector.openRead(userId);
            inputGestureDataList = readInputGesturesXml(inputStream, false);
            inputStream.close();
        } catch (IOException exception) {
            // In case we are unable to read from the file on disk or another IO operation error,
            // fail gracefully.
            Slog.e(TAG, "Failed to read from " + mInputGestureFileInjector.getAtomicFileForUserId(
                    userId), exception);
            return List.of();
        } catch (Exception exception) {
            // In the case of any other exception, we want it to bubble up as this would be due
            // to malformed trusted XML data.
            throw new RuntimeException(
                    "Failed to read from " + mInputGestureFileInjector.getAtomicFileForUserId(
                            userId), exception);
        }
        return inputGestureDataList;
    }

    /**
     * Writes to the local disk storage the list of customized input gestures provided as a param.
     *
     * @param userId               The user id to store the {@link InputGestureData} list under.
     * @param inputGestureDataList The list of custom input gestures for the given {@code userId}.
     */
    public void saveInputGestures(int userId, List<InputGestureData> inputGestureDataList) {
        FileOutputStream outputStream = null;
        try {
            outputStream = mInputGestureFileInjector.startWrite(userId);
            writeInputGestureXml(outputStream, false, inputGestureDataList);
            mInputGestureFileInjector.finishWrite(userId, outputStream, true);
        } catch (IOException e) {
            Slog.e(TAG,
                    "Failed to write to file " + mInputGestureFileInjector.getAtomicFileForUserId(
                            userId), e);
            mInputGestureFileInjector.finishWrite(userId, outputStream, false);
        }
    }

    @VisibleForTesting
    List<InputGestureData> readInputGesturesXml(InputStream stream, boolean utf8Encoded)
            throws XmlPullParserException, IOException {
        List<InputGestureData> inputGestureDataList = new ArrayList<>();
        TypedXmlPullParser parser;
        if (utf8Encoded) {
            parser = Xml.newFastPullParser();
            parser.setInput(stream, StandardCharsets.UTF_8.name());
        } else {
            parser = Xml.resolvePullParser(stream);
        }
        int type;
        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
            if (type != XmlPullParser.START_TAG) {
                continue;
            }
            final String tag = parser.getName();
            if (TAG_ROOT.equals(tag)) {
                continue;
            }

            if (TAG_INPUT_GESTURE_LIST.equals(tag)) {
                inputGestureDataList.addAll(readInputGestureListFromXml(parser));
            }
        }
        return inputGestureDataList;
    }

    private InputGestureData readInputGestureFromXml(TypedXmlPullParser parser)
            throws XmlPullParserException, IOException, IllegalArgumentException {
        InputGestureData.Builder builder = new InputGestureData.Builder();
        builder.setKeyGestureType(parser.getAttributeInt(null, ATTR_KEY_GESTURE_TYPE));
        int outerDepth = parser.getDepth();
        int type;
        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
            // If the parser has left the initial scope when it was called, break out.
            if (outerDepth > parser.getDepth()) {
                throw new RuntimeException(
                        "Parser has left the initial scope of the tag that was being parsed on "
                                + "line number: "
                                + parser.getLineNumber());
            }

            // If the parser has reached the closing tag for the Input Gesture, break out.
            if (type == XmlPullParser.END_TAG && parser.getName().equals(TAG_INPUT_GESTURE)) {
                break;
            }

            final String tag = parser.getName();
            if (type == XmlPullParser.START_TAG && TAG_KEY_TRIGGER.equals(tag)) {
                builder.setTrigger(InputGestureData.createKeyTrigger(
                        parser.getAttributeInt(null, ATTR_KEY_TRIGGER_KEYCODE),
                        parser.getAttributeInt(null, ATTR_KEY_TRIGGER_MODIFIER_STATE)));
            } else if (type == XmlPullParser.START_TAG && TAG_TOUCHPAD_TRIGGER.equals(tag)) {
                builder.setTrigger(
                        InputGestureData.createTouchpadTrigger(parser.getAttributeInt(null,
                                ATTR_TOUCHPAD_TRIGGER_GESTURE_TYPE)));
            }
        }
        return builder.build();
    }

    private List<InputGestureData> readInputGestureListFromXml(TypedXmlPullParser parser) throws
            XmlPullParserException, IOException {
        List<InputGestureData> inputGestureDataList = new ArrayList<>();
        final int outerDepth = parser.getDepth();
        int type;
        while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
            // If the parser has left the initial scope when it was called, break out.
            if (outerDepth > parser.getDepth()) {
                throw new RuntimeException(
                        "Parser has left the initial scope of the tag that was being parsed on "
                                + "line number: "
                                + parser.getLineNumber());
            }

            // If the parser has reached the closing tag for the Input Gesture List, break out.
            if (type == XmlPullParser.END_TAG && parser.getName().equals(TAG_INPUT_GESTURE_LIST)) {
                break;
            }

            if (type != XmlPullParser.START_TAG) {
                continue;
            }

            final String tag = parser.getName();
            if (TAG_INPUT_GESTURE.equals(tag)) {
                try {
                    inputGestureDataList.add(readInputGestureFromXml(parser));
                } catch (IllegalArgumentException exception) {
                    Slog.w(TAG, "Invalid parameters for input gesture data: ", exception);
                    continue;
                }
            }
        }
        return inputGestureDataList;
    }

    @VisibleForTesting
    void writeInputGestureXml(OutputStream stream, boolean utf8Encoded,
            List<InputGestureData> inputGestureDataList) throws IOException {
        final TypedXmlSerializer serializer;
        if (utf8Encoded) {
            serializer = Xml.newFastSerializer();
            serializer.setOutput(stream, StandardCharsets.UTF_8.name());
        } else {
            serializer = Xml.resolveSerializer(stream);
        }

        serializer.startDocument(null, true);
        serializer.startTag(null, TAG_ROOT);
        writeInputGestureListToXml(serializer, inputGestureDataList);
        serializer.endTag(null, TAG_ROOT);
        serializer.endDocument();
    }

    private void writeInputGestureToXml(TypedXmlSerializer serializer,
            InputGestureData inputGestureData) throws IOException {
        // TODO(b/365064144): Implement storage of AppLaunch data.
        if (inputGestureData.getAction().appLaunchData() != null) {
            return;
        }

        serializer.startTag(null, TAG_INPUT_GESTURE);
        serializer.attributeInt(null, ATTR_KEY_GESTURE_TYPE,
                inputGestureData.getAction().keyGestureType());

        final InputGestureData.Trigger trigger = inputGestureData.getTrigger();
        if (trigger instanceof InputGestureData.KeyTrigger keyTrigger) {
            serializer.startTag(null, TAG_KEY_TRIGGER);
            serializer.attributeInt(null, ATTR_KEY_TRIGGER_KEYCODE, keyTrigger.getKeycode());
            serializer.attributeInt(null, ATTR_KEY_TRIGGER_MODIFIER_STATE,
                    keyTrigger.getModifierState());
            serializer.endTag(null, TAG_KEY_TRIGGER);
        } else if (trigger instanceof InputGestureData.TouchpadTrigger touchpadTrigger) {
            serializer.startTag(null, TAG_TOUCHPAD_TRIGGER);
            serializer.attributeInt(null, ATTR_TOUCHPAD_TRIGGER_GESTURE_TYPE,
                    touchpadTrigger.getTouchpadGestureType());
            serializer.endTag(null, TAG_TOUCHPAD_TRIGGER);
        }

        serializer.endTag(null, TAG_INPUT_GESTURE);
    }

    private void writeInputGestureListToXml(TypedXmlSerializer serializer,
            List<InputGestureData> inputGestureDataList) throws IOException {
        serializer.startTag(null, TAG_INPUT_GESTURE_LIST);
        for (final InputGestureData inputGestureData : inputGestureDataList) {
            writeInputGestureToXml(serializer, inputGestureData);
        }
        serializer.endTag(null, TAG_INPUT_GESTURE_LIST);
    }

    @VisibleForTesting
    static class FileInjector {
        private final SparseArray<AtomicFile> mAtomicFileMap = new SparseArray<>();
        private final String mFileName;

        FileInjector(String fileName) {
            mFileName = fileName;
        }

        InputStream openRead(int userId) throws FileNotFoundException {
            return getAtomicFileForUserId(userId).openRead();
        }

        FileOutputStream startWrite(int userId) throws IOException {
            return getAtomicFileForUserId(userId).startWrite();
        }

        void finishWrite(int userId, FileOutputStream os, boolean success) {
            if (success) {
                getAtomicFileForUserId(userId).finishWrite(os);
            } else {
                getAtomicFileForUserId(userId).failWrite(os);
            }
        }

        AtomicFile getAtomicFileForUserId(int userId) {
            if (!mAtomicFileMap.contains(userId)) {
                mAtomicFileMap.put(userId, new AtomicFile(new File(
                        Environment.buildPath(Environment.getDataSystemDeDirectory(userId),
                                INPUT_MANAGER_DIRECTORY), mFileName)));
            }
            return mAtomicFileMap.get(userId);
        }
    }
}
+6 −1
Original line number Diff line number Diff line
@@ -351,6 +351,9 @@ public class InputManagerService extends IInputManager.Stub
    // Manages loading PointerIcons
    private final PointerIconCache mPointerIconCache;

    // Manages storage and retrieval of input data.
    private final InputDataStore mInputDataStore;

    // Maximum number of milliseconds to wait for input event injection.
    private static final int INJECTION_TIMEOUT_MILLIS = 30 * 1000;

@@ -502,7 +505,9 @@ public class InputManagerService extends IInputManager.Stub
                injector.getUEventManager());
        mKeyboardBacklightController = injector.getKeyboardBacklightController(mNative, mDataStore);
        mStickyModifierStateController = new StickyModifierStateController();
        mKeyGestureController = new KeyGestureController(mContext, injector.getLooper());
        mInputDataStore = new InputDataStore();
        mKeyGestureController = new KeyGestureController(mContext, injector.getLooper(),
                mInputDataStore);
        mKeyboardLedController = new KeyboardLedController(mContext, injector.getLooper(),
                mNative);
        mKeyRemapper = new KeyRemapper(mContext, mNative, mDataStore, injector.getLooper());
+56 −3
Original line number Diff line number Diff line
@@ -92,6 +92,8 @@ final class KeyGestureController {
                    | KeyEvent.META_SHIFT_ON;

    private static final int MSG_NOTIFY_KEY_GESTURE_EVENT = 1;
    private static final int MSG_PERSIST_CUSTOM_GESTURES = 2;
    private static final int MSG_LOAD_CUSTOM_GESTURES = 3;

    // must match: config_settingsKeyBehavior in config.xml
    private static final int SETTINGS_KEY_BEHAVIOR_SETTINGS_ACTIVITY = 0;
@@ -116,6 +118,8 @@ final class KeyGestureController {
    private final SettingsObserver mSettingsObserver;
    private final AppLaunchShortcutManager mAppLaunchShortcutManager;
    private final InputGestureManager mInputGestureManager;
    @GuardedBy("mInputDataStore")
    private final InputDataStore mInputDataStore;
    private static final Object mUserLock = new Object();
    @UserIdInt
    @GuardedBy("mUserLock")
@@ -155,7 +159,7 @@ final class KeyGestureController {
    /** Currently fully consumed key codes per device */
    private final SparseArray<Set<Integer>> mConsumedKeysForDevice = new SparseArray<>();

    KeyGestureController(Context context, Looper looper) {
    KeyGestureController(Context context, Looper looper, InputDataStore inputDataStore) {
        mContext = context;
        mHandler = new Handler(looper, this::handleMessage);
        mSystemPid = Process.myPid();
@@ -175,6 +179,7 @@ final class KeyGestureController {
        mSettingsObserver = new SettingsObserver(mHandler);
        mAppLaunchShortcutManager = new AppLaunchShortcutManager(mContext);
        mInputGestureManager = new InputGestureManager(mContext);
        mInputDataStore = inputDataStore;
        initBehaviors();
        initKeyCombinationRules();
    }
@@ -434,6 +439,13 @@ final class KeyGestureController {
        mSettingsObserver.observe();
        mAppLaunchShortcutManager.systemRunning();
        mInputGestureManager.systemRunning();

        int userId;
        synchronized (mUserLock) {
            userId = mCurrentUserId;
        }
        // Load the system user's input gestures.
        mHandler.obtainMessage(MSG_LOAD_CUSTOM_GESTURES, userId).sendToTarget();
    }

    public boolean interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
@@ -955,6 +967,7 @@ final class KeyGestureController {
        synchronized (mUserLock) {
            mCurrentUserId = userId;
        }
        mHandler.obtainMessage(MSG_LOAD_CUSTOM_GESTURES, userId).sendToTarget();
    }

    @MainThread
@@ -995,6 +1008,17 @@ final class KeyGestureController {
                AidlKeyGestureEvent event = (AidlKeyGestureEvent) msg.obj;
                notifyKeyGestureEvent(event);
                break;
            case MSG_PERSIST_CUSTOM_GESTURES: {
                final int userId = (Integer) msg.obj;
                persistInputGestures(userId);
                break;
            }
            case MSG_LOAD_CUSTOM_GESTURES: {
                final int userId = (Integer) msg.obj;
                loadInputGestures(userId);
                break;
            }

        }
        return true;
    }
@@ -1040,22 +1064,31 @@ final class KeyGestureController {
    @InputManager.CustomInputGestureResult
    public int addCustomInputGesture(@UserIdInt int userId,
            @NonNull AidlInputGestureData inputGestureData) {
        return mInputGestureManager.addCustomInputGesture(userId,
        final int result = mInputGestureManager.addCustomInputGesture(userId,
                new InputGestureData(inputGestureData));
        if (result == InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS) {
            mHandler.obtainMessage(MSG_PERSIST_CUSTOM_GESTURES, userId).sendToTarget();
        }
        return result;
    }

    @BinderThread
    @InputManager.CustomInputGestureResult
    public int removeCustomInputGesture(@UserIdInt int userId,
            @NonNull AidlInputGestureData inputGestureData) {
        return mInputGestureManager.removeCustomInputGesture(userId,
        final int result = mInputGestureManager.removeCustomInputGesture(userId,
                new InputGestureData(inputGestureData));
        if (result == InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS) {
            mHandler.obtainMessage(MSG_PERSIST_CUSTOM_GESTURES, userId).sendToTarget();
        }
        return result;
    }

    @BinderThread
    public void removeAllCustomInputGestures(@UserIdInt int userId,
            @Nullable InputGestureData.Filter filter) {
        mInputGestureManager.removeAllCustomInputGestures(userId, filter);
        mHandler.obtainMessage(MSG_PERSIST_CUSTOM_GESTURES, userId).sendToTarget();
    }

    @BinderThread
@@ -1166,6 +1199,26 @@ final class KeyGestureController {
        }
    }

    private void persistInputGestures(int userId) {
        synchronized (mInputDataStore) {
            final List<InputGestureData> inputGestureDataList =
                    mInputGestureManager.getCustomInputGestures(userId,
                            null);
            mInputDataStore.saveInputGestures(userId, inputGestureDataList);
        }
    }

    private void loadInputGestures(int userId) {
        synchronized (mInputDataStore) {
            mInputGestureManager.removeAllCustomInputGestures(userId, null);
            final List<InputGestureData> inputGestureDataList = mInputDataStore.loadInputGestures(
                    userId);
            for (final InputGestureData inputGestureData : inputGestureDataList) {
                mInputGestureManager.addCustomInputGesture(userId, inputGestureData);
            }
        }
    }

    // A record of a registered key gesture event listener from one process.
    private class KeyGestureHandlerRecord implements IBinder.DeathRecipient {
        public final int mPid;
+444 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading