Input系统截断策略的分析与应用详解

目录
  • 引言
  • 截断策略的原理
  • 截断策略的应用
    • 初始化
    • 实现按键手势
  • power 键的亮屏与灭屏
  • 结束

引言

上一篇文章 Input系统: 按键事件分发 分析了按键事件的分发过程,虽然分析的对象只是按键事件,但是也从整体上,描绘了事件分发的过程。其中比较有意思的一环是事件截断策略,本文就来分析它的原理以及应用。

其实这篇文章早已经写好,这几天在完善细节的时候,我突然发现了源码中的一个 bug,这让我开始对自己的分析产生质疑,最终我拿了一台公司的样机 debug,我发现这确实是源码的一个 bug。本文在分析的过程中,会逐步揭开这个 bug。

截断策略的原理

根据 Input系统: 按键事件分发 的分析,事件开始分发前,会执行截断策略,如下

void InputDispatcher::notifyKey(const NotifyKeyArgs* args) {
    // ...
    uint32_t policyFlags = args->policyFlags;
    int32_t flags = args->flags;
    int32_t metaState = args->metaState;
    // ...
    // 根据事件参数创建 KeyEvent,截断策略需要使用它
    KeyEvent event;
    event.initialize(args->id, args->deviceId, args->source, args->displayId, INVALID_HMAC,
                     args->action, flags, keyCode, args->scanCode, metaState, repeatCount,
                     args->downTime, args->eventTime);
    android::base::Timer t;
    // 1. 执行截断策略,执行的结果保存到参数 policyFlags
    mPolicy->interceptKeyBeforeQueueing(&event, /*byref*/ policyFlags);
    if (t.duration() > SLOW_INTERCEPTION_THRESHOLD) {
        ALOGW("Excessive delay in interceptKeyBeforeQueueing; took %s ms",
              std::to_string(t.duration().count()).c_str());
    }
    bool needWake;
    { // acquire lock
        mLock.lock();
        if (shouldSendKeyToInputFilterLocked(args)) {
            // ...
        }
        // 2. 创建 KeyEntry , 并加入到 InputDispatcher 的收件箱中
        std::unique_ptr<KeyEntry> newEntry =
                std::make_unique<KeyEntry>(args->id, args->eventTime, args->deviceId, args->source,
                                           args->displayId, policyFlags, args->action, flags,
                                           keyCode, args->scanCode, metaState, repeatCount,
                                           args->downTime);
        needWake = enqueueInboundEventLocked(std::move(newEntry));
        mLock.unlock();
    } // release lock
    // 3. 如果有必要,唤醒 InputDispatcher 线程
    if (needWake) {
        mLooper->wake();
    }
}

根据 Input系统: InputManagerService的创建与启动 可知,底层的截断策略实现类是 NativeInputManager

// com_android_server_input_InputManagerService.cpp
void NativeInputManager::interceptKeyBeforeQueueing(const KeyEvent* keyEvent,
        uint32_t& policyFlags) {
    bool interactive = mInteractive.load();
    if (interactive) {
        policyFlags |= POLICY_FLAG_INTERACTIVE;
    }
    // 来自输入设备的按键事件是受信任的
    // 拥有注入权限的app,注入的按键事件也是受信任的,例如 SystemUI 注入 HOME, BACK, RECENT 按键事件
    if ((policyFlags & POLICY_FLAG_TRUSTED)) {
        nsecs_t when = keyEvent->getEventTime();
        JNIEnv* env = jniEnv();
        // 包装成上层的 KeyEvent 对象
        jobject keyEventObj = android_view_KeyEvent_fromNative(env, keyEvent);
        jint wmActions;
        if (keyEventObj) {
            // 1. 调用上层 InputManagerService#interceptKeyBeforeQueueing() 来执行截断策略
            wmActions = env->CallIntMethod(mServiceObj,
                    gServiceClassInfo.interceptKeyBeforeQueueing,
                    keyEventObj, policyFlags);
            if (checkAndClearExceptionFromCallback(env, "interceptKeyBeforeQueueing")) {
                wmActions = 0;
            }
            android_view_KeyEvent_recycle(env, keyEventObj);
            env->DeleteLocalRef(keyEventObj);
        } else {
            ALOGE("Failed to obtain key event object for interceptKeyBeforeQueueing.");
            wmActions = 0;
        }
        // 2. 处理截断策略的结果
        // 实际上就是把上层截断策略的结果转化为底层的状态
        handleInterceptActions(wmActions, when, /*byref*/ policyFlags);
    } else {
        // ...
    }
}
void NativeInputManager::handleInterceptActions(jint wmActions, nsecs_t when,
        uint32_t& policyFlags) {
    // 其实就是根据截断策略的结果,决定是否在 policyFlags 添加 POLICY_FLAG_PASS_TO_USER 标志位
    if (wmActions & WM_ACTION_PASS_TO_USER) {
        policyFlags |= POLICY_FLAG_PASS_TO_USER;
    } else {
#if DEBUG_INPUT_DISPATCHER_POLICY
        ALOGD("handleInterceptActions: Not passing key to user.");
#endif
    }
}

首先调用上层来执行截断策略,然后根据执行的结果,再决定是否在参数 policyFlags 添加 POLICY_FLAG_PASS_TO_USER 标志位。这个标志位,就决定了事件是否能传递给用户。

截断策略经过 InputManagerService,最终是由上层的 PhoneWindowManager 实现,如下

// PhoneWindowManager.java
    public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
        // ...
        // Handle special keys.
        switch (keyCode) {
            // ...
            case KeyEvent.KEYCODE_POWER: {
                // 返回的额结果去,去掉 ACTION_PASS_TO_USER 标志位
                result &= ~ACTION_PASS_TO_USER;
                isWakeKey = false; // wake-up will be handled separately
                if (down) {
                    interceptPowerKeyDown(event, interactiveAndOn);
                } else {
                    interceptPowerKeyUp(event, canceled);
                }
                break;
            }
            // ...
        }
        // ...
        return result;
    }

这里以 power 按键为例,它的按键事件的截断策略的处理结果,是去掉了 ACTION_PASS_TO_USER 标志位,也即告诉底层不要把事件发送给用户,这就是为何窗口(例如 Activity)无法收到 power 按键事件的原因。

现在,如果项目的硬件上新增一个按键,并且不想这个按键事件被分发给用户,你会搞了吗?

截断策略的应用

根据 Input系统: 按键事件分发 可知,截断策略发生在事件分发之前,因此它能及时处理一些系统功能的事件,例如,power 按键亮/灭屏没有延时,挂断按键挂断电话也没有延时。

刚才,我们看到了截断策略的一个作用,阻塞事件发送给用户/窗口。然而,它还可以实现按键的手势,手势包括单击,多击,长按,组合键(例如截屏组合键)。

下面来分析截断策略是如何实现按键手势中的单击、多击、长按。至于组合键,由于涉及分发策略,留到下一篇文章分析。

初始化

按键的手势是用 SingleKeyGestureDetector 来管理的,它在 PhoneWindowManager 中的初始化如下

// PhoneWindowManager.java
    private void initSingleKeyGestureRules() {
        mSingleKeyGestureDetector = new SingleKeyGestureDetector(mContext);
        int powerKeyGestures = 0;
        if (hasVeryLongPressOnPowerBehavior()) {
            powerKeyGestures |= KEY_VERYLONGPRESS;
        }
        if (hasLongPressOnPowerBehavior()) {
            powerKeyGestures |= KEY_LONGPRESS;
        }
        // 增加一个 power 按键手势的规则
        mSingleKeyGestureDetector.addRule(new PowerKeyRule(powerKeyGestures));
        if (hasLongPressOnBackBehavior()) {
            mSingleKeyGestureDetector.addRule(new BackKeyRule(KEY_LONGPRESS));
        }
    }

SingleKeyGestureDetector 根据配置,为 Power 键和 Back 键保存了规则(rule)。所谓的规则,就是如何实现单个按键的手势。

所有的规则的基类都是 SingleKeyGestureDetector.SingleKeyRule,它的使用方式用下面一段代码解释

SingleKeyRule rule =
    new SingleKeyRule(KEYCODE_POWER, KEY_LONGPRESS|KEY_VERYLONGPRESS) {
         int getMaxMultiPressCount() { // maximum multi press count. }
         void onPress(long downTime) { // short press behavior. }
         void onLongPress(long eventTime) { // long press behavior. }
         void onVeryLongPress(long eventTime) { // very long press behavior. }
         void onMultiPress(long downTime, int count) { // multi press behavior.  }
     };
  • getMaxMultiPressCount() 表示支持的按键的最大点击次数。如果返回1,表示只支持单击,如果返回3,表示支持双击和三击,and so on...
  • 单击按键会调用 onPress()
  • 多击按键会调用 onMultiPress(long downTime, int count),参数 count 表示多击的次数。
  • 长按按键会调用 onLongPress()
  • 长时间地长按按键,会调用 onVeryLongPress()

实现按键手势

截断策略在处理按键事件时,会处理按键手势,如下

// PhoneWindowManager.java
    public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
        // ...
        // 一般来说,轨迹球设备产生的事件,会设置 KeyEvent.FLAG_FALLBACK
        if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
            // 处理按键手势
            handleKeyGesture(event, interactiveAndOn);
        }
        // ...
        return result;
    }
    private void handleKeyGesture(KeyEvent event, boolean interactive) {
        // KeyCombinationManager 是用于实现组合按键功能,如果只按下单个按键,不会截断事件
        if (mKeyCombinationManager.interceptKey(event, interactive)) {
            // handled by combo keys manager.
            mSingleKeyGestureDetector.reset();
            return;
        }
        // GestureLauncherService 实现的双击打开 camera 功能
        // 原理很简单,就是判断两次 power 键按下的时间间隔
        if (event.getKeyCode() == KEYCODE_POWER && event.getAction() == KeyEvent.ACTION_DOWN) {
            mPowerKeyHandled = handleCameraGesture(event, interactive);
            if (mPowerKeyHandled) {
                // handled by camera gesture.
                mSingleKeyGestureDetector.reset();
                return;
            }
        }
        // 实现按键手势
        mSingleKeyGestureDetector.interceptKey(event, interactive);
    }

从这里可以看到,有三个类实现按键的手势,如下

  • KeyCombinationManager,它是用于实现组合按键的功能,例如,power 键 + 音量下键 实现的截屏功能。它的原理很简单,就是第一个按键按下后,在超时时间内等待第二个按键事件的到来。
  • GestureLauncherService,目前只实现了双击打开 Camera 功能,原理也很简单,当第一次按下 power 键,在规定时间内按下第二次 power 键,然后由 SystemUI 实现打开 Camera 功能。
  • SingleKeyGestureDetector,实现通用的按键的手势功能。

GestureLauncherService 在很多个 Android 版本中,都只实现了双击打开 Camera 的功能,它的功能明显与 SingleKeyGestureDetector 重合了。然而,更不幸的是,SingleKeyGestureDetector 实现的手势功能还有 bug,Google 的工程师是不是把这个按键手势功能给遗忘了?

KeyCombinationManager 会在下一篇文章中分析,GestureLauncherService 请大家自行分析,现在来看下 SingleKeyGestureDetector 是如何处理按键手势的

// SingleKeyGestureDetector.java
    void interceptKey(KeyEvent event, boolean interactive) {
        if (event.getAction() == KeyEvent.ACTION_DOWN) {
            if (mDownKeyCode == KeyEvent.KEYCODE_UNKNOWN || mDownKeyCode != event.getKeyCode()) {
                // 记录按键按下时,是否是非交互状态
                // 一般来说,灭屏状态就是非交互状态
                mBeganFromNonInteractive = !interactive;
            }
            interceptKeyDown(event);
        } else {
            interceptKeyUp(event);
        }
    }

SingleKeyGestureDetector 分别处理了按键的 DOWN 事件和 UP 事件,这两者合起来才实现了整个手势功能。

首先看下如何处理 DOWN 事件

// SingleKeyGestureDetector.java
    private void interceptKeyDown(KeyEvent event) {
        final int keyCode = event.getKeyCode();
        // 3. 收到同一个按键的长按事件,立即执行长按动作
        if (mDownKeyCode == keyCode) {
            if (mActiveRule != null && (event.getFlags() & KeyEvent.FLAG_LONG_PRESS) != 0
                    && mActiveRule.supportLongPress() && !mHandledByLongPress) {
                if (DEBUG) {
                    Log.i(TAG, "Long press key " + KeyEvent.keyCodeToString(keyCode));
                }
                mHandledByLongPress = true;
                // 移除长按消息
                mHandler.removeMessages(MSG_KEY_LONG_PRESS);
                mHandler.removeMessages(MSG_KEY_VERY_LONG_PRESS);
                // 立即执行长按动作
                // 注意,是立即,因为系统已经表示这是一个长按动作
                mActiveRule.onLongPress(event.getEventTime());
            }
            return;
        }
        // 表示这里前一个按键按下还没有抬起前,又有另外一个按键按下
        if (mDownKeyCode != KeyEvent.KEYCODE_UNKNOWN
                || (mActiveRule != null && !mActiveRule.shouldInterceptKey(keyCode))) {
            if (DEBUG) {
                Log.i(TAG, "Press another key " + KeyEvent.keyCodeToString(keyCode));
            }
            reset();
        }
        // 保存按下的按键 keycode
        mDownKeyCode = keyCode;
        // 1. 按下首次按下,寻找一个规则
        if (mActiveRule == null) {
            final int count = mRules.size();
            for (int index = 0; index < count; index++) {
                final SingleKeyRule rule = mRules.get(index);
                // 找到为按键添加规则
                if (rule.shouldInterceptKey(keyCode)) {
                    mActiveRule = rule;
                    // 找到有效的 rule,就退出循环
                    // 看来对于一个按键,只有最先添加的规则有效
                    break;
                }
            }
        }
        // 没有为按键事件找到一条规则,直接退出
        if (mActiveRule == null) {
            return;
        }
        final long eventTime = event.getEventTime();
        // 2. 首次按下时,发送一个长按的延时消息,用于实现按键的长按功能
        // mKeyPressCounter 记录的是按键按下的次数
        if (mKeyPressCounter == 0) {
            if (mActiveRule.supportLongPress()) {
                final Message msg = mHandler.obtainMessage(MSG_KEY_LONG_PRESS, keyCode, 0,
                        eventTime);
                msg.setAsynchronous(true);
                mHandler.sendMessageDelayed(msg, mLongPressTimeout);
            }
            if (mActiveRule.supportVeryLongPress()) {
                final Message msg = mHandler.obtainMessage(MSG_KEY_VERY_LONG_PRESS, keyCode, 0,
                        eventTime);
                msg.setAsynchronous(true);
                mHandler.sendMessageDelayed(msg, mVeryLongPressTimeout);
            }
        }
        // 4. 这里表示之前已经按键已经按下至少一次
        else {
            // 移除长按事件的延时消息
            mHandler.removeMessages(MSG_KEY_LONG_PRESS);
            mHandler.removeMessages(MSG_KEY_VERY_LONG_PRESS);
            // 移除单击事件或多击事件的延时消息
            mHandler.removeMessages(MSG_KEY_DELAYED_PRESS);
            // Trigger multi press immediately when reach max count.( > 1)
            // 达到最大点击次数,立即执行多击功能
            // 注意,这段代码是一个 bug
            if (mKeyPressCounter == mActiveRule.getMaxMultiPressCount() - 1) {
                if (DEBUG) {
                    Log.i(TAG, "Trigger multi press " + mActiveRule.toString() + " for it"
                            + " reach the max count " + mKeyPressCounter);
                }
                mActiveRule.onMultiPress(eventTime, mKeyPressCounter + 1);
                mKeyPressCounter = 0;
            }
        }
    }

SingleKeyGestureDetector 对按键 DOWN 事件的处理过程如下

  • 按键首次按下,为按键找到相应的规则,保存到 mActiveRule
  • 按键首次按下,会发送一个延时的长按消息,实现长按功能。当超时时,也就是按键按下没有抬起,并且系统也没有发送按键的长按事件,那么会执行 SingleKeyRule#onLongPress() 或/和 SingleKeyRule#onVeryLongPress()
  • 如果收到系统发送的按键的长按事件,那么移除长按消息,并立即SingleKeyRule#onLongPress()。为何要立即执行,而不是发送一个延时消息?因为系统已经表示这是一个长按事件,没有理由再使用一个延时来检测是否要触发长按。
  • 如果多次(至少超过1次)点击按键,那么移除长按、单击/多击消息,并在点击次数达到最大时,立即执行SingleKeyRule#onMultiPress()

注意,第4点中,当达到最大点击次数时,立即执行SingleKeyRule#onMultiPress(),并重置按键点击次数 mKeyPressCounter 为 0,这是一个 Bug,这段代码应该去掉。在后面的分析中,我将证明这会造成 bug。

SingleKeyGestureDetector 对按键按下事件的处理,确切来说只实现了长按的功能,而按键的单击功能以及多击功能,是在处理 UP 事件中实现的,如下

// SingleKeyGestureDetector.java
    private boolean interceptKeyUp(KeyEvent event) {
        // 按键已抬起,就不应该触发长按事件,所以需要移除延时的长按消息
        mHandler.removeMessages(MSG_KEY_LONG_PRESS);
        mHandler.removeMessages(MSG_KEY_VERY_LONG_PRESS);
        // 按键抬起,重置 mDownKeyCode
        mDownKeyCode = KeyEvent.KEYCODE_UNKNOWN;
        // 没有有效规则,不处理按键的抬起事件
        if (mActiveRule == null) {
            return false;
        }
        // 如果已经触发长按,不处理按键的抬起事件
        if (mHandledByLongPress) {
            mHandledByLongPress = false;
            mKeyPressCounter = 0;
            return true;
        }
        final long downTime = event.getDownTime();
        // 抬起按键的key code 要与规则的一样,否则无法触发规则
        if (event.getKeyCode() == mActiveRule.mKeyCode) {
            // 1. 规则只支持单击,那么发送消息,执行单击操作。
            if (mActiveRule.getMaxMultiPressCount() == 1) {
                // 注意,第三个参数为 arg2,但并没有使用
                Message msg = mHandler.obtainMessage(MSG_KEY_DELAYED_PRESS, mActiveRule.mKeyCode,
                        1, downTime);
                msg.setAsynchronous(true);
                mHandler.sendMessage(msg);
                return true;
            }
            // 走到这里,表示规则支持多击功能,那么必须记录多击的次数,用于实现按键多击功能
            mKeyPressCounter++;
            // 2. 规则支持多击,发送一个延迟消息来实现单击或者多击功能
            // 既然支持多击,那么肯定需要一个超时时间来检测是否有多击操作,所以这里要设置一个延时
            // 注意,第三个参数为 arg2,但并没有使用
            Message msg = mHandler.obtainMessage(MSG_KEY_DELAYED_PRESS, mActiveRule.mKeyCode,
                    mKeyPressCounter, downTime);
            msg.setAsynchronous(true);
            mHandler.sendMessageDelayed(msg, MULTI_PRESS_TIMEOUT);
            return true;
        }
        // 收到其他按键的 UP/CANCEL 事件,重置前一个按键的规则
        reset();
        return false;
    }

SingleKeyGestureDetector 处理 UP 事件分两种情况

  • 如果规则只支持单击,那么发送一个消息执行单击动作。这里我就有一个疑问了,为何不与前面一样,直接执行单击动作,反而还要多此一举地发送一个消息去执行单击动作?
  • 如果规则支持多击,那么首先把点击次数 mKeyPressCounter 加1,然后发送一个延时消息执行单击或者多击动作。为何要设置一个延时?对于单击操作,需要使用一个延时来检测没有再次点击的操作,对于多击操作,需要检测后面是否还有点击操作。

注意,以上两种情况下发送的消息,Message#arg2 的值其实是按键的点击次数。

现在来看下 MSG_KEY_DELAYED_PRESS 消息的处理流程。

// SingleKeyGestureDetector.java
private class KeyHandler extends Handler {
    @Override
    public void handleMessage(Message msg) {
        if (mActiveRule == null) {
            return;
        }
        final int keyCode = msg.arg1;
        final long eventTime = (long) msg.obj;
        switch(msg.what) {
            // ...
            case MSG_KEY_DELAYED_PRESS:
                // 虽然在发送消息的时候,使用了 Message#arg2 参数来表示按键的点击次数
                // 但是,这里仍然使用 mKeyPressCounter 来决定按键的点击次数
                if (mKeyPressCounter == 1) {
                    // 当规则支持多击功能时,单击功能由这里实现
                    mActiveRule.onPress(eventTime);
                } else {
                    // 当规则只支持单击功能时,mKeyPressCounter 永远为 0,因此单击功能由这里实现
                    // 当规则支持多击功能时,多击功能由这里实现
                    mActiveRule.onMultiPress(eventTime, mKeyPressCounter);
                }
                reset();
                break;
        }
    }
}

这个消息的处理过程,从表面上看,如果是单击就执行 SingleKeyRule#onPress(),如果是多击就执行 SingleKeyRule#onMultiPress()

然而实际情况,并非如此。由于没有使用 Message#arg2,而是直接使用 mKeyPressCounter 作为按键点击次数,这就导致两个问题

  • 当规则只支持单击功能时,mKeyPressCounter 永远为0。于是单击按键时,调用 SingleKeyRule#onMultiPress(),而非 SingleKeyRule#onPress(),这岂不是笑话。
  • 当规则支持多击功能时,如果单击按键,会调用 SingleKeyRule#onPress(),如果多击按键,会调用 SingleKeyRule#onMultiPress() 这一切看起来没有问题,实则不然。对于多击操作,前面分析过,在处理 DOWN 事件时,当达到最大点击次数时,会调用SingleKeyRule#onMultiPress(),并把 mKeyPressCounter 重置为0。之后再处理 UP 事件时,mKeyPressCounter 加1后变为了1,然后发送消息去执行,最后,奇迹般地执行了一次 SingleKeyRule#onPress()。对于一个多击操作,居然执行了一次单击动作,这简直是国际笑话。

以上两点问题,我用样机进行验证过,确实存在。但是,系统很好地支持了双击 power 打开 Camera,以及单击 power 亮/灭屏。这又是怎么回事呢?

  • 双击 power 打开 Camera,是由 GestureLauncherService 实现的。
  • 系统默认配置的 power 规则,只支持单击。

既然 power 规则只支持单击,理论上应该调用 SingleKeyRule#onMultiPress(long downTime, int count),并且参数 count 为0,这岂不还是错的。确实如此,不过源码又巧合地用另外一个Bug避开了这一个Bug。接着往下看

首先看下 power 键的规则

// PhoneWindowManager.java
    private final class PowerKeyRule extends SingleKeyGestureDetector.SingleKeyRule {
        PowerKeyRule(int gestures) {
            super(KEYCODE_POWER, gestures);
        }
        @Override
        int getMaxMultiPressCount() {
            // 默认配置返回1
            return getMaxMultiPressPowerCount();
        }
        @Override
        void onPress(long downTime) {
            powerPress(downTime, 1 /*count*/,
                    mSingleKeyGestureDetector.beganFromNonInteractive());
        }
        @Override
        void onLongPress(long eventTime) {
            if (mSingleKeyGestureDetector.beganFromNonInteractive()
                    && !mSupportLongPressPowerWhenNonInteractive) {
                Slog.v(TAG, "Not support long press power when device is not interactive.");
                return;
            }
            powerLongPress(eventTime);
        }
        @Override
        void onVeryLongPress(long eventTime) {
            mActivityManagerInternal.prepareForPossibleShutdown();
            powerVeryLongPress();
        }
        @Override
        void onMultiPress(long downTime, int count) {
            powerPress(downTime, count, mSingleKeyGestureDetector.beganFromNonInteractive());
        }
    }

power 键规则,默认支持最大的点击数为1,这是在 config.xml 中进行配置的,这里不细讲。

power 键规则中,无论是单击还是多击,默认都调用同一个函数,如下

    private void powerPress(long eventTime, int count, boolean beganFromNonInteractive) {
        if (mDefaultDisplayPolicy.isScreenOnEarly() && !mDefaultDisplayPolicy.isScreenOnFully()) {
            Slog.i(TAG, "Suppressed redundant power key press while "
                    + "already in the process of turning the screen on.");
            return;
        }
        final boolean interactive = Display.isOnState(mDefaultDisplay.getState());
        Slog.d(TAG, "powerPress: eventTime=" + eventTime + " interactive=" + interactive
                + " count=" + count + " beganFromNonInteractive=" + beganFromNonInteractive
                + " mShortPressOnPowerBehavior=" + mShortPressOnPowerBehavior);
        // 根据配置,决定power键的单击/多击行为
        if (count == 2) {
            powerMultiPressAction(eventTime, interactive, mDoublePressOnPowerBehavior);
        } else if (count == 3) {
            powerMultiPressAction(eventTime, interactive, mTriplePressOnPowerBehavior);
        }
        // 注意,beganFromNonInteractive 表示是否在非交互状态下点击 power 键
        // 这里判断条件的意思是,处于交互状态,并且不是非交互状态下点击power键
        // 说简单点,这里只支持在亮屏状态下单击power键功能
        else if (interactive && !beganFromNonInteractive) {
            if (mSideFpsEventHandler.onSinglePressDetected(eventTime)) {
                Slog.i(TAG, "Suppressing power key because the user is interacting with the "
                        + "fingerprint sensor");
                return;
            }
            switch (mShortPressOnPowerBehavior) {
                case SHORT_PRESS_POWER_NOTHING:
                    break;
                case SHORT_PRESS_POWER_GO_TO_SLEEP:
                    // 灭屏,不过要先进入 doze 模式
                    sleepDefaultDisplayFromPowerButton(eventTime, 0);
                    break;
                case SHORT_PRESS_POWER_REALLY_GO_TO_SLEEP:
                    // 跳过 doze 模式,直接进入 sleep 模式
                    sleepDefaultDisplayFromPowerButton(eventTime,
                            PowerManager.GO_TO_SLEEP_FLAG_NO_DOZE);
                    break;
                case SHORT_PRESS_POWER_REALLY_GO_TO_SLEEP_AND_GO_HOME:
                    // 跳过 doze 模式,进入 sleep 模式,并返回 home
                    if (sleepDefaultDisplayFromPowerButton(eventTime,
                            PowerManager.GO_TO_SLEEP_FLAG_NO_DOZE)) {
                        launchHomeFromHotKey(DEFAULT_DISPLAY);
                    }
                    break;
                case SHORT_PRESS_POWER_GO_HOME:
                    // 返回 home
                    shortPressPowerGoHome();
                    break;
                case SHORT_PRESS_POWER_CLOSE_IME_OR_GO_HOME: {
                    if (mDismissImeOnBackKeyPressed) {
                        // 关闭输入法
                        if (mInputMethodManagerInternal == null) {
                            mInputMethodManagerInternal =
                                    LocalServices.getService(InputMethodManagerInternal.class);
                        }
                        if (mInputMethodManagerInternal != null) {
                            mInputMethodManagerInternal.hideCurrentInputMethod(
                                    SoftInputShowHideReason.HIDE_POWER_BUTTON_GO_HOME);
                        }
                    } else {
                        // 返回 home
                        shortPressPowerGoHome();
                    }
                    break;
                }
            }
        }
    }

从整体看,如果参数 count 为2或者3,会执行多击的行为,否则,执行单击行为。

这个逻辑是不是有点奇怪呀,参数 count 不为2也不为3,难道就是一定为1吗?正是因为这个逻辑,才导致 count 为0时,也能执行单击动作,小朋友听了都直呼6。我想起了我一个前同事做的事,用一个 Bug 去解决另外一个 Bug。

power 键的亮屏与灭屏

好了,言归正传,power 键规则的单击行为,只包括了灭屏,并没有包含亮屏,这又是怎么回事呢?因为 power 键的亮屏,不是在规则中实现的,而是在截断策略中实现的,如下

    public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
        // ...
        if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
            // 处理单个按键的手势
            handleKeyGesture(event, interactiveAndOn);
        }
        // ...
        switch (keyCode) {
            // ...
            case KeyEvent.KEYCODE_POWER: {
                // power 按键事件不分发给用户
                result &= ~ACTION_PASS_TO_USER;
                // 这里表示 power 键不是唤醒键,是不是很奇怪,因为系统默认绑定了 power 键亮屏功能
                isWakeKey = false; // wake-up will be handled separately
                if (down) {
                    // power 亮屏在这里实现
                    interceptPowerKeyDown(event, interactiveAndOn);
                } else {
                    interceptPowerKeyUp(event, canceled);
                }
                break;
            }
            // ...
        }
        // ...
        return result;
    }
    private void interceptPowerKeyDown(KeyEvent event, boolean interactive) {
        // Hold a wake lock until the power key is released.
        if (!mPowerKeyWakeLock.isHeld()) {
            mPowerKeyWakeLock.acquire();
        }
        mWindowManagerFuncs.onPowerKeyDown(interactive);
        // 设备处于响铃状态,就静音,处于通话状态,就挂电话
        TelecomManager telecomManager = getTelecommService();
        boolean hungUp = false;
        if (telecomManager != null) {
            if (telecomManager.isRinging()) {
                telecomManager.silenceRinger();
            } else if ((mIncallPowerBehavior
                    & Settings.Secure.INCALL_POWER_BUTTON_BEHAVIOR_HANGUP) != 0
                    && telecomManager.isInCall() && interactive) {
                hungUp = telecomManager.endCall();
            }
        }
        // 检测 PowerManagerService 是否正在使用 sensor 灭屏
        final boolean handledByPowerManager = mPowerManagerInternal.interceptPowerKeyDown(event);
        sendSystemKeyToStatusBarAsync(event.getKeyCode());
        // mPowerKeyHandled 在长按,组合键,双击打开Camera情况下,会被设置为 true
        mPowerKeyHandled = mPowerKeyHandled || hungUp
                || handledByPowerManager || mKeyCombinationManager.isPowerKeyIntercepted();
        if (!mPowerKeyHandled) {
            // power 事件没有被其它地方使用,那么在灭屏状态下执行亮屏
            if (!interactive) {
                wakeUpFromPowerKey(event.getDownTime());
            }
        } else {
            // power 事件被其它地方使用,那么重置规则
            if (!mSingleKeyGestureDetector.isKeyIntercepted(KEYCODE_POWER)) {
                mSingleKeyGestureDetector.reset();
            }
        }
    }

截断策略处理 power 按键的 DOWN 事件过程如下

  • 如果 power 按键事件没有被其它地方使用,那么,在非交互状态下,一般指灭屏状态,会执行亮屏。
  • 如果 power 按键事件被其它地方使用,那么重置按键手势。

这里我又有一个疑问,为何要把 power 亮屏的代码的在这里单独处理?在创建规则时设置一个回调,是不是更好呢?

结束

我在支援公司的某个项目时,偶然发现有人在截断策略中,要实现一个新的双击power功能,以及添加三击power的功能,可谓是把源码修改得"鸡飞狗跳",我当时还嗤之以鼻,现在发现我错怪他了。但是呢,由于他不知道如何修复这个源码的 bug,所以他实现的过程还是非常丑陋。

最后,给看我文章的人一个小福利,你可以参考 Input系统: InputReader 处理按键事件 ,学会从底层映射一个按键到上层,然后根据本文,实现按键的手势,这绝壁是一个非常叼的事情。当然,如果手势中要包括多击功能,还得解决本文提出的 Bug,这个不难,小小思考下就可以了。

以上就是Input系统截断策略的分析与应用详解的详细内容,更多关于Input系统截断策略的资料请关注我们其它相关文章!

(0)

相关推荐

  • Input系统之InputReader处理合成事件详解

    目录 正文 生成合成事件 加载并解析输入设备的配置 InputReader 处理合成事件 创建与配置 InputDevice 配置基本参数 配置坐标系 配置 Surface 小结 正文 Input系统: InputReader 概要性分析 把 InputReader 的事件分为了两类,一类是合成事件,例如设备的增.删事件,另一类是元输入事件,也就是操作设备产生的事件,例如手指在触摸屏上滑动. 本文承接前文,以设备的扫描过程为例,分析合成事件的产生与处理过程.虽然设备的扫描过程只会生成部分合成事件

  • go语言之美迅速打rpm包实现详解

    目录 引言 各组件版本 准备 开始 service配置 运行 总结 引言 之前写过一篇如何手操rpm包,这次写写go语言打包有多爽. 各组件版本 # git大版本小于2搞不了 git 2.2.1 go 1.13.6 准备 rpmbuild命令 sudo yum install -y gcc make rpm-build redhat-rpm-config vim lrzsz git 2.2.1 版本 先用源安装 yum install git -y 如果安装完发现git版本不对就手动安装 编译好

  • 从"Show tabs"了解Android Input系统

    目录 Input源码解读——从"Show tabs"开始 Settings 写入设置 InputManagerService监听设置 通过 InputReader 请求刷新配置 EventHub 唤醒 InputReader 线程 InputReader线程刷新配置 InputDevice配置变化 TouchInputMapper 进一步处理 创建和初始化 PointerController 初始化 PointerController 加载 Pointer 相关资源 显示Tap 总体流

  • Input系统之InputReader概要性实例分析

    目录 InputReader 的创建 EventHub 创建过程如下 InputReader 的运行 EventHub 提供事件 InputReader 的创建 从 InputManagerService: 创建与启动 可知,Input 系统的主要功能,主要集中在 native 层,并且Input 系统的 native 层又包含 InputReader, InputClassifer, InputDispatcher 三个子模块.本文来分析 InputReader 从创建到启动的基本流程,为后续

  • Input系统之InputReader处理按键事件详解

    目录 前言 认识按键事件 处理按键事件 扫描码映射按键码 结束 前言 前面几篇文章已经为 Input 系统的分析打好了基础,现在是时候进行更深入的分析了. 通常,手机是不带键盘的,但是手机上仍然有按键,就是我们经常使用的电源键以及音量键.因此还是有必要分析按键事件的处理流程. 那么,掌握按键事件的处理流程,对我们有什么用处呢?例如,手机上添加了一个功能按键,你知道如何把这个物理按键映射到上层,然后处理这个按键吗?又例如,如果设备是不需要电源键,但是系统默认把某一个按键映射为电源键,那么我们如何使

  • Java设计模式之策略模式定义与用法详解

    本文实例讲述了Java策略模式定义与用法.分享给大家供大家参考,具体如下: 一. 定义: 定义一系列算法,把他们一个一个封装起来,并且使他们可以相互替换. 二. 优点: (1)上下文(Context)和具体策略(ConcreteStrategy)是松耦合关系,因此上下文只需要知道他要使用某一个实现  Strategy接口类的实例,但不需要知道是哪个类. (2)策略模式满足开闭原则,当增加新的具体类时,不需要修改上下文类的代码,上下文即可以引用新的具体策略的实例. 三. 实例: 下面就通过一个问题

  • Android Broadcast原理分析之registerReceiver详解

    目录 1. BroadcastReceiver概述 2. BroadcastReceiver分类 3. registerReceiver流程图 4. 源码解析 4.1 ContextImpl.registerReceiverInternal 4.2 LoadedApk.getReceiverDispatcher 4.3 ActivityManagerService.registerReceiver 5. 总结 1. BroadcastReceiver概述 广播作为四大组件之一,在平时开发过程中会

  • Vue编译器源码分析compileToFunctions作用详解

    目录 引言 Vue.prototype.$mount函数体 源码出处 options.delimiters & options.comments compileToFunctions函数逐行分析 createFunction 函数源码 引言 Vue编译器源码分析 接上篇文章我们来分析:compileToFunctions的作用. 经过前面的讲解,我们已经知道了 compileToFunctions 的真正来源你可能会问为什么要弄的这么复杂?为了搞清楚这个问题,我们还需要继续接触完整的代码. 下面

  • MySQL慢查询分析工具pt-query-digest详解

    目录 一.简介 二.安装pt-query-digest 三.pt-query-digest语法及重要选项 四.分析pt-query-digest输出结果 五.用法示例 一.简介 pt-query-digest是用于分析mysql慢查询的一个工具,它可以分析binlog.General log.slowlog,也可以通过SHOWPROCESSLIST或者通过tcpdump抓取的MySQL协议数据来进行分析.可以把分析结果输出到文件中,分析过程是先对查询语句的条件进行参数化,然后对参数化以后的查询进

  • Java太阳系小游戏分析和源码详解

    最近看了面向对象的一些知识,然后跟着老师的讲解做了一个太阳系各行星绕太阳转的小游戏,来练习巩固一下最近学的知识: 用到知识点:类的继承.方法的重载与重写.多态.封装等 分析: 1.需要加载图片.画图 2.建一个面板,主页面 3.行星类 效果图: 先看一下源码结构图: 现在逐步分析各个类的功能: 1)工具类-----util包中 --Constant类   封装了游戏中用到的常量 --GameUtil类  封装了游戏的图片加载功能 --MyFrame类  封装了游戏面板的构造,用于各面板的父类 -

  • java集合类源码分析之Set详解

    Set集合与List一样,都是继承自Collection接口,常用的实现类有HashSet和TreeSet.值得注意的是,HashSet是通过HashMap来实现的而TreeSet是通过TreeMap来实现的,所以HashSet和TreeSet都没有自己的数据结构,具体可以归纳如下: •Set集合中的元素不能重复,即元素唯一 •HashSet按元素的哈希值存储,所以是无序的,并且最多允许一个null对象 •TreeSet按元素的大小存储,所以是有序的,并且不允许null对象 •Set集合没有ge

  • PHP调试及性能分析工具Xdebug详解

    程序开发过程中,一般用得最多的调试方法就是用echo.print_r().var_dump().printf()等将语句打印出来.对PHP脚本的执行效率,通常是脚本执行时间.对数据库SQL的效率,通常是数据库Query时间,但这样并不能真正定位和分析脚本执行和数据库查询的瓶颈所在?对此,有一个叫Xdebug(www.xdebug.org)的PHP程序调试器(即一个Debug工具),可以用来跟踪,调试和分析PHP程序的运行状况. 一.以windows平台对此模块的安装做简单的介绍: 1. 下载PH

  • JAVA 枚举单例模式及源码分析的实例详解

    JAVA 枚举单例模式及源码分析的实例详解 单例模式的实现有很多种,网上也分析了如今实现单利模式最好用枚举,好处不外乎三点: 1.线程安全 2.不会因为序列化而产生新实例 3.防止反射攻击但是貌似没有一篇文章解释ENUM单例如何实现了上述三点,请高手解释一下这三点: 关于第一点线程安全,从反编译后的类源码中可以看出也是通过类加载机制保证的,应该是这样吧(解决) 关于第二点序列化问题,有一篇文章说枚举类自己实现了readResolve()方法,所以抗序列化,这个方法是当前类自己实现的(解决) 关于

  • nginx源码分析线程池详解

    nginx源码分析线程池详解 一.前言 nginx是采用多进程模型,master和worker之间主要通过pipe管道的方式进行通信,多进程的优势就在于各个进程互不影响.但是经常会有人问道,nginx为什么不采用多线程模型(这个除了之前一篇文章讲到的情况,别的只有去问作者了,HAHA).其实,nginx代码中提供了一个thread_pool(线程池)的核心模块来处理多任务的.下面就本人对该thread_pool这个模块的理解来跟大家做些分享(文中错误.不足还请大家指出,谢谢) 二.thread_

  • Java集合框架源码分析之LinkedHashMap详解

    LinkedHashMap简介 LinkedHashMap是HashMap的子类,与HashMap有着同样的存储结构,但它加入了一个双向链表的头结点,将所有put到LinkedHashmap的节点一一串成了一个双向循环链表,因此它保留了节点插入的顺序,可以使节点的输出顺序与输入顺序相同. LinkedHashMap可以用来实现LRU算法(这会在下面的源码中进行分析). LinkedHashMap同样是非线程安全的,只在单线程环境下使用. LinkedHashMap源码剖析 LinkedHashM

随机推荐