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 */ chooseBigEnoughSize(Size[] choices, int width, int height)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 compare(Size lhs, Size rhs)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 onResume()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 onPause()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 */ onClickOnSurfaceView(View v)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 CapturedImageSaver(Image capture)403 public CapturedImageSaver(Image capture) { 404 mCapture = capture; 405 } 406 407 @Override run()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