// 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 kPointerActions = [ 0, // DOWN 1, // UP 5, // POINTER_DOWN 6, // POINTER_UP ]; const double kDoubleErrorMargin = 1e-4; String diffMotionEvents(Map originalEvent, Map synthesizedEvent) { final StringBuffer diff = StringBuffer(); diffMaps( originalEvent, synthesizedEvent, diff, excludeKeys: const [ '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. 'motionEventId', // TODO(kaushikiska): add support for motion event diffing, https://github.com/flutter/flutter/issues/61022. ], ); diffActions(diff, originalEvent, synthesizedEvent); diffPointerProperties(diff, originalEvent, synthesizedEvent); diffPointerCoordsList(diff, originalEvent, synthesizedEvent); return diff.toString(); } void diffActions( StringBuffer diffBuffer, Map originalEvent, Map 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 originalEvent, Map synthesizedEvent, ) { final List> expectedList = (originalEvent['pointerProperties'] as List).cast>(); final List> actualList = (synthesizedEvent['pointerProperties'] as List).cast>(); 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 expected = expectedList[i].cast(); final Map actual = actualList[i].cast(); diffMaps(expected, actual, diffBuffer, messagePrefix: '[pointerProperty $i] '); } } void diffPointerCoordsList( StringBuffer diffBuffer, Map originalEvent, Map synthesizedEvent, ) { final List> expectedList = (originalEvent['pointerCoords'] as List).cast>(); final List> actualList = (synthesizedEvent['pointerCoords'] as List).cast>(); 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 expected = expectedList[i].cast(); final Map actual = actualList[i].cast(); diffPointerCoords(expected, actual, i, diffBuffer); } } void diffPointerCoords( Map expected, Map actual, int pointerIdx, StringBuffer diffBuffer, ) { diffMaps(expected, actual, diffBuffer, messagePrefix: '[pointerCoord $pointerIdx] '); } void diffMaps( Map expected, Map actual, StringBuffer diffBuffer, { List excludeKeys = const [], String messagePrefix = '', }) { const IterableEquality eq = IterableEquality(); 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 actionNames = [ '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;