blob: 664c3b04d01d1c625c0e57c55d1bc77003f21f60 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// ignore_for_file: avoid_as, unnecessary_null_comparison
// ignore_for_file: avoid_catches_without_on_clauses
import 'dart:core';
import 'dart:math';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:fidl_fuchsia_ui_pointerinjector/fidl_async.dart';
import 'package:fidl_fuchsia_ui_views/fidl_async.dart';
import 'package:flutter/gestures.dart';
import 'package:fuchsia_logger/logger.dart';
import 'package:fuchsia_services/services.dart';
import 'package:meta/meta.dart';
import 'package:pedantic/pedantic.dart';
/// Defines a class that uses the pointer injector service to inject pointer
/// events into child views.
///
/// Requires following services in the environment:
/// fuchsia.ui.pointerinjector.Registry
class PointerInjector {
final Registry _registry;
final DeviceProxy _device;
/// Callback when the injector encounters error during registration or when
/// injecting pointer events. Client code should call [dispose] and release
/// this instance. It may create another instance to continue injecting. Using
/// this instance after [onError] will result in undefined behavior.
final ui.VoidCallback onError;
/// Returns [true] if the PointerInjector is successfully registered.
bool registered = false;
/// Store events for injecting into the fuchsia.ui.pointerinjector channel;
/// the flow control policy allows at most one call in flight at a time.
final List<Event> _buffer = [];
/// Semaphore to control injection call on fuchsia.ui.pointerinjector channel;
/// the flow control policy allows at most one call in flight at a time.
bool _injectInFlight = false;
/// Constructor used for injecting mocks during testing.
@visibleForTesting
PointerInjector(
Registry registry,
DeviceProxy device, {
required this.onError,
}) : _registry = registry,
_device = device;
/// Construct PointerInjector from [/svc].
factory PointerInjector.fromSvcPath({required ui.VoidCallback onError}) {
final registry = RegistryProxy();
final device = DeviceProxy();
Incoming.fromSvcPath().connectToService(registry);
return PointerInjector(registry, device, onError: onError);
}
/// Closes connections to services.
void dispose() {
if (_registry is RegistryProxy) {
RegistryProxy proxy = _registry as RegistryProxy;
proxy.ctrl.close();
}
_device.ctrl.close();
}
/// Registers with the pointer injector service.
///
/// Returns immediately after submitting the registration request.
/// This means that the registration request may still be in-flight
/// on the server side when this function returns. Events can safely be
/// injected into the channel while registration is pending ("feed forward").
void register({
required ViewRef hostViewRef,
required ViewRef viewRef,
}) {
final config = Config(
deviceId: 1,
deviceType: DeviceType.touch,
context: Context.withView(hostViewRef),
target: Target.withView(viewRef),
viewport: Viewport(
extents: _viewportExtents(),
viewportToContextTransform: _identityTransform(),
),
dispatchPolicy: DispatchPolicy.exclusiveTarget,
);
final future =
_registry.register(config, _device.ctrl.request()).catchError((e) {
log.warning('Failed to register pointer injector: $e.');
registered = false;
// Registration failures are critical; higher-level code should handle
// them.
onError();
});
registered = true;
unawaited(future);
}
/// Dispatch [PointerEvent] event to embedded child.
Future<void> dispatchEvent({
required PointerEvent pointer,
}) async {
if (!_isValidPointerEvent(pointer)) {
return;
}
// We use the global position in the parent's local coordinate system.
// The injection viewport is set up to coincide with the local coordinate
// system, and all child-specific transforms are handled on the server.
final x = pointer.position.dx;
final y = pointer.position.dy;
final phase = pointer is PointerDownEvent
? EventPhase.add
: pointer is PointerUpEvent
? EventPhase.remove
: pointer is PointerMoveEvent
? EventPhase.change
: EventPhase.cancel;
final sample = PointerSample(
pointerId: ((pointer.device << 32) >> 32),
phase: phase,
positionInViewport: Float32List.fromList([x, y]),
);
final injectorEvent = Event(
timestamp: pointer.timeStamp.inMicroseconds * 1000,
data: Data.withPointerSample(sample),
traceFlowId: pointer.pointer, // TODO(fxbug.dev/84032)
);
// Queue up the event, because an injection call may be in flight.
_buffer.add(injectorEvent);
if (_injectInFlight) {
// Reachable if a previous call is stuck on the await, below. The _buffer
// ensures this call's event gets injected, eventually, in FIFO order.
return;
}
_injectInFlight = true;
while (_buffer.isNotEmpty) {
// FIDL call has a maximum size; fit in the limit.
int bufferEnd = min(_buffer.length, maxInject);
List<Event> batch = _buffer.sublist(0, bufferEnd);
_buffer.removeRange(0, bufferEnd);
await _device.inject(batch).catchError((e) {
log.warning('Failed to dispatch pointer events: $e.');
onError();
});
// After await, this call may wake up to find more events queued up in
// _buffer. Keep draining _buffer until all events are injected.
}
_injectInFlight = false;
}
// Check if [PointerEvent] is one of supported events.
bool _isValidPointerEvent(PointerEvent pointer) {
// TODO(fxbug.dev/84030) - Implement stream consistency checking: inject
// only valid streams, and reject broken streams.
return pointer != null &&
(pointer is PointerDownEvent ||
pointer is PointerUpEvent ||
pointer is PointerMoveEvent);
}
static List<Float32List> _viewportExtents() {
// Use the logical space of the parent view as the injection viewport.
// It means that pointer coordinates, in the parent's view, can be used
// verbatim for injecting into a child view. The fuchsia.ui.pointerinjector
// server handles the pointer coordinate transforms for the child view.
//
// Note that the Flutter instance's logical space can change size, but since
// the logical space *always* has its origin at Offset.zero, a size change
// does not need a new viewport, since the viewport merely anchors pointer
// coordinates received by the Flutter instance.
ui.Size window = ui.window.physicalSize / ui.window.devicePixelRatio;
return [
Float32List.fromList([0, 0]),
Float32List.fromList([window.width, window.height]),
];
}
static Float32List _identityTransform() => Float32List.fromList(<double>[
1, 0, 0, // first column
0, 1, 0, // second column
0, 0, 1, // third column
]);
}