Unverified Commit 293a2bf8 authored by Emmanuel Garcia's avatar Emmanuel Garcia Committed by GitHub

Android views using hybrid composition e2e driver test (#61507)

parent b7b60a2d
// 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_devicelab/framework/adb.dart';
import 'package:flutter_devicelab/framework/framework.dart';
import 'package:flutter_devicelab/tasks/integration_tests.dart';
Future<void> main() async {
deviceOperatingSystem = DeviceOperatingSystem.android;
await task(createHybridAndroidViewsIntegrationTest());
}
...@@ -60,6 +60,13 @@ TaskFunction createEmbeddedAndroidViewsIntegrationTest() { ...@@ -60,6 +60,13 @@ TaskFunction createEmbeddedAndroidViewsIntegrationTest() {
); );
} }
TaskFunction createHybridAndroidViewsIntegrationTest() {
return DriverTest(
'${flutterDirectory.path}/dev/integration_tests/hybrid_android_views',
'lib/main.dart',
);
}
TaskFunction createAndroidSemanticsIntegrationTest() { TaskFunction createAndroidSemanticsIntegrationTest() {
return DriverTest( return DriverTest(
'${flutterDirectory.path}/dev/integration_tests/android_semantics_testing', '${flutterDirectory.path}/dev/integration_tests/android_semantics_testing',
......
...@@ -249,6 +249,12 @@ tasks: ...@@ -249,6 +249,12 @@ tasks:
stage: devicelab stage: devicelab
required_agent_capabilities: ["mac/android"] required_agent_capabilities: ["mac/android"]
hybrid_android_views_integration_test:
description: >
Tests hybrid Android views.
stage: devicelab
required_agent_capabilities: ["mac/android"]
android_semantics_integration_test: android_semantics_integration_test:
description: > description: >
Tests that the Android accessibility bridge produces correct semantics. Tests that the Android accessibility bridge produces correct semantics.
......
# Integration test for hybrid composition on Android
This test verifies that the synthesized motion events that get to embedded
Android view are equal to the motion events that originally hit the FlutterView.
The test app's Android code listens to MotionEvents that get to FlutterView and
to an embedded Android view and sends them over a platform channel to the Dart
code where the events are matched.
This is what the app looks like:
![android_views test app](https://flutter.github.io/assets-for-api-docs/assets/readme-assets/android_views_test.png)
The blue part is the embedded Android view, because it is positioned at the top
left corner, the coordinate systems for FlutterView and for the embedded view's
virtual display has the same origin (this makes the MotionEvent comparison
easier as we don't need to translate the coordinates).
The app includes the following control buttons:
* RECORD - Start listening for MotionEvents for 3 seconds, matched/unmatched events are
displayed in the listview as they arrive.
* CLEAR - Clears the events that were recorded so far.
* SAVE - Saves the events that hit FlutterView to a file.
* PLAY FILE - Send a list of events from a bundled asset file to FlutterView.
A recorded touch events sequence is bundled as an asset in the
assets_for_android_view package which lives in the goldens repository.
When running this test with `flutter drive` the record touch sequences is
replayed and the test asserts that the events that got to FlutterView are
equivalent to the ones that got to the embedded view.
// 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.
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion 28
lintOptions {
disable 'InvalidPackage'
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "io.flutter.integration.platformviews"
minSdkVersion 16
targetSdkVersion 28
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
}
}
}
flutter {
source '../..'
}
<!-- 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. -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="io.flutter.integration.platformviews">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:name="io.flutter.app.FlutterApplication"
android:label="platform_views">
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<meta-data
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
android:value="true" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<!-- Hybrid composition -->
<meta-data
android:name="io.flutter.embedded_views_preview"
android:value="true" />
</application>
</manifest>
// 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 io.flutter.integration.platformviews;
import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import java.util.HashMap;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugins.GeneratedPluginRegistrant;
public class MainActivity extends FlutterActivity implements MethodChannel.MethodCallHandler {
final static int STORAGE_PERMISSION_CODE = 1;
MethodChannel mMethodChannel;
// The method result to complete with the Android permission request result.
// This is null when not waiting for the Android permission request;
private MethodChannel.Result permissionResult;
private View getFlutterView() {
// TODO(egarciad): Set an unique ID in FlutterView, so it's easier to look it up.
ViewGroup root = (ViewGroup)findViewById(android.R.id.content);
return ((ViewGroup)root.getChildAt(0)).getChildAt(0);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public void configureFlutterEngine(FlutterEngine flutterEngine) {
DartExecutor executor = flutterEngine.getDartExecutor();
flutterEngine
.getPlatformViewsController()
.getRegistry()
.registerViewFactory("simple_view", new SimpleViewFactory(executor));
mMethodChannel = new MethodChannel(executor, "android_views_integration");
mMethodChannel.setMethodCallHandler(this);
GeneratedPluginRegistrant.registerWith(flutterEngine);
}
@Override
public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
switch(methodCall.method) {
case "getStoragePermission":
if (permissionResult != null) {
result.error("error", "already waiting for permissions", null);
return;
}
permissionResult = result;
getExternalStoragePermissions();
return;
case "synthesizeEvent":
synthesizeEvent(methodCall, result);
return;
}
result.notImplemented();
}
@SuppressWarnings("unchecked")
public void synthesizeEvent(MethodCall methodCall, MethodChannel.Result result) {
MotionEvent event = MotionEventCodec.decode((HashMap<String, Object>) methodCall.arguments());
getFlutterView().dispatchTouchEvent(event);
// TODO(egarciad): This can be cleaned up.
mMethodChannel.invokeMethod("onTouch", MotionEventCodec.encode(event));
result.success(null);
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
if (requestCode != STORAGE_PERMISSION_CODE || permissionResult == null)
return;
boolean permisisonGranted = grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
sendPermissionResult(permisisonGranted);
}
private void getExternalStoragePermissions() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
return;
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED) {
sendPermissionResult(true);
return;
}
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, STORAGE_PERMISSION_CODE);
}
private void sendPermissionResult(boolean result) {
if (permissionResult == null)
return;
permissionResult.success(result);
permissionResult = null;
}
}
// 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 io.flutter.integration.platformviews;
import android.annotation.TargetApi;
import android.os.Build;
import android.view.MotionEvent;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import static android.view.MotionEvent.PointerCoords;
import static android.view.MotionEvent.PointerProperties;
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public class MotionEventCodec {
public static HashMap<String, Object> encode(MotionEvent event) {
ArrayList<HashMap<String,Object>> pointerProperties = new ArrayList<>();
ArrayList<HashMap<String,Object>> pointerCoords = new ArrayList<>();
for (int i = 0; i < event.getPointerCount(); i++) {
MotionEvent.PointerProperties properties = new MotionEvent.PointerProperties();
event.getPointerProperties(i, properties);
pointerProperties.add(encodePointerProperties(properties));
MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
event.getPointerCoords(i, coords);
pointerCoords.add(encodePointerCoords(coords));
}
HashMap<String, Object> eventMap = new HashMap<>();
eventMap.put("downTime", event.getDownTime());
eventMap.put("eventTime", event.getEventTime());
eventMap.put("action", event.getAction());
eventMap.put("pointerCount", event.getPointerCount());
eventMap.put("pointerProperties", pointerProperties);
eventMap.put("pointerCoords", pointerCoords);
eventMap.put("metaState", event.getMetaState());
eventMap.put("buttonState", event.getButtonState());
eventMap.put("xPrecision", event.getXPrecision());
eventMap.put("yPrecision", event.getYPrecision());
eventMap.put("deviceId", event.getDeviceId());
eventMap.put("edgeFlags", event.getEdgeFlags());
eventMap.put("source", event.getSource());
eventMap.put("flags", event.getFlags());
return eventMap;
}
private static HashMap<String, Object> encodePointerProperties(PointerProperties properties) {
HashMap<String, Object> map = new HashMap<>();
map.put("id", properties.id);
map.put("toolType", properties.toolType);
return map;
}
private static HashMap<String, Object> encodePointerCoords(PointerCoords coords) {
HashMap<String, Object> map = new HashMap<>();
map.put("orientation", coords.orientation);
map.put("pressure", coords.pressure);
map.put("size", coords.size);
map.put("toolMajor", coords.toolMajor);
map.put("toolMinor", coords.toolMinor);
map.put("touchMajor", coords.touchMajor);
map.put("touchMinor", coords.touchMinor);
map.put("x", coords.x);
map.put("y", coords.y);
return map;
}
@SuppressWarnings("unchecked")
public static MotionEvent decode(HashMap<String, Object> data) {
List<PointerProperties> pointerProperties = new ArrayList<>();
List<PointerCoords> pointerCoords = new ArrayList<>();
for (HashMap<String, Object> property : (List<HashMap<String, Object>>) data.get("pointerProperties")) {
pointerProperties.add(decodePointerProperties(property)) ;
}
for (HashMap<String, Object> coord : (List<HashMap<String, Object>>) data.get("pointerCoords")) {
pointerCoords.add(decodePointerCoords(coord)) ;
}
return MotionEvent.obtain(
(int) data.get("downTime"),
(int) data.get("eventTime"),
(int) data.get("action"),
(int) data.get("pointerCount"),
pointerProperties.toArray(new PointerProperties[pointerProperties.size()]),
pointerCoords.toArray(new PointerCoords[pointerCoords.size()]),
(int) data.get("metaState"),
(int) data.get("buttonState"),
(float) (double) data.get("xPrecision"),
(float) (double) data.get("yPrecision"),
(int) data.get("deviceId"),
(int) data.get("edgeFlags"),
(int) data.get("source"),
(int) data.get("flags")
);
}
private static PointerProperties decodePointerProperties(HashMap<String, Object> data) {
PointerProperties properties = new PointerProperties();
properties.id = (int) data.get("id");
properties.toolType = (int) data.get("toolType");
return properties;
}
private static PointerCoords decodePointerCoords(HashMap<String, Object> data) {
PointerCoords coords = new PointerCoords();
coords.orientation = (float) (double) data.get("orientation");
coords.pressure = (float) (double) data.get("pressure");
coords.size = (float) (double) data.get("size");
coords.toolMajor = (float) (double) data.get("toolMajor");
coords.toolMinor = (float) (double) data.get("toolMinor");
coords.touchMajor = (float) (double) data.get("touchMajor");
coords.touchMinor = (float) (double) data.get("touchMinor");
coords.x = (float) (double) data.get("x");
coords.y = (float) (double) data.get("y");
return coords;
}
}
// 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 io.flutter.integration.platformviews;
import android.app.AlertDialog;
import android.content.Context;
import android.graphics.PixelFormat;
import android.util.Log;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.TextView;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.platform.PlatformView;
public class SimplePlatformView implements PlatformView, MethodChannel.MethodCallHandler {
private final FrameLayout view;
private final MethodChannel methodChannel;
private final io.flutter.integration.platformviews.TouchPipe touchPipe;
SimplePlatformView(Context context, MethodChannel methodChannel) {
this.methodChannel = methodChannel;
this.methodChannel.setMethodCallHandler(this);
view = new FrameLayout(context) {
@Override
public boolean onTouchEvent(MotionEvent event) {
return true;
}
};
view.setBackgroundColor(0xff0000ff);
touchPipe = new TouchPipe(this.methodChannel, view);
}
@Override
public View getView() {
return view;
}
@Override
public void dispose() {}
@Override
public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
switch(methodCall.method) {
case "pipeTouchEvents":
touchPipe.enable();
result.success(null);
return;
case "stopTouchEvents":
touchPipe.disable();
result.success(null);
return;
case "showAndHideAlertDialog":
showAndHideAlertDialog(result);
return;
case "addChildViewAndWaitForClick":
addWindow(result);
return;
}
result.notImplemented();
}
private void showAndHideAlertDialog(MethodChannel.Result result) {
Context context = view.getContext();
AlertDialog.Builder builder = new AlertDialog.Builder(context);
TextView textView = new TextView(context);
textView.setText("This alert dialog will close in 1 second");
builder.setView(textView);
final AlertDialog alertDialog = builder.show();
result.success(null);
view.postDelayed(new Runnable() {
@Override
public void run() {
alertDialog.hide();
}
}, 1000);
}
private void addWindow(final MethodChannel.Result result) {
Context context = view.getContext();
final Button button = new Button(context);
button.setText("This view was added to the Android view");
view.addView(button);
button.setOnClickListener(v -> {
view.removeView(button);
result.success(null);
});
}
}
// 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 io.flutter.integration.platformviews;
import android.content.Context;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.platform.PlatformView;
import io.flutter.plugin.platform.PlatformViewFactory;
public class SimpleViewFactory extends PlatformViewFactory {
final DartExecutor executor;
public SimpleViewFactory(DartExecutor executor) {
super(null);
this.executor = executor;
}
@Override
public PlatformView create(Context context, int id, Object params) {
MethodChannel methodChannel = new MethodChannel(executor, "simple_view/" + id);
return new SimplePlatformView(context, methodChannel);
}
}
// 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 io.flutter.integration.platformviews;
import android.annotation.TargetApi;
import android.os.Build;
import android.view.MotionEvent;
import android.view.View;
import io.flutter.plugin.common.MethodChannel;
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
class TouchPipe implements View.OnTouchListener {
private final MethodChannel mMethodChannel;
private final View mView;
private boolean mEnabled;
TouchPipe(MethodChannel methodChannel, View view) {
mMethodChannel = methodChannel;
mView = view;
}
public void enable() {
if (mEnabled)
return;
mEnabled = true;
mView.setOnTouchListener(this);
}
public void disable() {
if(!mEnabled)
return;
mEnabled = false;
mView.setOnTouchListener(null);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
mMethodChannel.invokeMethod("onTouch", MotionEventCodec.encode(event));
return false;
}
}
// 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.
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
}
}
allprojects {
repositories {
google()
jcenter()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
delete rootProject.buildDir
}
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
android.enableR8=true
#Fri Jun 23 08:50:38 CEST 2017
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
// 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.
include ':app'
def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
def properties = new Properties()
assert localPropertiesFile.exists()
localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
// 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:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
class AndroidPlatformView extends StatelessWidget {
/// Creates a platform view for Android, which is rendered as a
/// native view.
/// `viewType` identifies the type of Android view to create.
const AndroidPlatformView({
Key key,
this.onPlatformViewCreated,
@required this.viewType,
}) : assert(viewType != null),
super(key: key);
/// The unique identifier for the view type to be embedded by this widget.
///
/// A PlatformViewFactory for this type must have been registered.
final String viewType;
/// {@template flutter.widgets.platformViews.createdParam}
/// Callback to invoke after the platform view has been created.
///
/// May be null.
/// {@endtemplate}
final PlatformViewCreatedCallback onPlatformViewCreated;
@override
Widget build(BuildContext context) {
return PlatformViewLink(
viewType: viewType,
surfaceFactory:
(BuildContext context, PlatformViewController controller) {
return AndroidViewSurface(
controller: controller as AndroidViewController,
gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{},
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
);
},
onCreatePlatformView: (PlatformViewCreationParams params) {
final AndroidViewController controller =
PlatformViewsService.initSurfaceAndroidView(
id: params.id,
viewType: params.viewType,
layoutDirection: TextDirection.ltr,
)
..addOnPlatformViewCreatedListener(params.onPlatformViewCreated);
if (onPlatformViewCreated != null) {
controller.addOnPlatformViewCreatedListener(onPlatformViewCreated);
}
return controller..create();
},
);
}
}
// 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:flutter/material.dart';
import 'package:flutter_driver/driver_extension.dart';
import 'motion_events_page.dart';
import 'nested_view_event_page.dart';
import 'page.dart';
final List<PageWidget> _allPages = <PageWidget>[
const MotionEventsPage(),
const NestedViewEventPage(),
];
void main() {
enableFlutterDriverExtension(handler: driverDataHandler.handleMessage);
runApp(MaterialApp(home: Home()));
}
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(
children: _allPages.map((PageWidget p) => _buildPageListTile(context, p)).toList(),
),
);
}
Widget _buildPageListTile(BuildContext context, PageWidget page) {
return ListTile(
title: Text(page.title),
key: page.tileKey,
onTap: () { _pushPage(context, page); },
);
}
void _pushPage(BuildContext context, PageWidget page) {
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (_) => Scaffold(
body: page,
)));
}
}
// 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:collection/collection.dart';
// Android MotionEvent actions for which a pointer index is encoded in the
// unmasked action code.
const List<int> kPointerActions = <int>[
0, // DOWN
1, // UP
5, // POINTER_DOWN
6, // POINTER_UP
];
const double kDoubleErrorMargin = 1e-4;
String diffMotionEvents(
Map<String, dynamic> originalEvent,
Map<String, dynamic> synthesizedEvent,
) {
final StringBuffer diff = StringBuffer();
diffMaps(originalEvent, synthesizedEvent, diff, excludeKeys: const <String>[
'pointerProperties', // Compared separately.
'pointerCoords', // Compared separately.
'source', // Unused by Flutter.
'deviceId', // Android documentation says that's an arbitrary number that shouldn't be depended on.
'action', // Compared separately.
]);
diffActions(diff, originalEvent, synthesizedEvent);
diffPointerProperties(diff, originalEvent, synthesizedEvent);
diffPointerCoordsList(diff, originalEvent, synthesizedEvent);
return diff.toString();
}
void diffActions(StringBuffer diffBuffer, Map<String, dynamic> originalEvent,
Map<String, dynamic> synthesizedEvent) {
final int synthesizedActionMasked =
getActionMasked(synthesizedEvent['action'] as int);
final int originalActionMasked = getActionMasked(originalEvent['action'] as int);
final String synthesizedActionName =
getActionName(synthesizedActionMasked, synthesizedEvent['action'] as int);
final String originalActionName =
getActionName(originalActionMasked, originalEvent['action'] as int);
if (synthesizedActionMasked != originalActionMasked)
diffBuffer.write(
'action (expected: $originalActionName actual: $synthesizedActionName) ');
if (kPointerActions.contains(originalActionMasked) &&
originalActionMasked == synthesizedActionMasked) {
final int originalPointer = getPointerIdx(originalEvent['action'] as int);
final int synthesizedPointer = getPointerIdx(synthesizedEvent['action'] as int);
if (originalPointer != synthesizedPointer)
diffBuffer.write(
'pointerIdx (expected: $originalPointer actual: $synthesizedPointer action: $originalActionName ');
}
}
void diffPointerProperties(StringBuffer diffBuffer,
Map<String, dynamic> originalEvent, Map<String, dynamic> synthesizedEvent) {
final List<Map<dynamic, dynamic>> expectedList =
(originalEvent['pointerProperties'] as List<dynamic>).cast<Map<dynamic, dynamic>>();
final List<Map<dynamic, dynamic>> actualList =
(synthesizedEvent['pointerProperties'] as List<dynamic>).cast<Map<dynamic, dynamic>>();
if (expectedList.length != actualList.length) {
diffBuffer.write(
'pointerProperties (actual length: ${actualList.length}, expected length: ${expectedList.length} ');
return;
}
for (int i = 0; i < expectedList.length; i++) {
final Map<String, dynamic> expected =
expectedList[i].cast<String, dynamic>();
final Map<String, dynamic> actual = actualList[i].cast<String, dynamic>();
diffMaps(expected, actual, diffBuffer,
messagePrefix: '[pointerProperty $i] ');
}
}
void diffPointerCoordsList(StringBuffer diffBuffer,
Map<String, dynamic> originalEvent, Map<String, dynamic> synthesizedEvent) {
final List<Map<dynamic, dynamic>> expectedList =
(originalEvent['pointerCoords'] as List<dynamic>).cast<Map<dynamic, dynamic>>();
final List<Map<dynamic, dynamic>> actualList =
(synthesizedEvent['pointerCoords'] as List<dynamic>).cast<Map<dynamic, dynamic>>();
if (expectedList.length != actualList.length) {
diffBuffer.write(
'pointerCoords (actual length: ${actualList.length}, expected length: ${expectedList.length} ');
return;
}
for (int i = 0; i < expectedList.length; i++) {
final Map<String, dynamic> expected =
expectedList[i].cast<String, dynamic>();
final Map<String, dynamic> actual = actualList[i].cast<String, dynamic>();
diffPointerCoords(expected, actual, i, diffBuffer);
}
}
void diffPointerCoords(Map<String, dynamic> expected,
Map<String, dynamic> actual, int pointerIdx, StringBuffer diffBuffer) {
diffMaps(expected, actual, diffBuffer, messagePrefix: '[pointerCoord $pointerIdx] ');
}
void diffMaps(
Map<String, dynamic> expected,
Map<String, dynamic> actual,
StringBuffer diffBuffer, {
List<String> excludeKeys = const <String>[],
String messagePrefix = '',
}) {
const IterableEquality<String> eq = IterableEquality<String>();
if (!eq.equals(expected.keys, actual.keys)) {
diffBuffer.write(
'${messagePrefix}keys (expected: ${expected.keys} actual: ${actual.keys} ');
return;
}
for (final String key in expected.keys) {
if (excludeKeys.contains(key))
continue;
if (doublesApproximatelyMatch(expected[key], actual[key]))
continue;
if (expected[key] != actual[key]) {
diffBuffer.write(
'$messagePrefix$key (expected: ${expected[key]} actual: ${actual[key]}) ');
}
}
}
int getActionMasked(int action) => action & 0xff;
int getPointerIdx(int action) => (action >> 8) & 0xff;
String getActionName(int actionMasked, int action) {
const List<String> actionNames = <String>[
'DOWN',
'UP',
'MOVE',
'CANCEL',
'OUTSIDE',
'POINTER_DOWN',
'POINTER_UP',
'HOVER_MOVE',
'SCROLL',
'HOVER_ENTER',
'HOVER_EXIT',
'BUTTON_PRESS',
'BUTTON_RELEASE',
];
if (actionMasked < actionNames.length)
return '${actionNames[actionMasked]}($action)';
else
return 'ACTION_$actionMasked';
}
bool doublesApproximatelyMatch(dynamic a, dynamic b) =>
a is double && b is double && (a - b).abs() < kDoubleErrorMargin;
// 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:io';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'android_platform_view.dart';
import 'page.dart';
class NestedViewEventPage extends PageWidget {
const NestedViewEventPage()
: super('Nested View Event Tests', const ValueKey<String>('NestedViewEventTile'));
@override
Widget build(BuildContext context) => NestedViewEventBody();
}
class NestedViewEventBody extends StatefulWidget {
@override
State<NestedViewEventBody> createState() => NestedViewEventBodyState();
}
enum _LastTestStatus {
pending,
success,
error
}
class NestedViewEventBodyState extends State<NestedViewEventBody> {
MethodChannel viewChannel;
_LastTestStatus lastTestStatus = _LastTestStatus.pending;
String lastError;
int id;
int nestedViewClickCount = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Nested view event'),
),
body: Column(
children: <Widget>[
SizedBox(
height: 300,
child: AndroidPlatformView(
viewType: 'simple_view',
onPlatformViewCreated: onPlatformViewCreated,
),
),
if (lastTestStatus != _LastTestStatus.pending) _statusWidget(),
if (viewChannel != null) ... <Widget>[
RaisedButton(
key: const ValueKey<String>('ShowAlertDialog'),
child: const Text('SHOW ALERT DIALOG'),
onPressed: onShowAlertDialogPressed,
),
Row(
children: <Widget>[
RaisedButton(
key: const ValueKey<String>('AddChildView'),
child: const Text('ADD CHILD VIEW'),
onPressed: onChildViewPressed,
),
RaisedButton(
key: const ValueKey<String>('TapChildView'),
child: const Text('TAP CHILD VIEW'),
onPressed: onTapChildViewPressed,
),
if (nestedViewClickCount > 0)
Text(
'Click count: $nestedViewClickCount',
key: const ValueKey<String>('NestedViewClickCount'),
),
],
),
],
],
),
);
}
Widget _statusWidget() {
assert(lastTestStatus != _LastTestStatus.pending);
final String message = lastTestStatus == _LastTestStatus.success ? 'Success' : lastError;
return Container(
color: lastTestStatus == _LastTestStatus.success ? Colors.green : Colors.red,
child: Text(
message,
key: const ValueKey<String>('Status'),
style: TextStyle(
color: lastTestStatus == _LastTestStatus.error ? Colors.yellow : null,
),
),
);
}
Future<void> onShowAlertDialogPressed() async {
if (lastTestStatus != _LastTestStatus.pending) {
setState(() {
lastTestStatus = _LastTestStatus.pending;
});
}
try {
await viewChannel.invokeMethod<void>('showAndHideAlertDialog');
setState(() {
lastTestStatus = _LastTestStatus.success;
});
} catch(e) {
setState(() {
lastTestStatus = _LastTestStatus.error;
lastError = '$e';
});
}
}
Future<void> onChildViewPressed() async {
try {
await viewChannel.invokeMethod<void>('addChildViewAndWaitForClick');
setState(() {
nestedViewClickCount++;
});
} catch(e) {
setState(() {
lastTestStatus = _LastTestStatus.error;
lastError = '$e';
});
}
}
Future<void> onTapChildViewPressed() async {
await Future<void>.delayed(const Duration(seconds: 1));
// Dispatch a tap event on the child view inside the platform view.
//
// Android mutates `MotionEvent` instances, so in this case *do not* dispatch
// new instances as it won't cover the `MotionEventTracker` class in the embedding
// which tracks events.
//
// See the issue this prevents: https://github.com/flutter/flutter/issues/61169
await Process.run('input', const <String>['tap', '250', '550']);
}
void onPlatformViewCreated(int id) {
this.id = id;
setState(() {
viewChannel = MethodChannel('simple_view/$id');
});
}
}
// 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:flutter/material.dart';
/// The base class of all the testing pages
//
/// A testing page has to override this in order to be put as one of the items in the main page.
abstract class PageWidget extends StatelessWidget {
const PageWidget(this.title, this.tileKey);
/// The title of the testing page
///
/// It will be shown on the main page as the text on the link which opens the page.
final String title;
/// The key of the ListTile that navigates to the page.
///
/// Used by the integration test to navigate to the corresponding page.
final ValueKey<String> tileKey;
}
name: hybrid_platform_views
description: An integration test for hybrid composition on Android
version: 1.0.0+1
dependencies:
flutter:
sdk: flutter
flutter_driver:
sdk: flutter
path_provider: 1.6.11
collection: 1.15.0-nullsafety
assets_for_android_views:
git:
url: https://github.com/flutter/goldens.git
ref: c47f1308188dca65b3899228cac37f252ea8b411
path: dev/integration_tests/assets_for_android_views
archive: 2.0.13 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
args: 1.6.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
async: 2.4.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
characters: 1.1.0-nullsafety # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
charcode: 1.1.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
convert: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
crypto: 2.1.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
file: 5.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
intl: 0.16.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
json_rpc_2: 2.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
matcher: 0.12.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
meta: 1.3.0-nullsafety # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
path: 1.7.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
path_provider_linux: 0.0.1+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
path_provider_macos: 0.0.4+3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
path_provider_platform_interface: 1.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
platform: 2.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
plugin_platform_interface: 1.0.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
process: 3.0.13 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
pub_semver: 1.4.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
source_span: 1.7.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
stack_trace: 1.9.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
stream_channel: 2.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
sync_http: 0.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
term_glyph: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
typed_data: 1.3.0-nullsafety # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
vector_math: 2.1.0-nullsafety # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
vm_service_client: 0.2.6+2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
web_socket_channel: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
webdriver: 2.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
xdg_directories: 0.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
dev_dependencies:
flutter_test:
sdk: flutter
test: 1.15.2
_fe_analyzer_shared: 5.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
analyzer: 0.39.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
boolean_selector: 2.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
clock: 1.0.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
coverage: 0.14.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
csslib: 0.16.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
fake_async: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
glob: 1.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
html: 0.14.0+3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
http: 0.12.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
http_multi_server: 2.2.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
http_parser: 3.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
io: 0.3.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
js: 0.6.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
logging: 0.11.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
mime: 0.9.6+3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
node_interop: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
node_io: 1.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
node_preamble: 1.4.12 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
package_config: 1.9.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
pedantic: 1.9.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
pool: 1.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
shelf: 0.7.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
shelf_packages_handler: 2.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
shelf_static: 0.2.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
shelf_web_socket: 0.2.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
source_map_stack_trace: 2.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
source_maps: 0.10.9 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
string_scanner: 1.0.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
test_api: 0.2.17 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
test_core: 0.3.10 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
vm_service: 4.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
watcher: 0.9.7+15 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
webkit_inspection_protocol: 0.7.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
yaml: 2.2.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"
flutter:
uses-material-design: true
# PUBSPEC CHECKSUM: bd09
// 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/flutter_driver.dart';
import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
Future<void> main() async {
FlutterDriver driver;
setUpAll(() async {
driver = await FlutterDriver.connect();
});
tearDownAll(() {
driver.close();
});
// Each test below must return back to the home page after finishing.
test('MotionEvent recomposition', () async {
final SerializableFinder motionEventsListTile =
find.byValueKey('MotionEventsListTile');
await driver.tap(motionEventsListTile);
await driver.waitFor(find.byValueKey('PlatformView'));
final String errorMessage = await driver.requestData('run test');
expect(errorMessage, '');
final SerializableFinder backButton = find.byValueKey('back');
await driver.tap(backButton);
});
group('Nested View Event', ()
{
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('AlertDialog from platform view context', () async {
final SerializableFinder showAlertDialog = find.byValueKey(
'ShowAlertDialog');
await driver.waitFor(showAlertDialog);
await driver.tap(showAlertDialog);
final String status = await driver.getText(find.byValueKey('Status'));
expect(status, 'Success');
});
test('Child view can handle touches', () async {
final SerializableFinder addChildView = find.byValueKey('AddChildView');
await driver.waitFor(addChildView);
await driver.tap(addChildView);
final SerializableFinder tapChildView = find.byValueKey('TapChildView');
await driver.tap(tapChildView);
final String nestedViewClickCount = await driver.getText(find.byValueKey('NestedViewClickCount'));
expect(nestedViewClickCount, 'Click count: 1');
});
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment