blob: 74b1ea592d0dfb58223bec60ea4c4a24ed6f7a61 [file] [log] [blame]
// Copyright 2018 The Chromium 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 'dart:io';
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 'motion_event_diff.dart';
MethodChannel channel = const MethodChannel('android_views_integration');
const String kEventsFileName = 'touchEvents';
/// 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<DataHandler> handlerCompleter = Completer<DataHandler>();
Future<String> handleMessage(String message) async {
final DataHandler handler = await handlerCompleter.future;
return handler(message);
}
}
FutureDataHandler driverDataHandler = FutureDataHandler();
void main() {
enableFlutterDriverExtension(handler: driverDataHandler.handleMessage);
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Android Views Integration Test',
home: Scaffold(
body: PlatformViewPage(),
),
);
}
}
class PlatformViewPage extends StatefulWidget {
@override
State createState() => PlatformViewState();
}
class PlatformViewState extends State<PlatformViewPage> {
static const int kEventsBufferSize = 1000;
MethodChannel viewChannel;
/// The list of motion events that were passed to the FlutterView.
List<Map<String, dynamic>> flutterViewEvents = <Map<String, dynamic>>[];
/// The list of motion events that were passed to the embedded view.
List<Map<String, dynamic>> embeddedViewEvents = <Map<String, dynamic>>[];
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
SizedBox(
height: 300.0,
child: AndroidView(
viewType: 'simple_view',
onPlatformViewCreated: onPlatformViewCreated),
),
Expanded(
child: ListView.builder(
itemBuilder: buildEventTile,
itemCount: flutterViewEvents.length,
),
),
Row(
children: <Widget>[
RaisedButton(
child: const Text('RECORD'),
onPressed: listenToFlutterViewEvents,
),
RaisedButton(
child: const Text('CLEAR'),
onPressed: () {
setState(() {
flutterViewEvents.clear();
embeddedViewEvents.clear();
});
},
),
RaisedButton(
child: const Text('SAVE'),
onPressed: () {
const StandardMessageCodec codec = StandardMessageCodec();
saveRecordedEvents(
codec.encodeMessage(flutterViewEvents), context);
},
),
RaisedButton(
key: const ValueKey<String>('play'),
child: const Text('PLAY FILE'),
onPressed: () { playEventsFile(); },
),
],
),
],
);
}
Future<String> playEventsFile() async {
const StandardMessageCodec codec = StandardMessageCodec();
try {
final ByteData data = await rootBundle.load('packages/assets_for_android_views/assets/touchEvents');
final List<dynamic> unTypedRecordedEvents = codec.decodeMessage(data);
final List<Map<String, dynamic>> recordedEvents = unTypedRecordedEvents
.cast<Map<dynamic, dynamic>>()
.map<Map<String, dynamic>>((Map<dynamic, dynamic> e) =>e.cast<String, dynamic>())
.toList();
await channel.invokeMethod<void>('pipeFlutterViewEvents');
await viewChannel.invokeMethod<void>('pipeTouchEvents');
print('replaying ${recordedEvents.length} motion events');
for (Map<String, dynamic> event in recordedEvents.reversed) {
await channel.invokeMethod<void>('synthesizeEvent', event);
}
await channel.invokeMethod<void>('stopFlutterViewEvents');
await viewChannel.invokeMethod<void>('stopTouchEvents');
if (flutterViewEvents.length != embeddedViewEvents.length)
return 'Synthesized ${flutterViewEvents.length} events but the embedded view received ${embeddedViewEvents.length} events';
final StringBuffer diff = StringBuffer();
for (int i = 0; i < flutterViewEvents.length; ++i) {
final String currentDiff = diffMotionEvents(flutterViewEvents[i], embeddedViewEvents[i]);
if (currentDiff.isEmpty)
continue;
if (diff.isNotEmpty)
diff.write(', ');
diff.write(currentDiff);
}
return diff.toString();
} catch(e) {
return e.toString();
}
}
@override
void initState() {
super.initState();
channel.setMethodCallHandler(onMethodChannelCall);
}
Future<void> saveRecordedEvents(ByteData data, BuildContext context) async {
if (!await channel.invokeMethod<bool>('getStoragePermission')) {
showMessage(
context, 'External storage permissions are required to save events');
return;
}
try {
final Directory outDir = await getExternalStorageDirectory();
// This test only runs on Android so we can assume path separator is '/'.
final File file = File('${outDir.path}/$kEventsFileName');
await file.writeAsBytes(data.buffer.asUint8List(0, data.lengthInBytes), flush: true);
showMessage(context, 'Saved original events to ${file.path}');
} catch (e) {
showMessage(context, 'Failed saving ${e.toString()}');
}
}
void showMessage(BuildContext context, String message) {
Scaffold.of(context).showSnackBar(SnackBar(
content: Text(message),
duration: const Duration(seconds: 3),
));
}
void onPlatformViewCreated(int id) {
viewChannel = MethodChannel('simple_view/$id');
viewChannel.setMethodCallHandler(onViewMethodChannelCall);
driverDataHandler.handlerCompleter.complete(handleDriverMessage);
}
void listenToFlutterViewEvents() {
channel.invokeMethod<void>('pipeFlutterViewEvents');
viewChannel.invokeMethod<void>('pipeTouchEvents');
Timer(const Duration(seconds: 3), () {
channel.invokeMethod<void>('stopFlutterViewEvents');
viewChannel.invokeMethod<void>('stopTouchEvents');
});
}
Future<String> handleDriverMessage(String message) async {
switch (message) {
case 'run test':
return playEventsFile();
}
return 'unknown message: "$message"';
}
Future<dynamic> onMethodChannelCall(MethodCall call) {
switch (call.method) {
case 'onTouch':
final Map<dynamic, dynamic> map = call.arguments;
flutterViewEvents.insert(0, map.cast<String, dynamic>());
if (flutterViewEvents.length > kEventsBufferSize)
flutterViewEvents.removeLast();
setState(() {});
break;
}
return Future<dynamic>.sync(null);
}
Future<dynamic> onViewMethodChannelCall(MethodCall call) {
switch (call.method) {
case 'onTouch':
final Map<dynamic, dynamic> map = call.arguments;
embeddedViewEvents.insert(0, map.cast<String, dynamic>());
if (embeddedViewEvents.length > kEventsBufferSize)
embeddedViewEvents.removeLast();
setState(() {});
break;
}
return Future<dynamic>.sync(null);
}
Widget buildEventTile(BuildContext context, int index) {
if (embeddedViewEvents.length > index)
return TouchEventDiff(
flutterViewEvents[index], embeddedViewEvents[index]);
return Text(
'Unmatched event, action: ${flutterViewEvents[index]['action']}');
}
}
class TouchEventDiff extends StatelessWidget {
const TouchEventDiff(this.originalEvent, this.synthesizedEvent);
final Map<String, dynamic> originalEvent;
final Map<String, dynamic> synthesizedEvent;
@override
Widget build(BuildContext context) {
Color color;
final String diff = diffMotionEvents(originalEvent, synthesizedEvent);
String msg;
final int action = synthesizedEvent['action'];
final String actionName = getActionName(getActionMasked(action), action);
if (diff.isEmpty) {
color = Colors.green;
msg = 'Matched event (action $actionName)';
} else {
color = Colors.red;
msg = '[$actionName] $diff';
}
return GestureDetector(
onLongPress: () {
print('expected:');
prettyPrintEvent(originalEvent);
print('\nactual:');
prettyPrintEvent(synthesizedEvent);
},
child: Container(
color: color,
margin: const EdgeInsets.only(bottom: 2.0),
child: Text(msg),
),
);
}
void prettyPrintEvent(Map<String, dynamic> event) {
final StringBuffer buffer = StringBuffer();
final int action = event['action'];
final int maskedAction = getActionMasked(action);
final String actionName = getActionName(maskedAction, action);
buffer.write('$actionName ');
if (maskedAction == 5 || maskedAction == 6) {
buffer.write('pointer: ${getPointerIdx(action)} ');
}
final List<Map<dynamic, dynamic>> coords = event['pointerCoords'].cast<Map<dynamic, dynamic>>();
for (int i = 0; i < coords.length; i++) {
buffer.write('p$i x: ${coords[i]['x']} y: ${coords[i]['y']}, pressure: ${coords[i]['pressure']} ');
}
print(buffer.toString());
}
}