From 9ff12f33408048592edd1b5ad0a28fb24f6d94be Mon Sep 17 00:00:00 2001 From: Guillaume Bernos Date: Thu, 7 May 2026 10:00:02 +0200 Subject: [PATCH] fix(database,android): fix a regression where rapidly opening and closing query streams on the same path could throw --- .../database/FirebaseDatabasePlugin.kt | 10 ++--- .../firebase_database/query_e2e.dart | 37 +++++++++++++++++++ 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/packages/firebase_database/firebase_database/android/src/main/kotlin/io/flutter/plugins/firebase/database/FirebaseDatabasePlugin.kt b/packages/firebase_database/firebase_database/android/src/main/kotlin/io/flutter/plugins/firebase/database/FirebaseDatabasePlugin.kt index 80c7ae0bef23..f9c0c6ffadf8 100644 --- a/packages/firebase_database/firebase_database/android/src/main/kotlin/io/flutter/plugins/firebase/database/FirebaseDatabasePlugin.kt +++ b/packages/firebase_database/firebase_database/android/src/main/kotlin/io/flutter/plugins/firebase/database/FirebaseDatabasePlugin.kt @@ -4,7 +4,6 @@ package io.flutter.plugins.firebase.database -import android.util.Log import androidx.annotation.NonNull import com.google.android.gms.tasks.Task import com.google.android.gms.tasks.TaskCompletionSource @@ -485,7 +484,7 @@ class FirebaseDatabasePlugin : } private fun removeEventStreamHandlers() { - for ((eventChannel, streamHandler) in streamHandlers) { + for ((eventChannel, streamHandler) in streamHandlers.toMap()) { streamHandler?.onCancel(null) eventChannel.setStreamHandler(null) } @@ -847,20 +846,21 @@ class FirebaseDatabasePlugin : override fun queryObserve(app: DatabasePigeonFirebaseApp, request: QueryRequest, callback: (KotlinResult) -> Unit) { try { - Log.d("FirebaseDatabase", "🔍 Kotlin: Setting up query observe for path=${request.path}") val database = getDatabaseFromPigeonApp(app) val reference = database.getReference(request.path) val query = queryFromModifiers(reference, request.modifiers) // Generate a unique channel name - val channelName = "firebase_database_query_${System.currentTimeMillis()}_${request.path.hashCode()}" + val channelName = + synchronized(this) { "firebase_database_query_${listenerCount++}" } // Set up the event channel val eventChannel = EventChannel(messenger, channelName) val streamHandler = EventStreamHandler(query, object : OnDispose { override fun run() { // Clean up when the stream is disposed - eventChannel.setStreamHandler(null) + eventChannel.setStreamHandler(null) + streamHandlers.remove(eventChannel) } }) eventChannel.setStreamHandler(streamHandler) diff --git a/tests/integration_test/firebase_database/query_e2e.dart b/tests/integration_test/firebase_database/query_e2e.dart index ab020ff4cab6..8b2ec17a3da5 100644 --- a/tests/integration_test/firebase_database/query_e2e.dart +++ b/tests/integration_test/firebase_database/query_e2e.dart @@ -608,6 +608,43 @@ void setupQueryTests() { ); }); + test( + 'cancels overlapping query streams without missing plugin', + () async { + const subscriptionCount = 128; + final queryRef = ref.child('overlapping-query-streams'); + await queryRef.set({'value': 1}); + + final errors = []; + final subscriptions = >[]; + final firstEventsReceived = Completer(); + var firstEventCount = 0; + + for (var i = 0; i < subscriptionCount; i++) { + subscriptions.add( + queryRef.onValue.listen( + (_) { + firstEventCount++; + if (firstEventCount >= subscriptionCount && + !firstEventsReceived.isCompleted) { + firstEventsReceived.complete(); + } + }, + onError: errors.add, + ), + ); + } + + await firstEventsReceived.future.timeout(const Duration(seconds: 10)); + await Future.wait( + subscriptions.map((subscription) => subscription.cancel()), + ); + + expect(errors, isEmpty); + }, + skip: defaultTargetPlatform != TargetPlatform.android, + ); + test( 'throw a `permission-denied` exception when accessing restricted data', () async {