遥控器组合键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。具体动作在这里实现。
- 创建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));
-
调用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都有调用,从执行顺序往下看;
- interceptKeyBeforeQueueing -> handleKeyGesture -> mKeyCombinationManager.interceptKey
- 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)的键值信息,查找是否有包含该按键的组合键规则,记录所有符合包含该按键的规则,并在数组中记录该按键。
-
第二个按键(按键2),
-
如果距离第一个按键的时间超出上限 COMBINE_KEY_DELAY_MILLIS(default 150),清空第一步记录的规则
-
如果距离第一个按键的时间在 COMBINE_KEY_DELAY_MILLIS内,记录该按键,并判断步骤1中的记录的规则中是否同时包含 按键1和按键2,以及其时间间隔是否满足COMBINE_KEY_DELAY_MILLIS,如果满足,则触发规则,执行规则的execute
-
-
松开按键,会记录松开按键的键值,并把步骤1中记录的按键和包含该按键的规则删除。并且执行规则cancel方法
那么,到这里就很清晰了。只要执行了规则的execute,那么就可以视为触发了规则。其实这一步很容易触发。同时按下两个键就足够了(只要没有手残一般150ms的限制都能满足),几乎是没有延迟的,会马上触发规则的execute,松开又会执行execute,利用这一点可以实现组合键长按执行定制操作。
有点跑偏了,回到最初的问题。为什么A款遥控器无法触发对应规则?
日志打印对比
A款遥控器按组合键的打印如下:


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

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

470

被折叠的 条评论
为什么被折叠?



