From 89b336109fd409a5e3623f42b9b93a4714cd152c Mon Sep 17 00:00:00 2001 From: Matan Lurey Date: Thu, 9 Jan 2025 19:23:40 -0800 Subject: [PATCH] Add a virtual-display (VD) platform view test, and refactor tests a bit. (#161349) Towards https://github.com/flutter/flutter/issues/161261. Still need to add a HC (Hybrid Composition) variant, but figured I'd do this incrementally to make it easier to review. --- .../native_driver_test/MainActivity.kt | 2 + .../extensions/NativeDriverSupportPlugin.kt | 5 + ...eGradientSurfaceViewPlatformViewFactory.kt | 98 +++++++++++++++++++ .../external_texture_smiley_face_main.dart | 2 +- .../flutter_rendered_blue_rectangle_main.dart | 2 +- ...ybrid_composition_platform_view_main.dart} | 9 +- .../virtual_display_platform_view_main.dart | 36 +++++++ .../platform_view_tap_color_change_main.dart | 2 +- .../lib/src/allow_list_devices.dart | 8 +- ...d_composition_platform_view_main_test.dart | 64 ++++++++++++ ...tual_display_platform_view_main_test.dart} | 16 ++- .../lib/extension.dart | 10 +- .../lib/src/backend/android/driver.dart | 6 ++ .../lib/src/common.dart | 3 + .../lib/src/driver.dart | 3 + 15 files changed, 250 insertions(+), 16 deletions(-) create mode 100644 dev/integration_tests/android_engine_test/android/app/src/main/kotlin/com/example/native_driver_test/fixtures/BlueOrangeGradientSurfaceViewPlatformViewFactory.kt rename dev/integration_tests/android_engine_test/lib/{platform_view_blue_orange_gradient_main.dart => platform_view/texture_layer_hybrid_composition_platform_view_main.dart} (72%) create mode 100644 dev/integration_tests/android_engine_test/lib/platform_view/virtual_display_platform_view_main.dart create mode 100644 dev/integration_tests/android_engine_test/test_driver/platform_view/texture_layer_hybrid_composition_platform_view_main_test.dart rename dev/integration_tests/android_engine_test/test_driver/{platform_view_blue_orange_gradient_main_test.dart => platform_view/virtual_display_platform_view_main_test.dart} (70%) 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 6b74332f17..66cc6cb672 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 @@ -12,6 +12,7 @@ import androidx.core.view.WindowInsetsCompat 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.BlueOrangeGradientSurfaceViewPlatformViewFactory import com.example.android_engine_test.fixtures.ChangingColorButtonPlatformViewFactory import com.example.android_engine_test.fixtures.OtherFaceTexturePlugin import com.example.android_engine_test.fixtures.SmileyFaceTexturePlugin @@ -35,6 +36,7 @@ class MainActivity : FlutterActivity() { .registry .apply { registerViewFactory("blue_orange_gradient_platform_view", BlueOrangeGradientPlatformViewFactory()) + registerViewFactory("blue_orange_gradient_surface_view_platform_view", BlueOrangeGradientSurfaceViewPlatformViewFactory()) registerViewFactory("changing_color_button_platform_view", ChangingColorButtonPlatformViewFactory()) } } diff --git a/dev/integration_tests/android_engine_test/android/app/src/main/kotlin/com/example/native_driver_test/extensions/NativeDriverSupportPlugin.kt b/dev/integration_tests/android_engine_test/android/app/src/main/kotlin/com/example/native_driver_test/extensions/NativeDriverSupportPlugin.kt index 13d77b6f49..f8d64a4158 100644 --- a/dev/integration_tests/android_engine_test/android/app/src/main/kotlin/com/example/native_driver_test/extensions/NativeDriverSupportPlugin.kt +++ b/dev/integration_tests/android_engine_test/android/app/src/main/kotlin/com/example/native_driver_test/extensions/NativeDriverSupportPlugin.kt @@ -7,6 +7,7 @@ package com.example.android_engine_test.extensions import android.app.Activity +import android.os.Build import android.os.SystemClock import android.view.MotionEvent import io.flutter.Log @@ -44,6 +45,10 @@ class NativeDriverSupportPlugin : return } when (call.method) { + "sdk_version" -> { + val versionMap = mapOf("version" to Build.VERSION.SDK_INT) + result.success(versionMap) + } "ping" -> { result.success(null) } diff --git a/dev/integration_tests/android_engine_test/android/app/src/main/kotlin/com/example/native_driver_test/fixtures/BlueOrangeGradientSurfaceViewPlatformViewFactory.kt b/dev/integration_tests/android_engine_test/android/app/src/main/kotlin/com/example/native_driver_test/fixtures/BlueOrangeGradientSurfaceViewPlatformViewFactory.kt new file mode 100644 index 0000000000..9736b39fe4 --- /dev/null +++ b/dev/integration_tests/android_engine_test/android/app/src/main/kotlin/com/example/native_driver_test/fixtures/BlueOrangeGradientSurfaceViewPlatformViewFactory.kt @@ -0,0 +1,98 @@ +// 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.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.LinearGradient +import android.graphics.Paint +import android.graphics.Shader +import android.view.SurfaceHolder +import android.view.SurfaceView +import android.view.View +import android.view.ViewGroup +import io.flutter.plugin.platform.PlatformView +import io.flutter.plugin.platform.PlatformViewFactory + +class BlueOrangeGradientSurfaceViewPlatformViewFactory : PlatformViewFactory(null) { + override fun create( + context: Context, + viewId: Int, + args: Any? + ): PlatformView = GradientSurfaceViewPlatformView(context) +} + +private class GradientSurfaceViewPlatformView( + context: Context +) : SurfaceView(context), + PlatformView, + SurfaceHolder.Callback { + val paint = Paint() + + init { + layoutParams = + ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + holder.addCallback(this) + } + + override fun getView(): View = this + + override fun dispose() {} + + override fun surfaceCreated(holder: SurfaceHolder) { + val canvas = holder.lockCanvas() + if (canvas != null) { + drawGradient(canvas) + holder.unlockCanvasAndPost(canvas) + } + } + + override fun surfaceChanged( + holder: SurfaceHolder, + format: Int, + width: Int, + height: Int + ) { + val canvas = holder.lockCanvas() + if (canvas != null) { + drawGradient(canvas) + holder.unlockCanvasAndPost(canvas) + } + } + + override fun surfaceDestroyed(holder: SurfaceHolder) {} + + private fun drawGradient(canvas: Canvas) { + canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), paint) + } + + override fun onSizeChanged( + w: Int, + h: Int, + oldw: Int, + oldh: Int + ) { + paint.shader = + LinearGradient( + 0f, + 0f, + w.toFloat(), + h.toFloat(), + intArrayOf( + Color.rgb(0x41, 0x69, 0xE1), + Color.rgb(0xFF, 0xA5, 0x00) + ), + null, + Shader.TileMode.CLAMP + ) + super.onSizeChanged(w, h, oldw, oldh) + } +} diff --git a/dev/integration_tests/android_engine_test/lib/external_texture_smiley_face_main.dart b/dev/integration_tests/android_engine_test/lib/external_texture_smiley_face_main.dart index 032aed5c36..b1b5bc9e4b 100644 --- a/dev/integration_tests/android_engine_test/lib/external_texture_smiley_face_main.dart +++ b/dev/integration_tests/android_engine_test/lib/external_texture_smiley_face_main.dart @@ -21,7 +21,7 @@ Future _fetchTexture(int width, int height) async { } void main() async { - ensureAndroidOrIosDevice(); + ensureAndroidDevice(); enableFlutterDriverExtension(commands: [nativeDriverCommands]); // Run on full screen. diff --git a/dev/integration_tests/android_engine_test/lib/flutter_rendered_blue_rectangle_main.dart b/dev/integration_tests/android_engine_test/lib/flutter_rendered_blue_rectangle_main.dart index a6b452b5b1..ded54bc11c 100644 --- a/dev/integration_tests/android_engine_test/lib/flutter_rendered_blue_rectangle_main.dart +++ b/dev/integration_tests/android_engine_test/lib/flutter_rendered_blue_rectangle_main.dart @@ -10,7 +10,7 @@ import 'package:flutter_driver/driver_extension.dart'; import 'src/allow_list_devices.dart'; void main() { - ensureAndroidOrIosDevice(); + ensureAndroidDevice(); enableFlutterDriverExtension(commands: [nativeDriverCommands]); // Run on full screen. diff --git a/dev/integration_tests/android_engine_test/lib/platform_view_blue_orange_gradient_main.dart b/dev/integration_tests/android_engine_test/lib/platform_view/texture_layer_hybrid_composition_platform_view_main.dart similarity index 72% rename from dev/integration_tests/android_engine_test/lib/platform_view_blue_orange_gradient_main.dart rename to dev/integration_tests/android_engine_test/lib/platform_view/texture_layer_hybrid_composition_platform_view_main.dart index 788abc8ee0..10c1dec7e7 100644 --- a/dev/integration_tests/android_engine_test/lib/platform_view_blue_orange_gradient_main.dart +++ b/dev/integration_tests/android_engine_test/lib/platform_view/texture_layer_hybrid_composition_platform_view_main.dart @@ -7,10 +7,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_driver/driver_extension.dart'; -import 'src/allow_list_devices.dart'; +import '../src/allow_list_devices.dart'; void main() async { - ensureAndroidOrIosDevice(); + ensureAndroidDevice(); enableFlutterDriverExtension(commands: [nativeDriverCommands]); // Run on full screen. @@ -25,6 +25,11 @@ final class MainApp extends StatelessWidget { Widget build(BuildContext context) { return const MaterialApp( debugShowCheckedModeBanner: false, + // It is assumed: + // - The Android SDK version is >= 23 (the test driver checks) + // - This view does NOT use a SurfaceView + // + // See https://github.com/flutter/flutter/blob/main/docs/platforms/android/Android-Platform-Views.md. home: AndroidView(viewType: 'blue_orange_gradient_platform_view'), ); } diff --git a/dev/integration_tests/android_engine_test/lib/platform_view/virtual_display_platform_view_main.dart b/dev/integration_tests/android_engine_test/lib/platform_view/virtual_display_platform_view_main.dart new file mode 100644 index 0000000000..4c51b60b44 --- /dev/null +++ b/dev/integration_tests/android_engine_test/lib/platform_view/virtual_display_platform_view_main.dart @@ -0,0 +1,36 @@ +// 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/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'; + +void main() async { + ensureAndroidDevice(); + enableFlutterDriverExtension(commands: [nativeDriverCommands]); + + // Run on full screen. + await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); + runApp(const MainApp()); +} + +final class MainApp extends StatelessWidget { + const MainApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + debugShowCheckedModeBanner: false, + // It is assumed: + // - The Android SDK version is >= 23 (the test driver checks) + // - This view DOES use a SurfaceView + // + // See https://github.com/flutter/flutter/blob/main/docs/platforms/android/Android-Platform-Views.md. + home: AndroidView(viewType: 'blue_orange_gradient_surface_view_platform_view'), + ); + } +} diff --git a/dev/integration_tests/android_engine_test/lib/platform_view_tap_color_change_main.dart b/dev/integration_tests/android_engine_test/lib/platform_view_tap_color_change_main.dart index 9bf0fc2d28..b2612cbe99 100644 --- a/dev/integration_tests/android_engine_test/lib/platform_view_tap_color_change_main.dart +++ b/dev/integration_tests/android_engine_test/lib/platform_view_tap_color_change_main.dart @@ -10,7 +10,7 @@ import 'package:flutter_driver/driver_extension.dart'; import 'src/allow_list_devices.dart'; void main() async { - ensureAndroidOrIosDevice(); + ensureAndroidDevice(); enableFlutterDriverExtension(commands: [nativeDriverCommands]); // Run on full screen. diff --git a/dev/integration_tests/android_engine_test/lib/src/allow_list_devices.dart b/dev/integration_tests/android_engine_test/lib/src/allow_list_devices.dart index b1f11353ad..f9a9a6f429 100644 --- a/dev/integration_tests/android_engine_test/lib/src/allow_list_devices.dart +++ b/dev/integration_tests/android_engine_test/lib/src/allow_list_devices.dart @@ -6,11 +6,11 @@ import 'dart:io' as io; import 'package:flutter/foundation.dart'; -/// Throws an [UnsupportedError] if the current platform is not Android or iOS. -void ensureAndroidOrIosDevice() { - if (kIsWeb || (!io.Platform.isAndroid && !io.Platform.isIOS)) { +/// Throws an [UnsupportedError] if the current platform is not Android. +void ensureAndroidDevice() { + if (kIsWeb || !io.Platform.isAndroid) { throw UnsupportedError( - 'This app should only run on Android or iOS devices. It uses native ' + 'This app should only run on Android devices. It uses native Android ' 'plugins that are not developed for other platforms, and would need to ' 'be adapted to run on other platforms. See the README.md for details.', ); diff --git a/dev/integration_tests/android_engine_test/test_driver/platform_view/texture_layer_hybrid_composition_platform_view_main_test.dart b/dev/integration_tests/android_engine_test/test_driver/platform_view/texture_layer_hybrid_composition_platform_view_main_test.dart new file mode 100644 index 0000000000..feb47acd08 --- /dev/null +++ b/dev/integration_tests/android_engine_test/test_driver/platform_view/texture_layer_hybrid_composition_platform_view_main_test.dart @@ -0,0 +1,64 @@ +// 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 goldenPrefix = 'texture_layer_hybrid_composition_platform_view'; + + 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(); + await flutterDriver.waitUntilFirstFrameRasterized(); + + // Double check that we are really probably testing using TLHC. + // See https://github.com/flutter/flutter/blob/main/docs/platforms/android/Android-Platform-Views.md. + if (await nativeDriver.sdkVersion case final int version when version < 23) { + fail('Requires SDK >= 23, got $version'); + } + }); + + tearDownAll(() async { + await nativeDriver.close(); + await flutterDriver.close(); + }); + + test('should screenshot and match a blue -> orange gradient', () async { + await flutterDriver.waitFor(find.byType('AndroidView')); + await expectLater( + nativeDriver.screenshot(), + matchesGoldenFile('$goldenPrefix.blue_orange_gradient_portrait.android.png'), + ); + }, timeout: Timeout.none); + + test('should rotate landscape and screenshot the gradient', () async { + await flutterDriver.waitFor(find.byType('AndroidView')); + await nativeDriver.rotateToLandscape(); + await expectLater( + nativeDriver.screenshot(), + matchesGoldenFile('$goldenPrefix.blue_orange_gradient_landscape_rotated.android.png'), + ); + + await nativeDriver.rotateResetDefault(); + await expectLater( + nativeDriver.screenshot(), + matchesGoldenFile('$goldenPrefix.blue_orange_gradient_portait_rotated_back.android.png'), + ); + }, timeout: Timeout.none); +} diff --git a/dev/integration_tests/android_engine_test/test_driver/platform_view_blue_orange_gradient_main_test.dart b/dev/integration_tests/android_engine_test/test_driver/platform_view/virtual_display_platform_view_main_test.dart similarity index 70% rename from dev/integration_tests/android_engine_test/test_driver/platform_view_blue_orange_gradient_main_test.dart rename to dev/integration_tests/android_engine_test/test_driver/platform_view/virtual_display_platform_view_main_test.dart index 69d7ad2b81..f6b6a26ac1 100644 --- a/dev/integration_tests/android_engine_test/test_driver/platform_view_blue_orange_gradient_main_test.dart +++ b/dev/integration_tests/android_engine_test/test_driver/platform_view/virtual_display_platform_view_main_test.dart @@ -7,12 +7,14 @@ 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'; +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 goldenPrefix = 'virtual_display_platform_view'; + late final FlutterDriver flutterDriver; late final NativeDriver nativeDriver; @@ -24,6 +26,12 @@ void main() async { nativeDriver = await AndroidNativeDriver.connect(flutterDriver); await nativeDriver.configureForScreenshotTesting(); await flutterDriver.waitUntilFirstFrameRasterized(); + + // Double check that we are really probably testing using Virtual Display. + // See https://github.com/flutter/flutter/blob/main/docs/platforms/android/Android-Platform-Views.md. + if (await nativeDriver.sdkVersion case final int version when version < 23) { + fail('Requires SDK >= 23, got $version'); + } }); tearDownAll(() async { @@ -35,7 +43,7 @@ void main() async { await flutterDriver.waitFor(find.byType('AndroidView')); await expectLater( nativeDriver.screenshot(), - matchesGoldenFile('platform_view_blue_orange_gradient_portrait.android.png'), + matchesGoldenFile('$goldenPrefix.blue_orange_gradient_portrait.android.png'), ); }, timeout: Timeout.none); @@ -44,13 +52,13 @@ void main() async { await nativeDriver.rotateToLandscape(); await expectLater( nativeDriver.screenshot(), - matchesGoldenFile('platform_view_blue_orange_gradient_landscape.android.png'), + matchesGoldenFile('$goldenPrefix.blue_orange_gradient_landscape_rotated.android.png'), ); await nativeDriver.rotateResetDefault(); await expectLater( nativeDriver.screenshot(), - matchesGoldenFile('platform_view_blue_orange_gradient_portrait_post_rotation.android.png'), + matchesGoldenFile('$goldenPrefix.blue_orange_gradient_portait_rotated_back.android.png'), ); }, timeout: Timeout.none); } diff --git a/dev/tools/android_driver_extensions/lib/extension.dart b/dev/tools/android_driver_extensions/lib/extension.dart index 32740c65a7..92f9139d2d 100644 --- a/dev/tools/android_driver_extensions/lib/extension.dart +++ b/dev/tools/android_driver_extensions/lib/extension.dart @@ -49,10 +49,14 @@ final class NativeDriverCommandExtension implements CommandExtension { if (result == null) { return const _MethodChannelResult({}); } - if (result is! Map) { - throw ArgumentError.value(result, 'result', 'Expected a Map'); + if (result is! Map) { + throw ArgumentError.value( + result, + 'result', + 'Expected a Map, got ${result.runtimeType}', + ); } - return _MethodChannelResult(result); + return _MethodChannelResult(result.cast()); } // While these could have been implemented in native code, they are already diff --git a/dev/tools/android_driver_extensions/lib/src/backend/android/driver.dart b/dev/tools/android_driver_extensions/lib/src/backend/android/driver.dart index e8f1deb866..7b8fec8bf8 100644 --- a/dev/tools/android_driver_extensions/lib/src/backend/android/driver.dart +++ b/dev/tools/android_driver_extensions/lib/src/backend/android/driver.dart @@ -71,6 +71,12 @@ final class AndroidNativeDriver implements NativeDriver { await _driver.sendCommand(NativeCommand.tap(finder)); } + @override + Future get sdkVersion async { + final Map result = await _driver.sendCommand(NativeCommand.getSdkVersion); + return result['version']! as int; + } + /// Waits for 2 seconds before completing. /// /// There is no perfect way, outside of polling, to know when the device is diff --git a/dev/tools/android_driver_extensions/lib/src/common.dart b/dev/tools/android_driver_extensions/lib/src/common.dart index 34c90b1ed9..8d8e0f2572 100644 --- a/dev/tools/android_driver_extensions/lib/src/common.dart +++ b/dev/tools/android_driver_extensions/lib/src/common.dart @@ -27,6 +27,9 @@ final class NativeCommand extends Command { /// Pings the device to ensure it is responsive. static const NativeCommand ping = NativeCommand('ping'); + /// Gets the SDK version code. + static const NativeCommand getSdkVersion = NativeCommand('sdk_version'); + /// The method to call on the plugin. final String method; diff --git a/dev/tools/android_driver_extensions/lib/src/driver.dart b/dev/tools/android_driver_extensions/lib/src/driver.dart index 1bf3ff192c..f6af26e2b8 100644 --- a/dev/tools/android_driver_extensions/lib/src/driver.dart +++ b/dev/tools/android_driver_extensions/lib/src/driver.dart @@ -45,6 +45,9 @@ abstract interface class NativeDriver { /// ``` Future ping(); + /// Returns the SDK version. + Future get sdkVersion; + /// Take a screenshot using a platform-specific mechanism. /// /// The image is returned as an opaque handle that can be used to retrieve