遥控器组合键OK+BACK抓取BugReport异常解析

遥控器组合键OK+BACK抓取BugReport异常解析

问题背景

现象:AndroidTV设备,多款遥控器中A款遥控器OK+BACK未能触发抓取Bugreport机制,其它款遥控器没有问题。

抓取Bugreport流程

简单来说,抓取bugreport这一动作由shell apk实现。接受一个Action触发,com.android.internal.intent.action.BUGREPORT_REQUESTED. 接受的广播代码位于

frameworks\base\packages\Shell\src\com\android\shell\BugreportRequestedReceiver.java

// frameworks\base\packages\Shell\src\com\android\shell\BugreportRequestedReceiver.java
Intent serviceIntent = new Intent(context, BugreportProgressService.class);
Log.d(TAG, "onReceive() ACTION: " + serviceIntent.getAction());
serviceIntent.setAction(intent.getAction());
serviceIntent.putExtra(EXTRA_ORIGINAL_INTENT, intent);
context.startService(serviceIntent);
// frameworks\base\packages\Shell\AndroidManifest.xml
<receiver
    android:name=".BugreportRequestedReceiver"
    android:exported="true"
    android:permission="android.permission.TRIGGER_SHELL_BUGREPORT">
    <intent-filter>
        <action android:name="com.android.internal.intent.action.BUGREPORT_REQUESTED" />
    </intent-filter>
</receiver>

可以看到,广播接收到Action之后启动了BugreportProgressService。具体动作在这里实现。

  1. 创建Notification,提示用户
NotificationManager nm = NotificationManager.from(mContext);
nm.createNotificationChannel(
        new NotificationChannel(NOTIFICATION_CHANNEL_ID,
                mContext.getString(R.string.bugreport_notification_channel),
                isTv(this) ? NotificationManager.IMPORTANCE_DEFAULT
                        : NotificationManager.IMPORTANCE_LOW));
  1. 调用mBugreportManager.startBugreport实现抓取bugreport。这个过程可以设置回调获取当前进度。

    BugreportCallbackImpl bugreportCallback = new BugreportCallbackImpl(info);
    try {
        synchronized (mLock) {
            mBugreportManager.startBugreport(bugreportFd, screenshotFd,
                    new BugreportParams(bugreportType), executor, bugreportCallback);
            bugreportCallback.trackInfoWithIdLocked();
        }
    } catch (RuntimeException e) {
        Log.i(TAG, "Error in generating bugreports: ", e);
        // The binder call didn't go through successfully, so need to close the fds.
        // If the calls went through API takes ownership.
        FileUtils.closeQuietly(bugreportFd);
        if (screenshotFd != null) {
            FileUtils.closeQuietly(screenshotFd);
        }
    }
    

这部分代码不做过多解析。总之,是Shell通过接受特定action实现

那么问题来了,组合键是怎么触发这一流程的?

组合键触发抓取Bugreport流程

上面提到,整个过程是由shell接受action实现的。那么是组合键是如何将这个action发出去的,具体由谁来发?从日志中竟然没有发现发现Action的进程,一度怀疑这玩意是由遥控器直接发出来的。后面想想Action这是Android定制的对象,遥控器按键似乎没有发Action的逻辑。还是继续代码看处理按键的逻辑

添加和初始化组合键规则

在PhoneWindowManager.java中,制定好了一套规则。在PhoneWindowManager初始化的时候通过KeyCombinationManager添加进去。当触发规则时,可以执行TwoKeysCombinationRule.excute()方法,在excute中定制组合按键要执行的操作。其中 interceptBugreportGestureTv就是具体的发送抓取bugreport广播的逻辑, cancelBugreportGestureTv在抓取bugreport前取消该动作,基于Handler消息机制实现。

// PhoneWindowManager.java
mKeyCombinationManager.addRule(
        new TwoKeysCombinationRule(KEYCODE_DPAD_CENTER, KEYCODE_BACK) {
            @Override
            void execute() {
                Log.d(TAG, "rule execute: exec");
                mBackKeyHandled = true;
                interceptBugreportGestureTv();
            }
            @Override
            void cancel() {
                Log.d(TAG, "rule cancel: exec");
                cancelBugreportGestureTv();
            }
            @Override
            long getKeyInterceptDelayMs() {
                return 0;
            }
        });


/**
 * TV only: recognizes a remote control gesture for capturing a bug report.
 */
private void interceptBugreportGestureTv() {
    Log.d(TAG, "interceptBugreportGestureTv: exec");
    mHandler.removeMessages(MSG_BUGREPORT_TV);
    // The bugreport capture chord is a long press on DPAD CENTER and BACK simultaneously.
    Message msg = Message.obtain(mHandler, MSG_BUGREPORT_TV);
    msg.setAsynchronous(true);
    mHandler.sendMessageDelayed(msg, BUGREPORT_TV_GESTURE_TIMEOUT_MILLIS);	// dealy 1000ms to send msg
}

private void cancelBugreportGestureTv() {
    Log.d(TAG, "cancelBugreportGestureTv: exec");
    mHandler.removeMessages(MSG_BUGREPORT_TV);
}

添加这个组合键规则之后,系统是如何触发呢?具体来看KeyCombinationManager的实现逻辑。

触发组合键

在PhoneWindowManager中除了添加组合按键Rule的时候,还有多处用到了KeyCombinationManager,代码如下:

public long interceptKeyBeforeDispatching(IBinder focusedToken, KeyEvent event,
        int policyFlags) {
    if (mKeyCombinationManager.isKeyConsumed(event)) {
        Log.d(TAG, "interceptKeyBeforeDispatching: exec, current was consumed by KeyCombinationManager.");
        return key_consumed;
    }
    if ((flags & KeyEvent.FLAG_FALLBACK) == 0) {
        final long now = SystemClock.uptimeMillis();
        final long interceptTimeout = mKeyCombinationManager.getKeyInterceptTimeout(keyCode);
        Log.d(TAG, "interceptKeyBeforeDispatching: exec, now=" + now + ", interceptTimeout=" + interceptTimeout);
        if (now < interceptTimeout) {
            return interceptTimeout - now;
        }
    }
}



public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
	//...
    if ((event.getFlags() & KeyEvent.FLAG_FALLBACK) == 0) {
        handleKeyGesture(event, interactiveAndOn);
    }
    //...
}



private void handleKeyGesture(KeyEvent event, boolean interactive) {
    if (mKeyCombinationManager.interceptKey(event, interactive)) {
        // handled by combo keys manager.
        Log.d(TAG, "handleKeyGesture: exec, handled by combo keys manager");
        mSingleKeyGestureDetector.reset();
        return;
    }
    //...
    mSingleKeyGestureDetector.interceptKey(event, interactive);
}

interceptKeyBeforeQueueing 以及 interceptKeyBeforeDispatching都有调用,从执行顺序往下看;

  1. interceptKeyBeforeQueueing -> handleKeyGesture -> mKeyCombinationManager.interceptKey
  2. interceptKeyBeforeDispatching -> mKeyCombinationManager.isKeyConsumed
    interceptKeyBeforeDispatching -> mKeyCombinationManager.getKeyInterceptTimeout

都是直接调用的KeyCombinationManager API,直接上KeyCombinationManager源码。

mKeyCombinationManager.interceptKey

//KeyCombinationManager.java
/**
 * Check if the key event could be intercepted by combination key rule before it is dispatched
 * to a window.
 * Return true if any active rule could be triggered by the key event, otherwise false.
 */
boolean interceptKey(KeyEvent event, boolean interactive) {
    synchronized (mLock) {
        return interceptKeyLocked(event, interactive);
    }
}
private boolean interceptKeyLocked(KeyEvent event, boolean interactive) {
    final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
    final int keyCode = event.getKeyCode();
    final int count = mActiveRules.size();
    final long eventTime = event.getEventTime();
    if (interactive && down) {
        if (mDownTimes.size() > 0) {    // already pressed at least one key
            if (count > 0 // has some key down, and some rule contain this key.
                    && eventTime > mDownTimes.valueAt(0) + COMBINE_KEY_DELAY_MILLIS) {
                Log.d(TAG, "interceptKeyLocked: time exceed, cancel");
                // exceed time from first key down.
                forAllRules(mActiveRules, (rule)-> rule.cancel());
                mActiveRules.clear();
                return false;
            } else if (count == 0) { // has some key down but no rule contains this key.
                return false;
            }
        }
        if (mDownTimes.get(keyCode) == 0) { // save keycode and eventTime when the key is firstly pressed
            mDownTimes.put(keyCode, eventTime);
        } else {
            // ignore old key, maybe a repeat key.
            return false;   // exit when key repeated
        }
        if (mDownTimes.size() == 1) {   // only one key was pressed
            mTriggeredRule = null;
            // check first key and pick active rules.
            forAllRules(mRules, (rule)-> {
                if (rule.shouldInterceptKey(keyCode)) { // find if there is any rule containing this key
                    mActiveRules.add(rule); // maybe serveral rules contain the first key
                }
            });
        } else {    // more than one key were pressed
            // Ignore if rule already triggered.
            if (mTriggeredRule != null) {   // the pressed has already trigger some rule, avoid triggering same rule in a short time
                return true;
            }
            // check if second key can trigger rule, or remove the non-match rule.
            forAllActiveRules((rule) -> {
                if (!rule.shouldInterceptKeys(mDownTimes)) {// all keys combinations not exceed time
                    return false;
                }
                Log.v(TAG, "Performing combination rule : " + rule);    // got the right rule
                mHandler.post(rule::execute);
                mTriggeredRule = rule;
                return true;
            });
            mActiveRules.clear();   // clear all the rules added before when pressed first key
            if (mTriggeredRule != null) {
                mActiveRules.add(mTriggeredRule);   // add the triggered rule
                return true;
            }
        }
    } else {    // 松开按键
        Log.d(TAG, "interceptKeyLocked: key up event, event=" + event);
        mDownTimes.delete(keyCode);
        for (int index = count - 1; index >= 0; index--) {
            final TwoKeysCombinationRule rule = mActiveRules.get(index);
            if (rule.shouldInterceptKey(keyCode)) {
                mHandler.post(rule::cancel);
                mActiveRules.remove(index);
            }
        }
    }
    return false;
}

代码就有点啰嗦,大概说下流程:

  1. 同时按下组合键按键,其实也有一个先后顺序。通过第一个按键(按键1)的键值信息,查找是否有包含该按键的组合键规则,记录所有符合包含该按键的规则,并在数组中记录该按键。

  2. 第二个按键(按键2),

    • 如果距离第一个按键的时间超出上限 COMBINE_KEY_DELAY_MILLIS(default 150),清空第一步记录的规则

    • 如果距离第一个按键的时间在 COMBINE_KEY_DELAY_MILLIS内,记录该按键,并判断步骤1中的记录的规则中是否同时包含 按键1和按键2,以及其时间间隔是否满足COMBINE_KEY_DELAY_MILLIS,如果满足,则触发规则,执行规则的execute

  3. 松开按键,会记录松开按键的键值,并把步骤1中记录的按键和包含该按键的规则删除。并且执行规则cancel方法

那么,到这里就很清晰了。只要执行了规则的execute,那么就可以视为触发了规则。其实这一步很容易触发。同时按下两个键就足够了(只要没有手残一般150ms的限制都能满足),几乎是没有延迟的,会马上触发规则的execute,松开又会执行execute,利用这一点可以实现组合键长按执行定制操作。

有点跑偏了,回到最初的问题。为什么A款遥控器无法触发对应规则?

日志打印对比

A款遥控器按组合键的打印如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传在这里插入图片描述

其它款遥控器的打印如下:

在这里插入图片描述

区别很明显,A款遥控器居然多出了ACTION_UP, 即便按键一直都是按下状态。多次试验确认,A款遥控器长按一个按键时,如果同时按另外一个按键就会触发之前按键的ACTION_UP事件。这就会导致即便找到了对应的组合键规则,也会因为其中一个按键的ACTION_UP事件把规则清除掉,组合键就永远无法触发。即便已经触发了对应的组合键规则,也会执行规则的cancel。效果类似于按下组合键的第二个键时,又松开了第一个按键

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值