Line data Source code
1 : import 'dart:async';
2 : import 'dart:convert';
3 :
4 : import 'package:at_client/at_client.dart';
5 : import 'package:at_client/src/exception/at_client_exception_util.dart';
6 : import 'package:at_client/src/listener/at_sign_change_listener.dart';
7 : import 'package:at_client/src/listener/switch_at_sign_event.dart';
8 : import 'package:at_client/src/manager/monitor.dart';
9 : import 'package:at_client/src/preference/monitor_preference.dart';
10 : import 'package:at_client/src/response/notification_response_parser.dart';
11 : import 'package:at_client/src/service/notification_service.dart';
12 : import 'package:at_commons/at_commons.dart';
13 : import 'package:at_utils/at_logger.dart';
14 :
15 : class NotificationServiceImpl
16 : implements NotificationService, AtSignChangeListener {
17 : Map<String, StreamController> streamListeners = {};
18 : final emptyRegex = '';
19 : static const notificationIdKey = '_latestNotificationId';
20 0 : static final Map<String, NotificationService> _notificationServiceMap = {};
21 :
22 : final _logger = AtSignLogger('NotificationServiceImpl');
23 : var _isMonitorPaused = false;
24 : late AtClient _atClient;
25 : Monitor? _monitor;
26 : ConnectivityListener? _connectivityListener;
27 : dynamic _lastMonitorRetried;
28 :
29 0 : static Future<NotificationService> create(AtClient atClient) async {
30 0 : if (_notificationServiceMap.containsKey(atClient.getCurrentAtSign())) {
31 0 : return _notificationServiceMap[atClient.getCurrentAtSign()]!;
32 : }
33 0 : final notificationService = NotificationServiceImpl._(atClient);
34 0 : await notificationService._init();
35 0 : _notificationServiceMap[atClient.getCurrentAtSign()!] = notificationService;
36 0 : return _notificationServiceMap[atClient.getCurrentAtSign()]!;
37 : }
38 :
39 0 : NotificationServiceImpl._(AtClient atClient) {
40 0 : _atClient = atClient;
41 : }
42 :
43 0 : Future<void> _init() async {
44 0 : _logger.finer('notification init starting monitor');
45 0 : await _startMonitor();
46 0 : if (_connectivityListener == null) {
47 0 : _connectivityListener = ConnectivityListener();
48 0 : _connectivityListener!.subscribe().listen((isConnected) {
49 : if (isConnected) {
50 0 : _logger.finer('starting monitor through connectivity listener event');
51 0 : _startMonitor();
52 : } else {
53 0 : _logger.finer('lost network connectivity');
54 : }
55 : });
56 : }
57 : }
58 :
59 0 : Future<void> _startMonitor() async {
60 0 : if (_monitor != null && _monitor!.status == MonitorStatus.started) {
61 0 : _logger.finer(
62 0 : 'monitor is already started for ${_atClient.getCurrentAtSign()}');
63 : return;
64 : }
65 0 : final lastNotificationTime = await _getLastNotificationTime();
66 0 : _monitor = Monitor(
67 0 : _internalNotificationCallback,
68 0 : _onMonitorError,
69 0 : _atClient.getCurrentAtSign()!,
70 0 : _atClient.getPreferences()!,
71 0 : MonitorPreference()..keepAlive = true,
72 0 : _monitorRetry);
73 0 : await _monitor!.start(lastNotificationTime: lastNotificationTime);
74 0 : if (_monitor!.status == MonitorStatus.started) {
75 0 : _isMonitorPaused = false;
76 : }
77 : }
78 :
79 0 : Future<int?> _getLastNotificationTime() async {
80 0 : var atKey = AtKey()..key = notificationIdKey;
81 0 : if (_atClient.getLocalSecondary()!.keyStore!.isKeyExists(atKey.key!)) {
82 0 : final atValue = await _atClient.get(atKey);
83 0 : if (atValue.value != null) {
84 0 : _logger.finer('json from hive: ${atValue.value}');
85 0 : return jsonDecode(atValue.value)['epochMillis'];
86 : }
87 : }
88 : return null;
89 : }
90 :
91 0 : @override
92 : void stopAllSubscriptions() {
93 0 : _isMonitorPaused = true;
94 0 : _monitor?.stop();
95 0 : _connectivityListener?.unSubscribe();
96 0 : streamListeners.forEach((regex, streamController) {
97 0 : if (!streamController.isClosed) () => streamController.close();
98 : });
99 0 : streamListeners.clear();
100 : }
101 :
102 0 : void _internalNotificationCallback(String notificationJSON) async {
103 : try {
104 0 : final notificationParser = NotificationResponseParser();
105 : final atNotifications = notificationParser
106 0 : .getAtNotifications(notificationParser.parse(notificationJSON));
107 0 : for (var atNotification in atNotifications) {
108 : // Saves latest notification id to the keys if its not a stats notification.
109 0 : if (atNotification.id != '-1') {
110 0 : await _atClient.put(AtKey()..key = notificationIdKey,
111 0 : jsonEncode(atNotification.toJson()));
112 : }
113 0 : streamListeners.forEach((regex, streamController) {
114 0 : if (regex != emptyRegex) {
115 0 : if (regex.allMatches(atNotification.key).isNotEmpty) {
116 0 : streamController.add(atNotification);
117 : }
118 : } else {
119 0 : streamController.add(atNotification);
120 : }
121 : });
122 : }
123 0 : } on Exception catch (e) {
124 0 : _logger.severe(
125 0 : 'exception processing: error:${e.toString()} notificationJson: $notificationJSON');
126 : }
127 : }
128 :
129 0 : void _monitorRetry() {
130 0 : if (_lastMonitorRetried != null &&
131 0 : DateTime.now().toUtc().difference(_lastMonitorRetried).inSeconds < 15) {
132 0 : _logger.info('Attempting to retry in less than 15 seconds... Rejected');
133 : return;
134 : }
135 0 : if (_isMonitorPaused) {
136 0 : _logger.finer('monitor is paused. not retrying');
137 : return;
138 : }
139 0 : _lastMonitorRetried = DateTime.now().toUtc();
140 0 : _logger.finer('monitor retry for ${_atClient.getCurrentAtSign()}');
141 0 : Future.delayed(
142 0 : Duration(seconds: 15),
143 0 : () async => _monitor!
144 0 : .start(lastNotificationTime: await _getLastNotificationTime()));
145 : }
146 :
147 0 : void _onMonitorError(Exception e) {
148 0 : _logger.severe('internal error in monitor: ${e.toString()}');
149 : }
150 :
151 : @override
152 0 : Future<NotificationResult> notify(NotificationParams notificationParams,
153 : {Function? onSuccess, Function? onError}) async {
154 0 : var notificationResult = NotificationResult()
155 0 : ..atKey = notificationParams.atKey;
156 : dynamic notificationId;
157 : try {
158 : // Notifies key to another notificationParams.atKey.sharedWith atsign
159 : // Returns the notificationId.
160 0 : notificationId = await _atClient.notifyChange(notificationParams);
161 0 : } on Exception catch (e) {
162 : // Setting notificationStatusEnum to errored
163 0 : notificationResult.notificationStatusEnum =
164 : NotificationStatusEnum.undelivered;
165 0 : var errorCode = AtClientExceptionUtil.getErrorCode(e);
166 0 : var atClientException = AtClientException(
167 0 : errorCode, AtClientExceptionUtil.getErrorDescription(errorCode));
168 0 : notificationResult.atClientException = atClientException;
169 : // Invoke onErrorCallback
170 : if (onError != null) {
171 0 : onError(notificationResult);
172 : }
173 : return notificationResult;
174 : }
175 0 : var notificationParser = NotificationResponseParser();
176 0 : notificationResult.notificationID =
177 0 : notificationParser.parse(notificationId).response;
178 : // Gets the notification status and parse the response.
179 0 : var notificationStatus = notificationParser.parse(
180 0 : await _getFinalNotificationStatus(notificationResult.notificationID!));
181 0 : switch (notificationStatus.response) {
182 0 : case 'delivered':
183 0 : notificationResult.notificationStatusEnum =
184 : NotificationStatusEnum.delivered;
185 : // If onSuccess callback is registered, invoke callback method.
186 : if (onSuccess != null) {
187 0 : onSuccess(notificationResult);
188 : }
189 : break;
190 0 : case 'undelivered':
191 0 : notificationResult.notificationStatusEnum =
192 : NotificationStatusEnum.undelivered;
193 0 : notificationResult.atClientException = AtClientException(
194 0 : error_codes['SecondaryConnectException'],
195 0 : error_description[error_codes['SecondaryConnectException']]);
196 : // If onError callback is registered, invoke callback method.
197 : if (onError != null) {
198 0 : onError(notificationResult);
199 : }
200 : break;
201 : }
202 : return notificationResult;
203 : }
204 :
205 : /// Queries the status of the notification
206 : /// Takes the notificationId as input as returns the status of the notification
207 0 : Future<String> _getFinalNotificationStatus(String notificationId) async {
208 : String status = '';
209 : // For every 2 seconds, queries the status of the notification
210 0 : while (status.isEmpty || status == 'data:queued') {
211 0 : await Future.delayed(Duration(seconds: 2),
212 0 : () async => status = await _atClient.notifyStatus(notificationId));
213 : }
214 : return status;
215 : }
216 :
217 0 : @override
218 : Stream<AtNotification> subscribe({String? regex}) {
219 0 : regex ??= emptyRegex;
220 0 : if (streamListeners.containsKey(regex)) {
221 0 : _logger.finer('subscription already exists');
222 0 : return streamListeners[regex]!.stream as Stream<AtNotification>;
223 : }
224 0 : final _controller = StreamController<AtNotification>.broadcast();
225 0 : streamListeners[regex] = _controller;
226 0 : _logger.finer('added regex to listener $regex');
227 0 : return _controller.stream;
228 : }
229 :
230 0 : @override
231 : void listenToAtSignChange(SwitchAtSignEvent switchAtSignEvent) {
232 0 : if (switchAtSignEvent.previousAtClient?.getCurrentAtSign() ==
233 0 : _atClient.getCurrentAtSign()) {
234 : // actions for previous atSign
235 0 : _logger.finer(
236 0 : 'stopping notification listeners for ${_atClient.getCurrentAtSign()}');
237 0 : stopAllSubscriptions();
238 : }
239 : }
240 : }
|