From ddb8e6e3bfef90c8674ac0ab5508e581a3873dae Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Wed, 22 Jul 2020 20:00:07 -0700 Subject: [PATCH] Test dynamic surface switch (#61918) --- .../androidviews/MainActivity.java | 72 ++++++++++++++++++- .../lib/android_platform_view.dart | 2 + .../lib/future_data_handler.dart | 35 +++++++++ .../hybrid_android_views/lib/main.dart | 1 + .../lib/motion_events_page.dart | 30 +------- .../lib/nested_view_event_page.dart | 25 +++++-- .../test_driver/main_test.dart | 64 ++++++++++++++--- 7 files changed, 186 insertions(+), 43 deletions(-) create mode 100644 dev/integration_tests/hybrid_android_views/lib/future_data_handler.dart diff --git a/dev/integration_tests/hybrid_android_views/android/app/src/main/java/io/flutter/integration/androidviews/MainActivity.java b/dev/integration_tests/hybrid_android_views/android/app/src/main/java/io/flutter/integration/androidviews/MainActivity.java index 4d5baebfe3c..12aa6baedb9 100644 --- a/dev/integration_tests/hybrid_android_views/android/app/src/main/java/io/flutter/integration/androidviews/MainActivity.java +++ b/dev/integration_tests/hybrid_android_views/android/app/src/main/java/io/flutter/integration/androidviews/MainActivity.java @@ -12,9 +12,14 @@ import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; +import java.lang.StringBuilder; import java.util.HashMap; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.android.FlutterImageView; +import io.flutter.embedding.android.FlutterSurfaceView; +import io.flutter.embedding.android.FlutterTextureView; +import io.flutter.embedding.android.FlutterView; import io.flutter.embedding.engine.dart.DartExecutor; import io.flutter.embedding.engine.FlutterEngine; import io.flutter.plugin.common.MethodCall; @@ -36,6 +41,63 @@ public class MainActivity extends FlutterActivity implements MethodChannel.Metho return ((ViewGroup)root.getChildAt(0)).getChildAt(0); } + private String getViewName(View view) { + if (view instanceof FlutterImageView) { + return "FlutterImageView"; + } + if (view instanceof FlutterSurfaceView) { + return "FlutterSurfaceView"; + } + if (view instanceof FlutterTextureView) { + return "FlutterTextureView"; + } + if (view instanceof FlutterView) { + return "FlutterView"; + } + if (view instanceof ViewGroup) { + return "ViewGroup"; + } + return "View"; + } + + private void recurseViewHierarchy(View current, String padding, StringBuilder builder) { + if (current.getVisibility() != View.VISIBLE || current.getAlpha() == 0) { + return; + } + String name = getViewName(current); + builder.append(padding); + builder.append("|-"); + builder.append(name); + builder.append("\n"); + + if (current instanceof ViewGroup) { + ViewGroup viewGroup = (ViewGroup) current; + for (int index = 0; index < viewGroup.getChildCount(); index++) { + recurseViewHierarchy(viewGroup.getChildAt(index), padding + " ", builder); + } + } + } + + /** + * Serializes the view hierarchy, so it can be sent to Dart over the method channel. + * + * Notation: + * |- + * |- ... child view ordered by z order. + * + * Example output: + * |- FlutterView + * |- FlutterImageView + * |- ViewGroup + * |- View + */ + private String getSerializedViewHierarchy() { + View root = getFlutterView(); + StringBuilder builder = new StringBuilder(); + recurseViewHierarchy(root, "", builder); + return builder.toString(); + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -65,19 +127,23 @@ public class MainActivity extends FlutterActivity implements MethodChannel.Metho getExternalStoragePermissions(); return; case "synthesizeEvent": - synthesizeEvent(methodCall, result); + synthesizeEvent(methodCall); + result.success(null); + return; + case "getViewHierarchy": + String viewHierarchy = getSerializedViewHierarchy(); + result.success(viewHierarchy); return; } result.notImplemented(); } @SuppressWarnings("unchecked") - public void synthesizeEvent(MethodCall methodCall, MethodChannel.Result result) { + public void synthesizeEvent(MethodCall methodCall) { MotionEvent event = MotionEventCodec.decode((HashMap) methodCall.arguments()); getFlutterView().dispatchTouchEvent(event); // TODO(egarciad): This can be cleaned up. mMethodChannel.invokeMethod("onTouch", MotionEventCodec.encode(event)); - result.success(null); } @Override diff --git a/dev/integration_tests/hybrid_android_views/lib/android_platform_view.dart b/dev/integration_tests/hybrid_android_views/lib/android_platform_view.dart index 8c784ff13c3..9ea6ab5109a 100644 --- a/dev/integration_tests/hybrid_android_views/lib/android_platform_view.dart +++ b/dev/integration_tests/hybrid_android_views/lib/android_platform_view.dart @@ -8,6 +8,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; +MethodChannel channel = const MethodChannel('android_views_integration'); + class AndroidPlatformView extends StatelessWidget { /// Creates a platform view for Android, which is rendered as a /// native view. diff --git a/dev/integration_tests/hybrid_android_views/lib/future_data_handler.dart b/dev/integration_tests/hybrid_android_views/lib/future_data_handler.dart new file mode 100644 index 00000000000..ed9b29b4547 --- /dev/null +++ b/dev/integration_tests/hybrid_android_views/lib/future_data_handler.dart @@ -0,0 +1,35 @@ +// 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:flutter_driver/driver_extension.dart'; + +typedef DriverHandler = Future Function(); + +/// Wraps a flutter driver [DataHandler] with one that waits until a delegate is set. +/// +/// This allows the driver test to call [FlutterDriver.requestData] before the handler was +/// set by the app in which case the requestData call will only complete once the app is ready +/// for it. +class FutureDataHandler { + final Map> _handlers = >{}; + + /// Registers a lazy handler that will be invoked on the next message from the driver. + Completer registerHandler(String key) { + _handlers[key] = Completer(); + return _handlers[key]; + } + + Future handleMessage(String message) async { + if (_handlers[message] == null) { + return 'Unsupported driver message: $message.\n' + 'Supported messages are: ${_handlers.keys}.'; + } + final DriverHandler handler = await _handlers[message].future; + return handler(); + } +} + +FutureDataHandler driverDataHandler = FutureDataHandler(); diff --git a/dev/integration_tests/hybrid_android_views/lib/main.dart b/dev/integration_tests/hybrid_android_views/lib/main.dart index 776d06e59f2..02d665c11e3 100644 --- a/dev/integration_tests/hybrid_android_views/lib/main.dart +++ b/dev/integration_tests/hybrid_android_views/lib/main.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_driver/driver_extension.dart'; +import 'future_data_handler.dart'; import 'motion_events_page.dart'; import 'nested_view_event_page.dart'; import 'page.dart'; diff --git a/dev/integration_tests/hybrid_android_views/lib/motion_events_page.dart b/dev/integration_tests/hybrid_android_views/lib/motion_events_page.dart index 75607ad94bf..1ae3df1f9b7 100644 --- a/dev/integration_tests/hybrid_android_views/lib/motion_events_page.dart +++ b/dev/integration_tests/hybrid_android_views/lib/motion_events_page.dart @@ -8,15 +8,13 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_driver/driver_extension.dart'; import 'package:path_provider/path_provider.dart'; import 'android_platform_view.dart'; +import 'future_data_handler.dart'; import 'motion_event_diff.dart'; import 'page.dart'; -MethodChannel channel = const MethodChannel('android_views_integration'); - const String kEventsFileName = 'touchEvents'; class MotionEventsPage extends PageWidget { @@ -29,22 +27,6 @@ class MotionEventsPage extends PageWidget { } } -/// Wraps a flutter driver [DataHandler] with one that waits until a delegate is set. -/// -/// This allows the driver test to call [FlutterDriver.requestData] before the handler was -/// set by the app in which case the requestData call will only complete once the app is ready -/// for it. -class FutureDataHandler { - final Completer handlerCompleter = Completer(); - - Future handleMessage(String message) async { - final DataHandler handler = await handlerCompleter.future; - return handler(message); - } -} - -FutureDataHandler driverDataHandler = FutureDataHandler(); - class MotionEventsBody extends StatefulWidget { @override State createState() => MotionEventsBodyState(); @@ -196,7 +178,7 @@ class MotionEventsBodyState extends State { void onPlatformViewCreated(int id) { viewChannel = MethodChannel('simple_view/$id'); viewChannel.setMethodCallHandler(onViewMethodChannelCall); - driverDataHandler.handlerCompleter.complete(handleDriverMessage); + driverDataHandler.registerHandler('run test').complete(playEventsFile); } void listenToFlutterViewEvents() { @@ -206,14 +188,6 @@ class MotionEventsBodyState extends State { }); } - Future handleDriverMessage(String message) async { - switch (message) { - case 'run test': - return playEventsFile(); - } - return 'unknown message: "$message"'; - } - Future onMethodChannelCall(MethodCall call) { switch (call.method) { case 'onTouch': diff --git a/dev/integration_tests/hybrid_android_views/lib/nested_view_event_page.dart b/dev/integration_tests/hybrid_android_views/lib/nested_view_event_page.dart index bea45e9be11..a5597b3bbd7 100644 --- a/dev/integration_tests/hybrid_android_views/lib/nested_view_event_page.dart +++ b/dev/integration_tests/hybrid_android_views/lib/nested_view_event_page.dart @@ -10,6 +10,7 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'android_platform_view.dart'; +import 'future_data_handler.dart'; import 'page.dart'; class NestedViewEventPage extends PageWidget { @@ -38,6 +39,7 @@ class NestedViewEventBodyState extends State { String lastError; int id; int nestedViewClickCount = 0; + bool showPlatformView = true; @override Widget build(BuildContext context) { @@ -49,10 +51,12 @@ class NestedViewEventBodyState extends State { children: [ SizedBox( height: 300, - child: AndroidPlatformView( - viewType: 'simple_view', - onPlatformViewCreated: onPlatformViewCreated, - ), + child: showPlatformView ? + AndroidPlatformView( + key: const ValueKey('PlatformView'), + viewType: 'simple_view', + onPlatformViewCreated: onPlatformViewCreated, + ) : null, ), if (lastTestStatus != _LastTestStatus.pending) _statusWidget(), if (viewChannel != null) ... [ @@ -61,6 +65,11 @@ class NestedViewEventBodyState extends State { child: const Text('SHOW ALERT DIALOG'), onPressed: onShowAlertDialogPressed, ), + RaisedButton( + key: const ValueKey('TogglePlatformView'), + child: const Text('TOGGLE PLATFORM VIEW'), + onPressed: onTogglePlatformView, + ), Row( children: [ RaisedButton( @@ -120,6 +129,12 @@ class NestedViewEventBodyState extends State { } } + Future onTogglePlatformView() async { + setState(() { + showPlatformView = !showPlatformView; + }); + } + Future onChildViewPressed() async { try { await viewChannel.invokeMethod('addChildViewAndWaitForClick'); @@ -152,5 +167,7 @@ class NestedViewEventBodyState extends State { setState(() { viewChannel = MethodChannel('simple_view/$id'); }); + driverDataHandler.registerHandler('hierarchy') + .complete(() => channel.invokeMethod('getViewHierarchy')); } } diff --git a/dev/integration_tests/hybrid_android_views/test_driver/main_test.dart b/dev/integration_tests/hybrid_android_views/test_driver/main_test.dart index f5d6daa0d77..75d5f3b85fb 100644 --- a/dev/integration_tests/hybrid_android_views/test_driver/main_test.dart +++ b/dev/integration_tests/hybrid_android_views/test_driver/main_test.dart @@ -18,10 +18,8 @@ Future main() async { }); // Each test below must return back to the home page after finishing. - test('MotionEvent recomposition', () async { - final SerializableFinder motionEventsListTile = - find.byValueKey('MotionEventsListTile'); + final SerializableFinder motionEventsListTile = find.byValueKey('MotionEventsListTile'); await driver.tap(motionEventsListTile); await driver.waitFor(find.byValueKey('PlatformView')); final String errorMessage = await driver.requestData('run test'); @@ -30,8 +28,7 @@ Future main() async { await driver.tap(backButton); }); - group('Nested View Event', () - { + group('Nested View Event', () { setUpAll(() async { final SerializableFinder wmListTile = find.byValueKey('NestedViewEventTile'); @@ -44,8 +41,7 @@ Future main() async { }); test('AlertDialog from platform view context', () async { - final SerializableFinder showAlertDialog = find.byValueKey( - 'ShowAlertDialog'); + final SerializableFinder showAlertDialog = find.byValueKey('ShowAlertDialog'); await driver.waitFor(showAlertDialog); await driver.tap(showAlertDialog); final String status = await driver.getText(find.byValueKey('Status')); @@ -58,8 +54,60 @@ Future main() async { await driver.tap(addChildView); final SerializableFinder tapChildView = find.byValueKey('TapChildView'); await driver.tap(tapChildView); - final String nestedViewClickCount = await driver.getText(find.byValueKey('NestedViewClickCount')); + final String nestedViewClickCount = + await driver.getText(find.byValueKey('NestedViewClickCount')); expect(nestedViewClickCount, 'Click count: 1'); }); }); + + group('Flutter surface switch', () { + setUpAll(() async { + final SerializableFinder wmListTile = find.byValueKey('NestedViewEventTile'); + await driver.tap(wmListTile); + }); + + tearDownAll(() async { + await driver.waitFor(find.pageBack()); + await driver.tap(find.pageBack()); + }); + + test('Uses FlutterImageView when Android view is on the screen', () async { + await driver.waitFor(find.byValueKey('PlatformView')); + + expect( + await driver.requestData('hierarchy'), + '|-FlutterView\n' + ' |-FlutterSurfaceView\n' // Flutter UI (hidden) + ' |-FlutterImageView\n' // Flutter UI (background surface) + ' |-ViewGroup\n' // Platform View + ' |-ViewGroup\n' + ' |-FlutterImageView\n' // Flutter UI (overlay surface) + ); + + // Hide platform view. + final SerializableFinder togglePlatformView = find.byValueKey('TogglePlatformView'); + await driver.tap(togglePlatformView); + await driver.waitForAbsent(find.byValueKey('PlatformView')); + + expect( + await driver.requestData('hierarchy'), + '|-FlutterView\n' + ' |-FlutterSurfaceView\n' // Just the Flutter UI + ); + + // Show platform view again. + await driver.tap(togglePlatformView); + await driver.waitFor(find.byValueKey('PlatformView')); + + expect( + await driver.requestData('hierarchy'), + '|-FlutterView\n' + ' |-FlutterSurfaceView\n' // Flutter UI (hidden) + ' |-FlutterImageView\n' // Flutter UI (background surface) + ' |-ViewGroup\n' // Platform View + ' |-ViewGroup\n' + ' |-FlutterImageView\n' // Flutter UI (overlay surface) + ); + }); + }); }