chromium/chrome/android/java/src/org/chromium/chrome/browser/status_indicator/StatusIndicatorCoordinator.java

// Copyright 2019 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.status_indicator;

import android.app.Activity;
import android.graphics.drawable.Drawable;
import android.view.View;
import android.view.ViewStub;

import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;

import org.chromium.base.Callback;
import org.chromium.base.supplier.Supplier;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.browser_controls.BrowserControlsStateProvider;
import org.chromium.chrome.browser.tab.TabObscuringHandler;
import org.chromium.components.browser_ui.widget.ViewResourceFrameLayout;
import org.chromium.ui.base.ViewUtils;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.modelutil.PropertyModelChangeProcessor;
import org.chromium.ui.resources.ResourceManager;
import org.chromium.ui.resources.dynamics.ViewResourceAdapter;

/**
 * The coordinator for a status indicator that is positioned below the status bar and is persistent.
 * Typically used to relay status, e.g. indicate user is offline.
 */
public class StatusIndicatorCoordinator {
    /** An observer that will be notified of the changes to the status indicator, e.g. height. */
    public interface StatusIndicatorObserver {
        /**
         * Called when the height of the status indicator changes.
         * @param newHeight The new height in pixels.
         */
        default void onStatusIndicatorHeightChanged(int newHeight) {}

        /**
         * Called when the background color of the status indicator changes.
         * @param newColor The new color as {@link ColorInt}.
         */
        default void onStatusIndicatorColorChanged(@ColorInt int newColor) {}

        /** Called when the "show" animation of the status indicator completes. */
        default void onStatusIndicatorShowAnimationEnd() {}
    }

    private StatusIndicatorMediator mMediator;
    private StatusIndicatorSceneLayer mSceneLayer;
    private boolean mIsShowing;
    private Runnable mRemoveOnLayoutChangeListener;
    private int mResourceId;
    private ViewResourceAdapter mResourceAdapter;
    private ResourceManager mResourceManager;
    private boolean mResourceRegistered;
    private Activity mActivity;
    private Callback<Runnable> mRequestRender;
    private boolean mInitialized;

    /**
     * Constructs the status indicator.
     * @param activity The {@link Activity} to find and inflate the status indicator view.
     * @param resourceManager The {@link ResourceManager} for the status indicator's cc layer.
     * @param browserControlsStateProvider The {@link BrowserControlsStateProvider} to listen to
     *                                     for the changes in controls offsets.
     * @param tabObscuringHandler Delegate object handling obscuring views.
     * @param statusBarColorWithoutStatusIndicatorSupplier A supplier that will get the status bar
     *                                                     color without taking the status indicator
     *                                                     into account.
     * @param canAnimateNativeBrowserControls Will supply a boolean meaning whether the native
     *                                        browser controls can be animated. This will be false
     *                                        where we can't have a reliable cc::BCOM instance, e.g.
     *                                        tab switcher.
     * @param requestRender Runnable to request a render when the cc-layer needs to be updated.
     */
    public StatusIndicatorCoordinator(
            Activity activity,
            ResourceManager resourceManager,
            BrowserControlsStateProvider browserControlsStateProvider,
            TabObscuringHandler tabObscuringHandler,
            Supplier<Integer> statusBarColorWithoutStatusIndicatorSupplier,
            Supplier<Boolean> canAnimateNativeBrowserControls,
            Callback<Runnable> requestRender) {
        mActivity = activity;
        mResourceManager = resourceManager;
        mRequestRender = requestRender;

        mSceneLayer = new StatusIndicatorSceneLayer(browserControlsStateProvider);
        mMediator =
                new StatusIndicatorMediator(
                        browserControlsStateProvider,
                        tabObscuringHandler,
                        statusBarColorWithoutStatusIndicatorSupplier,
                        canAnimateNativeBrowserControls);
    }

    public void destroy() {
        if (mInitialized) mRemoveOnLayoutChangeListener.run();
        if (mResourceRegistered) unregisterResource();
        mMediator.destroy();
    }

    /**
     * Show the status indicator with the initial properties with animations.
     *
     * @param statusText The status string that will be visible on the status indicator.
     * @param statusIcon The icon {@link Drawable} that will appear next to the status text.
     * @param backgroundColor The background color for the status indicator and the status bar.
     * @param textColor Status text color.
     * @param iconTint Status icon tint.
     */
    public void show(
            @NonNull String statusText,
            Drawable statusIcon,
            @ColorInt int backgroundColor,
            @ColorInt int textColor,
            @ColorInt int iconTint) {
        // TODO(crbug.com/40130539): We should make sure #show, #hide, and #updateContent can't be
        // called at the wrong time, or the call is ignored with a way to communicate this to the
        // caller, e.g. returning a boolean.
        if (mIsShowing) return;
        mIsShowing = true;

        if (!mInitialized) initialize();

        mMediator.animateShow(statusText, statusIcon, backgroundColor, textColor, iconTint);
    }

    /**
     * Update the status indicator text, icon and colors with animations. All of the properties will
     * be animated even if only one property changes. Support to animate a single property may be
     * added in the future if needed.
     *
     * @param statusText The string that will replace the current text.
     * @param statusIcon The icon that will replace the current icon.
     * @param backgroundColor The color that will replace the status indicator background color.
     * @param textColor The new text color to fit the new background.
     * @param iconTint The new icon tint to fit the background.
     * @param animationCompleteCallback The callback that will be run once the animations end.
     */
    public void updateContent(
            @NonNull String statusText,
            Drawable statusIcon,
            @ColorInt int backgroundColor,
            @ColorInt int textColor,
            @ColorInt int iconTint,
            Runnable animationCompleteCallback) {
        if (!mIsShowing) return;

        mMediator.animateUpdate(
                statusText,
                statusIcon,
                backgroundColor,
                textColor,
                iconTint,
                animationCompleteCallback);
    }

    /** Hide the status indicator with animations. */
    public void hide() {
        if (!mIsShowing) return;
        mIsShowing = false;

        mMediator.animateHide();
    }

    public void addObserver(StatusIndicatorObserver observer) {
        mMediator.addObserver(observer);
    }

    public void removeObserver(StatusIndicatorObserver observer) {
        mMediator.removeObserver(observer);
    }

    /**
     * @return The {@link StatusIndicatorSceneLayer}.
     */
    public StatusIndicatorSceneLayer getSceneLayer() {
        return mSceneLayer;
    }

    /** @return The class of the {@link SceneOverlay} owned by this coordinator. */
    public static Class getSceneOverlayClass() {
        return StatusIndicatorSceneLayer.class;
    }

    private void initialize() {
        final ViewStub stub = mActivity.findViewById(R.id.status_indicator_stub);
        final ViewResourceFrameLayout root = (ViewResourceFrameLayout) stub.inflate();
        mResourceId = root.getId();
        mSceneLayer.setResourceId(mResourceId);
        mResourceAdapter = root.getResourceAdapter();
        Callback<Runnable> invalidateCompositorView =
                callback -> {
                    mResourceAdapter.invalidate(null);
                    mRequestRender.onResult(callback);
                };
        PropertyModel model =
                new PropertyModel.Builder(StatusIndicatorProperties.ALL_KEYS)
                        .with(StatusIndicatorProperties.ANDROID_VIEW_VISIBILITY, View.GONE)
                        .with(StatusIndicatorProperties.COMPOSITED_VIEW_VISIBLE, false)
                        .build();
        PropertyModelChangeProcessor.create(
                model,
                new StatusIndicatorViewBinder.ViewHolder(root, mSceneLayer),
                StatusIndicatorViewBinder::bind);
        mMediator.initialize(
                model,
                this::registerResource,
                this::unregisterResource,
                invalidateCompositorView,
                () -> {
                    ViewUtils.requestLayout(root, "StatusIndicatorCoordinator.initialize Runnable");
                });
        root.addOnLayoutChangeListener(mMediator);
        mRemoveOnLayoutChangeListener = () -> root.removeOnLayoutChangeListener(mMediator);

        mInitialized = true;
    }

    private void registerResource() {
        if (mResourceRegistered) return;

        mResourceManager.getDynamicResourceLoader().registerResource(mResourceId, mResourceAdapter);
        mResourceRegistered = true;
    }

    private void unregisterResource() {
        if (!mResourceRegistered) return;

        mResourceAdapter.dropCachedBitmap();
        mResourceManager.getDynamicResourceLoader().unregisterResource(mResourceId);
        mResourceRegistered = false;
    }

    StatusIndicatorMediator getMediatorForTesting() {
        return mMediator;
    }
}