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