Using Firebase Cloud Storage for media in Android
Android apps often need more than a profile name or chat message: users add profile photos, trip videos, audio clips and receipts. Firebase Cloud Storage provides object storage for these binary files, while Firestore or Realtime Database can hold searchable metadata that points to each object.
The Android client can upload from a content URI, report progress and download through a controlled URL. Keep permissions narrow, storage rules strict and object names generated by the app rather than copied blindly from user input.
Storage and database roles
Choose the service by matching the data to its job. Text records and queryable relationships suit a database; images, video and audio belong in a storage bucket.
| App data | Firebase service | Suitable content |
|---|---|---|
| User profiles | Firestore | Names, settings and upload references |
| Live messages | Realtime Database | Small, frequently changing records |
| Original uploads | Cloud Storage | Photos, videos and audio files |
| Generated previews | Cloud Storage | Thumbnails and transcoded media |
A document can contain the object path, owner, MIME type and size. Add a token download URL only when that access model is appropriate. SharedPreferences can remember a local upload state, but it is not a place for large media.
Setting up the Android project
Apply the Google Services plugin and let the Firebase BOM align the Storage dependency with other Firebase libraries:
// Root build.gradle.kts
plugins {
id("com.google.gms.google-services") version "4.4.2" apply false
}
// app/build.gradle.kts
plugins {
id("com.google.gms.google-services")
}
dependencies {
implementation(platform("com.google.firebase:firebase-bom:33.7.0"))
implementation("com.google.firebase:firebase-storage")
}
Create a Firebase project, add an Android app with the package name matching applicationId, and download google-services.json. Put it under app/google-services.json, then enable Cloud Storage. Choose a bucket region by testing latency from users’ networks and confirming Firebase availability and data-residency obligations, because a bucket’s region cannot be changed later.
Securing uploads and access
Authenticate users before allowing private uploads and derive the object path from their Firebase Authentication UID. Security rules must enforce the same ownership rule on the server; hiding a button in the Android interface is not access control.
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /media/{userId}/{fileName} {
allow read: if request.auth != null
&& request.auth.uid == userId;
allow create, update: if request.auth != null
&& request.auth.uid == userId
&& request.resource.size < 10 * 1024 * 1024
&& request.resource.contentType.matches('image/.*|video/.*|audio/.*');
allow delete: if request.auth != null
&& request.auth.uid == userId;
}
}
}
Test rules with the Firebase emulator before release. Add Firebase App Check, use narrowly scoped claims where needed, and adjust the example size and content limits to the product. Firebase download URLs contain a token that may let its holder read the object, so private media should use authenticated SDK access or short-lived server-generated URLs.
Uploading images and video
Use the system picker so the app does not request broad storage permissions. The picker returns a content URI that Firebase can read during the upload:
val uid = Firebase.auth.currentUser?.uid ?: return
val mime = contentResolver.getType(uri) ?: "application/octet-stream"
val extension = MimeTypeMap.getSingleton()
.getExtensionFromMimeType(mime) ?: "bin"
val objectName = "${UUID.randomUUID()}.$extension"
val reference = Firebase.storage.reference
.child("media/$uid/$objectName")
val metadata = UploadTask.Metadata().apply {
contentType = mime
}
reference.putFile(uri, metadata)
.addOnProgressListener { snapshot ->
val percent = if (snapshot.totalBytes > 0) {
100L * snapshot.bytesTransferred / snapshot.totalBytes
} else {
0L
}
progressBar.progress = percent.toInt()
}
.addOnSuccessListener { snapshot ->
val objectPath = snapshot.reference.path
// Save the path with the related Firestore metadata.
}
.addOnFailureListener { error ->
// Display a mapped error and offer a suitable retry.
}
Before starting, verify:
- The user is authenticated and the destination path uses their UID.
- The MIME type, byte size and video duration are within product limits.
- Large transfers respect metered-data and battery preferences.
After completion, record:
- The immutable object path, MIME type and owner identifier.
- A user-facing caption and creation time in Firestore.
- Any replacement relationship, so an old object can be deleted safely.
Displaying media in the app
Load remote images with Coil or Glide when using an approved public or signed URL. In a ListView or RecyclerView, hold stable IDs, reuse the correct view type and show a placeholder while the request runs. A replacement image may receive a new download token, so use the complete URL as its cache key and invalidate the previous bitmap when necessary.
For the surrounding view hierarchy, layout fundamentals explains how LinearLayout and RelativeLayout behave before you refine a media card with ConstraintLayout. Set explicit image dimensions, preserve the aspect ratio and avoid downloading a full-resolution file for a small thumbnail.
For video, store or generate a poster image and pass the video URL to an appropriate player. Provide a meaningful description for accessibility, expose pause and retry controls, and avoid starting playback automatically just because a row became visible.
Handling progress and network errors
Treat upload and download tasks as state, not a single success callback. Report bytes transferred, preserve the selected URI until the task finishes, and map common StorageException codes to useful messages. Permission denied, unauthenticated, file-size limit and retry-limit errors need different remedies, so a generic “failed” label hides the next action.
Connection quality can change during a journey. Fast 4G or 5G around Sydney and Melbourne makes large uploads feel routine, while regional Western Australia and northern Queensland can have longer gaps or weaker coverage. Check the active network, warn before a video consumes significant mobile data, and let users retry when connectivity returns instead of forcing an immediate repeat.
Keep the user informed during retries and avoid duplicate objects. A Firebase task listener can update the visible percentage, while persistent application state should record pending, completed and failed work. For long videos, use a foreground worker when appropriate and reconcile local records with the bucket after the app restarts.
Downloading, caching, and sharing
Use getDownloadUrl() to obtain a token URL when that access model is appropriate, or retrieve a private object through the authenticated SDK. Do not call getBytes() for large media because it loads the file into memory. Cache thumbnails more aggressively than originals, set a disk limit and remove files that the app no longer references.
To share a private file, create a short-lived signed URL on a trusted server or Cloud Function, then pass that URL to the Android share sheet. When sharing a local cached file instead, expose it through a FileProvider and grant temporary read permission. Never embed a service-account key in the app or assume that a storage path alone grants access.
Localise the surrounding experience for Australia. Apps distributed through Google Play Australia commonly show prices in Australian dollars with GST included, so Cloud Storage budgets and in-app upgrade copy should use AUD. Use Australian English such as “colour” and “favourite”, and format dates day first for local users.
Sample media can reflect familiar places such as Bondi Beach, Uluru and Kakadu without exposing people unnecessarily. Obtain consent for identifiable faces and handle personal information consistently with the Australian Privacy Act and applicable Australian Privacy Principles. A friendly, direct tone also travels well locally: “All good—your video is ready” is clear and relaxed without forcing slang into technical errors.