Files: Internal & External Storage (Java)
SharedPreferences is fine for small key/value settings, but RecipeBox also needs to save actual files — most importantly, a photo the user snaps or picks for a recipe's photoUri. Android gives every app two private-ish storage areas for exactly this: internal storage and app-specific external storage. Neither requires the user to grant a runtime permission, and both are wiped clean automatically when the app is uninstalled.
Internal Storage
getFilesDir() — always private, never visible to other apps
App-Specific External
getExternalFilesDir() — on the SD/shared partition, still app-only
Shared / MediaStore
Visible in the user's Gallery, survives uninstall
Internal Storage: getFilesDir()
Every app is given a private directory on internal storage that only that app can read or write — not even the user can browse it without root access, and no other app can touch it. It is automatically deleted the moment the user uninstalls RecipeBox. This is the right place for data that should never leak outside the app: a cached copy of a recipe photo, a temporary export file, or anything sensitive.
// Saving a recipe photo to internal storage
public void saveRecipePhotoInternal(Context context, int recipeId, byte[] photoBytes) {
String fileName = "recipe_" + recipeId + ".jpg";
File file = new File(context.getFilesDir(), fileName);
try (FileOutputStream out = new FileOutputStream(file)) {
out.write(photoBytes);
Log.d("RecipeBox", "Saved photo to " + file.getAbsolutePath());
} catch (IOException e) {
Log.e("RecipeBox", "Failed to save photo", e);
}
}
- context.getFilesDir() returns a File pointing at /data/data/<package>/files.
- The try-with-resources block automatically closes the stream, even if write() throws.
- No manifest permission is needed — this directory always belongs to RecipeBox alone.
App-Specific External Storage: getExternalFilesDir()
External storage used to mean the shared SD card, which required a permission and was readable by other apps. Modern Android splits this: app-specific external storage (returned by context.getExternalFilesDir(null)) still lives on the external/shared partition — so it's a good spot for larger files like full-resolution recipe photos — but it is scoped to RecipeBox, requires no storage permission on modern Android, and is deleted automatically on uninstall, just like internal storage. Think of it as a middle ground: bigger and faster to fill than internal storage, but still private to the app.
| Storage | Private to app? | Deleted on uninstall? | Permission needed? |
|---|---|---|---|
| getFilesDir() | Yes | Yes | No |
| getExternalFilesDir() | Yes | Yes | No (modern Android) |
| Shared / MediaStore | No — visible to user & other apps | No | Runtime permission for legacy access; MediaStore API needs none for writes |
Scoped Storage & MediaStore (API 29+)
Since Android 10 (API 29), apps can no longer freely roam the shared external storage the way they once could — this is called scoped storage. If RecipeBox wants to save a photo somewhere the user's own Gallery app will actually show it (rather than hiding it inside RecipeBox's private folder), the correct tool is the MediaStore API, not a raw file path.
// Saving a recipe photo into the shared Pictures collection via MediaStore
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DISPLAY_NAME, "recipebox_" + recipeId + ".jpg");
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
values.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/RecipeBox");
Uri collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
Uri photoUri = resolver.insert(collection, values);
try (OutputStream out = resolver.openOutputStream(photoUri)) {
out.write(photoBytes);
}
- resolver.insert() asks the system to create a new Gallery entry and hand back a content:// Uri.
- RecipeBox never touches a raw file path — the system manages where the bytes physically live.
- No storage permission is required to write a new file this way, even on modern Android.
Reading a File Back: FileInputStream
Whichever directory you chose, reading a file back follows the same pattern in reverse — open an input stream, read the bytes, close the stream. Here is a complete round trip using RecipeBox's app-specific external storage, which is the most common choice for a recipe's photo.
// Save a recipe photo to app-specific external storage
public File saveRecipePhotoExternal(Context context, int recipeId, byte[] photoBytes) throws IOException {
File dir = context.getExternalFilesDir("recipe_photos");
if (dir != null && !dir.exists()) {
dir.mkdirs();
}
File file = new File(dir, "recipe_" + recipeId + ".jpg");
try (FileOutputStream out = new FileOutputStream(file)) {
out.write(photoBytes);
}
return file;
}
// Read it back later, e.g. to display in an ImageView
public byte[] readRecipePhoto(File file) throws IOException {
try (FileInputStream in = new FileInputStream(file)) {
byte[] buffer = new byte[(int) file.length()];
int bytesRead = in.read(buffer);
Log.d("RecipeBox", bytesRead + " bytes read from " + file.getName());
return buffer;
}
}
- getExternalFilesDir("recipe_photos") gives a subfolder dedicated to photos, keeping things tidy.
- file.length() tells us exactly how many bytes to allocate for the read buffer.
- The resulting File's path (or a wrapped content:// Uri) is what gets stored in the Recipe.photoUri field.
Choosing the Right Storage for RecipeBox
| RecipeBox need | Recommended storage | Why |
|---|---|---|
| Recipe photo, private thumbnail cache | App-specific external (getExternalFilesDir()) | Larger capacity, still private, auto-cleaned |
| Small internal cache/export file | Internal (getFilesDir()) | Guaranteed private, no permission ever needed |
| Let user see/share recipe photo in Gallery | MediaStore | Visible to Gallery and other apps, survives uninstall |
| User preferences (sort order, theme) | SharedPreferences | Simple key/value, not a file at all |
| Structured recipe data (title, ingredients, steps) | Database (next lesson) | Needs querying, relationships, and updates — not raw files |