Camera2 & CameraX (Java)

RecipeBox's recipe editor lets a user attach a photo of their finished dish (stored as the Recipe class's photoUri field). Getting that photo means talking to the device's camera, and Android gives two very different ways to do that.

Camera2: Powerful but Painful

Camera2 is Android's low-level camera API. It exposes almost everything the hardware can do — manual focus, exposure, per-frame capture sessions — but that power comes at a real cost. Camera2 is notoriously inconsistent across device manufacturers: a capture session configured one way behaves correctly on a Pixel and subtly wrong on a budget phone from a different vendor, orientation and rotation handling has to be worked out by hand, and getting a reliable preview-plus-capture pipeline working across the wide spread of real Android hardware historically took a lot of device-specific workaround code. It's honest to say most apps that don't need Camera2's fine-grained control are better off avoiding it directly.

CameraX: Built on Camera2, Smoothing It Over

CameraX is a Jetpack library built on top of Camera2 that deliberately absorbs those device inconsistencies so RecipeBox's code doesn't have to. It exposes a small set of use cases — Preview, ImageCapture, ImageAnalysis — and handles the device-specific quirks internally. For a straightforward "take a photo of your dish" screen, CameraX is the right choice.

Adding the Dependency

// app/build.gradle
dependencies {
    implementation 'androidx.camera:camera-core:1.3.4'
    implementation 'androidx.camera:camera-camera2:1.3.4'
    implementation 'androidx.camera:camera-lifecycle:1.3.4'
    implementation 'androidx.camera:camera-view:1.3.4'
}

PreviewView in the Layout

<!-- activity_capture_dish.xml -->
<androidx.camera.view.PreviewView
    android:id="@+id/previewView"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

<Button
    android:id="@+id/btnCapture"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom|center_horizontal"
    android:text="Capture Dish Photo" />

The CAMERA Runtime Permission

Like location in the earlier lesson, camera access is a runtime-dangerous permission covered by the Runtime Permissions lesson. RecipeBox must request and receive Manifest.permission.CAMERA before binding any CameraX use case — attempting to bind without it throws a SecurityException.

Binding Preview & ImageCapture to the Lifecycle

// CaptureDishActivity.java (CAMERA permission already granted)
private ImageCapture imageCapture;

private void startCamera() {
    ListenableFuture<ProcessCameraProvider> cameraProviderFuture =
            ProcessCameraProvider.getInstance(this);

    cameraProviderFuture.addListener(() -> {
        try {
            ProcessCameraProvider cameraProvider = cameraProviderFuture.get();

            Preview preview = new Preview.Builder().build();
            PreviewView previewView = findViewById(R.id.previewView);
            preview.setSurfaceProvider(previewView.getSurfaceProvider());

            imageCapture = new ImageCapture.Builder().build();

            CameraSelector cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA;

            cameraProvider.unbindAll();
            cameraProvider.bindToLifecycle(
                    this, cameraSelector, preview, imageCapture);

        } catch (ExecutionException | InterruptedException e) {
            Log.e("RecipeBoxCamera", "Camera init failed", e);
        }
    }, ContextCompat.getMainExecutor(this));
}
Why lifecycle-binding matters: passing this (the Activity, a LifecycleOwner) to bindToLifecycle() means CameraX automatically starts the camera when the Activity reaches STARTED and stops/releases it on STOP — RecipeBox never has to manually open or close the camera in onResume()/onPause(), and there's no risk of leaving the camera hardware locked if the user navigates away mid-preview.

Capturing a Photo & Saving via FileProvider

The Content Providers lesson covers FileProvider in detail; here it's used to get a safe, shareable URI for the captured photo file rather than exposing a raw file path.

private void captureDishPhoto() {
    File photoFile = new File(getExternalFilesDir(Environment.DIRECTORY_PICTURES),
            "recipe_dish_" + System.currentTimeMillis() + ".jpg");

    ImageCapture.OutputFileOptions outputOptions =
            new ImageCapture.OutputFileOptions.Builder(photoFile).build();

    imageCapture.takePicture(outputOptions, ContextCompat.getMainExecutor(this),
            new ImageCapture.OnImageSavedCallback() {
                @Override
                public void onImageSaved(ImageCapture.OutputFileResults output) {
                    Uri photoUri = FileProvider.getUriForFile(
                            CaptureDishActivity.this,
                            "com.codingwithsonu.recipebox.fileprovider",
                            photoFile);
                    saveUriToRecipe(photoUri); // sets Recipe.photoUri
                }

                @Override
                public void onError(ImageCaptureException exception) {
                    Log.e("RecipeBoxCamera", "Capture failed", exception);
                }
            });
}
Explanation: FileProvider.getUriForFile() wraps the saved JPEG in a content:// URI, which is what gets stored in Recipe.photoUri and later handed to an ImageView or shared with another app — the same pattern the Content Providers lesson explains for sharing files safely.

Rotation Handled Automatically

One more genuinely painful part of raw Camera2 used to be figuring out sensor orientation versus device rotation by hand — get it wrong and photos come out sideways depending on how the phone was held. CameraX detects the device's rotation and the camera sensor's orientation together and corrects the saved image automatically, so a dish photo taken in portrait or landscape on any supported device comes out right-side up without RecipeBox writing any rotation-matrix code itself.

CAMERA permission

granted at runtime

→

bindToLifecycle()

Preview + ImageCapture

→

takePicture()

saves JPEG file

→

FileProvider

content:// URI → Recipe.photoUri

With a dish photo captured, the next lesson looks at playing media back inside RecipeBox — see ExoPlayer (Java) for a recipe's video-instructions feature.