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

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

Add APIs for adding custom input gestures(keyboard shortcuts)

DD: go/customizable_shortcuts
PRD: go/custom-kb-shortcuts

Part 1 of chain of CLs for customizable shortcuts:
- Adds APIs
- In memory storage for custom input gestures

Bug: 365064144
Test: atest InputTests:CustomInputGestureManagerTests
Flag: com.android.hardware.input.enable_customizable_input_gestures
Change-Id: I3578226db9a740db5f44457432e88aef94f12d4e
parent 367acd63
Loading
Loading
Loading
Loading
+31 −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 android.hardware.input;

/** @hide */
@JavaDerive(equals=true)
parcelable AidlInputGestureData {
    int keycode;
    int modifierState;
    int gestureType;

    // App launch parameters: Only set if gestureType is KEY_GESTURE_TYPE_LAUNCH_APPLICATION
    String appLaunchCategory;
    String appLaunchRole;
    String appLaunchPackageName;
    String appLaunchClassName;
}
+18 −0
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
package android.hardware.input;

import android.graphics.Rect;
import android.hardware.input.AidlInputGestureData;
import android.hardware.input.HostUsiVersion;
import android.hardware.input.InputDeviceIdentifier;
import android.hardware.input.KeyboardLayout;
@@ -261,4 +262,21 @@ interface IInputManager {
    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
            + "android.Manifest.permission.MANAGE_KEY_GESTURES)")
    void unregisterKeyGestureHandler(IKeyGestureHandler handler);

    @PermissionManuallyEnforced
    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
            + "android.Manifest.permission.MANAGE_KEY_GESTURES)")
    int addCustomInputGesture(in AidlInputGestureData data);

    @PermissionManuallyEnforced
    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
            + "android.Manifest.permission.MANAGE_KEY_GESTURES)")
    int removeCustomInputGesture(in AidlInputGestureData data);

    @PermissionManuallyEnforced
    @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = "
            + "android.Manifest.permission.MANAGE_KEY_GESTURES)")
    void removeAllCustomInputGestures();

    AidlInputGestureData[] getCustomInputGestures();
}
+249 −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 android.hardware.input;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.view.KeyEvent;

import java.util.Objects;

/**
 * Data class to store input gesture data.
 *
 * <p>
 * All input gestures are of type Trigger -> Action(Key gesture type, app data). And currently types
 * of triggers supported are:
 * - KeyTrigger (Keycode + modifierState)
 * - TODO(b/365064144): Add Touchpad gesture based trigger
 * </p>
 * @hide
 */
public final class InputGestureData {

    @NonNull
    private final AidlInputGestureData mInputGestureData;

    public InputGestureData(AidlInputGestureData inputGestureData) {
        this.mInputGestureData = inputGestureData;
        validate();
    }

    /** Returns the trigger information for this input gesture */
    public Trigger getTrigger() {
        if (mInputGestureData.keycode != KeyEvent.KEYCODE_UNKNOWN) {
            return new KeyTrigger(mInputGestureData.keycode, mInputGestureData.modifierState);
        }
        throw new RuntimeException("InputGestureData is corrupted, invalid trigger type!");
    }

    /** Returns the action to perform for this input gesture */
    public Action getAction() {
        return new Action(mInputGestureData.gestureType, getAppLaunchData());
    }

    private void validate() {
        Trigger trigger = getTrigger();
        Action action = getAction();
        if (trigger == null) {
            throw new IllegalArgumentException("No trigger found");
        }
        if (action.keyGestureType == KeyGestureEvent.KEY_GESTURE_TYPE_UNSPECIFIED) {
            throw new IllegalArgumentException("No system action found");
        }
        if (action.keyGestureType == KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION
                && action.appLaunchData == null) {
            throw new IllegalArgumentException(
                    "No app launch data for system action launch application");
        }
    }

    public AidlInputGestureData getAidlData() {
        return mInputGestureData;
    }

    @Nullable
    private AppLaunchData getAppLaunchData() {
        if (mInputGestureData.gestureType != KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION) {
            return null;
        }
        return AppLaunchData.createLaunchData(mInputGestureData.appLaunchCategory,
                mInputGestureData.appLaunchRole, mInputGestureData.appLaunchPackageName,
                mInputGestureData.appLaunchClassName);
    }

    /** Builder class for creating {@link InputGestureData} */
    public static class Builder {
        @Nullable
        private Trigger mTrigger = null;
        private int mKeyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_UNSPECIFIED;
        @Nullable
        private AppLaunchData mAppLaunchData = null;

        /** Set input gesture trigger data for key based gestures */
        public Builder setTrigger(Trigger trigger) {
            mTrigger = trigger;
            return this;
        }

        /** Set input gesture system action */
        public Builder setKeyGestureType(@KeyGestureEvent.KeyGestureType int keyGestureType) {
            mKeyGestureType = keyGestureType;
            return this;
        }

        /** Set input gesture system action as launching a target app */
        public Builder setAppLaunchData(@NonNull AppLaunchData appLaunchData) {
            mKeyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION;
            mAppLaunchData = appLaunchData;
            return this;
        }

        /** Creates {@link android.hardware.input.InputGestureData} based on data provided */
        public InputGestureData build() throws IllegalArgumentException {
            if (mTrigger == null) {
                throw new IllegalArgumentException("No trigger found");
            }
            if (mKeyGestureType == KeyGestureEvent.KEY_GESTURE_TYPE_UNSPECIFIED) {
                throw new IllegalArgumentException("No system action found");
            }
            if (mKeyGestureType == KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION
                    && mAppLaunchData == null) {
                throw new IllegalArgumentException(
                        "No app launch data for system action launch application");
            }
            AidlInputGestureData data = new AidlInputGestureData();
            if (mTrigger instanceof KeyTrigger keyTrigger) {
                data.keycode = keyTrigger.getKeycode();
                data.modifierState = keyTrigger.getModifierState();
            } else {
                throw new IllegalArgumentException("Invalid trigger type!");
            }
            data.gestureType = mKeyGestureType;
            if (mAppLaunchData != null) {
                if (mAppLaunchData instanceof AppLaunchData.CategoryData categoryData) {
                    data.appLaunchCategory = categoryData.getCategory();
                } else if (mAppLaunchData instanceof AppLaunchData.RoleData roleData) {
                    data.appLaunchRole = roleData.getRole();
                } else if (mAppLaunchData instanceof AppLaunchData.ComponentData componentData) {
                    data.appLaunchPackageName = componentData.getPackageName();
                    data.appLaunchClassName = componentData.getClassName();
                } else {
                    throw new IllegalArgumentException("AppLaunchData type is invalid!");
                }
            }
            return new InputGestureData(data);
        }
    }

    @Override
    public String toString() {
        return "InputGestureData { "
                + "trigger = " + getTrigger()
                + ", action = " + getAction()
                + " }";
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        InputGestureData that = (InputGestureData) o;
        return mInputGestureData.keycode == that.mInputGestureData.keycode
                && mInputGestureData.modifierState == that.mInputGestureData.modifierState
                && mInputGestureData.gestureType == that.mInputGestureData.gestureType
                && Objects.equals(mInputGestureData.appLaunchCategory, that.mInputGestureData.appLaunchCategory)
                && Objects.equals(mInputGestureData.appLaunchRole, that.mInputGestureData.appLaunchRole)
                && Objects.equals(mInputGestureData.appLaunchPackageName, that.mInputGestureData.appLaunchPackageName)
                && Objects.equals(mInputGestureData.appLaunchPackageName, that.mInputGestureData.appLaunchPackageName);
    }

    @Override
    public int hashCode() {
        int _hash = 1;
        _hash = 31 * _hash + mInputGestureData.keycode;
        _hash = 31 * _hash + mInputGestureData.modifierState;
        _hash = 31 * _hash + mInputGestureData.gestureType;
        _hash = 31 * _hash + (mInputGestureData.appLaunchCategory != null
                ? mInputGestureData.appLaunchCategory.hashCode() : 0);
        _hash = 31 * _hash + (mInputGestureData.appLaunchRole != null
                ? mInputGestureData.appLaunchRole.hashCode() : 0);
        _hash = 31 * _hash + (mInputGestureData.appLaunchPackageName != null
                ? mInputGestureData.appLaunchPackageName.hashCode() : 0);
        _hash = 31 * _hash + (mInputGestureData.appLaunchPackageName != null
                ? mInputGestureData.appLaunchPackageName.hashCode() : 0);
        return _hash;
    }

    public interface Trigger {
    }

    /** Creates a input gesture trigger based on a key press */
    public static Trigger createKeyTrigger(int keycode, int modifierState) {
        return new KeyTrigger(keycode, modifierState);
    }

    /** Key based input gesture trigger */
    public static class KeyTrigger implements Trigger {
        private static final int SHORTCUT_META_MASK =
                KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON
                        | KeyEvent.META_SHIFT_ON;
        private final int mKeycode;
        private final int mModifierState;

        private KeyTrigger(int keycode, int modifierState) {
            if (keycode <= KeyEvent.KEYCODE_UNKNOWN || keycode > KeyEvent.getMaxKeyCode()) {
                throw new IllegalArgumentException("Invalid keycode = " + keycode);
            }
            mKeycode = keycode;
            mModifierState = modifierState;
        }

        public int getKeycode() {
            return mKeycode;
        }

        public int getModifierState() {
            return mModifierState;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof KeyTrigger that)) return false;
            return mKeycode == that.mKeycode && mModifierState == that.mModifierState;
        }

        @Override
        public int hashCode() {
            return Objects.hash(mKeycode, mModifierState);
        }

        @Override
        public String toString() {
            return "KeyTrigger{" +
                    "mKeycode=" + KeyEvent.keyCodeToString(mKeycode) +
                    ", mModifierState=" + mModifierState +
                    '}';
        }
    }

    /** Data for action to perform when input gesture is triggered */
    public record Action(@KeyGestureEvent.KeyGestureType int keyGestureType,
                         @Nullable AppLaunchData appLaunchData) {
    }
}
+125 −0
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package android.hardware.input;

import static com.android.input.flags.Flags.FLAG_INPUT_DEVICE_VIEW_BEHAVIOR_API;
import static com.android.input.flags.Flags.FLAG_DEVICE_ASSOCIATIONS;
import static com.android.hardware.input.Flags.enableCustomizableInputGestures;
import static com.android.hardware.input.Flags.keyboardLayoutPreviewFlag;
import static com.android.hardware.input.Flags.keyboardGlyphMap;

@@ -257,6 +258,52 @@ public final class InputManager {
        int REMAPPABLE_MODIFIER_KEY_CAPS_LOCK = KeyEvent.KEYCODE_CAPS_LOCK;
    }

    /**
     * Custom input gesture error: Input gesture already exists
     *
     * @hide
     */
    public static final int CUSTOM_INPUT_GESTURE_RESULT_SUCCESS = 1;

    /**
     * Custom input gesture error: Input gesture already exists
     *
     * @hide
     */
    public static final int CUSTOM_INPUT_GESTURE_RESULT_ERROR_ALREADY_EXISTS = 2;

    /**
     * Custom input gesture error: Input gesture does not exist
     *
     * @hide
     */
    public static final int CUSTOM_INPUT_GESTURE_RESULT_ERROR_DOES_NOT_EXIST = 3;

    /**
     * Custom input gesture error: Input gesture is reserved for system action
     *
     * @hide
     */
    public static final int CUSTOM_INPUT_GESTURE_RESULT_ERROR_RESERVED_GESTURE = 4;

    /**
     * Custom input gesture error: Failure error code for all other errors/warnings
     *
     * @hide
     */
    public static final int CUSTOM_INPUT_GESTURE_RESULT_ERROR_OTHER = 5;

    /** @hide */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef(prefix = { "CUSTOM_INPUT_GESTURE_RESULT_" }, value = {
            CUSTOM_INPUT_GESTURE_RESULT_SUCCESS,
            CUSTOM_INPUT_GESTURE_RESULT_ERROR_ALREADY_EXISTS,
            CUSTOM_INPUT_GESTURE_RESULT_ERROR_DOES_NOT_EXIST,
            CUSTOM_INPUT_GESTURE_RESULT_ERROR_RESERVED_GESTURE,
            CUSTOM_INPUT_GESTURE_RESULT_ERROR_OTHER,
    })
    public @interface CustomInputGestureResult {}

    /**
     * Switch State: Unknown.
     *
@@ -1432,6 +1479,84 @@ public final class InputManager {
        mGlobal.unregisterKeyGestureEventHandler(handler);
    }

    /** Adds a new custom input gesture
     *
     * @param inputGestureData gesture data to add as custom gesture
     *
     * @hide
     */
    @RequiresPermission(Manifest.permission.MANAGE_KEY_GESTURES)
    @CustomInputGestureResult
    public int addCustomInputGesture(@NonNull InputGestureData inputGestureData) {
        if (!enableCustomizableInputGestures()) {
            return CUSTOM_INPUT_GESTURE_RESULT_ERROR_OTHER;
        }
        try {
            return mIm.addCustomInputGesture(inputGestureData.getAidlData());
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
        }
        return CUSTOM_INPUT_GESTURE_RESULT_ERROR_OTHER;
    }

    /** Removes an existing custom gesture
     *
     * <p> NOTE: Should not be used to remove system gestures. This API is only to be used to
     * remove gestures added using {@link #addCustomInputGesture(InputGestureData)}
     *
     * @param inputGestureData gesture data for the existing custom gesture to remove
     *
     * @hide
     */
    @RequiresPermission(Manifest.permission.MANAGE_KEY_GESTURES)
    @CustomInputGestureResult
    public int removeCustomInputGesture(@NonNull InputGestureData inputGestureData) {
        if (!enableCustomizableInputGestures()) {
            return CUSTOM_INPUT_GESTURE_RESULT_ERROR_OTHER;
        }
        try {
            return mIm.removeCustomInputGesture(inputGestureData.getAidlData());
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
        }
        return CUSTOM_INPUT_GESTURE_RESULT_ERROR_OTHER;
    }

    /** Removes all custom input gestures
     *
     * @hide
     */
    @RequiresPermission(Manifest.permission.MANAGE_KEY_GESTURES)
    public void removeAllCustomInputGestures() {
        if (!enableCustomizableInputGestures()) {
            return;
        }
        try {
            mIm.removeAllCustomInputGestures();
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
        }
    }

    /** Get all custom input gestures
     *
     * @hide
     */
    public List<InputGestureData> getCustomInputGestures() {
        List<InputGestureData> result = new ArrayList<>();
        if (!enableCustomizableInputGestures()) {
            return result;
        }
        try {
            for (AidlInputGestureData data : mIm.getCustomInputGestures()) {
                result.add(new InputGestureData(data));
            }
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
        }
        return result;
    }

    /**
     * A callback used to be notified about battery state changes for an input device. The
     * {@link #onBatteryStateChanged(int, long, BatteryState)} method will be called once after the
+7 −0
Original line number Diff line number Diff line
@@ -142,6 +142,13 @@ flag {
  bug: "373458181"
}

flag {
    name: "enable_customizable_input_gestures"
    namespace: "input"
    description: "Enables keyboard shortcut customization support"
    bug: "365064144"
}

flag {
  name: "override_power_key_behavior_in_focused_window"
  namespace: "input_native"
Loading