Skip to content

Commit c701eef

Browse files
authored
fix: harden Firebase background message handling and error reporting (#820)
* fix: guard background message handler with runZonedGuarded * refactor: pass logger into background message handler * fix: guard contact lookup in background isolate with fallback on DB errors * fix: report background handler errors to Crashlytics - rename _initFirebase to _initFirebaseApp for clarity - initialize Firebase in background isolate before Crashlytics usage - record unhandled bg isolate errors via Crashlytics + logger * fix: initialize Firebase before guarded background handler execution
1 parent d9713a2 commit c701eef

1 file changed

Lines changed: 59 additions & 11 deletions

File tree

lib/bootstrap.dart

Lines changed: 59 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import 'dart:async';
22
import 'dart:io';
33
import 'dart:ui';
44

5+
import 'package:flutter/foundation.dart';
6+
7+
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
58
import 'package:logging/logging.dart';
69
import 'package:workmanager/workmanager.dart';
710
import 'package:firebase_core/firebase_core.dart';
@@ -31,7 +34,7 @@ Future<InstanceRegistry> bootstrap() async {
3134
final registry = InstanceRegistry();
3235

3336
// External SDKs (Side effects only, don't need registration)
34-
await _initFirebase();
37+
await _initFirebaseApp();
3538
await _initFirebaseMessaging();
3639
await _initLocalPushs();
3740

@@ -188,7 +191,7 @@ Future<void> _initCallkeep(FeatureAccess featureAccess) async {
188191
/// Initializes Firebase for background services. This initialization must be called in an isolate
189192
/// when Firebase components are used. For more details, refer to the Firebase documentation:
190193
/// https://firebase.google.com/docs/cloud-messaging/flutter/receive
191-
Future<void> _initFirebase() async {
194+
Future<void> _initFirebaseApp() async {
192195
try {
193196
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
194197
} catch (e) {
@@ -230,6 +233,30 @@ Future<void> _initFirebaseMessaging() async {
230233
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
231234
final logger = Logger('_firebaseMessagingBackgroundHandler');
232235

236+
// Ensure Firebase services are initialized before configuring Crashlytics.
237+
await _initFirebaseApp();
238+
239+
await runZonedGuarded(
240+
() => _handleBackgroundMessage(message, logger),
241+
(error, stack) => _recordBackgroundError(error, stack, logger),
242+
);
243+
}
244+
245+
/// Records background isolate errors to both the local logger and Firebase Crashlytics.
246+
void _recordBackgroundError(Object error, StackTrace stack, Logger logger) {
247+
logger.severe('Unhandled background error', error, stack);
248+
249+
FirebaseCrashlytics.instance.recordFlutterFatalError(
250+
FlutterErrorDetails(
251+
exception: error,
252+
stack: stack,
253+
context: ErrorDescription('Firebase background handler logic failure'),
254+
),
255+
);
256+
}
257+
258+
/// Core logic for processing background messages.
259+
Future<void> _handleBackgroundMessage(RemoteMessage message, Logger logger) async {
233260
// Cache remote configuration
234261
final remoteCacheConfigService = await DefaultRemoteCacheConfigService.init();
235262

@@ -250,15 +277,9 @@ Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
250277
_dHandleInspectPush(message.data, true);
251278

252279
if (appPush is PendingCallPush && Platform.isAndroid) {
253-
final appDatabase = await IsolateDatabase.create();
254-
final contactsRepository = ContactsRepository(
255-
appDatabase: appDatabase,
256-
contactsRemoteDataSource: null,
257-
contactsLocalDataSource: null,
258-
);
259-
260-
final contact = await contactsRepository.getContactByPhoneNumber(appPush.call.handle);
261-
final displayName = contact?.maybeName ?? appPush.call.displayName;
280+
// Known issue: [SqliteException] with code 5 (database is locked) may occur
281+
// due to concurrent database access from multiple isolates.
282+
final displayName = await _resolveContactDisplayNameWithFallback(appPush, logger);
262283

263284
AndroidCallkeepServices.backgroundPushNotificationBootstrapService.reportNewIncomingCall(
264285
appPush.call.id,
@@ -284,6 +305,33 @@ Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
284305
}
285306
}
286307

308+
/// Attempts to resolve the contact name from the database, falling back to push data on error.
309+
///
310+
/// This process is susceptible to [SqliteException] with code 5 (database is locked)
311+
/// when multiple isolates (e.g., background FCM and main app) access the database
312+
/// concurrently. If any error occurs, the display name from the push payload is returned.
313+
Future<String> _resolveContactDisplayNameWithFallback(PendingCallPush appPush, Logger logger) async {
314+
try {
315+
final appDatabase = await IsolateDatabase.create();
316+
final contactsRepository = ContactsRepository(
317+
appDatabase: appDatabase,
318+
contactsRemoteDataSource: null,
319+
contactsLocalDataSource: null,
320+
);
321+
322+
final contact = await contactsRepository.getContactByPhoneNumber(appPush.call.handle);
323+
return contact?.maybeName ?? appPush.call.displayName;
324+
} catch (e, stackTrace) {
325+
logger.severe(
326+
'Failed to resolve contact name from database for handle: ${appPush.call.handle}. '
327+
'Fallback to push display name will be used.',
328+
e,
329+
stackTrace,
330+
);
331+
return appPush.call.displayName;
332+
}
333+
}
334+
287335
Future _initLocalPushs() async {
288336
await FlutterLocalNotificationsPlugin().initialize(
289337
const InitializationSettings(

0 commit comments

Comments
 (0)