blob: 70c637a084448ec002c3217c050db14ceb3295c4 [file] [log] [blame]
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.camera.integration.core;
import static androidx.camera.core.ImageCapture.ERROR_CAMERA_CLOSED;
import static androidx.camera.core.ImageCapture.ERROR_CAPTURE_FAILED;
import static androidx.camera.core.ImageCapture.ERROR_FILE_IO;
import static androidx.camera.core.ImageCapture.ERROR_INVALID_CAMERA;
import static androidx.camera.core.ImageCapture.ERROR_UNKNOWN;
import static androidx.camera.core.ImageCapture.FLASH_MODE_AUTO;
import static androidx.camera.core.ImageCapture.FLASH_MODE_OFF;
import static androidx.camera.core.ImageCapture.FLASH_MODE_ON;
import static androidx.camera.core.MirrorMode.MIRROR_MODE_ON_FRONT_ONLY;
import static androidx.camera.testing.impl.FileUtil.canDeviceWriteToMediaStore;
import static androidx.camera.testing.impl.FileUtil.createFolder;
import static androidx.camera.testing.impl.FileUtil.createParentFolder;
import static androidx.camera.testing.impl.FileUtil.generateVideoFileOutputOptions;
import static androidx.camera.testing.impl.FileUtil.generateVideoMediaStoreOptions;
import static androidx.camera.testing.impl.FileUtil.getAbsolutePathFromUri;
import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_DURATION_LIMIT_REACHED;
import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_FILE_SIZE_LIMIT_REACHED;
import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_INSUFFICIENT_STORAGE;
import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_NONE;
import static androidx.camera.video.VideoRecordEvent.Finalize.ERROR_SOURCE_INACTIVE;
import static java.util.Objects.requireNonNull;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.ContentValues;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.display.DisplayManager;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.os.StrictMode;
import android.os.SystemClock;
import android.provider.MediaStore;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.Range;
import android.util.Rational;
import android.view.Display;
import android.view.GestureDetector;
import android.view.Menu;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.view.Window;
import android.widget.Button;
import android.widget.CompoundButton;
import android.widget.ImageButton;
import android.widget.PopupMenu;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ToggleButton;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.DoNotInline;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
import androidx.annotation.UiThread;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.app.AppCompatActivity;
import androidx.camera.camera2.internal.compat.quirk.CrashWhenTakingPhotoWithAutoFlashAEModeQuirk;
import androidx.camera.camera2.internal.compat.quirk.ImageCaptureFailWithAutoFlashQuirk;
import androidx.camera.camera2.internal.compat.quirk.ImageCaptureFlashNotFireQuirk;
import androidx.camera.camera2.interop.Camera2CameraInfo;
import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
import androidx.camera.core.AspectRatio;
import androidx.camera.core.Camera;
import androidx.camera.core.CameraControl;
import androidx.camera.core.CameraInfo;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.DisplayOrientedMeteringPointFactory;
import androidx.camera.core.DynamicRange;
import androidx.camera.core.ExperimentalLensFacing;
import androidx.camera.core.ExposureState;
import androidx.camera.core.FocusMeteringAction;
import androidx.camera.core.FocusMeteringResult;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.ImageProxy;
import androidx.camera.core.MeteringPointFactory;
import androidx.camera.core.Preview;
import androidx.camera.core.TorchState;
import androidx.camera.core.UseCase;
import androidx.camera.core.UseCaseGroup;
import androidx.camera.core.ViewPort;
import androidx.camera.core.impl.CameraInfoInternal;
import androidx.camera.core.impl.Quirks;
import androidx.camera.core.impl.utils.AspectRatioUtil;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.camera.video.ExperimentalPersistentRecording;
import androidx.camera.video.FileOutputOptions;
import androidx.camera.video.MediaStoreOutputOptions;
import androidx.camera.video.OutputOptions;
import androidx.camera.video.PendingRecording;
import androidx.camera.video.Quality;
import androidx.camera.video.QualitySelector;
import androidx.camera.video.Recorder;
import androidx.camera.video.Recording;
import androidx.camera.video.RecordingStats;
import androidx.camera.video.VideoCapabilities;
import androidx.camera.video.VideoCapture;
import androidx.camera.video.VideoRecordEvent;
import androidx.core.content.ContextCompat;
import androidx.core.math.MathUtils;
import androidx.core.util.Consumer;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModelProvider;
import androidx.test.espresso.IdlingResource;
import androidx.test.espresso.idling.CountingIdlingResource;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import java.io.File;
import java.text.Format;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
/**
* An activity with four use cases: (1) view finder, (2) image capture, (3) image analysis, (4)
* video capture.
*
* <p>All four use cases are created with CameraX and tied to the activity's lifecycle. CameraX
* automatically connects and disconnects the use cases from the camera in response to changes in
* the activity's lifecycle. Therefore, the use cases function properly when the app is paused and
* resumed and when the device is rotated. The complex interactions between the camera and these
* lifecycle events are handled internally by CameraX.
*/
public class CameraXActivity extends AppCompatActivity {
private static final String TAG = "CameraXActivity";
private static final String[] REQUIRED_PERMISSIONS;
private static final List<DynamicRangeUiData> DYNAMIC_RANGE_UI_DATA = new ArrayList<>();
static {
// From Android T, skips the permission check of WRITE_EXTERNAL_STORAGE since it won't be
// granted any more.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
REQUIRED_PERMISSIONS = new String[]{
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO
};
} else {
REQUIRED_PERMISSIONS = new String[]{
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.WRITE_EXTERNAL_STORAGE
};
}
DYNAMIC_RANGE_UI_DATA.add(new DynamicRangeUiData(
DynamicRange.SDR,
"SDR",
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));
DYNAMIC_RANGE_UI_DATA.add(new DynamicRangeUiData(
DynamicRange.HLG_10_BIT,
"HDR (HLG, 10-bit)",
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));
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));
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));
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));
}
//Use this activity title when Camera Pipe configuration is used by core test app
private static final String APP_TITLE_FOR_CAMERA_PIPE = "CameraPipe Core Test App";
// Possible values for this intent key: "backward" or "forward".
private static final String INTENT_EXTRA_CAMERA_DIRECTION = "camera_direction";
// Possible values for this intent key: "switch_test_case", "preview_test_case" or
// "default_test_case".
private static final String INTENT_EXTRA_E2E_TEST_CASE = "e2e_test_case";
// Launch the activity with the specified video quality.
private static final String INTENT_EXTRA_VIDEO_QUALITY = "video_quality";
// Launch the activity with the specified video mirror mode.
private static final String INTENT_EXTRA_VIDEO_MIRROR_MODE = "video_mirror_mode";
public static final String INTENT_EXTRA_CAMERA_IMPLEMENTATION = "camera_implementation";
public static final String INTENT_EXTRA_CAMERA_IMPLEMENTATION_NO_HISTORY =
"camera_implementation_no_history";
// Launch the activity with the specified target aspect ratio.
public static final String INTENT_EXTRA_TARGET_ASPECT_RATIO = "target_aspect_ratio";
// Launch the activity with the specified scale type. The default value is FILL_CENTER.
public static final String INTENT_EXTRA_SCALE_TYPE = "scale_type";
public static final int INTENT_EXTRA_FILL_CENTER = 1;
public static final int INTENT_EXTRA_FIT_CENTER = 4;
// Launch the activity with the specified camera id.
@VisibleForTesting
public static final String INTENT_EXTRA_CAMERA_ID = "camera_id";
// Launch the activity with the specified use case combination.
@VisibleForTesting
public static final String INTENT_EXTRA_USE_CASE_COMBINATION = "use_case_combination";
@VisibleForTesting
// Sets this bit to bind Preview when using INTENT_EXTRA_USE_CASE_COMBINATION
public static final int BIND_PREVIEW = 0x1;
@VisibleForTesting
// Sets this bit to bind ImageCapture when using INTENT_EXTRA_USE_CASE_COMBINATION
public static final int BIND_IMAGE_CAPTURE = 0x2;
@VisibleForTesting
// Sets this bit to bind VideoCapture when using INTENT_EXTRA_USE_CASE_COMBINATION
public static final int BIND_VIDEO_CAPTURE = 0x4;
@VisibleForTesting
// Sets this bit to bind ImageAnalysis when using INTENT_EXTRA_USE_CASE_COMBINATION
public static final int BIND_IMAGE_ANALYSIS = 0x8;
static final CameraSelector BACK_SELECTOR =
new CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build();
static final CameraSelector FRONT_SELECTOR =
new CameraSelector.Builder().requireLensFacing(
CameraSelector.LENS_FACING_FRONT).build();
private CameraSelector mExternalCameraSelector = null;
private final AtomicLong mImageAnalysisFrameCount = new AtomicLong(0);
private final AtomicLong mPreviewFrameCount = new AtomicLong(0);
// Automatically stops the video recording when this length value is set to be non-zero and
// video length reaches the length in ms.
private long mVideoCaptureAutoStopLength = 0;
final MutableLiveData<String> mImageAnalysisResult = new MutableLiveData<>();
private static final String BACKWARD = "BACKWARD";
private static final String SWITCH_TEST_CASE = "switch_test_case";
private static final String PREVIEW_TEST_CASE = "preview_test_case";
private static final String DESCRIPTION_FLASH_MODE_NOT_SUPPORTED = "FLASH_MODE_NOT_SUPPORTED";
private static final Quality QUALITY_AUTO = null;
// The target aspect ratio of Preview and ImageCapture. It can be adjusted by setting
// INTENT_EXTRA_TARGET_ASPECT_RATIO for the e2e testing.
private int mTargetAspectRatio = AspectRatio.RATIO_DEFAULT;
private Recording mActiveRecording;
/** The camera to use */
CameraSelector mCurrentCameraSelector = BACK_SELECTOR;
ProcessCameraProvider mCameraProvider;
private CameraXViewModel.CameraProviderResult mCameraProviderResult;
// TODO: Move the analysis processing, capture processing to separate threads, so
// there is smaller impact on the preview.
View mViewFinder;
private List<UseCase> mUseCases;
ExecutorService mImageCaptureExecutorService;
private VideoCapture<Recorder> mVideoCapture;
private Recorder mRecorder;
Camera mCamera;
private CameraSelector mLaunchingCameraIdSelector = null;
private int mLaunchingCameraLensFacing = CameraSelector.LENS_FACING_UNKNOWN;
private ToggleButton mVideoToggle;
private ToggleButton mPhotoToggle;
private ToggleButton mAnalysisToggle;
private ToggleButton mPreviewToggle;
private Button mTakePicture;
private ImageButton mCameraDirectionButton;
private ImageButton mFlashButton;
private TextView mTextView;
private ImageButton mTorchButton;
private ToggleButton mCaptureQualityToggle;
private Button mPlusEV;
private Button mDecEV;
private ToggleButton mZslToggle;
private TextView mZoomRatioLabel;
private SeekBar mZoomSeekBar;
private Button mZoomIn2XToggle;
private Button mZoomResetToggle;
private Toast mEvToast = null;
private Toast mPSToast = null;
private ToggleButton mPreviewStabilizationToggle;
private OpenGLRenderer mPreviewRenderer;
private DisplayManager.DisplayListener mDisplayListener;
private RecordUi mRecordUi;
private Quality mVideoQuality;
private DynamicRange mDynamicRange = DynamicRange.SDR;
private final Set<DynamicRange> mSelectableDynamicRanges = new HashSet<>();
private int mVideoMirrorMode = MIRROR_MODE_ON_FRONT_ONLY;
private boolean mIsPreviewStabilizationOn = false;
SessionMediaUriSet mSessionImagesUriSet = new SessionMediaUriSet();
SessionMediaUriSet mSessionVideosUriSet = new SessionMediaUriSet();
// Analyzer to be used with ImageAnalysis.
private final ImageAnalysis.Analyzer mAnalyzer = new ImageAnalysis.Analyzer() {
@Override
public void analyze(@NonNull ImageProxy image) {
// Since we set the callback handler to a main thread handler, we can call
// setValue() here. If we weren't on the main thread, we would have to call
// postValue() instead.
mImageAnalysisResult.setValue(
Long.toString(image.getImageInfo().getTimestamp()));
try {
if (mImageAnalysisFrameCount.get() >= FRAMES_UNTIL_IMAGE_ANALYSIS_IS_READY
&& !mAnalysisIdlingResource.isIdleNow()) {
mAnalysisIdlingResource.decrement();
}
} catch (IllegalStateException e) {
Log.e(TAG, "Unexpected counter decrement");
}
image.close();
}
};
private final FutureCallback<Integer> mEVFutureCallback = new FutureCallback<Integer>() {
@Override
public void onSuccess(@Nullable Integer result) {
if (result == null) {
return;
}
CameraInfo cameraInfo = getCameraInfo();
if (cameraInfo != null) {
ExposureState exposureState = cameraInfo.getExposureState();
float ev = result * exposureState.getExposureCompensationStep().floatValue();
Log.d(TAG, "success new EV: " + ev);
showEVToast(String.format("EV: %.2f", ev));
}
}
@Override
public void onFailure(@NonNull Throwable t) {
Log.d(TAG, "failed " + t);
showEVToast("Fail to set EV");
}
};
// Listener that handles all ToggleButton events.
private final CompoundButton.OnCheckedChangeListener mOnCheckedChangeListener =
(compoundButton, isChecked) -> tryBindUseCases();
private final Consumer<Long> mFrameUpdateListener = timestamp -> {
if (mPreviewFrameCount.getAndIncrement() >= FRAMES_UNTIL_VIEW_IS_READY) {
try {
if (!this.mViewIdlingResource.isIdleNow()) {
Log.d(TAG, FRAMES_UNTIL_VIEW_IS_READY + " or more counted on preview."
+ " Make IdlingResource idle.");
this.mViewIdlingResource.decrement();
}
} catch (IllegalStateException e) {
Log.e(TAG, "Unexpected decrement. Continuing");
}
}
};
// Espresso testing variables
private static final int FRAMES_UNTIL_VIEW_IS_READY = 5;
// Espresso testing variables
private static final int FRAMES_UNTIL_IMAGE_ANALYSIS_IS_READY = 5;
private final CountingIdlingResource mViewIdlingResource = new CountingIdlingResource("view");
private final CountingIdlingResource mInitializationIdlingResource =
new CountingIdlingResource("initialization");
private final CountingIdlingResource mAnalysisIdlingResource =
new CountingIdlingResource("analysis");
private final CountingIdlingResource mImageSavedIdlingResource =
new CountingIdlingResource("imagesaved");
private final CountingIdlingResource mVideoSavedIdlingResource =
new CountingIdlingResource("videosaved");
/**
* Saves the error message of the last take picture action if any error occurs. This will be
* null which means no error occurs.
*/
@Nullable
private String mLastTakePictureErrorMessage = null;
/**
* Retrieve idling resource that waits for image received by analyzer).
*/
@VisibleForTesting
@NonNull
public IdlingResource getAnalysisIdlingResource() {
return mAnalysisIdlingResource;
}
/**
* Retrieve idling resource that waits view to get texture update.
*/
@VisibleForTesting
@NonNull
public IdlingResource getViewIdlingResource() {
return mViewIdlingResource;
}
/**
* Retrieve idling resource that waits for capture to complete (save or error).
*/
@VisibleForTesting
@NonNull
public IdlingResource getImageSavedIdlingResource() {
return mImageSavedIdlingResource;
}
/**
* Retrieve idling resource that waits for a video being recorded and saved.
*/
@VisibleForTesting
@NonNull
public IdlingResource getVideoSavedIdlingResource() {
return mVideoSavedIdlingResource;
}
/**
* Retrieve idling resource that waits for initialization to finish.
*/
@VisibleForTesting
@NonNull
public IdlingResource getInitializationIdlingResource() {
return mInitializationIdlingResource;
}
/**
* Returns the result of CameraX initialization.
*
* <p>This will only be set after initialization has finished, which will occur once
* {@link #getInitializationIdlingResource()} is idle.
*
* <p>Should only be called on the main thread.
*/
@VisibleForTesting
@MainThread
@Nullable
public CameraXViewModel.CameraProviderResult getCameraProviderResult() {
return mCameraProviderResult;
}
/**
* Retrieve idling resource that waits for view to display frames before proceeding.
*/
@VisibleForTesting
public void resetViewIdlingResource() {
mPreviewFrameCount.set(0);
// Make the view idling resource non-idle, until required frame count achieved.
if (mViewIdlingResource.isIdleNow()) {
mViewIdlingResource.increment();
}
}
/**
* Retrieve idling resource that waits for ImageAnalysis to receive images.
*/
@VisibleForTesting
public void resetAnalysisIdlingResource() {
mImageAnalysisFrameCount.set(0);
// Make the analysis idling resource non-idle, until required images achieved.
if (mAnalysisIdlingResource.isIdleNow()) {
mAnalysisIdlingResource.increment();
}
}
/**
* Retrieve idling resource that waits for VideoCapture to record a video.
*/
@VisibleForTesting
public void resetVideoSavedIdlingResource() {
// Make the video saved idling resource non-idle, until required video length recorded.
if (mVideoSavedIdlingResource.isIdleNow()) {
mVideoSavedIdlingResource.increment();
}
}
/**
* Delete images that were taking during this session so far.
* May leak images if pending captures not completed.
*/
@VisibleForTesting
public void deleteSessionImages() {
mSessionImagesUriSet.deleteAllUris();
}
/**
* Delete videos that were taking during this session so far.
*/
@VisibleForTesting
public void deleteSessionVideos() {
mSessionVideosUriSet.deleteAllUris();
}
@SuppressLint("NullAnnotationGroup")
@OptIn(markerClass = androidx.camera.core.ExperimentalZeroShutterLag.class)
@ImageCapture.CaptureMode
int getCaptureMode() {
if (mZslToggle.isChecked()) {
return ImageCapture.CAPTURE_MODE_ZERO_SHUTTER_LAG;
} else {
return mCaptureQualityToggle.isChecked() ? ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY :
ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY;
}
}
private boolean isFlashAvailable() {
CameraInfo cameraInfo = getCameraInfo();
return mPhotoToggle.isChecked() && cameraInfo != null && cameraInfo.hasFlashUnit();
}
@SuppressLint("RestrictedApiAndroidX")
private boolean isFlashTestSupported(@ImageCapture.FlashMode int flashMode) {
switch (flashMode) {
case FLASH_MODE_OFF:
return false;
case FLASH_MODE_AUTO:
CameraInfo cameraInfo = getCameraInfo();
if (cameraInfo instanceof CameraInfoInternal) {
Quirks deviceQuirks =
androidx.camera.camera2.internal.compat.quirk.DeviceQuirks.getAll();
Quirks cameraQuirks = ((CameraInfoInternal) cameraInfo).getCameraQuirks();
if (deviceQuirks.contains(CrashWhenTakingPhotoWithAutoFlashAEModeQuirk.class)
|| cameraQuirks.contains(ImageCaptureFailWithAutoFlashQuirk.class)
|| cameraQuirks.contains(ImageCaptureFlashNotFireQuirk.class)) {
Toast.makeText(this, DESCRIPTION_FLASH_MODE_NOT_SUPPORTED,
Toast.LENGTH_SHORT).show();
return false;
}
}
break;
default: // fall out
}
return true;
}
private boolean isExposureCompensationSupported() {
CameraInfo cameraInfo = getCameraInfo();
return cameraInfo != null
&& cameraInfo.getExposureState().isExposureCompensationSupported();
}
private void setUpFlashButton() {
mFlashButton.setOnClickListener(v -> {
@ImageCapture.FlashMode int flashMode = getImageCapture().getFlashMode();
if (flashMode == FLASH_MODE_ON) {
getImageCapture().setFlashMode(FLASH_MODE_OFF);
} else if (flashMode == FLASH_MODE_OFF) {
getImageCapture().setFlashMode(FLASH_MODE_AUTO);
} else if (flashMode == FLASH_MODE_AUTO) {
getImageCapture().setFlashMode(FLASH_MODE_ON);
}
updateButtonsUi();
});
}
@SuppressLint({"MissingPermission", "NullAnnotationGroup"})
@OptIn(markerClass = ExperimentalPersistentRecording.class)
private void setUpRecordButton() {
mRecordUi.getButtonRecord().setOnClickListener((view) -> {
RecordUi.State state = mRecordUi.getState();
switch (state) {
case IDLE:
createDefaultVideoFolderIfNotExist();
final PendingRecording pendingRecording;
String fileName = "video_" + System.currentTimeMillis();
String extension = "mp4";
if (canDeviceWriteToMediaStore()) {
// Use MediaStoreOutputOptions for public share media storage.
pendingRecording = getVideoCapture().getOutput().prepareRecording(
this,
generateVideoMediaStoreOptions(getContentResolver(), fileName));
} else {
// Use FileOutputOption for devices in MediaStoreVideoCannotWrite Quirk.
pendingRecording = getVideoCapture().getOutput().prepareRecording(
this, generateVideoFileOutputOptions(fileName, extension));
}
resetVideoSavedIdlingResource();
if (isPersistentRecordingEnabled()) {
pendingRecording.asPersistentRecording();
}
mActiveRecording = pendingRecording
.withAudioEnabled()
.start(ContextCompat.getMainExecutor(CameraXActivity.this),
mVideoRecordEventListener);
mRecordUi.setState(RecordUi.State.RECORDING);
break;
case RECORDING:
case PAUSED:
mActiveRecording.stop();
mActiveRecording = null;
mRecordUi.setState(RecordUi.State.STOPPING);
break;
case STOPPING:
// Record button should be disabled.
default:
throw new IllegalStateException(
"Unexpected state when click record button: " + state);
}
});
mRecordUi.getButtonPause().setOnClickListener(view -> {
RecordUi.State state = mRecordUi.getState();
switch (state) {
case RECORDING:
mActiveRecording.pause();
mRecordUi.setState(RecordUi.State.PAUSED);
break;
case PAUSED:
mActiveRecording.resume();
mRecordUi.setState(RecordUi.State.RECORDING);
break;
case IDLE:
case STOPPING:
// Pause button should be invisible.
default:
throw new IllegalStateException(
"Unexpected state when click pause button: " + state);
}
});
// Final reference to this record UI
mRecordUi.getButtonDynamicRange().setText(getDynamicRangeIconName(mDynamicRange));
mRecordUi.getButtonDynamicRange().setOnClickListener(view -> {
PopupMenu popup = new PopupMenu(this, view);
Menu menu = popup.getMenu();
final int groupId = Menu.NONE;
for (DynamicRange dynamicRange : mSelectableDynamicRanges) {
int itemId = dynamicRangeToItemId(dynamicRange);
menu.add(groupId, itemId, itemId, getDynamicRangeMenuItemName(dynamicRange));
if (Objects.equals(dynamicRange, mDynamicRange)) {
// Apply the checked item for the selected dynamic range to the menu.
menu.findItem(itemId).setChecked(true);
}
}
// Make menu single checkable
menu.setGroupCheckable(groupId, true, true);
popup.setOnMenuItemClickListener(item -> {
DynamicRange dynamicRange = itemIdToDynamicRange(item.getItemId());
if (!Objects.equals(dynamicRange, mDynamicRange)) {
mDynamicRange = dynamicRange;
if (Build.VERSION.SDK_INT >= 26) {
updateWindowColorMode();
}
mRecordUi.getButtonDynamicRange()
.setText(getDynamicRangeIconName(mDynamicRange));
// Dynamic range changed, rebind UseCases
tryBindUseCases();
}
return true;
});
popup.show();
});
mRecordUi.getButtonQuality().setText(getQualityIconName(mVideoQuality));
mRecordUi.getButtonQuality().setOnClickListener(view -> {
PopupMenu popup = new PopupMenu(this, view);
Menu menu = popup.getMenu();
// Add Auto item
final int groupId = Menu.NONE;
final int autoOrder = 0;
final int autoMenuId = qualityToItemId(QUALITY_AUTO);
menu.add(groupId, autoMenuId, autoOrder, getQualityMenuItemName(QUALITY_AUTO));
if (mVideoQuality == QUALITY_AUTO) {
menu.findItem(autoMenuId).setChecked(true);
}
// Add device supported qualities
VideoCapabilities videoCapabilities = Recorder.getVideoCapabilities(
mCamera.getCameraInfo());
List<Quality> supportedQualities = videoCapabilities.getSupportedQualities(
mDynamicRange);
// supportedQualities has been sorted by descending order.
for (int i = 0; i < supportedQualities.size(); i++) {
Quality quality = supportedQualities.get(i);
int itemId = qualityToItemId(quality);
menu.add(groupId, itemId, autoOrder + 1 + i, getQualityMenuItemName(quality));
if (mVideoQuality == quality) {
menu.findItem(itemId).setChecked(true);
}
}
// Make menu single checkable
menu.setGroupCheckable(groupId, true, true);
popup.setOnMenuItemClickListener(item -> {
Quality quality = itemIdToQuality(item.getItemId());
if (quality != mVideoQuality) {
mVideoQuality = quality;
mRecordUi.getButtonQuality().setText(getQualityIconName(mVideoQuality));
// Quality changed, rebind UseCases
tryBindUseCases();
}
return true;
});
popup.show();
});
}
@RequiresApi(26)
private void updateWindowColorMode() {
int colorMode = ActivityInfo.COLOR_MODE_DEFAULT;
if (!Objects.equals(mDynamicRange, DynamicRange.SDR)) {
colorMode = ActivityInfo.COLOR_MODE_HDR;
}
Api26Impl.setColorMode(requireNonNull(getWindow()), colorMode);
}
private static boolean hasTenBitDynamicRange(@NonNull Set<DynamicRange> dynamicRanges) {
for (DynamicRange dynamicRange : dynamicRanges) {
if (dynamicRange.getBitDepth() == DynamicRange.BIT_DEPTH_10_BIT) {
return true;
}
}
return false;
}
private final Consumer<VideoRecordEvent> mVideoRecordEventListener = event -> {
updateRecordingStats(event.getRecordingStats());
if (event instanceof VideoRecordEvent.Finalize) {
VideoRecordEvent.Finalize finalize = (VideoRecordEvent.Finalize) event;
switch (finalize.getError()) {
case ERROR_NONE:
case ERROR_FILE_SIZE_LIMIT_REACHED:
case ERROR_DURATION_LIMIT_REACHED:
case ERROR_INSUFFICIENT_STORAGE:
case ERROR_SOURCE_INACTIVE:
Uri uri = finalize.getOutputResults().getOutputUri();
OutputOptions outputOptions = finalize.getOutputOptions();
String msg;
String videoFilePath;
if (outputOptions instanceof MediaStoreOutputOptions) {
msg = "Saved uri " + uri;
videoFilePath = getAbsolutePathFromUri(
getApplicationContext().getContentResolver(),
uri
);
updateVideoSavedSessionData(uri);
} else if (outputOptions instanceof FileOutputOptions) {
videoFilePath = ((FileOutputOptions) outputOptions).getFile().getPath();
MediaScannerConnection.scanFile(this,
new String[]{videoFilePath}, null,
(path, uri1) -> {
Log.i(TAG, "Scanned " + path + " -> uri= " + uri1);
updateVideoSavedSessionData(uri1);
});
msg = "Saved file " + videoFilePath;
} else {
throw new AssertionError("Unknown or unsupported OutputOptions type: "
+ outputOptions.getClass().getSimpleName());
}
// The video file path is used in tracing e2e test log. Don't remove it.
Log.d(TAG, "Saved video file: " + videoFilePath);
if (finalize.getError() != ERROR_NONE) {
msg += " with code (" + finalize.getError() + ")";
}
Log.d(TAG, msg, finalize.getCause());
Toast.makeText(CameraXActivity.this, msg, Toast.LENGTH_LONG).show();
break;
default:
String errMsg = "Video capture failed by (" + finalize.getError() + "): "
+ finalize.getCause();
Log.e(TAG, errMsg, finalize.getCause());
Toast.makeText(CameraXActivity.this, errMsg, Toast.LENGTH_LONG).show();
}
mRecordUi.setState(RecordUi.State.IDLE);
}
};
private void updateVideoSavedSessionData(@NonNull Uri uri) {
if (mSessionVideosUriSet != null) {
mSessionVideosUriSet.add(uri);
}
if (!mVideoSavedIdlingResource.isIdleNow()) {
mVideoSavedIdlingResource.decrement();
}
}
private void updateRecordingStats(@NonNull RecordingStats stats) {
double durationMs = TimeUnit.NANOSECONDS.toMillis(stats.getRecordedDurationNanos());
// Show megabytes in International System of Units (SI)
double sizeMb = stats.getNumBytesRecorded() / (1000d * 1000d);
String msg = String.format("%.2f sec\n%.2f MB", durationMs / 1000d, sizeMb);
mRecordUi.getTextStats().setText(msg);
if (mVideoCaptureAutoStopLength > 0 && durationMs >= mVideoCaptureAutoStopLength
&& mRecordUi.getState() == RecordUi.State.RECORDING) {
mRecordUi.getButtonRecord().callOnClick();
}
}
private void setUpTakePictureButton() {
mTakePicture.setOnClickListener(
new View.OnClickListener() {
long mStartCaptureTime = 0;
@Override
public void onClick(View view) {
mImageSavedIdlingResource.increment();
mStartCaptureTime = SystemClock.elapsedRealtime();
createDefaultPictureFolderIfNotExist();
Format formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS",
Locale.US);
String fileName = "CoreTestApp-" + formatter.format(
Calendar.getInstance().getTime()) + ".jpg";
ContentValues contentValues = new ContentValues();
contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName);
contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");
ImageCapture.OutputFileOptions outputFileOptions =
new ImageCapture.OutputFileOptions.Builder(
getContentResolver(),
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues).build();
getImageCapture().takePicture(outputFileOptions,
mImageCaptureExecutorService,
new ImageCapture.OnImageSavedCallback() {
@Override
public void onImageSaved(
@NonNull ImageCapture.OutputFileResults
outputFileResults) {
Log.d(TAG, "Saved image to "
+ outputFileResults.getSavedUri());
try {
mImageSavedIdlingResource.decrement();
} catch (IllegalStateException e) {
Log.e(TAG, "Error: unexpected onImageSaved "
+ "callback received. Continuing.");
}
long duration =
SystemClock.elapsedRealtime() - mStartCaptureTime;
runOnUiThread(() -> Toast.makeText(CameraXActivity.this,
"Image captured in " + duration + " ms",
Toast.LENGTH_SHORT).show());
if (mSessionImagesUriSet != null) {
mSessionImagesUriSet.add(
requireNonNull(
outputFileResults.getSavedUri()));
}
}
@Override
public void onError(@NonNull ImageCaptureException exception) {
Log.e(TAG, "Failed to save image.", exception);
mLastTakePictureErrorMessage =
getImageCaptureErrorMessage(exception);
if (!mImageSavedIdlingResource.isIdleNow()) {
mImageSavedIdlingResource.decrement();
}
}
});
}
});
}
private String getImageCaptureErrorMessage(@NonNull ImageCaptureException exception) {
String errorCodeString;
int errorCode = exception.getImageCaptureError();
switch (errorCode) {
case ERROR_UNKNOWN:
errorCodeString = "ImageCaptureErrorCode: ERROR_UNKNOWN";
break;
case ERROR_FILE_IO:
errorCodeString = "ImageCaptureErrorCode: ERROR_FILE_IO";
break;
case ERROR_CAPTURE_FAILED:
errorCodeString = "ImageCaptureErrorCode: ERROR_CAPTURE_FAILED";
break;
case ERROR_CAMERA_CLOSED:
errorCodeString = "ImageCaptureErrorCode: ERROR_CAMERA_CLOSED";
break;
case ERROR_INVALID_CAMERA:
errorCodeString = "ImageCaptureErrorCode: ERROR_INVALID_CAMERA";
break;
default:
errorCodeString = "ImageCaptureErrorCode: " + errorCode;
break;
}
return errorCodeString + ", Message: " + exception.getMessage() + ", Cause: "
+ exception.getCause();
}
@SuppressWarnings("ObjectToString")
private void setUpCameraDirectionButton() {
mCameraDirectionButton.setOnClickListener(v -> {
Log.d(TAG, "Change camera direction: " + mCurrentCameraSelector);
CameraSelector switchedCameraSelector =
getSwitchedCameraSelector(mCurrentCameraSelector);
try {
if (isUseCasesCombinationSupported(switchedCameraSelector, mUseCases)) {
mCurrentCameraSelector = switchedCameraSelector;
tryBindUseCases();
} else {
String msg = "Camera of the other lens facing can't support current use case "
+ "combination.";
Log.d(TAG, msg);
Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
}
} catch (IllegalArgumentException e) {
Toast.makeText(this, "Failed to switch Camera. Error:" + e.getMessage(),
Toast.LENGTH_SHORT).show();
}
});
}
@NonNull
private CameraSelector getSwitchedCameraSelector(
@NonNull CameraSelector currentCameraSelector) {
CameraSelector switchedCameraSelector;
// When the activity is launched with a specific camera id, camera switch function
// will switch the cameras between the camera of the specified camera id and the
// default camera of the opposite lens facing.
if (mLaunchingCameraIdSelector != null) {
if (currentCameraSelector != mLaunchingCameraIdSelector) {
switchedCameraSelector = mLaunchingCameraIdSelector;
} else {
if (mLaunchingCameraLensFacing == CameraSelector.LENS_FACING_BACK) {
switchedCameraSelector = FRONT_SELECTOR;
} else {
switchedCameraSelector = BACK_SELECTOR;
}
}
} else {
if (currentCameraSelector == BACK_SELECTOR) {
switchedCameraSelector = FRONT_SELECTOR;
} else if (currentCameraSelector == FRONT_SELECTOR) {
if (mExternalCameraSelector != null) {
switchedCameraSelector = mExternalCameraSelector;
} else {
switchedCameraSelector = BACK_SELECTOR;
}
} else {
switchedCameraSelector = BACK_SELECTOR;
}
}
return switchedCameraSelector;
}
private boolean isUseCasesCombinationSupported(@NonNull CameraSelector cameraSelector,
@NonNull List<UseCase> useCases) {
if (mCameraProvider == null) {
throw new IllegalStateException("Need to obtain mCameraProvider first!");
}
Camera targetCamera = mCameraProvider.bindToLifecycle(this, cameraSelector);
return targetCamera.isUseCasesCombinationSupported(useCases.toArray(new UseCase[0]));
}
private void setUpTorchButton() {
mTorchButton.setOnClickListener(v -> {
requireNonNull(getCameraInfo());
requireNonNull(getCameraControl());
Integer torchState = getCameraInfo().getTorchState().getValue();
boolean toggledState = !Objects.equals(torchState, TorchState.ON);
Log.d(TAG, "Set camera torch: " + toggledState);
ListenableFuture<Void> future = getCameraControl().enableTorch(toggledState);
Futures.addCallback(future, new FutureCallback<Void>() {
@Override
public void onSuccess(@Nullable Void result) {
}
@Override
public void onFailure(@NonNull Throwable t) {
throw new RuntimeException(t);
}
}, CameraXExecutors.directExecutor());
});
}
private void setUpEVButton() {
mPlusEV.setOnClickListener(v -> {
requireNonNull(getCameraInfo());
requireNonNull(getCameraControl());
ExposureState exposureState = getCameraInfo().getExposureState();
Range<Integer> range = exposureState.getExposureCompensationRange();
int ec = exposureState.getExposureCompensationIndex();
if (range.contains(ec + 1)) {
ListenableFuture<Integer> future =
getCameraControl().setExposureCompensationIndex(ec + 1);
Futures.addCallback(future, mEVFutureCallback,
CameraXExecutors.mainThreadExecutor());
} else {
showEVToast(String.format("EV: %.2f", range.getUpper()
* exposureState.getExposureCompensationStep().floatValue()));
}
});
mDecEV.setOnClickListener(v -> {
requireNonNull(getCameraInfo());
requireNonNull(getCameraControl());
ExposureState exposureState = getCameraInfo().getExposureState();
Range<Integer> range = exposureState.getExposureCompensationRange();
int ec = exposureState.getExposureCompensationIndex();
if (range.contains(ec - 1)) {
ListenableFuture<Integer> future =
getCameraControl().setExposureCompensationIndex(ec - 1);
Futures.addCallback(future, mEVFutureCallback,
CameraXExecutors.mainThreadExecutor());
} else {
showEVToast(String.format("EV: %.2f", range.getLower()
* exposureState.getExposureCompensationStep().floatValue()));
}
});
}
void showEVToast(String message) {
if (mEvToast != null) {
mEvToast.cancel();
}
mEvToast = Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT);
mEvToast.show();
}
void showPreviewStabilizationToast(String message) {
if (mPSToast != null) {
mPSToast.cancel();
}
mPSToast = Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT);
mPSToast.show();
}
private void updateAppUIForE2ETest(@NonNull String testCase) {
if (getSupportActionBar() != null) {
getSupportActionBar().hide();
}
mCaptureQualityToggle.setVisibility(View.GONE);
mZslToggle.setVisibility(View.GONE);
mPlusEV.setVisibility(View.GONE);
mDecEV.setVisibility(View.GONE);
mZoomSeekBar.setVisibility(View.GONE);
mZoomRatioLabel.setVisibility(View.GONE);
mTextView.setVisibility(View.GONE);
if (testCase.equals(PREVIEW_TEST_CASE) || testCase.equals(SWITCH_TEST_CASE)) {
mTorchButton.setVisibility(View.GONE);
mFlashButton.setVisibility(View.GONE);
mTakePicture.setVisibility(View.GONE);
mZoomIn2XToggle.setVisibility(View.GONE);
mZoomResetToggle.setVisibility(View.GONE);
mVideoToggle.setVisibility(View.GONE);
mPhotoToggle.setVisibility(View.GONE);
mPreviewToggle.setVisibility(View.GONE);
mAnalysisToggle.setVisibility(View.GONE);
mRecordUi.hideUi();
if (!testCase.equals(SWITCH_TEST_CASE)) {
mCameraDirectionButton.setVisibility(View.GONE);
}
}
}
private void updatePreviewRatioAndScaleTypeByIntent(ViewStub viewFinderStub) {
Bundle bundle = this.getIntent().getExtras();
if (bundle != null) {
mTargetAspectRatio = bundle.getInt(INTENT_EXTRA_TARGET_ASPECT_RATIO,
AspectRatio.RATIO_4_3);
int scaleType = bundle.getInt(INTENT_EXTRA_SCALE_TYPE, INTENT_EXTRA_FILL_CENTER);
if (scaleType == INTENT_EXTRA_FIT_CENTER) {
// Scale the view according to the target aspect ratio, display size and device
// orientation, so preview can be entirely contained within the view.
DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
Rational ratio = (mTargetAspectRatio == AspectRatio.RATIO_16_9)
? AspectRatioUtil.ASPECT_RATIO_16_9 : AspectRatioUtil.ASPECT_RATIO_4_3;
int orientation = getResources().getConfiguration().orientation;
ViewGroup.LayoutParams lp = viewFinderStub.getLayoutParams();
if (orientation == Configuration.ORIENTATION_PORTRAIT) {
lp.width = displayMetrics.widthPixels;
lp.height = displayMetrics.widthPixels / ratio.getDenominator()
* ratio.getNumerator();
} else {
lp.height = displayMetrics.heightPixels;
lp.width = displayMetrics.heightPixels / ratio.getDenominator()
* ratio.getNumerator();
}
viewFinderStub.setLayoutParams(lp);
}
}
}
@SuppressLint({"NullAnnotationGroup", "RestrictedApiAndroidX"})
@OptIn(markerClass = androidx.camera.core.ExperimentalZeroShutterLag.class)
private void updateButtonsUi() {
mRecordUi.setEnabled(mVideoToggle.isChecked());
mTakePicture.setEnabled(mPhotoToggle.isChecked());
mCaptureQualityToggle.setEnabled(mPhotoToggle.isChecked());
mZslToggle.setVisibility(getCameraInfo() != null
&& getCameraInfo().isZslSupported() ? View.VISIBLE : View.GONE);
mZslToggle.setEnabled(mPhotoToggle.isChecked());
mCameraDirectionButton.setEnabled(getCameraInfo() != null);
mPreviewStabilizationToggle.setEnabled(mCamera != null
&& Preview.getPreviewCapabilities(getCameraInfo()).isStabilizationSupported());
mTorchButton.setEnabled(isFlashAvailable());
// Flash button
mFlashButton.setEnabled(mPhotoToggle.isChecked() && isFlashAvailable());
if (mPhotoToggle.isChecked()) {
int flashMode = getImageCapture().getFlashMode();
if (isFlashTestSupported(flashMode)) {
// Reset content description if flash is ready for test.
mFlashButton.setContentDescription("");
} else {
// Set content description for e2e testing.
mFlashButton.setContentDescription(DESCRIPTION_FLASH_MODE_NOT_SUPPORTED);
}
switch (flashMode) {
case FLASH_MODE_ON:
mFlashButton.setImageResource(R.drawable.ic_flash_on);
break;
case FLASH_MODE_OFF:
mFlashButton.setImageResource(R.drawable.ic_flash_off);
break;
case FLASH_MODE_AUTO:
mFlashButton.setImageResource(R.drawable.ic_flash_auto);
break;
}
}
mPlusEV.setEnabled(isExposureCompensationSupported());
mDecEV.setEnabled(isExposureCompensationSupported());
mZoomIn2XToggle.setEnabled(is2XZoomSupported());
}
private void setUpButtonEvents() {
mVideoToggle.setOnCheckedChangeListener(mOnCheckedChangeListener);
mPhotoToggle.setOnCheckedChangeListener(mOnCheckedChangeListener);
mAnalysisToggle.setOnCheckedChangeListener(mOnCheckedChangeListener);
mPreviewToggle.setOnCheckedChangeListener(mOnCheckedChangeListener);
setUpRecordButton();
setUpFlashButton();
setUpTakePictureButton();
setUpCameraDirectionButton();
setUpTorchButton();
setUpEVButton();
setUpZoomButton();
setUpPreviewStabilizationButton();
mCaptureQualityToggle.setOnCheckedChangeListener(mOnCheckedChangeListener);
mZslToggle.setOnCheckedChangeListener(mOnCheckedChangeListener);
}
private void updateUseCaseCombinationByIntent(@NonNull Intent intent) {
Bundle bundle = intent.getExtras();
if (bundle == null) {
return;
}
int useCaseCombination = bundle.getInt(INTENT_EXTRA_USE_CASE_COMBINATION, 0);
if (useCaseCombination == 0) {
return;
}
mPreviewToggle.setChecked((useCaseCombination & BIND_PREVIEW) != 0L);
mPhotoToggle.setChecked((useCaseCombination & BIND_IMAGE_CAPTURE) != 0L);
mVideoToggle.setChecked((useCaseCombination & BIND_VIDEO_CAPTURE) != 0L);
mAnalysisToggle.setChecked((useCaseCombination & BIND_IMAGE_ANALYSIS) != 0L);
}
private void updateVideoMirrorModeByIntent(@NonNull Intent intent) {
int mirrorMode = intent.getIntExtra(INTENT_EXTRA_VIDEO_MIRROR_MODE, -1);
if (mirrorMode != -1) {
Log.d(TAG, "updateVideoMirrorModeByIntent: mirrorMode = " + mirrorMode);
mVideoMirrorMode = mirrorMode;
}
}
private void updateVideoQualityByIntent(@NonNull Intent intent) {
Bundle bundle = intent.getExtras();
if (bundle == null) {
return;
}
Quality quality = itemIdToQuality(bundle.getInt(INTENT_EXTRA_VIDEO_QUALITY, 0));
if (quality == QUALITY_AUTO || !mVideoToggle.isChecked()) {
return;
}
if (mCameraProvider == null) {
throw new IllegalStateException("Need to obtain mCameraProvider first!");
}
// Check and set specific quality.
Camera targetCamera = mCameraProvider.bindToLifecycle(this, mCurrentCameraSelector);
VideoCapabilities videoCapabilities = Recorder.getVideoCapabilities(
targetCamera.getCameraInfo());
List<Quality> supportedQualities = videoCapabilities.getSupportedQualities(mDynamicRange);
if (supportedQualities.contains(quality)) {
mVideoQuality = quality;
mRecordUi.getButtonQuality().setText(getQualityIconName(mVideoQuality));
}
}
@SuppressLint("NullAnnotationGroup")
@OptIn(markerClass = ExperimentalLensFacing.class)
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//if different Camera Provider (CameraPipe vs Camera2 was initialized in previous session,
//then close this application.
closeAppIfCameraProviderMismatch(this.getIntent());
setContentView(R.layout.activity_camera_xmain);
mImageCaptureExecutorService = Executors.newSingleThreadExecutor();
Display display = null;
if (Build.VERSION.SDK_INT >= 30) {
display = OpenGLActivity.Api30Impl.getDisplay(this);
}
OpenGLRenderer previewRenderer = mPreviewRenderer =
new OpenGLRenderer(OpenGLActivity.getHdrEncodingsSupportedByDisplay(display));
ViewStub viewFinderStub = findViewById(R.id.viewFinderStub);
updatePreviewRatioAndScaleTypeByIntent(viewFinderStub);
updateVideoMirrorModeByIntent(getIntent());
mViewFinder = OpenGLActivity.chooseViewFinder(getIntent().getExtras(), viewFinderStub,
previewRenderer);
mViewFinder.addOnLayoutChangeListener(
(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom)
-> tryBindUseCases());
mVideoToggle = findViewById(R.id.VideoToggle);
mPhotoToggle = findViewById(R.id.PhotoToggle);
mAnalysisToggle = findViewById(R.id.AnalysisToggle);
mPreviewToggle = findViewById(R.id.PreviewToggle);
updateUseCaseCombinationByIntent(getIntent());
mTakePicture = findViewById(R.id.Picture);
mFlashButton = findViewById(R.id.flash_toggle);
mCameraDirectionButton = findViewById(R.id.direction_toggle);
mTorchButton = findViewById(R.id.torch_toggle);
mCaptureQualityToggle = findViewById(R.id.capture_quality);
mPlusEV = findViewById(R.id.plus_ev_toggle);
mDecEV = findViewById(R.id.dec_ev_toggle);
mZslToggle = findViewById(R.id.zsl_toggle);
mPreviewStabilizationToggle = findViewById(R.id.preview_stabilization);
mZoomSeekBar = findViewById(R.id.seekBar);
mZoomRatioLabel = findViewById(R.id.zoomRatio);
mZoomIn2XToggle = findViewById(R.id.zoom_in_2x_toggle);
mZoomResetToggle = findViewById(R.id.zoom_reset_toggle);
mTextView = findViewById(R.id.textView);
mRecordUi = new RecordUi(
findViewById(R.id.Video),
findViewById(R.id.video_pause),
findViewById(R.id.video_stats),
findViewById(R.id.video_quality),
findViewById(R.id.video_persistent),
findViewById(R.id.video_dynamic_range)
);
setUpButtonEvents();
setupViewFinderGestureControls();
mImageAnalysisResult.observe(
this,
text -> {
if (mImageAnalysisFrameCount.getAndIncrement() % 30 == 0) {
mTextView.setText(
"ImgCount: " + mImageAnalysisFrameCount.get() + " @ts: "
+ text);
}
});
mDisplayListener = new DisplayManager.DisplayListener() {
@Override
public void onDisplayAdded(int displayId) {
}
@Override
public void onDisplayRemoved(int displayId) {
}
@Override
public void onDisplayChanged(int displayId) {
Display viewFinderDisplay = mViewFinder.getDisplay();
if (viewFinderDisplay != null && viewFinderDisplay.getDisplayId() == displayId) {
previewRenderer.invalidateSurface(
Surfaces.toSurfaceRotationDegrees(viewFinderDisplay.getRotation()));
}
}
};
DisplayManager dpyMgr =
requireNonNull(ContextCompat.getSystemService(this, DisplayManager.class));
dpyMgr.registerDisplayListener(mDisplayListener, new Handler(Looper.getMainLooper()));
StrictMode.VmPolicy vmPolicy =
new StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build();
StrictMode.setVmPolicy(vmPolicy);
StrictMode.ThreadPolicy threadPolicy =
new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build();
StrictMode.setThreadPolicy(threadPolicy);
// Get params from adb extra string
Bundle bundle = this.getIntent().getExtras();
if (bundle != null) {
String launchingCameraId = bundle.getString(INTENT_EXTRA_CAMERA_ID, null);
if (launchingCameraId != null) {
mLaunchingCameraIdSelector = createCameraSelectorById(launchingCameraId);
mCurrentCameraSelector = mLaunchingCameraIdSelector;
} else {
String newCameraDirection = bundle.getString(INTENT_EXTRA_CAMERA_DIRECTION);
if (newCameraDirection != null) {
if (newCameraDirection.equals(BACKWARD)) {
mCurrentCameraSelector = BACK_SELECTOR;
} else {
mCurrentCameraSelector = FRONT_SELECTOR;
}
}
}
String cameraImplementation = bundle.getString(INTENT_EXTRA_CAMERA_IMPLEMENTATION);
boolean cameraImplementationNoHistory =
bundle.getBoolean(INTENT_EXTRA_CAMERA_IMPLEMENTATION_NO_HISTORY, false);
if (cameraImplementationNoHistory) {
Intent newIntent = new Intent(getIntent());
newIntent.removeExtra(INTENT_EXTRA_CAMERA_IMPLEMENTATION);
newIntent.removeExtra(INTENT_EXTRA_CAMERA_IMPLEMENTATION_NO_HISTORY);
setIntent(newIntent);
}
if (cameraImplementation != null) {
if (cameraImplementation.equalsIgnoreCase(
CameraXViewModel.CAMERA_PIPE_IMPLEMENTATION_OPTION)) {
setTitle(APP_TITLE_FOR_CAMERA_PIPE);
}
CameraXViewModel.configureCameraProvider(
cameraImplementation, cameraImplementationNoHistory);
}
// Update the app UI according to the e2e test case.
String testCase = bundle.getString(INTENT_EXTRA_E2E_TEST_CASE);
if (testCase != null) {
updateAppUIForE2ETest(testCase);
}
}
mInitializationIdlingResource.increment();
CameraXViewModel viewModel = new ViewModelProvider(this).get(CameraXViewModel.class);
viewModel.getCameraProvider().observe(this, cameraProviderResult -> {
mCameraProviderResult = cameraProviderResult;
mInitializationIdlingResource.decrement();
if (cameraProviderResult.hasProvider()) {
mCameraProvider = cameraProviderResult.getProvider();
//initialize mExternalCameraSelector
CameraSelector externalCameraSelectorLocal = new CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_EXTERNAL).build();
List<CameraInfo> cameraInfos = externalCameraSelectorLocal.filter(
mCameraProvider.getAvailableCameraInfos());
if (cameraInfos.size() > 0) {
mExternalCameraSelector = externalCameraSelectorLocal;
}
updateVideoQualityByIntent(getIntent());
tryBindUseCases();
} else {
Log.e(TAG, "Failed to retrieve ProcessCameraProvider",
cameraProviderResult.getError());
Toast.makeText(getApplicationContext(), "Unable to initialize CameraX. See logs "
+ "for details.", Toast.LENGTH_LONG).show();
}
});
setupPermissions();
}
/**
* Close current app if CameraProvider from intent of current activity doesn't match with
* CameraProvider stored in the CameraXViewModel, because CameraProvider can't be changed
* between Camera2 and Camera Pipe while app is running.
*/
private void closeAppIfCameraProviderMismatch(Intent mIntent) {
String cameraImplementation = null;
boolean cameraImplementationNoHistory = false;
Bundle bundle = mIntent.getExtras();
if (bundle != null) {
cameraImplementation = bundle.getString(INTENT_EXTRA_CAMERA_IMPLEMENTATION);
cameraImplementationNoHistory =
bundle.getBoolean(INTENT_EXTRA_CAMERA_IMPLEMENTATION_NO_HISTORY, false);
}
if (!cameraImplementationNoHistory) {
if (!CameraXViewModel.isCameraProviderUnInitializedOrSameAsParameter(
cameraImplementation)) {
Toast.makeText(CameraXActivity.this, "Please relaunch "
+ "the app to apply new CameraX configuration.",
Toast.LENGTH_LONG).show();
finish();
System.exit(0);
}
}
}
@Override
public void onDestroy() {
super.onDestroy();
DisplayManager dpyMgr =
requireNonNull(ContextCompat.getSystemService(this, DisplayManager.class));
dpyMgr.unregisterDisplayListener(mDisplayListener);
mPreviewRenderer.shutdown();
mImageCaptureExecutorService.shutdown();
}
void tryBindUseCases() {
tryBindUseCases(false);
}
/**
* Try building and binding current use cases.
*
* @param calledBySelf flag indicates if this is a recursive call.
*/
void tryBindUseCases(boolean calledBySelf) {
boolean isViewFinderReady = mViewFinder.getWidth() != 0 && mViewFinder.getHeight() != 0;
boolean isCameraReady = mCameraProvider != null;
if (isPermissionMissing() || !isCameraReady || !isViewFinderReady) {
// No-op if permission if something is not ready. It will try again upon the
// next thing being ready.
return;
}
// Clear listening frame update before unbind all.
mPreviewRenderer.clearFrameUpdateListener();
// Remove ZoomState observer from old CameraInfo to prevent from receiving event from old
// CameraInfo
if (mCamera != null) {
mCamera.getCameraInfo().getZoomState().removeObservers(this);
}
// Stop in-progress video recording if it's not a persistent recording.
if (hasRunningRecording() && !isPersistentRecordingEnabled()) {
mActiveRecording.stop();
mActiveRecording = null;
mRecordUi.setState(RecordUi.State.STOPPING);
}
mCameraProvider.unbindAll();
try {
// Binds to lifecycle without use cases to make sure mCamera can be retrieved for
// tests to do necessary checks.
mCamera = mCameraProvider.bindToLifecycle(this, mCurrentCameraSelector);
// Retrieves the lens facing info when the activity is launched with a specified
// camera id.
if (mCurrentCameraSelector == mLaunchingCameraIdSelector
&& mLaunchingCameraLensFacing == CameraSelector.LENS_FACING_UNKNOWN) {
mLaunchingCameraLensFacing = getLensFacing(mCamera.getCameraInfo());
}
List<UseCase> useCases = buildUseCases();
mCamera = bindToLifecycleSafely(useCases);
// Set the use cases after a successful binding.
mUseCases = useCases;
} catch (IllegalArgumentException ex) {
String msg;
if (mVideoQuality != QUALITY_AUTO) {
msg = "Bind too many use cases or video quality is too large.";
} else if (!Objects.equals(mDynamicRange, DynamicRange.SDR)) {
msg = "Bind too many use cases or unsupported dynamic range combination.";
} else {
msg = "Bind too many use cases.";
}
Log.e(TAG, "bindToLifecycle() failed. " + msg, ex);
Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
// Restore toggle buttons to the previous state if the bind failed.
if (mUseCases != null) {
mPreviewToggle.setChecked(getPreview() != null);
mPhotoToggle.setChecked(getImageCapture() != null);
mAnalysisToggle.setChecked(getImageAnalysis() != null);
mVideoToggle.setChecked(getVideoCapture() != null);
}
// Reset video quality to avoid always fail by quality too large.
mRecordUi.getButtonQuality().setText(getQualityIconName(mVideoQuality = QUALITY_AUTO));
// Reset video dynamic range to avoid failure
mRecordUi.getButtonDynamicRange().setText(
getDynamicRangeIconName(mDynamicRange = DynamicRange.SDR));
reduceUseCaseToFindSupportedCombination();
if (!calledBySelf) {
// Only call self if not already calling self to avoid an infinite loop.
tryBindUseCases(true);
}
}
updateButtonsUi();
}
private boolean hasRunningRecording() {
RecordUi.State recordState = mRecordUi.getState();
return recordState == RecordUi.State.RECORDING || recordState == RecordUi.State.PAUSED;
}
private boolean isPersistentRecordingEnabled() {
return mRecordUi.getButtonPersistent().isChecked();
}
/**
* Checks whether currently checked use cases combination can be supported or not.
*/
private boolean isCheckedUseCasesCombinationSupported() {
return mCamera.isUseCasesCombinationSupported(buildUseCases().toArray(new UseCase[0]));
}
/**
* Unchecks use case to find a supported use cases combination.
*
* <p>Only VideoCapture or ImageAnalysis will be tried to uncheck. If only Preview and
* ImageCapture are remained, the combination should always be supported.
*/
private void reduceUseCaseToFindSupportedCombination() {
// Checks whether current combination can be supported
if (isCheckedUseCasesCombinationSupported()) {
return;
}
// Remove VideoCapture to check whether the new use cases combination can be supported.
if (mVideoToggle.isChecked()) {
mVideoToggle.setChecked(false);
if (isCheckedUseCasesCombinationSupported()) {
return;
}
}
// Remove ImageAnalysis to check whether the new use cases combination can be supported.
if (mAnalysisToggle.isChecked()) {
mAnalysisToggle.setChecked(false);
// No need to do further use case combination check since Preview + ImageCapture
// should be always supported.
}
}
/**
* Builds all use cases based on current settings and return as an array.
*/
@SuppressLint("RestrictedApiAndroidX")
private List<UseCase> buildUseCases() {
List<UseCase> useCases = new ArrayList<>();
if (mPreviewToggle.isChecked()) {
Preview preview = new Preview.Builder()
.setTargetName("Preview")
.setTargetAspectRatio(mTargetAspectRatio)
.setPreviewStabilizationEnabled(mIsPreviewStabilizationOn)
.build();
resetViewIdlingResource();
// Use the listener of the future to make sure the Preview setup the new surface.
mPreviewRenderer.attachInputPreview(preview).addListener(() -> {
Log.d(TAG, "OpenGLRenderer get the new surface for the Preview");
mPreviewRenderer.setFrameUpdateListener(
ContextCompat.getMainExecutor(this), mFrameUpdateListener
);
}, ContextCompat.getMainExecutor(this));
useCases.add(preview);
}
if (mPhotoToggle.isChecked()) {
ImageCapture imageCapture = new ImageCapture.Builder()
.setCaptureMode(getCaptureMode())
.setTargetAspectRatio(mTargetAspectRatio)
.setTargetName("ImageCapture")
.build();
useCases.add(imageCapture);
}
if (mAnalysisToggle.isChecked()) {
ImageAnalysis imageAnalysis = new ImageAnalysis.Builder()
.setTargetName("ImageAnalysis")
.build();
useCases.add(imageAnalysis);
// Make the analysis idling resource non-idle, until the required frames received.
resetAnalysisIdlingResource();
imageAnalysis.setAnalyzer(ContextCompat.getMainExecutor(this), mAnalyzer);
}
if (mVideoToggle.isChecked()) {
// Update possible dynamic ranges for current camera
updateDynamicRangeConfiguration();
// Recreate the Recorder except there's a running persistent recording, existing
// Recorder. We may later consider reuse the Recorder everytime if the quality didn't
// change.
if (mVideoCapture == null
|| mRecorder == null
|| !(hasRunningRecording() && isPersistentRecordingEnabled())) {
Recorder.Builder builder = new Recorder.Builder();
if (mVideoQuality != QUALITY_AUTO) {
builder.setQualitySelector(QualitySelector.from(mVideoQuality));
}
mRecorder = builder.build();
mVideoCapture = new VideoCapture.Builder<>(mRecorder)
.setMirrorMode(mVideoMirrorMode)
.setDynamicRange(mDynamicRange)
.build();
}
useCases.add(mVideoCapture);
}
return useCases;
}
private void updateDynamicRangeConfiguration() {
mSelectableDynamicRanges.clear();
// Get the list of available dynamic ranges for the current quality
VideoCapabilities videoCapabilities = Recorder.getVideoCapabilities(
mCamera.getCameraInfo());
Set<DynamicRange> supportedDynamicRanges =
videoCapabilities.getSupportedDynamicRanges();
if (supportedDynamicRanges.size() > 1) {
mRecordUi.setDynamicRangeConfigurable(true);
if (hasTenBitDynamicRange(supportedDynamicRanges)) {
mSelectableDynamicRanges.add(DynamicRange.HDR_UNSPECIFIED_10_BIT);
}
} else {
mRecordUi.setDynamicRangeConfigurable(false);
}
mSelectableDynamicRanges.addAll(supportedDynamicRanges);
// In case the previous dynamic range held in mDynamicRange isn't supported, reset
// to SDR.
if (!mSelectableDynamicRanges.contains(mDynamicRange)) {
mDynamicRange = DynamicRange.SDR;
}
}
/**
* Request permission if missing.
*/
private void setupPermissions() {
if (isPermissionMissing()) {
ActivityResultLauncher<String[]> permissionLauncher =
registerForActivityResult(
new ActivityResultContracts.RequestMultiplePermissions(),
result -> {
for (String permission : REQUIRED_PERMISSIONS) {
if (!requireNonNull(result.get(permission))) {
Toast.makeText(getApplicationContext(),
"Camera permission denied.",
Toast.LENGTH_SHORT)
.show();
finish();
return;
}
}
tryBindUseCases();
});
permissionLauncher.launch(REQUIRED_PERMISSIONS);
} else {
// Permissions already granted. Start camera.
tryBindUseCases();
}
}
/** Returns true if any of the required permissions is missing. */
private boolean isPermissionMissing() {
for (String permission : REQUIRED_PERMISSIONS) {
if (ContextCompat.checkSelfPermission(this, permission)
!= PackageManager.PERMISSION_GRANTED) {
return true;
}
}
return false;
}
void createDefaultPictureFolderIfNotExist() {
File pictureFolder = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES);
if (createFolder(pictureFolder)) {
Log.e(TAG, "Failed to create directory: " + pictureFolder);
}
}
/** Checks the folder existence by how the video file be created. */
private void createDefaultVideoFolderIfNotExist() {
String videoFilePath =
getAbsolutePathFromUri(getApplicationContext().getContentResolver(),
MediaStore.Video.Media.EXTERNAL_CONTENT_URI);
if (videoFilePath == null || !createParentFolder(videoFilePath)) {
Log.e(TAG, "Failed to create parent directory for: " + videoFilePath);
}
}
/**
* Binds use cases to the current lifecycle.
*/
private Camera bindToLifecycleSafely(List<UseCase> useCases) {
ViewPort viewPort = new ViewPort.Builder(new Rational(mViewFinder.getWidth(),
mViewFinder.getHeight()),
mViewFinder.getDisplay().getRotation())
.setScaleType(ViewPort.FILL_CENTER).build();
UseCaseGroup.Builder useCaseGroupBuilder = new UseCaseGroup.Builder().setViewPort(
viewPort);
for (UseCase useCase : useCases) {
useCaseGroupBuilder.addUseCase(useCase);
}
mCamera = mCameraProvider.bindToLifecycle(this, mCurrentCameraSelector,
useCaseGroupBuilder.build());
setupZoomSeeker();
return mCamera;
}
private static final int MAX_SEEKBAR_VALUE = 100000;
void showZoomRatioIsAlive() {
mZoomRatioLabel.setTextColor(getResources().getColor(R.color.zoom_ratio_activated));
}
void showNormalZoomRatio() {
mZoomRatioLabel.setTextColor(getResources().getColor(R.color.zoom_ratio_set));
}
ScaleGestureDetector.SimpleOnScaleGestureListener mScaleGestureListener =
new ScaleGestureDetector.SimpleOnScaleGestureListener() {
@Override
public boolean onScale(@NonNull ScaleGestureDetector detector) {
if (mCamera == null) {
return true;
}
CameraInfo cameraInfo = mCamera.getCameraInfo();
float newZoom =
requireNonNull(cameraInfo.getZoomState().getValue()).getZoomRatio()
* detector.getScaleFactor();
setZoomRatio(newZoom);
return true;
}
};
GestureDetector.OnGestureListener onTapGestureListener =
new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onSingleTapUp(@NonNull MotionEvent e) {
if (mCamera == null) {
return false;
}
// Since we are showing full camera preview we will be using
// DisplayOrientedMeteringPointFactory to map the view's (x, y) to a
// metering point.
MeteringPointFactory factory =
new DisplayOrientedMeteringPointFactory(
mViewFinder.getDisplay(),
mCamera.getCameraInfo(),
mViewFinder.getWidth(),
mViewFinder.getHeight());
FocusMeteringAction action = new FocusMeteringAction.Builder(
factory.createPoint(e.getX(), e.getY())
).build();
Futures.addCallback(
mCamera.getCameraControl().startFocusAndMetering(action),
new FutureCallback<FocusMeteringResult>() {
@Override
public void onSuccess(FocusMeteringResult result) {
Log.d(TAG, "Focus and metering succeeded.");
}
@Override
public void onFailure(@NonNull Throwable t) {
Log.e(TAG, "Focus and metering failed.", t);
}
},
CameraXExecutors.mainThreadExecutor());
return true;
}
};
private void setupZoomSeeker() {
CameraControl cameraControl = mCamera.getCameraControl();
CameraInfo cameraInfo = mCamera.getCameraInfo();
mZoomSeekBar.setMax(MAX_SEEKBAR_VALUE);
mZoomSeekBar.setProgress(
(int) (requireNonNull(cameraInfo.getZoomState().getValue()).getLinearZoom()
* MAX_SEEKBAR_VALUE));
mZoomSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (!fromUser) {
return;
}
float percentage = (float) progress / MAX_SEEKBAR_VALUE;
showNormalZoomRatio();
ListenableFuture<Void> listenableFuture =
cameraControl.setLinearZoom(percentage);
Futures.addCallback(listenableFuture, new FutureCallback<Void>() {
@Override
public void onSuccess(@Nullable Void result) {
Log.d(TAG, "setZoomPercentage " + percentage + " onSuccess");
showZoomRatioIsAlive();
}
@Override
public void onFailure(@NonNull Throwable t) {
Log.d(TAG, "setZoomPercentage " + percentage + " failed, " + t);
}
}, ContextCompat.getMainExecutor(CameraXActivity.this));
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
cameraInfo.getZoomState().removeObservers(this);
cameraInfo.getZoomState().observe(this,
state -> {
String str = String.format("%.2fx", state.getZoomRatio());
mZoomRatioLabel.setText(str);
mZoomSeekBar.setProgress((int) (MAX_SEEKBAR_VALUE * state.getLinearZoom()));
});
}
private boolean is2XZoomSupported() {
CameraInfo cameraInfo = getCameraInfo();
return cameraInfo != null
&& requireNonNull(cameraInfo.getZoomState().getValue()).getMaxZoomRatio() >= 2.0f;
}
private void setUpZoomButton() {
mZoomIn2XToggle.setOnClickListener(v -> setZoomRatio(2.0f));
mZoomResetToggle.setOnClickListener(v -> setZoomRatio(1.0f));
}
private void setUpPreviewStabilizationButton() {
mPreviewStabilizationToggle.setOnClickListener(v -> {
mIsPreviewStabilizationOn = !mIsPreviewStabilizationOn;
if (mIsPreviewStabilizationOn) {
showPreviewStabilizationToast("Preview Stabilization On, FOV changes");
}
tryBindUseCases();
});
}
void setZoomRatio(float newZoom) {
if (mCamera == null) {
return;
}
CameraInfo cameraInfo = mCamera.getCameraInfo();
CameraControl cameraControl = mCamera.getCameraControl();
float clampedNewZoom = MathUtils.clamp(newZoom,
requireNonNull(cameraInfo.getZoomState().getValue()).getMinZoomRatio(),
cameraInfo.getZoomState().getValue().getMaxZoomRatio());
Log.d(TAG, "setZoomRatio ratio: " + clampedNewZoom);
showNormalZoomRatio();
ListenableFuture<Void> listenableFuture = cameraControl.setZoomRatio(
clampedNewZoom);
Futures.addCallback(listenableFuture, new FutureCallback<Void>() {
@Override
public void onSuccess(@Nullable Void result) {
Log.d(TAG, "setZoomRatio onSuccess: " + clampedNewZoom);
showZoomRatioIsAlive();
}
@Override
public void onFailure(@NonNull Throwable t) {
Log.d(TAG, "setZoomRatio failed, " + t);
}
}, ContextCompat.getMainExecutor(CameraXActivity.this));
}
private void setupViewFinderGestureControls() {
GestureDetector tapGestureDetector = new GestureDetector(this, onTapGestureListener);
ScaleGestureDetector scaleDetector = new ScaleGestureDetector(this, mScaleGestureListener);
mViewFinder.setOnTouchListener((view, e) -> {
boolean tapEventProcessed = tapGestureDetector.onTouchEvent(e);
boolean scaleEventProcessed = scaleDetector.onTouchEvent(e);
return tapEventProcessed || scaleEventProcessed;
});
}
private class SessionMediaUriSet {
private final Set<Uri> mSessionMediaUris;
SessionMediaUriSet() {
mSessionMediaUris = Collections.synchronizedSet(new HashSet<>());
}
public void add(@NonNull Uri uri) {
mSessionMediaUris.add(uri);
}
public void deleteAllUris() {
synchronized (mSessionMediaUris) {
Iterator<Uri> it = mSessionMediaUris.iterator();
while (it.hasNext()) {
try {
getContentResolver().delete(it.next(), null, null);
} catch (SecurityException e) {
Log.w(TAG, "Cannot delete the content.", e);
}
it.remove();
}
}
}
}
@UiThread
private static class RecordUi {
enum State {
IDLE, RECORDING, PAUSED, STOPPING
}
private final Button mButtonRecord;
private final Button mButtonPause;
private final TextView mTextStats;
private final Button mButtonQuality;
private final ToggleButton mButtonPersistent;
private final Button mButtonDynamicRange;
private boolean mDynamicRangeConfigurable = false;
private boolean mEnabled = false;
private State mState = State.IDLE;
RecordUi(@NonNull Button buttonRecord, @NonNull Button buttonPause,
@NonNull TextView textStats, @NonNull Button buttonQuality,
@NonNull ToggleButton buttonPersistent,
@NonNull Button buttonDynamicRange) {
mButtonRecord = buttonRecord;
mButtonPause = buttonPause;
mTextStats = textStats;
mButtonQuality = buttonQuality;
mButtonPersistent = buttonPersistent;
mButtonDynamicRange = buttonDynamicRange;
}
void setEnabled(boolean enabled) {
mEnabled = enabled;
if (enabled) {
mTextStats.setText("");
mTextStats.setVisibility(View.VISIBLE);
mButtonQuality.setVisibility(View.VISIBLE);
mButtonPersistent.setVisibility(View.VISIBLE);
mButtonDynamicRange.setVisibility(View.VISIBLE);
updateUi();
} else {
mButtonRecord.setText("Record");
mButtonRecord.setEnabled(false);
mButtonPause.setVisibility(View.INVISIBLE);
mButtonQuality.setVisibility(View.INVISIBLE);
mButtonDynamicRange.setVisibility(View.INVISIBLE);
mTextStats.setVisibility(View.GONE);
mButtonPersistent.setVisibility(View.INVISIBLE);
}
}
void setState(@NonNull State state) {
mState = state;
updateUi();
}
@NonNull
State getState() {
return mState;
}
void hideUi() {
mButtonRecord.setVisibility(View.GONE);
mButtonPause.setVisibility(View.GONE);
mTextStats.setVisibility(View.GONE);
mButtonPersistent.setVisibility(View.GONE);
}
private void setDynamicRangeConfigurable(boolean configurable) {
if (configurable != mDynamicRangeConfigurable) {
mDynamicRangeConfigurable = configurable;
boolean buttonEnabled = mButtonDynamicRange.isEnabled();
mButtonDynamicRange.setEnabled(buttonEnabled && mDynamicRangeConfigurable);
}
}
private void updateUi() {
if (!mEnabled) {
return;
}
switch (mState) {
case IDLE:
mButtonRecord.setText("Record");
mButtonRecord.setEnabled(true);
mButtonPause.setText("Pause");
mButtonPause.setVisibility(View.INVISIBLE);
mButtonPersistent.setEnabled(true);
mButtonQuality.setEnabled(true);
mButtonDynamicRange.setEnabled(mDynamicRangeConfigurable);
break;
case RECORDING:
mButtonRecord.setText("Stop");
mButtonRecord.setEnabled(true);
mButtonPause.setText("Pause");
mButtonPause.setVisibility(View.VISIBLE);
mButtonPersistent.setEnabled(false);
mButtonQuality.setEnabled(false);
mButtonDynamicRange.setEnabled(false);
break;
case STOPPING:
mButtonRecord.setText("Saving");
mButtonRecord.setEnabled(false);
mButtonPause.setText("Pause");
mButtonPause.setVisibility(View.INVISIBLE);
mButtonPersistent.setEnabled(false);
mButtonQuality.setEnabled(true);
mButtonDynamicRange.setEnabled(mDynamicRangeConfigurable);
break;
case PAUSED:
mButtonRecord.setText("Stop");
mButtonRecord.setEnabled(true);
mButtonPause.setText("Resume");
mButtonPause.setVisibility(View.VISIBLE);
mButtonPersistent.setEnabled(false);
mButtonQuality.setEnabled(true);
mButtonDynamicRange.setEnabled(mDynamicRangeConfigurable);
break;
}
}
Button getButtonRecord() {
return mButtonRecord;
}
Button getButtonPause() {
return mButtonPause;
}
TextView getTextStats() {
return mTextStats;
}
@NonNull
Button getButtonQuality() {
return mButtonQuality;
}
ToggleButton getButtonPersistent() {
return mButtonPersistent;
}
@NonNull
Button getButtonDynamicRange() {
return mButtonDynamicRange;
}
}
Preview getPreview() {
return findUseCase(Preview.class);
}
ImageAnalysis getImageAnalysis() {
return findUseCase(ImageAnalysis.class);
}
ImageCapture getImageCapture() {
return findUseCase(ImageCapture.class);
}
@Nullable
View getViewFinder() {
return mViewFinder;
}
/**
* Returns the error message of the last take picture action if any error occurs. Returns
* null if no error occurs.
*/
@VisibleForTesting
@Nullable
String getLastTakePictureErrorMessage() {
return mLastTakePictureErrorMessage;
}
@VisibleForTesting
void cleanTakePictureErrorMessage() {
mLastTakePictureErrorMessage = null;
}
@SuppressWarnings("unchecked")
VideoCapture<Recorder> getVideoCapture() {
return findUseCase(VideoCapture.class);
}
@VisibleForTesting
void setVideoCaptureAutoStopLength(long autoStopLengthInMs) {
mVideoCaptureAutoStopLength = autoStopLengthInMs;
}
/**
* Finds the use case by the given class.
*/
@Nullable
private <T extends UseCase> T findUseCase(Class<T> useCaseSubclass) {
if (mUseCases != null) {
for (UseCase useCase : mUseCases) {
if (useCaseSubclass.isInstance(useCase)) {
return useCaseSubclass.cast(useCase);
}
}
}
return null;
}
@VisibleForTesting
@Nullable
public Camera getCamera() {
return mCamera;
}
@VisibleForTesting
@Nullable
CameraInfo getCameraInfo() {
return mCamera != null ? mCamera.getCameraInfo() : null;
}
@VisibleForTesting
@Nullable
CameraControl getCameraControl() {
return mCamera != null ? mCamera.getCameraControl() : null;
}
@NonNull
private static String getQualityIconName(@Nullable Quality quality) {
if (quality == QUALITY_AUTO) {
return "Auto";
} else if (quality == Quality.UHD) {
return "UHD";
} else if (quality == Quality.FHD) {
return "FHD";
} else if (quality == Quality.HD) {
return "HD";
} else if (quality == Quality.SD) {
return "SD";
}
return "?";
}
@NonNull
private static String getQualityMenuItemName(@Nullable Quality quality) {
if (quality == QUALITY_AUTO) {
return "Auto";
} else if (quality == Quality.UHD) {
return "UHD (2160P)";
} else if (quality == Quality.FHD) {
return "FHD (1080P)";
} else if (quality == Quality.HD) {
return "HD (720P)";
} else if (quality == Quality.SD) {
return "SD (480P)";
}
return "Unknown quality";
}
private static int qualityToItemId(@Nullable Quality quality) {
if (quality == QUALITY_AUTO) {
return 0;
} else if (quality == Quality.UHD) {
return 1;
} else if (quality == Quality.FHD) {
return 2;
} else if (quality == Quality.HD) {
return 3;
} else if (quality == Quality.SD) {
return 4;
} else {
throw new IllegalArgumentException("Undefined quality: " + quality);
}
}
@Nullable
private static Quality itemIdToQuality(int itemId) {
switch (itemId) {
case 0:
return QUALITY_AUTO;
case 1:
return Quality.UHD;
case 2:
return Quality.FHD;
case 3:
return Quality.HD;
case 4:
return Quality.SD;
default:
throw new IllegalArgumentException("Undefined item id: " + itemId);
}
}
@NonNull
private String getDynamicRangeIconName(@NonNull DynamicRange dynamicRange) {
int resId = R.string.toggle_video_dyn_rng_unknown;
for (DynamicRangeUiData uiData : DYNAMIC_RANGE_UI_DATA) {
if (Objects.equals(dynamicRange, uiData.mDynamicRange)) {
resId = uiData.mToggleLabelRes;
break;
}
}
return getString(resId);
}
@NonNull
private static String getDynamicRangeMenuItemName(@NonNull DynamicRange dynamicRange) {
String menuItemName = dynamicRange.toString();
for (DynamicRangeUiData uiData : DYNAMIC_RANGE_UI_DATA) {
if (Objects.equals(dynamicRange, uiData.mDynamicRange)) {
menuItemName = uiData.mMenuItemName;
break;
}
}
return menuItemName;
}
private static int dynamicRangeToItemId(@NonNull DynamicRange dynamicRange) {
int itemId = -1;
for (int i = 0; i < DYNAMIC_RANGE_UI_DATA.size(); i++) {
DynamicRangeUiData uiData = DYNAMIC_RANGE_UI_DATA.get(i);
if (Objects.equals(dynamicRange, uiData.mDynamicRange)) {
itemId = i;
break;
}
}
if (itemId == -1) {
throw new IllegalArgumentException("Unsupported dynamic range: " + dynamicRange);
}
return itemId;
}
@NonNull
private static DynamicRange itemIdToDynamicRange(int itemId) {
if (itemId < 0 || itemId >= DYNAMIC_RANGE_UI_DATA.size()) {
throw new IllegalArgumentException("Undefined item id: " + itemId);
}
return DYNAMIC_RANGE_UI_DATA.get(itemId).mDynamicRange;
}
private static CameraSelector createCameraSelectorById(@Nullable String cameraId) {
return new CameraSelector.Builder().addCameraFilter(cameraInfos -> {
for (CameraInfo cameraInfo : cameraInfos) {
if (Objects.equals(cameraId, getCameraId(cameraInfo))) {
return Collections.singletonList(cameraInfo);
}
}
throw new IllegalArgumentException("No camera can be find for id: " + cameraId);
}).build();
}
private static int getLensFacing(@NonNull CameraInfo cameraInfo) {
try {
return getCamera2LensFacing(cameraInfo);
} catch (IllegalArgumentException e) {
return getCamera2PipeLensFacing(cameraInfo);
}
}
@SuppressLint("NullAnnotationGroup")
@OptIn(markerClass = ExperimentalCamera2Interop.class)
private static int getCamera2LensFacing(@NonNull CameraInfo cameraInfo) {
Integer lensFacing = Camera2CameraInfo.from(cameraInfo).getCameraCharacteristic(
CameraCharacteristics.LENS_FACING);
return lensFacing == null ? CameraCharacteristics.LENS_FACING_BACK : lensFacing;
}
@SuppressLint("NullAnnotationGroup")
@OptIn(markerClass =
androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop.class)
private static int getCamera2PipeLensFacing(@NonNull CameraInfo cameraInfo) {
Integer lensFacing =
androidx.camera.camera2.pipe.integration.interop.Camera2CameraInfo.from(
cameraInfo).getCameraCharacteristic(CameraCharacteristics.LENS_FACING);
return lensFacing == null ? CameraCharacteristics.LENS_FACING_BACK : lensFacing;
}
@NonNull
private static String getCameraId(@NonNull CameraInfo cameraInfo) {
try {
return getCamera2CameraId(cameraInfo);
} catch (IllegalArgumentException e) {
return getCameraPipeCameraId(cameraInfo);
}
}
@SuppressLint("NullAnnotationGroup")
@OptIn(markerClass = ExperimentalCamera2Interop.class)
@NonNull
private static String getCamera2CameraId(@NonNull CameraInfo cameraInfo) {
return Camera2CameraInfo.from(cameraInfo).getCameraId();
}
@SuppressLint("NullAnnotationGroup")
@OptIn(markerClass =
androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop.class)
@NonNull
private static String getCameraPipeCameraId(@NonNull CameraInfo cameraInfo) {
return androidx.camera.camera2.pipe.integration.interop.Camera2CameraInfo.from(
cameraInfo).getCameraId();
}
private static final class DynamicRangeUiData {
private DynamicRangeUiData(
@NonNull DynamicRange dynamicRange,
@NonNull String menuItemName,
int toggleLabelRes) {
mDynamicRange = dynamicRange;
mMenuItemName = menuItemName;
mToggleLabelRes = toggleLabelRes;
}
DynamicRange mDynamicRange;
String mMenuItemName;
int mToggleLabelRes;
}
@RequiresApi(26)
static class Api26Impl {
private Api26Impl() {
// This class is not instantiable.
}
@DoNotInline
static void setColorMode(@NonNull Window window, int colorMode) {
window.setColorMode(colorMode);
}
}
}