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