Bug description

喜马拉雅启动后显示用户协议相关 Dialog。这时不响应 touch event。复现后,回到 Home,重新进入也不恢复。

概率性发生的。重启后概率性恢复。

相关 log:

2713  2713 W ViewRootImpl[MainActivity]: Dropping event due to no window focus: MotionEvent { action=ACTION_DOWN, actionButton=0, id[0]=0, x[0]=934.4269, y[0]=460.23578, toolType[0]=TOOL_TYPE_FINGER, buttonState=0, metaState=0, flags=0x0, edgeFlags=0x0, pointerCount=1, historySize=0, eventTime=89426937, downTime=89426937, deviceId=4, source=0x1002 }

Recurrence step

这是跑 monkey 的时候复现的。手动复现步骤如下:

Root cause

直接原因是 ViewRootImpl 对象的 mStopped 字段为 true。


// frameworks/base/core/java/android/view/ViewRootImpl.java
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)
            || mStopped
            || (mIsAmbientMode && !q.mEvent.isFromSource(InputDevice.SOURCE_CLASS_BUTTON))
            || (mPausedForTransition && !isBack(q.mEvent))) {
        // Drop non-terminal input events.
        Slog.w(mTag, "Dropping event due to no window focus: " + q.mEvent);
        return true;
    }
    return false;
}

mStopped 表示相关 Window 的状态,如果是 true 则表明 inactive,所以会 drop event。而 mStopped 就是在 Activity 的 performRestart()performStop() 方法中改变的。

// frameworks/base/core/java/android/app/Activity.java
final void performRestart() {
    if (mToken != null && mParent == null) {
        // No need to check mStopped, the roots will check if they were actually stopped.
        WindowManagerGlobal.getInstance().setStoppedState(mToken, false /* stopped */);
}}
final void performStop(boolean preserveWindow) {
    // If we're preserving the window, don't setStoppedState to true, since we
    // need the window started immediately again. Stopping the window will
    // destroys hardware resources and causes flicker.
    if (!preserveWindow && mToken != null && mParent == null) {
        WindowManagerGlobal.getInstance().setStoppedState(mToken, true);
}}

// frameworks/base/core/java/android/view/WindowManagerGlobal.java
public void setStoppedState(IBinder token, boolean stopped) {
    synchronized (mLock) {
        int count = mViews.size();
        for (int i = 0; i < count; i++) {
            if (token == null || mParams.get(i).token == token) {
                ViewRootImpl root = mRoots.get(i);
                root.setWindowStopped(stopped);
}}}}

// frameworks/base/core/java/android/view/ViewRootImpl.java
// Set to true if the owner of this window is in the stopped state,
// so the window should no longer be active.
boolean mStopped = false;
void setWindowStopped(boolean stopped) {
    if (mStopped != stopped) {
        mStopped = stopped;
        ...
}}

注意 setStoppedState() 方法的第一个参数是 token,这是 WindowManager.LayoutParams 类的 token 字段。用 token 来表示改变 mStopped 的是哪一个 Window。这里会遍历所有 ViewRootImpl,如果 token 相同则调用 setWindowStopped() 方法。

接下来看一下,这个 token 是怎么来的。对于一个 dialog window,它的 token 是在 addView() 时赋值的,其实就是启动该 dialog 的 activity window 的 mAppToken。最终是在 adjustLayoutParamsForSubWindow() 方法中赋值的。

// frameworks/base/core/java/android/view/WindowManagerImpl.java
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
// frameworks/base/core/java/android/view/WindowManagerGlobal.java
public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {
    final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
    if (parentWindow != null) {
	        parentWindow.adjustLayoutParamsForSubWindow(wparams);
    }
    ViewRootImpl root;
    root = new ViewRootImpl(view.getContext(), display);
    view.setLayoutParams(wparams);
    mViews.add(view);
    mRoots.add(root);
    mParams.add(wparams);

    root.setView(view, wparams, panelParentView);
}

// frameworks/base/core/java/android/view/Window.java
void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
    if (wp.token == null) {
        wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
}}