1 /*
2  * Copyright (C) 2019 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 package com.android.cts.verifier.camera.bokeh;
17 
18 import com.android.cts.verifier.PassFailButtons;
19 import com.android.cts.verifier.R;
20 
21 import com.android.ex.camera2.blocking.BlockingCameraManager;
22 import com.android.ex.camera2.blocking.BlockingCameraManager.BlockingOpenException;
23 import com.android.ex.camera2.blocking.BlockingStateCallback;
24 import com.android.ex.camera2.blocking.BlockingSessionCallback;
25 
26 import android.app.AlertDialog;
27 import android.content.res.Configuration;
28 import android.graphics.Bitmap;
29 import android.graphics.BitmapFactory;
30 import android.graphics.Color;
31 import android.graphics.ColorFilter;
32 import android.graphics.ColorMatrixColorFilter;
33 import android.graphics.ImageFormat;
34 import android.graphics.Matrix;
35 import android.graphics.RectF;
36 import android.graphics.SurfaceTexture;
37 import android.hardware.camera2.CameraAccessException;
38 import android.hardware.camera2.CameraCaptureSession;
39 import android.hardware.camera2.CameraCharacteristics;
40 import android.hardware.camera2.CameraCharacteristics.Key;
41 import android.hardware.camera2.CameraDevice;
42 import android.hardware.camera2.CameraManager;
43 import android.hardware.camera2.CameraMetadata;
44 import android.hardware.camera2.CaptureRequest;
45 import android.hardware.camera2.CaptureResult;
46 import android.hardware.camera2.params.Capability;
47 import android.hardware.camera2.params.StreamConfigurationMap;
48 import android.hardware.camera2.TotalCaptureResult;
49 import android.media.Image;
50 import android.media.ImageReader;
51 import android.os.Bundle;
52 import android.os.Handler;
53 import android.os.HandlerThread;
54 import android.util.Log;
55 import android.util.Size;
56 import android.util.SparseArray;
57 import android.view.Menu;
58 import android.view.MenuItem;
59 import android.view.View;
60 import android.view.Surface;
61 import android.view.TextureView;
62 import android.widget.AdapterView;
63 import android.widget.ArrayAdapter;
64 import android.widget.Button;
65 import android.widget.ImageButton;
66 import android.widget.ImageView;
67 import android.widget.Spinner;
68 import android.widget.TextView;
69 import android.widget.Toast;
70 import android.content.Context;
71 
72 import java.lang.Math;
73 import java.nio.ByteBuffer;
74 import java.util.ArrayList;
75 import java.util.Arrays;
76 import java.util.HashMap;
77 import java.util.Comparator;
78 import java.util.List;
79 import java.util.Optional;
80 import java.util.Set;
81 import java.util.TreeSet;
82 
83 /**
84  * Tests for manual verification of bokeh modes supported by the camera device.
85  */
86 public class CameraBokehActivity extends PassFailButtons.Activity
87         implements TextureView.SurfaceTextureListener,
88                    ImageReader.OnImageAvailableListener {
89 
90     private static final String TAG = "CameraBokehActivity";
91     private static final boolean VERBOSE = Log.isLoggable(TAG, Log.VERBOSE);
92     private static final int SESSION_READY_TIMEOUT_MS = 5000;
93     private static final Size FULLHD = new Size(1920, 1080);
94     private static final ColorMatrixColorFilter sJFIF_YUVToRGB_Filter =
95             new ColorMatrixColorFilter(new float[] {
96                         1f,        0f,    1.402f, 0f, -179.456f,
97                         1f, -0.34414f, -0.71414f, 0f,   135.46f,
98                         1f,    1.772f,        0f, 0f, -226.816f,
99                         0f,        0f,        0f, 1f,        0f
100                     });
101 
102     private TextureView mPreviewView;
103     private SurfaceTexture mPreviewTexture;
104     private Surface mPreviewSurface;
105     private int mPreviewTexWidth, mPreviewTexHeight;
106 
107     private ImageView mImageView;
108     private ColorFilter mCurrentColorFilter;
109 
110     private Spinner mCameraSpinner;
111     private TextView mTestLabel;
112     private TextView mPreviewLabel;
113     private TextView mImageLabel;
114 
115     private CameraManager mCameraManager;
116     private String[] mCameraIdList;
117     private HandlerThread mCameraThread;
118     private Handler mCameraHandler;
119     private BlockingCameraManager mBlockingCameraManager;
120     private CameraCharacteristics mCameraCharacteristics;
121     private BlockingStateCallback mCameraListener;
122 
123     private BlockingSessionCallback mSessionListener;
124     private CaptureRequest.Builder mPreviewRequestBuilder;
125     private CaptureRequest mPreviewRequest;
126     private CaptureRequest.Builder mStillCaptureRequestBuilder;
127     private CaptureRequest mStillCaptureRequest;
128 
129     private HashMap<String, ArrayList<Capability>> mTestCases = new HashMap<>();
130     private int mCurrentCameraIndex = -1;
131     private String mCameraId;
132     private CameraCaptureSession mCaptureSession;
133     private CameraDevice mCameraDevice;
134 
135     SizeComparator mSizeComparator = new SizeComparator();
136 
137     private Size mPreviewSize;
138     private Size mJpegSize;
139     private ImageReader mJpegImageReader;
140     private ImageReader mYuvImageReader;
141 
142     private SparseArray<String> mModeNames;
143 
144     private CameraCombination mNextCombination;
145     private Size mMaxBokehStreamingSize;
146 
147     private Button mNextButton;
148 
149     private final TreeSet<CameraCombination> mTestedCombinations = new TreeSet<>(COMPARATOR);
150     private final TreeSet<CameraCombination> mUntestedCombinations = new TreeSet<>(COMPARATOR);
151     private final TreeSet<String> mUntestedCameras = new TreeSet<>();
152 
153     // Menu to show the test progress
154     private static final int MENU_ID_PROGRESS = Menu.FIRST + 1;
155 
156     private class CameraCombination {
157         private final int mCameraIndex;
158         private final int mMode;
159         private final Size mPreviewSize;
160         private final boolean mIsStillCapture;
161         private final String mCameraId;
162         private final String mModeName;
163 
CameraCombination(int cameraIndex, int mode, int streamingWidth, int streamingHeight, String cameraId, String modeName, boolean isStillCapture)164         private CameraCombination(int cameraIndex, int mode,
165                 int streamingWidth, int streamingHeight,
166                 String cameraId, String modeName,
167                 boolean isStillCapture) {
168             this.mCameraIndex = cameraIndex;
169             this.mMode = mode;
170             this.mPreviewSize = new Size(streamingWidth, streamingHeight);
171             this.mCameraId = cameraId;
172             this.mModeName = modeName;
173             this.mIsStillCapture = isStillCapture;
174         }
175 
176         @Override
toString()177         public String toString() {
178             return String.format("Camera %s, mode %s, intent %s",
179                     mCameraId, mModeName, mIsStillCapture ? "PREVIEW" : "STILL_CAPTURE");
180         }
181     }
182 
183     private static final Comparator<CameraCombination> COMPARATOR =
184         Comparator.<CameraCombination, Integer>comparing(c -> c.mCameraIndex)
185             .thenComparing(c -> c.mMode)
186             .thenComparing(c -> c.mIsStillCapture);
187 
188     private CameraCaptureSession.CaptureCallback mCaptureCallback =
189             new CameraCaptureSession.CaptureCallback() {
190         @Override
191         public void onCaptureProgressed(CameraCaptureSession session,
192                                         CaptureRequest request,
193                                         CaptureResult partialResult) {
194             // Don't need to do anything here.
195         }
196 
197         @Override
198         public void onCaptureCompleted(CameraCaptureSession session,
199                                        CaptureRequest request,
200                                        TotalCaptureResult result) {
201             // Don't need to do anything here.
202         }
203     };
204 
205     @Override
onCreate(Bundle savedInstanceState)206     public void onCreate(Bundle savedInstanceState) {
207         super.onCreate(savedInstanceState);
208 
209         setContentView(R.layout.cb_main);
210 
211         setPassFailButtonClickListeners();
212 
213         mPreviewView = (TextureView) findViewById(R.id.preview_view);
214         mImageView = (ImageView) findViewById(R.id.image_view);
215 
216         mPreviewView.setSurfaceTextureListener(this);
217 
218         mCameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);
219         try {
220             mCameraIdList = mCameraManager.getCameraIdList();
221             for (String id : mCameraIdList) {
222                 CameraCharacteristics characteristics =
223                         mCameraManager.getCameraCharacteristics(id);
224                 Key<Capability[]> key =
225                         CameraCharacteristics.CONTROL_AVAILABLE_EXTENDED_SCENE_MODE_CAPABILITIES;
226                 Capability[] extendedSceneModeCaps = characteristics.get(key);
227 
228                 if (extendedSceneModeCaps == null) {
229                     continue;
230                 }
231 
232                 ArrayList<Capability> nonOffModes = new ArrayList<>();
233                 for (Capability cap : extendedSceneModeCaps) {
234                     int mode = cap.getMode();
235                     if (mode == CameraMetadata.CONTROL_EXTENDED_SCENE_MODE_BOKEH_STILL_CAPTURE ||
236                             mode == CameraMetadata.CONTROL_EXTENDED_SCENE_MODE_BOKEH_CONTINUOUS) {
237                         nonOffModes.add(cap);
238                     }
239                 }
240 
241                 if (nonOffModes.size() > 0) {
242                     mUntestedCameras.add("All combinations for Camera " + id);
243                     mTestCases.put(id, nonOffModes);
244                 }
245 
246             }
247         } catch (CameraAccessException e) {
248             e.printStackTrace();
249         }
250 
251         // If no supported bokeh modes, mark the test as pass
252         if (mTestCases.size() == 0) {
253             setInfoResources(R.string.camera_bokeh_test, R.string.camera_bokeh_no_support, -1);
254             setPassButtonEnabled(true);
255         } else {
256             setInfoResources(R.string.camera_bokeh_test, R.string.camera_bokeh_test_info, -1);
257             // disable "Pass" button until all combinations are tested
258             setPassButtonEnabled(false);
259         }
260 
261         Set<String> cameraIdSet = mTestCases.keySet();
262         String[] cameraNames = new String[cameraIdSet.size()];
263         int i = 0;
264         for (String id : cameraIdSet) {
265             cameraNames[i++] = "Camera " + id;
266         }
267         mCameraSpinner = (Spinner) findViewById(R.id.cameras_selection);
268         mCameraSpinner.setAdapter(
269             new ArrayAdapter<String>(
270                 this, R.layout.camera_list_item, cameraNames));
271         mCameraSpinner.setOnItemSelectedListener(mCameraSpinnerListener);
272 
273         mTestLabel = (TextView) findViewById(R.id.test_label);
274         mPreviewLabel = (TextView) findViewById(R.id.preview_label);
275         mImageLabel = (TextView) findViewById(R.id.image_label);
276 
277         // Must be kept in sync with camera bokeh mode manually
278         mModeNames = new SparseArray(2);
279         mModeNames.append(
280                 CameraMetadata.CONTROL_EXTENDED_SCENE_MODE_BOKEH_STILL_CAPTURE, "STILL_CAPTURE");
281         mModeNames.append(
282                 CameraMetadata.CONTROL_EXTENDED_SCENE_MODE_BOKEH_CONTINUOUS, "CONTINUOUS");
283 
284         mNextButton = findViewById(R.id.next_button);
285         mNextButton.setOnClickListener(v -> {
286                 if (mNextCombination != null) {
287                     mUntestedCombinations.remove(mNextCombination);
288                     mTestedCombinations.add(mNextCombination);
289                 }
290                 setUntestedCombination();
291 
292                 if (mNextCombination != null) {
293                     if (mNextCombination.mIsStillCapture) {
294                         takePicture();
295                     } else {
296                         if (mCaptureSession != null) {
297                             mCaptureSession.close();
298                         }
299                         startPreview();
300                     }
301                 }
302         });
303 
304         mBlockingCameraManager = new BlockingCameraManager(mCameraManager);
305         mCameraListener = new BlockingStateCallback();
306     }
307 
308     /**
309      * Set an untested combination of resolution and bokeh mode for the current camera.
310      * Triggered by next button click.
311      */
setUntestedCombination()312     private void setUntestedCombination() {
313         Optional<CameraCombination> combination = mUntestedCombinations.stream().filter(
314             c -> c.mCameraIndex == mCurrentCameraIndex).findFirst();
315         if (!combination.isPresent()) {
316             Toast.makeText(this, "All Camera " + mCurrentCameraIndex + " tests are done.",
317                 Toast.LENGTH_SHORT).show();
318             mNextCombination = null;
319 
320             if (mUntestedCombinations.isEmpty() && mUntestedCameras.isEmpty()) {
321                 setPassButtonEnabled(true);
322             }
323             return;
324         }
325 
326         // There is untested combination for the current camera, set the next untested combination.
327         mNextCombination = combination.get();
328         int nextMode = mNextCombination.mMode;
329         ArrayList<Capability> bokehCaps = mTestCases.get(mCameraId);
330         for (Capability cap : bokehCaps) {
331             if (cap.getMode() == nextMode) {
332                 mMaxBokehStreamingSize = cap.getMaxStreamingSize();
333             }
334         }
335 
336         // Update bokeh mode and use case
337         String testString = "Mode: " + mModeNames.get(mNextCombination.mMode);
338         if (mNextCombination.mIsStillCapture) {
339             testString += "\nIntent: Capture";
340         } else {
341             testString += "\nIntent: Preview";
342         }
343         testString += "\n\nPress Next if the bokeh effect works as intended";
344         mTestLabel.setText(testString);
345 
346         // Update preview view and image view bokeh expectation
347         boolean previewIsBokehCompatible =
348                 mSizeComparator.compare(mNextCombination.mPreviewSize, mMaxBokehStreamingSize) <= 0;
349         String previewLabel = "Normal preview";
350         if (previewIsBokehCompatible || mNextCombination.mIsStillCapture) {
351             previewLabel += " with bokeh";
352         }
353         mPreviewLabel.setText(previewLabel);
354 
355         String imageLabel;
356         if (mNextCombination.mIsStillCapture) {
357             imageLabel = "JPEG with bokeh";
358         } else {
359             imageLabel = "YUV";
360             if (previewIsBokehCompatible) {
361                 imageLabel += " with bokeh";
362             }
363         }
364         mImageLabel.setText(imageLabel);
365     }
366 
367     @Override
onCreateOptionsMenu(Menu menu)368     public boolean onCreateOptionsMenu(Menu menu) {
369         menu.add(Menu.NONE, MENU_ID_PROGRESS, Menu.NONE, "Current Progress");
370         return super.onCreateOptionsMenu(menu);
371     }
372 
373     @Override
onOptionsItemSelected(MenuItem item)374     public boolean onOptionsItemSelected(MenuItem item) {
375         boolean ret = true;
376         switch (item.getItemId()) {
377             case MENU_ID_PROGRESS:
378                 showCombinationsDialog();
379                 ret = true;
380                 break;
381             default:
382                 ret = super.onOptionsItemSelected(item);
383                 break;
384         }
385         return ret;
386     }
387 
showCombinationsDialog()388     private void showCombinationsDialog() {
389         AlertDialog.Builder builder =
390                 new AlertDialog.Builder(CameraBokehActivity.this);
391         builder.setMessage(getTestDetails())
392                 .setTitle("Current Progress")
393                 .setPositiveButton("OK", null);
394         builder.show();
395     }
396 
397     @Override
onResume()398     public void onResume() {
399         super.onResume();
400 
401         startBackgroundThread();
402 
403         int cameraIndex = mCameraSpinner.getSelectedItemPosition();
404         if (cameraIndex >= 0) {
405             setUpCamera(mCameraSpinner.getSelectedItemPosition());
406         }
407     }
408 
409     @Override
onPause()410     public void onPause() {
411         shutdownCamera();
412         stopBackgroundThread();
413 
414         super.onPause();
415     }
416 
417     @Override
getTestDetails()418     public String getTestDetails() {
419         StringBuilder reportBuilder = new StringBuilder();
420         reportBuilder.append("Tested combinations:\n");
421         for (CameraCombination combination: mTestedCombinations) {
422             reportBuilder.append(combination);
423             reportBuilder.append("\n");
424         }
425 
426         reportBuilder.append("Untested cameras:\n");
427         for (String untestedCamera : mUntestedCameras) {
428             reportBuilder.append(untestedCamera);
429             reportBuilder.append("\n");
430         }
431         reportBuilder.append("Untested combinations:\n");
432         for (CameraCombination combination: mUntestedCombinations) {
433             reportBuilder.append(combination);
434             reportBuilder.append("\n");
435         }
436         return reportBuilder.toString();
437     }
438 
439     @Override
onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height)440     public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture,
441             int width, int height) {
442         mPreviewTexture = surfaceTexture;
443         mPreviewTexWidth = width;
444         mPreviewTexHeight = height;
445 
446         mPreviewSurface = new Surface(mPreviewTexture);
447 
448         if (mCameraDevice != null) {
449             startPreview();
450         }
451     }
452 
453     @Override
onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height)454     public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
455         // Ignored, Camera does all the work for us
456         if (VERBOSE) {
457             Log.v(TAG, "onSurfaceTextureSizeChanged: " + width + " x " + height);
458         }
459     }
460 
461     @Override
onSurfaceTextureDestroyed(SurfaceTexture surface)462     public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
463         mPreviewTexture = null;
464         return true;
465     }
466 
467     @Override
onSurfaceTextureUpdated(SurfaceTexture surface)468     public void onSurfaceTextureUpdated(SurfaceTexture surface) {
469         // Invoked every time there's a new Camera preview frame
470     }
471 
472     @Override
onImageAvailable(ImageReader reader)473     public void onImageAvailable(ImageReader reader) {
474         Image img = null;
475         try {
476             img = reader.acquireNextImage();
477             if (img == null) {
478                 Log.d(TAG, "Invalid image!");
479                 return;
480             }
481             final int format = img.getFormat();
482 
483             Size configuredSize = (format == ImageFormat.YUV_420_888 ? mPreviewSize : mJpegSize);
484             Bitmap imgBitmap = null;
485             if (format == ImageFormat.YUV_420_888) {
486                 ByteBuffer yBuffer = img.getPlanes()[0].getBuffer();
487                 ByteBuffer uBuffer = img.getPlanes()[1].getBuffer();
488                 ByteBuffer vBuffer = img.getPlanes()[2].getBuffer();
489                 yBuffer.rewind();
490                 uBuffer.rewind();
491                 vBuffer.rewind();
492                 int w = configuredSize.getWidth();
493                 int h = configuredSize.getHeight();
494                 int stride = img.getPlanes()[0].getRowStride();
495                 int uStride = img.getPlanes()[1].getRowStride();
496                 int vStride = img.getPlanes()[2].getRowStride();
497                 int uPStride = img.getPlanes()[1].getPixelStride();
498                 int vPStride = img.getPlanes()[2].getPixelStride();
499                 byte[] row = new byte[configuredSize.getWidth()];
500                 byte[] uRow = new byte[(configuredSize.getWidth()/2-1)*uPStride + 1];
501                 byte[] vRow = new byte[(configuredSize.getWidth()/2-1)*vPStride + 1];
502                 int[] imgArray = new int[w * h];
503                 for (int y = 0, j = 0, rowStart = 0, uRowStart = 0, vRowStart = 0; y < h;
504                      y++, rowStart += stride) {
505                     yBuffer.position(rowStart);
506                     yBuffer.get(row);
507                     if (y % 2 == 0) {
508                         uBuffer.position(uRowStart);
509                         uBuffer.get(uRow);
510                         vBuffer.position(vRowStart);
511                         vBuffer.get(vRow);
512                         uRowStart += uStride;
513                         vRowStart += vStride;
514                     }
515                     for (int x = 0, i = 0; x < w; x++) {
516                         int yval = row[i] & 0xFF;
517                         int uval = uRow[i/2 * uPStride] & 0xFF;
518                         int vval = vRow[i/2 * vPStride] & 0xFF;
519                         // Write YUV directly; the ImageView color filter will convert to RGB for us.
520                         imgArray[j] = Color.rgb(yval, uval, vval);
521                         i++;
522                         j++;
523                     }
524                 }
525                 img.close();
526                 imgBitmap = Bitmap.createBitmap(imgArray, w, h, Bitmap.Config.ARGB_8888);
527             } else if (format == ImageFormat.JPEG) {
528                 ByteBuffer jpegBuffer = img.getPlanes()[0].getBuffer();
529                 jpegBuffer.rewind();
530                 byte[] jpegData = new byte[jpegBuffer.limit()];
531                 jpegBuffer.get(jpegData);
532                 imgBitmap = BitmapFactory.decodeByteArray(jpegData, 0, jpegData.length);
533                 img.close();
534             } else {
535                 Log.i(TAG, "Unsupported image format: " + format);
536             }
537             if (imgBitmap != null) {
538                 final Bitmap bitmap = imgBitmap;
539                 runOnUiThread(new Runnable() {
540                     @Override
541                     public void run() {
542                         if (format == ImageFormat.YUV_420_888 && (mCurrentColorFilter == null ||
543                                 !mCurrentColorFilter.equals(sJFIF_YUVToRGB_Filter))) {
544                             mCurrentColorFilter = sJFIF_YUVToRGB_Filter;
545                             mImageView.setColorFilter(mCurrentColorFilter);
546                         } else if (format == ImageFormat.JPEG && mCurrentColorFilter != null &&
547                                 mCurrentColorFilter.equals(sJFIF_YUVToRGB_Filter)) {
548                             mCurrentColorFilter = null;
549                             mImageView.clearColorFilter();
550                         }
551                         mImageView.setImageBitmap(bitmap);
552                     }
553                 });
554             }
555         } catch (java.lang.IllegalStateException e) {
556             // Swallow exceptions
557             e.printStackTrace();
558         } finally {
559             if (img != null) {
560                 img.close();
561             }
562         }
563     }
564 
565     private AdapterView.OnItemSelectedListener mCameraSpinnerListener =
566             new AdapterView.OnItemSelectedListener() {
567                 public void onItemSelected(AdapterView<?> parent,
568                         View view, int pos, long id) {
569                     if (mCurrentCameraIndex != pos) {
570                         setUpCamera(pos);
571                     }
572                 }
573 
574                 public void onNothingSelected(AdapterView parent) {
575                 }
576             };
577 
578     private class SizeComparator implements Comparator<Size> {
579         @Override
compare(Size lhs, Size rhs)580         public int compare(Size lhs, Size rhs) {
581             long lha = lhs.getWidth() * lhs.getHeight();
582             long rha = rhs.getWidth() * rhs.getHeight();
583             if (lha == rha) {
584                 lha = lhs.getWidth();
585                 rha = rhs.getWidth();
586             }
587             return (lha < rha) ? -1 : (lha > rha ? 1 : 0);
588         }
589     }
590 
setUpCamera(int index)591     private void setUpCamera(int index) {
592         shutdownCamera();
593 
594         mCurrentCameraIndex = index;
595         mCameraId = mCameraIdList[index];
596         try {
597             mCameraCharacteristics = mCameraManager.getCameraCharacteristics(mCameraId);
598             mCameraDevice = mBlockingCameraManager.openCamera(mCameraId,
599                     mCameraListener, mCameraHandler);
600         } catch (CameraAccessException e) {
601             e.printStackTrace();
602         } catch (BlockingOpenException e) {
603             e.printStackTrace();
604         }
605 
606         // Update untested cameras
607         mUntestedCameras.remove("All combinations for Camera " + mCameraId);
608 
609         StreamConfigurationMap config =
610                 mCameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
611         Size[] jpegSizes = config.getOutputSizes(ImageFormat.JPEG);
612         Arrays.sort(jpegSizes, mSizeComparator);
613         mJpegSize = jpegSizes[jpegSizes.length-1];
614 
615         Size[] yuvSizes = config.getOutputSizes(ImageFormat.YUV_420_888);
616         Arrays.sort(yuvSizes, mSizeComparator);
617         Size maxYuvSize = yuvSizes[yuvSizes.length-1];
618         if (mSizeComparator.compare(maxYuvSize, FULLHD) > 1) {
619             maxYuvSize = FULLHD;
620         }
621 
622         // Update untested entries
623         ArrayList<Capability> currentTestCase = mTestCases.get(mCameraId);
624         for (Capability bokehCap : currentTestCase) {
625             Size maxStreamingSize = bokehCap.getMaxStreamingSize();
626             Size previewSize;
627             if ((maxStreamingSize.getWidth() == 0 && maxStreamingSize.getHeight() == 0) ||
628                     (mSizeComparator.compare(maxStreamingSize, maxYuvSize) > 0)) {
629                 previewSize = maxYuvSize;
630             } else {
631                 previewSize = maxStreamingSize;
632             }
633 
634             CameraCombination combination = new CameraCombination(
635                     index, bokehCap.getMode(), previewSize.getWidth(),
636                     previewSize.getHeight(), mCameraId,
637                     mModeNames.get(bokehCap.getMode()),
638                     /*isStillCapture*/false);
639 
640             if (!mTestedCombinations.contains(combination)) {
641                 mUntestedCombinations.add(combination);
642             }
643 
644             // For BOKEH_MODE_STILL_CAPTURE, add 2 combinations: one streaming, one still capture.
645             if (bokehCap.getMode() ==
646                     CaptureRequest.CONTROL_EXTENDED_SCENE_MODE_BOKEH_STILL_CAPTURE) {
647                 CameraCombination combination2 = new CameraCombination(
648                         index, bokehCap.getMode(), previewSize.getWidth(),
649                         previewSize.getHeight(), mCameraId,
650                         mModeNames.get(bokehCap.getMode()),
651                         /*isStillCapture*/true);
652 
653                 if (!mTestedCombinations.contains(combination2)) {
654                     mUntestedCombinations.add(combination2);
655                 }
656             }
657         }
658 
659         mJpegImageReader = ImageReader.newInstance(
660                 mJpegSize.getWidth(), mJpegSize.getHeight(), ImageFormat.JPEG, 1);
661         mJpegImageReader.setOnImageAvailableListener(this, mCameraHandler);
662 
663         setUntestedCombination();
664 
665         if (mPreviewTexture != null) {
666             startPreview();
667         }
668     }
669 
shutdownCamera()670     private void shutdownCamera() {
671         if (null != mCaptureSession) {
672             mCaptureSession.close();
673             mCaptureSession = null;
674         }
675         if (null != mCameraDevice) {
676             mCameraDevice.close();
677             mCameraDevice = null;
678         }
679         if (null != mJpegImageReader) {
680             mJpegImageReader.close();
681             mJpegImageReader = null;
682         }
683         if (null != mYuvImageReader) {
684             mYuvImageReader.close();
685             mYuvImageReader = null;
686         }
687     }
688 
configurePreviewTextureTransform()689     private void configurePreviewTextureTransform() {
690         int rotation = getWindowManager().getDefaultDisplay().getRotation();
691         Configuration config = getResources().getConfiguration();
692         int degrees = 0;
693         switch (rotation) {
694             case Surface.ROTATION_0: degrees = 0; break;
695             case Surface.ROTATION_90: degrees = 90; break;
696             case Surface.ROTATION_180: degrees = 180; break;
697             case Surface.ROTATION_270: degrees = 270; break;
698         }
699         Matrix matrix = mPreviewView.getTransform(null);
700         int deviceOrientation = Configuration.ORIENTATION_PORTRAIT;
701         if ((degrees % 180 == 0 && config.orientation == Configuration.ORIENTATION_LANDSCAPE) ||
702                 (degrees % 180 == 90 && config.orientation == Configuration.ORIENTATION_PORTRAIT)) {
703             deviceOrientation = Configuration.ORIENTATION_LANDSCAPE;
704         }
705         int effectiveWidth = mPreviewSize.getWidth();
706         int effectiveHeight = mPreviewSize.getHeight();
707         if (deviceOrientation == Configuration.ORIENTATION_PORTRAIT) {
708             int temp = effectiveWidth;
709             effectiveWidth = effectiveHeight;
710             effectiveHeight = temp;
711         }
712 
713         RectF viewRect = new RectF(0, 0, mPreviewTexWidth, mPreviewTexHeight);
714         RectF bufferRect = new RectF(0, 0, effectiveWidth, effectiveHeight);
715         float centerX = viewRect.centerX();
716         float centerY = viewRect.centerY();
717         bufferRect.offset(centerX - bufferRect.centerX(),
718                 centerY - bufferRect.centerY());
719 
720         matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL);
721 
722         matrix.postRotate((360 - degrees) % 360, centerX, centerY);
723         if ((degrees % 180) == 90) {
724             int temp = effectiveWidth;
725             effectiveWidth = effectiveHeight;
726             effectiveHeight = temp;
727         }
728         // Scale to fit view, avoiding any crop
729         float scale = Math.min(mPreviewTexWidth / (float) effectiveWidth,
730                 mPreviewTexHeight / (float) effectiveHeight);
731         matrix.postScale(scale, scale, centerX, centerY);
732 
733         mPreviewView.setTransform(matrix);
734     }
735     /**
736      * Starts a background thread and its {@link Handler}.
737      */
startBackgroundThread()738     private void startBackgroundThread() {
739         mCameraThread = new HandlerThread("CameraBokehBackground");
740         mCameraThread.start();
741         mCameraHandler = new Handler(mCameraThread.getLooper());
742     }
743 
744     /**
745      * Stops the background thread and its {@link Handler}.
746      */
stopBackgroundThread()747     private void stopBackgroundThread() {
748         mCameraThread.quitSafely();
749         try {
750             mCameraThread.join();
751             mCameraThread = null;
752             mCameraHandler = null;
753         } catch (InterruptedException e) {
754             e.printStackTrace();
755         }
756     }
757 
startPreview()758     private void startPreview() {
759         try {
760             if (mPreviewSize == null || !mPreviewSize.equals(mNextCombination.mPreviewSize)) {
761                 mPreviewSize = mNextCombination.mPreviewSize;
762 
763                 mYuvImageReader = ImageReader.newInstance(mPreviewSize.getWidth(),
764                         mPreviewSize.getHeight(), ImageFormat.YUV_420_888, 1);
765                 mYuvImageReader.setOnImageAvailableListener(this, mCameraHandler);
766             };
767 
768             mPreviewTexture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
769             mPreviewRequestBuilder =
770                     mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
771             mPreviewRequestBuilder.addTarget(mPreviewSurface);
772             mPreviewRequestBuilder.addTarget(mYuvImageReader.getSurface());
773 
774             mStillCaptureRequestBuilder =
775                     mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
776             mStillCaptureRequestBuilder.addTarget(mPreviewSurface);
777             mStillCaptureRequestBuilder.addTarget(mJpegImageReader.getSurface());
778 
779             mSessionListener = new BlockingSessionCallback();
780             List<Surface> outputSurfaces = new ArrayList<Surface>(/*capacity*/3);
781             outputSurfaces.add(mPreviewSurface);
782             outputSurfaces.add(mYuvImageReader.getSurface());
783             outputSurfaces.add(mJpegImageReader.getSurface());
784             mCameraDevice.createCaptureSession(outputSurfaces, mSessionListener, mCameraHandler);
785             mCaptureSession = mSessionListener.waitAndGetSession(/*timeoutMs*/3000);
786 
787             configurePreviewTextureTransform();
788 
789             /* Set bokeh mode and start streaming */
790             int bokehMode = mNextCombination.mMode;
791             mPreviewRequestBuilder.set(CaptureRequest.CONTROL_EXTENDED_SCENE_MODE, bokehMode);
792             mStillCaptureRequestBuilder.set(CaptureRequest.CONTROL_EXTENDED_SCENE_MODE, bokehMode);
793             mPreviewRequest = mPreviewRequestBuilder.build();
794             mStillCaptureRequest = mStillCaptureRequestBuilder.build();
795 
796             mCaptureSession.setRepeatingRequest(mPreviewRequest, mCaptureCallback, mCameraHandler);
797         } catch (CameraAccessException e) {
798             e.printStackTrace();
799         }
800     }
801 
takePicture()802     private void takePicture() {
803         try {
804             mCaptureSession.stopRepeating();
805             mSessionListener.getStateWaiter().waitForState(
806                     BlockingSessionCallback.SESSION_READY, SESSION_READY_TIMEOUT_MS);
807 
808             mCaptureSession.capture(mStillCaptureRequest, mCaptureCallback, mCameraHandler);
809         } catch (CameraAccessException e) {
810             e.printStackTrace();
811         }
812     }
813 
setPassButtonEnabled(boolean enabled)814     private void setPassButtonEnabled(boolean enabled) {
815         ImageButton pass_button = (ImageButton) findViewById(R.id.pass_button);
816         pass_button.setEnabled(enabled);
817     }
818 }
819