import 'dart:async'; import 'dart:math'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:get_it/get_it.dart'; import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; import 'package:moxxmpp/moxxmpp.dart'; import 'package:moxxyv2/service/connectivity.dart'; import 'package:synchronized/synchronized.dart'; /// This class implements a reconnection policy that is connectivity aware with a random /// backoff. This means that we perform the random backoff only as long as we are /// connected. Otherwise, we idle until we have a connection again. class MoxxyReconnectionPolicy extends ReconnectionPolicy { MoxxyReconnectionPolicy({ bool isTesting = false, this.maxBackoffTime }) : _isTesting = isTesting, _timerLock = Lock(), _log = Logger('MoxxyReconnectionPolicy'), super(); final Logger _log; /// The backoff timer @visibleForTesting Timer? timer; final Lock _timerLock; /// Just for testing purposes final bool _isTesting; /// Maximum backoff time final int? maxBackoffTime; /// To be called when the conectivity changes Future onConnectivityChanged(bool regained, bool lost) async { // Do nothing if we should not reconnect if (!shouldReconnect && regained) { _log.finest('Connectivity changed but not attempting reconnection as shouldReconnect is false'); return; } if (lost) { // We just lost network connectivity _log.finest('Lost network connectivity. Queueing failure...'); // Cancel the timer if it was running await _stopTimer(); await setIsReconnecting(false); triggerConnectionLost!(); } else if (regained && shouldReconnect) { // We should reconnect _log.finest('Network regained. Attempting reconnection...'); await _attemptReconnection(true); } } @override Future reset() async { await _stopTimer(); await setIsReconnecting(false); } Future _stopTimer() async { await _timerLock.synchronized(() { if (timer != null) { timer!.cancel(); timer = null; _log.finest('Destroying timer'); } }); } @visibleForTesting Future onTimerElapsed() async { await _stopTimer(); _log.finest('Performing reconnect'); await performReconnect!(); } Future _attemptReconnection(bool immediately) async { if (await testAndSetIsReconnecting()) { // Attempt reconnecting int seconds; if (_isTesting) { seconds = 9999; } else { final r = Random().nextInt(15); if (maxBackoffTime != null) { seconds = min(maxBackoffTime!, r); } else { seconds = r; } } await _stopTimer(); if (immediately) { _log.finest('Immediately attempting reconnection...'); await onTimerElapsed(); } else { _log.finest('Started backoff timer with ${seconds}s backoff'); await _timerLock.synchronized(() { timer = Timer(Duration(seconds: seconds), onTimerElapsed); }); } } else { _log.severe('_attemptReconnection called while reconnect is running!'); } } @override Future onFailure() async { final state = GetIt.I.get().currentState; if (state != ConnectivityResult.none) { await _attemptReconnection(false); } else { _log.fine('Failure occurred while no network connection is available. Waiting for connection...'); } } @override Future onSuccess() async { await reset(); } }