개발

Java layer key delivery of Android key distribution process

JUNIT74 2022. 11. 14. 15:37

Android에서 키이벤트가 어떤 순서로 전달이 되는지 잘 보여준다.

 

https://blog.fearcat.in/a?ID=00550-0b3af172-8163-456d-bc6c-621154e1ebf0 

 

Java layer key delivery of Android key distribution process - Fear Cat

Java layer key transfer of Android input subsystem Platform: Android 6.0 In Android development, when customizing Activity and View, onKeyDown, onKeyUp, dispatchKeyEvent are often rewritten. At the same time, View also has setOnKeyListener, etc. When a key

blog.fearcat.in

 

아래 Step들은 키가 어떻게 등록되고 모니터링되는지에 대한 순서를 설명한다.

우선 InputManagerService가 시작된 이후 InputManagerService는 키이벤트를 모니터링하게되는데 키이벤트가 발생되었을 때 InputManagerService는 현재 Activate된 Window에 전달한다.

그럼 InputManagerService에서 현재 Activated된 Window에 어떻게 키를 전달할 수 있을까?

Window는 InputManagerSerivce에 키를 입력받을 수 있도록 InputChannel을 등록한다. 좀 더 자세하게 살펴보면 Activity가 시작할 때 ViewRootImpl을 하나 생성하고 setView() 메소드에서 Activity의 DecorView가 세팅된다. 그리고 Activity는 키이벤트를 받기위한 InputChannel을 등록한다.

 

setView()에는 키이벤트를 받기 위한 아래와 같은 중요 기능들을 포함한다.

1. Activity가 현재 활성화되어 있다는 것을 InputManagerService에 알려주기위해 requestLayout()을 호출하고 동시에 InputDispatcher에 자신의 Window를 등록하고

2. Java layer에서 키를 전달받는 WindowInputEventReceiver를 생성한다.

다음으로는 Native의 InputManagerService에 키이벤트 수신채널을 서버측에 등록을 위해 mWindowSession의 addToDisplayAsUser()를 호출하는 것이다. (mWindowSession(IWindowSession)은 위의 그림과 같이 ViewRootImpl과 시스템의 WindowManagerSerivce간의 AIDL 인터페이스이다)

그리고 클라이언트측에 해당 어플리케이션의 Message Looper에 등록하여 InputManagerService가 키이벤트를 모니터링할 때 현재 활성화된 Window와 InputManagerSerivce안의 키이벤트 수신채널(InputChannel)을 찾고 이 InputChannel을 통하여 서버에서 클라이언트의 Message Looper에 notify한다.

그렇게 함으로 키이벤트는 현재 활성화된 수신채널에 등록된 어플리케이션의 Activity에 전달된다.

 

Hardware 키이벤트 발생시 Java단의 키 전달은

 

ViewRootImpl의 WindowInputEventReceiver안의 onInputEvent(InputEvent) 함수로 부터 시작된다. InputDispatcher가 키이벤트를 처리할 때 키는 현재 활성화된 Window의 InputChannel로 전달된다. 이후 NativeInputEventReceiver의 handleEvent()를 호출하고 마침내 InputEventReceiver의 onInputEvent(InputEvent) 콜백함수가 호출된다.

 

onInputEvnet(InputEvent)함수는 ViewRootImpl의 enqueueInputEvent(InputEvent, InputEventReceiver, flags, processImmediately )를 호출하고 processImmediately가 ture여서 즉시 키 이벤트가 처리되어야하는 경우 doProcessInputEvents() -> deliverInputEvent(QueuedInputEvent_ 순으로 호출한다.

InputStage의 deliver(QueuedInputEvent) 함수는 deliverInputEvent 함수내에서 호출된다.

 

deliver()함수를 포함하는 InputStage는 위와 같이 abstract(추상) 클래스이며 chain of responsibility 패턴을 따르고 있다고 설명되어있다.

 

chain of responsibility 패턴은 위의 설명대로 이벤트가 deliver()함수에 의해 각 InputStage를 상속받은 클래스에 전달이 되었을 때 해당 이벤트를 처리하거나 다음 InputStage에 전달하는 방식을 말한다.

아래 그림은 InputStage를 상속받는 클래스들이며 ViewRootImpl의 setView() 함수안에서 구성된다.

 

실제로 키이벤트 분배는 ViewRootImpl.deliverInputEvent에서 시작되고 deliver()함수는 여기서 호출이 된다.

 

    private void deliverInputEvent(QueuedInputEvent q) {
        Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, "deliverInputEvent",
                q.mEvent.getSequenceNumber());
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onInputEvent(q.mEvent, 0);
        }

        InputStage stage;
        if (q.shouldSendToSynthesizer()) {
            stage = mSyntheticInputStage;
        } else {
            stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;
        }

        if (q.mEvent instanceof KeyEvent) {
            mUnhandledKeyManager.preDispatch((KeyEvent) q.mEvent);
        }

        if (stage != null) {
            handleWindowFocusChanged();
            stage.deliver(q);
        } else {
            finishInputEvent(q);
        }
    }

 

처음으로 수행되는 InputStage는 위의 코드에서 QueuedInputEvent의 상태에 따라 결정되는데 q.shouldSendToSynthesizer()는 일반적으로 여기에서 false이며 주로 "stage = q.shulidSkipIme() ? mFirstPostImeInputStage : mFirstInputStage" 구문에서 shouldSkipIme가 실제 QueuedInputEvent가 enqueueInputEvent(event, this, 0, true)에 의해 생성될 때 세번째 파라미터인 0이므로 shouldSkipIme가 false이므로 mFirstPostImeInputStage로 분배될 것이다. mFirstPostImeInputStage는 ViewRootImpl의 setView가 호출될 때 NativePreImeInputStage가 assign된다.

 

다음으로는 Activity와 View에 관련된 InputStage인 ViewPostImeInputStage에 대해서 알아본다.

ViewRootImpl에 정의되어있는 ViewPostImeInputStage는 processKeyEvent() 함수가 정의되어 있고 아래와 같다.

InputStage의 deliver() ViewPostImeInputStage의 onProcess()를 호출하게되고 processKeyEvent()는 onProcess()에서 KeyEvent일 경우 호출되게 된다.

 

private int processKeyEvent(QueuedInputEvent q) {
    final KeyEvent event = (KeyEvent)q.mEvent;

    if (mUnhandledKeyManager.preViewDispatch(event)) {
        return FINISH_HANDLED;
    }

    // Deliver the key to the view hierarchy.
    if (mView.dispatchKeyEvent(event)) {
        return FINISH_HANDLED;
    }

    ...

    // This dispatch is for windows that don't have a Window.Callback. Otherwise,
    // the Window.Callback usually will have already called this (see
    // DecorView.superDispatchKeyEvent) leaving this call a no-op.
    if (mUnhandledKeyManager.dispatch(mView, event)) {
        return FINISH_HANDLED;
    }

    int groupNavigationDirection = 0;

    if (event.getAction() == KeyEvent.ACTION_DOWN
            && event.getKeyCode() == KeyEvent.KEYCODE_TAB) {
        if (KeyEvent.metaStateHasModifiers(event.getMetaState(), KeyEvent.META_META_ON)) {
            groupNavigationDirection = View.FOCUS_FORWARD;
        } else if (KeyEvent.metaStateHasModifiers(event.getMetaState(),
                KeyEvent.META_META_ON | KeyEvent.META_SHIFT_ON)) {
            groupNavigationDirection = View.FOCUS_BACKWARD;
        }
    }

    ...

    // Apply the fallback event policy.
    if (mFallbackEventHandler.dispatchKeyEvent(event)) {
        return FINISH_HANDLED;
    }
    if (shouldDropInputEvent(q)) {
        return FINISH_NOT_HANDLED;
    }

    // Handle automatic focus changes.
    if (event.getAction() == KeyEvent.ACTION_DOWN) {
        if (groupNavigationDirection != 0) {
            if (performKeyboardGroupNavigation(groupNavigationDirection)) {
                return FINISH_HANDLED;
            }
        } else {
            if (performFocusNavigation(event)) {
                return FINISH_HANDLED;
            }
        }
    }
    return FORWARD;
}

 

위의 코드를 보면

첫번째로 DecorView의 dispatchKeyEvent() 함수를 호출하여 키를 전달한다. DecorView는 View 계층에서 최상위 노드이다. 키이벤트는 최상위 노드부터 포커스가 있는 View의 경로를 따라 분배된다.

그 다음으로는 View 계층에서 처리되지 않은 키에 대해서 dispatch() 함수를 호출하고 세 번째로는 탭키를  처리한다음 4방향키에 대해서 포커스를 이동한다.

 

public boolean dispatchKeyEvent(KeyEvent event) {

    ...
    
    if (!mWindow.isDestroyed()) {
        final Window.Callback cb = mWindow.getCallback();
        final boolean handled = cb != null && mFeatureId < 0 ? cb.dispatchKeyEvent(event)
                : super.dispatchKeyEvent(event);
        if (handled) {
            return true;
        }
    }
    
    return isDown ? mWindow.onKeyDown(mFeatureId, event.getKeyCode(), event)
            : mWindow.onKeyUp(mFeatureId, event.getKeyCode(), event);
}

 

위 코드는  DecorVeiw의 dispatchKeyEvent() 함수이며 mWindow.getCallback()은 Activity이다.

소스에서 보듯이 Callback이 존재하고 mFeatureId가 0보다 작을 경우에는 Activity의 dispatchKeyEvent()가 호출되고 그렇지 않을 경우에는 상위 클래스인 View의 dispatchKeyEvent()가 호출된다. 아래는 DecorView의 Heierachy를 보여준다.

 

 

바로 위에도 언급되었지만 Activity는 Window.Callback 인터페이스를 implement하고 있고 attach() 함수안에서 자신을 Window.setCallback() 함수를 통해서 Window에 자신을 등록한다. 따라서 Callback은 null이 될 수 없다.

DecorView는 Window가 초기화될 때 installDecor() 함수안에서 생성되고 mFeatureId는 이 함수안에서 -1로 초기화되므로  DecorView의 dispatchKeyEvent() 에서 항상 Activity의 dispatchKeyEvent() 함수가 호출되고 키이벤트는 View안에서 분배된다.

 

Activity.dispatchKeyEvent(KeyEvent event)

public boolean dispatchKeyEvent(KeyEvent event) {
    onUserInteraction();

    // Let action bars open menus in response to the menu key prioritized over
    // the window handling it
    final int keyCode = event.getKeyCode();
    if (keyCode == KeyEvent.KEYCODE_MENU &&
            mActionBar != null && mActionBar.onMenuKeyEvent(event)) {
        return true;
    }

    Window win = getWindow();
    if (win.superDispatchKeyEvent(event)) {
        return true;
    }
    View decor = mDecor;
    if (decor == null) decor = win.getDecorView();
    return event.dispatch(this, decor != null
            ? decor.getKeyDispatcherState() : null, this);
}

 

PhoneWindow.superDispatchKeyEvent(KeyEvent event)

public boolean superDispatchKeyEvent(KeyEvent event) {
    return mDecor.superDispatchKeyEvent(event);
}

 

DecorView.superDispatchKeyEvent(KeyEvent event)

public boolean superDispatchKeyEvent(KeyEvent event) {
    // Give priority to closing action modes if applicable.
    if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
        final int action = event.getAction();
        // Back cancels action modes first.
        if (mPrimaryActionMode != null) {
            if (action == KeyEvent.ACTION_UP) {
                mPrimaryActionMode.finish();
            }
            return true;
        }
    }

    if (super.dispatchKeyEvent(event)) {
        return true;
    }

    return (getViewRootImpl() != null) && getViewRootImpl().dispatchUnhandledKeyEvent(event);
}

 

ViewGroup.dispatchKeyEvent(KeyEvent event)

public boolean dispatchKeyEvent(KeyEvent event) {
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onKeyEvent(event, 1);
    }

    if ((mPrivateFlags & (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS))
            == (PFLAG_FOCUSED | PFLAG_HAS_BOUNDS)) {
        if (super.dispatchKeyEvent(event)) {
            return true;
        }
    } else if (mFocused != null && (mFocused.mPrivateFlags & PFLAG_HAS_BOUNDS)
            == PFLAG_HAS_BOUNDS) {
        if (mFocused.dispatchKeyEvent(event)) {
            return true;
        }
    }

    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(event, 1);
    }
    return false;
}

 

ViewGroup에 포커스 되어있고 사이즈가 있다면 키이벤트는 View의 dispatchKeyEvent()가 호출이 되고 그렇지않고 ViewGroup안에 포커스된 child view가 존재하고 해당 view에 사이즈가 존재하면 키이벤트는 View에 전달된다.

 

View.dispatchKeyEvent(KeyEvent event)

public boolean dispatchKeyEvent(KeyEvent event) {
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onKeyEvent(event, 0);
    }

    // Give any attached key listener a first crack at the event.
    // noinspection SimplifiableIfStatement
    ListenerInfo li = mListenerInfo;
    if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
            && li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
        return true;
    }

    if (event.dispatch(this, mAttachInfo != null
            ? mAttachInfo.mKeyDispatchState : null, this)) {
        return true;
    }

    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    }
    
    return false;
}

 

View에 onKeyListener가 등록되어있고 View가 Enable상태라면 onKeyListener의 onKey() 함수가 호출이 된다. KeyEvent.dispatch(Callback receiver, DispatcherState state, Object target) 함수를 호출하는데 receiver로 자신을 callback으로 등록한다. 이 함수 내에서 View의 onKeyUp()/onKeyDown() 함수가 호출된다.

 

View.onKeyDown(int keyCode, KeyEvent event), View.onKeyUp(int keyCode, KeyEvent event)

 

public boolean onKeyDown(int keyCode, KeyEvent event) {
    if (KeyEvent.isConfirmKey(keyCode)) {
        // View가 직접 처리하고 Activity의 onKeyDown/onKeyUp은 confirm key를 수신하지 못한다.
        if ((mViewFlags & ENABLED_MASK) == DISABLED) {
            return true;
        }

        if (event.getRepeatCount() == 0) {
            // Long clickable items don't necessarily have to be clickable.
            final boolean clickable = (mViewFlags & CLICKABLE) == CLICKABLE
                    || (mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE;
            if (clickable || (mViewFlags & TOOLTIP) == TOOLTIP) {
                // For the purposes of menu anchoring and drawable hotspots,
                // key events are considered to be at the center of the view.
                final float x = getWidth() / 2f;
                final float y = getHeight() / 2f;
                if (clickable) {
                    setPressed(true, x, y);
                }
                checkForLongClick(
                        ViewConfiguration.getLongPressTimeout(),
                        x,
                        y,
                        // This is not a touch gesture -- do not classify it as one.
                        TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__UNKNOWN_CLASSIFICATION);
                return true;
            }
        }
    }

    return false;
}

public boolean onKeyUp(int keyCode, KeyEvent event) {
    if (KeyEvent.isConfirmKey(keyCode)) {
        if ((mViewFlags & ENABLED_MASK) == DISABLED) {
            return true;
        }
        if ((mViewFlags & CLICKABLE) == CLICKABLE && isPressed()) {
            setPressed(false);

            if (!mHasPerformedLongPress) {
                // This is a tap, so remove the longpress check
                removeLongPressCallback();
                if (!event.isCanceled()) {
                    return performClickInternal();
                }
            }
        }
    }

    return false;
}

 

KeyEvent.isConfirmKey(keyCode)는 KEYCODE_DPAD_CENTER_KEYCODE_ENTER, KEYCODE_SPACE, KEYCODE_NUMPAD_ENTER 이렇게 4개의 keycode를 체크하게된다.

View에서 처리되지 않으면 Activity의 onKeyDown()/onKeyUp()이 호출된다.