chromium/ui/android/java/src/org/chromium/ui/resources/dynamics/ViewResourceAdapter.java

// Copyright 2014 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.resources.dynamics;

import android.graphics.Bitmap;
import android.graphics.Rect;
import android.view.View;
import android.view.View.OnLayoutChangeListener;
import android.view.ViewGroup;

import androidx.annotation.CallSuper;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.Callback;
import org.chromium.base.ObserverList;
import org.chromium.base.ThreadUtils;
import org.chromium.base.TraceEvent;
import org.chromium.ui.resources.Resource;
import org.chromium.ui.resources.ResourceFactory;

/**
 * An adapter that exposes a {@link View} as a {@link DynamicResourceSnapshot}. In order to properly
 * use this adapter {@link ViewResourceAdapter#invalidate(Rect)} must be called when parts of the
 * {@link View} are invalidated.  For {@link ViewGroup}s the easiest way to do this is to override
 * {@link ViewGroup#invalidateChildInParent(int[], Rect)}.
 */
public class ViewResourceAdapter
        implements DynamicResource, OnLayoutChangeListener, CaptureObserver {
    /** Abstraction around the mechanism for actually capturing bitmaps.  */
    public interface CaptureMechanism {
        /** See {@link Resource#shouldRemoveResourceOnNullBitmap()}. */
        boolean shouldRemoveResourceOnNullBitmap();

        /** Called when the size of the view changes. */
        default void onViewSizeChange(View view, float scale) {}

        /** Called to drop any cached bitmaps to free up memory. */
        void dropCachedBitmap();

        /**
         * Called to trigger the actual bitmap capture.
         *
         * @param view The view being captured.
         * @param dirtyRect The area that has changed since last capture.
         * @param scale Scalar to apply to width and height when capturing a bitmap.
         * @param observer To be notified before and after the capture happens.
         * @param onBitmapCapture The callback to return the recorded image.
         * @return If the dirty rect can be cleared on a successful capture.
         */
        boolean startBitmapCapture(
                View view,
                Rect dirtyRect,
                float scale,
                CaptureObserver observer,
                Callback<Bitmap> onBitmapCapture);
    }

    private final View mView;
    private final Rect mDirtyRect = new Rect();
    private final Rect mViewSize = new Rect();
    private final ThreadUtils.ThreadChecker mThreadChecker = new ThreadUtils.ThreadChecker();
    private final CaptureMechanism mCaptureMechanism;
    private float mScale = 1;

    private final ObserverList<Callback<Resource>> mOnResourceReadyObservers = new ObserverList<>();

    /**
     * Builds a {@link ViewResourceAdapter} instance around {@code view}.
     *
     * @param view The {@link View} to expose as a {@link Resource}.
     */
    @SuppressWarnings("NewApi")
    public ViewResourceAdapter(View view) {
        mView = view;

        // It is possible the view has not had an layout pass yet, and these values are wrong. Even
        // when this is the case, because we also listen to onLayout changes, we will update
        // accordingly.
        mView.addOnLayoutChangeListener(this);
        mViewSize.set(0, 0, mView.getWidth(), mView.getHeight());
        mDirtyRect.set(mViewSize);

        mCaptureMechanism = new SoftwareDraw();
    }

    /**
     * Triggers a bitmap capture ignoring whether the view is dirty. Depending on this mechanism, it
     * may do some or all of the work, and may be sync or async.
     */
    @SuppressWarnings("NewApi")
    public void triggerBitmapCapture() {
        mThreadChecker.assertOnValidThread();
        try (TraceEvent e = TraceEvent.scoped("ViewResourceAdapter:getBitmap")) {
            if (mCaptureMechanism.startBitmapCapture(
                    mView, new Rect(mDirtyRect), mScale, this, this::onCapture)) {
                mDirtyRect.setEmpty();
            }
        }
    }

    private void onCapture(Bitmap bitmap) {
        mThreadChecker.assertOnValidThread();
        Resource resource =
                new DynamicResourceSnapshot(
                        bitmap,
                        mCaptureMechanism.shouldRemoveResourceOnNullBitmap(),
                        mViewSize,
                        createNativeResource());
        for (Callback<Resource> observer : mOnResourceReadyObservers) observer.onResult(resource);
    }

    /**
     * Set the downsampling scale. The rendered size is not affected.
     * @param scale The scale to use. <1 means the Bitmap is smaller than the View.
     */
    public void setDownsamplingScale(float scale) {
        assert scale <= 1;
        if (mScale != scale) {
            invalidate(null);
        }
        mScale = scale;
    }

    /** {@see Resource#createNativeResource()}. */
    public long createNativeResource() {
        return ResourceFactory.createBitmapResource(null);
    }

    @Override
    public void addOnResourceReadyCallback(Callback<Resource> onResourceReady) {
        mOnResourceReadyObservers.addObserver(onResourceReady);
    }

    @Override
    public void removeOnResourceReadyCallback(Callback<Resource> onResourceReady) {
        mOnResourceReadyObservers.removeObserver(onResourceReady);
    }

    /**
     * On every render frame, this resources will check to see if it is dirty. If so, it will take
     * a bitmap capture and push it to the renderer. Subclasses can override this method to suppress
     * when captures are/can be taken.
     * @return If a bitmap capture should be taken/started.
     */
    @CallSuper
    protected boolean isDirty() {
        return !mDirtyRect.isEmpty();
    }

    @Override
    public void onResourceRequested() {
        if (!mOnResourceReadyObservers.isEmpty() && isDirty()) {
            triggerBitmapCapture();
        }
    }

    @Override
    public void onLayoutChange(
            View v,
            int left,
            int top,
            int right,
            int bottom,
            int oldLeft,
            int oldTop,
            int oldRight,
            int oldBottom) {
        final int width = right - left;
        final int height = bottom - top;
        final int oldWidth = oldRight - oldLeft;
        final int oldHeight = oldBottom - oldTop;

        if (width != oldWidth || height != oldHeight) {
            mViewSize.set(0, 0, width, height);
            mDirtyRect.set(0, 0, width, height);
            mCaptureMechanism.onViewSizeChange(mView, mScale);
        }
    }

    /**
     * Invalidates a particular region of the {@link View} that needs to be repainted.
     * @param dirtyRect The region to invalidate, or {@code null} if the entire {@code Bitmap}
     *                  should be redrawn.
     */
    public void invalidate(Rect dirtyRect) {
        if (dirtyRect == null) {
            mDirtyRect.set(0, 0, mView.getWidth(), mView.getHeight());
        } else {
            mDirtyRect.union(dirtyRect);
        }
    }

    /** Drops the cached bitmap to free up memory. */
    public void dropCachedBitmap() {
        mCaptureMechanism.dropCachedBitmap();
    }

    /** Returns the dirty rect that will be drawn on capture. */
    @VisibleForTesting
    public Rect getDirtyRect() {
        return mDirtyRect;
    }

    /** Clears the contents of the current dirty rect. */
    protected void setDirtyRectEmpty() {
        mDirtyRect.setEmpty();
    }
}