diff --git a/packages/flutter_tools/lib/src/base/utils.dart b/packages/flutter_tools/lib/src/base/utils.dart index 31ee099195d..fddf40ce871 100644 --- a/packages/flutter_tools/lib/src/base/utils.dart +++ b/packages/flutter_tools/lib/src/base/utils.dart @@ -10,6 +10,7 @@ import 'package:crypto/crypto.dart'; import 'package:intl/intl.dart'; import 'package:quiver/time.dart'; +import '../globals.dart'; import 'context.dart'; import 'file_system.dart'; import 'platform.dart'; @@ -216,3 +217,42 @@ class Uuid { } Clock get clock => context.putIfAbsent(Clock, () => const Clock()); + +typedef Future AsyncCallback(); + +/// A [Timer] inspired class that: +/// - has a different initial value for the first callback delay +/// - waits for a callback to be complete before it starts the next timer +class Poller { + Poller(this.callback, this.pollingInterval, { this.initialDelay: Duration.ZERO }) { + new Future.delayed(initialDelay, _handleCallback); + } + + final AsyncCallback callback; + final Duration initialDelay; + final Duration pollingInterval; + + bool _cancelled = false; + Timer _timer; + + Future _handleCallback() async { + if (_cancelled) + return; + + try { + await callback(); + } catch (error) { + printTrace('Error from poller: $error'); + } + + if (!_cancelled) + _timer = new Timer(pollingInterval, _handleCallback); + } + + /// Cancels the poller. + void cancel() { + _cancelled = true; + _timer?.cancel(); + _timer = null; + } +} diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index 614ffc2742b..e46519ac5a9 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -116,35 +116,28 @@ abstract class PollingDeviceDiscovery extends DeviceDiscovery { final String name; ItemListNotifier _items; - Timer _timer; + Poller _poller; Future> pollingGetDevices(); void startPolling() { - if (_timer == null) { + if (_poller == null) { _items ??= new ItemListNotifier(); - bool _fetchingDevices = false; - _timer = new Timer.periodic(_pollingInterval, (Timer timer) async { - if (_fetchingDevices) { - printTrace('Skipping device poll: already in progress'); - return; - } - _fetchingDevices = true; + + _poller = new Poller(() async { try { final List devices = await pollingGetDevices().timeout(_pollingTimeout); _items.updateWithNewList(devices); } on TimeoutException { printTrace('Device poll timed out.'); - } finally { - _fetchingDevices = false; } - }); + }, _pollingInterval); } } void stopPolling() { - _timer?.cancel(); - _timer = null; + _poller?.cancel(); + _poller = null; } @override diff --git a/packages/flutter_tools/test/utils_test.dart b/packages/flutter_tools/test/utils_test.dart index ace2edfcced..370e13fbb1f 100644 --- a/packages/flutter_tools/test/utils_test.dart +++ b/packages/flutter_tools/test/utils_test.dart @@ -2,6 +2,8 @@ // 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_tools/src/base/utils.dart'; import 'package:flutter_tools/src/base/version.dart'; import 'package:test/test.dart'; @@ -115,4 +117,52 @@ baz=qux expect(new Version.parse('Preview2.2'), isNull); }); }); + + group('Poller', () { + const Duration kShortDelay = const Duration(milliseconds: 100); + + Poller poller; + + tearDown(() { + poller?.cancel(); + }); + + test('fires at start', () async { + bool called = false; + poller = new Poller(() { + called = true; + }, const Duration(seconds: 1)); + expect(called, false); + await new Future.delayed(kShortDelay); + expect(called, true); + }); + + test('runs periodically', () async { + // Ensure we get the first (no-delay) callback, and one of the periodic callbacks. + int callCount = 0; + poller = new Poller(() { + callCount++; + }, new Duration(milliseconds: kShortDelay.inMilliseconds ~/ 2)); + expect(callCount, 0); + await new Future.delayed(kShortDelay); + expect(callCount, greaterThanOrEqualTo(2)); + }); + + test('no quicker then the periodic delay', () async { + // Make sure that the poller polls at delay + the time it took to run the callback. + final Completer completer = new Completer(); + DateTime firstTime; + poller = new Poller(() async { + if (firstTime == null) + firstTime = new DateTime.now(); + else + completer.complete(new DateTime.now().difference(firstTime)); + + // introduce a delay + await new Future.delayed(kShortDelay); + }, kShortDelay); + final Duration duration = await completer.future; + expect(duration, greaterThanOrEqualTo(new Duration(milliseconds: kShortDelay.inMilliseconds * 2))); + }); + }); }