chromium/content/public/android/java/src/org/chromium/content/browser/ViewEventSinkImpl.java

// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.content.browser;

import android.content.res.Configuration;

import org.chromium.base.TraceEvent;
import org.chromium.base.UserData;
import org.chromium.content.browser.webcontents.WebContentsImpl;
import org.chromium.content.browser.webcontents.WebContentsImpl.UserDataFactory;
import org.chromium.content_public.browser.ViewEventSink;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.ViewAndroidDelegate;
import org.chromium.ui.base.ViewUtils;
import org.chromium.ui.base.WindowAndroid.ActivityStateObserver;

/** Implementation of the interface {@link ViewEventSink}. */
public final class ViewEventSinkImpl implements ViewEventSink, ActivityStateObserver, UserData {
    private final WebContentsImpl mWebContents;

    // Whether the container view has view-level focus.
    private Boolean mHasViewFocus;

    // This is used in place of window focus on the container view, as we can't actually use window
    // focus due to issues where content expects to be focused while a popup steals window focus.
    // See https://crbug.com/686232 for more context.
    private boolean mPaused;

    // Whether we consider this WebContents to have input focus. This is computed through
    // mHasViewFocus and mPaused. See the comments on mPaused for how this doesn't exactly match
    // Android's notion of input focus and why we need to do this.
    private Boolean mHasInputFocus;
    private boolean mHideKeyboardOnBlur;

    private static final class UserDataFactoryLazyHolder {
        private static final UserDataFactory<ViewEventSinkImpl> INSTANCE = ViewEventSinkImpl::new;
    }

    public static ViewEventSinkImpl from(WebContents webContents) {
        return ((WebContentsImpl) webContents)
                .getOrSetUserData(ViewEventSinkImpl.class, UserDataFactoryLazyHolder.INSTANCE);
    }

    public ViewEventSinkImpl(WebContents webContents) {
        mWebContents = (WebContentsImpl) webContents;
    }

    @Override
    public void setAccessDelegate(ViewEventSink.InternalAccessDelegate accessDelegate) {
        GestureListenerManagerImpl.fromWebContents(mWebContents).setScrollDelegate(accessDelegate);
        ContentUiEventHandler.fromWebContents(mWebContents).setEventDelegate(accessDelegate);
    }

    @Override
    public void onAttachedToWindow() {
        WindowEventObserverManager.from(mWebContents).onAttachedToWindow();
    }

    @Override
    public void onDetachedFromWindow() {
        WindowEventObserverManager.from(mWebContents).onDetachedFromWindow();
        // Stylus Writing
        if (mWebContents.getStylusWritingHandler() != null) {
            ViewAndroidDelegate viewAndroidDelegate = mWebContents.getViewAndroidDelegate();
            if (viewAndroidDelegate != null) {
                mWebContents
                        .getStylusWritingHandler()
                        .onDetachedFromWindow(viewAndroidDelegate.getContainerView().getContext());
            }
        }
    }

    @Override
    public void onWindowFocusChanged(boolean hasWindowFocus) {
        WindowEventObserverManager.from(mWebContents).onWindowFocusChanged(hasWindowFocus);
    }

    @Override
    public void onViewFocusChanged(boolean gainFocus) {
        if (mHasViewFocus != null && mHasViewFocus == gainFocus) return;
        mHasViewFocus = gainFocus;
        onFocusChanged();

        // Stylus Writing
        if (mWebContents.getStylusWritingHandler() != null) {
            mWebContents.getStylusWritingHandler().onFocusChanged(gainFocus);
        }
    }

    @Override
    public void setHideKeyboardOnBlur(boolean hideKeyboardOnBlur) {
        mHideKeyboardOnBlur = hideKeyboardOnBlur;
    }

    @SuppressWarnings("javadoc")
    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        try {
            TraceEvent.begin("ViewEventSink.onConfigurationChanged");
            WindowEventObserverManager.from(mWebContents).onConfigurationChanged(newConfig);
            // To request layout has side effect, but it seems OK as it only happen in
            // onConfigurationChange and layout has to be changed in most case.
            ViewAndroidDelegate delegate = mWebContents.getViewAndroidDelegate();
            if (delegate != null) {
                ViewUtils.requestLayout(
                        delegate.getContainerView(), "ViewEventSinkImpl.onConfigurationChanged");
            }
        } finally {
            TraceEvent.end("ViewEventSink.onConfigurationChanged");
        }
    }

    private void onFocusChanged() {
        // Wait for view focus to be set before propagating focus changes.
        if (mHasViewFocus == null) return;

        // See the comments on mPaused for why we use it to compute input focus.
        boolean hasInputFocus = mHasViewFocus && !mPaused;
        if (mHasInputFocus != null && mHasInputFocus == hasInputFocus) return;
        mHasInputFocus = hasInputFocus;

        if (mWebContents == null) {
            // CVC is on its way to destruction. The rest needs not running as all the states
            // will be discarded, or WebContentsUserData-based objects are not reachable
            // any more. Simply return here.
            return;
        }
        WindowEventObserverManager.from(mWebContents)
                .onViewFocusChanged(mHasInputFocus, mHideKeyboardOnBlur);
        mWebContents.setFocus(mHasInputFocus);
    }

    // ActivityStateObserver

    @Override
    public void onActivityPaused() {
        // When the activity pauses, the content should lose focus.
        // TODO(mthiesse): See https://crbug.com/686232 for context. Desktop platforms use keyboard
        // focus to trigger blur/focus, and the equivalent to this on Android is Window focus.
        // However, we don't use Window focus because of the complexity around popups stealing
        // Window focus.
        if (mPaused) return;
        mPaused = true;
        onFocusChanged();
    }

    @Override
    public void onActivityResumed() {
        // When the activity resumes, the View#onFocusChanged may not be called, so we should
        // restore the View focus state.
        if (!mPaused) return;
        mPaused = false;
        onFocusChanged();
    }

    @Override
    public void onActivityDestroyed() {}

    @Override
    public void onPauseForTesting() {
        onActivityPaused();
    }

    @Override
    public void onResumeForTesting() {
        onActivityResumed();
    }
}