Firebase Storage (Java)

Firebase Cloud Storage is where user-generated files live — for RecipeBox, that means the actual recipe photo files, as opposed to the recipe's text data. It shares the same Firebase project and the same signed-in-user model as Firebase Auth and Firestore, so access can be controlled with the same per-user rules approach already used for both.

StorageReference: A Pointer Into the Bucket

A StorageReference doesn't hold a file itself — it points at a location inside your project's storage bucket, the same way a File path points at a location on disk without necessarily containing anything yet.

FirebaseStorage storage = FirebaseStorage.getInstance();
StorageReference storageRef = storage.getReference();

// A pointer to where this specific recipe's photo will live
StorageReference photoRef = storageRef.child("recipe_photos/" + recipeId + ".jpg");
Explanation: "recipe_photos/" + recipeId + ".jpg" namespaces every recipe's photo under its own predictable path, using the same recipeId already used as the Firestore document ID — making the two easy to line up later.

Uploading a File

putFile() uploads a local file (typically a Uri from the camera or gallery, the same photoUri concept used on the Recipe class) and returns an UploadTask, which supports progress, success, and failure listeners.

Uri localFileUri = Uri.fromFile(new File(localPhotoPath));  // the photo just taken/picked

UploadTask uploadTask = photoRef.putFile(localFileUri);

uploadTask.addOnProgressListener(taskSnapshot -> {
    double percent = (100.0 * taskSnapshot.getBytesTransferred()) / taskSnapshot.getTotalByteCount();
    Log.d("RecipeBox", "Upload progress: " + percent + "%");
}).addOnFailureListener(e -> {
    Log.e("RecipeBox", "Photo upload failed", e);
}).addOnSuccessListener(taskSnapshot -> {
    Log.d("RecipeBox", "Photo uploaded successfully");
    // fetch the download URL next — see below
});
Explanation:
  • addOnProgressListener() is useful for showing an upload progress bar for larger photo files.
  • Uploads happen off the main thread automatically — no manual ExecutorService needed here, unlike the Room writes covered earlier.

Getting the Download URL

A successful upload doesn't hand back a usable URL by itself — that requires one more call, getDownloadUrl(), on the completed task's storage reference.

uploadTask.addOnSuccessListener(taskSnapshot -> {
    taskSnapshot.getStorage().getDownloadUrl().addOnSuccessListener(downloadUri -> {
        String photoUrl = downloadUri.toString();

        // Save the URL string on the Recipe document in Firestore (previous lesson)
        firestoreDb.collection("recipes").document(recipeId)
            .update("photoUri", photoUrl);
    });
});
Explanation: This is the key idea tying Firestore and Storage together: the recipe's data (title, ingredients, steps, category) lives in a Firestore document, but the recipe's photo file lives in Storage — Firestore just keeps a plain string URL pointing at it. Neither service stores the other's kind of data.

Displaying the Uploaded Photo Later

Once a recipe's photoUri field holds a Storage download URL, showing it is no different from loading any other image URL — an image-loading library (such as Glide or Picasso, covered conceptually alongside ImageView & ImageButton) can load it straight into an ImageView without any Storage-specific code.

Glide.with(context)
    .load(recipe.photoUri)   // the download URL saved earlier
    .into(recipeImageView);

Storage Security Rules

Just as Firestore Security Rules are the real enforcement behind "a user can only access their own recipes," Storage Security Rules enforce the same principle for files, evaluated on Google's servers for every upload and download request.

rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {
    match /recipe_photos/{allPaths=**} {
      allow read, write: if request.auth != null;
    }
  }
}
Explanation: This baseline rule requires the caller to be signed in at all for any access under recipe_photos/. A stricter version could check the file's path against the caller's UID, mirroring exactly the resource.data.ownerUid check used in Firestore's rules — the principle is identical, only the syntax differs slightly between the two products.
With recipe data in Firestore and recipe photos in Storage, RecipeBox's Firebase integration is complete. The next section shifts to Testing & Debugging, starting with Logging & Logcat — the most basic, most-used tool for understanding what an app is actually doing.