Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/android_unit_tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ jobs:
# Tasks are listed per package rather than using the root testDebugUnitTest
# task, which would also build the Dart sources of the aggregate test app.
# Add a task here when a package gains an android/src/test directory.
run: cd tests/android && ./gradlew :firebase_crashlytics:testDebugUnitTest :cloud_functions:testDebugUnitTest
run: cd tests/android && ./gradlew :firebase_crashlytics:testDebugUnitTest :cloud_functions:testDebugUnitTest :firebase_storage:testDebugUnitTest
- name: 'Upload test reports'
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ android {
implementation platform("com.google.firebase:firebase-bom:${getRootProjectExtOrCoreProperty("FirebaseSDKVersion", firebaseCoreProject)}")
implementation 'com.google.firebase:firebase-storage'
implementation 'androidx.annotation:annotation:1.7.0'
testImplementation 'junit:junit:4.13.2'
}

testOptions {
unitTests.returnDefaultValues = true
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,17 @@ class FlutterFirebaseStoragePlugin : FlutterFirebasePlugin, FlutterPlugin, Fireb
private var channel: MethodChannel? = null
private var messenger: BinaryMessenger? = null

// Per instance, as each engine attaches its own plugin instance and numbers its tasks from 0.
private val inProgressTasks = FlutterFirebaseStorageTaskRegistry<FlutterFirebaseStorageTask>()
private val eventChannels: MutableMap<String, EventChannel> = HashMap()
private val streamHandlers: MutableMap<String, StreamHandler> = HashMap()

override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
initInstance(binding.binaryMessenger)
}

override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
FlutterFirebaseStorageTask.cancelInProgressTasks()
cancelInProgressTasks()
channel?.setMethodCallHandler(null)
checkNotNull(messenger)
FirebaseStorageHostApi.setUp(messenger!!, null)
Expand All @@ -51,6 +56,11 @@ class FlutterFirebaseStoragePlugin : FlutterFirebasePlugin, FlutterPlugin, Fireb
this.messenger = messenger
}

private fun cancelInProgressTasks() {
inProgressTasks.removeAll().forEach { it.destroy() }
}

@Synchronized
private fun registerEventChannel(
prefix: String,
identifier: String,
Expand All @@ -64,6 +74,12 @@ class FlutterFirebaseStoragePlugin : FlutterFirebasePlugin, FlutterPlugin, Fireb
return identifier
}

@Synchronized
private fun removeEventChannel(identifier: String) {
eventChannels.remove(identifier)?.setStreamHandler(null)
streamHandlers.remove(identifier)
}

@Synchronized
private fun removeEventListeners() {
val eventChannelKeys: List<String> = ArrayList(eventChannels.keys)
Expand Down Expand Up @@ -284,10 +300,11 @@ class FlutterFirebaseStoragePlugin : FlutterFirebasePlugin, FlutterPlugin, Fireb
val androidMetaData = getMetaDataFromPigeon(settableMetaData)
val storageTask =
FlutterFirebaseStorageTask.uploadBytes(
handle.toInt(), androidReference, data, androidMetaData)
handle.toInt(), androidReference, data, androidMetaData, inProgressTasks)
try {
val identifier = UUID.randomUUID().toString().lowercase(Locale.US)
val handler = storageTask.startTaskWithMethodChannel(channel!!, identifier)
val handler =
storageTask.startTaskWithMethodChannel(channel!!, identifier, ::removeEventChannel)
callback(
Result.success(
registerEventChannel(
Expand All @@ -311,10 +328,11 @@ class FlutterFirebaseStoragePlugin : FlutterFirebasePlugin, FlutterPlugin, Fireb
val bytes = stringToByteData(data, format.toInt())
val storageTask =
FlutterFirebaseStorageTask.uploadBytes(
handle.toInt(), androidReference, bytes!!, androidMetaData)
handle.toInt(), androidReference, bytes!!, androidMetaData, inProgressTasks)
try {
val identifier = UUID.randomUUID().toString().lowercase(Locale.US)
val handler = storageTask.startTaskWithMethodChannel(channel!!, identifier)
val handler =
storageTask.startTaskWithMethodChannel(channel!!, identifier, ::removeEventChannel)
callback(
Result.success(
registerEventChannel(
Expand All @@ -338,10 +356,12 @@ class FlutterFirebaseStoragePlugin : FlutterFirebasePlugin, FlutterPlugin, Fireb
handle.toInt(),
androidReference,
Uri.fromFile(File(filePath)),
settableMetaData?.let { getMetaDataFromPigeon(it) })
settableMetaData?.let { getMetaDataFromPigeon(it) },
inProgressTasks)
try {
val identifier = UUID.randomUUID().toString().lowercase(Locale.US)
val handler = storageTask.startTaskWithMethodChannel(channel!!, identifier)
val handler =
storageTask.startTaskWithMethodChannel(channel!!, identifier, ::removeEventChannel)
callback(
Result.success(
registerEventChannel(
Expand All @@ -360,10 +380,12 @@ class FlutterFirebaseStoragePlugin : FlutterFirebasePlugin, FlutterPlugin, Fireb
) {
val androidReference = getReferenceFromPigeon(app, reference)
val storageTask =
FlutterFirebaseStorageTask.downloadFile(handle.toInt(), androidReference, File(filePath))
FlutterFirebaseStorageTask.downloadFile(
handle.toInt(), androidReference, File(filePath), inProgressTasks)
try {
val identifier = UUID.randomUUID().toString().lowercase(Locale.US)
val handler = storageTask.startTaskWithMethodChannel(channel!!, identifier)
val handler =
storageTask.startTaskWithMethodChannel(channel!!, identifier, ::removeEventChannel)
callback(
Result.success(
registerEventChannel(
Expand All @@ -378,7 +400,7 @@ class FlutterFirebaseStoragePlugin : FlutterFirebasePlugin, FlutterPlugin, Fireb
handle: Long,
callback: (Result<Map<String, Any>>) -> Unit
) {
val storageTask = FlutterFirebaseStorageTask.getInProgressTaskForHandle(handle.toInt())
val storageTask = inProgressTasks.get(handle.toInt())
if (storageTask == null) {
val statusMap = HashMap<String, Any>()
statusMap["status"] = false
Expand Down Expand Up @@ -407,7 +429,7 @@ class FlutterFirebaseStoragePlugin : FlutterFirebasePlugin, FlutterPlugin, Fireb
handle: Long,
callback: (Result<Map<String, Any>>) -> Unit
) {
val storageTask = FlutterFirebaseStorageTask.getInProgressTaskForHandle(handle.toInt())
val storageTask = inProgressTasks.get(handle.toInt())
if (storageTask == null) {
val statusMap = HashMap<String, Any>()
statusMap["status"] = false
Expand Down Expand Up @@ -436,7 +458,7 @@ class FlutterFirebaseStoragePlugin : FlutterFirebasePlugin, FlutterPlugin, Fireb
handle: Long,
callback: (Result<Map<String, Any>>) -> Unit
) {
val storageTask = FlutterFirebaseStorageTask.getInProgressTaskForHandle(handle.toInt())
val storageTask = inProgressTasks.get(handle.toInt())
if (storageTask == null) {
val statusMap = HashMap<String, Any>()
statusMap["status"] = false
Expand Down Expand Up @@ -501,7 +523,7 @@ class FlutterFirebaseStoragePlugin : FlutterFirebasePlugin, FlutterPlugin, Fireb
override fun didReinitializeFirebaseCore(): Task<Void> {
val taskCompletionSource = TaskCompletionSource<Void>()
cachedThreadPool.execute {
FlutterFirebaseStorageTask.cancelInProgressTasks()
cancelInProgressTasks()
taskCompletionSource.setResult(null)
removeEventListeners()
}
Expand All @@ -513,9 +535,6 @@ class FlutterFirebaseStoragePlugin : FlutterFirebasePlugin, FlutterPlugin, Fireb
const val STORAGE_TASK_EVENT_NAME = "taskEvent"
const val DEFAULT_ERROR_CODE = "firebase_storage"

val eventChannels: MutableMap<String, EventChannel> = HashMap()
val streamHandlers: MutableMap<String, StreamHandler> = HashMap()

fun getExceptionDetails(exception: Exception): Map<String, String> {
val storageException = FlutterFirebaseStorageException.parserExceptionToFlutter(exception)
val details: MutableMap<String, String> = HashMap()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
package io.flutter.plugins.firebase.storage

import android.net.Uri
import android.util.SparseArray
import androidx.annotation.NonNull
import com.google.firebase.storage.FileDownloadTask
import com.google.firebase.storage.StorageMetadata
Expand All @@ -24,7 +23,8 @@ private constructor(
private val reference: StorageReference,
private val bytes: ByteArray?,
private val fileUri: Uri?,
private val metadata: StorageMetadata?
private val metadata: StorageMetadata?,
private val inProgressTasks: FlutterFirebaseStorageTaskRegistry<FlutterFirebaseStorageTask>
) {
private val pauseSyncObject = Object()
private val resumeSyncObject = Object()
Expand All @@ -33,12 +33,13 @@ private constructor(
private var destroyed: Boolean = false

init {
synchronized(inProgressTasks) { inProgressTasks.put(handle, this) }
inProgressTasks.put(handle, this)
}

fun startTaskWithMethodChannel(
@NonNull channel: MethodChannel,
@NonNull identifier: String
@NonNull identifier: String,
onCancelled: (String) -> Unit
): TaskStateChannelStreamHandler {
storageTask =
when (type) {
Expand All @@ -51,7 +52,8 @@ private constructor(
FlutterFirebaseStorageTaskType.DOWNLOAD -> reference.getFile(fileUri!!)
}

return TaskStateChannelStreamHandler(this, reference.storage, storageTask as Any, identifier)
return TaskStateChannelStreamHandler(
this, reference.storage, storageTask as Any, identifier, onCancelled)
}

fun getSnapshot(): Any = storageTask.snapshot
Expand Down Expand Up @@ -88,76 +90,68 @@ private constructor(
if (destroyed) return
destroyed = true

synchronized(inProgressTasks) {
if (storageTask.isInProgress || storageTask.isPaused) {
storageTask.cancel()
}
inProgressTasks.remove(handle)
if (storageTask.isInProgress || storageTask.isPaused) {
storageTask.cancel()
}
inProgressTasks.remove(handle, this)

synchronized(cancelSyncObject) { cancelSyncObject.notifyAll() }
synchronized(pauseSyncObject) { pauseSyncObject.notifyAll() }
synchronized(resumeSyncObject) { resumeSyncObject.notifyAll() }
}

companion object {
val inProgressTasks: SparseArray<FlutterFirebaseStorageTask> = SparseArray()

@JvmStatic
fun getInProgressTaskForHandle(handle: Int): FlutterFirebaseStorageTask? {
synchronized(inProgressTasks) {
return inProgressTasks.get(handle)
}
}

@JvmStatic
fun cancelInProgressTasks() {
synchronized(inProgressTasks) {
val tasks = ArrayList<FlutterFirebaseStorageTask>(inProgressTasks.size())
for (i in 0 until inProgressTasks.size()) {
val task: FlutterFirebaseStorageTask? = inProgressTasks.valueAt(i)
task?.let { tasks.add(it) }
}
inProgressTasks.clear()
tasks.forEach { it.destroy() }
}
}

@JvmStatic
fun uploadBytes(
handle: Int,
reference: StorageReference,
data: ByteArray,
metadata: StorageMetadata?
metadata: StorageMetadata?,
inProgressTasks: FlutterFirebaseStorageTaskRegistry<FlutterFirebaseStorageTask>
): FlutterFirebaseStorageTask {
return FlutterFirebaseStorageTask(
FlutterFirebaseStorageTaskType.BYTES, handle, reference, data, null, metadata)
FlutterFirebaseStorageTaskType.BYTES,
handle,
reference,
data,
null,
metadata,
inProgressTasks)
}

@JvmStatic
fun uploadFile(
handle: Int,
reference: StorageReference,
fileUri: Uri,
metadata: StorageMetadata?
metadata: StorageMetadata?,
inProgressTasks: FlutterFirebaseStorageTaskRegistry<FlutterFirebaseStorageTask>
): FlutterFirebaseStorageTask {
return FlutterFirebaseStorageTask(
FlutterFirebaseStorageTaskType.FILE, handle, reference, null, fileUri, metadata)
FlutterFirebaseStorageTaskType.FILE,
handle,
reference,
null,
fileUri,
metadata,
inProgressTasks)
}

@JvmStatic
fun downloadFile(
handle: Int,
reference: StorageReference,
file: File
file: File,
inProgressTasks: FlutterFirebaseStorageTaskRegistry<FlutterFirebaseStorageTask>
): FlutterFirebaseStorageTask {
return FlutterFirebaseStorageTask(
FlutterFirebaseStorageTaskType.DOWNLOAD,
handle,
reference,
null,
Uri.fromFile(file),
null)
null,
inProgressTasks)
}

@JvmStatic
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright 2026, the Chromium project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
package io.flutter.plugins.firebase.storage

/**
* The in-progress tasks of one plugin instance, keyed by their Dart handle.
*
* Handles come from a per-isolate counter, so each Flutter engine needs its own registry: with a
* shared one, engines overwrite each other's tasks, and detaching one engine cancels them all.
*/
internal class FlutterFirebaseStorageTaskRegistry<T : Any> {
private val tasks = HashMap<Int, T>()

@Synchronized
fun put(handle: Int, task: T) {
tasks[handle] = task
}

@Synchronized fun get(handle: Int): T? = tasks[handle]

/** Removes [task], unless [handle] now belongs to another task. */
@Synchronized
fun remove(handle: Int, task: T) {
if (tasks[handle] === task) tasks.remove(handle)
}

/** Removes and returns all tasks. */
@Synchronized
fun removeAll(): List<T> {
val all = ArrayList(tasks.values)
tasks.clear()
return all
}
}
Loading
Loading