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

Commit 6e52a4de authored by Vaibhav Devmurari's avatar Vaibhav Devmurari Committed by Android (Google) Code Review
Browse files

Merge "Add APIs for adding custom input gestures(keyboard shortcuts)" into main

parents e4864486 da8a45b6
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