android 蓝牙键盘快捷键的系统定制

 当手机连上蓝牙键盘后,设置中有一个键盘快捷键的功能。可以快速唤醒某些app,或者执行操作。

android 默认的快捷键配置并不完善,而且有的是无效组合按键。按了完全没有效果。这是因为默认配置并没有进行配置。

配置蓝牙键盘快捷键

当我们想修改这些快捷键时,就需要在代码中进行定制修改。

这个就涉及到一个关键文件。   frameworks/base/core/res/res/xml/bookmarks.xml

<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2007 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.
-->

<!--
     Default system bookmarks for AOSP.
     Bookmarks for vendor apps should be added to a bookmarks resource overlay; not here.

     Typical shortcuts (not necessarily defined here):
       'b': Browser
       'c': Contacts
       'e': Email
       'g': GMail
       'k': Calendar
       'm': Maps
       'p': Music
       's': SMS
       't': Talk
       'u': Calculator
       'y': YouTube
-->
<bookmarks>
    <bookmark
        category="android.intent.category.APP_BROWSER"
        shortcut="b" />
    <bookmark
        category="android.intent.category.APP_CONTACTS"
        shortcut="c" />
    <bookmark
        category="android.intent.category.APP_EMAIL"
        shortcut="e" />
    <bookmark
        category="android.intent.category.APP_CALENDAR"
        shortcut="k" />
    <bookmark
        category="android.intent.category.APP_MAPS"
        shortcut="m" />
    <bookmark
        category="android.intent.category.APP_MUSIC"
        shortcut="p" />
    <bookmark
        category="android.intent.category.APP_MESSAGING"
        shortcut="s" />
    <bookmark
        category="android.intent.category.APP_CALCULATOR"
        shortcut="u" />
</bookmarks>

当我们想要去定制时,可以去overlay  覆写这个文件。平台不同文件的路径也可能不同,我这边举个例子:vendor/qcom/proprietary/commonsys/resource-overlay/common/Frameworks/res/xml/bookmarks.xml

在这个文件里面去加自己想要开启的app就可以了,这里默认的就可以打开一些android 默认app。

category : 的配置是intent 类里面的。 给大家列举一些。

 public static final String CATEGORY_DEFAULT = "android.intent.category.DEFAULT";

public static final String CATEGORY_BROWSABLE = "android.intent.category.BROWSABLE";

public static final String CATEGORY_VOICE = "android.intent.category.VOICE";

public static final String CATEGORY_ALTERNATIVE = "android.intent.category.ALTERNATIVE";

public static final String CATEGORY_SELECTED_ALTERNATIVE = "android.intent.category.SELECTED_ALTERNATIVE";

public static final String CATEGORY_TAB = "android.intent.category.TAB";

public static final String CATEGORY_LAUNCHER = "android.intent.category.LAUNCHER";

public static final String CATEGORY_LEANBACK_LAUNCHER = "android.intent.category.LEANBACK_LAUNCHER";

public static final String CATEGORY_CAR_LAUNCHER = "android.intent.category.CAR_LAUNCHER";

public static final String CATEGORY_COMMUNAL_MODE = "android.intent.category.COMMUNAL_MODE";

public static final String CATEGORY_LEANBACK_SETTINGS = "android.intent.category.LEANBACK_SETTINGS";

public static final String CATEGORY_SETUP_WIZARD = "android.intent.category.SETUP_WIZARD";

public static final String CATEGORY_LAUNCHER_APP = "android.intent.category.LAUNCHER_APP";

public static final String CATEGORY_EMBED = "android.intent.category.EMBED";

public static final String CATEGORY_APP_MARKET = "android.intent.category.APP_MARKET";

public static final String CATEGORY_CAR_DOCK = "android.intent.category.CAR_DOCK";

public static final String CATEGORY_DESK_DOCK = "android.intent.category.DESK_DOCK";

public static final String CATEGORY_VR_HOME = "android.intent.category.VR_HOME";

public static final String CATEGORY_ACCESSIBILITY_SHORTCUT_TARGET =
            "android.intent.category.ACCESSIBILITY_SHORTCUT_TARGET";

 public static final String CATEGORY_APP_BROWSER = "android.intent.category.APP_BROWSER";

public static final String CATEGORY_APP_CALCULATOR = "android.intent.category.APP_CALCULATOR";

public static final String CATEGORY_APP_CALENDAR = "android.intent.category.APP_CALENDAR";

public static final String CATEGORY_APP_CONTACTS = "android.intent.category.APP_CONTACTS";

public static final String CATEGORY_APP_EMAIL = "android.intent.category.APP_EMAIL";

public static final String CATEGORY_APP_GALLERY = "android.intent.category.APP_GALLERY";

public static final String CATEGORY_APP_MAPS = "android.intent.category.APP_MAPS";

public static final String CATEGORY_APP_MESSAGING = "android.intent.category.APP_MESSAGING";

public static final String CATEGORY_APP_MUSIC = "android.intent.category.APP_MUSIC";

public static final String CATEGORY_APP_FILES = "android.intent.category.APP_FILES";

public static final String CATEGORY_APP_WEATHER = "android.intent.category.APP_WEATHER";

public static final String CATEGORY_APP_FITNESS = "android.intent.category.APP_FITNESS";

shortcut :   这是组合键的配合按键。 

如果你想启动一些第三方的软件也是可以的。

可以通过像启动activity的方式来配置这个xml文件。  例如:

<bookmark
package="com.google.android.apps.googleassistant"
class="com.google.android.apps.googleassistant.AssistantActivity"
category="android.intent.category.DEFAULT"
shortcut="a" />

配置上包名,类名就可以利用蓝牙键盘的快捷键去启动app。

当新增一个快捷键时,你可能想把上图中设置中的提示ui也改一下。

这时候就需要到frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcuts.java文件中去修改。

private KeyboardShortcutGroup getDefaultApplicationShortcuts() {
        final int userId = mContext.getUserId();
        List<KeyboardShortcutInfo> keyboardShortcutInfoAppItems = new ArrayList<>();

        // Assist.
        final AssistUtils assistUtils = new AssistUtils(mContext);
        final ComponentName assistComponent = assistUtils.getAssistComponentForUser(userId);
        // Not all devices have an assist component.
        if (assistComponent != null) {
            PackageInfo assistPackageInfo = null;
            try {
                assistPackageInfo = mPackageManager.getPackageInfo(
                        assistComponent.getPackageName(), 0, userId);
            } catch (RemoteException e) {
                Log.e(TAG, "PackageManagerService is dead");
            }

            if (assistPackageInfo != null) {
                final Icon assistIcon = Icon.createWithResource(
                        assistPackageInfo.applicationInfo.packageName,
                        assistPackageInfo.applicationInfo.icon);

                keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
                        mContext.getString(R.string.keyboard_shortcut_group_applications_assist),
                        assistIcon,
                        KeyEvent.KEYCODE_UNKNOWN,
                        KeyEvent.META_META_ON));
            }
        }

        // Browser.
        final Icon browserIcon = getIconForIntentCategory(Intent.CATEGORY_APP_BROWSER, userId);
        if (browserIcon != null) {
            keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
                    mContext.getString(R.string.keyboard_shortcut_group_applications_browser),
                    browserIcon,
                    KeyEvent.KEYCODE_B,
                    KeyEvent.META_META_ON));
        }


        // Contacts.
        final Icon contactsIcon = getIconForIntentCategory(Intent.CATEGORY_APP_CONTACTS, userId);
        if (contactsIcon != null) {
            keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
                    mContext.getString(R.string.keyboard_shortcut_group_applications_contacts),
                    contactsIcon,
                    KeyEvent.KEYCODE_C,
                    KeyEvent.META_META_ON));
        }

        // Email.
        final Icon emailIcon = getIconForIntentCategory(Intent.CATEGORY_APP_EMAIL, userId);
        if (emailIcon != null) {
            keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
                    mContext.getString(R.string.keyboard_shortcut_group_applications_email),
                    emailIcon,
                    KeyEvent.KEYCODE_E,
                    KeyEvent.META_META_ON));
        }

        // Messaging.
        final Icon messagingIcon = getIconForIntentCategory(Intent.CATEGORY_APP_MESSAGING, userId);
        if (messagingIcon != null) {
            keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
                    mContext.getString(R.string.keyboard_shortcut_group_applications_sms),
                    messagingIcon,
                    KeyEvent.KEYCODE_S,
                    KeyEvent.META_META_ON));
        }

        // Music.
        final Icon musicIcon = getIconForIntentCategory(Intent.CATEGORY_APP_MUSIC, userId);
        if (musicIcon != null) {
            keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
                    mContext.getString(R.string.keyboard_shortcut_group_applications_music),
                    musicIcon,
                    KeyEvent.KEYCODE_P,
                    KeyEvent.META_META_ON));
        }

        // Calendar.
        final Icon calendarIcon = getIconForIntentCategory(Intent.CATEGORY_APP_CALENDAR, userId);
        if (calendarIcon != null) {
            keyboardShortcutInfoAppItems.add(new KeyboardShortcutInfo(
                    mContext.getString(R.string.keyboard_shortcut_group_applications_calendar),
                    calendarIcon,
                    KeyEvent.KEYCODE_L,
                    KeyEvent.META_META_ON));
        }

        final int itemsSize = keyboardShortcutInfoAppItems.size();
        if (itemsSize == 0) {
            return null;
        }

        // Sorts by label, case insensitive with nulls and/or empty labels last.
        Collections.sort(keyboardShortcutInfoAppItems, mApplicationItemsComparator);
        return new KeyboardShortcutGroup(
                mContext.getString(R.string.keyboard_shortcut_group_applications),
                keyboardShortcutInfoAppItems,
                true);
    }

这个方法里面加载了那些图标和字段,只要仿造这个配置去新增一个就可以了。

原生bug解决: 蓝牙快捷键打开日历失败,唤醒语音助手失败。

打开日历失败:
只需要再bookmarks.xml  中 新增一个标签

<bookmark
category="android.intent.category.APP_CALENDAR"
shortcut="l" />

唤醒语音助手失败:

唤醒语音助手的快捷键只有一个。

当我们只按这一个键的时候,默认情况可能是打开all app 界面,而不是展示的唤醒语音助手

当我们想去bookmarks.xml去配置的时候,发现这个里面配的都是组合键,单独一个 se键是配置不了的。 shortcut 为空的话是不生效的。

解决方案:给唤醒 语音助手配置一个组合按键,举例:Search+a ,根据上面的说明配置bookmarks.xml 文件,然后再改一下提示界面的ui。

KeyboardShortcuts.java  中的   KeyEvent.KEYCODE_UNKNOWN   改为  KeyEvent.KEYCODE_A    就行了。

如果不想改键的话,就需要修改代码了,我也进行了尝试,但是效果不太好;每次按其他的组合键时,键盘会先发送单 Search 按键的事件,然后再发送  组合键的事件。会导致启动应用的同时,还会唤醒语音助手,使用体验很差,所以还是推荐直接改成组合键的方式唤醒语音助手。

蓝牙键盘快捷键运行流程

蓝牙键盘快捷键按下后,会发送按键事件上来。被frameworks/base/services/core/java/com/android/server/policy/PhoneWindowManager.java

这个类接收到。

interceptKeyBeforeDispatching()

public long interceptKeyBeforeDispatching(IBinder focusedToken, KeyEvent event,
            int policyFlags) {


*********


    if (isUserSetupComplete() && !keyguardOn) {
            if (mModifierShortcutManager.interceptKey(event)) {
                dismissKeyboardShortcutsMenu();
                mPendingMetaAction = false;
                mPendingCapsLockToggle = false;
                return key_consumed;
            }
    }

********

}

mModifierShortcutManager.interceptKey(event)

event 事件会在这个方法中进行处理。

frameworks/base/services/core/java/com/android/server/policy/ModifierShortcutManager.java

/**
     * Handle the shortcut from {@link KeyEvent}
     *
     * @param event Description of the key event.
     * @return True if invoked the shortcut, otherwise false.
     */
    boolean interceptKey(KeyEvent event) {
        if (event.getRepeatCount() != 0) {
            return false;
        }

        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 true;
                }
            }
            return false;
        }

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

        final KeyCharacterMap kcm = event.getKeyCharacterMap();
        if (handleIntentShortcut(kcm, keyCode, metaState)) {
            return true;
        }

        if (handleShortcutService(keyCode, metaState)) {
            return true;
        }

        return false;
    }

handleIntentShortcut(kcm, keyCode, metaState)  

这个方法会从event中拿信息去匹配

/**
     * Handle the shortcut to {@link Intent}
     *
     * @param kcm the {@link KeyCharacterMap} associated with the keyboard device.
     * @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 handleIntentShortcut(KeyCharacterMap kcm, int keyCode, int metaState) {
        // 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) {
            if (kcm.isPrintingKey(keyCode)) {
                mConsumeSearchKeyUp = true;
                mSearchKeyShortcutPending = false;
            } else {
                return false;
            }
        } else if ((metaState & KeyEvent.META_META_MASK) != 0) {
            // Invoke shortcuts using Meta.
            metaState &= ~KeyEvent.META_META_MASK;
        } else {
            // Handle application launch keys.
            String category = sApplicationLaunchKeyCategories.get(keyCode);
            if (category != null) {
                Intent intent = Intent.makeMainSelectorActivity(Intent.ACTION_MAIN, category);
                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                try {
                    mContext.startActivityAsUser(intent, UserHandle.CURRENT);
                } catch (ActivityNotFoundException ex) {
                    Slog.w(TAG, "Dropping application launch key because "
                            + "the activity to which it is registered was not found: "
                            + "keyCode=" + KeyEvent.keyCodeToString(keyCode) + ","
                            + " category=" + category);
                }
                return true;
            } else {
                return false;
            }
        }

        final Intent shortcutIntent = getIntent(kcm, keyCode, metaState);
        Log.d("ModifierShortcutManager", " handleIntentShortcut: shortcut Intent = " + shortcutIntent);
        if (shortcutIntent != null) {
            shortcutIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            try {
                mContext.startActivityAsUser(shortcutIntent, UserHandle.CURRENT);
            } catch (ActivityNotFoundException ex) {
                Slog.w(TAG, "Dropping shortcut key combination because "
                        + "the activity to which it is registered was not found: "
                        + "META+ or SEARCH" + KeyEvent.keyCodeToString(keyCode));
            }
            return true;
        }
        return false;
    }

final Intent shortcutIntent = getIntent(kcm, keyCode, metaState);

会通过这个方法获取intent 来确认执行的操作

/**
     * Gets the shortcut intent for a given keycode+modifier. Make sure you
     * strip whatever modifier is used for invoking shortcuts (for example,
     * if 'Sym+A' should invoke a shortcut on 'A', you should strip the
     * 'Sym' bit from the modifiers before calling this method.
     * <p>
     * This will first try an exact match (with modifiers), and then try a
     * match without modifiers (primary character on a key).
     *
     * @param kcm The key character map of the device on which the key was pressed.
     * @param keyCode The key code.
     * @param metaState The meta state, omitting any modifiers that were used
     * to invoke the shortcut.
     * @return The intent that matches the shortcut, or null if not found.
     */
    private Intent getIntent(KeyCharacterMap kcm, int keyCode, int metaState) {
        // If a modifier key other than shift is also pressed, skip it.
        final boolean isShiftOn = KeyEvent.metaStateHasModifiers(metaState, KeyEvent.META_SHIFT_ON);
        if (!isShiftOn && !KeyEvent.metaStateHasNoModifiers(metaState)) {
            return null;
        }

        ShortcutInfo shortcut = null;

        // If the Shift key is pressed, then search for the shift shortcuts.
        SparseArray<ShortcutInfo> shortcutMap = isShiftOn ? mShiftShortcuts : mIntentShortcuts;

        // First try the exact keycode (with modifiers).
        int shortcutChar = kcm.get(keyCode, metaState);
        if (shortcutChar != 0) {
            shortcut = shortcutMap.get(shortcutChar);
        }

        // Next try the primary character on that key.
        if (shortcut == null) {
            shortcutChar = Character.toLowerCase(kcm.getDisplayLabel(keyCode));
            if (shortcutChar != 0) {
                shortcut = shortcutMap.get(shortcutChar);
            }
        }

        return (shortcut != null) ? shortcut.intent : null;
    }

shortcut = shortcutMap.get(shortcutChar);

从shortcutMap 中提取  intent。shortcutMap   就等于   mIntentShortcuts

再看看 mIntentShortcuts  是如何往里面存值的。        

 private void loadShortcuts() {
        PackageManager packageManager = mContext.getPackageManager();
        try {
            XmlResourceParser parser = mContext.getResources().getXml(
                    com.android.internal.R.xml.bookmarks);
            XmlUtils.beginDocument(parser, TAG_BOOKMARKS);

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

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

                if (!TAG_BOOKMARK.equals(parser.getName())) {
                    break;
                }

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

                if (TextUtils.isEmpty(shortcutName)) {
                    Log.w(TAG, "Unable to get shortcut for: " + packageName + "/" + className);
                    continue;
                }

                final int shortcutChar = shortcutName.charAt(0);
                final boolean isShiftShortcut = (shiftName != null && shiftName.equals("true"));

                final Intent intent;
                final String title;
                if (packageName != null && className != null) {
                    ActivityInfo info = null;
                    ComponentName componentName = new ComponentName(packageName, className);
                    try {
                        info = packageManager.getActivityInfo(componentName,
                                PackageManager.MATCH_DIRECT_BOOT_AWARE
                                        | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
                                        | PackageManager.MATCH_UNINSTALLED_PACKAGES);
                    } catch (PackageManager.NameNotFoundException e) {
                        String[] packages = packageManager.canonicalToCurrentPackageNames(
                                new String[] { packageName });
                        componentName = new ComponentName(packages[0], className);
                        try {
                            info = packageManager.getActivityInfo(componentName,
                                    PackageManager.MATCH_DIRECT_BOOT_AWARE
                                            | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
                                            | PackageManager.MATCH_UNINSTALLED_PACKAGES);
                        } catch (PackageManager.NameNotFoundException e1) {
                            Log.w(TAG, "Unable to add bookmark: " + packageName
                                    + "/" + className + " not found.");
                            continue;
                        }
                    }

                    intent = new Intent(Intent.ACTION_MAIN);
                    intent.addCategory(Intent.CATEGORY_LAUNCHER);
                    intent.setComponent(componentName);
                    title = info.loadLabel(packageManager).toString();
                } else if (categoryName != null) {
                    intent = Intent.makeMainSelectorActivity(Intent.ACTION_MAIN, categoryName);
                    title = "";
                } else {
                    Log.w(TAG, "Unable to add bookmark for shortcut " + shortcutName
                            + ": missing package/class or category attributes");
                    continue;
                }

                ShortcutInfo shortcut = new ShortcutInfo(title, intent);
                if (isShiftShortcut) {
                    mShiftShortcuts.put(shortcutChar, shortcut);
                } else {
                    mIntentShortcuts.put(shortcutChar, shortcut);
                }
            }
        } catch (XmlPullParserException | IOException e) {
            Log.e(TAG, "Got exception parsing bookmarks.", e);
        }
    }

XmlResourceParser parser = mContext.getResources().getXml(
com.android.internal.R.xml.bookmarks);

从  bookmarks.xml  载入数据

把  组合键转换为  ASCII  值当做key,intent  当做valus  存进map里面

final int shortcutChar = shortcutName.charAt(0);

mIntentShortcuts.put(shortcutChar, shortcut);

这样getIntent()  中就可以通过event ,来获取相应的intent去执行了。

mContext.startActivityAsUser(shortcutIntent, UserHandle.CURRENT);

那么整个流程就是这样了。谢谢大家的观看!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值