0

I am creating a custom camera app using surface view and Camera 1 API, there is some kind of problem in the lifecycle and i just cannot get it to work, but basically I am releasing the camera and then calling it again :

Camera is being used after Camera.release() was called

Ideally when back button is pressed should go back to previous activity, and when home button pressed should re open without any problem.

Would somebody guide me through a good implementation of this calls such as release and open the camera:

Custom Surface View Class:

public class ImageSurfaceView extends SurfaceView implements 
SurfaceHolder.Callback {
private Camera camera;
private SurfaceHolder surfaceHolder;
public final String TAG = ImageSurfaceView.class.getSimpleName();

public ImageSurfaceView(Context context, Camera camera) {
    super(context);
    this.camera = camera;
    this.surfaceHolder = getHolder();
    this.surfaceHolder.addCallback(this);
}


@Override
public void surfaceCreated(SurfaceHolder holder) {
    try {
        this.camera.setPreviewDisplay(holder);
        this.camera.startPreview();
    } catch (IOException ex){
        Log.e(TAG, "surfaceCreated: "+ex.getMessage() );
    }

}

@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

}

@Override
public void surfaceDestroyed(SurfaceHolder holder) {
    this.camera.stopPreview();
    this.camera.release();
    this.camera = null;
}

}

and my CameraAcitivty LifeCycle CallBacks:

@Override
protected void onRestart() {
    super.onRestart();
    camera = null;
    requestCamera();
}

@Override
protected void onResume() {
    super.onResume();
    try {
        requestCamera();
    } catch (RuntimeException ex){
        Log.e(TAG, "onResume: "+ex.getMessage() );
    }
}

@Override
protected void onPause() {
    super.onPause();
    if (camera != null) {
        camera.setPreviewCallback(null);
        imageSurfaceView.getHolder().removeCallback(imageSurfaceView);
        camera.release();
        camera = null;
    }
}

@Override
protected void onStop() {
    super.onStop();
    isSurfaceCreated = false;
}

@Override
protected void onDestroy() {
    super.onDestroy();
    releaseCameraAndPreview();
}

private void releaseCameraAndPreview(){

    if (camera != null) {
        camera.stopPreview();
        camera.release();
        camera = null;
    }
    if(imageSurfaceView != null){
        imageSurfaceView.destroyDrawingCache();
    }
}

private void requestCamera(){
    if (camera == null) {
        if (checkPermission()) {
            callCameraThread();
            Toast.makeText(getApplicationContext(), "Permission already granted", Toast.LENGTH_LONG).show();
        } else {
            requestPermission();
        }
    }
}

public Camera checkDeviceCamera(){
    Camera mCamera = null;
    try {
        mCamera = Camera.open(0);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return mCamera;
}

private void callCameraThread(){
    if(mThread == null){
        mThread = new CameraHandlerThread();
    }

    synchronized (mThread){
        mThread.openCamera();
    }
}

public synchronized void loadSurface(){
    while(camera == null){
        try {
            this.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    if(!isSurfaceCreated) {
        imageSurfaceView = new ImageSurfaceView(CameraActivity.this, camera);
        cameraPreviewLayout.addView(imageSurfaceView);
        isSurfaceCreated = true;
        imgGhost = new ImageView(this);
        imgGhost.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
        imgGhost.setBackground(ContextCompat.getDrawable(this, R.drawable.fantasma_doc_front));
        cameraPreviewLayout.addView(imgGhost);
    }
}

private CameraHandlerThread mThread = null;
private class CameraHandlerThread extends HandlerThread {
    Handler mHandler = null;

    CameraHandlerThread(){
        super("CameraHandlerThread");
        start();
        mHandler = new Handler(getLooper());
    }

    synchronized  void notifyCameraOpened(){
        notifyAll();
    }

    void openCamera(){
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                camera = checkDeviceCamera();
                notifyCameraOpened();
            }
        });
        try{
            wait();
        } catch (InterruptedException ex){
            Log.e(TAG, "openCamera: "+"wait was interrupted" );
        }
    }
}

I dont know how to handle properly the releasing of camera and creation of surface View, now after some changes I did, I am able to press home button and come back without problem, but onBackPressed crashes, before was the opposite, Thanks in advance for any help in this matter

Jhonycage
  • 759
  • 3
  • 16
  • 36
  • 1
    The simple lifecycle is to open camera onResume and release it onPause .All improvements for this must not change this basic flow. I don't think you need wait() in openCamera(), your UI thread can continue while the background HandlerThread opens the camera. – Alex Cohn Sep 20 '17 at 05:20
  • thanks for that clarification, helped me a lot to solve the issue – Jhonycage Sep 21 '17 at 04:46
  • The reason why I wait is because the surfaceView will throw exception if the camera is null , I'm not sure if is 100% correct but in my case – Jhonycage Sep 23 '17 at 20:50
  • That's true, your implementation of **ImageSurfaceView** is not prepared to accept **camera == null** in constructor. But you can simply post the request to **loadSurface()** on UI thread from ** CameraHandlerThread. notifyCameraOpened()**. – Alex Cohn Sep 24 '17 at 08:17
  • Another issue: you hold two references to Camera: in **ImageSurfaceView** and in **Activity**. When you release the camera, it can happen that **surfaceDestroyed** will be invoked after **onPause**. I prefer to always use the same **releaseCameraAndPreview()** (or equivalent), and have it synchronized (e.g. on **Activity.this**). – Alex Cohn Sep 24 '17 at 08:25
  • 1
    I got it to work, although I am not certain it is the best way, I will post code tomorrow, also your words are very interesting, I will get into that tomorrow as well, perhaps I am not understanding them all the way through, but could you give a short example of how to use the notifyCameraOpened() and regarding the second Issue releaseCameraAndPreview() , surfaceDestroyed was definitely being called after onPause(), but I did not solve it using a Synchronized method for that, would be very nice to see that approach also. Thanks Alex – Jhonycage Sep 24 '17 at 18:59
  • 1
    Hi @AlexCohn, I posted my answer, I hope you could have a look and give some feedback, I would like to ask for some example of how to implement your suggested solutions as my interest is in the best practices. – Jhonycage Sep 29 '17 at 14:35

1 Answers1

0

so, pretty much I tried to minimize duplicated reference to the camera, here is how onBackPressed looks like:

@Override
public void onBackPressed() {
    if (isPreviewing) {  //if previewing layout is on screen, then I want to return to the camera, without saving the picture
        rlPicturePreview.setVisibility(View.GONE);
        llCameraControl.setVisibility(View.VISIBLE);
        isPreviewing = false;
        isPictureProcessing = false;
        camera.startPreview();
    } else {
        if(!isPictureProcessing) {   //Otherwise if there is no process happening I want to go back to previous calling activity, therefore closing the camera
            previousDataExists = false;
            isSurfaceCreated = false;
            DriverFragment.docPicturesArray.clear();
            CameraDocActivity.this.finish();
        }
    }
}

This is how onResume() looks like now, I added support for most modern API's tested in lollipop, Marshmallow and Nougat

@Override
protected void onResume() {
    super.onResume();
    try {
        if(Build.VERSION.SDK_INT < Build.VERSION_CODES.M){
            callCameraThread();
            loadSurface();
        } else {
            requestCamera();
        }
    } catch (RuntimeException ex){
        Log.e(TAG, "onResume: "+ex.getMessage() );
    }
}

in onRestart just remove the requestCamera():

@Override
protected void onRestart() {
    super.onRestart();
    camera = null;
}

and lastly a null validation in onSurfaceDestroyed():

@Override
public void surfaceDestroyed(SurfaceHolder holder) {
    if(this.camera != null) {
        this.camera.stopPreview();
        this.camera.release();
        this.camera = null;
    }

Now it doesn't have any problem when home button is pressed, or if back button is pressed, I make sure that camera is set to null everytime it passes trhough onRestart(), after closed, and in onStop i make sure the flag isSurfaceCreated is set to false.

Jhonycage
  • 759
  • 3
  • 16
  • 36
  • This definitely looks better now, but I don't know what CameraDocActivity is and why you kill it in onBackPressed – Alex Cohn Sep 30 '17 at 17:40
  • CameraDocActivity is the activity hosting the camera when back button is pressed behaviour should be close the camera ignoring any taken pictures happened to be saved and return to a fragment who's the one calling without any data in docPicturesArray – Jhonycage Oct 01 '17 at 04:42
  • I have another button which confirms t he taken pictures and notify through an interface – Jhonycage Oct 01 '17 at 04:43
  • The other activity will have been put on pause if your CameraActivity handles input (and receives onBackPressed()). On the other hand, if it runs some picture processing (and save to disk) in the background, **finish()** won't interrupt it. **finish()** only marks an activity to be closed, asynchronously. – Alex Cohn Oct 01 '17 at 07:35