126 lines
3.5 KiB
Dart
126 lines
3.5 KiB
Dart
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<void> 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);
|
|
await triggerConnectionLost!();
|
|
} else if (regained && shouldReconnect) {
|
|
// We should reconnect
|
|
_log.finest('Network regained. Attempting reconnection...');
|
|
await _attemptReconnection(true);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<void> reset() async {
|
|
await _stopTimer();
|
|
await setIsReconnecting(false);
|
|
}
|
|
|
|
Future<void> _stopTimer() async {
|
|
await _timerLock.synchronized(() {
|
|
if (timer != null) {
|
|
timer!.cancel();
|
|
timer = null;
|
|
_log.finest('Destroying timer');
|
|
}
|
|
});
|
|
}
|
|
|
|
@visibleForTesting
|
|
Future<void> onTimerElapsed() async {
|
|
await _stopTimer();
|
|
|
|
_log.finest('Performing reconnect');
|
|
await performReconnect!();
|
|
}
|
|
|
|
Future<void> _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<void> onFailure() async {
|
|
final state = GetIt.I.get<ConnectivityService>().currentState;
|
|
|
|
if (state != ConnectivityResult.none) {
|
|
await _attemptReconnection(false);
|
|
} else {
|
|
_log.fine('Failure occurred while no network connection is available. Waiting for connection...');
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<void> onSuccess() async {
|
|
await reset();
|
|
}
|
|
}
|