Working with Firebase Storage in Android: Part 1

Uploading files to Firebase Storage

If you follow my writing (here or here), then you know I’ve been working on a new Android app called AfterShoot — it’s an AI-powered Android app that helps users take better pictures while also managing their digital waste.

One of the important features that I had to implement in this app was the ability to handle feedback provided by users — meaning, if my trained model predicted an incorrect result, I should be able to take that feedback from the user about what the correct output should be and use that to retrain my model for better results.

If you are new to Firebase, you might want to check out my earlier posts on Firebase here:

Firebase Cloud Storage

While implementing a custom data warehouse to store all this user data might be a mammoth task to undertake manually, thankfully Firebase provides us with an easy way to handle such scenarios. To achieve this functionality, I’ll be using Firebase Storage to upload and save the images that were incorrectly predicted by my model.

Here’s a screenshot showcasing what my Firebase Storage directory structure will look like:

For instance, if my model predicted a blurred picture as a good picture, the user now has an option to submit this feedback back to me. Upon doing so, that image will be uploaded into the blurred folder.

I can probably run a weekly training schedule for my model based on the feedback I’ve received. Doing so will result in a better model over time.

As the use case for using Firebase Storage is clear, in this blog post I’ll be outlining how I implemented it in AfterShoot. You can use the learnings to implement a similar feature in your app, as well!

Implementing Firebase Cloud Storage in Android

Before we go ahead with Android-specific configurations, it’s crucial that you’ve configured a Firebase Project with your app. To do so, you can find a good tutorial here:

Once done, you might also want to set up Firebase Storage from your Firebase Console. To do so, go to your app’s Firebase Console and navigate to the Storage option. From there, click on “Get Started” and follow the on-screen instructions.

Step 1: Add the required dependencies

To use Firebase Storage, you need to add the following dependency to your app’s build.gradle file:

dependencies {
  //...
  implementation 'com.google.firebase:firebase-storage:19.1.0'
}

Once done, sync your changes with Gradle and let it download the required dependencies.

Step 2: Initialize Firebase storage instance and configure the folders

Once the Firebase dependencies are added, the next step is to get an instance of the storage bucket/folder to which you want to upload your files.

For instance, the code below shows how to get access to the instance of the feedback folders from the screenshot earlier.

class MainActivity : AppCompatActivity(){
    // get access to the root folder of the storage
    private val rootRef = FirebaseStorage.getInstance().reference
    //create a new folder called user_feedback inside the root folder
    private val feedbackRef = rootRef.child("user_feedback")
    
    // create folders for each type of feedback
    private val blurRef = feedbackRef.child("blurred")
    private val blinksRef = feedbackRef.child("blinks")
    private val croppedFacesRef = feedbackRef.child("cropped_faces")
    // ...
  
}

As you can see, the getReference() method gives me access to the root folder, from which I can navigate to any nested subfolder with relative ease.

Step 3: Uploading files to Firebase Storage

Once the folder path is configured, you might want to upload a particular file to that folder. To do so, we use the helper method child() along with putBytes(), available inside the FirebaseStorage class.

The child() method will determine the name of the file that’s to be created, while the putBytes() method will determine the content of that file. Here’s what the code for doing this looks like:

class MainActivity : AppCompatActivity(){
   
      // ...
      
      // quality here is the quality of the image; good, blurred, overexposed, underexposed, etc.
      fun uploadImage(image: Image, quality: QUALITY) {
        getFirebaseRef(quality)
                // specify the name of the image that is to be uploaded
                .child("${image.name}.jpg")
                // you can also use putBytes() to submit a byteArray instead
                .putFile(image.uri)
                .addOnProgressListener {
                    // notify the user about current progress
                    val completePercent = (it.bytesTransferred / it.totalByteCount) * 100
                    if (completePercent.toInt() % 10 == 0) {
                        Toast.makeText(requireContext(), "Uploading : ${completePercent}% done", Toast.LENGTH_SHORT).show()
                    }
                }
                .addOnSuccessListener {
                    // file upload complered
                    Toast.makeText(requireContext(), "Feedback submitted!", Toast.LENGTH_SHORT).show()̥
                }
                .addOnFailureListener {
                    // file upload failed̥
                    it.printStackTrace()
                }
      }
      
      // a helper method that returns different references (blurRef, overexposedRef, etc.) based on the quality of image
      private fun getFirebaseRef(quality: QUALITY) = rootRef.child(quality.toString().toLowerCase(Locale.ROOT))
  
}

As you can see in the code above, as soon as the uploadImage() method is called, Firebase will initiate an upload task for the provided image into the storage bucket. We also have an option to monitor its progress as it’s being uploaded.

You can also specify the file’s metadata while you’re uploading it. For example, to let the storage know that you’re uploading a .jpeg image, you can set its content type accordingly while uploading the image:

class MainActivity : AppCompatActivity(){
    // ...
    fun uploadImage(image: Image, quality: QUALITY) {
        
        // specify the metadata of the uploaded file
        val metadata = StorageMetadata.Builder()
                .setContentType("image/jpeg")
                .build()

        val task = getFirebaseRef(quality)
                .child(image.uri.lastPathSegment ?: "${image.name}.jpg")
                // specify the metadata alongwith the file
                .putFile(image.uri, metadata)
                // ...
    }
  
}

Step 4: Managing the upload operation

Since the upload process requires internet connectivity and, in practical scenarios, it’s not guaranteed that the device will always be connected to the internet, you need to be able to handle disconnections or limited connectivity gracefully.

For example, if the device is connected to mobile data, you’d want to pause the upload process and wait for the device to be connected to a WiFi network to resume the process. Firebase allows us to do this as well, and the code snippet below shows how to do so:

class MainActivity : AppCompatActivity(){
   
      // ...
   
      fun uploadImage(image: Image, quality: QUALITY) {
        
        // the current upload task
        val uploadTask = getFirebaseRef(quality)
                        // specify the name of the image that is to be uploaded
                        .child("${image.name}.jpg")
                        // you can also use putBytes() to submit a byteArray instead
                        .putFile(image.uri)
        // ... 
        
        // pause the task
        uploadTask.pause()
        
        // resume the task
        task.resume()
        
        // cancel the task
        task.cancel()
        
      }
      
}

You can also attach the corresponding listeners to your task, in addition to the 3 listeners we attached above:

class MainActivity : AppCompatActivity(){
    // ...
    
    fun uploadImage(image: Image, quality: QUALITY) {
        val task = getFirebaseRef(quality)
                .child(image.uri.lastPathSegment ?: "${image.name}.jpg")
                .putFile(image.uri)
                // ...
                .addOnPausedListener { 
                    // called as soon as the task is paused
                }
                .addOnCanceledListener { 
                    // called as soon as the task is cancelled
                }
    }     
}

Step 5: Offloading the upload task into a foreground service

Uploading the image from the app might not pose any issues whatsoever since Firebase handles this task in a background thread. However, it makes more sense to offload it into a foreground thread so that the upload task continues even if the user exits your app while the file is being uploaded.

You can do so by creating a foreground service that contains the code to upload the image that we’ve written earlier, and instead of calling the uploadImage() method directly, start the service instead. You can learn more about how to do this in a blog I wrote earlier:

And that’s it! As you saw in this blog, it’s extremely easy to work with Firebase Storage, and in no time you can set up a robust and scalable data warehouse for your app!

In the next part of this blog post, we’ll look into working with the uploaded files, including how you can fetch, move, delete and modify them from an Android app.

If you want to look at the source code for the contents covered in this blog post, you can find it here:

Thanks for reading! If you enjoyed this story, please click the 👏 button and share it to help others find it! Feel free to leave a comment 💬 below.

Have feedback? Let’s connect on Twitter.

Avatar photo

Fritz

Our team has been at the forefront of Artificial Intelligence and Machine Learning research for more than 15 years and we're using our collective intelligence to help others learn, understand and grow using these new technologies in ethical and sustainable ways.

Comments 0 Responses

Leave a Reply

Your email address will not be published. Required fields are marked *