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