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

Commit 695314eb authored by Vaibhav Devmurari's avatar Vaibhav Devmurari
Browse files

Shift App launch shortcut handling to KeyGestureController

As pre requisite to custom shortcut implementation, we need to move
all shortcut handling to input (KeyGestureController) from PWM.

Bug: 358569822
Test: atest KeyGestureControllerTests
Test: atest ModifierShortcutTests
Flag: com.android.hardware.input.use_key_gesture_event_handler
Change-Id: If07bb66a5615faccd8251d70f8ae68bd19ad1b7a
parent ce7e0227
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -279,4 +279,6 @@ interface IInputManager {
    void removeAllCustomInputGestures();

    AidlInputGestureData[] getCustomInputGestures();

    AidlInputGestureData[] getAppLaunchBookmarks();
}
+24 −6
Original line number Diff line number Diff line
@@ -1494,9 +1494,8 @@ public final class InputManager {
        try {
            return mIm.addCustomInputGesture(inputGestureData.getAidlData());
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
            throw e.rethrowFromSystemServer();
        }
        return CUSTOM_INPUT_GESTURE_RESULT_ERROR_OTHER;
    }

    /** Removes an existing custom gesture
@@ -1517,9 +1516,8 @@ public final class InputManager {
        try {
            return mIm.removeCustomInputGesture(inputGestureData.getAidlData());
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
            throw e.rethrowFromSystemServer();
        }
        return CUSTOM_INPUT_GESTURE_RESULT_ERROR_OTHER;
    }

    /** Removes all custom input gestures
@@ -1534,7 +1532,7 @@ public final class InputManager {
        try {
            mIm.removeAllCustomInputGestures();
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
            throw e.rethrowFromSystemServer();
        }
    }

@@ -1552,9 +1550,29 @@ public final class InputManager {
                result.add(new InputGestureData(data));
            }
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
            throw e.rethrowFromSystemServer();
        }
        return result;
    }

    /**
     * Return the set of application launch bookmarks handled by the input framework.
     *
     * @return list of {@link InputGestureData} containing the application launch shortcuts parsed
     * at boot time from {@code bookmarks.xml}.
     *
     * @hide
     */
    public List<InputGestureData> getAppLaunchBookmarks() {
        try {
            List<InputGestureData> result = new ArrayList<>();
            for (AidlInputGestureData data : mIm.getAppLaunchBookmarks()) {
                result.add(new InputGestureData(data));
            }
            return result;
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

    /**
+336 −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.annotation.Nullable;
import android.annotation.SuppressLint;
import android.app.role.RoleManager;
import android.content.Context;
import android.content.Intent;
import android.content.res.XmlResourceParser;
import android.hardware.input.AppLaunchData;
import android.hardware.input.InputGestureData;
import android.hardware.input.KeyGestureEvent;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.LongSparseArray;
import android.util.SparseArray;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;

import com.android.internal.R;
import com.android.internal.policy.IShortcutService;
import com.android.internal.util.XmlUtils;

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

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

/**
 * Manages quick launch app shortcuts by parsing {@code bookmarks.xml} and intercepting the
 * correct key combinations for the app shortcuts defined.
 *
 * Currently there are 2 ways of defining shortcuts:
 * - Adding shortcuts to {@code bookmarks.xml}
 * - Calling into {@code registerShortcutKey()}.
 */
final class AppLaunchShortcutManager {
    private static final String TAG = "AppShortcutManager";

    private static final String TAG_BOOKMARKS = "bookmarks";
    private static final String TAG_BOOKMARK = "bookmark";

    private static final String ATTRIBUTE_PACKAGE = "package";
    private static final String ATTRIBUTE_CLASS = "class";
    private static final String ATTRIBUTE_SHORTCUT = "shortcut";
    private static final String ATTRIBUTE_CATEGORY = "category";
    private static final String ATTRIBUTE_SHIFT = "shift";
    private static final String ATTRIBUTE_ROLE = "role";

    private static final int SHORTCUT_CODE_META_MASK =
            KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON | KeyEvent.META_SHIFT_ON
                    | KeyEvent.META_META_ON;

    private LongSparseArray<IShortcutService> mShortcutKeyServices = new LongSparseArray<>();

    /* Table of Application Launch keys.  Maps from key codes to intent categories.
     *
     * These are special keys that are used to launch particular kinds of applications,
     * such as a web browser.  HID defines nearly a hundred of them in the Consumer (0x0C)
     * usage page.  We don't support quite that many yet...
     */
    private static final SparseArray<String> sApplicationLaunchKeyCategories;
    private static final SparseArray<String> sApplicationLaunchKeyRoles;
    static {
        sApplicationLaunchKeyRoles = new SparseArray<>();
        sApplicationLaunchKeyCategories = new SparseArray<>();
        sApplicationLaunchKeyRoles.append(
                KeyEvent.KEYCODE_EXPLORER, RoleManager.ROLE_BROWSER);
        sApplicationLaunchKeyCategories.append(
                KeyEvent.KEYCODE_ENVELOPE, Intent.CATEGORY_APP_EMAIL);
        sApplicationLaunchKeyCategories.append(
                KeyEvent.KEYCODE_CONTACTS, Intent.CATEGORY_APP_CONTACTS);
        sApplicationLaunchKeyCategories.append(
                KeyEvent.KEYCODE_CALENDAR, Intent.CATEGORY_APP_CALENDAR);
        sApplicationLaunchKeyCategories.append(
                KeyEvent.KEYCODE_MUSIC, Intent.CATEGORY_APP_MUSIC);
        sApplicationLaunchKeyCategories.append(
                KeyEvent.KEYCODE_CALCULATOR, Intent.CATEGORY_APP_CALCULATOR);
    }

    private final Context mContext;
    private boolean mSearchKeyShortcutPending = false;
    private boolean mConsumeSearchKeyUp = true;
    private final Map<InputGestureData.Trigger, InputGestureData> mBookmarks = new HashMap<>();

    @SuppressLint("MissingPermission")
    AppLaunchShortcutManager(Context context) {
        mContext = context;
    }

    public void systemRunning() {
        loadShortcuts();
    }

    private void loadShortcuts() {
        try {
            XmlResourceParser parser = mContext.getResources().getXml(R.xml.bookmarks);
            XmlUtils.beginDocument(parser, TAG_BOOKMARKS);
            KeyCharacterMap virtualKcm = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);

            while (true) {
                XmlUtils.nextElement(parser);

                if (parser.getEventType() == XmlPullParser.END_DOCUMENT) {
                    break;
                }

                if (!TAG_BOOKMARK.equals(parser.getName())) {
                    Log.w(TAG, "TAG_BOOKMARK not found");
                    break;
                }

                String packageName = parser.getAttributeValue(null, ATTRIBUTE_PACKAGE);
                String className = parser.getAttributeValue(null, ATTRIBUTE_CLASS);
                String categoryName = parser.getAttributeValue(null, ATTRIBUTE_CATEGORY);
                String shiftName = parser.getAttributeValue(null, ATTRIBUTE_SHIFT);
                String roleName = parser.getAttributeValue(null, ATTRIBUTE_ROLE);

                // TODO(b/358569822): Shift bookmarks to use keycode instead of shortcutChar
                int keycode = KeyEvent.KEYCODE_UNKNOWN;
                String shortcut = parser.getAttributeValue(null, ATTRIBUTE_SHORTCUT);
                if (!TextUtils.isEmpty(shortcut)) {
                    KeyEvent[] events = virtualKcm.getEvents(new char[]{shortcut.toLowerCase(
                            Locale.ROOT).charAt(0)});
                    // Single key press can generate the character
                    if (events != null && events.length == 2) {
                        keycode = events[0].getKeyCode();
                    }
                }
                if (keycode == KeyEvent.KEYCODE_UNKNOWN) {
                    Log.w(TAG, "Keycode required for bookmark with category=" + categoryName
                            + " packageName=" + packageName + " className=" + className
                            + " role=" + roleName + " shiftName=" + shiftName
                            + " shortcut=" + shortcut);
                    continue;
                }

                final boolean isShiftShortcut = (shiftName != null && shiftName.toLowerCase(
                        Locale.ROOT).equals("true"));
                AppLaunchData launchData = null;
                if (!TextUtils.isEmpty(packageName) && !TextUtils.isEmpty(className)) {
                    launchData = AppLaunchData.createLaunchDataForComponent(packageName, className);
                } else if (!TextUtils.isEmpty(categoryName)) {
                    launchData = AppLaunchData.createLaunchDataForCategory(categoryName);
                } else if (!TextUtils.isEmpty(roleName)) {
                    launchData = AppLaunchData.createLaunchDataForRole(roleName);
                }
                if (launchData != null) {
                    Log.d(TAG, "adding shortcut " + launchData + "shift="
                            + isShiftShortcut + " keycode=" + keycode);
                    // All bookmarks are based on Action key
                    int modifierState =
                            KeyEvent.META_META_ON | (isShiftShortcut ? KeyEvent.META_SHIFT_ON : 0);
                    InputGestureData bookmark = new InputGestureData.Builder()
                            .setTrigger(InputGestureData.createKeyTrigger(keycode, modifierState))
                            .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION)
                            .setAppLaunchData(launchData)
                            .build();
                    mBookmarks.put(bookmark.getTrigger(), bookmark);
                }
            }
        } catch (XmlPullParserException | IOException e) {
            Log.e(TAG, "Got exception parsing bookmarks.", e);
        }
    }

    public void registerShortcutKey(long shortcutCode, IShortcutService shortcutService)
            throws RemoteException {
        IShortcutService service = mShortcutKeyServices.get(shortcutCode);
        if (service != null && service.asBinder().pingBinder()) {
            throw new RemoteException("Key: " + shortcutCode + ", already exists.");
        }

        mShortcutKeyServices.put(shortcutCode, shortcutService);
    }

    /**
     * Handle the shortcut to {@link IShortcutService}
     * @param keyCode The key code of the event.
     * @param metaState The meta key modifier state.
     * @return True if invoked the shortcut, otherwise false.
     */
    private boolean handleShortcutService(int keyCode, int metaState) {
        final long shortcutCodeMeta = metaState & SHORTCUT_CODE_META_MASK;
        if (shortcutCodeMeta == 0) {
            return false;
        }
        long shortcutCode = keyCode | (shortcutCodeMeta << Integer.SIZE);
        IShortcutService shortcutService = mShortcutKeyServices.get(shortcutCode);
        if (shortcutService != null) {
            try {
                shortcutService.notifyShortcutKeyPressed(shortcutCode);
            } catch (RemoteException e) {
                Log.w(TAG,
                        "Shortcut key service not found, deleting shortcut code: " + shortcutCode);
                mShortcutKeyServices.delete(shortcutCode);
            }
            return true;
        }
        return false;
    }

    /**
     * Handle the shortcut to Launch application.
     *
     * @param keyEvent The key event.
     */
    @SuppressLint("MissingPermission")
    @Nullable
    private AppLaunchData interceptShortcut(KeyEvent keyEvent) {
        final int keyCode = keyEvent.getKeyCode();
        final int modifierState = keyEvent.getMetaState() & SHORTCUT_CODE_META_MASK;
        // Shortcuts are invoked through Search+key, so intercept those here
        // Any printing key that is chorded with Search should be consumed
        // even if no shortcut was invoked.  This prevents text from being
        // inadvertently inserted when using a keyboard that has built-in macro
        // shortcut keys (that emit Search+x) and some of them are not registered.
        if (mSearchKeyShortcutPending) {
            KeyCharacterMap kcm = keyEvent.getKeyCharacterMap();
            if (kcm != null && kcm.isPrintingKey(keyCode)) {
                mConsumeSearchKeyUp = true;
                mSearchKeyShortcutPending = false;
            } else {
                return null;
            }
        } else if (modifierState == 0) {
            AppLaunchData appLaunchData = null;
            // Handle application launch keys.
            String role = sApplicationLaunchKeyRoles.get(keyCode);
            String category = sApplicationLaunchKeyCategories.get(keyCode);
            if (!TextUtils.isEmpty(role)) {
                appLaunchData = AppLaunchData.createLaunchDataForRole(role);
            } else if (!TextUtils.isEmpty(category)) {
                appLaunchData = AppLaunchData.createLaunchDataForCategory(category);
            }

            return appLaunchData;
        }

        if (keyCode == KeyEvent.KEYCODE_UNKNOWN) {
            return null;
        }
        InputGestureData gesture = mBookmarks.get(
                InputGestureData.createKeyTrigger(keyCode, modifierState));
        if (gesture == null) {
            return null;
        }
        return gesture.getAction().appLaunchData();
    }

    /**
     * Handle the shortcut from {@link KeyEvent}
     *
     * @param event Description of the key event.
     */
    public InterceptKeyResult interceptKey(KeyEvent event) {
        if (event.getRepeatCount() != 0) {
            return InterceptKeyResult.DO_NOTHING;
        }

        final int metaState = event.getModifiers();
        final int keyCode = event.getKeyCode();
        if (keyCode == KeyEvent.KEYCODE_SEARCH) {
            if (event.getAction() == KeyEvent.ACTION_DOWN) {
                mSearchKeyShortcutPending = true;
                mConsumeSearchKeyUp = false;
            } else {
                mSearchKeyShortcutPending = false;
                if (mConsumeSearchKeyUp) {
                    mConsumeSearchKeyUp = false;
                    return InterceptKeyResult.CONSUME_KEY;
                }
            }
            return InterceptKeyResult.DO_NOTHING;
        }

        if (event.getAction() != KeyEvent.ACTION_DOWN) {
            return InterceptKeyResult.DO_NOTHING;
        }

        // Intercept shortcuts defined in bookmarks or through application launch keycodes
        AppLaunchData appLaunchData = interceptShortcut(event);

        // TODO(b/358569822): Ideally shortcut service custom shortcuts should be either
        //  migrated to bookmarks or customizable shortcut APIs.
        if (appLaunchData == null && handleShortcutService(keyCode, metaState)) {
            return InterceptKeyResult.CONSUME_KEY;
        }

        return new InterceptKeyResult(/* consumed =*/ false, appLaunchData);
    }

    /**
     * @return a list of {@link InputGestureData} containing the application launch shortcuts parsed
     * at boot time from {@code bookmarks.xml}.
     */
    public List<InputGestureData> getBookmarks() {
        return new ArrayList<>(mBookmarks.values());
    }

    public void dump(IndentingPrintWriter ipw) {
        ipw.println("AppLaunchShortcutManager:");
        ipw.increaseIndent();
        for (InputGestureData data : mBookmarks.values()) {
            ipw.println(data);
        }
        ipw.decreaseIndent();
    }

    public record InterceptKeyResult(boolean consumed, @Nullable AppLaunchData appLaunchData) {
        private static final InterceptKeyResult DO_NOTHING = new InterceptKeyResult(false, null);
        private static final InterceptKeyResult CONSUME_KEY = new InterceptKeyResult(true, null);
    }
}
+11 −0
Original line number Diff line number Diff line
@@ -23,11 +23,13 @@ import android.graphics.PointF;
import android.hardware.display.DisplayViewport;
import android.hardware.input.KeyGestureEvent;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.SparseBooleanArray;
import android.view.InputChannel;
import android.view.inputmethod.InputMethodSubtype;

import com.android.internal.inputmethod.InputMethodSubtypeHandle;
import com.android.internal.policy.IShortcutService;

import java.util.List;

@@ -278,6 +280,15 @@ public abstract class InputManagerInternal {
     */
    public abstract void setAccessibilityPointerIconScaleFactor(int displayId, float scaleFactor);


    /**
     * Register shortcuts for input manager to dispatch.
     * Shortcut code is packed as (metaState << Integer.SIZE) | keyCode
     * @hide
     */
    public abstract void registerShortcutKey(long shortcutCode,
            IShortcutService shortcutKeyReceiver) throws RemoteException;

    /**
     * Set whether the given input device can wake up the kernel from sleep
     * when it generates input events. By default, usually only internal (built-in)
+12 −0
Original line number Diff line number Diff line
@@ -130,6 +130,7 @@ import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.inputmethod.InputMethodSubtypeHandle;
import com.android.internal.os.SomeArgs;
import com.android.internal.policy.IShortcutService;
import com.android.internal.policy.KeyInterceptionInfo;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.Preconditions;
@@ -3020,6 +3021,11 @@ public class InputManagerService extends IInputManager.Stub
        return mKeyGestureController.getCustomInputGestures(UserHandle.getCallingUserId());
    }

    @Override
    public AidlInputGestureData[] getAppLaunchBookmarks() {
        return mKeyGestureController.getAppLaunchBookmarks();
    }

    private void handleCurrentUserChanged(@UserIdInt int userId) {
        mCurrentUserId = userId;
        mKeyGestureController.setCurrentUserId(userId);
@@ -3563,6 +3569,12 @@ public class InputManagerService extends IInputManager.Stub
            mHandler.obtainMessage(MSG_CURRENT_USER_CHANGED, newUserId).sendToTarget();
        }

        @Override
        public void registerShortcutKey(long shortcutCode, IShortcutService shortcutKeyReceiver)
                throws RemoteException {
            mKeyGestureController.registerShortcutKey(shortcutCode, shortcutKeyReceiver);
        }

        @Override
        public boolean setKernelWakeEnabled(int deviceId, boolean enabled) {
            return mNative.setKernelWakeEnabled(deviceId, enabled);
Loading