chromium/chrome/android/java/src/org/chromium/chrome/browser/feedback/ScreenshotTask.java

// Copyright 2017 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.feedback;

import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Rect;

import androidx.annotation.Nullable;

import org.jni_zero.CalledByNative;
import org.jni_zero.JNINamespace;
import org.jni_zero.NativeMethods;

import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.chrome.browser.app.ChromeActivity;
import org.chromium.chrome.browser.tab.SadTab;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetControllerProvider;
import org.chromium.ui.UiUtils;
import org.chromium.ui.base.WindowAndroid;

/** A utility class to take a feedback-formatted screenshot of an {@link Activity}. */
@JNINamespace("chrome::android")
public final class ScreenshotTask implements ScreenshotSource {
    /**
     * Maximum dimension for the screenshot to be sent to the feedback handler.  This size
     * ensures the size of bitmap < 1MB, which is a requirement of the handler.
     */
    private static final int MAX_FEEDBACK_SCREENSHOT_DIMENSION = 600;

    private final Activity mActivity;

    private boolean mDone;
    private Bitmap mBitmap;
    private Runnable mCallback;
    private @ScreenshotMode int mScreenshotMode;

    /**
     * Creates a {@link ScreenshotTask} instance that, will grab a screenshot of {@code activity}.
     * @param activity The {@link Activity} to grab a screenshot of.
     * @param screenshotMode The kind of screenshot to take.
     */
    public ScreenshotTask(Activity activity, @ScreenshotMode int screenshotMode) {
        mActivity = activity;
        mScreenshotMode = screenshotMode;
    }

    /**
     * Creates a {@link ScreenshotTask} instance that, will grab a screenshot of {@code activity}.
     * @param activity The {@link Activity} to grab a screenshot of.
     */
    public ScreenshotTask(Activity activity) {
        this(activity, ScreenshotMode.DEFAULT);
    }

    // ScreenshotSource implementation.
    @Override
    public void capture(@Nullable Runnable callback) {
        mCallback = callback;

        switch (mScreenshotMode) {
            case ScreenshotMode.DEFAULT:
                if (shouldTakeCompositorScreenshot(mActivity)
                        && takeCompositorScreenshot(mActivity)) {
                    return;
                }
                if (takeAndroidViewScreenshot(mActivity)) return;
                break;
            case ScreenshotMode.COMPOSITOR:
                if (takeCompositorScreenshot(mActivity)) return;
                break;
            case ScreenshotMode.ANDROID_VIEW:
                if (takeAndroidViewScreenshot(mActivity)) return;
                break;
        }

        // If neither the compositor nor the Android view screenshot tasks were kicked off, admit
        // defeat and return a {@code null} screenshot.
        PostTask.postTask(
                TaskTraits.UI_DEFAULT,
                new Runnable() {
                    @Override
                    public void run() {
                        onBitmapReceived(null);
                    }
                });
    }

    @Override
    public boolean isReady() {
        return mDone;
    }

    @Override
    public Bitmap getScreenshot() {
        return mBitmap;
    }

    // This will be called on the UI thread in response to
    // ScreenshotTaskJni.get().grabWindowSnapshotAsync.
    @CalledByNative
    private void onBytesReceived(byte[] pngBytes) {
        Bitmap bitmap = null;
        if (pngBytes != null) bitmap = BitmapFactory.decodeByteArray(pngBytes, 0, pngBytes.length);
        onBitmapReceived(bitmap);
    }

    private void onBitmapReceived(@Nullable Bitmap bitmap) {
        mDone = true;
        mBitmap = bitmap;
        if (mCallback != null) mCallback.run();
        mCallback = null;
    }

    private boolean takeCompositorScreenshot(@Nullable Activity activity) {
        if (activity == null) return false;

        Rect rect = new Rect();
        activity.getWindow().getDecorView().getRootView().getWindowVisibleDisplayFrame(rect);
        ScreenshotTaskJni.get()
                .grabWindowSnapshotAsync(
                        this,
                        ((ChromeActivity) activity).getWindowAndroid(),
                        rect.width(),
                        rect.height());

        return true;
    }

    private boolean takeAndroidViewScreenshot(@Nullable final Activity activity) {
        if (activity == null) return false;

        PostTask.postTask(
                TaskTraits.UI_DEFAULT,
                new Runnable() {
                    @Override
                    public void run() {
                        Bitmap bitmap =
                                UiUtils.generateScaledScreenshot(
                                        activity.getWindow().getDecorView().getRootView(),
                                        MAX_FEEDBACK_SCREENSHOT_DIMENSION,
                                        Bitmap.Config.ARGB_8888);
                        onBitmapReceived(bitmap);
                    }
                });

        return true;
    }

    private boolean shouldTakeCompositorScreenshot(Activity activity) {
        // If Activity isn't a ChromeActivity, we aren't using the Compositor to render.
        if (!(activity instanceof ChromeActivity)) return false;

        ChromeActivity chromeActivity = (ChromeActivity) activity;
        Tab currentTab = chromeActivity.getActivityTab();

        // If the bottom sheet is currently open, then do not use the Compositor based screenshot
        // so that the Android View for the bottom sheet will be captured.
        // TODO(crbug.com/40573072): When the sheet is partially opened both the compositor
        // and Android views should be captured in the screenshot.
        if (BottomSheetControllerProvider.from(chromeActivity.getWindowAndroid()).isSheetOpen()) {
            return false;
        }

        // If the grid tab switcher is in use, do not use the compositor, it will snapshot the last
        // active tab instead of the current screen if we try to use it.
        if (chromeActivity.isInOverviewMode()) {
            return false;
        }

        // If the tab is null, assume in the tab switcher so a Compositor snapshot is good.
        if (currentTab == null) return true;
        // If the tab is not interactable, also assume in the tab switcher.
        if (!currentTab.isUserInteractable()) return true;
        // If the tab focused and not showing Android widget based content, then use the Compositor
        // based screenshot.
        if (currentTab.getNativePage() == null && !SadTab.isShowing(currentTab)) return true;

        // Assume the UI is drawn primarily by Android widgets, so do not use the Compositor
        // screenshot.
        return false;
    }

    @NativeMethods
    interface Natives {
        void grabWindowSnapshotAsync(
                ScreenshotTask callback, WindowAndroid window, int width, int height);
    }
}