diff --git a/dev/integration_tests/android_engine_test/android/app/src/main/kotlin/com/example/native_driver_test/MainActivity.kt b/dev/integration_tests/android_engine_test/android/app/src/main/kotlin/com/example/native_driver_test/MainActivity.kt index 807b51c39c..6b74332f17 100644 --- a/dev/integration_tests/android_engine_test/android/app/src/main/kotlin/com/example/native_driver_test/MainActivity.kt +++ b/dev/integration_tests/android_engine_test/android/app/src/main/kotlin/com/example/native_driver_test/MainActivity.kt @@ -13,6 +13,7 @@ import androidx.core.view.WindowInsetsControllerCompat import com.example.android_engine_test.extensions.NativeDriverSupportPlugin import com.example.android_engine_test.fixtures.BlueOrangeGradientPlatformViewFactory import com.example.android_engine_test.fixtures.ChangingColorButtonPlatformViewFactory +import com.example.android_engine_test.fixtures.OtherFaceTexturePlugin import com.example.android_engine_test.fixtures.SmileyFaceTexturePlugin import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine @@ -25,6 +26,7 @@ class MainActivity : FlutterActivity() { .plugins .apply { add(SmileyFaceTexturePlugin()) + add(OtherFaceTexturePlugin()) add(NativeDriverSupportPlugin()) } diff --git a/dev/integration_tests/android_engine_test/android/app/src/main/kotlin/com/example/native_driver_test/fixtures/OtherFaceTexturePlugin.kt b/dev/integration_tests/android_engine_test/android/app/src/main/kotlin/com/example/native_driver_test/fixtures/OtherFaceTexturePlugin.kt new file mode 100644 index 0000000000..24cb3a2b25 --- /dev/null +++ b/dev/integration_tests/android_engine_test/android/app/src/main/kotlin/com/example/native_driver_test/fixtures/OtherFaceTexturePlugin.kt @@ -0,0 +1,91 @@ +// 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. + +@file:Suppress("PackageName") + +package com.example.android_engine_test.fixtures + +import android.graphics.Color +import android.graphics.Paint +import android.os.Build +import android.view.Surface +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.view.TextureRegistry.SurfaceTextureEntry + +class OtherFaceTexturePlugin : + FlutterPlugin, + MethodCallHandler { + private val tag = "OtherFaceTexturePlugin" + private lateinit var channel: MethodChannel + private lateinit var binding: FlutterPluginBinding + private lateinit var surfaceTextureEntry: SurfaceTextureEntry + + private var surface: Surface? = null + + override fun onAttachedToEngine(binding: FlutterPluginBinding) { + this.binding = binding + channel = MethodChannel(binding.binaryMessenger, "other_face_texture") + channel.setMethodCallHandler(this) + surfaceTextureEntry = binding.textureRegistry.createSurfaceTexture() + } + + override fun onDetachedFromEngine(binding: FlutterPluginBinding) { + channel.setMethodCallHandler(null) + surfaceTextureEntry.release() + } + + override fun onMethodCall( + call: MethodCall, + result: MethodChannel.Result + ) { + if (call.method == "initTexture") { + val height = call.argument("height") ?: 1 + val width = call.argument("width") ?: 1 + surfaceTextureEntry.surfaceTexture().setDefaultBufferSize(width, height) + result.success(updateTexture()) + } else { + result.notImplemented() + } + } + + private fun updateTexture(): Long { + var surface = this.surface + if (surface == null) { + surface = Surface(surfaceTextureEntry.surfaceTexture()) + this.surface = surface + } + drawOnSurface(surface!!) + return surfaceTextureEntry.id() + } + + private fun drawOnSurface(surface: Surface) { + val canvas = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + surface.lockHardwareCanvas() + } else { + surface.lockCanvas(null) + } + + // Yellow background + canvas.drawRGB(255, 230, 15) + + val paint = Paint() + paint.style = Paint.Style.FILL + + // Black eyes + paint.color = Color.BLACK + canvas.drawCircle(225f, 225f, 25f, paint) // Left eye + canvas.drawCircle(425f, 225f, 25f, paint) // Right eye + + // Black mouth + paint.color = Color.BLACK + canvas.drawCircle(300f, 300f, 50f, paint) // Simple mouth + + surface.unlockCanvasAndPost(canvas) + } +} diff --git a/dev/integration_tests/android_engine_test/lib/external_texture_other_face_main.dart b/dev/integration_tests/android_engine_test/lib/external_texture_other_face_main.dart new file mode 100644 index 0000000000..3df4b896e5 --- /dev/null +++ b/dev/integration_tests/android_engine_test/lib/external_texture_other_face_main.dart @@ -0,0 +1,54 @@ +// 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. + +import 'dart:async'; + +import 'package:android_driver_extensions/extension.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_driver/driver_extension.dart'; + +import 'src/allow_list_devices.dart'; + +const MethodChannel _channel = MethodChannel('other_face_texture'); +Future _fetchTexture(int width, int height) async { + final int? result = await _channel.invokeMethod('initTexture', { + 'width': width, + 'height': height, + }); + return result!; +} + +void main() async { + ensureAndroidOrIosDevice(); + enableFlutterDriverExtension(commands: [nativeDriverCommands]); + + // Run on full screen. + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); + + // Fetch the texture ID. + final Future textureId = _fetchTexture(512, 512); + runApp(MainApp(textureId)); +} + +final class MainApp extends StatelessWidget { + const MainApp(this.textureId, {super.key}); + final Future textureId; + + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + home: FutureBuilder( + future: textureId, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + return Texture(textureId: snapshot.data!); + } + return const CircularProgressIndicator(); + }, + ), + ); + } +} diff --git a/dev/integration_tests/android_engine_test/test_driver/external_texture_other_face_main_test.dart b/dev/integration_tests/android_engine_test/test_driver/external_texture_other_face_main_test.dart new file mode 100644 index 0000000000..4ea49c151c --- /dev/null +++ b/dev/integration_tests/android_engine_test/test_driver/external_texture_other_face_main_test.dart @@ -0,0 +1,54 @@ +// 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. + +import 'package:android_driver_extensions/native_driver.dart'; +import 'package:android_driver_extensions/skia_gold.dart'; +import 'package:flutter_driver/flutter_driver.dart'; +import 'package:test/test.dart'; + +import '_luci_skia_gold_prelude.dart'; + +void main() async { + // To test the golden file generation locally, comment out the following line. + // autoUpdateGoldenFiles = true; + + const String appName = 'com.example.android_engine_test'; + late final FlutterDriver flutterDriver; + late final NativeDriver nativeDriver; + + setUpAll(() async { + if (isLuci) { + await enableSkiaGoldComparator(); + } + flutterDriver = await FlutterDriver.connect(); + nativeDriver = await AndroidNativeDriver.connect(flutterDriver); + await nativeDriver.configureForScreenshotTesting(); + }); + + tearDownAll(() async { + await nativeDriver.close(); + await flutterDriver.close(); + }); + + test('should screenshot and match an external smiley face texture', () async { + await flutterDriver.waitFor(find.byType('Texture')); + + // On Android: Background the app, trim memory, and restore the app. + if (nativeDriver case final AndroidNativeDriver nativeDriver) { + print('Backgrounding the app, trimming memory, and resuming the app.'); + await nativeDriver.backgroundApp(); + + print('Trimming memory.'); + await nativeDriver.simulateLowMemory(appName: appName); + + print('Resuming the app.'); + await nativeDriver.resumeApp(appName: appName); + } + + await expectLater( + nativeDriver.screenshot(), + matchesGoldenFile('external_texture_other_face.android.png'), + ); + }, timeout: Timeout.none); +} diff --git a/engine/src/flutter/shell/platform/android/surface_texture_external_texture_vk_impeller.cc b/engine/src/flutter/shell/platform/android/surface_texture_external_texture_vk_impeller.cc index eb144e83bd..0cf96aa97e 100644 --- a/engine/src/flutter/shell/platform/android/surface_texture_external_texture_vk_impeller.cc +++ b/engine/src/flutter/shell/platform/android/surface_texture_external_texture_vk_impeller.cc @@ -122,15 +122,17 @@ void SurfaceTextureExternalTextureVKImpeller::ProcessFrame( VALIDATION_LOG << "Invalid external texture."; return; } + SkMatrix matrix = context.canvas->GetTransform(); + SkRect mapped_bounds = matrix.mapRect(bounds); const auto& surface_context = SurfaceContextVK::Cast(*context.aiks_context->GetContext()); const auto& context_vk = ContextVK::Cast(*surface_context.GetParent()); - auto dst_texture = - GetCachedTextureSource(surface_context.GetParent(), // - ISize::MakeWH(bounds.width(), bounds.height()) // - ); + auto dst_texture = GetCachedTextureSource( + surface_context.GetParent(), // + ISize::MakeWH(mapped_bounds.width(), mapped_bounds.height()) // + ); if (!dst_texture || !dst_texture->IsValid()) { VALIDATION_LOG << "Could not fetch trampoline texture target."; return; @@ -224,7 +226,10 @@ void SurfaceTextureExternalTextureVKImpeller::DrawFrame( PaintContext& context, const SkRect& bounds, const DlImageSampling sampling) const { - context.canvas->DrawImage(dl_image_, SkPoint{0, 0}, sampling, context.paint); + context.canvas->DrawImageRect( + dl_image_, + SkRect::MakeSize(SkSize::Make(dl_image_->width(), dl_image_->height())), + bounds, sampling, context.paint); } } // namespace flutter