chromium/chrome/browser/gesturenav/android/java/src/org/chromium/chrome/browser/gesturenav/NativePageBitmapCapturer.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.gesturenav;

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.view.View;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import org.chromium.base.Callback;
import org.chromium.base.UnownedUserData;
import org.chromium.base.UnownedUserDataHost;
import org.chromium.base.UnownedUserDataKey;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.ui.native_page.NativePage;
import org.chromium.ui.resources.dynamics.CaptureUtils;
import org.chromium.url.GURL;

/** Capture native page as a bitmap. */
public class NativePageBitmapCapturer implements UnownedUserData {
    // Share SoftwareDraw in order to share a single java Bitmap across all tabs in a window
    // as the tab size won't change inside one single window.
    private static final UnownedUserDataKey<NativePageBitmapCapturer> CAPTURER_KEY =
            new UnownedUserDataKey<>(NativePageBitmapCapturer.class);

    private NativePageBitmapCapturer() {}

    /**
     * Capture native page as a bitmap.
     *
     * @param tab The target tab to be captured.
     * @param callback Executed with a non-null bitmap if the tab is presenting a native page. Empty
     *     bitmap if capturing fails, such as out of memory error.
     * @param topControlsHeight Height of the top controls.
     * @return True if the capture is successfully triggered; otherwise false.
     */
    public static boolean maybeCaptureNativeView(
            @NonNull Tab tab, @NonNull Callback<Bitmap> callback, int topControlsHeight) {
        if (!isCapturable(tab)) {
            return false;
        }

        UnownedUserDataHost host = tab.getWindowAndroid().getUnownedUserDataHost();
        if (CAPTURER_KEY.retrieveDataFromHost(host) == null) {
            CAPTURER_KEY.attachToHost(host, new NativePageBitmapCapturer());
        }

        // TODO(crbug.com/330230340): capture bitmap asynchronously.
        Bitmap bitmap = capture(tab, topControlsHeight);
        PostTask.postTask(TaskTraits.UI_USER_VISIBLE, () -> callback.onResult(bitmap));
        return true;
    }

    /**
     * Synchronous version of {@link #maybeCaptureNativeView(Tab, Callback, int)}.
     *
     * @param tab The target tab to be captured.
     * @param topControlsHeight Height of the top controls.
     * @return Null if fails; otherwise, a Bitmap object.
     */
    @Nullable
    public static Bitmap maybeCaptureNativeViewSync(@NonNull Tab tab, int topControlsHeight) {
        if (!isCapturable(tab)) {
            return null;
        }

        return capture(tab, topControlsHeight);
    }

    private static boolean isCapturable(Tab tab) {
        if (!tab.isNativePage()) {
            return false;
        }
        // The native page, like NTP, is displayed before the url is loaded. Return early to
        // prevent capturing the current NTP as the screenshot of the previous page
        GURL lastCommittedUrl = tab.getWebContents().getLastCommittedUrl();
        if (!NativePage.isNativePageUrl(lastCommittedUrl, tab.isIncognitoBranded(), false)) {
            return false;
        }
        if (tab.getWindowAndroid() == null) return false;

        View view = tab.getView();
        // The view is not laid out yet.
        if (view.getWidth() == 0 || view.getHeight() == 0) return false;
        return true;
    }

    private static Bitmap capture(Tab tab, int topControlsHeight) {
        UnownedUserDataHost host = tab.getWindowAndroid().getUnownedUserDataHost();
        if (CAPTURER_KEY.retrieveDataFromHost(host) == null) {
            CAPTURER_KEY.attachToHost(host, new NativePageBitmapCapturer());
        }
        final var capturer = CAPTURER_KEY.retrieveDataFromHost(host);

        View view = tab.getView();

        Bitmap bitmap = CaptureUtils.createBitmap(view.getWidth(), view.getHeight());
        bitmap.eraseColor(tab.getNativePage().getBackgroundColor());

        Canvas canvas = new Canvas(bitmap);
        float scale = capturer.getScale();

        // TODO(crbug.com/330230340): capture bitmap asynchronously.
        // Translate to exclude the area of the top controls.
        canvas.translate(0, -topControlsHeight);
        canvas.scale(scale, scale);
        view.draw(canvas);
        return bitmap;
    }

    private float getScale() {
        return (float)
                ChromeFeatureList.getFieldTrialParamByFeatureAsDouble(
                        ChromeFeatureList.BACK_FORWARD_TRANSITIONS, "downscale", 1);
    }
}