Home | History | Annotate | Download | only in cameratoo
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.example.android.camera2.cameratoo;
     18 
     19 import android.app.Activity;
     20 import android.graphics.ImageFormat;
     21 import android.hardware.camera2.CameraAccessException;
     22 import android.hardware.camera2.CameraCharacteristics;
     23 import android.hardware.camera2.CameraCaptureSession;
     24 import android.hardware.camera2.CameraDevice;
     25 import android.hardware.camera2.CameraManager;
     26 import android.hardware.camera2.CaptureFailure;
     27 import android.hardware.camera2.CaptureRequest;
     28 import android.hardware.camera2.TotalCaptureResult;
     29 import android.hardware.camera2.params.StreamConfigurationMap;
     30 import android.media.Image;
     31 import android.media.ImageReader;
     32 import android.os.Bundle;
     33 import android.os.Environment;
     34 import android.os.Handler;
     35 import android.os.HandlerThread;
     36 import android.os.Looper;
     37 import android.util.Size;
     38 import android.util.Log;
     39 import android.view.Surface;
     40 import android.view.SurfaceHolder;
     41 import android.view.SurfaceView;
     42 import android.view.View;
     43 
     44 import java.io.File;
     45 import java.io.FileNotFoundException;
     46 import java.io.FileOutputStream;
     47 import java.io.IOException;
     48 import java.nio.ByteBuffer;
     49 import java.util.ArrayList;
     50 import java.util.Arrays;
     51 import java.util.Collections;
     52 import java.util.Comparator;
     53 import java.util.List;
     54 
     55 /**
     56  * A basic demonstration of how to write a point-and-shoot camera app against the new
     57  * android.hardware.camera2 API.
     58  */
     59 public class CameraTooActivity extends Activity {
     60     /** Output files will be saved as /sdcard/Pictures/cameratoo*.jpg */
     61     static final String CAPTURE_FILENAME_PREFIX = "cameratoo";
     62     /** Tag to distinguish log prints. */
     63     static final String TAG = "CameraToo";
     64 
     65     /** An additional thread for running tasks that shouldn't block the UI. */
     66     HandlerThread mBackgroundThread;
     67     /** Handler for running tasks in the background. */
     68     Handler mBackgroundHandler;
     69     /** Handler for running tasks on the UI thread. */
     70     Handler mForegroundHandler;
     71     /** View for displaying the camera preview. */
     72     SurfaceView mSurfaceView;
     73     /** Used to retrieve the captured image when the user takes a snapshot. */
     74     ImageReader mCaptureBuffer;
     75     /** Handle to the Android camera services. */
     76     CameraManager mCameraManager;
     77     /** The specific camera device that we're using. */
     78     CameraDevice mCamera;
     79     /** Our image capture session. */
     80     CameraCaptureSession mCaptureSession;
     81 
     82     /**
     83      * Given {@code choices} of {@code Size}s supported by a camera, chooses the smallest one whose
     84      * width and height are at least as large as the respective requested values.
     85      * @param choices The list of sizes that the camera supports for the intended output class
     86      * @param width The minimum desired width
     87      * @param height The minimum desired height
     88      * @return The optimal {@code Size}, or an arbitrary one if none were big enough
     89      */
     90     static Size chooseBigEnoughSize(Size[] choices, int width, int height) {
     91         // Collect the supported resolutions that are at least as big as the preview Surface
     92         List<Size> bigEnough = new ArrayList<Size>();
     93         for (Size option : choices) {
     94             if (option.getWidth() >= width && option.getHeight() >= height) {
     95                 bigEnough.add(option);
     96             }
     97         }
     98 
     99         // Pick the smallest of those, assuming we found any
    100         if (bigEnough.size() > 0) {
    101             return Collections.min(bigEnough, new CompareSizesByArea());
    102         } else {
    103             Log.e(TAG, "Couldn't find any suitable preview size");
    104             return choices[0];
    105         }
    106     }
    107 
    108     /**
    109      * Compares two {@code Size}s based on their areas.
    110      */
    111     static class CompareSizesByArea implements Comparator<Size> {
    112         @Override
    113         public int compare(Size lhs, Size rhs) {
    114             // We cast here to ensure the multiplications won't overflow
    115             return Long.signum((long) lhs.getWidth() * lhs.getHeight() -
    116                     (long) rhs.getWidth() * rhs.getHeight());
    117         }
    118     }
    119 
    120     /**
    121      * Called when our {@code Activity} gains focus. <p>Starts initializing the camera.</p>
    122      */
    123     @Override
    124     protected void onResume() {
    125         super.onResume();
    126 
    127         // Start a background thread to manage camera requests
    128         mBackgroundThread = new HandlerThread("background");
    129         mBackgroundThread.start();
    130         mBackgroundHandler = new Handler(mBackgroundThread.getLooper());
    131         mForegroundHandler = new Handler(getMainLooper());
    132 
    133         mCameraManager = (CameraManager) getSystemService(CAMERA_SERVICE);
    134 
    135         // Inflate the SurfaceView, set it as the main layout, and attach a listener
    136         View layout = getLayoutInflater().inflate(R.layout.mainactivity, null);
    137         mSurfaceView = (SurfaceView) layout.findViewById(R.id.mainSurfaceView);
    138         mSurfaceView.getHolder().addCallback(mSurfaceHolderCallback);
    139         setContentView(mSurfaceView);
    140 
    141         // Control flow continues in mSurfaceHolderCallback.surfaceChanged()
    142     }
    143 
    144     /**
    145      * Called when our {@code Activity} loses focus. <p>Tears everything back down.</p>
    146      */
    147     @Override
    148     protected void onPause() {
    149         super.onPause();
    150 
    151         try {
    152             // Ensure SurfaceHolderCallback#surfaceChanged() will run again if the user returns
    153             mSurfaceView.getHolder().setFixedSize(/*width*/0, /*height*/0);
    154 
    155             // Cancel any stale preview jobs
    156             if (mCaptureSession != null) {
    157                 mCaptureSession.close();
    158                 mCaptureSession = null;
    159             }
    160         } finally {
    161             if (mCamera != null) {
    162                 mCamera.close();
    163                 mCamera = null;
    164             }
    165         }
    166 
    167         // Finish processing posted messages, then join on the handling thread
    168         mBackgroundThread.quitSafely();
    169         try {
    170             mBackgroundThread.join();
    171         } catch (InterruptedException ex) {
    172             Log.e(TAG, "Background worker thread was interrupted while joined", ex);
    173         }
    174 
    175         // Close the ImageReader now that the background thread has stopped
    176         if (mCaptureBuffer != null) mCaptureBuffer.close();
    177     }
    178 
    179     /**
    180      * Called when the user clicks on our {@code SurfaceView}, which has ID {@code mainSurfaceView}
    181      * as defined in the {@code mainactivity.xml} layout file. <p>Captures a full-resolution image
    182      * and saves it to permanent storage.</p>
    183      */
    184     public void onClickOnSurfaceView(View v) {
    185         if (mCaptureSession != null) {
    186             try {
    187                 CaptureRequest.Builder requester =
    188                         mCamera.createCaptureRequest(mCamera.TEMPLATE_STILL_CAPTURE);
    189                 requester.addTarget(mCaptureBuffer.getSurface());
    190                 try {
    191                     // This handler can be null because we aren't actually attaching any callback
    192                     mCaptureSession.capture(requester.build(), /*listener*/null, /*handler*/null);
    193                 } catch (CameraAccessException ex) {
    194                     Log.e(TAG, "Failed to file actual capture request", ex);
    195                 }
    196             } catch (CameraAccessException ex) {
    197                 Log.e(TAG, "Failed to build actual capture request", ex);
    198             }
    199         } else {
    200             Log.e(TAG, "User attempted to perform a capture outside our session");
    201         }
    202 
    203         // Control flow continues in mImageCaptureListener.onImageAvailable()
    204     }
    205 
    206     /**
    207      * Callbacks invoked upon state changes in our {@code SurfaceView}.
    208      */
    209     final SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() {
    210         /** The camera device to use, or null if we haven't yet set a fixed surface size. */
    211         private String mCameraId;
    212 
    213         /** Whether we received a change callback after setting our fixed surface size. */
    214         private boolean mGotSecondCallback;
    215 
    216         @Override
    217         public void surfaceCreated(SurfaceHolder holder) {
    218             // This is called every time the surface returns to the foreground
    219             Log.i(TAG, "Surface created");
    220             mCameraId = null;
    221             mGotSecondCallback = false;
    222         }
    223 
    224         @Override
    225         public void surfaceDestroyed(SurfaceHolder holder) {
    226             Log.i(TAG, "Surface destroyed");
    227             holder.removeCallback(this);
    228             // We don't stop receiving callbacks forever because onResume() will reattach us
    229         }
    230 
    231         @Override
    232         public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    233             // On the first invocation, width and height were automatically set to the view's size
    234             if (mCameraId == null) {
    235                 // Find the device's back-facing camera and set the destination buffer sizes
    236                 try {
    237                     for (String cameraId : mCameraManager.getCameraIdList()) {
    238                         CameraCharacteristics cameraCharacteristics =
    239                                 mCameraManager.getCameraCharacteristics(cameraId);
    240                         if (cameraCharacteristics.get(cameraCharacteristics.LENS_FACING) ==
    241                                 CameraCharacteristics.LENS_FACING_BACK) {
    242                             Log.i(TAG, "Found a back-facing camera");
    243                             StreamConfigurationMap info = cameraCharacteristics
    244                                     .get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
    245 
    246                             // Bigger is better when it comes to saving our image
    247                             Size largestSize = Collections.max(
    248                                     Arrays.asList(info.getOutputSizes(ImageFormat.JPEG)),
    249                                     new CompareSizesByArea());
    250 
    251                             // Prepare an ImageReader in case the user wants to capture images
    252                             Log.i(TAG, "Capture size: " + largestSize);
    253                             mCaptureBuffer = ImageReader.newInstance(largestSize.getWidth(),
    254                                     largestSize.getHeight(), ImageFormat.JPEG, /*maxImages*/2);
    255                             mCaptureBuffer.setOnImageAvailableListener(
    256                                     mImageCaptureListener, mBackgroundHandler);
    257 
    258                             // Danger, W.R.! Attempting to use too large a preview size could
    259                             // exceed the camera bus' bandwidth limitation, resulting in
    260                             // gorgeous previews but the storage of garbage capture data.
    261                             Log.i(TAG, "SurfaceView size: " +
    262                                     mSurfaceView.getWidth() + 'x' + mSurfaceView.getHeight());
    263                             Size optimalSize = chooseBigEnoughSize(
    264                                     info.getOutputSizes(SurfaceHolder.class), width, height);
    265 
    266                             // Set the SurfaceHolder to use the camera's largest supported size
    267                             Log.i(TAG, "Preview size: " + optimalSize);
    268                             SurfaceHolder surfaceHolder = mSurfaceView.getHolder();
    269                             surfaceHolder.setFixedSize(optimalSize.getWidth(),
    270                                     optimalSize.getHeight());
    271 
    272                             mCameraId = cameraId;
    273                             return;
    274 
    275                             // Control flow continues with this method one more time
    276                             // (since we just changed our own size)
    277                         }
    278                     }
    279                 } catch (CameraAccessException ex) {
    280                     Log.e(TAG, "Unable to list cameras", ex);
    281                 }
    282 
    283                 Log.e(TAG, "Didn't find any back-facing cameras");
    284             // This is the second time the method is being invoked: our size change is complete
    285             } else if (!mGotSecondCallback) {
    286                 if (mCamera != null) {
    287                     Log.e(TAG, "Aborting camera open because it hadn't been closed");
    288                     return;
    289                 }
    290 
    291                 // Open the camera device
    292                 try {
    293                     mCameraManager.openCamera(mCameraId, mCameraStateCallback,
    294                             mBackgroundHandler);
    295                 } catch (CameraAccessException ex) {
    296                     Log.e(TAG, "Failed to configure output surface", ex);
    297                 }
    298                 mGotSecondCallback = true;
    299 
    300                 // Control flow continues in mCameraStateCallback.onOpened()
    301             }
    302         }};
    303 
    304     /**
    305      * Calledbacks invoked upon state changes in our {@code CameraDevice}. <p>These are run on
    306      * {@code mBackgroundThread}.</p>
    307      */
    308     final CameraDevice.StateCallback mCameraStateCallback =
    309             new CameraDevice.StateCallback() {
    310         @Override
    311         public void onOpened(CameraDevice camera) {
    312             Log.i(TAG, "Successfully opened camera");
    313             mCamera = camera;
    314             try {
    315                 List<Surface> outputs = Arrays.asList(
    316                         mSurfaceView.getHolder().getSurface(), mCaptureBuffer.getSurface());
    317                 camera.createCaptureSession(outputs, mCaptureSessionListener,
    318                         mBackgroundHandler);
    319             } catch (CameraAccessException ex) {
    320                 Log.e(TAG, "Failed to create a capture session", ex);
    321             }
    322 
    323             // Control flow continues in mCaptureSessionListener.onConfigured()
    324         }
    325 
    326         @Override
    327         public void onDisconnected(CameraDevice camera) {
    328             Log.e(TAG, "Camera was disconnected");
    329         }
    330 
    331         @Override
    332         public void onError(CameraDevice camera, int error) {
    333             Log.e(TAG, "State error on device '" + camera.getId() + "': code " + error);
    334         }};
    335 
    336     /**
    337      * Callbacks invoked upon state changes in our {@code CameraCaptureSession}. <p>These are run on
    338      * {@code mBackgroundThread}.</p>
    339      */
    340     final CameraCaptureSession.StateCallback mCaptureSessionListener =
    341             new CameraCaptureSession.StateCallback() {
    342         @Override
    343         public void onConfigured(CameraCaptureSession session) {
    344             Log.i(TAG, "Finished configuring camera outputs");
    345             mCaptureSession = session;
    346 
    347             SurfaceHolder holder = mSurfaceView.getHolder();
    348             if (holder != null) {
    349                 try {
    350                     // Build a request for preview footage
    351                     CaptureRequest.Builder requestBuilder =
    352                             mCamera.createCaptureRequest(mCamera.TEMPLATE_PREVIEW);
    353                     requestBuilder.addTarget(holder.getSurface());
    354                     CaptureRequest previewRequest = requestBuilder.build();
    355 
    356                     // Start displaying preview images
    357                     try {
    358                         session.setRepeatingRequest(previewRequest, /*listener*/null,
    359                                 /*handler*/null);
    360                     } catch (CameraAccessException ex) {
    361                         Log.e(TAG, "Failed to make repeating preview request", ex);
    362                     }
    363                 } catch (CameraAccessException ex) {
    364                     Log.e(TAG, "Failed to build preview request", ex);
    365                 }
    366             }
    367             else {
    368                 Log.e(TAG, "Holder didn't exist when trying to formulate preview request");
    369             }
    370         }
    371 
    372         @Override
    373         public void onClosed(CameraCaptureSession session) {
    374             mCaptureSession = null;
    375         }
    376 
    377         @Override
    378         public void onConfigureFailed(CameraCaptureSession session) {
    379             Log.e(TAG, "Configuration error on device '" + mCamera.getId());
    380         }};
    381 
    382     /**
    383      * Callback invoked when we've received a JPEG image from the camera.
    384      */
    385     final ImageReader.OnImageAvailableListener mImageCaptureListener =
    386             new ImageReader.OnImageAvailableListener() {
    387         @Override
    388         public void onImageAvailable(ImageReader reader) {
    389             // Save the image once we get a chance
    390             mBackgroundHandler.post(new CapturedImageSaver(reader.acquireNextImage()));
    391 
    392             // Control flow continues in CapturedImageSaver#run()
    393         }};
    394 
    395     /**
    396      * Deferred processor responsible for saving snapshots to disk. <p>This is run on
    397      * {@code mBackgroundThread}.</p>
    398      */
    399     static class CapturedImageSaver implements Runnable {
    400         /** The image to save. */
    401         private Image mCapture;
    402 
    403         public CapturedImageSaver(Image capture) {
    404             mCapture = capture;
    405         }
    406 
    407         @Override
    408         public void run() {
    409             try {
    410                 // Choose an unused filename under the Pictures/ directory
    411                 File file = File.createTempFile(CAPTURE_FILENAME_PREFIX, ".jpg",
    412                         Environment.getExternalStoragePublicDirectory(
    413                                 Environment.DIRECTORY_PICTURES));
    414                 try (FileOutputStream ostream = new FileOutputStream(file)) {
    415                     Log.i(TAG, "Retrieved image is" +
    416                             (mCapture.getFormat() == ImageFormat.JPEG ? "" : "n't") + " a JPEG");
    417                     ByteBuffer buffer = mCapture.getPlanes()[0].getBuffer();
    418                     Log.i(TAG, "Captured image size: " +
    419                             mCapture.getWidth() + 'x' + mCapture.getHeight());
    420 
    421                     // Write the image out to the chosen file
    422                     byte[] jpeg = new byte[buffer.remaining()];
    423                     buffer.get(jpeg);
    424                     ostream.write(jpeg);
    425                 } catch (FileNotFoundException ex) {
    426                     Log.e(TAG, "Unable to open output file for writing", ex);
    427                 } catch (IOException ex) {
    428                     Log.e(TAG, "Failed to write the image to the output file", ex);
    429                 }
    430             } catch (IOException ex) {
    431                 Log.e(TAG, "Unable to create a new output file", ex);
    432             } finally {
    433                 mCapture.close();
    434             }
    435         }
    436     }
    437 }
    438