Line data Source code
1 : import 'dart:convert';
2 : import 'dart:developer' as developer;
3 : import 'package:flutter/foundation.dart';
4 : import 'events.dart';
5 : import 'platform_interface.dart';
6 :
7 : /// Register the DevTools service extensions for native_workmanager.
8 : /// This allows the DevTools Extension to request real-time task metrics,
9 : /// queue sizes, and DAG states without needing continuous polling.
10 : bool _devToolsExtensionsRegistered = false;
11 :
12 5 : @pragma('vm:entry-point')
13 : void registerDevToolsExtensions() {
14 : if (!kDebugMode && !kProfileMode) return;
15 : if (_devToolsExtensionsRegistered) return;
16 : _devToolsExtensionsRegistered = true;
17 :
18 5 : developer.registerExtension('ext.native_workmanager.getMetrics',
19 0 : (method, parameters) async {
20 : try {
21 0 : final metrics = await NativeWorkManagerPlatform.instance.getMetrics();
22 0 : return developer.ServiceExtensionResponse.result(jsonEncode(metrics));
23 : } catch (e) {
24 0 : return developer.ServiceExtensionResponse.error(
25 : developer.ServiceExtensionResponse.extensionError,
26 0 : e.toString(),
27 : );
28 : }
29 : });
30 :
31 5 : developer.registerExtension('ext.native_workmanager.syncQueue',
32 0 : (method, parameters) async {
33 : try {
34 : final success =
35 0 : await NativeWorkManagerPlatform.instance.syncOfflineQueue();
36 0 : return developer.ServiceExtensionResponse.result(
37 0 : jsonEncode({'success': success}));
38 : } catch (e) {
39 0 : return developer.ServiceExtensionResponse.error(
40 : developer.ServiceExtensionResponse.extensionError,
41 0 : e.toString(),
42 : );
43 : }
44 : });
45 : }
46 :
47 : /// Type-safe delegate for forwarding task events to third-party SDKs.
48 : ///
49 : /// Implement this class and pass to [NativeWorkManager.configure] via
50 : /// [ObservabilityConfig] to integrate with any analytics, crash-reporting,
51 : /// or logging SDK without relying on `dynamic` reflection.
52 : ///
53 : /// ## Example — Sentry
54 : ///
55 : /// ```dart
56 : /// class SentryWorkManagerLogger implements WorkManagerLogger {
57 : /// @override
58 : /// void onTaskStart(String taskId, String workerType) {
59 : /// Sentry.addBreadcrumb(Breadcrumb(
60 : /// message: 'Background task started: $taskId ($workerType)',
61 : /// category: 'native_workmanager',
62 : /// ));
63 : /// }
64 : ///
65 : /// @override
66 : /// void onTaskComplete(TaskEvent event) {
67 : /// Sentry.addBreadcrumb(Breadcrumb(
68 : /// message: 'Background task completed: ${event.taskId}',
69 : /// category: 'native_workmanager',
70 : /// ));
71 : /// }
72 : ///
73 : /// @override
74 : /// void onTaskFail(TaskEvent event) {
75 : /// Sentry.captureMessage(
76 : /// 'Background task failed: ${event.taskId}',
77 : /// hint: Hint.withMap({'message': event.message ?? ''}),
78 : /// );
79 : /// }
80 : /// }
81 : ///
82 : /// // Wire up during initialization:
83 : /// NativeWorkManager.configure(
84 : /// observability: ObservabilityConfig.fromLogger(SentryWorkManagerLogger()),
85 : /// );
86 : /// ```
87 : ///
88 : /// ## Example — Firebase
89 : ///
90 : /// ```dart
91 : /// class FirebaseWorkManagerLogger implements WorkManagerLogger {
92 : /// @override
93 : /// void onTaskStart(String taskId, String workerType) {
94 : /// FirebaseAnalytics.instance.logEvent(
95 : /// name: 'bg_task_start',
96 : /// parameters: {'task_id': taskId, 'worker': workerType},
97 : /// );
98 : /// }
99 : ///
100 : /// @override
101 : /// void onTaskComplete(TaskEvent event) {
102 : /// FirebaseAnalytics.instance.logEvent(
103 : /// name: 'bg_task_success',
104 : /// parameters: {'task_id': event.taskId},
105 : /// );
106 : /// }
107 : ///
108 : /// @override
109 : /// void onTaskFail(TaskEvent event) {
110 : /// FirebaseCrashlytics.instance.recordError(
111 : /// event.message ?? 'Unknown error',
112 : /// null,
113 : /// reason: 'Background task failed: ${event.taskId}',
114 : /// );
115 : /// }
116 : /// }
117 : /// ```
118 : abstract class WorkManagerLogger {
119 : /// Called when a background task begins execution.
120 : void onTaskStart(String taskId, String workerType);
121 :
122 : /// Called when a background task completes successfully.
123 : void onTaskComplete(TaskEvent event);
124 :
125 : /// Called when a background task fails.
126 : void onTaskFail(TaskEvent event);
127 : }
128 :
129 : /// Configuration for built-in observability hooks.
130 : ///
131 : /// Pass to [NativeWorkManager.configure] to receive callbacks whenever
132 : /// a background task starts, completes, or fails. Useful for analytics,
133 : /// performance monitoring, and crash reporting — without having to manually
134 : /// subscribe to the events/progress streams everywhere in your app.
135 : ///
136 : /// ## Setup
137 : ///
138 : /// ```dart
139 : /// void main() async {
140 : /// WidgetsFlutterBinding.ensureInitialized();
141 : /// await NativeWorkManager.initialize();
142 : ///
143 : /// NativeWorkManager.configure(
144 : /// observability: ObservabilityConfig(
145 : /// onTaskStart: (taskId, workerType) {
146 : /// analytics.track('bg_task_start', {'worker': workerType});
147 : /// },
148 : /// onTaskComplete: (event) {
149 : /// performance.record('task_duration', {
150 : /// 'taskId': event.taskId,
151 : /// 'elapsed': event.timestamp.difference(startTimes[event.taskId]!),
152 : /// });
153 : /// },
154 : /// onTaskFail: (event) {
155 : /// crashlytics.log('Background task failed: ${event.taskId}');
156 : /// if (event.message != null) {
157 : /// crashlytics.recordError(event.message!, null);
158 : /// }
159 : /// },
160 : /// ),
161 : /// );
162 : ///
163 : /// runApp(MyApp());
164 : /// }
165 : /// ```
166 : ///
167 : /// ## Callback Guarantees
168 : ///
169 : /// - All callbacks are invoked on the **main thread** (same as the events/progress streams).
170 : /// - Callbacks are **fire-and-forget** — exceptions inside them are caught and logged
171 : /// to avoid disrupting the events stream.
172 : /// - `onTaskStart` fires when the native worker **actually begins execution**, driven by
173 : /// a dedicated lifecycle event from the native side. It fires for **all** tasks —
174 : /// including fast workers that never emit a progress update.
175 : /// - `onTaskComplete` / `onTaskFail` are mutually exclusive for a given task.
176 : @immutable
177 : class ObservabilityConfig {
178 2 : const ObservabilityConfig({
179 : this.onTaskStart,
180 : this.onTaskComplete,
181 : this.onTaskFail,
182 : this.onProgress,
183 : });
184 :
185 : /// Create an [ObservabilityConfig] from a [WorkManagerLogger] implementation.
186 : ///
187 : /// Prefer this over the deprecated `useSentry()` / `useFirebase()` stubs.
188 : /// Implement [WorkManagerLogger] with your SDK's actual API calls, then pass
189 : /// it here — fully type-safe, no `dynamic` reflection involved.
190 : ///
191 : /// ```dart
192 : /// NativeWorkManager.configure(
193 : /// observability: ObservabilityConfig.fromLogger(MyLogger()),
194 : /// );
195 : /// ```
196 1 : factory ObservabilityConfig.fromLogger(WorkManagerLogger logger) {
197 1 : return ObservabilityConfig(
198 1 : onTaskStart: (taskId, workerType) =>
199 1 : logger.onTaskStart(taskId, workerType),
200 2 : onTaskComplete: (event) => logger.onTaskComplete(event),
201 2 : onTaskFail: (event) => logger.onTaskFail(event),
202 : );
203 : }
204 :
205 : /// Called when the native worker begins execution.
206 : ///
207 : /// The `workerType` parameter is the worker class name (e.g. `'HttpDownloadWorker'`,
208 : /// `'HttpUploadWorker'`, `'DartCallbackWorker'`), or an empty string if
209 : /// the native side does not report a type.
210 : ///
211 : /// This callback fires reliably for **all** tasks — including fast workers
212 : /// that never emit progress. It is driven by a native "started" lifecycle
213 : /// event emitted when the worker actually begins execution, not by the first
214 : /// progress update.
215 : final void Function(String taskId, String workerType)? onTaskStart;
216 :
217 : /// Called when a task completes successfully.
218 : final void Function(TaskEvent event)? onTaskComplete;
219 :
220 : /// Called when a task fails.
221 : final void Function(TaskEvent event)? onTaskFail;
222 :
223 : /// Called on every [TaskProgress] update for any task.
224 : ///
225 : /// Use for a global progress dashboard or logging. For task-specific
226 : /// progress, filter by `progress.taskId`.
227 : final void Function(TaskProgress progress)? onProgress;
228 : }
229 :
230 : /// Internal dispatcher that routes events/progress to [ObservabilityConfig].
231 : ///
232 : /// Not part of the public API — use [NativeWorkManager.configure] instead.
233 : class ObservabilityDispatcher {
234 2 : ObservabilityDispatcher(this._config);
235 :
236 : final ObservabilityConfig _config;
237 :
238 : /// Called by [NativeWorkManager] internals when a progress update arrives.
239 1 : void dispatchProgress(TaskProgress progress) {
240 : // Phase 2: Post real-time event to DevTools for streaming observability.
241 : // This allows the DevTools extension to update progress bars without polling.
242 : if (kDebugMode || kProfileMode) {
243 2 : developer.postEvent('native_workmanager.progress', {
244 1 : 'taskId': progress.taskId,
245 1 : 'progress': progress.progress,
246 1 : 'message': progress.message,
247 1 : 'speed': progress.networkSpeed,
248 1 : 'timeRemaining': progress.timeRemaining?.inSeconds,
249 : });
250 : }
251 :
252 2 : if (_config.onProgress != null) {
253 5 : _safeCall(() => _config.onProgress!(progress), 'onProgress');
254 : }
255 : }
256 :
257 : /// Called by [NativeWorkManager] internals when a task event arrives.
258 : ///
259 : /// Handles both lifecycle events ([TaskEvent.isStarted]) and completion
260 : /// events (success / failure).
261 1 : void dispatchEvent(TaskEvent event) {
262 : // Phase 2: Post real-time status change to DevTools.
263 : if (kDebugMode || kProfileMode) {
264 2 : developer.postEvent('native_workmanager.event', {
265 1 : 'taskId': event.taskId,
266 1 : 'success': event.success,
267 1 : 'isStarted': event.isStarted,
268 1 : 'workerType': event.workerType,
269 1 : 'message': event.message,
270 2 : 'timestamp': event.timestamp.toIso8601String(),
271 1 : 'resultData': event.resultData,
272 : });
273 : }
274 :
275 1 : if (event.isStarted) {
276 2 : if (_config.onTaskStart != null) {
277 1 : _safeCall(
278 6 : () => _config.onTaskStart!(event.taskId, event.workerType ?? ''),
279 : 'onTaskStart',
280 : );
281 : }
282 : return;
283 : }
284 :
285 1 : if (event.success) {
286 2 : if (_config.onTaskComplete != null) {
287 5 : _safeCall(() => _config.onTaskComplete!(event), 'onTaskComplete');
288 : }
289 : } else {
290 2 : if (_config.onTaskFail != null) {
291 5 : _safeCall(() => _config.onTaskFail!(event), 'onTaskFail');
292 : }
293 : }
294 : }
295 :
296 1 : static void _safeCall(void Function() fn, String callbackName) {
297 : try {
298 1 : fn();
299 : } catch (e, stack) {
300 : // Swallow exceptions so a buggy callback can't break the events stream.
301 2 : debugPrint(
302 1 : '[native_workmanager] ObservabilityConfig.$callbackName threw: $e\n$stack',
303 : );
304 : }
305 : }
306 : }
|