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 17 package androidx.camera.integration.core; 18 19 import static android.os.Environment.getExternalStoragePublicDirectory; 20 21 import static androidx.camera.core.ImageCapture.ERROR_CAMERA_CLOSED; 22 import static androidx.camera.core.ImageCapture.ERROR_CAPTURE_FAILED; 23 import static androidx.camera.core.ImageCapture.ERROR_FILE_IO; 24 import static androidx.camera.core.ImageCapture.ERROR_INVALID_CAMERA; 25 import static androidx.camera.core.ImageCapture.ERROR_UNKNOWN; 26 import static androidx.camera.core.ImageCapture.FLASH_MODE_AUTO; 27 import static androidx.camera.core.ImageCapture.FLASH_MODE_OFF; 28 import static androidx.camera.core.ImageCapture.FLASH_MODE_ON; 29 import static androidx.camera.core.ImageCapture.FLASH_MODE_SCREEN; 30 import static androidx.camera.core.ImageCapture.FLASH_TYPE_ONE_SHOT_FLASH; 31 import static androidx.camera.core.ImageCapture.FLASH_TYPE_USE_TORCH_AS_FLASH; 32 import static androidx.camera.core.ImageCapture.OUTPUT_FORMAT_JPEG; 33 import static androidx.camera.core.ImageCapture.OUTPUT_FORMAT_JPEG_ULTRA_HDR; 34 import static androidx.camera.core.ImageCapture.OUTPUT_FORMAT_RAW; 35 import static androidx.camera.core.ImageCapture.OUTPUT_FORMAT_RAW_JPEG; 36 import static androidx.camera.core.ImageCapture.getImageCaptureCapabilities; 37 import static androidx.camera.core.MirrorMode.MIRROR_MODE_ON_FRONT_ONLY; 38 import static androidx.camera.integration.core.CameraXViewModel.getConfiguredCameraXCameraImplementation; 39 import static androidx.camera.testing.impl.FileUtil.canDeviceWriteToMediaStore; 40 import static androidx.camera.testing.impl.FileUtil.createFolder; 41 import static androidx.camera.testing.impl.FileUtil.createParentFolder; 42 import static androidx.camera.testing.impl.FileUtil.generateVideoFileOutputOptions; 43 import static androidx.camera.testing.impl.FileUtil.generateVideoMediaStoreOptions; 44 import static androidx.camera.testing.impl.FileUtil.getAbsolutePathFromUri; 45 import static androidx.camera.testing.impl.FileUtil.writeTextToExternalFile; 46 import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_DURATION_LIMIT_REACHED; 47 import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_FILE_SIZE_LIMIT_REACHED; 48 import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_INSUFFICIENT_STORAGE; 49 import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_NONE; 50 import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_SOURCE_INACTIVE; 51 52 import static java.util.Objects.requireNonNull; 53 54 import android.Manifest; 55 import android.annotation.SuppressLint; 56 import android.content.ContentValues; 57 import android.content.Intent; 58 import android.content.pm.ActivityInfo; 59 import android.content.pm.PackageManager; 60 import android.content.res.Configuration; 61 import android.graphics.Rect; 62 import android.hardware.camera2.CameraCharacteristics; 63 import android.hardware.display.DisplayManager; 64 import android.media.MediaScannerConnection; 65 import android.net.Uri; 66 import android.os.Build; 67 import android.os.Bundle; 68 import android.os.Environment; 69 import android.os.Handler; 70 import android.os.Looper; 71 import android.os.StrictMode; 72 import android.os.SystemClock; 73 import android.provider.MediaStore; 74 import android.util.DisplayMetrics; 75 import android.util.Log; 76 import android.util.Range; 77 import android.util.Rational; 78 import android.view.Display; 79 import android.view.GestureDetector; 80 import android.view.Menu; 81 import android.view.MenuItem; 82 import android.view.MotionEvent; 83 import android.view.View; 84 import android.view.ViewGroup; 85 import android.view.ViewStub; 86 import android.view.Window; 87 import android.widget.Button; 88 import android.widget.CompoundButton; 89 import android.widget.ImageButton; 90 import android.widget.PopupMenu; 91 import android.widget.SeekBar; 92 import android.widget.TextView; 93 import android.widget.Toast; 94 import android.widget.ToggleButton; 95 96 import androidx.activity.result.ActivityResultLauncher; 97 import androidx.activity.result.contract.ActivityResultContracts; 98 import androidx.annotation.MainThread; 99 import androidx.annotation.OptIn; 100 import androidx.annotation.RequiresApi; 101 import androidx.annotation.UiThread; 102 import androidx.annotation.VisibleForTesting; 103 import androidx.appcompat.app.AppCompatActivity; 104 import androidx.camera.camera2.internal.compat.quirk.CrashWhenTakingPhotoWithAutoFlashAEModeQuirk; 105 import androidx.camera.camera2.internal.compat.quirk.ImageCaptureFailWithAutoFlashQuirk; 106 import androidx.camera.camera2.internal.compat.quirk.ImageCaptureFlashNotFireQuirk; 107 import androidx.camera.camera2.interop.Camera2CameraInfo; 108 import androidx.camera.camera2.interop.ExperimentalCamera2Interop; 109 import androidx.camera.camera2.pipe.integration.compat.quirk.DeviceQuirks; 110 import androidx.camera.core.AspectRatio; 111 import androidx.camera.core.Camera; 112 import androidx.camera.core.CameraControl; 113 import androidx.camera.core.CameraInfo; 114 import androidx.camera.core.CameraSelector; 115 import androidx.camera.core.DisplayOrientedMeteringPointFactory; 116 import androidx.camera.core.DynamicRange; 117 import androidx.camera.core.ExperimentalLensFacing; 118 import androidx.camera.core.ExposureState; 119 import androidx.camera.core.FocusMeteringAction; 120 import androidx.camera.core.FocusMeteringResult; 121 import androidx.camera.core.ImageAnalysis; 122 import androidx.camera.core.ImageCapture; 123 import androidx.camera.core.ImageCaptureCapabilities; 124 import androidx.camera.core.ImageCaptureException; 125 import androidx.camera.core.ImageProxy; 126 import androidx.camera.core.LowLightBoostState; 127 import androidx.camera.core.MeteringPointFactory; 128 import androidx.camera.core.Preview; 129 import androidx.camera.core.TorchState; 130 import androidx.camera.core.UseCase; 131 import androidx.camera.core.UseCaseGroup; 132 import androidx.camera.core.ViewPort; 133 import androidx.camera.core.impl.CameraInfoInternal; 134 import androidx.camera.core.impl.Quirks; 135 import androidx.camera.core.impl.StreamSpec; 136 import androidx.camera.core.impl.utils.AspectRatioUtil; 137 import androidx.camera.core.impl.utils.executor.CameraXExecutors; 138 import androidx.camera.lifecycle.ProcessCameraProvider; 139 import androidx.camera.testing.impl.StreamSharingForceEnabledEffect; 140 import androidx.camera.video.ExperimentalPersistentRecording; 141 import androidx.camera.video.FileOutputOptions; 142 import androidx.camera.video.MediaStoreOutputOptions; 143 import androidx.camera.video.OutputOptions; 144 import androidx.camera.video.PendingRecording; 145 import androidx.camera.video.Quality; 146 import androidx.camera.video.QualitySelector; 147 import androidx.camera.video.Recorder; 148 import androidx.camera.video.Recording; 149 import androidx.camera.video.RecordingStats; 150 import androidx.camera.video.VideoCapabilities; 151 import androidx.camera.video.VideoCapture; 152 import androidx.camera.video.VideoRecordEvent; 153 import androidx.camera.view.ScreenFlashView; 154 import androidx.camera.view.impl.ZoomGestureDetector; 155 import androidx.core.content.ContextCompat; 156 import androidx.core.math.MathUtils; 157 import androidx.core.util.Consumer; 158 import androidx.lifecycle.MutableLiveData; 159 import androidx.lifecycle.ViewModelProvider; 160 import androidx.test.espresso.IdlingResource; 161 import androidx.test.espresso.idling.CountingIdlingResource; 162 163 import com.google.common.util.concurrent.FutureCallback; 164 import com.google.common.util.concurrent.Futures; 165 import com.google.common.util.concurrent.ListenableFuture; 166 167 import org.jspecify.annotations.NonNull; 168 import org.jspecify.annotations.Nullable; 169 170 import java.io.File; 171 import java.text.Format; 172 import java.text.SimpleDateFormat; 173 import java.util.ArrayList; 174 import java.util.Calendar; 175 import java.util.Collections; 176 import java.util.HashMap; 177 import java.util.HashSet; 178 import java.util.Iterator; 179 import java.util.List; 180 import java.util.Locale; 181 import java.util.Map; 182 import java.util.Objects; 183 import java.util.Set; 184 import java.util.concurrent.ExecutorService; 185 import java.util.concurrent.Executors; 186 import java.util.concurrent.TimeUnit; 187 import java.util.concurrent.atomic.AtomicLong; 188 189 /** 190 * An activity with four use cases: (1) view finder, (2) image capture, (3) image analysis, (4) 191 * video capture. 192 * 193 * <p>All four use cases are created with CameraX and tied to the activity's lifecycle. CameraX 194 * automatically connects and disconnects the use cases from the camera in response to changes in 195 * the activity's lifecycle. Therefore, the use cases function properly when the app is paused and 196 * resumed and when the device is rotated. The complex interactions between the camera and these 197 * lifecycle events are handled internally by CameraX. 198 */ 199 @SuppressLint("NullAnnotationGroup") 200 public class CameraXActivity extends AppCompatActivity { 201 private static final String TAG = "CameraXActivity"; 202 private static final String[] REQUIRED_PERMISSIONS; 203 private static final List<DynamicRangeUiData> DYNAMIC_RANGE_UI_DATA = new ArrayList<>(); 204 205 // StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED is not public 206 @SuppressLint("RestrictedApiAndroidX") 207 private static final Range<Integer> FPS_UNSPECIFIED = StreamSpec.FRAME_RATE_RANGE_UNSPECIFIED; 208 private static final Map<Integer, Range<Integer>> ID_TO_FPS_RANGE_MAP = new HashMap<>(); 209 private static final Map<Integer, Integer> ID_TO_ASPECT_RATIO_MAP = new HashMap<>(); 210 211 static { 212 // From Android T, skips the permission check of WRITE_EXTERNAL_STORAGE since it won't be 213 // granted any more. 214 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 215 REQUIRED_PERMISSIONS = new String[]{ 216 Manifest.permission.CAMERA, 217 Manifest.permission.RECORD_AUDIO 218 }; 219 } else { 220 REQUIRED_PERMISSIONS = new String[]{ 221 Manifest.permission.CAMERA, 222 Manifest.permission.RECORD_AUDIO, 223 Manifest.permission.WRITE_EXTERNAL_STORAGE 224 }; 225 } 226 DYNAMIC_RANGE_UI_DATA.add(new DynamicRangeUiData( DynamicRange.SDR, "SDR", R.string.toggle_video_dyn_rng_sdr))227 DYNAMIC_RANGE_UI_DATA.add(new DynamicRangeUiData( 228 DynamicRange.SDR, 229 "SDR", 230 R.string.toggle_video_dyn_rng_sdr)); DYNAMIC_RANGE_UI_DATA.add(new DynamicRangeUiData( DynamicRange.HDR_UNSPECIFIED_10_BIT, "HDR (Auto, 10-bit)", R.string.toggle_video_dyn_rng_hdr_auto))231 DYNAMIC_RANGE_UI_DATA.add(new DynamicRangeUiData( 232 DynamicRange.HDR_UNSPECIFIED_10_BIT, 233 "HDR (Auto, 10-bit)", 234 R.string.toggle_video_dyn_rng_hdr_auto)); DYNAMIC_RANGE_UI_DATA.add(new DynamicRangeUiData( DynamicRange.HLG_10_BIT, "HDR (HLG, 10-bit)", R.string.toggle_video_dyn_rng_hlg))235 DYNAMIC_RANGE_UI_DATA.add(new DynamicRangeUiData( 236 DynamicRange.HLG_10_BIT, 237 "HDR (HLG, 10-bit)", 238 R.string.toggle_video_dyn_rng_hlg)); DYNAMIC_RANGE_UI_DATA.add(new DynamicRangeUiData( DynamicRange.HDR10_10_BIT, "HDR (HDR10, 10-bit)", R.string.toggle_video_dyn_rng_hdr_ten))239 DYNAMIC_RANGE_UI_DATA.add(new DynamicRangeUiData( 240 DynamicRange.HDR10_10_BIT, 241 "HDR (HDR10, 10-bit)", 242 R.string.toggle_video_dyn_rng_hdr_ten)); DYNAMIC_RANGE_UI_DATA.add(new DynamicRangeUiData( DynamicRange.HDR10_PLUS_10_BIT, "HDR (HDR10+, 10-bit)", R.string.toggle_video_dyn_rng_hdr_ten_plus))243 DYNAMIC_RANGE_UI_DATA.add(new DynamicRangeUiData( 244 DynamicRange.HDR10_PLUS_10_BIT, 245 "HDR (HDR10+, 10-bit)", 246 R.string.toggle_video_dyn_rng_hdr_ten_plus)); DYNAMIC_RANGE_UI_DATA.add(new DynamicRangeUiData( DynamicRange.DOLBY_VISION_8_BIT, "HDR (Dolby Vision, 8-bit)", R.string.toggle_video_dyn_rng_hdr_dolby_vision_8))247 DYNAMIC_RANGE_UI_DATA.add(new DynamicRangeUiData( 248 DynamicRange.DOLBY_VISION_8_BIT, 249 "HDR (Dolby Vision, 8-bit)", 250 R.string.toggle_video_dyn_rng_hdr_dolby_vision_8)); DYNAMIC_RANGE_UI_DATA.add(new DynamicRangeUiData( DynamicRange.DOLBY_VISION_10_BIT, "HDR (Dolby Vision, 10-bit)", R.string.toggle_video_dyn_rng_hdr_dolby_vision_10))251 DYNAMIC_RANGE_UI_DATA.add(new DynamicRangeUiData( 252 DynamicRange.DOLBY_VISION_10_BIT, 253 "HDR (Dolby Vision, 10-bit)", 254 R.string.toggle_video_dyn_rng_hdr_dolby_vision_10)); 255 256 // TODO - Indicate whether the FPS ranges are supported with 257 // `CameraInfo.getSupportedFrameRateRanges()`, but we may want to try unsupported cases too 258 // sometimes for testing, so the unsupported ones still should be options (perhaps greyed 259 // out or struck-through). ID_TO_FPS_RANGE_MAP.put(R.id.fps_unspecified, FPS_UNSPECIFIED)260 ID_TO_FPS_RANGE_MAP.put(R.id.fps_unspecified, FPS_UNSPECIFIED); ID_TO_FPS_RANGE_MAP.put(R.id.fps_15, new Range<>(15, 15))261 ID_TO_FPS_RANGE_MAP.put(R.id.fps_15, new Range<>(15, 15)); ID_TO_FPS_RANGE_MAP.put(R.id.fps_30, new Range<>(30, 30))262 ID_TO_FPS_RANGE_MAP.put(R.id.fps_30, new Range<>(30, 30)); ID_TO_FPS_RANGE_MAP.put(R.id.fps_60, new Range<>(60, 60))263 ID_TO_FPS_RANGE_MAP.put(R.id.fps_60, new Range<>(60, 60)); 264 ID_TO_ASPECT_RATIO_MAP.put(R.id.aspect_ratio_default, AspectRatio.RATIO_DEFAULT)265 ID_TO_ASPECT_RATIO_MAP.put(R.id.aspect_ratio_default, AspectRatio.RATIO_DEFAULT); ID_TO_ASPECT_RATIO_MAP.put(R.id.aspect_ratio_4_3, AspectRatio.RATIO_4_3)266 ID_TO_ASPECT_RATIO_MAP.put(R.id.aspect_ratio_4_3, AspectRatio.RATIO_4_3); ID_TO_ASPECT_RATIO_MAP.put(R.id.aspect_ratio_16_9, AspectRatio.RATIO_16_9)267 ID_TO_ASPECT_RATIO_MAP.put(R.id.aspect_ratio_16_9, AspectRatio.RATIO_16_9); 268 } 269 270 //Use this activity title when Camera Pipe configuration is used by core test app 271 private static final String APP_TITLE_FOR_CAMERA_PIPE = "CameraPipe Core Test App"; 272 273 // Possible values for this intent key: "backward" or "forward". 274 private static final String INTENT_EXTRA_CAMERA_DIRECTION = "camera_direction"; 275 // Possible values for this intent key: "switch_test_case", "preview_test_case" or 276 // "default_test_case". 277 private static final String INTENT_EXTRA_E2E_TEST_CASE = "e2e_test_case"; 278 // Launch the activity with the specified video quality. 279 private static final String INTENT_EXTRA_VIDEO_QUALITY = "video_quality"; 280 // Launch the activity with the view finder position log into a text file. 281 private static final String INTENT_EXTRA_LOG_VIEWFINDER_POSITION = "log_view_finder_position"; 282 // Launch the activity with the specified video mirror mode. 283 private static final String INTENT_EXTRA_VIDEO_MIRROR_MODE = "video_mirror_mode"; 284 public static final String INTENT_EXTRA_CAMERA_IMPLEMENTATION = "camera_implementation"; 285 public static final String INTENT_EXTRA_CAMERA_IMPLEMENTATION_NO_HISTORY = 286 "camera_implementation_no_history"; 287 288 // Launch the activity with the specified target aspect ratio. 289 public static final String INTENT_EXTRA_TARGET_ASPECT_RATIO = "target_aspect_ratio"; 290 291 // Launch the activity with the specified scale type. The default value is FILL_CENTER. 292 public static final String INTENT_EXTRA_SCALE_TYPE = "scale_type"; 293 public static final int INTENT_EXTRA_FILL_CENTER = 1; 294 public static final int INTENT_EXTRA_FIT_CENTER = 4; 295 296 // Launch the activity with the specified camera id. 297 @VisibleForTesting 298 public static final String INTENT_EXTRA_CAMERA_ID = "camera_id"; 299 // Launch the activity with the specified use case combination. 300 @VisibleForTesting 301 public static final String INTENT_EXTRA_USE_CASE_COMBINATION = "use_case_combination"; 302 @VisibleForTesting 303 // Sets this bit to bind Preview when using INTENT_EXTRA_USE_CASE_COMBINATION 304 public static final int BIND_PREVIEW = 0x1; 305 @VisibleForTesting 306 // Sets this bit to bind ImageCapture when using INTENT_EXTRA_USE_CASE_COMBINATION 307 public static final int BIND_IMAGE_CAPTURE = 0x2; 308 @VisibleForTesting 309 // Sets this bit to bind VideoCapture when using INTENT_EXTRA_USE_CASE_COMBINATION 310 public static final int BIND_VIDEO_CAPTURE = 0x4; 311 @VisibleForTesting 312 // Sets this bit to bind ImageAnalysis when using INTENT_EXTRA_USE_CASE_COMBINATION 313 public static final int BIND_IMAGE_ANALYSIS = 0x8; 314 // Launch the activity with the specified stream sharing force enable settings. Note that 315 // StreamSharing will only take effect when both Preview and VideoCapture are bound. 316 @VisibleForTesting 317 public static final String INTENT_EXTRA_FORCE_ENABLE_STREAM_SHARING = 318 "force_enable_stream_sharing"; 319 320 static final CameraSelector BACK_SELECTOR = 321 new CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build(); 322 static final CameraSelector FRONT_SELECTOR = 323 new CameraSelector.Builder().requireLensFacing( 324 CameraSelector.LENS_FACING_FRONT).build(); 325 private CameraSelector mExternalCameraSelector = null; 326 327 private final AtomicLong mImageAnalysisFrameCount = new AtomicLong(0); 328 private final AtomicLong mPreviewFrameCount = new AtomicLong(0); 329 // Automatically stops the video recording when this length value is set to be non-zero and 330 // video length reaches the length in ms. 331 private long mVideoCaptureAutoStopLength = 0; 332 final MutableLiveData<String> mImageAnalysisResult = new MutableLiveData<>(); 333 private static final String BACKWARD = "BACKWARD"; 334 private static final String SWITCH_TEST_CASE = "switch_test_case"; 335 private static final String PREVIEW_TEST_CASE = "preview_test_case"; 336 337 /** Represents screen flash is set and fully supported (non-legacy device) */ 338 private static final String DESCRIPTION_FLASH_MODE_SCREEN = "FLASH_MODE_SCREEN"; 339 /** Represents screen flash is set, but not supported due to being legacy device */ 340 private static final String DESCRIPTION_SCREEN_FLASH_NOT_SUPPORTED_LEGACY = 341 "SCREEN_FLASH_NOT_SUPPORTED_LEGACY"; 342 /** Represents the lack of physical flash unit for current camera */ 343 private static final String DESCRIPTION_FLASH_UNIT_NOT_AVAILABLE = "FLASH_UNIT_NOT_AVAILABLE"; 344 /** Represents current (if any) flash mode not being supported */ 345 private static final String DESCRIPTION_FLASH_MODE_NOT_SUPPORTED = "FLASH_MODE_NOT_SUPPORTED"; 346 private static final Quality QUALITY_AUTO = null; 347 348 // The target aspect ratio of Preview and ImageCapture. It can be adjusted by setting 349 // INTENT_EXTRA_TARGET_ASPECT_RATIO for the e2e testing. 350 private int mTargetAspectRatio = AspectRatio.RATIO_DEFAULT; 351 private Recording mActiveRecording; 352 /** The camera to use */ 353 CameraSelector mCurrentCameraSelector = BACK_SELECTOR; 354 ProcessCameraProvider mCameraProvider; 355 private CameraXViewModel.CameraProviderResult mCameraProviderResult; 356 357 // TODO: Move the analysis processing, capture processing to separate threads, so 358 // there is smaller impact on the preview. 359 View mViewFinder; 360 private List<UseCase> mUseCases; 361 ExecutorService mFileWriterExecutorService; 362 ExecutorService mImageCaptureExecutorService; 363 private VideoCapture<Recorder> mVideoCapture; 364 private Recorder mRecorder; 365 Camera mCamera; 366 367 private CameraSelector mLaunchingCameraIdSelector = null; 368 private int mLaunchingCameraLensFacing = CameraSelector.LENS_FACING_UNKNOWN; 369 370 private ToggleButton mVideoToggle; 371 private ToggleButton mPhotoToggle; 372 private ToggleButton mAnalysisToggle; 373 private ToggleButton mPreviewToggle; 374 375 private Button mTakePicture; 376 private ImageButton mCameraDirectionButton; 377 private ImageButton mFlashButton; 378 private ScreenFlashView mScreenFlashView; 379 private TextView mTextView; 380 private ImageButton mTorchButton; 381 private TextView mTorchStrengthText; 382 private SeekBar mTorchStrengthSeekBar; 383 private ToggleButton mCaptureQualityToggle; 384 private Button mPlusEV; 385 private Button mDecEV; 386 private ToggleButton mZslToggle; 387 private TextView mZoomRatioLabel; 388 private SeekBar mZoomSeekBar; 389 private Button mZoomIn2XToggle; 390 private Button mZoomResetToggle; 391 private Button mButtonImageOutputFormat; 392 private Toast mEvToast = null; 393 private Toast mPSToast = null; 394 private ToggleButton mPreviewStabilizationToggle; 395 private ToggleButton mLowLightBoostToggle; 396 397 private OpenGLRenderer mPreviewRenderer; 398 private DisplayManager.DisplayListener mDisplayListener; 399 private RecordUi mRecordUi; 400 private DynamicRangeUi mDynamicRangeUi; 401 private Quality mVideoQuality; 402 private boolean mAudioMuted = false; 403 private DynamicRange mDynamicRange = DynamicRange.SDR; 404 private @ImageCapture.OutputFormat int mImageOutputFormat = OUTPUT_FORMAT_JPEG; 405 private Set<DynamicRange> mDisplaySupportedHighDynamicRanges = Collections.emptySet(); 406 private final Set<DynamicRange> mSelectableDynamicRanges = new HashSet<>(); 407 private int mVideoMirrorMode = MIRROR_MODE_ON_FRONT_ONLY; 408 private boolean mIsPreviewStabilizationOn = false; 409 private boolean mIsLowLightBoostOn = false; 410 private Range<Integer> mFpsRange = FPS_UNSPECIFIED; 411 private boolean mForceEnableStreamSharing; 412 private boolean mDisableViewPort; 413 private boolean mEnableTorchAsFlash; 414 415 SessionMediaUriSet mSessionImagesUriSet = new SessionMediaUriSet(); 416 SessionMediaUriSet mSessionVideosUriSet = new SessionMediaUriSet(); 417 418 // Analyzer to be used with ImageAnalysis. 419 private final ImageAnalysis.Analyzer mAnalyzer = new ImageAnalysis.Analyzer() { 420 @Override 421 public void analyze(@NonNull ImageProxy image) { 422 // Since we set the callback handler to a main thread handler, we can call 423 // setValue() here. If we weren't on the main thread, we would have to call 424 // postValue() instead. 425 mImageAnalysisResult.setValue( 426 Long.toString(image.getImageInfo().getTimestamp())); 427 try { 428 if (mImageAnalysisFrameCount.get() >= FRAMES_UNTIL_IMAGE_ANALYSIS_IS_READY 429 && !mAnalysisIdlingResource.isIdleNow()) { 430 mAnalysisIdlingResource.decrement(); 431 } 432 } catch (IllegalStateException e) { 433 Log.e(TAG, "Unexpected counter decrement"); 434 } 435 image.close(); 436 } 437 }; 438 439 private final FutureCallback<Integer> mEVFutureCallback = new FutureCallback<Integer>() { 440 441 @Override 442 public void onSuccess(@Nullable Integer result) { 443 if (result == null) { 444 return; 445 } 446 CameraInfo cameraInfo = getCameraInfo(); 447 if (cameraInfo != null) { 448 ExposureState exposureState = cameraInfo.getExposureState(); 449 float ev = result * exposureState.getExposureCompensationStep().floatValue(); 450 Log.d(TAG, "success new EV: " + ev); 451 showEVToast(String.format("EV: %.2f", ev)); 452 } 453 } 454 455 @Override 456 public void onFailure(@NonNull Throwable t) { 457 Log.d(TAG, "failed " + t); 458 showEVToast("Fail to set EV"); 459 } 460 }; 461 462 // Listener that handles all ToggleButton events. 463 private final CompoundButton.OnCheckedChangeListener mOnCheckedChangeListener = 464 (compoundButton, isChecked) -> tryBindUseCases(); 465 466 private final Consumer<Long> mFrameUpdateListener = timestamp -> { 467 if (mPreviewFrameCount.getAndIncrement() >= FRAMES_UNTIL_VIEW_IS_READY) { 468 try { 469 if (!this.mViewIdlingResource.isIdleNow()) { 470 Log.d(TAG, FRAMES_UNTIL_VIEW_IS_READY + " or more counted on preview." 471 + " Make IdlingResource idle."); 472 this.mViewIdlingResource.decrement(); 473 } 474 } catch (IllegalStateException e) { 475 Log.e(TAG, "Unexpected decrement. Continuing"); 476 } 477 } 478 }; 479 480 // Espresso testing variables 481 private static final int FRAMES_UNTIL_VIEW_IS_READY = 5; 482 // Espresso testing variables 483 private static final int FRAMES_UNTIL_IMAGE_ANALYSIS_IS_READY = 5; 484 private final CountingIdlingResource mViewIdlingResource = new CountingIdlingResource("view"); 485 private final CountingIdlingResource mInitializationIdlingResource = 486 new CountingIdlingResource("initialization"); 487 private final CountingIdlingResource mAnalysisIdlingResource = 488 new CountingIdlingResource("analysis"); 489 private final CountingIdlingResource mImageSavedIdlingResource = 490 new CountingIdlingResource("imagesaved"); 491 private final CountingIdlingResource mVideoSavedIdlingResource = 492 new CountingIdlingResource("videosaved"); 493 494 /** 495 * Saves the error message of the last take picture action if any error occurs. This will be 496 * null which means no error occurs. 497 */ 498 private @Nullable String mLastTakePictureErrorMessage = null; 499 500 /** 501 * Retrieve idling resource that waits for image received by analyzer). 502 */ 503 @VisibleForTesting getAnalysisIdlingResource()504 public @NonNull IdlingResource getAnalysisIdlingResource() { 505 return mAnalysisIdlingResource; 506 } 507 508 /** 509 * Retrieve idling resource that waits view to get texture update. 510 */ 511 @VisibleForTesting getViewIdlingResource()512 public @NonNull IdlingResource getViewIdlingResource() { 513 return mViewIdlingResource; 514 } 515 516 /** 517 * Retrieve idling resource that waits for capture to complete (save or error). 518 */ 519 @VisibleForTesting getImageSavedIdlingResource()520 public @NonNull IdlingResource getImageSavedIdlingResource() { 521 return mImageSavedIdlingResource; 522 } 523 524 /** 525 * Retrieve idling resource that waits for a video being recorded and saved. 526 */ 527 @VisibleForTesting getVideoSavedIdlingResource()528 public @NonNull IdlingResource getVideoSavedIdlingResource() { 529 return mVideoSavedIdlingResource; 530 } 531 532 /** 533 * Retrieve idling resource that waits for initialization to finish. 534 */ 535 @VisibleForTesting getInitializationIdlingResource()536 public @NonNull IdlingResource getInitializationIdlingResource() { 537 return mInitializationIdlingResource; 538 } 539 540 /** 541 * Returns the result of CameraX initialization. 542 * 543 * <p>This will only be set after initialization has finished, which will occur once 544 * {@link #getInitializationIdlingResource()} is idle. 545 * 546 * <p>Should only be called on the main thread. 547 */ 548 @VisibleForTesting 549 @MainThread getCameraProviderResult()550 public CameraXViewModel.@Nullable CameraProviderResult getCameraProviderResult() { 551 return mCameraProviderResult; 552 } 553 554 /** 555 * Retrieve idling resource that waits for view to display frames before proceeding. 556 */ 557 @VisibleForTesting resetViewIdlingResource()558 public void resetViewIdlingResource() { 559 mPreviewFrameCount.set(0); 560 // Make the view idling resource non-idle, until required frame count achieved. 561 if (mViewIdlingResource.isIdleNow()) { 562 mViewIdlingResource.increment(); 563 } 564 } 565 566 /** 567 * Retrieve idling resource that waits for ImageAnalysis to receive images. 568 */ 569 @VisibleForTesting resetAnalysisIdlingResource()570 public void resetAnalysisIdlingResource() { 571 mImageAnalysisFrameCount.set(0); 572 // Make the analysis idling resource non-idle, until required images achieved. 573 if (mAnalysisIdlingResource.isIdleNow()) { 574 mAnalysisIdlingResource.increment(); 575 } 576 } 577 578 /** 579 * Retrieve idling resource that waits for VideoCapture to record a video. 580 */ 581 @VisibleForTesting resetVideoSavedIdlingResource()582 public void resetVideoSavedIdlingResource() { 583 // Make the video saved idling resource non-idle, until required video length recorded. 584 if (mVideoSavedIdlingResource.isIdleNow()) { 585 mVideoSavedIdlingResource.increment(); 586 } 587 } 588 589 /** 590 * Delete images that were taking during this session so far. 591 * May leak images if pending captures not completed. 592 */ 593 @VisibleForTesting deleteSessionImages()594 public void deleteSessionImages() { 595 mSessionImagesUriSet.deleteAllUris(); 596 } 597 598 /** 599 * Delete videos that were taking during this session so far. 600 */ 601 @VisibleForTesting deleteSessionVideos()602 public void deleteSessionVideos() { 603 mSessionVideosUriSet.deleteAllUris(); 604 } 605 606 @SuppressLint("NullAnnotationGroup") 607 @OptIn(markerClass = androidx.camera.core.ExperimentalZeroShutterLag.class) 608 @ImageCapture.CaptureMode getCaptureMode()609 int getCaptureMode() { 610 if (mZslToggle.isChecked()) { 611 return ImageCapture.CAPTURE_MODE_ZERO_SHUTTER_LAG; 612 } else { 613 return mCaptureQualityToggle.isChecked() ? ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY : 614 ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY; 615 } 616 } 617 618 /** Returns whether any kind of flash (physical/screen) is available */ isFlashAvailable()619 private boolean isFlashAvailable() { 620 return isFlashUnitAvailable() || isScreenFlashAvailable(); 621 } 622 623 /** Returns whether physical flash unit is available */ isFlashUnitAvailable()624 private boolean isFlashUnitAvailable() { 625 CameraInfo cameraInfo = getCameraInfo(); 626 return mPhotoToggle.isChecked() && cameraInfo != null && cameraInfo.hasFlashUnit(); 627 } 628 629 /** Returns whether screen flash is available */ isScreenFlashAvailable()630 private boolean isScreenFlashAvailable() { 631 return mPhotoToggle.isChecked() && isFrontCamera(); 632 } 633 634 @SuppressLint("RestrictedApiAndroidX") isFlashTestSupported(@mageCapture.FlashMode int flashMode)635 private boolean isFlashTestSupported(@ImageCapture.FlashMode int flashMode) { 636 switch (flashMode) { 637 case FLASH_MODE_OFF: 638 return false; 639 case FLASH_MODE_AUTO: 640 CameraInfo cameraInfo = getCameraInfo(); 641 if (cameraInfo instanceof CameraInfoInternal) { 642 643 Quirks deviceQuirks = CameraXViewModel.CAMERA_PIPE_IMPLEMENTATION_OPTION.equals( 644 getConfiguredCameraXCameraImplementation()) ? DeviceQuirks.all 645 : androidx.camera.camera2.internal.compat.quirk.DeviceQuirks.getAll(); 646 Quirks cameraQuirks = ((CameraInfoInternal) cameraInfo).getCameraQuirks(); 647 648 if (deviceQuirks.contains(CrashWhenTakingPhotoWithAutoFlashAEModeQuirk.class) 649 || cameraQuirks.contains(ImageCaptureFailWithAutoFlashQuirk.class) 650 || cameraQuirks.contains(ImageCaptureFlashNotFireQuirk.class) 651 || deviceQuirks.contains( 652 androidx.camera.camera2.pipe.integration.compat.quirk 653 .CrashWhenTakingPhotoWithAutoFlashAEModeQuirk.class) 654 || cameraQuirks.contains( 655 androidx.camera.camera2.pipe.integration.compat.quirk 656 .ImageCaptureFailWithAutoFlashQuirk.class) 657 || cameraQuirks.contains( 658 androidx.camera.camera2.pipe.integration.compat.quirk 659 .ImageCaptureFlashNotFireQuirk.class)) { 660 661 Toast.makeText(this, DESCRIPTION_FLASH_MODE_NOT_SUPPORTED, 662 Toast.LENGTH_SHORT).show(); 663 return false; 664 } 665 } 666 break; 667 default: // fall out 668 } 669 return true; 670 } 671 isExposureCompensationSupported()672 private boolean isExposureCompensationSupported() { 673 CameraInfo cameraInfo = getCameraInfo(); 674 return cameraInfo != null 675 && cameraInfo.getExposureState().isExposureCompensationSupported(); 676 } 677 setUpFlashButton()678 private void setUpFlashButton() { 679 mFlashButton.setOnClickListener(v -> { 680 @ImageCapture.FlashMode int flashMode = getImageCapture().getFlashMode(); 681 682 if (flashMode == FLASH_MODE_OFF) { 683 if (isFlashUnitAvailable()) { 684 getImageCapture().setFlashMode(FLASH_MODE_AUTO); 685 } else if (isScreenFlashAvailable()) { 686 setUpScreenFlash(); 687 } else { 688 Log.e(TAG, 689 "Flash button clicked despite lack of both physical and screen flash"); 690 } 691 } else if (flashMode == FLASH_MODE_AUTO) { 692 getImageCapture().setFlashMode(FLASH_MODE_ON); 693 } else if (flashMode == FLASH_MODE_ON) { 694 if (!isScreenFlashAvailable()) { 695 getImageCapture().setFlashMode(FLASH_MODE_OFF); 696 } else { 697 setUpScreenFlash(); 698 } 699 } else if (flashMode == FLASH_MODE_SCREEN) { 700 getImageCapture().setFlashMode(FLASH_MODE_OFF); 701 } 702 updateButtonsUi(); 703 }); 704 } 705 setUpScreenFlash()706 private void setUpScreenFlash() { 707 if (!isFrontCamera()) { 708 return; 709 } 710 711 mScreenFlashView.setScreenFlashWindow(getWindow()); 712 getImageCapture().setScreenFlash( 713 mScreenFlashView.getScreenFlash()); 714 getImageCapture().setFlashMode(FLASH_MODE_SCREEN); 715 } 716 717 @SuppressLint({"MissingPermission", "NullAnnotationGroup"}) 718 @OptIn(markerClass = ExperimentalPersistentRecording.class) setUpRecordButton()719 private void setUpRecordButton() { 720 mRecordUi.getButtonRecord().setOnClickListener((view) -> { 721 RecordUi.State state = mRecordUi.getState(); 722 switch (state) { 723 case IDLE: 724 createDefaultVideoFolderIfNotExist(); 725 final PendingRecording pendingRecording; 726 String fileName = "video_" + System.currentTimeMillis(); 727 String extension = "mp4"; 728 if (canDeviceWriteToMediaStore()) { 729 // Use MediaStoreOutputOptions for public share media storage. 730 pendingRecording = getVideoCapture().getOutput().prepareRecording( 731 this, 732 generateVideoMediaStoreOptions(getContentResolver(), fileName)); 733 } else { 734 // Use FileOutputOption for devices in MediaStoreVideoCannotWrite Quirk. 735 pendingRecording = getVideoCapture().getOutput().prepareRecording( 736 this, generateVideoFileOutputOptions(fileName, extension)); 737 } 738 739 resetVideoSavedIdlingResource(); 740 741 if (isPersistentRecordingEnabled()) { 742 pendingRecording.asPersistentRecording(); 743 } 744 mActiveRecording = pendingRecording 745 .withAudioEnabled(mAudioMuted) 746 .start(ContextCompat.getMainExecutor(CameraXActivity.this), 747 mVideoRecordEventListener); 748 mRecordUi.setState(RecordUi.State.RECORDING); 749 break; 750 case RECORDING: 751 case PAUSED: 752 mActiveRecording.stop(); 753 mActiveRecording = null; 754 mRecordUi.setState(RecordUi.State.STOPPING); 755 break; 756 case STOPPING: 757 // Record button should be disabled. 758 default: 759 throw new IllegalStateException( 760 "Unexpected state when click record button: " + state); 761 } 762 }); 763 764 mRecordUi.getButtonPause().setOnClickListener(view -> { 765 RecordUi.State state = mRecordUi.getState(); 766 switch (state) { 767 case RECORDING: 768 mActiveRecording.pause(); 769 mRecordUi.setState(RecordUi.State.PAUSED); 770 break; 771 case PAUSED: 772 mActiveRecording.resume(); 773 mRecordUi.setState(RecordUi.State.RECORDING); 774 break; 775 case IDLE: 776 case STOPPING: 777 // Pause button should be invisible. 778 default: 779 throw new IllegalStateException( 780 "Unexpected state when click pause button: " + state); 781 } 782 }); 783 784 // Final reference to this record UI 785 mRecordUi.getButtonQuality().setText(getQualityIconName(mVideoQuality)); 786 mRecordUi.getButtonQuality().setOnClickListener(view -> { 787 PopupMenu popup = new PopupMenu(this, view); 788 Menu menu = popup.getMenu(); 789 790 // Add Auto item 791 final int groupId = Menu.NONE; 792 final int autoOrder = 0; 793 final int autoMenuId = qualityToItemId(QUALITY_AUTO); 794 menu.add(groupId, autoMenuId, autoOrder, getQualityMenuItemName(QUALITY_AUTO)); 795 if (mVideoQuality == QUALITY_AUTO) { 796 menu.findItem(autoMenuId).setChecked(true); 797 } 798 799 // Add device supported qualities 800 VideoCapabilities videoCapabilities = Recorder.getVideoCapabilities( 801 mCamera.getCameraInfo()); 802 List<Quality> supportedQualities = videoCapabilities.getSupportedQualities( 803 mDynamicRange); 804 // supportedQualities has been sorted by descending order. 805 for (int i = 0; i < supportedQualities.size(); i++) { 806 Quality quality = supportedQualities.get(i); 807 int itemId = qualityToItemId(quality); 808 menu.add(groupId, itemId, autoOrder + 1 + i, getQualityMenuItemName(quality)); 809 if (mVideoQuality == quality) { 810 menu.findItem(itemId).setChecked(true); 811 } 812 813 } 814 // Make menu single checkable 815 menu.setGroupCheckable(groupId, true, true); 816 817 popup.setOnMenuItemClickListener(item -> { 818 Quality quality = itemIdToQuality(item.getItemId()); 819 if (quality != mVideoQuality) { 820 mVideoQuality = quality; 821 mRecordUi.getButtonQuality().setText(getQualityIconName(mVideoQuality)); 822 // Quality changed, rebind UseCases 823 tryBindUseCases(); 824 } 825 return true; 826 }); 827 828 popup.show(); 829 }); 830 831 Runnable buttonMuteUpdater = () -> mRecordUi.getButtonMute().setImageResource( 832 mAudioMuted ? R.drawable.ic_mic_off : R.drawable.ic_mic_on); 833 buttonMuteUpdater.run(); 834 mRecordUi.getButtonMute().setOnClickListener(view -> { 835 mAudioMuted = !mAudioMuted; 836 if (mActiveRecording != null) { 837 mActiveRecording.mute(mAudioMuted); 838 } 839 buttonMuteUpdater.run(); 840 }); 841 } 842 setUpDynamicRangeButton()843 private void setUpDynamicRangeButton() { 844 mDynamicRangeUi.setDisplayedDynamicRange(mDynamicRange); 845 mDynamicRangeUi.getButton().setOnClickListener(view -> { 846 PopupMenu popup = new PopupMenu(this, view); 847 Menu menu = popup.getMenu(); 848 849 final int groupId = Menu.NONE; 850 for (DynamicRange dynamicRange : mSelectableDynamicRanges) { 851 int itemId = dynamicRangeToItemId(dynamicRange); 852 menu.add(groupId, itemId, itemId, getDynamicRangeMenuItemName(dynamicRange)); 853 if (Objects.equals(dynamicRange, mDynamicRange)) { 854 // Apply the checked item for the selected dynamic range to the menu. 855 menu.findItem(itemId).setChecked(true); 856 } 857 } 858 859 // Make menu single checkable 860 menu.setGroupCheckable(groupId, true, true); 861 862 popup.setOnMenuItemClickListener(item -> { 863 DynamicRange dynamicRange = itemIdToDynamicRange(item.getItemId()); 864 if (!Objects.equals(dynamicRange, mDynamicRange)) { 865 setSelectedDynamicRange(dynamicRange); 866 // Dynamic range changed, rebind UseCases 867 tryBindUseCases(); 868 } 869 return true; 870 }); 871 872 popup.show(); 873 }); 874 } 875 setUpImageOutputFormatButton()876 private void setUpImageOutputFormatButton() { 877 mButtonImageOutputFormat.setText(getImageOutputFormatIconName(mImageOutputFormat)); 878 mButtonImageOutputFormat.setOnClickListener(view -> { 879 PopupMenu popup = new PopupMenu(this, view); 880 Menu menu = popup.getMenu(); 881 final int groupId = Menu.NONE; 882 883 // Add device supported output formats. 884 ImageCaptureCapabilities capabilities = getImageCaptureCapabilities( 885 mCamera.getCameraInfo()); 886 Set<Integer> supportedOutputFormats = capabilities.getSupportedOutputFormats(); 887 for (int supportedOutputFormat : supportedOutputFormats) { 888 // Add output format item to menu. 889 final int menuItemId = imageOutputFormatToItemId(supportedOutputFormat); 890 final int order = menu.size(); 891 final String menuItemName = getImageOutputFormatMenuItemName(supportedOutputFormat); 892 893 menu.add(groupId, menuItemId, order, menuItemName); 894 if (mImageOutputFormat == supportedOutputFormat) { 895 menu.findItem(menuItemId).setChecked(true); 896 } 897 } 898 899 // Make menu single checkable. 900 menu.setGroupCheckable(groupId, true, true); 901 902 // Set item click listener. 903 popup.setOnMenuItemClickListener(item -> { 904 int outputFormat = itemIdToImageOutputFormat(item.getItemId()); 905 if (outputFormat != mImageOutputFormat) { 906 mImageOutputFormat = outputFormat; 907 final String newIconName = getImageOutputFormatIconName(mImageOutputFormat); 908 mButtonImageOutputFormat.setText(newIconName); 909 910 // Output format changed, rebind UseCases. 911 tryBindUseCases(); 912 } 913 return true; 914 }); 915 916 popup.show(); 917 }); 918 } 919 setSelectedDynamicRange(@onNull DynamicRange dynamicRange)920 private void setSelectedDynamicRange(@NonNull DynamicRange dynamicRange) { 921 mDynamicRange = dynamicRange; 922 if (Build.VERSION.SDK_INT >= 26) { 923 updateWindowColorMode(); 924 } 925 mDynamicRangeUi.setDisplayedDynamicRange(mDynamicRange); 926 } 927 928 @RequiresApi(26) updateWindowColorMode()929 private void updateWindowColorMode() { 930 int colorMode = ActivityInfo.COLOR_MODE_DEFAULT; 931 if (!Objects.equals(mDynamicRange, DynamicRange.SDR)) { 932 colorMode = ActivityInfo.COLOR_MODE_HDR; 933 } 934 Api26Impl.setColorMode(requireNonNull(getWindow()), colorMode); 935 } 936 hasTenBitDynamicRange(@onNull Set<DynamicRange> dynamicRanges)937 private static boolean hasTenBitDynamicRange(@NonNull Set<DynamicRange> dynamicRanges) { 938 for (DynamicRange dynamicRange : dynamicRanges) { 939 if (dynamicRange.getBitDepth() == DynamicRange.BIT_DEPTH_10_BIT) { 940 return true; 941 } 942 } 943 return false; 944 } 945 946 private final Consumer<VideoRecordEvent> mVideoRecordEventListener = event -> { 947 updateRecordingStats(event.getRecordingStats()); 948 949 if (event instanceof VideoRecordEvent.Finalize) { 950 VideoRecordEvent.Finalize finalize = (VideoRecordEvent.Finalize) event; 951 952 switch (finalize.getError()) { 953 case ERROR_NONE: 954 case ERROR_FILE_SIZE_LIMIT_REACHED: 955 case ERROR_DURATION_LIMIT_REACHED: 956 case ERROR_INSUFFICIENT_STORAGE: 957 case ERROR_SOURCE_INACTIVE: 958 Uri uri = finalize.getOutputResults().getOutputUri(); 959 OutputOptions outputOptions = finalize.getOutputOptions(); 960 String msg; 961 String videoFilePath; 962 if (outputOptions instanceof MediaStoreOutputOptions) { 963 msg = "Saved uri " + uri; 964 videoFilePath = getAbsolutePathFromUri( 965 getApplicationContext().getContentResolver(), 966 uri 967 ); 968 updateVideoSavedSessionData(uri); 969 } else if (outputOptions instanceof FileOutputOptions) { 970 videoFilePath = ((FileOutputOptions) outputOptions).getFile().getPath(); 971 MediaScannerConnection.scanFile(this, 972 new String[]{videoFilePath}, null, 973 (path, uri1) -> { 974 Log.i(TAG, "Scanned " + path + " -> uri= " + uri1); 975 updateVideoSavedSessionData(uri1); 976 }); 977 msg = "Saved file " + videoFilePath; 978 } else { 979 throw new AssertionError("Unknown or unsupported OutputOptions type: " 980 + outputOptions.getClass().getSimpleName()); 981 } 982 // The video file path is used in tracing e2e test log. Don't remove it. 983 Log.d(TAG, "Saved video file: " + videoFilePath); 984 985 if (finalize.getError() != ERROR_NONE) { 986 msg += " with code (" + finalize.getError() + ")"; 987 } 988 Log.d(TAG, msg, finalize.getCause()); 989 Toast.makeText(CameraXActivity.this, msg, Toast.LENGTH_LONG).show(); 990 break; 991 default: 992 String errMsg = "Video capture failed by (" + finalize.getError() + "): " 993 + finalize.getCause(); 994 Log.e(TAG, errMsg, finalize.getCause()); 995 Toast.makeText(CameraXActivity.this, errMsg, Toast.LENGTH_LONG).show(); 996 } 997 mRecordUi.setState(RecordUi.State.IDLE); 998 } 999 }; 1000 updateVideoSavedSessionData(@onNull Uri uri)1001 private void updateVideoSavedSessionData(@NonNull Uri uri) { 1002 if (mSessionVideosUriSet != null) { 1003 mSessionVideosUriSet.add(uri); 1004 } 1005 1006 if (!mVideoSavedIdlingResource.isIdleNow()) { 1007 mVideoSavedIdlingResource.decrement(); 1008 } 1009 } 1010 updateRecordingStats(@onNull RecordingStats stats)1011 private void updateRecordingStats(@NonNull RecordingStats stats) { 1012 double durationMs = TimeUnit.NANOSECONDS.toMillis(stats.getRecordedDurationNanos()); 1013 // Show megabytes in International System of Units (SI) 1014 double sizeMb = stats.getNumBytesRecorded() / (1000d * 1000d); 1015 String msg = String.format("%.2f sec\n%.2f MB", durationMs / 1000d, sizeMb); 1016 mRecordUi.getTextStats().setText(msg); 1017 1018 if (mVideoCaptureAutoStopLength > 0 && durationMs >= mVideoCaptureAutoStopLength 1019 && mRecordUi.getState() == RecordUi.State.RECORDING) { 1020 mRecordUi.getButtonRecord().callOnClick(); 1021 } 1022 } 1023 setUpTakePictureButton()1024 private void setUpTakePictureButton() { 1025 mTakePicture.setOnClickListener( 1026 new View.OnClickListener() { 1027 long mStartCaptureTime = 0; 1028 1029 @Override 1030 public void onClick(View view) { 1031 mImageSavedIdlingResource.increment(); 1032 mStartCaptureTime = SystemClock.elapsedRealtime(); 1033 1034 ImageCapture.OnImageSavedCallback callback = new ImageCapture 1035 .OnImageSavedCallback() { 1036 @Override 1037 public void onImageSaved( 1038 ImageCapture.@NonNull OutputFileResults 1039 outputFileResults) { 1040 Log.d(TAG, "Saved image to " 1041 + outputFileResults.getSavedUri()); 1042 try { 1043 mImageSavedIdlingResource.decrement(); 1044 } catch (IllegalStateException e) { 1045 Log.e(TAG, "Error: unexpected onImageSaved " 1046 + "callback received. Continuing."); 1047 } 1048 1049 long duration = 1050 SystemClock.elapsedRealtime() 1051 - mStartCaptureTime; 1052 runOnUiThread(() -> Toast.makeText(CameraXActivity.this, 1053 "Image captured in " + duration + " ms", 1054 Toast.LENGTH_SHORT).show()); 1055 if (mSessionImagesUriSet != null) { 1056 mSessionImagesUriSet.add( 1057 requireNonNull( 1058 outputFileResults.getSavedUri())); 1059 } 1060 } 1061 1062 @Override 1063 public void onError( 1064 @NonNull ImageCaptureException exception) { 1065 Log.e(TAG, "Failed to save image.", exception); 1066 1067 mLastTakePictureErrorMessage = 1068 getImageCaptureErrorMessage(exception); 1069 if (!mImageSavedIdlingResource.isIdleNow()) { 1070 mImageSavedIdlingResource.decrement(); 1071 } 1072 } 1073 }; 1074 1075 if (mImageOutputFormat == OUTPUT_FORMAT_RAW_JPEG) { 1076 ImageCapture.OutputFileOptions rawOutputFileOptions = 1077 createOutputFileOptions(OUTPUT_FORMAT_RAW); 1078 ImageCapture.OutputFileOptions jpegOutputFileOptions = 1079 createOutputFileOptions(OUTPUT_FORMAT_JPEG); 1080 getImageCapture().takePicture( 1081 rawOutputFileOptions, 1082 jpegOutputFileOptions, 1083 mImageCaptureExecutorService, 1084 callback); 1085 } else { 1086 ImageCapture.OutputFileOptions outputFileOptions = 1087 createOutputFileOptions(mImageOutputFormat); 1088 getImageCapture().takePicture( 1089 outputFileOptions, 1090 mImageCaptureExecutorService, 1091 callback); 1092 } 1093 } 1094 }); 1095 } 1096 1097 @SuppressLint("RestrictedApiAndroidX") createOutputFileOptions( @mageCapture.OutputFormat int imageOutputFormat)1098 private ImageCapture.@NonNull OutputFileOptions createOutputFileOptions( 1099 @ImageCapture.OutputFormat int imageOutputFormat) { 1100 createDefaultPictureFolderIfNotExist(); 1101 Format formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", 1102 Locale.US); 1103 1104 String suffix = ""; 1105 String mimetype = ""; 1106 switch (imageOutputFormat) { 1107 case OUTPUT_FORMAT_RAW: 1108 suffix = ".dng"; 1109 mimetype = "image/x-adobe-dng"; 1110 break; 1111 case OUTPUT_FORMAT_JPEG_ULTRA_HDR: 1112 case OUTPUT_FORMAT_JPEG: 1113 suffix = ".jpg"; 1114 mimetype = "image/jpeg"; 1115 break; 1116 } 1117 String fileName = "CoreTestApp-" + formatter.format( 1118 Calendar.getInstance().getTime()) + suffix; 1119 1120 ContentValues contentValues = new ContentValues(); 1121 contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName); 1122 contentValues.put(MediaStore.MediaColumns.MIME_TYPE, mimetype); 1123 return new ImageCapture.OutputFileOptions.Builder( 1124 getContentResolver(), 1125 MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 1126 contentValues).build(); 1127 } 1128 1129 getImageCaptureErrorMessage(@onNull ImageCaptureException exception)1130 private String getImageCaptureErrorMessage(@NonNull ImageCaptureException exception) { 1131 String errorCodeString; 1132 int errorCode = exception.getImageCaptureError(); 1133 1134 switch (errorCode) { 1135 case ERROR_UNKNOWN: 1136 errorCodeString = "ImageCaptureErrorCode: ERROR_UNKNOWN"; 1137 break; 1138 case ERROR_FILE_IO: 1139 errorCodeString = "ImageCaptureErrorCode: ERROR_FILE_IO"; 1140 break; 1141 case ERROR_CAPTURE_FAILED: 1142 errorCodeString = "ImageCaptureErrorCode: ERROR_CAPTURE_FAILED"; 1143 break; 1144 case ERROR_CAMERA_CLOSED: 1145 errorCodeString = "ImageCaptureErrorCode: ERROR_CAMERA_CLOSED"; 1146 break; 1147 case ERROR_INVALID_CAMERA: 1148 errorCodeString = "ImageCaptureErrorCode: ERROR_INVALID_CAMERA"; 1149 break; 1150 default: 1151 errorCodeString = "ImageCaptureErrorCode: " + errorCode; 1152 break; 1153 } 1154 1155 return errorCodeString + ", Message: " + exception.getMessage() + ", Cause: " 1156 + exception.getCause(); 1157 } 1158 1159 @SuppressWarnings("ObjectToString") setUpCameraDirectionButton()1160 private void setUpCameraDirectionButton() { 1161 mCameraDirectionButton.setOnClickListener(v -> { 1162 Log.d(TAG, "Change camera direction: " + mCurrentCameraSelector); 1163 CameraSelector switchedCameraSelector = 1164 getSwitchedCameraSelector(mCurrentCameraSelector); 1165 try { 1166 if (isUseCasesCombinationSupported(switchedCameraSelector, mUseCases)) { 1167 mCurrentCameraSelector = switchedCameraSelector; 1168 tryBindUseCases(); 1169 } else { 1170 String msg = "Camera of the other lens facing can't support current use case " 1171 + "combination."; 1172 Log.d(TAG, msg); 1173 Toast.makeText(this, msg, Toast.LENGTH_LONG).show(); 1174 } 1175 } catch (IllegalArgumentException e) { 1176 Toast.makeText(this, "Failed to switch Camera. Error:" + e.getMessage(), 1177 Toast.LENGTH_SHORT).show(); 1178 } 1179 }); 1180 } 1181 getSwitchedCameraSelector( @onNull CameraSelector currentCameraSelector)1182 private @NonNull CameraSelector getSwitchedCameraSelector( 1183 @NonNull CameraSelector currentCameraSelector) { 1184 CameraSelector switchedCameraSelector; 1185 // When the activity is launched with a specific camera id, camera switch function 1186 // will switch the cameras between the camera of the specified camera id and the 1187 // default camera of the opposite lens facing. 1188 if (mLaunchingCameraIdSelector != null) { 1189 if (currentCameraSelector != mLaunchingCameraIdSelector) { 1190 switchedCameraSelector = mLaunchingCameraIdSelector; 1191 } else { 1192 if (mLaunchingCameraLensFacing == CameraSelector.LENS_FACING_BACK) { 1193 switchedCameraSelector = FRONT_SELECTOR; 1194 } else { 1195 switchedCameraSelector = BACK_SELECTOR; 1196 } 1197 } 1198 } else { 1199 if (currentCameraSelector == BACK_SELECTOR) { 1200 switchedCameraSelector = FRONT_SELECTOR; 1201 } else if (currentCameraSelector == FRONT_SELECTOR) { 1202 if (mExternalCameraSelector != null) { 1203 switchedCameraSelector = mExternalCameraSelector; 1204 } else { 1205 switchedCameraSelector = BACK_SELECTOR; 1206 } 1207 } else { 1208 switchedCameraSelector = BACK_SELECTOR; 1209 } 1210 } 1211 1212 return switchedCameraSelector; 1213 } 1214 isUseCasesCombinationSupported(@onNull CameraSelector cameraSelector, @NonNull List<UseCase> useCases)1215 private boolean isUseCasesCombinationSupported(@NonNull CameraSelector cameraSelector, 1216 @NonNull List<UseCase> useCases) { 1217 if (mCameraProvider == null) { 1218 throw new IllegalStateException("Need to obtain mCameraProvider first!"); 1219 } 1220 1221 Camera targetCamera = mCameraProvider.bindToLifecycle(this, cameraSelector); 1222 return targetCamera.isUseCasesCombinationSupported(useCases.toArray(new UseCase[0])); 1223 } 1224 setUpTorchButton()1225 private void setUpTorchButton() { 1226 mTorchButton.setOnClickListener(v -> { 1227 requireNonNull(getCameraInfo()); 1228 requireNonNull(getCameraControl()); 1229 Integer torchState = getCameraInfo().getTorchState().getValue(); 1230 boolean toggledState = !Objects.equals(torchState, TorchState.ON); 1231 Log.d(TAG, "Set camera torch: " + toggledState); 1232 ListenableFuture<Void> future = getCameraControl().enableTorch(toggledState); 1233 Futures.addCallback(future, new FutureCallback<Void>() { 1234 @Override 1235 public void onSuccess(@Nullable Void result) { 1236 } 1237 1238 @Override 1239 public void onFailure(@NonNull Throwable t) { 1240 throw new RuntimeException(t); 1241 } 1242 }, CameraXExecutors.directExecutor()); 1243 }); 1244 } 1245 setUpEVButton()1246 private void setUpEVButton() { 1247 mPlusEV.setOnClickListener(v -> { 1248 requireNonNull(getCameraInfo()); 1249 requireNonNull(getCameraControl()); 1250 1251 ExposureState exposureState = getCameraInfo().getExposureState(); 1252 Range<Integer> range = exposureState.getExposureCompensationRange(); 1253 int ec = exposureState.getExposureCompensationIndex(); 1254 1255 if (range.contains(ec + 1)) { 1256 ListenableFuture<Integer> future = 1257 getCameraControl().setExposureCompensationIndex(ec + 1); 1258 Futures.addCallback(future, mEVFutureCallback, 1259 CameraXExecutors.mainThreadExecutor()); 1260 } else { 1261 showEVToast(String.format("EV: %.2f", range.getUpper() 1262 * exposureState.getExposureCompensationStep().floatValue())); 1263 } 1264 }); 1265 1266 mDecEV.setOnClickListener(v -> { 1267 requireNonNull(getCameraInfo()); 1268 requireNonNull(getCameraControl()); 1269 1270 ExposureState exposureState = getCameraInfo().getExposureState(); 1271 Range<Integer> range = exposureState.getExposureCompensationRange(); 1272 int ec = exposureState.getExposureCompensationIndex(); 1273 1274 if (range.contains(ec - 1)) { 1275 ListenableFuture<Integer> future = 1276 getCameraControl().setExposureCompensationIndex(ec - 1); 1277 Futures.addCallback(future, mEVFutureCallback, 1278 CameraXExecutors.mainThreadExecutor()); 1279 } else { 1280 showEVToast(String.format("EV: %.2f", range.getLower() 1281 * exposureState.getExposureCompensationStep().floatValue())); 1282 } 1283 }); 1284 } 1285 showEVToast(String message)1286 void showEVToast(String message) { 1287 if (mEvToast != null) { 1288 mEvToast.cancel(); 1289 } 1290 mEvToast = Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT); 1291 mEvToast.show(); 1292 } 1293 showPreviewStabilizationToast(String message)1294 void showPreviewStabilizationToast(String message) { 1295 if (mPSToast != null) { 1296 mPSToast.cancel(); 1297 } 1298 mPSToast = Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT); 1299 mPSToast.show(); 1300 } 1301 updateAppUIForE2ETest()1302 private void updateAppUIForE2ETest() { 1303 Bundle bundle = getIntent().getExtras(); 1304 if (bundle == null) { 1305 return; 1306 } 1307 1308 String testCase = bundle.getString(INTENT_EXTRA_E2E_TEST_CASE); 1309 if (testCase == null) { 1310 return; 1311 } 1312 1313 if (getSupportActionBar() != null) { 1314 getSupportActionBar().hide(); 1315 } 1316 mCaptureQualityToggle.setVisibility(View.GONE); 1317 mZslToggle.setVisibility(View.GONE); 1318 mPlusEV.setVisibility(View.GONE); 1319 mDecEV.setVisibility(View.GONE); 1320 mZoomSeekBar.setVisibility(View.GONE); 1321 mZoomRatioLabel.setVisibility(View.GONE); 1322 mTextView.setVisibility(View.GONE); 1323 1324 if (testCase.equals(PREVIEW_TEST_CASE) || testCase.equals(SWITCH_TEST_CASE)) { 1325 mTorchButton.setVisibility(View.GONE); 1326 mFlashButton.setVisibility(View.GONE); 1327 mTakePicture.setVisibility(View.GONE); 1328 mZoomIn2XToggle.setVisibility(View.GONE); 1329 mZoomResetToggle.setVisibility(View.GONE); 1330 mVideoToggle.setVisibility(View.GONE); 1331 mPhotoToggle.setVisibility(View.GONE); 1332 mPreviewToggle.setVisibility(View.GONE); 1333 mAnalysisToggle.setVisibility(View.GONE); 1334 mDynamicRangeUi.getButton().setVisibility(View.GONE); 1335 mButtonImageOutputFormat.setVisibility(View.GONE); 1336 mRecordUi.hideUi(); 1337 mPreviewStabilizationToggle.setVisibility(View.GONE); 1338 mLowLightBoostToggle.setVisibility(View.GONE); 1339 if (!testCase.equals(SWITCH_TEST_CASE)) { 1340 mCameraDirectionButton.setVisibility(View.GONE); 1341 } 1342 } 1343 } 1344 updatePreviewRatioAndScaleTypeByIntent(ViewStub viewFinderStub)1345 private void updatePreviewRatioAndScaleTypeByIntent(ViewStub viewFinderStub) { 1346 Bundle bundle = this.getIntent().getExtras(); 1347 if (bundle != null) { 1348 mTargetAspectRatio = bundle.getInt(INTENT_EXTRA_TARGET_ASPECT_RATIO, 1349 AspectRatio.RATIO_DEFAULT); 1350 int scaleType = bundle.getInt(INTENT_EXTRA_SCALE_TYPE, INTENT_EXTRA_FILL_CENTER); 1351 if (scaleType == INTENT_EXTRA_FIT_CENTER) { 1352 // Scale the view according to the target aspect ratio, display size and device 1353 // orientation, so preview can be entirely contained within the view. 1354 DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); 1355 Rational ratio = (mTargetAspectRatio == AspectRatio.RATIO_16_9) 1356 ? AspectRatioUtil.ASPECT_RATIO_16_9 : AspectRatioUtil.ASPECT_RATIO_4_3; 1357 int orientation = getResources().getConfiguration().orientation; 1358 ViewGroup.LayoutParams lp = viewFinderStub.getLayoutParams(); 1359 if (orientation == Configuration.ORIENTATION_PORTRAIT) { 1360 lp.width = displayMetrics.widthPixels; 1361 lp.height = displayMetrics.widthPixels / ratio.getDenominator() 1362 * ratio.getNumerator(); 1363 } else { 1364 lp.height = displayMetrics.heightPixels; 1365 lp.width = displayMetrics.heightPixels / ratio.getDenominator() 1366 * ratio.getNumerator(); 1367 } 1368 viewFinderStub.setLayoutParams(lp); 1369 } 1370 } 1371 } 1372 updateDynamicRangeUiState()1373 private void updateDynamicRangeUiState() { 1374 // Only show dynamic range if video or preview are enabled 1375 boolean visible = (mVideoToggle.isChecked() || mPreviewToggle.isChecked()); 1376 // Dynamic range is configurable if it's visible, there's more than 1 choice, and there 1377 // isn't a recording in progress 1378 boolean configurable = visible 1379 && mSelectableDynamicRanges.size() > 1 1380 && mRecordUi.getState() != RecordUi.State.RECORDING; 1381 1382 if (configurable) { 1383 mDynamicRangeUi.setState(DynamicRangeUi.State.CONFIGURABLE); 1384 } else if (visible) { 1385 mDynamicRangeUi.setState(DynamicRangeUi.State.VISIBLE); 1386 } else { 1387 mDynamicRangeUi.setState(DynamicRangeUi.State.HIDDEN); 1388 } 1389 } 1390 updateImageOutputFormatUiState()1391 private void updateImageOutputFormatUiState() { 1392 int visible = mPhotoToggle.isChecked() ? View.VISIBLE : View.GONE; 1393 mButtonImageOutputFormat.setVisibility(visible); 1394 } 1395 1396 @SuppressLint({"NullAnnotationGroup", "RestrictedApiAndroidX"}) 1397 @OptIn(markerClass = androidx.camera.core.ExperimentalZeroShutterLag.class) updateButtonsUi()1398 private void updateButtonsUi() { 1399 mRecordUi.setEnabled(mVideoToggle.isChecked()); 1400 updateDynamicRangeUiState(); 1401 updateImageOutputFormatUiState(); 1402 1403 mTakePicture.setEnabled(mPhotoToggle.isChecked()); 1404 mCaptureQualityToggle.setEnabled(mPhotoToggle.isChecked()); 1405 mZslToggle.setVisibility(getCameraInfo() != null 1406 && getCameraInfo().isZslSupported() ? View.VISIBLE : View.GONE); 1407 mZslToggle.setEnabled(mPhotoToggle.isChecked()); 1408 mCameraDirectionButton.setEnabled(getCameraInfo() != null); 1409 mPreviewStabilizationToggle.setEnabled(mCamera != null 1410 && Preview.getPreviewCapabilities(getCameraInfo()).isStabilizationSupported()); 1411 mLowLightBoostToggle.setEnabled( 1412 mCamera != null && mCamera.getCameraInfo().isLowLightBoostSupported()); 1413 mTorchButton.setEnabled(isFlashUnitAvailable()); 1414 // Flash button 1415 mFlashButton.setEnabled(isFlashAvailable()); 1416 if (mPhotoToggle.isChecked()) { 1417 int flashMode = getImageCapture().getFlashMode(); 1418 switch (flashMode) { 1419 case FLASH_MODE_ON: 1420 mFlashButton.setImageResource(R.drawable.ic_flash_on); 1421 break; 1422 case FLASH_MODE_OFF: 1423 mFlashButton.setImageResource(R.drawable.ic_flash_off); 1424 break; 1425 case FLASH_MODE_AUTO: 1426 mFlashButton.setImageResource(R.drawable.ic_flash_auto); 1427 break; 1428 case FLASH_MODE_SCREEN: 1429 mFlashButton.setImageResource(R.drawable.ic_flash_screen); 1430 break; 1431 } 1432 } 1433 setFlashButtonContentDescription(); 1434 1435 mPlusEV.setEnabled(isExposureCompensationSupported()); 1436 mDecEV.setEnabled(isExposureCompensationSupported()); 1437 mZoomIn2XToggle.setEnabled(is2XZoomSupported()); 1438 1439 // this function may make some view visible again, so need to update for E2E tests again 1440 updateAppUIForE2ETest(); 1441 1442 invalidateOptionsMenu(); 1443 } 1444 1445 // Set or reset content description for e2e testing. setFlashButtonContentDescription()1446 private void setFlashButtonContentDescription() { 1447 // This is set even if button is not enabled, to better represent why it is not enabled. 1448 if (!isFlashUnitAvailable()) { 1449 mFlashButton.setContentDescription(DESCRIPTION_FLASH_UNIT_NOT_AVAILABLE); 1450 } 1451 1452 if (!mPhotoToggle.isChecked()) { 1453 return; 1454 } 1455 1456 int flashMode = getImageCapture().getFlashMode(); 1457 1458 // Button may be enabled even when flash unit is not available, due to screen flash. 1459 if (isFlashUnitAvailable()) { 1460 // Even if flash unit is available, some flash modes still may not be suitable for tests 1461 if (isFlashTestSupported(flashMode)) { 1462 // Reset content description if flash is ready for test. 1463 // TODO: Set content description specific to flash mode, may need to check the 1464 // E2E tests first if that will be okay. 1465 mFlashButton.setContentDescription(""); 1466 } else { 1467 mFlashButton.setContentDescription(DESCRIPTION_FLASH_MODE_NOT_SUPPORTED); 1468 } 1469 } 1470 1471 // Screen flash does not depend on flash unit or the quirks in isFlashTestSupported, so 1472 // will override the previously set descriptions without any concern to those. 1473 if (flashMode == FLASH_MODE_SCREEN) { 1474 if (isLegacyDevice(requireNonNull(getCameraInfo()))) { 1475 mFlashButton.setContentDescription( 1476 DESCRIPTION_SCREEN_FLASH_NOT_SUPPORTED_LEGACY); 1477 } else { 1478 mFlashButton.setContentDescription(DESCRIPTION_FLASH_MODE_SCREEN); 1479 } 1480 } 1481 1482 Log.d(TAG, "Flash Button content description = " + mFlashButton.getContentDescription()); 1483 } 1484 setUpButtonEvents()1485 private void setUpButtonEvents() { 1486 mVideoToggle.setOnCheckedChangeListener(mOnCheckedChangeListener); 1487 mPhotoToggle.setOnCheckedChangeListener(mOnCheckedChangeListener); 1488 mAnalysisToggle.setOnCheckedChangeListener(mOnCheckedChangeListener); 1489 mPreviewToggle.setOnCheckedChangeListener(mOnCheckedChangeListener); 1490 1491 setUpRecordButton(); 1492 setUpDynamicRangeButton(); 1493 setUpImageOutputFormatButton(); 1494 setUpFlashButton(); 1495 setUpTakePictureButton(); 1496 setUpCameraDirectionButton(); 1497 setUpTorchButton(); 1498 setUpEVButton(); 1499 setUpZoomButton(); 1500 setUpPreviewStabilizationButton(); 1501 mCaptureQualityToggle.setOnCheckedChangeListener(mOnCheckedChangeListener); 1502 mZslToggle.setOnCheckedChangeListener(mOnCheckedChangeListener); 1503 } 1504 updateUseCaseCombinationByIntent(@onNull Intent intent)1505 private void updateUseCaseCombinationByIntent(@NonNull Intent intent) { 1506 Bundle bundle = intent.getExtras(); 1507 1508 if (bundle == null) { 1509 return; 1510 } 1511 1512 int useCaseCombination = bundle.getInt(INTENT_EXTRA_USE_CASE_COMBINATION, 0); 1513 1514 if (useCaseCombination == 0) { 1515 return; 1516 } 1517 1518 mPreviewToggle.setChecked((useCaseCombination & BIND_PREVIEW) != 0L); 1519 mPhotoToggle.setChecked((useCaseCombination & BIND_IMAGE_CAPTURE) != 0L); 1520 mVideoToggle.setChecked((useCaseCombination & BIND_VIDEO_CAPTURE) != 0L); 1521 mAnalysisToggle.setChecked((useCaseCombination & BIND_IMAGE_ANALYSIS) != 0L); 1522 } 1523 updateStreamSharingForceEnableStateByIntent(@onNull Intent intent)1524 private void updateStreamSharingForceEnableStateByIntent(@NonNull Intent intent) { 1525 Bundle bundle = intent.getExtras(); 1526 if (bundle == null) { 1527 return; 1528 } 1529 1530 mForceEnableStreamSharing = bundle.getBoolean(INTENT_EXTRA_FORCE_ENABLE_STREAM_SHARING, 1531 false); 1532 } 1533 updateVideoMirrorModeByIntent(@onNull Intent intent)1534 private void updateVideoMirrorModeByIntent(@NonNull Intent intent) { 1535 int mirrorMode = intent.getIntExtra(INTENT_EXTRA_VIDEO_MIRROR_MODE, -1); 1536 if (mirrorMode != -1) { 1537 Log.d(TAG, "updateVideoMirrorModeByIntent: mirrorMode = " + mirrorMode); 1538 mVideoMirrorMode = mirrorMode; 1539 } 1540 } 1541 updateVideoQualityByIntent(@onNull Intent intent)1542 private void updateVideoQualityByIntent(@NonNull Intent intent) { 1543 Bundle bundle = intent.getExtras(); 1544 if (bundle == null) { 1545 return; 1546 } 1547 1548 Quality quality = itemIdToQuality(bundle.getInt(INTENT_EXTRA_VIDEO_QUALITY, 0)); 1549 if (quality == QUALITY_AUTO || !mVideoToggle.isChecked()) { 1550 return; 1551 } 1552 1553 if (mCameraProvider == null) { 1554 throw new IllegalStateException("Need to obtain mCameraProvider first!"); 1555 } 1556 1557 // Check and set specific quality. 1558 Camera targetCamera = mCameraProvider.bindToLifecycle(this, mCurrentCameraSelector); 1559 VideoCapabilities videoCapabilities = Recorder.getVideoCapabilities( 1560 targetCamera.getCameraInfo()); 1561 List<Quality> supportedQualities = videoCapabilities.getSupportedQualities(mDynamicRange); 1562 if (supportedQualities.contains(quality)) { 1563 mVideoQuality = quality; 1564 mRecordUi.getButtonQuality().setText(getQualityIconName(mVideoQuality)); 1565 } 1566 } 1567 1568 @SuppressLint("NullAnnotationGroup") 1569 @OptIn(markerClass = ExperimentalLensFacing.class) 1570 @Override onCreate(@ullable Bundle savedInstanceState)1571 protected void onCreate(@Nullable Bundle savedInstanceState) { 1572 super.onCreate(savedInstanceState); 1573 1574 //if different Camera Provider (CameraPipe vs Camera2 was initialized in previous session, 1575 //then close this application. 1576 closeAppIfCameraProviderMismatch(this.getIntent()); 1577 1578 setContentView(R.layout.activity_camera_xmain); 1579 mFileWriterExecutorService = Executors.newSingleThreadExecutor(); 1580 mImageCaptureExecutorService = Executors.newSingleThreadExecutor(); 1581 mDisplaySupportedHighDynamicRanges = Collections.emptySet(); 1582 if (Build.VERSION.SDK_INT >= 30) { 1583 Display display = OpenGLActivity.Api30Impl.getDisplay(this); 1584 mDisplaySupportedHighDynamicRanges = 1585 OpenGLActivity.getHighDynamicRangesSupportedByDisplay(display); 1586 } 1587 OpenGLRenderer previewRenderer = mPreviewRenderer = 1588 new OpenGLRenderer(mDisplaySupportedHighDynamicRanges); 1589 ViewStub viewFinderStub = findViewById(R.id.viewFinderStub); 1590 updatePreviewRatioAndScaleTypeByIntent(viewFinderStub); 1591 updateVideoMirrorModeByIntent(getIntent()); 1592 1593 Bundle bundle = this.getIntent().getExtras(); 1594 1595 mViewFinder = OpenGLActivity.chooseViewFinder(getIntent().getExtras(), viewFinderStub, 1596 previewRenderer); 1597 mViewFinder.addOnLayoutChangeListener( 1598 (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) 1599 -> { 1600 tryBindUseCases(); 1601 1602 if (bundle == null) { 1603 return; 1604 } 1605 1606 if (!bundle.getBoolean(INTENT_EXTRA_LOG_VIEWFINDER_POSITION)) { 1607 return; 1608 } 1609 1610 Rect rect = new Rect(); 1611 v.getGlobalVisibleRect(rect); 1612 1613 String viewFinderPositionText = 1614 rect.left + "," + rect.top + "," + rect.right + "," + rect.bottom; 1615 String fileName = "camerax_view_finder_position_" + System.currentTimeMillis(); 1616 writeTextToExternalStorage(viewFinderPositionText, fileName, "txt"); 1617 }); 1618 1619 mVideoToggle = findViewById(R.id.VideoToggle); 1620 mPhotoToggle = findViewById(R.id.PhotoToggle); 1621 mAnalysisToggle = findViewById(R.id.AnalysisToggle); 1622 mPreviewToggle = findViewById(R.id.PreviewToggle); 1623 1624 updateUseCaseCombinationByIntent(getIntent()); 1625 updateStreamSharingForceEnableStateByIntent(getIntent()); 1626 1627 mTakePicture = findViewById(R.id.Picture); 1628 mFlashButton = findViewById(R.id.flash_toggle); 1629 mScreenFlashView = findViewById(R.id.screen_flash_view); 1630 mCameraDirectionButton = findViewById(R.id.direction_toggle); 1631 mTorchButton = findViewById(R.id.torch_toggle); 1632 mTorchStrengthText = findViewById(R.id.torchStrength); 1633 mTorchStrengthSeekBar = findViewById(R.id.torchStrengthBar); 1634 mCaptureQualityToggle = findViewById(R.id.capture_quality); 1635 mPlusEV = findViewById(R.id.plus_ev_toggle); 1636 mDecEV = findViewById(R.id.dec_ev_toggle); 1637 mZslToggle = findViewById(R.id.zsl_toggle); 1638 mPreviewStabilizationToggle = findViewById(R.id.preview_stabilization); 1639 mLowLightBoostToggle = findViewById(R.id.low_light_boost); 1640 mZoomSeekBar = findViewById(R.id.seekBar); 1641 mZoomRatioLabel = findViewById(R.id.zoomRatio); 1642 mZoomIn2XToggle = findViewById(R.id.zoom_in_2x_toggle); 1643 mZoomResetToggle = findViewById(R.id.zoom_reset_toggle); 1644 1645 mTextView = findViewById(R.id.textView); 1646 mDynamicRangeUi = new DynamicRangeUi(findViewById(R.id.dynamic_range)); 1647 mButtonImageOutputFormat = findViewById(R.id.image_output_format); 1648 mRecordUi = new RecordUi( 1649 findViewById(R.id.Video), 1650 findViewById(R.id.video_pause), 1651 findViewById(R.id.video_stats), 1652 findViewById(R.id.video_quality), 1653 findViewById(R.id.video_persistent), 1654 findViewById(R.id.video_mute), 1655 (newState) -> updateDynamicRangeUiState() 1656 ); 1657 1658 setUpButtonEvents(); 1659 setupViewFinderGestureControls(); 1660 1661 mImageAnalysisResult.observe( 1662 this, 1663 text -> { 1664 if (mImageAnalysisFrameCount.getAndIncrement() % 30 == 0) { 1665 mTextView.setText( 1666 "ImgCount: " + mImageAnalysisFrameCount.get() + " @ts: " 1667 + text); 1668 } 1669 }); 1670 1671 mDisplayListener = new DisplayManager.DisplayListener() { 1672 @Override 1673 public void onDisplayAdded(int displayId) { 1674 1675 } 1676 1677 @Override 1678 public void onDisplayRemoved(int displayId) { 1679 1680 } 1681 1682 @Override 1683 public void onDisplayChanged(int displayId) { 1684 Display viewFinderDisplay = mViewFinder.getDisplay(); 1685 if (viewFinderDisplay != null && viewFinderDisplay.getDisplayId() == displayId) { 1686 previewRenderer.invalidateSurface( 1687 Surfaces.toSurfaceRotationDegrees(viewFinderDisplay.getRotation())); 1688 } 1689 } 1690 }; 1691 1692 DisplayManager dpyMgr = 1693 requireNonNull(ContextCompat.getSystemService(this, DisplayManager.class)); 1694 dpyMgr.registerDisplayListener(mDisplayListener, new Handler(Looper.getMainLooper())); 1695 1696 StrictMode.VmPolicy vmPolicy = 1697 new StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build(); 1698 StrictMode.setVmPolicy(vmPolicy); 1699 StrictMode.ThreadPolicy threadPolicy = 1700 new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build(); 1701 StrictMode.setThreadPolicy(threadPolicy); 1702 1703 // Get params from adb extra string 1704 if (bundle != null) { 1705 String launchingCameraId = bundle.getString(INTENT_EXTRA_CAMERA_ID, null); 1706 1707 if (launchingCameraId != null) { 1708 mLaunchingCameraIdSelector = createCameraSelectorById(launchingCameraId); 1709 mCurrentCameraSelector = mLaunchingCameraIdSelector; 1710 } else { 1711 String newCameraDirection = bundle.getString(INTENT_EXTRA_CAMERA_DIRECTION); 1712 if (newCameraDirection != null) { 1713 if (newCameraDirection.equals(BACKWARD)) { 1714 mCurrentCameraSelector = BACK_SELECTOR; 1715 } else { 1716 mCurrentCameraSelector = FRONT_SELECTOR; 1717 } 1718 } 1719 } 1720 1721 String cameraImplementation = bundle.getString(INTENT_EXTRA_CAMERA_IMPLEMENTATION); 1722 boolean cameraImplementationNoHistory = 1723 bundle.getBoolean(INTENT_EXTRA_CAMERA_IMPLEMENTATION_NO_HISTORY, false); 1724 if (cameraImplementationNoHistory) { 1725 Intent newIntent = new Intent(getIntent()); 1726 newIntent.removeExtra(INTENT_EXTRA_CAMERA_IMPLEMENTATION); 1727 newIntent.removeExtra(INTENT_EXTRA_CAMERA_IMPLEMENTATION_NO_HISTORY); 1728 setIntent(newIntent); 1729 } 1730 1731 if (cameraImplementation != null) { 1732 if (cameraImplementation.equalsIgnoreCase( 1733 CameraXViewModel.CAMERA_PIPE_IMPLEMENTATION_OPTION)) { 1734 setTitle(APP_TITLE_FOR_CAMERA_PIPE); 1735 } 1736 CameraXViewModel.configureCameraProvider( 1737 cameraImplementation, cameraImplementationNoHistory); 1738 } 1739 1740 // Update the app UI according to the e2e test case. 1741 updateAppUIForE2ETest(); 1742 } 1743 1744 mInitializationIdlingResource.increment(); 1745 CameraXViewModel viewModel = new ViewModelProvider(this).get(CameraXViewModel.class); 1746 viewModel.getCameraProvider().observe(this, cameraProviderResult -> { 1747 mCameraProviderResult = cameraProviderResult; 1748 mInitializationIdlingResource.decrement(); 1749 if (cameraProviderResult.hasProvider()) { 1750 mCameraProvider = cameraProviderResult.getProvider(); 1751 1752 //initialize mExternalCameraSelector 1753 CameraSelector externalCameraSelectorLocal = new CameraSelector.Builder() 1754 .requireLensFacing(CameraSelector.LENS_FACING_EXTERNAL).build(); 1755 List<CameraInfo> cameraInfos = externalCameraSelectorLocal.filter( 1756 mCameraProvider.getAvailableCameraInfos()); 1757 if (cameraInfos.size() > 0) { 1758 mExternalCameraSelector = externalCameraSelectorLocal; 1759 } 1760 1761 updateVideoQualityByIntent(getIntent()); 1762 tryBindUseCases(); 1763 } else { 1764 Log.e(TAG, "Failed to retrieve ProcessCameraProvider", 1765 cameraProviderResult.getError()); 1766 Toast.makeText(getApplicationContext(), "Unable to initialize CameraX. See logs " 1767 + "for details.", Toast.LENGTH_LONG).show(); 1768 } 1769 }); 1770 1771 setupPermissions(); 1772 } 1773 1774 @Override onCreateOptionsMenu(Menu menu)1775 public boolean onCreateOptionsMenu(Menu menu) { 1776 getMenuInflater().inflate(R.menu.actionbar_menu, menu); 1777 return true; 1778 } 1779 1780 @Override onPrepareOptionsMenu(Menu menu)1781 public boolean onPrepareOptionsMenu(Menu menu) { 1782 updateMenuItems(menu); 1783 return true; 1784 } 1785 updateMenuItems(Menu menu)1786 private void updateMenuItems(Menu menu) { 1787 menu.findItem(requireNonNull(getKeyByValue(ID_TO_FPS_RANGE_MAP, mFpsRange))).setChecked( 1788 true); 1789 menu.findItem(R.id.fps).setEnabled(mPreviewToggle.isChecked() || mVideoToggle.isChecked()); 1790 1791 menu.findItem(requireNonNull( 1792 getKeyByValue(ID_TO_ASPECT_RATIO_MAP, mTargetAspectRatio))).setChecked(true); 1793 1794 menu.findItem(R.id.stream_sharing).setChecked(mForceEnableStreamSharing); 1795 // StreamSharing requires both Preview & VideoCapture use cases in core-test-app 1796 // (since ImageCapture can't be added due to lack of effect) 1797 menu.findItem(R.id.stream_sharing).setEnabled( 1798 mPreviewToggle.isChecked() && mVideoToggle.isChecked()); 1799 1800 menu.findItem(R.id.view_port).setChecked(mDisableViewPort); 1801 menu.findItem(R.id.torch_as_flash).setChecked(mEnableTorchAsFlash); 1802 } 1803 getKeyByValue(Map<T, E> map, E value)1804 private static <T, E> T getKeyByValue(Map<T, E> map, E value) { 1805 for (Map.Entry<T, E> entry : map.entrySet()) { 1806 if (Objects.equals(value, entry.getValue())) { 1807 return entry.getKey(); 1808 } 1809 } 1810 return null; // No key found for the given value 1811 } 1812 1813 @Override onOptionsItemSelected(@onNull MenuItem item)1814 public boolean onOptionsItemSelected(@NonNull MenuItem item) { 1815 // Handle item selection. 1816 Log.d(TAG, "onOptionsItemSelected: item = " + item); 1817 1818 int groupId = item.getGroupId(); 1819 int itemId = item.getItemId(); 1820 1821 if (groupId == R.id.fps_group) { 1822 if (ID_TO_FPS_RANGE_MAP.containsKey(itemId)) { 1823 mFpsRange = ID_TO_FPS_RANGE_MAP.get(itemId); 1824 } else { 1825 Log.e(TAG, "Unknown item " + item.getTitle()); 1826 return super.onOptionsItemSelected(item); 1827 } 1828 } else if (groupId == R.id.aspect_ratio_group) { 1829 if (ID_TO_ASPECT_RATIO_MAP.containsKey(itemId)) { 1830 mTargetAspectRatio = requireNonNull(ID_TO_ASPECT_RATIO_MAP.get(itemId)); 1831 } else { 1832 Log.e(TAG, "Unknown item " + item.getTitle()); 1833 return super.onOptionsItemSelected(item); 1834 } 1835 } else if (itemId == R.id.stream_sharing) { 1836 mForceEnableStreamSharing = !mForceEnableStreamSharing; 1837 } else if (itemId == R.id.view_port) { 1838 mDisableViewPort = !mDisableViewPort; 1839 } else if (itemId == R.id.torch_as_flash) { 1840 mEnableTorchAsFlash = !mEnableTorchAsFlash; 1841 } else { 1842 Log.d(TAG, "Not handling item " + item.getTitle()); 1843 return super.onOptionsItemSelected(item); 1844 } 1845 1846 item.setChecked(!item.isChecked()); 1847 1848 // Some configuration option may be changed, rebind UseCases 1849 tryBindUseCases(); 1850 1851 return super.onOptionsItemSelected(item); 1852 } 1853 1854 /** 1855 * Writes text data to a file in public external directory for reading during tests. 1856 */ writeTextToExternalStorage(@onNull String text, @NonNull String filename, @NonNull String extension)1857 private void writeTextToExternalStorage(@NonNull String text, @NonNull String filename, 1858 @NonNull String extension) { 1859 mFileWriterExecutorService.execute(() -> { 1860 writeTextToExternalFile(text, filename, extension); 1861 }); 1862 } 1863 1864 /** 1865 * Close current app if CameraProvider from intent of current activity doesn't match with 1866 * CameraProvider stored in the CameraXViewModel, because CameraProvider can't be changed 1867 * between Camera2 and Camera Pipe while app is running. 1868 */ closeAppIfCameraProviderMismatch(Intent mIntent)1869 private void closeAppIfCameraProviderMismatch(Intent mIntent) { 1870 String cameraImplementation = null; 1871 boolean cameraImplementationNoHistory = false; 1872 Bundle bundle = mIntent.getExtras(); 1873 if (bundle != null) { 1874 cameraImplementation = bundle.getString(INTENT_EXTRA_CAMERA_IMPLEMENTATION); 1875 cameraImplementationNoHistory = 1876 bundle.getBoolean(INTENT_EXTRA_CAMERA_IMPLEMENTATION_NO_HISTORY, false); 1877 } 1878 1879 if (!cameraImplementationNoHistory) { 1880 if (!CameraXViewModel.isCameraProviderUnInitializedOrSameAsParameter( 1881 cameraImplementation)) { 1882 Toast.makeText(CameraXActivity.this, "Please relaunch " 1883 + "the app to apply new CameraX configuration.", 1884 Toast.LENGTH_LONG).show(); 1885 finish(); 1886 System.exit(0); 1887 } 1888 } 1889 } 1890 1891 1892 @Override onDestroy()1893 public void onDestroy() { 1894 super.onDestroy(); 1895 DisplayManager dpyMgr = 1896 requireNonNull(ContextCompat.getSystemService(this, DisplayManager.class)); 1897 dpyMgr.unregisterDisplayListener(mDisplayListener); 1898 mPreviewRenderer.shutdown(); 1899 mFileWriterExecutorService.shutdown(); 1900 mImageCaptureExecutorService.shutdown(); 1901 } 1902 tryBindUseCases()1903 void tryBindUseCases() { 1904 tryBindUseCases(false); 1905 } 1906 1907 /** 1908 * Try building and binding current use cases. 1909 * 1910 * @param calledBySelf flag indicates if this is a recursive call. 1911 */ tryBindUseCases(boolean calledBySelf)1912 void tryBindUseCases(boolean calledBySelf) { 1913 boolean isViewFinderReady = mViewFinder.getWidth() != 0 && mViewFinder.getHeight() != 0; 1914 boolean isCameraReady = mCameraProvider != null; 1915 if (isPermissionMissing() || !isCameraReady || !isViewFinderReady) { 1916 // No-op if permission if something is not ready. It will try again upon the 1917 // next thing being ready. 1918 return; 1919 } 1920 // Clear listening frame update before unbind all. 1921 mPreviewRenderer.clearFrameUpdateListener(); 1922 1923 // Remove ZoomState observer from old CameraInfo to prevent from receiving event from old 1924 // CameraInfo 1925 if (mCamera != null) { 1926 mCamera.getCameraInfo().getZoomState().removeObservers(this); 1927 } 1928 1929 // Stop in-progress video recording if it's not a persistent recording. 1930 if (hasRunningRecording() && !isPersistentRecordingEnabled()) { 1931 mActiveRecording.stop(); 1932 mActiveRecording = null; 1933 mRecordUi.setState(RecordUi.State.STOPPING); 1934 } 1935 1936 mCameraProvider.unbindAll(); 1937 try { 1938 // Binds to lifecycle without use cases to make sure mCamera can be retrieved for 1939 // tests to do necessary checks. 1940 mCamera = mCameraProvider.bindToLifecycle(this, mCurrentCameraSelector); 1941 1942 // Retrieves the lens facing info when the activity is launched with a specified 1943 // camera id. 1944 if (mCurrentCameraSelector == mLaunchingCameraIdSelector 1945 && mLaunchingCameraLensFacing == CameraSelector.LENS_FACING_UNKNOWN) { 1946 mLaunchingCameraLensFacing = getLensFacing(mCamera.getCameraInfo()); 1947 } 1948 1949 List<UseCase> useCases = buildUseCases(); 1950 mCamera = bindToLifecycleSafely(useCases); 1951 1952 // Set the use cases after a successful binding. 1953 mUseCases = useCases; 1954 } catch (IllegalArgumentException ex) { 1955 String msg = getBindFailedErrorMessage(); 1956 Log.e(TAG, "bindToLifecycle() failed. " + msg, ex); 1957 Toast.makeText(this, msg, Toast.LENGTH_LONG).show(); 1958 1959 // Restore toggle buttons to the previous state if the bind failed. 1960 if (mUseCases != null) { 1961 mPreviewToggle.setChecked(getPreview() != null); 1962 mPhotoToggle.setChecked(getImageCapture() != null); 1963 mAnalysisToggle.setChecked(getImageAnalysis() != null); 1964 mVideoToggle.setChecked(getVideoCapture() != null); 1965 } 1966 // Reset video quality to avoid always fail by quality too large. 1967 mRecordUi.getButtonQuality().setText(getQualityIconName(mVideoQuality = QUALITY_AUTO)); 1968 // Reset video dynamic range to avoid failure 1969 setSelectedDynamicRange(DynamicRange.SDR); 1970 1971 reduceUseCaseToFindSupportedCombination(); 1972 1973 if (!calledBySelf) { 1974 // Only call self if not already calling self to avoid an infinite loop. 1975 tryBindUseCases(true); 1976 } 1977 } 1978 updateButtonsUi(); 1979 } 1980 getBindFailedErrorMessage()1981 private @NonNull String getBindFailedErrorMessage() { 1982 if (mVideoQuality != QUALITY_AUTO) { 1983 return "Bind too many use cases or video quality is too large."; 1984 } else if (mImageOutputFormat == OUTPUT_FORMAT_JPEG_ULTRA_HDR 1985 && Objects.equals(mDynamicRange, DynamicRange.SDR)) { 1986 return "Bind too many use cases or device does not support concurrent SDR and HDR."; 1987 } else if (!Objects.equals(mDynamicRange, DynamicRange.SDR)) { 1988 return "Bind too many use cases or unsupported dynamic range combination."; 1989 } 1990 return "Bind too many use cases."; 1991 } 1992 hasRunningRecording()1993 private boolean hasRunningRecording() { 1994 RecordUi.State recordState = mRecordUi.getState(); 1995 return recordState == RecordUi.State.RECORDING || recordState == RecordUi.State.PAUSED; 1996 } 1997 isPersistentRecordingEnabled()1998 private boolean isPersistentRecordingEnabled() { 1999 return mRecordUi.getButtonPersistent().isChecked(); 2000 } 2001 2002 /** 2003 * Checks whether currently checked use cases combination can be supported or not. 2004 */ isCheckedUseCasesCombinationSupported()2005 private boolean isCheckedUseCasesCombinationSupported() { 2006 return mCamera.isUseCasesCombinationSupported(buildUseCases().toArray(new UseCase[0])); 2007 } 2008 2009 /** 2010 * Unchecks use case to find a supported use cases combination. 2011 * 2012 * <p>Only VideoCapture or ImageAnalysis will be tried to uncheck. If only Preview and 2013 * ImageCapture are remained, the combination should always be supported. 2014 */ reduceUseCaseToFindSupportedCombination()2015 private void reduceUseCaseToFindSupportedCombination() { 2016 // Checks whether current combination can be supported 2017 if (isCheckedUseCasesCombinationSupported()) { 2018 return; 2019 } 2020 2021 // Remove VideoCapture to check whether the new use cases combination can be supported. 2022 if (mVideoToggle.isChecked()) { 2023 mVideoToggle.setChecked(false); 2024 if (isCheckedUseCasesCombinationSupported()) { 2025 return; 2026 } 2027 } 2028 2029 // Remove ImageAnalysis to check whether the new use cases combination can be supported. 2030 if (mAnalysisToggle.isChecked()) { 2031 mAnalysisToggle.setChecked(false); 2032 // No need to do further use case combination check since Preview + ImageCapture 2033 // should be always supported. 2034 } 2035 } 2036 2037 /** 2038 * Builds all use cases based on current settings and return as an array. 2039 */ 2040 @SuppressLint("RestrictedApiAndroidX") buildUseCases()2041 private List<UseCase> buildUseCases() { 2042 List<UseCase> useCases = new ArrayList<>(); 2043 if (mVideoToggle.isChecked() || mPreviewToggle.isChecked()) { 2044 // Update possible dynamic ranges for current camera 2045 updateDynamicRangeConfiguration(); 2046 } 2047 2048 if (mPreviewToggle.isChecked()) { 2049 Preview preview = new Preview.Builder() 2050 .setTargetName("Preview") 2051 .setTargetAspectRatio(mTargetAspectRatio) 2052 .setPreviewStabilizationEnabled(mIsPreviewStabilizationOn) 2053 .setDynamicRange( 2054 mVideoToggle.isChecked() ? DynamicRange.UNSPECIFIED : mDynamicRange) 2055 .setTargetFrameRate(mFpsRange) 2056 .build(); 2057 resetViewIdlingResource(); 2058 // Use the listener of the future to make sure the Preview setup the new surface. 2059 mPreviewRenderer.attachInputPreview(preview).addListener(() -> { 2060 Log.d(TAG, "OpenGLRenderer get the new surface for the Preview"); 2061 mPreviewRenderer.setFrameUpdateListener( 2062 ContextCompat.getMainExecutor(this), mFrameUpdateListener 2063 ); 2064 }, ContextCompat.getMainExecutor(this)); 2065 2066 useCases.add(preview); 2067 } 2068 2069 if (mPhotoToggle.isChecked()) { 2070 int flashType = FLASH_TYPE_ONE_SHOT_FLASH; 2071 if (mEnableTorchAsFlash) { 2072 flashType = FLASH_TYPE_USE_TORCH_AS_FLASH; 2073 } 2074 2075 ImageCapture imageCapture = new ImageCapture.Builder() 2076 .setFlashType(flashType) 2077 .setCaptureMode(getCaptureMode()) 2078 .setTargetAspectRatio(mTargetAspectRatio) 2079 .setOutputFormat(mImageOutputFormat) 2080 .setTargetName("ImageCapture") 2081 .build(); 2082 useCases.add(imageCapture); 2083 } 2084 2085 if (mAnalysisToggle.isChecked()) { 2086 ImageAnalysis imageAnalysis = new ImageAnalysis.Builder() 2087 .setTargetName("ImageAnalysis") 2088 .setTargetAspectRatio(mTargetAspectRatio) 2089 .build(); 2090 useCases.add(imageAnalysis); 2091 // Make the analysis idling resource non-idle, until the required frames received. 2092 resetAnalysisIdlingResource(); 2093 imageAnalysis.setAnalyzer(ContextCompat.getMainExecutor(this), mAnalyzer); 2094 } 2095 2096 if (mVideoToggle.isChecked()) { 2097 // Recreate the Recorder except there's a running persistent recording, existing 2098 // Recorder. We may later consider reuse the Recorder everytime if the quality didn't 2099 // change. 2100 if (mVideoCapture == null 2101 || mRecorder == null 2102 || !(hasRunningRecording() && isPersistentRecordingEnabled())) { 2103 Recorder.Builder builder = new Recorder.Builder(); 2104 if (mVideoQuality != QUALITY_AUTO) { 2105 builder.setQualitySelector(QualitySelector.from(mVideoQuality)); 2106 } 2107 mRecorder = builder.setAspectRatio(mTargetAspectRatio).build(); 2108 mVideoCapture = new VideoCapture.Builder<>(mRecorder) 2109 .setMirrorMode(mVideoMirrorMode) 2110 .setDynamicRange(mDynamicRange) 2111 .setTargetFrameRate(mFpsRange) 2112 .build(); 2113 } 2114 useCases.add(mVideoCapture); 2115 } 2116 return useCases; 2117 } 2118 updateDynamicRangeConfiguration()2119 private void updateDynamicRangeConfiguration() { 2120 mSelectableDynamicRanges.clear(); 2121 2122 Set<DynamicRange> supportedDynamicRanges = Collections.singleton(DynamicRange.SDR); 2123 // The dynamic range here (mDynamicRange) is considered the dynamic range for 2124 // Preview/VideoCapture for following reasons: 2125 // 1. ImageAnalysis currently only support SDR, so only update supported ranges if 2126 // ImageAnalysis is not enabled. 2127 // 2. ImageCapture's dynamic range is determined by its output format (JPEG -> SDR, 2128 // Ultra HDR -> HDR unspecified), so mDynamicRange can be updated but does not affect 2129 // ImageCapture's configuration. 2130 if (!mAnalysisToggle.isChecked()) { 2131 if (mVideoToggle.isChecked()) { 2132 // Get the list of available dynamic ranges for the current quality 2133 VideoCapabilities videoCapabilities = Recorder.getVideoCapabilities( 2134 mCamera.getCameraInfo()); 2135 supportedDynamicRanges = videoCapabilities.getSupportedDynamicRanges(); 2136 } else if (mPreviewToggle.isChecked()) { 2137 supportedDynamicRanges = new HashSet<>(); 2138 // Add SDR as its always available 2139 supportedDynamicRanges.add(DynamicRange.SDR); 2140 2141 // Add all HDR dynamic ranges supported by the display 2142 Set<DynamicRange> queryResult = mCamera.getCameraInfo() 2143 .querySupportedDynamicRanges( 2144 Collections.singleton(DynamicRange.UNSPECIFIED)); 2145 supportedDynamicRanges.addAll(queryResult); 2146 } 2147 } 2148 2149 if (supportedDynamicRanges.size() > 1) { 2150 if (hasTenBitDynamicRange(supportedDynamicRanges)) { 2151 mSelectableDynamicRanges.add(DynamicRange.HDR_UNSPECIFIED_10_BIT); 2152 } 2153 } 2154 mSelectableDynamicRanges.addAll(supportedDynamicRanges); 2155 2156 // In case the previous dynamic range held in mDynamicRange isn't supported, reset 2157 // to SDR. 2158 if (!mSelectableDynamicRanges.contains(mDynamicRange)) { 2159 setSelectedDynamicRange(DynamicRange.SDR); 2160 } 2161 } 2162 2163 /** 2164 * Request permission if missing. 2165 */ setupPermissions()2166 private void setupPermissions() { 2167 if (isPermissionMissing()) { 2168 ActivityResultLauncher<String[]> permissionLauncher = 2169 registerForActivityResult( 2170 new ActivityResultContracts.RequestMultiplePermissions(), 2171 result -> { 2172 for (String permission : REQUIRED_PERMISSIONS) { 2173 if (!requireNonNull(result.get(permission))) { 2174 Toast.makeText(getApplicationContext(), 2175 "Camera permission denied.", 2176 Toast.LENGTH_SHORT) 2177 .show(); 2178 finish(); 2179 return; 2180 } 2181 } 2182 tryBindUseCases(); 2183 }); 2184 2185 permissionLauncher.launch(REQUIRED_PERMISSIONS); 2186 } else { 2187 // Permissions already granted. Start camera. 2188 tryBindUseCases(); 2189 } 2190 } 2191 2192 /** Returns true if any of the required permissions is missing. */ isPermissionMissing()2193 private boolean isPermissionMissing() { 2194 for (String permission : REQUIRED_PERMISSIONS) { 2195 if (ContextCompat.checkSelfPermission(this, permission) 2196 != PackageManager.PERMISSION_GRANTED) { 2197 return true; 2198 } 2199 } 2200 return false; 2201 } 2202 createDefaultPictureFolderIfNotExist()2203 void createDefaultPictureFolderIfNotExist() { 2204 File pictureFolder = getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES); 2205 if (createFolder(pictureFolder)) { 2206 Log.e(TAG, "Failed to create directory: " + pictureFolder); 2207 } 2208 } 2209 2210 /** Checks the folder existence by how the video file be created. */ createDefaultVideoFolderIfNotExist()2211 private void createDefaultVideoFolderIfNotExist() { 2212 String videoFilePath = 2213 getAbsolutePathFromUri(getApplicationContext().getContentResolver(), 2214 MediaStore.Video.Media.EXTERNAL_CONTENT_URI); 2215 if (videoFilePath == null || !createParentFolder(videoFilePath)) { 2216 Log.e(TAG, "Failed to create parent directory for: " + videoFilePath); 2217 } 2218 } 2219 2220 /** 2221 * Binds use cases to the current lifecycle. 2222 */ bindToLifecycleSafely(List<UseCase> useCases)2223 private Camera bindToLifecycleSafely(List<UseCase> useCases) { 2224 Log.d(TAG, "bindToLifecycleSafely: mDisableViewPort = " + mDisableViewPort 2225 + ", mForceEnableStreamSharing = " + mForceEnableStreamSharing); 2226 2227 UseCaseGroup.Builder useCaseGroupBuilder = new UseCaseGroup.Builder(); 2228 for (UseCase useCase : useCases) { 2229 useCaseGroupBuilder.addUseCase(useCase); 2230 } 2231 2232 if (!mDisableViewPort) { 2233 ViewPort viewPort = new ViewPort.Builder(new Rational(mViewFinder.getWidth(), 2234 mViewFinder.getHeight()), 2235 mViewFinder.getDisplay().getRotation()) 2236 .setScaleType(ViewPort.FILL_CENTER).build(); 2237 useCaseGroupBuilder.setViewPort(viewPort); 2238 } 2239 2240 // Force-enable stream sharing 2241 if (mForceEnableStreamSharing) { 2242 @SuppressLint("RestrictedApiAndroidX") 2243 StreamSharingForceEnabledEffect effect = new StreamSharingForceEnabledEffect(); 2244 useCaseGroupBuilder.addEffect(effect); 2245 } 2246 2247 mCamera = mCameraProvider.bindToLifecycle(this, mCurrentCameraSelector, 2248 useCaseGroupBuilder.build()); 2249 setupZoomSeeker(); 2250 setupTorchStrengthSeeker(); 2251 setUpLowLightBoostButton(); 2252 return mCamera; 2253 } 2254 2255 private static final int MAX_SEEKBAR_VALUE = 100000; 2256 showZoomRatioIsAlive()2257 void showZoomRatioIsAlive() { 2258 mZoomRatioLabel.setTextColor(getResources().getColor(R.color.zoom_ratio_activated)); 2259 } 2260 showNormalZoomRatio()2261 void showNormalZoomRatio() { 2262 mZoomRatioLabel.setTextColor(getResources().getColor(R.color.zoom_ratio_set)); 2263 } 2264 2265 @SuppressLint("RestrictedApiAndroidX") 2266 ZoomGestureDetector.OnZoomGestureListener mZoomGestureListener = zoomEvent -> { 2267 if (mCamera != null && zoomEvent instanceof ZoomGestureDetector.ZoomEvent.Move) { 2268 CameraInfo cameraInfo = mCamera.getCameraInfo(); 2269 float newZoom = 2270 requireNonNull(cameraInfo.getZoomState().getValue()).getZoomRatio() 2271 * ((ZoomGestureDetector.ZoomEvent.Move) zoomEvent) 2272 .getIncrementalScaleFactor(); 2273 setZoomRatio(newZoom); 2274 } 2275 return true; 2276 }; 2277 2278 GestureDetector.OnGestureListener onTapGestureListener = 2279 new GestureDetector.SimpleOnGestureListener() { 2280 @Override 2281 public boolean onSingleTapUp(@NonNull MotionEvent e) { 2282 if (mCamera == null) { 2283 return false; 2284 } 2285 // Since we are showing full camera preview we will be using 2286 // DisplayOrientedMeteringPointFactory to map the view's (x, y) to a 2287 // metering point. 2288 MeteringPointFactory factory = 2289 new DisplayOrientedMeteringPointFactory( 2290 mViewFinder.getDisplay(), 2291 mCamera.getCameraInfo(), 2292 mViewFinder.getWidth(), 2293 mViewFinder.getHeight()); 2294 FocusMeteringAction action = new FocusMeteringAction.Builder( 2295 factory.createPoint(e.getX(), e.getY()) 2296 ).build(); 2297 Futures.addCallback( 2298 mCamera.getCameraControl().startFocusAndMetering(action), 2299 new FutureCallback<FocusMeteringResult>() { 2300 @Override 2301 public void onSuccess(FocusMeteringResult result) { 2302 Log.d(TAG, "Focus and metering succeeded."); 2303 } 2304 2305 @Override 2306 public void onFailure(@NonNull Throwable t) { 2307 Log.e(TAG, "Focus and metering failed.", t); 2308 } 2309 }, 2310 CameraXExecutors.mainThreadExecutor()); 2311 return true; 2312 } 2313 }; 2314 2315 @SuppressLint("NewApi") setupTorchStrengthSeeker()2316 private void setupTorchStrengthSeeker() { 2317 if (mCamera.getCameraInfo().isTorchStrengthSupported()) { 2318 mTorchStrengthText.setVisibility(View.VISIBLE); 2319 mTorchStrengthText.setText( 2320 "L" + (mCamera.getCameraInfo().getTorchStrengthLevel().getValue())); 2321 2322 mTorchStrengthSeekBar.setVisibility(View.VISIBLE); 2323 mTorchStrengthSeekBar.setMin(1); 2324 mTorchStrengthSeekBar.setMax(mCamera.getCameraInfo().getMaxTorchStrengthLevel()); 2325 mTorchStrengthSeekBar.setProgress( 2326 mCamera.getCameraInfo().getTorchStrengthLevel().getValue()); 2327 mTorchStrengthSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { 2328 @Override 2329 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 2330 if (!fromUser) { 2331 return; 2332 } 2333 mCamera.getCameraControl().setTorchStrengthLevel(progress); 2334 mTorchStrengthText.setText("L" + progress); 2335 } 2336 2337 @Override 2338 public void onStartTrackingTouch(SeekBar seekBar) { 2339 // No-op 2340 } 2341 2342 @Override 2343 public void onStopTrackingTouch(SeekBar seekBar) { 2344 // No-op 2345 } 2346 }); 2347 } else { 2348 mTorchStrengthText.setVisibility(View.GONE); 2349 mTorchStrengthSeekBar.setVisibility(View.GONE); 2350 } 2351 } 2352 setupZoomSeeker()2353 private void setupZoomSeeker() { 2354 CameraControl cameraControl = mCamera.getCameraControl(); 2355 CameraInfo cameraInfo = mCamera.getCameraInfo(); 2356 2357 mZoomSeekBar.setMax(MAX_SEEKBAR_VALUE); 2358 mZoomSeekBar.setProgress( 2359 (int) (requireNonNull(cameraInfo.getZoomState().getValue()).getLinearZoom() 2360 * MAX_SEEKBAR_VALUE)); 2361 mZoomSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { 2362 @Override 2363 public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 2364 if (!fromUser) { 2365 return; 2366 } 2367 2368 float percentage = (float) progress / MAX_SEEKBAR_VALUE; 2369 showNormalZoomRatio(); 2370 ListenableFuture<Void> listenableFuture = 2371 cameraControl.setLinearZoom(percentage); 2372 2373 Futures.addCallback(listenableFuture, new FutureCallback<Void>() { 2374 @Override 2375 public void onSuccess(@Nullable Void result) { 2376 Log.d(TAG, "setZoomPercentage " + percentage + " onSuccess"); 2377 showZoomRatioIsAlive(); 2378 } 2379 2380 @Override 2381 public void onFailure(@NonNull Throwable t) { 2382 Log.d(TAG, "setZoomPercentage " + percentage + " failed, " + t); 2383 } 2384 }, ContextCompat.getMainExecutor(CameraXActivity.this)); 2385 } 2386 2387 @Override 2388 public void onStartTrackingTouch(SeekBar seekBar) { 2389 2390 } 2391 2392 @Override 2393 public void onStopTrackingTouch(SeekBar seekBar) { 2394 2395 } 2396 }); 2397 2398 cameraInfo.getZoomState().removeObservers(this); 2399 cameraInfo.getZoomState().observe(this, 2400 state -> { 2401 String str = String.format("%.2fx", state.getZoomRatio()); 2402 mZoomRatioLabel.setText(str); 2403 mZoomSeekBar.setProgress((int) (MAX_SEEKBAR_VALUE * state.getLinearZoom())); 2404 }); 2405 } 2406 is2XZoomSupported()2407 private boolean is2XZoomSupported() { 2408 CameraInfo cameraInfo = getCameraInfo(); 2409 return cameraInfo != null 2410 && requireNonNull(cameraInfo.getZoomState().getValue()).getMaxZoomRatio() >= 2.0f; 2411 } 2412 setUpZoomButton()2413 private void setUpZoomButton() { 2414 mZoomIn2XToggle.setOnClickListener(v -> setZoomRatio(2.0f)); 2415 2416 mZoomResetToggle.setOnClickListener(v -> setZoomRatio(1.0f)); 2417 } 2418 setUpPreviewStabilizationButton()2419 private void setUpPreviewStabilizationButton() { 2420 mPreviewStabilizationToggle.setOnClickListener(v -> { 2421 mIsPreviewStabilizationOn = !mIsPreviewStabilizationOn; 2422 if (mIsPreviewStabilizationOn) { 2423 showPreviewStabilizationToast("Preview Stabilization On, FOV changes"); 2424 } 2425 tryBindUseCases(); 2426 }); 2427 } 2428 2429 @SuppressWarnings("FutureReturnValueIgnored") setUpLowLightBoostButton()2430 private void setUpLowLightBoostButton() { 2431 mIsLowLightBoostOn = false; 2432 mLowLightBoostToggle.setVisibility( 2433 mCamera == null || !mCamera.getCameraInfo().isLowLightBoostSupported() ? View.GONE 2434 : View.VISIBLE); 2435 if (mLowLightBoostToggle.hasOnClickListeners()) { 2436 return; 2437 } 2438 mLowLightBoostToggle.setOnClickListener(v -> { 2439 mIsLowLightBoostOn = !mIsLowLightBoostOn; 2440 if (mCamera == null) { 2441 return; 2442 } 2443 if (!mCamera.getCameraInfo().getLowLightBoostState().hasObservers()) { 2444 // Show the low-light boost state to the toggle button text for easy observation. 2445 mCamera.getCameraInfo().getLowLightBoostState().observe( 2446 this, 2447 state -> { 2448 int resId; 2449 switch (state) { 2450 case LowLightBoostState.INACTIVE: 2451 resId = R.string.toggle_low_light_boost_inactive; 2452 break; 2453 case LowLightBoostState.ACTIVE: 2454 resId = R.string.toggle_low_light_boost_active; 2455 break; 2456 default: 2457 resId = R.string.toggle_low_light_boost_off; 2458 } 2459 mLowLightBoostToggle.setText(resId); 2460 } 2461 ); 2462 } 2463 mCamera.getCameraControl().enableLowLightBoostAsync(mIsLowLightBoostOn); 2464 }); 2465 } 2466 setZoomRatio(float newZoom)2467 void setZoomRatio(float newZoom) { 2468 if (mCamera == null) { 2469 return; 2470 } 2471 2472 CameraInfo cameraInfo = mCamera.getCameraInfo(); 2473 CameraControl cameraControl = mCamera.getCameraControl(); 2474 float clampedNewZoom = MathUtils.clamp(newZoom, 2475 requireNonNull(cameraInfo.getZoomState().getValue()).getMinZoomRatio(), 2476 cameraInfo.getZoomState().getValue().getMaxZoomRatio()); 2477 2478 Log.d(TAG, "setZoomRatio ratio: " + clampedNewZoom); 2479 showNormalZoomRatio(); 2480 ListenableFuture<Void> listenableFuture = cameraControl.setZoomRatio( 2481 clampedNewZoom); 2482 Futures.addCallback(listenableFuture, new FutureCallback<Void>() { 2483 @Override 2484 public void onSuccess(@Nullable Void result) { 2485 Log.d(TAG, "setZoomRatio onSuccess: " + clampedNewZoom); 2486 showZoomRatioIsAlive(); 2487 } 2488 2489 @Override 2490 public void onFailure(@NonNull Throwable t) { 2491 Log.d(TAG, "setZoomRatio failed, " + t); 2492 } 2493 }, ContextCompat.getMainExecutor(CameraXActivity.this)); 2494 } 2495 2496 @SuppressLint("RestrictedApiAndroidX") setupViewFinderGestureControls()2497 private void setupViewFinderGestureControls() { 2498 GestureDetector tapGestureDetector = new GestureDetector(this, onTapGestureListener); 2499 ZoomGestureDetector scaleDetector = new ZoomGestureDetector(this, mZoomGestureListener); 2500 mViewFinder.setOnTouchListener((view, e) -> { 2501 boolean tapEventProcessed = tapGestureDetector.onTouchEvent(e); 2502 boolean scaleEventProcessed = scaleDetector.onTouchEvent(e); 2503 return tapEventProcessed || scaleEventProcessed; 2504 }); 2505 } 2506 2507 private class SessionMediaUriSet { 2508 private final Set<Uri> mSessionMediaUris; 2509 SessionMediaUriSet()2510 SessionMediaUriSet() { 2511 mSessionMediaUris = Collections.synchronizedSet(new HashSet<>()); 2512 } 2513 add(@onNull Uri uri)2514 public void add(@NonNull Uri uri) { 2515 mSessionMediaUris.add(uri); 2516 } 2517 deleteAllUris()2518 public void deleteAllUris() { 2519 synchronized (mSessionMediaUris) { 2520 Iterator<Uri> it = mSessionMediaUris.iterator(); 2521 while (it.hasNext()) { 2522 try { 2523 getContentResolver().delete(it.next(), null, null); 2524 } catch (SecurityException e) { 2525 Log.w(TAG, "Cannot delete the content.", e); 2526 } 2527 it.remove(); 2528 } 2529 } 2530 } 2531 } 2532 2533 private static class DynamicRangeUi { 2534 2535 enum State { 2536 // Button can be selected to choose dynamic range 2537 CONFIGURABLE, 2538 // Button is visible, but cannot be selected 2539 VISIBLE, 2540 // Button is not visible, cannot be selected 2541 HIDDEN 2542 } 2543 2544 private State mState = State.HIDDEN; 2545 2546 private final Button mButtonDynamicRange; 2547 DynamicRangeUi(@onNull Button buttonDynamicRange)2548 DynamicRangeUi(@NonNull Button buttonDynamicRange) { 2549 mButtonDynamicRange = buttonDynamicRange; 2550 } 2551 setState(@onNull State newState)2552 void setState(@NonNull State newState) { 2553 if (newState != mState) { 2554 mState = newState; 2555 switch (newState) { 2556 case HIDDEN: { 2557 mButtonDynamicRange.setEnabled(false); 2558 mButtonDynamicRange.setVisibility(View.INVISIBLE); 2559 break; 2560 } 2561 case VISIBLE: { 2562 mButtonDynamicRange.setEnabled(false); 2563 mButtonDynamicRange.setVisibility(View.VISIBLE); 2564 break; 2565 } 2566 case CONFIGURABLE: { 2567 mButtonDynamicRange.setEnabled(true); 2568 mButtonDynamicRange.setVisibility(View.VISIBLE); 2569 break; 2570 } 2571 } 2572 } 2573 } 2574 getButton()2575 @NonNull Button getButton() { 2576 return mButtonDynamicRange; 2577 } 2578 setDisplayedDynamicRange(@onNull DynamicRange dynamicRange)2579 void setDisplayedDynamicRange(@NonNull DynamicRange dynamicRange) { 2580 int resId = R.string.toggle_video_dyn_rng_unknown; 2581 for (DynamicRangeUiData uiData : DYNAMIC_RANGE_UI_DATA) { 2582 if (Objects.equals(dynamicRange, uiData.mDynamicRange)) { 2583 resId = uiData.mToggleLabelRes; 2584 break; 2585 } 2586 } 2587 mButtonDynamicRange.setText(resId); 2588 } 2589 } 2590 2591 @UiThread 2592 private static class RecordUi { 2593 2594 enum State { 2595 IDLE, RECORDING, PAUSED, STOPPING 2596 } 2597 2598 private final Button mButtonRecord; 2599 private final Button mButtonPause; 2600 private final TextView mTextStats; 2601 private final Button mButtonQuality; 2602 private final ToggleButton mButtonPersistent; 2603 private final ImageButton mButtonMute; 2604 private boolean mEnabled = false; 2605 private State mState = State.IDLE; 2606 private final Consumer<State> mNewStateConsumer; 2607 RecordUi(@onNull Button buttonRecord, @NonNull Button buttonPause, @NonNull TextView textStats, @NonNull Button buttonQuality, @NonNull ToggleButton buttonPersistent, @NonNull ImageButton buttonMute, @NonNull Consumer<State> onNewState)2608 RecordUi(@NonNull Button buttonRecord, @NonNull Button buttonPause, 2609 @NonNull TextView textStats, @NonNull Button buttonQuality, 2610 @NonNull ToggleButton buttonPersistent, @NonNull ImageButton buttonMute, 2611 @NonNull Consumer<State> onNewState) { 2612 mButtonRecord = buttonRecord; 2613 mButtonPause = buttonPause; 2614 mTextStats = textStats; 2615 mButtonQuality = buttonQuality; 2616 mButtonPersistent = buttonPersistent; 2617 mButtonMute = buttonMute; 2618 mNewStateConsumer = onNewState; 2619 } 2620 setEnabled(boolean enabled)2621 void setEnabled(boolean enabled) { 2622 mEnabled = enabled; 2623 if (enabled) { 2624 mTextStats.setText(""); 2625 mTextStats.setVisibility(View.VISIBLE); 2626 mButtonQuality.setVisibility(View.VISIBLE); 2627 mButtonPersistent.setVisibility(View.VISIBLE); 2628 mButtonMute.setVisibility(View.VISIBLE); 2629 updateUi(); 2630 } else { 2631 mButtonRecord.setText("Record"); 2632 mButtonRecord.setEnabled(false); 2633 mButtonPause.setVisibility(View.INVISIBLE); 2634 mButtonQuality.setVisibility(View.INVISIBLE); 2635 mTextStats.setVisibility(View.GONE); 2636 mButtonPersistent.setVisibility(View.INVISIBLE); 2637 mButtonMute.setVisibility(View.INVISIBLE); 2638 } 2639 } 2640 setState(@onNull State state)2641 void setState(@NonNull State state) { 2642 if (state != mState) { 2643 mState = state; 2644 updateUi(); 2645 mNewStateConsumer.accept(state); 2646 } 2647 } 2648 getState()2649 @NonNull State getState() { 2650 return mState; 2651 } 2652 hideUi()2653 void hideUi() { 2654 mButtonRecord.setVisibility(View.GONE); 2655 mButtonPause.setVisibility(View.GONE); 2656 mTextStats.setVisibility(View.GONE); 2657 mButtonPersistent.setVisibility(View.GONE); 2658 mButtonMute.setVisibility(View.GONE); 2659 } 2660 updateUi()2661 private void updateUi() { 2662 if (!mEnabled) { 2663 return; 2664 } 2665 switch (mState) { 2666 case IDLE: 2667 mButtonRecord.setText("Record"); 2668 mButtonRecord.setEnabled(true); 2669 mButtonPause.setText("Pause"); 2670 mButtonPause.setVisibility(View.INVISIBLE); 2671 mButtonPersistent.setEnabled(true); 2672 mButtonMute.setEnabled(true); 2673 mButtonQuality.setEnabled(true); 2674 break; 2675 case RECORDING: 2676 mButtonRecord.setText("Stop"); 2677 mButtonRecord.setEnabled(true); 2678 mButtonPause.setText("Pause"); 2679 mButtonPause.setVisibility(View.VISIBLE); 2680 mButtonPersistent.setEnabled(false); 2681 mButtonMute.setEnabled(true); 2682 mButtonQuality.setEnabled(false); 2683 break; 2684 case STOPPING: 2685 mButtonRecord.setText("Saving"); 2686 mButtonRecord.setEnabled(false); 2687 mButtonPause.setText("Pause"); 2688 mButtonPause.setVisibility(View.INVISIBLE); 2689 mButtonPersistent.setEnabled(false); 2690 mButtonMute.setEnabled(false); 2691 mButtonQuality.setEnabled(true); 2692 break; 2693 case PAUSED: 2694 mButtonRecord.setText("Stop"); 2695 mButtonRecord.setEnabled(true); 2696 mButtonPause.setText("Resume"); 2697 mButtonPause.setVisibility(View.VISIBLE); 2698 mButtonPersistent.setEnabled(false); 2699 mButtonMute.setEnabled(true); 2700 mButtonQuality.setEnabled(true); 2701 break; 2702 } 2703 } 2704 getButtonRecord()2705 Button getButtonRecord() { 2706 return mButtonRecord; 2707 } 2708 getButtonPause()2709 Button getButtonPause() { 2710 return mButtonPause; 2711 } 2712 getTextStats()2713 TextView getTextStats() { 2714 return mTextStats; 2715 } 2716 getButtonQuality()2717 @NonNull Button getButtonQuality() { 2718 return mButtonQuality; 2719 } 2720 getButtonPersistent()2721 ToggleButton getButtonPersistent() { 2722 return mButtonPersistent; 2723 } 2724 getButtonMute()2725 ImageButton getButtonMute() { 2726 return mButtonMute; 2727 } 2728 } 2729 getPreview()2730 Preview getPreview() { 2731 return findUseCase(Preview.class); 2732 } 2733 getImageAnalysis()2734 ImageAnalysis getImageAnalysis() { 2735 return findUseCase(ImageAnalysis.class); 2736 } 2737 getImageCapture()2738 ImageCapture getImageCapture() { 2739 return findUseCase(ImageCapture.class); 2740 } 2741 getViewFinder()2742 @Nullable View getViewFinder() { 2743 return mViewFinder; 2744 } 2745 2746 /** 2747 * Returns the error message of the last take picture action if any error occurs. Returns 2748 * null if no error occurs. 2749 */ 2750 @VisibleForTesting getLastTakePictureErrorMessage()2751 @Nullable String getLastTakePictureErrorMessage() { 2752 return mLastTakePictureErrorMessage; 2753 } 2754 2755 @VisibleForTesting cleanTakePictureErrorMessage()2756 void cleanTakePictureErrorMessage() { 2757 mLastTakePictureErrorMessage = null; 2758 } 2759 2760 @SuppressWarnings("unchecked") getVideoCapture()2761 VideoCapture<Recorder> getVideoCapture() { 2762 return findUseCase(VideoCapture.class); 2763 } 2764 2765 @VisibleForTesting setVideoCaptureAutoStopLength(long autoStopLengthInMs)2766 void setVideoCaptureAutoStopLength(long autoStopLengthInMs) { 2767 mVideoCaptureAutoStopLength = autoStopLengthInMs; 2768 } 2769 2770 /** 2771 * Finds the use case by the given class. 2772 */ findUseCase(Class<T> useCaseSubclass)2773 private <T extends UseCase> @Nullable T findUseCase(Class<T> useCaseSubclass) { 2774 if (mUseCases != null) { 2775 for (UseCase useCase : mUseCases) { 2776 if (useCaseSubclass.isInstance(useCase)) { 2777 return useCaseSubclass.cast(useCase); 2778 } 2779 } 2780 } 2781 return null; 2782 } 2783 2784 @VisibleForTesting getCamera()2785 public @Nullable Camera getCamera() { 2786 return mCamera; 2787 } 2788 2789 @VisibleForTesting getCameraInfo()2790 @Nullable CameraInfo getCameraInfo() { 2791 return mCamera != null ? mCamera.getCameraInfo() : null; 2792 } 2793 2794 @VisibleForTesting getCameraControl()2795 @Nullable CameraControl getCameraControl() { 2796 return mCamera != null ? mCamera.getCameraControl() : null; 2797 } 2798 getQualityIconName(@ullable Quality quality)2799 private static @NonNull String getQualityIconName(@Nullable Quality quality) { 2800 if (quality == QUALITY_AUTO) { 2801 return "Auto"; 2802 } else if (quality == Quality.UHD) { 2803 return "UHD"; 2804 } else if (quality == Quality.FHD) { 2805 return "FHD"; 2806 } else if (quality == Quality.HD) { 2807 return "HD"; 2808 } else if (quality == Quality.SD) { 2809 return "SD"; 2810 } 2811 return "?"; 2812 } 2813 getQualityMenuItemName(@ullable Quality quality)2814 private static @NonNull String getQualityMenuItemName(@Nullable Quality quality) { 2815 if (quality == QUALITY_AUTO) { 2816 return "Auto"; 2817 } else if (quality == Quality.UHD) { 2818 return "UHD (2160P)"; 2819 } else if (quality == Quality.FHD) { 2820 return "FHD (1080P)"; 2821 } else if (quality == Quality.HD) { 2822 return "HD (720P)"; 2823 } else if (quality == Quality.SD) { 2824 return "SD (480P)"; 2825 } 2826 return "Unknown quality"; 2827 } 2828 qualityToItemId(@ullable Quality quality)2829 private static int qualityToItemId(@Nullable Quality quality) { 2830 if (quality == QUALITY_AUTO) { 2831 return 0; 2832 } else if (quality == Quality.UHD) { 2833 return 1; 2834 } else if (quality == Quality.FHD) { 2835 return 2; 2836 } else if (quality == Quality.HD) { 2837 return 3; 2838 } else if (quality == Quality.SD) { 2839 return 4; 2840 } else { 2841 throw new IllegalArgumentException("Undefined quality: " + quality); 2842 } 2843 } 2844 itemIdToQuality(int itemId)2845 private static @Nullable Quality itemIdToQuality(int itemId) { 2846 switch (itemId) { 2847 case 0: 2848 return QUALITY_AUTO; 2849 case 1: 2850 return Quality.UHD; 2851 case 2: 2852 return Quality.FHD; 2853 case 3: 2854 return Quality.HD; 2855 case 4: 2856 return Quality.SD; 2857 default: 2858 throw new IllegalArgumentException("Undefined item id: " + itemId); 2859 } 2860 } 2861 getDynamicRangeMenuItemName(@onNull DynamicRange dynamicRange)2862 private static @NonNull String getDynamicRangeMenuItemName(@NonNull DynamicRange dynamicRange) { 2863 String menuItemName = dynamicRange.toString(); 2864 for (DynamicRangeUiData uiData : DYNAMIC_RANGE_UI_DATA) { 2865 if (Objects.equals(dynamicRange, uiData.mDynamicRange)) { 2866 menuItemName = uiData.mMenuItemName; 2867 break; 2868 } 2869 } 2870 return menuItemName; 2871 } 2872 dynamicRangeToItemId(@onNull DynamicRange dynamicRange)2873 private static int dynamicRangeToItemId(@NonNull DynamicRange dynamicRange) { 2874 int itemId = -1; 2875 for (int i = 0; i < DYNAMIC_RANGE_UI_DATA.size(); i++) { 2876 DynamicRangeUiData uiData = DYNAMIC_RANGE_UI_DATA.get(i); 2877 if (Objects.equals(dynamicRange, uiData.mDynamicRange)) { 2878 itemId = i; 2879 break; 2880 } 2881 } 2882 if (itemId == -1) { 2883 throw new IllegalArgumentException("Unsupported dynamic range: " + dynamicRange); 2884 } 2885 return itemId; 2886 } 2887 itemIdToDynamicRange(int itemId)2888 private static @NonNull DynamicRange itemIdToDynamicRange(int itemId) { 2889 if (itemId < 0 || itemId >= DYNAMIC_RANGE_UI_DATA.size()) { 2890 throw new IllegalArgumentException("Undefined item id: " + itemId); 2891 } 2892 return DYNAMIC_RANGE_UI_DATA.get(itemId).mDynamicRange; 2893 } 2894 2895 @SuppressLint("RestrictedApiAndroidX") getImageOutputFormatIconName( @mageCapture.OutputFormat int format)2896 private static @NonNull String getImageOutputFormatIconName( 2897 @ImageCapture.OutputFormat int format) { 2898 if (format == OUTPUT_FORMAT_JPEG) { 2899 return "Jpeg"; 2900 } else if (format == OUTPUT_FORMAT_JPEG_ULTRA_HDR) { 2901 return "Ultra HDR"; 2902 } else if (format == OUTPUT_FORMAT_RAW) { 2903 return "Raw"; 2904 } else if (format == OUTPUT_FORMAT_RAW_JPEG) { 2905 return "Raw + Jpeg"; 2906 } 2907 return "?"; 2908 } 2909 2910 @SuppressLint("RestrictedApiAndroidX") getImageOutputFormatMenuItemName( @mageCapture.OutputFormat int format)2911 private static @NonNull String getImageOutputFormatMenuItemName( 2912 @ImageCapture.OutputFormat int format) { 2913 if (format == OUTPUT_FORMAT_JPEG) { 2914 return "Jpeg"; 2915 } else if (format == OUTPUT_FORMAT_JPEG_ULTRA_HDR) { 2916 return "Ultra HDR"; 2917 } else if (format == OUTPUT_FORMAT_RAW) { 2918 return "Raw"; 2919 } else if (format == OUTPUT_FORMAT_RAW_JPEG) { 2920 return "Raw + Jpeg"; 2921 } 2922 return "Unknown format"; 2923 } 2924 2925 @SuppressLint("RestrictedApiAndroidX") imageOutputFormatToItemId(@mageCapture.OutputFormat int format)2926 private static int imageOutputFormatToItemId(@ImageCapture.OutputFormat int format) { 2927 if (format == OUTPUT_FORMAT_JPEG) { 2928 return 0; 2929 } else if (format == OUTPUT_FORMAT_JPEG_ULTRA_HDR) { 2930 return 1; 2931 } else if (format == OUTPUT_FORMAT_RAW) { 2932 return 2; 2933 } else if (format == OUTPUT_FORMAT_RAW_JPEG) { 2934 return 3; 2935 } else { 2936 throw new IllegalArgumentException("Undefined output format: " + format); 2937 } 2938 } 2939 2940 @SuppressLint("RestrictedApiAndroidX") 2941 @ImageCapture.OutputFormat itemIdToImageOutputFormat(int itemId)2942 private static int itemIdToImageOutputFormat(int itemId) { 2943 switch (itemId) { 2944 case 0: 2945 return OUTPUT_FORMAT_JPEG; 2946 case 1: 2947 return OUTPUT_FORMAT_JPEG_ULTRA_HDR; 2948 case 2: 2949 return OUTPUT_FORMAT_RAW; 2950 case 3: 2951 return OUTPUT_FORMAT_RAW_JPEG; 2952 default: 2953 throw new IllegalArgumentException("Undefined item id: " + itemId); 2954 } 2955 } 2956 createCameraSelectorById(@ullable String cameraId)2957 private static CameraSelector createCameraSelectorById(@Nullable String cameraId) { 2958 return new CameraSelector.Builder().addCameraFilter(cameraInfos -> { 2959 for (CameraInfo cameraInfo : cameraInfos) { 2960 if (Objects.equals(cameraId, getCameraId(cameraInfo))) { 2961 return Collections.singletonList(cameraInfo); 2962 } 2963 } 2964 2965 throw new IllegalArgumentException("No camera can be find for id: " + cameraId); 2966 }).build(); 2967 } 2968 2969 private static int getLensFacing(@NonNull CameraInfo cameraInfo) { 2970 try { 2971 return getCamera2LensFacing(cameraInfo); 2972 } catch (IllegalArgumentException e) { 2973 return getCamera2PipeLensFacing(cameraInfo); 2974 } 2975 } 2976 2977 private boolean isFrontCamera() { 2978 return getLensFacing(Objects.requireNonNull(getCameraInfo())) 2979 == CameraSelector.LENS_FACING_FRONT; 2980 } 2981 2982 @SuppressLint("NullAnnotationGroup") 2983 @OptIn(markerClass = ExperimentalCamera2Interop.class) 2984 private static int getCamera2LensFacing(@NonNull CameraInfo cameraInfo) { 2985 Integer lensFacing = Camera2CameraInfo.from(cameraInfo).getCameraCharacteristic( 2986 CameraCharacteristics.LENS_FACING); 2987 2988 return lensFacing == null ? CameraCharacteristics.LENS_FACING_BACK : lensFacing; 2989 } 2990 2991 @SuppressLint("NullAnnotationGroup") 2992 @OptIn(markerClass = 2993 androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop.class) 2994 private static int getCamera2PipeLensFacing(@NonNull CameraInfo cameraInfo) { 2995 Integer lensFacing = 2996 androidx.camera.camera2.pipe.integration.interop.Camera2CameraInfo.from( 2997 cameraInfo).getCameraCharacteristic(CameraCharacteristics.LENS_FACING); 2998 2999 return lensFacing == null ? CameraCharacteristics.LENS_FACING_BACK : lensFacing; 3000 } 3001 3002 private static boolean isLegacyDevice(@NonNull CameraInfo cameraInfo) { 3003 if (CameraXViewModel.CAMERA_PIPE_IMPLEMENTATION_OPTION.equals( 3004 getConfiguredCameraXCameraImplementation())) { 3005 return isCameraPipeLegacyDevice(cameraInfo); 3006 } 3007 return isCamera2LegacyDevice(cameraInfo); 3008 } 3009 3010 @SuppressLint("NullAnnotationGroup") 3011 @OptIn(markerClass = ExperimentalCamera2Interop.class) 3012 private static boolean isCamera2LegacyDevice(@NonNull CameraInfo cameraInfo) { 3013 return Camera2CameraInfo.from(cameraInfo).getCameraCharacteristic( 3014 CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL 3015 ) == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY; 3016 } 3017 3018 @SuppressLint("NullAnnotationGroup") 3019 @OptIn(markerClass = 3020 androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop.class) 3021 private static boolean isCameraPipeLegacyDevice(@NonNull CameraInfo cameraInfo) { 3022 return androidx.camera.camera2.pipe.integration.interop.Camera2CameraInfo.from(cameraInfo) 3023 .getCameraCharacteristic( 3024 CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL 3025 ) == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY; 3026 } 3027 3028 private static @NonNull String getCameraId(@NonNull CameraInfo cameraInfo) { 3029 try { 3030 return getCamera2CameraId(cameraInfo); 3031 } catch (IllegalArgumentException e) { 3032 return getCameraPipeCameraId(cameraInfo); 3033 } 3034 } 3035 3036 @SuppressLint("NullAnnotationGroup") 3037 @OptIn(markerClass = ExperimentalCamera2Interop.class) 3038 private static @NonNull String getCamera2CameraId(@NonNull CameraInfo cameraInfo) { 3039 return Camera2CameraInfo.from(cameraInfo).getCameraId(); 3040 } 3041 3042 @SuppressLint("NullAnnotationGroup") 3043 @OptIn(markerClass = 3044 androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop.class) 3045 private static @NonNull String getCameraPipeCameraId(@NonNull CameraInfo cameraInfo) { 3046 return androidx.camera.camera2.pipe.integration.interop.Camera2CameraInfo.from( 3047 cameraInfo).getCameraId(); 3048 } 3049 3050 private static final class DynamicRangeUiData { 3051 private DynamicRangeUiData( 3052 @NonNull DynamicRange dynamicRange, 3053 @NonNull String menuItemName, 3054 int toggleLabelRes) { 3055 mDynamicRange = dynamicRange; 3056 mMenuItemName = menuItemName; 3057 mToggleLabelRes = toggleLabelRes; 3058 } 3059 3060 DynamicRange mDynamicRange; 3061 String mMenuItemName; 3062 int mToggleLabelRes; 3063 } 3064 3065 @RequiresApi(26) 3066 static class Api26Impl { 3067 private Api26Impl() { 3068 // This class is not instantiable. 3069 } 3070 3071 static void setColorMode(@NonNull Window window, int colorMode) { 3072 window.setColorMode(colorMode); 3073 } 3074 3075 } 3076 } 3077