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