@@ -6,6 +6,8 @@ import 'package:jni/jni.dart';
66import 'package:meta/meta.dart' ;
77
88import '../../../sentry_flutter.dart' ;
9+ import '../../isolate/isolate_logger.dart' ;
10+ import '../../isolate/isolate_worker.dart' ;
911import '../../replay/scheduled_recorder.dart' ;
1012import '../../screenshot/screenshot.dart' ;
1113import 'binding.dart' as native;
@@ -14,21 +16,30 @@ import 'binding.dart' as native;
1416// cumbersome, see https://github.com/dart-lang/native/issues/1794
1517@internal
1618class AndroidReplayRecorder extends ScheduledScreenshotRecorder {
17- _AndroidNativeReplayWorker ? _worker;
19+ final WorkerConfig _config;
20+ final SpawnWorkerFn _spawn;
21+ Worker ? _worker;
1822
1923 @internal // visible for testing, used by SentryNativeJava
2024 static AndroidReplayRecorder Function (SentryFlutterOptions ) factory =
2125 AndroidReplayRecorder .new ;
2226
23- AndroidReplayRecorder (super .options) {
27+ AndroidReplayRecorder (super .options, {SpawnWorkerFn ? spawn})
28+ : _config = WorkerConfig (
29+ debugName: 'SentryAndroidReplayRecorder' ,
30+ debug: options.debug,
31+ diagnosticLevel: options.diagnosticLevel,
32+ automatedTestMode: options.automatedTestMode,
33+ ),
34+ _spawn = spawn ?? spawnWorker {
2435 super .callback = _addReplayScreenshot;
2536 }
2637
2738 @override
2839 Future <void > start () async {
29- final spawningWorker = _AndroidNativeReplayWorker .spawn ();
40+ if (_worker != null ) return ;
41+ _worker = await _spawn (_config, _entryPoint);
3042 await super .start ();
31- _worker = await spawningWorker;
3243 }
3344
3445 @override
@@ -50,7 +61,7 @@ class AndroidReplayRecorder extends ScheduledScreenshotRecorder {
5061 '${screenshot .width }x${screenshot .height } pixels, '
5162 '${data .lengthInBytes } bytes)' );
5263
53- await _worker! .nativeAddScreenshot (_WorkItem (
64+ await _worker! .request (_WorkItem (
5465 timestamp: timestamp,
5566 data: data.buffer.asUint8List (),
5667 width: screenshot.width,
@@ -68,127 +79,71 @@ class AndroidReplayRecorder extends ScheduledScreenshotRecorder {
6879 }
6980 }
7081 }
71- }
72-
73- // Based on https://dart.dev/language/isolates#robust-ports-example
74- class _AndroidNativeReplayWorker {
75- final SendPort _commands;
76- final ReceivePort _responses;
77- final Map <int , Completer <Object ?>> _activeRequests = {};
78- int _idCounter = 0 ;
79- bool _closed = false ;
80-
81- static Future <_AndroidNativeReplayWorker > spawn () async {
82- // Create a receive port and add its initial message handler
83- final initPort = RawReceivePort ();
84- final connection = Completer <(ReceivePort , SendPort )>.sync ();
85- initPort.handler = (SendPort commandPort) {
86- connection.complete ((
87- ReceivePort .fromRawReceivePort (initPort),
88- commandPort,
89- ));
90- };
91-
92- // Spawn the isolate.
93- try {
94- await Isolate .spawn (_startRemoteIsolate, (initPort.sendPort),
95- debugName: 'SentryReplayRecorder' );
96- } on Object {
97- initPort.close ();
98- rethrow ;
99- }
100-
101- final (ReceivePort receivePort, SendPort sendPort) =
102- await connection.future;
10382
104- return _AndroidNativeReplayWorker ._(receivePort, sendPort);
83+ static void _entryPoint ((SendPort , WorkerConfig ) init) {
84+ final (host, config) = init;
85+ runWorker (config, host, _AndroidReplayHandler (config));
10586 }
87+ }
10688
107- _AndroidNativeReplayWorker ._(this ._responses, this ._commands) {
108- _responses.listen (_handleResponsesFromIsolate);
109- }
89+ class _AndroidReplayHandler extends WorkerHandler {
90+ final WorkerConfig _config;
91+ // Android Bitmap creation is a bit costly so we reuse it between captures.
92+ native .Bitmap ? _bitmap;
93+ late final native .ReplayIntegration _nativeReplay;
11094
111- Future <Object ?> nativeAddScreenshot (_WorkItem item) async {
112- if (_closed) throw StateError ('Closed' );
113- final completer = Completer <Object ?>.sync ();
114- final id = _idCounter++ ;
115- _activeRequests[id] = completer;
116- _commands.send ((id, item));
117- return await completer.future;
95+ _AndroidReplayHandler (this ._config) {
96+ _nativeReplay = native .SentryFlutterPlugin .Companion
97+ .privateSentryGetReplayIntegration ()! ;
11898 }
11999
120- void _handleResponsesFromIsolate (dynamic message) {
121- final (int id, Object ? response) = message as (int , Object ? );
122- final completer = _activeRequests.remove (id)! ;
123-
124- if (response is RemoteError ) {
125- completer.completeError (response);
126- } else {
127- completer.complete (response);
128- }
129-
130- if (_closed && _activeRequests.isEmpty) _responses.close ();
100+ @override
101+ FutureOr <void > onMessage (Object ? message) {
102+ IsolateLogger .log (
103+ SentryLevel .warning, 'Unexpected fire-and-forget message: $message ' );
131104 }
132105
133- /// This is the actual Android native implementation, the rest is just plumbing.
134- static void _handleCommandsToIsolate (
135- ReceivePort receivePort,
136- SendPort sendPort,
137- ) {
138- // Android Bitmap creation is a bit costly so we reuse it between captures.
139- native . Bitmap ? bitmap;
106+ @override
107+ FutureOr < Object ?> onRequest ( Object ? payload) {
108+ if (payload is ! _WorkItem ) {
109+ IsolateLogger . log (
110+ SentryLevel .warning, 'Unexpected payload type: $ payload ' );
111+ return null ;
112+ }
140113
141- final _nativeReplay = native . SentryFlutterPlugin . Companion
142- . privateSentryGetReplayIntegration () ! ;
114+ final item = payload;
115+ JByteBuffer ? jBuffer ;
143116
144- receivePort.listen ((message) {
145- if (message == 'shutdown' ) {
146- receivePort.close ();
147- return ;
148- }
149- final (id, item) = message as (int , _WorkItem );
150- try {
151- if (bitmap != null ) {
152- if (bitmap! .getWidth () != item.width ||
153- bitmap! .getHeight () != item.height) {
154- bitmap! .release ();
155- bitmap = null ;
156- }
117+ try {
118+ if (_bitmap != null ) {
119+ if (_bitmap! .getWidth () != item.width ||
120+ _bitmap! .getHeight () != item.height) {
121+ _bitmap! .release ();
122+ _bitmap = null ;
157123 }
124+ }
158125
159- // https://developer.android.com/reference/android/graphics/Bitmap#createBitmap(int,%20int,%20android.graphics.Bitmap.Config)
160- // Note: while the generated API is nullable, the docs say the returned value cannot be null..
161- bitmap ?? = native .Bitmap .createBitmap$3 (
162- item.width, item.height, native .Bitmap$Config .ARGB_8888 );
126+ // https://developer.android.com/reference/android/graphics/Bitmap#createBitmap(int,%20int,%20android.graphics.Bitmap.Config)
127+ // Note: while the generated API is nullable, the docs say the returned value cannot be null..
128+ _bitmap ?? = native .Bitmap .createBitmap$3 (
129+ item.width, item.height, native .Bitmap$Config .ARGB_8888 );
163130
164- final jBuffer = JByteBuffer .fromList (item.data);
165- try {
166- bitmap! .copyPixelsFromBuffer (jBuffer);
167- } finally {
168- jBuffer.release ();
169- }
131+ jBuffer = JByteBuffer .fromList (item.data);
132+ _bitmap! .copyPixelsFromBuffer (jBuffer);
170133
171- // TODO timestamp is currently missing in onScreenshotRecorded()
172- _nativeReplay.onScreenshotRecorded (bitmap ! );
134+ // TODO timestamp is currently missing in onScreenshotRecorded()
135+ _nativeReplay.onScreenshotRecorded (_bitmap ! );
173136
174- sendPort.send ((id, null ));
175- } catch (e, stacktrace) {
176- sendPort.send ((id, RemoteError (e.toString (), stacktrace.toString ())));
137+ return null ;
138+ } catch (exception, stackTrace) {
139+ IsolateLogger .log (SentryLevel .error, 'Failed to add replay screenshot' ,
140+ exception: exception, stackTrace: stackTrace);
141+ if (_config.automatedTestMode) {
142+ rethrow ;
177143 }
178- });
179- }
180-
181- static void _startRemoteIsolate (SendPort sendPort) {
182- final receivePort = ReceivePort ();
183- sendPort.send (receivePort.sendPort);
184- _handleCommandsToIsolate (receivePort, sendPort);
185- }
186-
187- void close () {
188- if (! _closed) {
189- _closed = true ;
190- _commands.send ('shutdown' );
191- if (_activeRequests.isEmpty) _responses.close ();
144+ return null ;
145+ } finally {
146+ jBuffer? .release ();
192147 }
193148 }
194149}
0 commit comments