chromium/chrome/android/java/src/org/chromium/chrome/browser/fullscreen/FullscreenHtmlApiHandlerBase.java

// Copyright 2024 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.chrome.browser.fullscreen;

import android.app.Activity;
import android.os.Handler;
import android.os.Message;
import android.view.View;
import android.view.View.OnLayoutChangeListener;
import android.view.Window;
import android.view.WindowManager;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.core.util.ObjectsCompat;

import org.chromium.base.ActivityState;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.ApplicationStatus.ActivityStateListener;
import org.chromium.base.ApplicationStatus.WindowFocusChangedListener;
import org.chromium.base.BuildInfo;
import org.chromium.base.ObserverList;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.cc.input.BrowserControlsState;
import org.chromium.chrome.browser.ActivityTabProvider;
import org.chromium.chrome.browser.ActivityTabProvider.ActivityTabTabObserver;
import org.chromium.chrome.browser.multiwindow.MultiWindowUtils;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabAttributeKeys;
import org.chromium.chrome.browser.tab.TabAttributes;
import org.chromium.chrome.browser.tab.TabBrowserControlsConstraintsHelper;
import org.chromium.chrome.browser.tab.TabHidingType;
import org.chromium.chrome.browser.tab.TabUtils;
import org.chromium.chrome.browser.tabmodel.TabModelSelector;
import org.chromium.chrome.browser.tabmodel.TabModelSelectorTabObserver;
import org.chromium.components.embedder_support.view.ContentView;
import org.chromium.content_public.browser.GestureListenerManager;
import org.chromium.content_public.browser.NavigationHandle;
import org.chromium.content_public.browser.SelectionPopupController;
import org.chromium.content_public.browser.WebContents;
import org.chromium.ui.base.ViewUtils;

import java.lang.ref.WeakReference;

/** Handles updating the UI based on requests to the HTML Fullscreen API. */
public abstract class FullscreenHtmlApiHandlerBase
        implements ActivityStateListener,
                WindowFocusChangedListener,
                FullscreenManager {
    private static final boolean DEBUG_LOGS = false;

    protected static final int MSG_ID_SET_VISIBILITY_FOR_SYSTEM_BARS = 1;
    protected  static final int MSG_ID_UNSET_FULLSCREEN_LAYOUT = 2;

    // The time we allow the Android notification bar to be shown when it is requested temporarily
    // by the Android system (this value is additive on top of the show duration imposed by
    // Android).
    protected static final long ANDROID_CONTROLS_SHOW_DURATION_MS = 200;
    // Delay to allow a frame to render between getting the fullscreen layout update and leaving
    // layout fullscreen mode.
    private static final long CLEAR_LAYOUT_FULLSCREEN_DELAY_MS = 20;

    protected final Activity mActivity;
    protected final Handler mHandler;
    private final ObservableSupplierImpl<Boolean> mPersistentModeSupplier;
    private final ObservableSupplier<Boolean> mAreControlsHidden;
    private final boolean mExitFullscreenOnStop;
    private final ObserverList<FullscreenManager.Observer> mObservers = new ObserverList<>();

    // We need to cache WebContents/ContentView since we are setting fullscreen UI state on
    // the WebContents's container view, and a Tab can change to have null web contents/
    // content view, i.e., if you navigate to a native page.
    @Nullable private WebContents mWebContentsInFullscreen;
    @Nullable private View mContentViewInFullscreen;
    @Nullable protected Tab mTabInFullscreen;
    @Nullable private FullscreenOptions mFullscreenOptions;

    private FullscreenToast mToast;

    private OnLayoutChangeListener mFullscreenOnLayoutChangeListener;

    private FullscreenOptions mPendingFullscreenOptions;

    private ActivityTabTabObserver mActiveTabObserver;
    private TabModelSelectorTabObserver mTabFullscreenObserver;
    @Nullable private Tab mTab;

    private boolean mNotifyOnNextExit;

    // Current ContentView. Updates when active tab is switched or WebContents is swapped
    // in the current Tab.
    private ContentView mContentView;

    protected ContentView getContentView() {
        return mContentView;
    }

    /**
     * Update the current content view that can be shown in fullscreen mode, e.g. when the active
     * tab is switched or when web contents are swapped in the current Tab.
     * @param contentView The new content view.
     */
    protected void setContentView(ContentView contentView) {
        mContentView = contentView;
    }

    // This static inner class holds a WeakReference to the outer object, to avoid triggering the
    // lint HandlerLeak warning.
    private static class FullscreenHandler extends Handler {
        private final WeakReference<FullscreenHtmlApiHandlerBase> mFullscreenHtmlApiHandler;

        public FullscreenHandler(FullscreenHtmlApiHandlerBase fullscreenHtmlApiHandlerBase) {
            mFullscreenHtmlApiHandler =
                    new WeakReference<FullscreenHtmlApiHandlerBase>(fullscreenHtmlApiHandlerBase);
        }

        @Override
        public void handleMessage(Message msg) {
            if (msg == null) return;
            FullscreenHtmlApiHandlerBase fullscreenHtmlApiHandlerBase =
                    mFullscreenHtmlApiHandler.get();
            if (fullscreenHtmlApiHandlerBase == null) return;

            final WebContents webContents = fullscreenHtmlApiHandlerBase.mWebContentsInFullscreen;
            if (webContents == null) return;

            final View contentView = fullscreenHtmlApiHandlerBase.mContentViewInFullscreen;
            if (contentView == null) return;

            switch (msg.what) {
                case MSG_ID_SET_VISIBILITY_FOR_SYSTEM_BARS:
                    {
                        assert fullscreenHtmlApiHandlerBase.getPersistentFullscreenMode()
                                : "Calling after we exited fullscreen";
                        assert fullscreenHtmlApiHandlerBase.mFullscreenOptions != null;

                        if (!fullscreenHtmlApiHandlerBase.hasDesiredStateForSystemBars(
                                contentView, fullscreenHtmlApiHandlerBase.mFullscreenOptions)) {
                            fullscreenHtmlApiHandlerBase.hideSystemBars(
                                    contentView, fullscreenHtmlApiHandlerBase.mFullscreenOptions);
                            if (DEBUG_LOGS) {
                                fullscreenHtmlApiHandlerBase.logHandleMessageHideSystemBars(
                                        contentView);
                            }
                        }

                        if (!fullscreenHtmlApiHandlerBase.isLayoutFullscreen(contentView)) return;

                        // Trigger an update to unset layout fullscreen mode once the view has been
                        // laid out after this system UI update.  Without clearing this flag, the
                        // keyboard appearing will not trigger a relayout of the contents, which
                        // prevents updating the overdraw amount to the renderer.
                        contentView.addOnLayoutChangeListener(
                                new OnLayoutChangeListener() {
                                    @Override
                                    public void onLayoutChange(
                                            View v,
                                            int left,
                                            int top,
                                            int right,
                                            int bottom,
                                            int oldLeft,
                                            int oldTop,
                                            int oldRight,
                                            int oldBottom) {
                                        sendEmptyMessageDelayed(
                                                MSG_ID_UNSET_FULLSCREEN_LAYOUT,
                                                CLEAR_LAYOUT_FULLSCREEN_DELAY_MS);
                                        contentView.removeOnLayoutChangeListener(this);
                                    }
                                });

                        ViewUtils.requestLayout(
                                contentView,
                                "FullscreenHtmlApiHandler.FullscreenHandler.handleMessage");
                        break;
                    }
                case MSG_ID_UNSET_FULLSCREEN_LAYOUT:
                    {
                        // Change this assert to simply ignoring the message to work around
                        // https://crbug/365638
                        // TODO(aberent): Fix bug assert getPersistentFullscreenMode() : "Calling
                        // after we exited fullscreen";
                        if (!fullscreenHtmlApiHandlerBase.getPersistentFullscreenMode()) return;
                        fullscreenHtmlApiHandlerBase.unsetLayoutFullscreen(contentView);
                        if (DEBUG_LOGS) {
                            fullscreenHtmlApiHandlerBase.logHandlerUnsetFullscreenLayout(
                                    contentView);
                        }
                        fullscreenHtmlApiHandlerBase.unsetTranslucentStatusBar();
                        break;
                    }
                default:
                    assert false : "Unexpected message for ID: " + msg.what;
                    break;
            }
        }
    }

    /**
     * Constructs the handler that will manage the UI transitions from the HTML fullscreen API.
     *
     * @param activity The activity that supports fullscreen.
     * @param areControlsHidden Supplier of a flag indicating if browser controls are hidden.
     * @param exitFullscreenOnStop Whether fullscreen mode should exit on stop - should be true for
     *     Activities that are not always fullscreen.
     */
    public FullscreenHtmlApiHandlerBase(
            Activity activity,
            ObservableSupplier<Boolean> areControlsHidden,
            boolean exitFullscreenOnStop) {
        mActivity = activity;
        mAreControlsHidden = areControlsHidden;
        mAreControlsHidden.addObserver(this::maybeEnterFullscreenFromPendingState);
        mHandler = new FullscreenHandler(this);

        mPersistentModeSupplier = new ObservableSupplierImpl<>();
        mPersistentModeSupplier.set(false);
        mExitFullscreenOnStop = exitFullscreenOnStop;
    }

    /**
     * Initialize the FullscreeHtmlApiHandler.
     * @param activityTabProvider Provider of the current activity tab.
     * @param modelSelector The tab model selector that will be monitored for tab changes.
     */
    void initialize(ActivityTabProvider activityTabProvider, TabModelSelector modelSelector) {
        ApplicationStatus.registerStateListenerForActivity(this, mActivity);
        ApplicationStatus.registerWindowFocusChangedListener(this);
        mActiveTabObserver =
                new ActivityTabTabObserver(activityTabProvider) {
                    @Override
                    protected void onObservingDifferentTab(Tab tab, boolean hint) {
                        mTab = tab;
                        setContentView(tab != null ? tab.getContentView() : null);
                        if (tab != null) {
                            updateMultiTouchZoomSupport(!getPersistentFullscreenMode());
                        }
                    }
                };

        mTabFullscreenObserver =
                new TabModelSelectorTabObserver(modelSelector) {
                    @Override
                    public void onContentChanged(Tab tab) {
                        setContentView(tab.getContentView());
                    }

                    @Override
                    public void onHidden(Tab tab, @TabHidingType int reason) {
                        // Clean up any fullscreen state that might impact other tabs.
                        exitPersistentFullscreenMode();
                    }

                    @Override
                    public void onDidFinishNavigationInPrimaryMainFrame(
                            Tab tab, NavigationHandle navigation) {
                        if (!navigation.isSameDocument() && tab == modelSelector.getCurrentTab()) {
                            exitPersistentFullscreenMode();
                        }
                    }

                    @Override
                    public void onInteractabilityChanged(Tab tab, boolean interactable) {
                        // Compare |tab| with |TabModelSelector#getCurrentTab()| which is a safer
                        // indicator for the active tab than |mTab|, since the invocation order of
                        // ActivityTabTabObserver and TabModelSelectorTabObserver is not explicitly
                        // defined.
                        if (!interactable || tab != modelSelector.getCurrentTab()) return;
                        onTabInteractable(tab);
                    }
                };
    }

    @VisibleForTesting
    void onTabInteractable(Tab tab) {
        Runnable enterFullscreen = getAndClearEnterFullscreenRunnable(tab);
        if (enterFullscreen != null) enterFullscreen.run();
    }

    @Override
    public void addObserver(FullscreenManager.Observer observer) {
        mObservers.addObserver(observer);
    }

    @Override
    public void removeObserver(FullscreenManager.Observer observer) {
        mObservers.removeObserver(observer);
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    private FullscreenToast getToast() {
        if (mToast == null) {
            mToast = new FullscreenToast.AndroidToast(mActivity, this::getPersistentFullscreenMode);
        }
        return mToast;
    }

    @Override
    public void onEnterFullscreen(Tab tab, FullscreenOptions options) {
        if (shouldSkipEnterFullscreenRequest(options)) return;
        // If enabling fullscreen while the tab is not interactable, fullscreen
        // will be delayed until the tab is interactable.
        Runnable r =
                () -> {
                    enterPersistentFullscreenMode(options);
                    destroySelectActionMode(tab);
                    setEnterFullscreenRunnable(tab, null);
                    for (FullscreenManager.Observer observer : mObservers) {
                        observer.onEnterFullscreen(tab, options);
                    }
                };
        if (tab.isUserInteractable()) {
            r.run();
        } else {
            setEnterFullscreenRunnable(tab, r);
        }
    }

    private boolean shouldSkipEnterFullscreenRequest(FullscreenOptions options) {
        // Do not process the request again if we're already in fullscreen mode and the request
        // with the same option (could be in pending state) is received.
        return getPersistentFullscreenMode()
                && (ObjectsCompat.equals(mFullscreenOptions, options)
                        || ObjectsCompat.equals(mPendingFullscreenOptions, options));
    }

    @Override
    public void onExitFullscreen(Tab tab) {
        if (tab != mTab) return;
        setEnterFullscreenRunnable(tab, null);
        boolean wasInPersistentFullscreenMode = getPersistentFullscreenMode();
        exitPersistentFullscreenMode();
        if (wasInPersistentFullscreenMode || mNotifyOnNextExit) {
            mNotifyOnNextExit = false;
            for (FullscreenManager.Observer observer : mObservers) {
                observer.onExitFullscreen(tab);
            }
        }
    }

    /**
     * @see GestureListenerManager#updateMultiTouchZoomSupport(boolean).
     */
    @VisibleForTesting
    protected void updateMultiTouchZoomSupport(boolean enable) {
        if (mTab == null || mTab.isHidden()) return;
        WebContents webContents = mTab.getWebContents();
        if (webContents != null) {
            GestureListenerManager manager = GestureListenerManager.fromWebContents(webContents);
            if (manager != null) manager.updateMultiTouchZoomSupport(enable);
        }
    }

    @VisibleForTesting
    /* package */ void destroySelectActionMode(Tab tab) {
        WebContents webContents = tab.getWebContents();
        if (webContents != null) {
            SelectionPopupController.fromWebContents(webContents).destroySelectActionMode();
        }
    }

    private void setEnterFullscreenRunnable(Tab tab, Runnable runnable) {
        TabAttributes attrs = TabAttributes.from(tab);
        if (runnable == null) {
            attrs.clear(TabAttributeKeys.ENTER_FULLSCREEN);
        } else {
            attrs.set(TabAttributeKeys.ENTER_FULLSCREEN, runnable);
        }
    }

    private Runnable getAndClearEnterFullscreenRunnable(Tab tab) {
        Runnable r =
                tab != null ? TabAttributes.from(tab).get(TabAttributeKeys.ENTER_FULLSCREEN) : null;
        if (r != null) setEnterFullscreenRunnable(tab, null);
        return r;
    }

    /**
     * Enters persistent fullscreen mode. In this mode, the browser controls will be
     * permanently hidden until this mode is exited.
     *
     * @param options Options to choose mode of fullscreen.
     */
    private void enterPersistentFullscreenMode(FullscreenOptions options) {
        if (!shouldSkipEnterFullscreenRequest(options)) {
            mPersistentModeSupplier.set(true);
            mNotifyOnNextExit = true;
            if (mAreControlsHidden.get()) {
                // The browser controls are currently hidden.
                enterFullscreen(mTab, options);
            } else {
                // We should hide browser controls first.
                mPendingFullscreenOptions = options;
            }
        }
        updateMultiTouchZoomSupport(false);
    }

    /**
     * Enter fullscreen if there was a pending request due to browser controls yet to be hidden.
     * @param controlsHidden {@code true} if the controls are now hidden.
     */
    private void maybeEnterFullscreenFromPendingState(boolean controlsHidden) {
        if (!controlsHidden || mTab == null) return;
        if (mPendingFullscreenOptions != null) {
            if (mPendingFullscreenOptions.canceled()) {
                // Restore browser controls if the fullscreen process got canceled.
                TabBrowserControlsConstraintsHelper.update(mTab, BrowserControlsState.SHOWN, true);
            } else {
                enterFullscreen(mTab, mPendingFullscreenOptions);
            }
            mPendingFullscreenOptions = null;
        }
    }

    @Override
    public void exitPersistentFullscreenMode() {
        if (getPersistentFullscreenMode()) {
            getToast().onExitPersistentFullscreen();
            mPersistentModeSupplier.set(false);

            if (mWebContentsInFullscreen != null && mTabInFullscreen != null) {
                exitFullscreen(mWebContentsInFullscreen, mContentViewInFullscreen);
            } else {
                if (mPendingFullscreenOptions != null) mPendingFullscreenOptions.setCanceled();
                if (mAreControlsHidden.get()) {
                    TabBrowserControlsConstraintsHelper.update(
                            mTab, BrowserControlsState.SHOWN, true);
                }
            }
            mWebContentsInFullscreen = null;
            mContentViewInFullscreen = null;
            mTabInFullscreen = null;
            mFullscreenOptions = null;
        }
        updateMultiTouchZoomSupport(true);
    }

    @Override
    public boolean getPersistentFullscreenMode() {
        return mPersistentModeSupplier.get();
    }

    /**
     * @return An observable supplier that determines whether the app is in persistent fullscreen
     *     mode.
     */
    @Override
    public ObservableSupplier<Boolean> getPersistentFullscreenModeSupplier() {
        return mPersistentModeSupplier;
    }

    private void exitFullscreen(WebContents webContents, View contentView) {
        getToast().onExitFullscreen();
        mHandler.removeMessages(MSG_ID_SET_VISIBILITY_FOR_SYSTEM_BARS);
        mHandler.removeMessages(MSG_ID_UNSET_FULLSCREEN_LAYOUT);

        unsetTranslucentStatusBar();
        showSystemBars(contentView);
        if (DEBUG_LOGS) logExitFullscreen(contentView);
        resetExitFullscreenLayoutChangeListener(contentView);
        if (webContents != null && !webContents.isDestroyed()) webContents.exitFullscreen();

        // Ensure that the layout change listener to bring back browser controls is called on
        // automotive devices that never hide system bars.
        if (BuildInfo.getInstance().isAutomotive) {
            ViewUtils.requestLayout(contentView, "FullscreenHtmlApiHandler.exitFullScreen");
        }
    }

    private void resetExitFullscreenLayoutChangeListener(View contentView) {
        if (mFullscreenOnLayoutChangeListener != null) {
            contentView.removeOnLayoutChangeListener(mFullscreenOnLayoutChangeListener);
        }
        mFullscreenOnLayoutChangeListener =
                new OnLayoutChangeListener() {
                    @Override
                    public void onLayoutChange(
                            View v,
                            int left,
                            int top,
                            int right,
                            int bottom,
                            int oldLeft,
                            int oldTop,
                            int oldRight,
                            int oldBottom) {
                        if ((bottom - top) <= (oldBottom - oldTop)
                                || BuildInfo.getInstance().isAutomotive) {
                            // At this point, browser controls are hidden. Show browser controls
                            // only if it's permitted.
                            TabBrowserControlsConstraintsHelper.update(
                                    mTab, BrowserControlsState.SHOWN, true);
                            contentView.removeOnLayoutChangeListener(this);
                        }
                    }
                };
            contentView.addOnLayoutChangeListener(mFullscreenOnLayoutChangeListener);
    }

    private boolean isAlreadyInFullscreenOrNavigationHidden(View contentView) {
        return isStatusBarHidden(contentView) || isNavigationBarHidden(contentView);
    }

    private boolean hasDesiredStateForSystemBars(View contentView, FullscreenOptions options) {
        assert options != null;

        boolean shouldDisplayStatusBar = options.showStatusBar;
        boolean shouldDisplayNavigationBar = options.showNavigationBar;

        if (shouldDisplayStatusBar == isStatusBarHidden(contentView)) return false;
        if (shouldDisplayNavigationBar == isNavigationBarHidden(contentView)) return false;

        return true;
    }

    /**
     * Handles hiding the system UI components to allow the content to take up the full screen.
     *
     * @param tab The tab that is entering fullscreen.
     */
    private void enterFullscreen(final Tab tab, FullscreenOptions options) {
        assert !(options.showNavigationBar && options.showStatusBar)
                : "Cannot enter fullscreen with both status and navigation bars visible!";

        if (DEBUG_LOGS) logEnterFullscreenOptions(options);
        WebContents webContents = tab.getWebContents();
        if (webContents == null) return;
        mFullscreenOptions = options;
        final View contentView = tab.getContentView();

        if (isAlreadyInFullscreenOrNavigationHidden(contentView)) {
            // We are already in fullscreen mode and the fullscreen options match what is
            // needed; nothing to do.
            if (hasDesiredStateForSystemBars(contentView, mFullscreenOptions)) return;

            resetEnterFullscreenLayoutChangeListener(contentView);
            adjustSystemBarsInFullscreenMode(contentView, mFullscreenOptions);
        } else if (isLayoutFullscreen(contentView) || isLayoutHidingNavigation(contentView)) {
            resetEnterFullscreenLayoutChangeListener(contentView);
            hideSystemBars(contentView, mFullscreenOptions);
        } else {
            Activity activity = TabUtils.getActivity(tab);
            boolean isMultiWindow = MultiWindowUtils.getInstance().isInMultiWindowMode(activity);

            // To avoid a double layout that is caused by the system when just hiding
            // the status bar set the status bar as translucent immediately. This causes
            // it not to take up space so the layout is stable. (See https://crbug.com/935015).
            // Do not do this in multi-window mode or if the system bars can't be dismissed (i.e.
            // on some automotive devices), since the status bar will be forced to always stay
            // visible.
            if (!mFullscreenOptions.showStatusBar
                    && !isMultiWindow
                    && !BuildInfo.getInstance().isAutomotive) {
                setTranslucentStatusBar();
            }

            resetEnterFullscreenLayoutChangeListener(contentView);
            if (!mFullscreenOptions.showNavigationBar) hideNavigationBar(contentView);
            if (!mFullscreenOptions.showStatusBar) setLayoutFullscreen(contentView);
        }

        if (DEBUG_LOGS) logEnterFullscreen(contentView);

        // Request a layout so the updated system visibility takes affect.
        // The flow will continue in the handler of MSG_ID_UNSET_FULLSCREEN_LAYOUT message.
        ViewUtils.requestLayout(contentView, "FullscreenHtmlApiHandler.enterFullScreen");

        mWebContentsInFullscreen = webContents;
        mContentViewInFullscreen = contentView;
        mTabInFullscreen = tab;
        getToast().onEnterFullscreen();
    }

    private void resetEnterFullscreenLayoutChangeListener(View contentView) {
        if (mFullscreenOnLayoutChangeListener != null) {
            contentView.removeOnLayoutChangeListener(mFullscreenOnLayoutChangeListener);
        }

        mFullscreenOnLayoutChangeListener =
                new OnLayoutChangeListener() {
                    @Override
                    public void onLayoutChange(
                            View v,
                            int left,
                            int top,
                            int right,
                            int bottom,
                            int oldLeft,
                            int oldTop,
                            int oldRight,
                            int oldBottom) {
                        // On certain sites playing embedded video (http://crbug.com/293782),
                        // setting the layout as fullscreen does not always trigger a view-level
                        // layout with an updated height. To work around this, do not check for an
                        // increased height and always just trigger the next step of the
                        // fullscreen initialization.
                        // Posting the message to set the fullscreen flag because setting it
                        // directly in the onLayoutChange would have no effect.
                        mHandler.sendEmptyMessage(MSG_ID_SET_VISIBILITY_FOR_SYSTEM_BARS);

                        if ((bottom - top) <= (oldBottom - oldTop)
                                && (right - left) <= (oldRight - oldLeft)
                                // Some automotive devices never hide the system bars, so Chrome
                                // can't rely on detecting a change in insets.
                                && !BuildInfo.getInstance().isAutomotive) {
                            return;
                        }

                        getToast().onFullscreenLayout();
                        contentView.removeOnLayoutChangeListener(this);
                    }
                };

        contentView.addOnLayoutChangeListener(mFullscreenOnLayoutChangeListener);
    }

    // ActivityStateListener

    @Override
    public void onActivityStateChange(Activity activity, int newState) {
        if (newState == ActivityState.STOPPED && mExitFullscreenOnStop) {
            // Exit fullscreen in onStop to ensure the system UI flags are set correctly when
            // showing again (on JB MR2+ builds, the omnibox would be covered by the
            // notification bar when this was done in onStart()).
            exitPersistentFullscreenMode();
        } else if (newState == ActivityState.DESTROYED) {
            ApplicationStatus.unregisterActivityStateListener(this);
            ApplicationStatus.unregisterWindowFocusChangedListener(this);
        }
    }

    // WindowFocusChangedListener

    @Override
    public void onWindowFocusChanged(Activity activity, boolean hasWindowFocus) {
        if (mActivity != activity) return;

        // Window focus events can occur before the fullscreen toast is ready. It may skip and
        // wait till fullscreen is entered, by which time the toast object will be ready.
        if (mToast != null) mToast.onWindowFocusChanged(hasWindowFocus);

        mHandler.removeMessages(MSG_ID_SET_VISIBILITY_FOR_SYSTEM_BARS);
        mHandler.removeMessages(MSG_ID_UNSET_FULLSCREEN_LAYOUT);
        if (mTabInFullscreen == null || !getPersistentFullscreenMode() || !hasWindowFocus) return;
        mHandler.sendEmptyMessageDelayed(
                MSG_ID_SET_VISIBILITY_FOR_SYSTEM_BARS, ANDROID_CONTROLS_SHOW_DURATION_MS);
    }

    /*
     * Clears the current window attributes to not contain windowFlags. This
     * is slightly different that Window.clearFlags which then sets a
     * forced window attribute on the Window object that cannot be cleared.
     */
    protected void clearWindowFlags(int windowFlags) {
        Window window = mActivity.getWindow();
        final WindowManager.LayoutParams attrs = window.getAttributes();
        if ((attrs.flags & windowFlags) != 0) {
            attrs.flags &= ~windowFlags;
            window.setAttributes(attrs);
        }
    }

    /*
     * Sets the current window attributes to contain windowFlags. This
     * is slightly different that Window.setFlags which then sets a
     * forced window attribute on the Window object that cannot be cleared.
     */
    protected void setWindowFlags(int windowFlags) {
        Window window = mActivity.getWindow();
        final WindowManager.LayoutParams attrs = window.getAttributes();
        attrs.flags |= windowFlags;
        window.setAttributes(attrs);
    }

    /** Destroys the FullscreenHtmlApiHandler. */
    public void destroy() {
        mTab = null;
        setContentView(null);
        if (mActiveTabObserver != null) mActiveTabObserver.destroy();
        if (mTabFullscreenObserver != null) mTabFullscreenObserver.destroy();
        mObservers.clear();
    }

    void setTabForTesting(Tab tab) {
        mTab = tab;
    }

    ObserverList<FullscreenManager.Observer> getObserversForTesting() {
        return mObservers;
    }

    FullscreenOptions getPendingFullscreenOptionsForTesting() {
        return mPendingFullscreenOptions;
    }

    boolean isToastVisibleForTesting() {
        return getToast().isVisible();
    }

    /**
     * Hide the system bars (to enter fullscreen mode) based on the fullscreen options.
     * @param contentView The content view being shown or to be shown in fullscreen mode.
     * @param fullscreenOptions The fullscreen options to guide what UI is shown or hidden.
     */
    abstract void hideSystemBars(View contentView, FullscreenOptions fullscreenOptions);

    /**
     * Show the system bars (to exit fullscreen mode).
     * @param contentView The content view being shown or to be shown in fullscreen mode.
     */
    abstract void showSystemBars(View contentView);

    /**
     * Adjust the visibility of system bars while already in fullscreen mode.
     * @param contentView The content view being shown or to be shown in fullscreen mode.
     * @param fullscreenOptions The fullscreen options to guide what UI is shown or hidden.
     */
    abstract void adjustSystemBarsInFullscreenMode(
            View contentView, FullscreenOptions fullscreenOptions);

    /**
     * Whether the status bar is hidden.
     * @param contentView The content view being shown or to be shown in fullscreen mode.
     */
    abstract boolean isStatusBarHidden(View contentView);

    /**
     * Whether the navigation bar is hidden.
     * @param contentView The content view being shown or to be shown in fullscreen mode.
     */
    abstract boolean isNavigationBarHidden(View contentView);

    /**
     * Whether the view layout is laid out as if in fullscreen mode.
     * @param contentView The content view being shown or to be shown in fullscreen mode.
     */
    abstract boolean isLayoutFullscreen(View contentView);

    /**
     * Whether the view layout is laid out as if to hide the navigation bar.
     * @param contentView The content view being shown or to be shown in fullscreen mode.
     */
    abstract boolean isLayoutHidingNavigation(View contentView);

    /**
     * Hide the navigation bar to give more space to display the view.
     * @param contentView The content view being shown or to be shown in fullscreen mode.
     */
    abstract void hideNavigationBar(View contentView);

    /**
     * Request that the view's layout display itself in fullscreen mode.
     * @param contentView The content view being shown or to be shown in fullscreen mode.
     */
    abstract void setLayoutFullscreen(View contentView);

    /**
     * Remove the request to the view's layout to display itself in fullscreen mode.
     * @param contentView The content view being shown or to be shown in fullscreen mode.
     */
    abstract void unsetLayoutFullscreen(View contentView);

    /**
     * Set a translucent status for the status bar.
     */
    abstract void setTranslucentStatusBar();

    /**
     * Unset the status bar's translucent status.
     */
    abstract void unsetTranslucentStatusBar();

    /**
     * Logs when a view enters fullscreen mode.
     * @param contentView The content view being shown or to be shown in fullscreen mode.
     */
    abstract void logEnterFullscreen(View contentView);

    /**
     * Logs the fullscreen options when a view requests to enter fullscreen mode.
     * @param fullscreenOptions The content view being shown or to be shown in fullscreen mode.
     */
    abstract void logEnterFullscreenOptions(FullscreenOptions fullscreenOptions);

    /**
     * Logs when a view exits fullscreen mode.
     * @param contentView The content view being shown or to be shown in fullscreen mode.
     */
    abstract void logExitFullscreen(View contentView);

    /**
     * Logs when the handler processes a message to unset the view's layout being shown as if in
     * fullscreen mode.
     * @param contentView The content view being shown or to be shown in fullscreen mode.
     */
    abstract void logHandlerUnsetFullscreenLayout(View contentView);

    /**
     * Logs when the handler processes a message to hide the system bars.
     * @param contentView The content view being shown or to be shown in fullscreen mode.
     */
    abstract void logHandleMessageHideSystemBars(View contentView);
}