chromium/chrome/android/java/src/org/chromium/chrome/browser/fullscreen/FullscreenHtmlApiHandlerCompat.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.view.View;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowManager;

import androidx.annotation.NonNull;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.view.WindowInsetsControllerCompat;

import org.chromium.base.BuildInfo;
import org.chromium.base.Log;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.components.embedder_support.view.ContentView;

public class FullscreenHtmlApiHandlerCompat extends FullscreenHtmlApiHandlerBase
        implements View.OnApplyWindowInsetsListener {

    // TAG length is limited to 20 characters, so we cannot use full class name:
    private static final String TAG = "FullscreenApiCompat";

    /**
     * Constructs the legacy 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 FullscreenHtmlApiHandlerCompat(
            Activity activity,
            ObservableSupplier<Boolean> areControlsHidden,
            boolean exitFullscreenOnStop) {
        super(activity, areControlsHidden, exitFullscreenOnStop);
    }

    // View.OnApplyWindowInsetsListener

    @NonNull
    @Override
    public WindowInsets onApplyWindowInsets(@NonNull View v, @NonNull WindowInsets insets) {
        if (mTabInFullscreen != null && getPersistentFullscreenMode()) {
            mHandler.sendEmptyMessageDelayed(
                    MSG_ID_SET_VISIBILITY_FOR_SYSTEM_BARS, ANDROID_CONTROLS_SHOW_DURATION_MS);
        }
        return insets;
    }

    // FullscreenHtmlApiHandlerBase

    @Override
    protected void setContentView(ContentView contentView) {
        ContentView oldContentView = getContentView();
        if (contentView == oldContentView) return;
        if (oldContentView != null) {
            oldContentView.setOnApplyWindowInsetsListener(null);
        }
        super.setContentView(contentView);
        if (contentView != null) {
            contentView.setOnApplyWindowInsetsListener(this);
        }
    }

    @Override
    void hideSystemBars(View contentView, FullscreenOptions fullscreenOptions) {
        boolean showNavigationBar =
                fullscreenOptions != null && fullscreenOptions.showNavigationBar;
        boolean showStatusBar = fullscreenOptions != null && fullscreenOptions.showStatusBar;

        assert !(showNavigationBar && showStatusBar)
                : "Cannot enter fullscreen with both status and navigation bars visible!";

        WindowInsetsControllerCompat windowInsetsController = getWindowInsetsController();

        windowInsetsController.setSystemBarsBehavior(
                WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
        setLayoutFullscreen(/* not used */ contentView);
        if (!showStatusBar && !showNavigationBar) {
            windowInsetsController.hide(WindowInsetsCompat.Type.systemBars());
        } else if (!showNavigationBar) {
            windowInsetsController.hide(WindowInsetsCompat.Type.navigationBars());
        } else /* !showStatusBar */ {
            windowInsetsController.hide(WindowInsetsCompat.Type.statusBars());
        }
    }

    @Override
    void showSystemBars(View contentView) {
        WindowInsetsControllerCompat windowInsetsController = getWindowInsetsController();

        windowInsetsController.setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_DEFAULT);
        // SystemBars() includes both the navigation and status bars, as well as captions and some
        // system overlays.
        windowInsetsController.show(WindowInsetsCompat.Type.systemBars());
        unsetLayoutFullscreen(/* not used */ contentView);
    }

    @Override
    void adjustSystemBarsInFullscreenMode(View contentView, FullscreenOptions fullscreenOptions) {
        boolean showNavigationBar =
                fullscreenOptions != null && fullscreenOptions.showNavigationBar;
        boolean showStatusBar = fullscreenOptions != null && fullscreenOptions.showStatusBar;

        assert !(showNavigationBar && showStatusBar)
                : "Cannot enter fullscreen with both status and navigation bars visible!";

        WindowInsetsControllerCompat windowInsetsController = getWindowInsetsController();

        windowInsetsController.setSystemBarsBehavior(
                WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
        setLayoutFullscreen(/* not used */ contentView);

        if (!showStatusBar && !showNavigationBar) {
            // SystemBars() includes both the navigation and status bars, as well as captions and
            // some system overlays.
            windowInsetsController.hide(WindowInsetsCompat.Type.systemBars());
        } else if (!showNavigationBar) {
            windowInsetsController.hide(WindowInsetsCompat.Type.navigationBars());
            windowInsetsController.show(WindowInsetsCompat.Type.statusBars());
        } else /* !showStatusBar */ {
            windowInsetsController.hide(WindowInsetsCompat.Type.statusBars());
            windowInsetsController.show(WindowInsetsCompat.Type.navigationBars());
        }
    }

    @Override
    boolean isStatusBarHidden(View contentView) {
        return !getWindowInsets(contentView).isVisible(WindowInsetsCompat.Type.statusBars());
    }

    @Override
    boolean isNavigationBarHidden(View contentView) {
        return !getWindowInsets(contentView).isVisible(WindowInsetsCompat.Type.navigationBars());
    }

    @Override
    boolean isLayoutFullscreen(View contentView) {
        return !mActivity.getWindow().getDecorView().getFitsSystemWindows();
    }

    @Override
    boolean isLayoutHidingNavigation(View contentView) {
        return isLayoutFullscreen(contentView);
    }

    @Override
    void hideNavigationBar(View contentView) {
        getWindowInsetsController().hide(WindowInsetsCompat.Type.navigationBars());
    }

    // TODO(crbug.com/41492646): Coordinate usage of #setDecorFitsSystemWindows
    @Override
    void setLayoutFullscreen(View contentView) {
        // Avoid setting this on automotive, as automotive devices are inconsistent in their
        // support for drawing edge-to-edge.
        if (BuildInfo.getInstance().isAutomotive) {
            return;
        }
        // TODO(crbug.com/41492929): Account for floating windows.
        WindowCompat.setDecorFitsSystemWindows(mActivity.getWindow(), false);
    }

    // TODO(crbug.com/41492646): Coordinate usage of #setDecorFitsSystemWindows
    @Override
    void unsetLayoutFullscreen(View contentView) {
        // TODO(crbug.com/41492929): Account for floating windows.
        WindowCompat.setDecorFitsSystemWindows(mActivity.getWindow(), true);
    }

    @Override
    void setTranslucentStatusBar() {
        setWindowFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
    }

    @Override
    void unsetTranslucentStatusBar() {
        clearWindowFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
    }

    @Override
    void logEnterFullscreen(View contentView) {
        Log.i(TAG, "enterFullscreen, systemUiVisibility=" + getSystemUIVisibility(contentView));
    }

    @Override
    void logEnterFullscreenOptions(FullscreenOptions fullscreenOptions) {
        Log.i(TAG, "enterFullscreen, options=" + fullscreenOptions.toString());
    }

    @Override
    void logExitFullscreen(View contentView) {
        Log.i(TAG, "exitFullscreen, systemUiVisibility=" + getSystemUIVisibility(contentView));
    }

    @Override
    void logHandlerUnsetFullscreenLayout(View contentView) {
        Log.i(
                TAG,
                "handleMessage clear fullscreen flag, systemUiVisibility="
                        + getSystemUIVisibility(contentView));
    }

    @Override
    void logHandleMessageHideSystemBars(View contentView) {
        Log.i(
                TAG,
                "handleMessage set flags, systemUiVisibility="
                        + getSystemUIVisibility(contentView));
    }

    private WindowInsetsCompat getWindowInsets(View contentView) {
        return WindowInsetsCompat.toWindowInsetsCompat(
                contentView.getRootWindowInsets(), contentView);
    }

    private WindowInsetsControllerCompat getWindowInsetsController() {
        Window window = mActivity.getWindow();
        return WindowCompat.getInsetsController(window, window.getDecorView());
    }

    private String getSystemUIVisibility(View contentView) {
        boolean statusBarVisibility = !isStatusBarHidden(contentView);
        boolean navBarVisibility = !isNavigationBarHidden(contentView);
        boolean systemBarVisibility =
                getWindowInsets(contentView).isVisible(WindowInsetsCompat.Type.systemBars());
        boolean decorViewFitsWindow = isLayoutFullscreen(contentView);
        boolean transientBars =
                getWindowInsetsController().getSystemBarsBehavior()
                        == WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;

        return String.format(
                "{statusbar=%b, navbar=%b, systemBars=%b, decorViewFitsWindow=%b,"
                        + " transientBars=%b}",
                statusBarVisibility,
                navBarVisibility,
                systemBarVisibility,
                decorViewFitsWindow,
                transientBars);
    }
}