Android P (API 28) 弹窗按键无响应 Dropping event due to no window focus

Android P (API 28) 弹窗按键无响应 Dropping event due to no window focus

发现在Android 28 ( P ) 版本上出现弹窗之后,按键无反应,只能重启,而其他版本的手机却没有问题

通过抓日志,看到在按键的时候,日志里不断出现下面两条日志:

W/ViewRootImpl: Dropping event due to no window focus: KeyEvent { action=ACTION_DOWN, keyCode=KEYCODE_DPAD_DOWN, scanCode=108, metaState=0, flags=0x8, repeatCount=0, eventTime=95320779, downTime=95320779, deviceId=5, source=0x301 }
W/ViewRootImpl: Cancelling event due to no window focus: KeyEvent { action=ACTION_UP, keyCode=KEYCODE_DPAD_CENTER, scanCode=28, metaState=0, flags=0x28, repeatCount=0, eventTime=95325159, downTime=95324964, deviceId=5, source=0x301 }
W/ViewRootImpl: Cancelling event due to no window focus: KeyEvent { action=ACTION_UP, keyCode=KEYCODE_DPAD_CENTER, scanCode=28, metaState=0, flags=0x28, repeatCount=0, eventTime=95325159, downTime=95324964, deviceId=5, source=0x301 }
W/ViewRootImpl: Cancelling event due to no window focus: KeyEvent { action=ACTION_UP, keyCode=KEYCODE_DPAD_CENTER, scanCode=28, metaState=0, flags=0x28, repeatCount=0, eventTime=95325159, downTime=95324964, deviceId=5, source=0x301 }
W/ViewRootImpl: Cancelling event due to no window focus: KeyEvent { action=ACTION_UP, keyCode=KEYCODE_DPAD_CENTER, scanCode=28, metaState=0, flags=0x28, repeatCount=0, eventTime=95325159, downTime=95324964, deviceId=5, source=0x301 }
W/ViewRootImpl: Cancelling event due to no window focus: KeyEvent { action=ACTION_UP, keyCode=KEYCODE_DPAD_CENTER, scanCode=28, metaState=0, flags=0x28, repeatCount=0, eventTime=95325159, downTime=95324964, deviceId=5, source=0x301 }
W/ViewRootImpl: Cancelling event due to no window focus: KeyEvent { action=ACTION_UP, keyCode=KEYCODE_DPAD_CENTER, scanCode=28, metaState=0, flags=0x28, repeatCount=0, eventTime=95325159, downTime=95324964, deviceId=5, source=0x301 }
W/ViewRootImpl: Cancelling event due to no window focus: KeyEvent { action=ACTION_UP, keyCode=KEYCODE_DPAD_CENTER, scanCode=28, metaState=0, flags=0x28, repeatCount=0, eventTime=95325159, downTime=95324964, deviceId=5, source=0x301 }
W/ViewRootImpl: Cancelling event due to no window focus: KeyEvent { action=ACTION_UP, keyCode=KEYCODE_DPAD_CENTER, scanCode=28, metaState=0, flags=0x28, repeatCount=0, eventTime=95325159, downTime=95324964, deviceId=5, source=0x301 }
W/ViewRootImpl: Cancelling event due to no window focus: KeyEvent { action=ACTION_UP, keyCode=KEYCODE_DPAD_CENTER, scanCode=28, metaState=0, flags=0x28, repeatCount=0, eventTime=95325159, downTime=95324964, deviceId=5, source=0x301 }

跟踪源码发现是在 ViewRootImpl 中抛出的异常,可以看到有四种情况可以进入该异常

protected boolean shouldDropInputEvent(QueuedInputEvent q) {
    if (mView == null || !mAdded) {
        Slog.w(mTag, "Dropping event due to root view being removed: " + q.mEvent);
        return true;
    } else if ((!mAttachInfo.mHasWindowFocus
            && !q.mEvent.isFromSource(InputDevice.SOURCE_CLASS_POINTER)
            && !isAutofillUiShowing()) || mStopped
            || (mIsAmbientMode && !q.mEvent.isFromSource(InputDevice.SOURCE_CLASS_BUTTON))
            || (mPausedForTransition && !isBack(q.mEvent))) {
        // This is a focus event and the window doesn't currently have input focus or
        // has stopped. This could be an event that came back from the previous stage
        // but the window has lost focus or stopped in the meantime.
        if (isTerminalInputEvent(q.mEvent)) {
            // Don't drop terminal input events, however mark them as canceled.
            q.mEvent.cancel();
            Slog.w(mTag, "Cancelling event due to no window focus: " + q.mEvent);
            return false;
        }
 
        // Drop non-terminal input events.
        Slog.w(mTag, "Dropping event due to no window focus: " + q.mEvent);
        return true;
    }
    return false;
}

断点调试不好使,最终通过打印日志定位问题
mAttachInfo.mHasWindowFocus = false
mIsAmbientMode = false
mStopped = false
mPausedForTransition = false

通过判断最终确定是由于 mAttachInfo.mHasWindowFocus = false,导致一直无法处理按键事件,抛出按键异常。

为什么 mAttachInfo.mHasWindowFocus = false呢?明明应该是当前Dialog获得了焦点,却是false呢?

下面对mAttachInfo.mHasWindowFocus赋值的地方打印日志进行判断

正常情况下的流程

  1. windowFocusChanged
  2. performTraversals
    mAttachInfo.mHasWindowFocus = false
  3. handleWindowFocusChanged
    mAttachInfo.mHasWindowFocus = true
    最终mAttachInfo.mHasWindowFocus = true

异常情况下的流程

  1. windowFocusChanged
  2. deliverInputEvent
    mAttachInfo.mHasWindowFocus = false
  3. handleWindowFocusChanged
    mAttachInfo.mHasWindowFocus = true
  4. performTraversals
    mAttachInfo.mHasWindowFocus = false
    最终mAttachInfo.mHasWindowFocus = false

通过流程对比可以发现 ,主要是handleWindowFocusChanged和performTraversals的流程调换了导致mAttachInfo.mHasWindowFocus = false

下面是上面涉及的所有主要方法

// 异步Binder线程执行
public void windowFocusChanged(boolean hasFocus, boolean inTouchMode) {
    synchronized (this) {
        mWindowFocusChanged = true;
        mUpcomingWindowFocus = hasFocus;
        mUpcomingInTouchMode = inTouchMode;
    }
    Message msg = Message.obtain();
    msg.what = MSG_WINDOW_FOCUS_CHANGED;
    mHandler.sendMessage(msg);
}
 
// Handler里面执行
private void handleWindowFocusChanged() {
    ...
    mAttachInfo.mHasWindowFocus = hasWindowFocus;
    ...
}
 
// Handler里面执行按键事件
private void deliverInputEvent(QueuedInputEvent q) {
    ...
    if (stage != null) {
        // Android 28 增加了这一行,28以下没有这一行
        handleWindowFocusChanged();
        stage.deliver(q);
    } else {
        finishInputEvent(q);
    }
}
 
Dialog.show()-->WindowManagerImpl.addView(mDecor, l)-->WindowManagerGlobal.addView()-->ViewRootImpl.setView()-->requestLayout()-->scheduleTraversals()
 
// 对Handler加锁,只能执行async的方法,普通Handler暂不执行
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
 
// 继续执行Input的Handler消息
scheduleConsumeBatchedInput()-->doProcessInputEvents()-->deliverInputEvent(q)
 
// 对Handler加锁,以后可以执行普通Handler
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
 
// 解锁之后,马上执行performTraversals,然后执行其他普通Handler
private void performTraversals() {
    ···
    if (mFirst) {
        ...
        // Android 29 删除了这一行
        mAttachInfo.mHasWindowFocus = false;
        ...
    }
    ···
    mFirst = false;
    ...
}

Dialog.show() 方法最终会对Handler加锁,除了deliverInputEvent方法,只能先运行performTraversals方法,所以如果没有按键事件deliverInputEvent,handleWindowFocusChanged正常会在performTraversals之后

有按键事件deliverInputEvent,handleWindowFocusChanged就会在performTraversals之前,造成mAttachInfo.mHasWindowFocus 始终为 false,出现按键异常,无法获焦

那为什么Android 29和Android 27及以下的版本没问题呢?
1、Android 29没问题

在performTraversals删了mAttachInfo.mHasWindowFocus = false代码,所以自然mAttachInfo.mHasWindowFocus != false

只会在Android 28 版本出现
private void performTraversals() {
    ···
    if (mFirst) {
        ...
        // Android 29 删除了这一行
        mAttachInfo.mHasWindowFocus = false;
        ...
    }
    ···
    mFirst = false;
    ...
}
2、Android 27及以下没问题

Android 28版本在deliverInputEvent增加了一行代码,也就是增加处理handleWindowFocusChanged逻辑。

所以Android 27及以下,在deliverInputEvent里面根本就没有handleWindowFocusChanged处理,handleWindowFocusChanged就放在了performTraversals之后

// deliverInputEvent按键事件里面,Android 28 增加了这一行,27及以下没有这一行
private void deliverInputEvent(QueuedInputEvent q) {
    ...
    if (stage != null) {
        // Android 28 增加了这一行,27及以下没有这一行
        handleWindowFocusChanged();
        stage.deliver(q);
    } else {
        finishInputEvent(q);
    }
}
解决办法

在Dialog、PopupWindow、DialogFragment都会有该问题

参考解决办法,在dispatchKeyEvent方法里面,通过反射把mAttachInfo.mHasWindowFocus重置为true

我是在TV上遇到的,都是用按键操作的,所有可以用dispatchKeyEvent。不过手机上源码也是一样的,可以用类似的解决办法,在合适的时机如dispatchTouchEvent,通过反射把mAttachInfo.mHasWindowFocus重置为true

@Override
public boolean dispatchKeyEvent(@NonNull KeyEvent event) {
    if (Build.VERSION.SDK_INT == 28) {
        LogUtils.d(TAG, event.toString());
        try {
            ViewParent viewRootImpl = getWindow().getDecorView().getParent();
            Class viewRootImplClass = viewRootImpl.getClass();
 
            Field mAttachInfoField = viewRootImplClass.getDeclaredField("mAttachInfo");
            mAttachInfoField.setAccessible(true);
            Object mAttachInfo = mAttachInfoField.get(viewRootImpl);
            Class mAttachInfoClass = mAttachInfo.getClass();
 
            Field mHasWindowFocusField = mAttachInfoClass.getDeclaredField("mHasWindowFocus");
            mHasWindowFocusField.setAccessible(true);
            mHasWindowFocusField.set(mAttachInfo, true);
            boolean mHasWindowFocus = (boolean) mHasWindowFocusField.get(mAttachInfo);
 
            LogUtils.i(TAG, "dispatchKeyEvent set mHasWindowFocus to true and get it as: ", mHasWindowFocus);
        } catch (Exception e) {
            LogUtils.i(TAG, "dispatchKeyEvent set mHasWindowFocus to true error: ", e.toString());
            e.printStackTrace();
        }
    }
 
    return super.dispatchKeyEvent(event);
}

note: PopupWindow 里面没有 dispatchKeyEvent 方法,但是里面设置的 contentView 有该方法,只要通过 contentView 找到 ViewRootImpl,然后在通过反射设置即可。

  • 8
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 11
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值