Capturing photos with camera API in Android
Modern Android apps can send users to an external camera activity or control the hardware directly. The direct route exposes focus, exposure, image format and orientation controls, so it suits specialised capture workflows and developers who want to understand the camera pipeline. CameraX is usually quicker for production features, while Camera2 gives explicit control over cameras, capture requests and output surfaces.
A reliable flow must also handle denied permission, full storage and rotation without leaking resources. The example below combines a Camera2 preview with an ImageReader and saves a JPEG through MediaStore. It fits the Android phones commonly sold through Australian retailers and apps distributed through Google Play, while remaining usable where mobile coverage is limited.
Set up the project
Create an Android Studio project in Kotlin using AndroidX Activity. Put the camera code in a helper class rather than the activity, and give the screen a preview container plus a capture button. Keep all ImageReader and CameraDevice callbacks on a background HandlerThread.
Add these manifest declarations:
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature
android:name="android.hardware.camera.any"
android:required="false" />
Treat the camera feature as optional if the app has another useful function; otherwise mark it required. Avoid broad storage permissions for modern gallery access, and choose a minimum SDK that matches your actual test devices. The deprecated android.hardware.Camera class is unsuitable for new work, so use Camera2 or CameraX instead.
Request camera permission at runtime
CAMERA is a dangerous permission, so a manifest entry alone does not permit access. Explain why the app needs it, then request it when the user opens the camera screen. If the user has selected “Don’t ask again,” show a clear path to system settings instead of repeatedly requesting permission.
private val requestCamera =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
if (granted) cameraHelper.open() else showPermissionMessage()
}
if (checkSelfPermission(Manifest.permission.CAMERA) ==
PackageManager.PERMISSION_GRANTED
) {
cameraHelper.open()
} else {
requestCamera.launch(Manifest.permission.CAMERA)
}
Treat denial as a normal UI state and keep the rest of the screen usable. If the app also offers a gallery import, that feature can continue without camera access. Revoke any temporary URI grants when a share flow finishes.
Choose where photos are stored
On Android 10 and later, insert into MediaStore and place it in Pictures/AndroidTutorials. Write the file while IS_PENDING is set, then clear that value after the bytes are safely written. This prevents gallery apps from showing a half-written image and avoids legacy storage permissions.
For Android 9 and earlier, save to the app-specific external Pictures directory. Share it with FileProvider rather than a file:// URI, and revoke temporary URI access after sharing. In both cases, create a unique JPEG name and persist the final content URI so the activity can reopen the photo later.
Treat EXIF data as part of the privacy decision. GPS coordinates can reveal a home, a school, or a visit to a sensitive location. Strip location metadata unless the product genuinely needs it and the user understands that choice.
Connect the preview and capture request
Camera2 requires several objects to agree on one session. Inspect CameraCharacteristics to select the back-facing camera and its JPEG output size, create a TextureView or SurfaceView preview, and pair it with an ImageReader configured for JPEG images. The camera may expose several sizes, so choose one supported by both surfaces.
The three common capture paths suit different levels of control:
| API path | Control and scope | Best fit | Complexity |
|---|---|---|---|
| Camera2 | Direct camera pipeline with fine-grained control | New apps needing explicit camera behaviour | High |
| CameraX | Consistent camera abstraction with many built-in use cases | Most new camera features | Low |
| External capture intent | Delegates capture to another installed camera | Simple photo features with minimal code | Very low |
After the user presses the shutter, build a TEMPLATE_STILL_CAPTURE request with the preview and ImageReader surfaces as targets. Supply a JPEG orientation derived from the sensor orientation and current display rotation. A capture session then coordinates the preview stream and still image without blocking the main thread.
Handle CameraDevice.StateCallback failures by closing the session, releasing the reader and offering a retry. Camera access can fail when another app holds the camera, so give the error path as much attention as the successful callback.
Save images and recover from failures
ImageReader delivers the JPEG as bytes on its background handler. Create the MediaStore row first, open an output stream for its URI, and write the complete byte array. The following core snippet applies inside an Android 10-and-later branch:
val values = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, "capture_${System.currentTimeMillis()}.jpg")
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
put(MediaStore.Images.Media.RELATIVE_PATH, "${Environment.DIRECTORY_PICTURES}/AndroidTutorials")
put(MediaStore.Images.Media.IS_PENDING, 1)
}
val uri = contentResolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
values
) ?: throw IOException("Could not create image entry")
contentResolver.openOutputStream(uri)?.use { it.write(jpegBytes) }
?: throw IOException("Could not open image output")
values.clear()
values.put(MediaStore.Images.Media.IS_PENDING, 0)
contentResolver.update(uri, values, null, null)
After a successful write, update the row with IS_PENDING set to zero. If writing fails, delete the incomplete row and remove the ImageReader listener. Keep the original buffer untouched until the stream closes, and report a recoverable message such as “Couldn’t save photo” rather than leaving the user with a silent failure.
A small bitmap thumbnail can be generated from the captured bytes for the gallery preview, but retain the original JPEG for sharing and editing. Recompress only when you deliberately need a smaller file; repeated compression can reduce detail and increase storage use.
Handle rotation, lifecycle and release checks
Calculate JPEG orientation once for each capture and recompute it after a configuration change. Lock the activity to portrait if your layout cannot support both orientations, or rebuild the preview transform so the image is not stretched. For unusual OEM builds that resize the activity surface, the supplied window position guidance provides additional coordinate-related behaviour to check.
Open the camera only while the screen is active, and close the CameraDevice, session, ImageReader and background thread in the matching stop or destroy callback. Releasing these resources prevents another app from being denied access and avoids callbacks arriving after the screen has gone. Test a locked device, app backgrounding and a rotation during capture.
Run device tests under Australian conditions: strong midday sun around Bondi or Melbourne’s Royal Botanic Gardens, dim pubs, and dusty outdoor scenes near Uluru. Check autofocus, exposure and white balance rather than assuming they behave like an indoor emulator. Saving locally matters on regional trips where reception around the Blue Mountains can be unreliable.
Use Australian English in visible copy, and respect the device’s locale and 12/24-hour preference when showing capture times; “arvo” is familiar locally, but timestamps should remain unambiguous. Before release, remove unnecessary EXIF location data, document the camera permission rationale, and test the production build distributed through Google Play. A camera flow that opens quickly, stores the original JPEG and releases hardware cleanly is ready for everyday Australian use.