chromium/ui/android/java/src/org/chromium/ui/base/ApplicationViewportInsetSupplier.java

// Copyright 2020 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.ui.base;

import org.chromium.base.Callback;
import org.chromium.base.lifetime.Destroyable;
import org.chromium.base.supplier.ObservableSupplier;
import org.chromium.base.supplier.ObservableSupplierImpl;
import org.chromium.ui.mojom.VirtualKeyboardMode;

/**
 * A class responsible for managing multiple users of UI insets over the application viewport.
 *
 * <p>UI insets are complicated. The application viewport is provided by CompositorViewHolder but
 * the browser provides various UI controls which overlay the viewport. How these UI controls
 * interact with the underlying WebContents varies between controls and can depend on web-APIs.
 *
 * <p>For example, browser controls cause the WebContents to resize so that the web page reflows in
 * response to showing/hiding (although the timing of when this happens is non-straightforward). On
 * the other hand, the virtual keyboard should resize only the page's visualViewport, without
 * affecting layout. Chrome provides "keyboard accessories" that appear to the user to be part of
 * the keyboard but are actually separate UI components. To make matters even more complicated, the
 * page can change how the virtual keyboard affects the page (to affect page layout).
 *
 * <p>This class aims to centralize and encapsulate all these complex interactions so clients don't
 * have to worry about the details. This class currently handles only the keyboard and keyboard
 * accessory but there are plans to move browser controls into here as well
 * (https://crbug.com/1211066).
 *
 * <p>Features needing to know if anything is obscuring part of the screen listen to this class via
 * {@link #addObserver(Callback)} which observes changes to a {@link ViewportInsets} object which
 * has various inset types clients can use. See that class for more detials about the inset types.
 *
 * <pre>
 * In general:
 *  - Features that want to modify the inset should pass around the {@link
 *    ApplicationViewportInsetSupplier} object.
 *  - Features only interested in what the current inset is should pass around an {@link
 *    ObservableSupplier<ViewportInsets>} object.
 * </pre>
 */
public class ApplicationViewportInsetSupplier extends ObservableSupplierImpl<ViewportInsets>
        implements Destroyable {
    /** Keyboard related suppliers */
    private ObservableSupplier<Integer> mKeyboardInsetSupplier;

    private ObservableSupplier<Integer> mKeyboardAccessoryInsetSupplier;

    private ObservableSupplier<Integer> mBottomSheetInsetSupplier;

    /** The observer that gets attached to all inset suppliers. */
    private final Callback<Integer> mInsetSupplierObserver = (unused) -> computeInsets();

    /**
     * By default, the virtual keyboard overlays content, only resizing the visual viewport.
     *
     * Web content has APIs that change how the virtual keyboard interacts with content. This class
     * needs to know which mode we're in to determine how different kinds of insets are computed.
     */
    @VirtualKeyboardMode.EnumType
    private int mVirtualKeyboardMode = VirtualKeyboardMode.RESIZES_VISUAL;

    /** Default constructor. */
    ApplicationViewportInsetSupplier() {
        super();
        // Make sure this is initialized to 0 since "Integer" is an object and would be null
        // otherwise.
        super.set(new ViewportInsets());
    }

    public static ApplicationViewportInsetSupplier createForTests() {
        return new ApplicationViewportInsetSupplier();
    }

    @Override
    public void set(ViewportInsets value) {
        throw new IllegalStateException(
                "#set(...) should not be called directly on ApplicationViewportInsetSupplier.");
    }

    /** Clean up observers and suppliers. */
    @Override
    public void destroy() {
        setKeyboardInsetSupplier(null);
        setKeyboardAccessoryInsetSupplier(null);
    }

    /**
     * Notifies this object when the VirtualKeyboardMode of the currently active WebContents is
     * changed.
     *
     * This can happen as a result of a web content API call or swapping a WebContents or Tab.
     */
    public void setVirtualKeyboardMode(@VirtualKeyboardMode.EnumType int mode) {
        if (mVirtualKeyboardMode == mode) return;

        mVirtualKeyboardMode = mode;

        computeInsets();
    }

    /**
     * Sets the inset supplier for the soft keyboard itself.
     *
     * Pass null to unset the current supplier.
     */
    public void setKeyboardInsetSupplier(ObservableSupplier<Integer> insetSupplier) {
        boolean didRemove = false;

        if (mKeyboardInsetSupplier != null) {
            mKeyboardInsetSupplier.removeObserver(mInsetSupplierObserver);
            didRemove = true;
        }

        mKeyboardInsetSupplier = insetSupplier;

        if (mKeyboardInsetSupplier != null) {
            mKeyboardInsetSupplier.addObserver(mInsetSupplierObserver);
        } else if (didRemove) {
            // If a supplier was removed, removeObserver will not have notified observers (unlike
            // addObserver) so make sure insets get recomputed in this case.
            computeInsets();
        }
    }

    /**
     * Sets the inset supplier for the keyboard accessory.
     *
     * Pass null to unset the current supplier.
     */
    public void setKeyboardAccessoryInsetSupplier(ObservableSupplier<Integer> insetSupplier) {
        boolean didRemove = false;
        if (mKeyboardAccessoryInsetSupplier != null) {
            mKeyboardAccessoryInsetSupplier.removeObserver(mInsetSupplierObserver);
            didRemove = true;
        }

        mKeyboardAccessoryInsetSupplier = insetSupplier;

        if (mKeyboardAccessoryInsetSupplier != null) {
            mKeyboardAccessoryInsetSupplier.addObserver(mInsetSupplierObserver);
        } else if (didRemove) {
            // If a supplier was removed, removeObserver will not have notified observers (unlike
            // addObserver) so make sure insets get recomputed in this case.
            computeInsets();
        }
    }

    public void setBottomSheetInsetSupplier(ObservableSupplier<Integer> insetSupplier) {
        boolean didRemove = false;
        if (mBottomSheetInsetSupplier != null) {
            mBottomSheetInsetSupplier.removeObserver(mInsetSupplierObserver);
            didRemove = true;
        }

        mBottomSheetInsetSupplier = insetSupplier;

        if (mBottomSheetInsetSupplier != null) {
            mBottomSheetInsetSupplier.addObserver(mInsetSupplierObserver);
        } else if (didRemove) {
            // If a supplier was removed, removeObserver will not have notified observers (unlike
            // addObserver) so make sure insets get recomputed in this case.
            computeInsets();
        }
    }

    /** Compute the new total inset based on all registered suppliers. */
    private void computeInsets() {
        ViewportInsets newValues = new ViewportInsets();

        int keyboardInset = intFromSupplier(mKeyboardInsetSupplier);
        int accessoryInset = intFromSupplier(mKeyboardAccessoryInsetSupplier);
        int totalKeyboardInset = keyboardInset + accessoryInset;

        int bottomSheetInset = intFromSupplier(mBottomSheetInsetSupplier);
        newValues.viewVisibleHeightInset = accessoryInset;
        newValues.visualViewportBottomInset =
                mVirtualKeyboardMode == VirtualKeyboardMode.RESIZES_VISUAL ? totalKeyboardInset : 0;

        // If the VirtualKeyboardMode is set to OVERLAYS_CONTENT or RESIZES_VISUAL, the
        // WebContents size will not match the View size when the keyboard is showing (these
        // modes mean "keyboard doesn't resize web content"). In that case, *outset* by the shown
        // keyboard height to keep the WebContents from being resized.
        boolean vkModeOutsetsWebContentsHeight =
                mVirtualKeyboardMode == VirtualKeyboardMode.OVERLAYS_CONTENT
                        || mVirtualKeyboardMode == VirtualKeyboardMode.RESIZES_VISUAL;
        int webContentsInset = 0;
        if (vkModeOutsetsWebContentsHeight) {
            // Avoid insetting by the accessory and outset by the keyboard to counter the View
            // resize.
            webContentsInset = -keyboardInset;
        } else {
            // The keyboard should cause the WebContents to resize. The keyboard itself is already
            // accounted for in the View height so just add the accessory.
            webContentsInset = accessoryInset;
        }
        webContentsInset += bottomSheetInset;

        newValues.webContentsHeightInset = webContentsInset;

        super.set(newValues);
    }

    private int intFromSupplier(ObservableSupplier<Integer> supplier) {
        if (supplier == null || supplier.get() == null) return 0;
        return supplier.get();
    }

    public boolean insetsAffectWebContentsSize() {
        return mVirtualKeyboardMode == VirtualKeyboardMode.RESIZES_CONTENT
                || mBottomSheetInsetSupplier != null;
    }
}