blob: 5494eda1e89df4666c2a96ce6c7836597194ed26 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package dev.flutter.plugins.integration_test;
import android.annotation.TargetApi;
import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.view.Choreographer;
import android.view.PixelCopy;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.android.FlutterSurfaceView;
import io.flutter.embedding.android.FlutterView;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.Result;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.StringBuilder;
/**
* FlutterDeviceScreenshot is a utility class that allows to capture a screenshot
* that includes both Android views and the Flutter UI.
*
* To take screenshots, the rendering surface must be changed to {@code FlutterImageView},
* since surfaces like {@code FlutterSurfaceView} and {@code FlutterTextureView} are opaque
* when the view hierarchy is rendered to a bitmap.
*
* It's also necessary to ask the framework to schedule a frame, and then add a listener
* that waits for that frame to be presented by the Android framework.
*/
@TargetApi(19)
class FlutterDeviceScreenshot {
/**
* Finds the {@code FlutterView} added to the {@code activity} view hierarchy.
*
* <p> This assumes that there's only one {@code FlutterView} per activity, which
* is always the case.
*
* @param activity typically, {code FlutterActivity}.
* @return the Flutter view.
*/
@Nullable
private static FlutterView getFlutterView(@NonNull Activity activity) {
return (FlutterView)activity.findViewById(FlutterActivity.FLUTTER_VIEW_ID);
}
/**
* Whether the app is run with instrumentation.
*
* @return true if the app is running with instrumentation.
*/
static boolean hasInstrumentation() {
// TODO(egarciad): InstrumentationRegistry requires the uiautomator dependency.
// However, Flutter adds test dependencies to release builds.
// As a result, disable screenshots with instrumentation until the issue is fixed.
// https://github.com/flutter/flutter/issues/56591
return false;
}
/**
* Captures a screenshot using ui automation.
*
* @return byte array containing the screenshot.
*/
static byte[] captureWithUiAutomation() throws IOException {
return new byte[0];
}
// Whether the flutter surface is already converted to an image.
private static boolean flutterSurfaceConvertedToImage = false;
/**
* Converts the Flutter surface to an image view.
* This allows to render the view hierarchy to a bitmap since
* {@code FlutterSurfaceView} and {@code FlutterTextureView} cannot be rendered to a bitmap.
*
* @param activity typically {@code FlutterActivity}.
*/
static void convertFlutterSurfaceToImage(@NonNull Activity activity) {
final FlutterView flutterView = getFlutterView(activity);
if (flutterView != null && !flutterSurfaceConvertedToImage) {
flutterView.convertToImageView();
flutterSurfaceConvertedToImage = true;
}
}
/**
* Restores the original Flutter surface.
* The new surface will either be {@code FlutterSurfaceView} or {@code FlutterTextureView}.
*
* @param activity typically {@code FlutterActivity}.
* @param onDone callback called once the surface has been restored.
*/
static void revertFlutterImage(@NonNull Activity activity) {
final FlutterView flutterView = getFlutterView(activity);
if (flutterView != null && flutterSurfaceConvertedToImage) {
flutterView.revertImageView(() -> {
flutterSurfaceConvertedToImage = false;
});
}
}
// Handlers use to capture a view.
private static Handler backgroundHandler;
private static Handler mainHandler;
/**
* Captures a screenshot by drawing the view to a Canvas.
*
* <p> {@code convertFlutterSurfaceToImage} must be called prior to capturing the view,
* otherwise the result is an error.
*
* @param activity this is {@link FlutterActivity}.
* @param methodChannel the method channel to call into Dart.
* @param result the result for the method channel that will contain the byte array.
*/
static void captureView(
@NonNull Activity activity, @NonNull MethodChannel methodChannel, @NonNull Result result) {
final FlutterView flutterView = getFlutterView(activity);
if (flutterView == null) {
result.error("Could not copy the pixels", "FlutterView is null", null);
return;
}
if (!flutterSurfaceConvertedToImage) {
result.error("Could not copy the pixels", "Flutter surface must be converted to image first", null);
return;
}
// Ask the framework to schedule a new frame.
methodChannel.invokeMethod("scheduleFrame", null);
if (backgroundHandler == null) {
final HandlerThread screenshotBackgroundThread = new HandlerThread("screenshot");
screenshotBackgroundThread.start();
backgroundHandler = new Handler(screenshotBackgroundThread.getLooper());
}
if (mainHandler == null) {
mainHandler = new Handler(Looper.getMainLooper());
}
takeScreenshot(backgroundHandler, mainHandler, flutterView, result);
}
/**
* Waits for the next Android frame.
*
* @param r a callback.
*/
private static void waitForAndroidFrame(Runnable r) {
Choreographer.getInstance()
.postFrameCallback(
new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
r.run();
}
});
}
/**
* Waits until a Flutter frame is rendered by the Android OS.
*
* @param backgroundHandler the handler associated to a background thread.
* @param mainHandler the handler associated to the platform thread.
* @param view the flutter view.
* @param result the result that contains the byte array.
*/
private static void takeScreenshot(
@NonNull Handler backgroundHandler,
@NonNull Handler mainHandler,
@NonNull FlutterView view,
@NonNull Result result) {
final boolean acquired = view.acquireLatestImageViewFrame();
// The next frame may already have already been comitted.
// The next frame is guaranteed to have the Flutter image.
waitForAndroidFrame(
() -> {
waitForAndroidFrame(
() -> {
if (acquired) {
FlutterDeviceScreenshot.convertViewToBitmap(view, result, backgroundHandler);
} else {
takeScreenshot(backgroundHandler, mainHandler, view, result);
}
});
});
}
/**
* Renders {@code FlutterView} to a Bitmap.
*
* If successful, The byte array is provided in the result.
*
* @param flutterView the Flutter view.
* @param result the result that contains the byte array.
* @param backgroundHandler a background handler to avoid blocking the platform thread.
*/
private static void convertViewToBitmap(
@NonNull FlutterView flutterView, @NonNull Result result, @NonNull Handler backgroundHandler) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
final Bitmap bitmap =
Bitmap.createBitmap(
flutterView.getWidth(), flutterView.getHeight(), Bitmap.Config.RGB_565);
final Canvas canvas = new Canvas(bitmap);
flutterView.draw(canvas);
final ByteArrayOutputStream output = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, /*quality=*/ 100, output);
result.success(output.toByteArray());
return;
}
final Bitmap bitmap =
Bitmap.createBitmap(
flutterView.getWidth(), flutterView.getHeight(), Bitmap.Config.ARGB_8888);
final int[] flutterViewLocation = new int[2];
flutterView.getLocationInWindow(flutterViewLocation);
final int flutterViewLeft = flutterViewLocation[0];
final int flutterViewTop = flutterViewLocation[1];
final Rect flutterViewRect =
new Rect(
flutterViewLeft,
flutterViewTop,
flutterViewLeft + flutterView.getWidth(),
flutterViewTop + flutterView.getHeight());
final Activity flutterActivity = (Activity) flutterView.getContext();
PixelCopy.request(
flutterActivity.getWindow(),
flutterViewRect,
bitmap,
(int copyResult) -> {
final Handler mainHandler = new Handler(Looper.getMainLooper());
if (copyResult == PixelCopy.SUCCESS) {
final ByteArrayOutputStream output = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, /*quality=*/ 100, output);
mainHandler.post(
() -> {
result.success(output.toByteArray());
});
} else {
mainHandler.post(
() -> {
result.error("Could not copy the pixels", "result was " + copyResult, null);
});
}
},
backgroundHandler);
}
}