Search code examples
androidandroid-cameraandroid-camera2android-hardware

Android Camera2 app breaks after leaving camera activity and re-entering


I'm trying to implement a simple camera photo capture app based on the tutorial located here: https://android.jlelse.eu/the-least-you-can-do-with-camera2-api-2971c8c81b8b

The app works great the first time. The preview shows, and I can take pictures which save correctly.

My issue is that when I close the app, hit back, minimize the app, etc... and then return to the camera activity, the preview no longer shows anything, and the app crashes when I try to take a photo.

I'm almost positive that I'm not shutting down the camera correctly in some way, but I've combed through my code line by line compared to a working implementation, and I can't see what I'm doing differently/wrong.

package com.example.cameratest;

import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.graphics.SurfaceTexture;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.os.Environment;
import android.os.Handler;
import android.os.HandlerThread;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Size;
import android.view.Display;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Toast;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.Locale;

public class SecondCamera extends AppCompatActivity {

    // Define the variables we need to use the camera

    // Our request code can be anything. I like 8.
    private static final int CAMERA_REQUEST_CODE = 8;
    private CameraManager cameraManager;
    private int cameraFacing;
    private TextureView.SurfaceTextureListener surfaceTextureListener;
    private String cameraId;
    private Size previewSize;
    private CameraDevice cameraDevice;
    private TextureView textureView;
    private CameraCaptureSession cameraCaptureSession;

    private HandlerThread backgroundThread;
    private Handler backgroundHandler;

    private CameraDevice.StateCallback stateCallback;

    private CaptureRequest.Builder captureRequestBuilder;
    private CaptureRequest captureRequest;
    private File galleryFolder;
    private WindowManager windowManager;
    private int windowHeight;
    private int windowWidth;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second_camera);

        // Get the size of our display in order to properly scale the camera view
        Display display = getWindowManager().getDefaultDisplay();
        Point size = new Point();
        display.getSize(size);
        windowHeight = size.y;
        windowWidth = size.x;

        textureView = (TextureView) findViewById(R.id.cameraTextureView);

        // Let's ask for permission to use the camera feature
        ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}, CAMERA_REQUEST_CODE);
        // Get the camera system service
        cameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
        // Make sure we use the back camera
        cameraFacing = CameraCharacteristics.LENS_FACING_BACK;

        // Set up a listener to communicate to our TextureView
        surfaceTextureListener = new TextureView.SurfaceTextureListener(){
            @Override
            public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
                setUpCamera();
                openCamera();
            }

            @Override
            public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) {

            }

            @Override
            public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
                return false;
            }

            @Override
            public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {

            }
        };

        textureView.setSurfaceTextureListener(surfaceTextureListener);

        // Manage the three states of our CameraDevice. Opened, Closed, and Error
        stateCallback = new CameraDevice.StateCallback(){

            @Override
            public void onOpened(@NonNull CameraDevice cameraDevice) {
                // Grab our camera device and start up the preview
                SecondCamera.this.cameraDevice = cameraDevice;
                createPreviewSession();
                createImageGallery();
            }

            @Override
            public void onDisconnected(@NonNull CameraDevice cameraDevice) {
                // Close and disconnect the cameraDevice
                cameraDevice.close();
                SecondCamera.this.cameraDevice = null;
            }

            @Override
            public void onError(@NonNull CameraDevice cameraDevice, int i) {
                // Close and disconnect the cameraDevice
                cameraDevice.close();
                SecondCamera.this.cameraDevice = null;
            }
        };
    }

    @Override
    protected void onResume(){
        super.onResume();
        openBackgroundThread();
        // If our texture view is available, let's set up and open the camera on it.
        if(textureView.isAvailable()){
            setUpCamera();
            openCamera();
        } else {
            // If not, we need to set up the SurfaceTextureListener,
            // which will do the same once the texture view is available
            textureView.setSurfaceTextureListener(surfaceTextureListener);
        }
    }

    @Override
    protected void onPause(){
        // Close the Camera and Background Thread to avoid memory leakage
        closeBackgroundThread();
        closeCamera();
        super.onPause();
    }

    /**
     * Close the CameraCaptureSession and CameraDevice
     */
    private void closeCamera(){
        if (cameraCaptureSession != null){
            cameraCaptureSession.close();
            cameraCaptureSession = null;
        }
        if (cameraDevice != null){
            cameraDevice.close();
            cameraDevice = null;
        }
    }

    /**
     * Shut down our backgroundThread and handler
     */
    private void closeBackgroundThread(){
        backgroundThread.quitSafely();
        try {
            backgroundThread.join();
            backgroundThread = null;
            backgroundHandler = null;
        } catch (InterruptedException ie){
            ie.printStackTrace();
        }
    }

    /**
     * Open a thread in the background in order to run the camera
     */
    private void openBackgroundThread(){
        backgroundThread = new HandlerThread("Camera Background Thread");
        backgroundThread.start();
        backgroundHandler = new Handler(backgroundThread.getLooper());
    }

    /**
     * Establish the camera to use, and get information to scale our preview correctly
     */
    private void setUpCamera(){
        try{
            for (String cameraID: cameraManager.getCameraIdList()){
                CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraID);
                // If we find the appropriate camera:
                if (characteristics.get(CameraCharacteristics.LENS_FACING) == cameraFacing){
                    // Get the preview size we need, and set this class's camera ID
                    StreamConfigurationMap streamConfigurationMap = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
                    // The first element in the list of output sizes is the highest resolution one.
                    // Get a Size object which is prepared for a SurfaceTexture.
                    Size[] possibleSizes = streamConfigurationMap.getOutputSizes(SurfaceTexture.class);
                    previewSize = chooseOptimalSize(possibleSizes, windowWidth, windowHeight);
                    this.cameraId = cameraID;
                }
            }
            if (this.cameraId == null){
                Toast.makeText(this, "ERROR, NO CAMERA FOUND", Toast.LENGTH_SHORT).show();
            }
        } catch (CameraAccessException cae){
            cae.printStackTrace();
        }
    }

    /**
     * Attempt to open the camera in the background thread
     * if we find that we have been granted permission.
     */
    private void openCamera(){
        try {
            if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED){
                cameraManager.openCamera(cameraId, stateCallback, backgroundHandler);
            }
            else {
                Toast.makeText(this, "Camera Permissions must be enabled for this activity to function", Toast.LENGTH_SHORT).show();
            }
        } catch (CameraAccessException cae){
            cae.printStackTrace();
        }
    }

    /**
     * Initialize a preview for our camera screen, so the user can
     * see in real time what the camera sees.
     */
    private void createPreviewSession(){
        try {
            // Get the surfaceTexture out of our textureView. This is what we stream to
            SurfaceTexture surfaceTexture = textureView.getSurfaceTexture();
            // We want to set our size correctly based on the preview Size from before
            surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight());
            // Set up a Surface based on our Surface Texture
            Surface previewSurface = new Surface(surfaceTexture);
            // We want to build a request for a preview style stream, and send it to our surface
            captureRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
            captureRequestBuilder.addTarget(previewSurface);

            cameraDevice.createCaptureSession(Collections.singletonList(previewSurface), new CameraCaptureSession.StateCallback() {
                @Override
                public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
                    if (cameraDevice != null){
                        try {
                            captureRequest = captureRequestBuilder.build();
                            SecondCamera.this.cameraCaptureSession = cameraCaptureSession;
                            SecondCamera.this.cameraCaptureSession.setRepeatingRequest(captureRequest, null, backgroundHandler);
                        } catch (CameraAccessException cae) {
                            cae.printStackTrace();
                        }
                    }
                }

                @Override
                public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) {
                    // Do nothing?
                }

            }, backgroundHandler);
        } catch (CameraAccessException cae){
            cae.printStackTrace();
        }
    }

    /**
     * Initialize a folder in our pictures library to save photos to
     */
    private void createImageGallery(){
        File storageDirectory = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
        galleryFolder = new File(storageDirectory, getResources().getString(R.string.app_name));
        if (!galleryFolder.exists()){
            boolean wasCreated = galleryFolder.mkdirs();
            if (!wasCreated){
                System.out.println("Failed to create directory " + galleryFolder.getPath());
            }
        }
    }

    /**
     * Create a temp file to store our image to
     */
    private File createImageFile(File galleryFolder) throws IOException {
        // Grab our timestamp and use it to create a unique image name
        String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmsss", Locale.getDefault()).format(new Date());
        String imageFilename = getResources().getString(R.string.app_name) + timeStamp;
        Toast.makeText(this, "Created image " + imageFilename + ".jpg", Toast.LENGTH_SHORT).show();
        return File.createTempFile(imageFilename, ".jpg", galleryFolder);

    }

    /**
     * Capture everything on the screen, and output it to our file
     */
    public void takePhoto(View view){
        try (FileOutputStream outputPhoto = new FileOutputStream(createImageFile(galleryFolder))) {
            lock();
            textureView.getBitmap().compress(Bitmap.CompressFormat.PNG, 100, outputPhoto);
        } catch (IOException ioe) {
            ioe.printStackTrace();
        } finally {
            unlock();
        }
    }

    /**
     * Lock our camera preview, as if the shutter of a camera
     */
    private void lock(){
        try {
            // Lock the screen for a second
            cameraCaptureSession.capture(captureRequestBuilder.build(), null, backgroundHandler);
        } catch (CameraAccessException cae){
            cae.printStackTrace();
        }
    }

    /**
     * Unlock our camera preview, allowing the user to see freely again
     */
    private void unlock(){
        try {
            // Go back to the repeating preview request
            cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(),
                    null, backgroundHandler);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

    /**
     * Do some calculations to figure out how to show the correct
     * aspect ratio for our camera preview
     *
     * @param outputSizes The array of possible output sizes
     * @param width The width of our device screen
     * @param height The height of our device screen
     * @return The size to set our display to
     */
    private Size chooseOptimalSize(Size[] outputSizes, int width, int height) {
        double preferredRatio = height / (double) width;
        Size currentOptimalSize = outputSizes[0];
        double currentOptimalRatio = currentOptimalSize.getWidth() / (double) currentOptimalSize.getHeight();
        for (Size currentSize : outputSizes) {
            double currentRatio = currentSize.getWidth() / (double) currentSize.getHeight();
            if (Math.abs(preferredRatio - currentRatio) <
                    Math.abs(preferredRatio - currentOptimalRatio)) {
                currentOptimalSize = currentSize;
                currentOptimalRatio = currentRatio;
            }
        }
        return currentOptimalSize;
    }

}

In the logs on crash, I see:

E/CameraDeviceState: Cannot call configure while in state: 0
E/AndroidRuntime: FATAL EXCEPTION: Camera Background Thread
    Process: com.example.cameratest, PID: 21313
    java.lang.IllegalStateException: Session has been closed; further changes are illegal.
        at android.hardware.camera2.impl.CameraCaptureSessionImpl.checkNotClosed(CameraCaptureSessionImpl.java:627)
        at android.hardware.camera2.impl.CameraCaptureSessionImpl.setRepeatingRequest(CameraCaptureSessionImpl.java:234)
        at com.example.cameratest.SecondCamera$3.onConfigured(SecondCamera.java:263)
        at java.lang.reflect.Method.invoke(Native Method)
        at android.hardware.camera2.dispatch.InvokeDispatcher.dispatch(InvokeDispatcher.java:39)
        at android.hardware.camera2.dispatch.HandlerDispatcher$1.run(HandlerDispatcher.java:65)
        at android.os.Handler.handleCallback(Handler.java:751)
        at android.os.Handler.dispatchMessage(Handler.java:95)
        at android.os.Looper.loop(Looper.java:154)
        at android.os.HandlerThread.run(HandlerThread.java:61)

This makes me believe now that there is something wrong with my CameraCaputreSession, but I have no idea what.

EDIT

Changing the line

cameraManager.openCamera(cameraId, stateCallback, backgroundHandler);

In my openCamera() method to pass in null as the third parameter instead of my backgroundHandler seems to have fixed it, but to be honest, I have absolutely no idea why. Could anyone explain this to me? Was this the right solution?


Solution

  • One possibility - you start up your background thread in onResume(), but you also try to open the camera in TextureViewListener's onSurfaceTextureAvailable.

    On the first start, you only hook up the TextureViewListener after setting up the background thread in onResume, but you never unregister that listener. On second start, it's possible the onSurfaceTextureAvailable callback is running before onResume(), because the app UI becomes visible in onStart(), not onResume().

    That'll open the camera with a null background thread (going to the main thread), and then onResume will fire, and open the camera a second time, evicting the first camera object from onSurfaceTextureAvailable. Resulting in errors when further operations are done on that first camera object.

    One fix would be to just unregister the TextureViewListener in onPaused, or restructure things so that getting an early onSurfaceTextureAvailable callback also sets up the background thread.