This will allow the view to layout the preview to display the correct aspect ratio. - * - * @param width width of camera source buffers. - * @param height height of camera source buffers. - */ - void onPreviewSourceDimensUpdated(int width, int height) { - mCameraView.onPreviewSourceDimensUpdated(width, height); + @Nullable + public Camera getCamera() { + return mCamera; } + @NonNull public CameraXView.CaptureMode getCaptureMode() { return mCaptureMode; } - public void setCaptureMode(CameraXView.CaptureMode captureMode) { + public void setCaptureMode(@NonNull CameraXView.CaptureMode captureMode) { this.mCaptureMode = captureMode; rebindToLifecycle(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXUtil.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXUtil.java index f1fae6bcbd..f3a03076da 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXUtil.java @@ -1,5 +1,6 @@ package org.thoughtcrime.securesms.mediasend.camerax; +import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; import android.graphics.Bitmap; @@ -19,14 +20,13 @@ import android.util.Size; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; -import androidx.camera.camera2.impl.compat.CameraManagerCompat; -import androidx.camera.core.CameraX; +import androidx.camera.camera2.internal.compat.CameraManagerCompat; +import androidx.camera.core.CameraSelector; import androidx.camera.core.ImageCapture; import androidx.camera.core.ImageProxy; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mediasend.LegacyCameraModels; -import org.thoughtcrime.securesms.migrations.LegacyMigrationJob; import org.thoughtcrime.securesms.util.Stopwatch; import java.io.ByteArrayOutputStream; @@ -57,11 +57,12 @@ public class CameraXUtil { @SuppressWarnings("SuspiciousNameCombination") @RequiresApi(21) - public static ImageResult toJpeg(@NonNull ImageProxy image, int rotation, boolean flip) throws IOException { + public static ImageResult toJpeg(@NonNull ImageProxy image, boolean flip) throws IOException { ImageProxy.PlaneProxy[] planes = image.getPlanes(); ByteBuffer buffer = planes[0].getBuffer(); Rect cropRect = shouldCropImage(image) ? image.getCropRect() : null; byte[] data = new byte[buffer.capacity()]; + int rotation = image.getImageInfo().getRotationDegrees(); buffer.get(data); @@ -86,25 +87,25 @@ public class CameraXUtil { return Build.VERSION.SDK_INT >= 21 && !LegacyCameraModels.isLegacyCameraModel(); } - public static int toCameraDirectionInt(@Nullable CameraX.LensFacing facing) { - if (facing == CameraX.LensFacing.FRONT) { + public static int toCameraDirectionInt(int facing) { + if (facing == CameraSelector.LENS_FACING_FRONT) { return Camera.CameraInfo.CAMERA_FACING_FRONT; } else { return Camera.CameraInfo.CAMERA_FACING_BACK; } } - public static @NonNull CameraX.LensFacing toLensFacing(int cameraDirectionInt) { + public static int toLensFacing(@CameraSelector.LensFacing int cameraDirectionInt) { if (cameraDirectionInt == Camera.CameraInfo.CAMERA_FACING_FRONT) { - return CameraX.LensFacing.FRONT; + return CameraSelector.LENS_FACING_FRONT; } else { - return CameraX.LensFacing.BACK; + return CameraSelector.LENS_FACING_BACK; } } - public static @NonNull ImageCapture.CaptureMode getOptimalCaptureMode() { - return FastCameraModels.contains(Build.MODEL) ? ImageCapture.CaptureMode.MAX_QUALITY - : ImageCapture.CaptureMode.MIN_LATENCY; + public static @NonNull @ImageCapture.CaptureMode int getOptimalCaptureMode() { + return FastCameraModels.contains(Build.MODEL) ? ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY + : ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY; } public static int getIdealResolution(int displayWidth, int displayHeight) { @@ -186,7 +187,7 @@ public class CameraXUtil { @RequiresApi(21) public static int getLowestSupportedHardwareLevel(@NonNull Context context) { - CameraManager cameraManager = CameraManagerCompat.from(context).unwrap(); + @SuppressLint("RestrictedApi") CameraManager cameraManager = CameraManagerCompat.from(context).unwrap(); try { int supported = maxHardwareLevel(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXView.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXView.java index 446a3f0fc7..f1c76cc949 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/CameraXView.java @@ -20,10 +20,6 @@ import android.Manifest.permission; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; -import android.graphics.Matrix; -import android.graphics.Paint; -import android.graphics.Rect; -import android.graphics.SurfaceTexture; import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManager.DisplayListener; import android.os.Bundle; @@ -33,17 +29,14 @@ import android.os.Parcelable; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; -import android.util.Size; import android.view.Display; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.Surface; -import android.view.TextureView; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; -import android.view.animation.BaseInterpolator; -import android.view.animation.DecelerateInterpolator; +import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -51,33 +44,42 @@ import androidx.annotation.RequiresApi; import androidx.annotation.RequiresPermission; import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo.Scope; -import androidx.annotation.UiThread; -import androidx.camera.core.CameraInfoUnavailableException; -import androidx.camera.core.CameraX; -import androidx.camera.core.FlashMode; +import androidx.camera.core.Camera; +import androidx.camera.core.CameraSelector; +import androidx.camera.core.DisplayOrientedMeteringPointFactory; import androidx.camera.core.FocusMeteringAction; +import androidx.camera.core.FocusMeteringResult; import androidx.camera.core.ImageCapture; +import androidx.camera.core.ImageCapture.OnImageCapturedCallback; +import androidx.camera.core.ImageProxy; import androidx.camera.core.MeteringPoint; +import androidx.camera.core.impl.LensFacingConverter; +import androidx.camera.core.impl.utils.executor.CameraXExecutors; +import androidx.camera.core.impl.utils.futures.FutureCallback; +import androidx.camera.core.impl.utils.futures.Futures; import androidx.lifecycle.LifecycleOwner; +import com.google.common.util.concurrent.ListenableFuture; + import org.thoughtcrime.securesms.R; -import java.io.File; import java.io.FileDescriptor; import java.util.concurrent.Executor; /** * A {@link View} that displays a preview of the camera with methods {@link - * #takePicture(Executor, OnImageCapturedListener)}, - * {@link #takePicture(File, Executor, OnImageSavedListener)}, - * {@link #startRecording(File, Executor, OnVideoSavedListener)} and {@link #stopRecording()}. + * #takePicture(Executor, OnImageCapturedCallback)}, + * {@link #startRecording(FileDescriptor, Executor, VideoCapture.OnVideoSavedCallback)} and {@link #stopRecording()}. * *
Because the Camera is a limited resource and consumes a high amount of power, CameraView must * be opened/closed. CameraView will handle opening/closing automatically through use of a {@link * LifecycleOwner}. Use {@link #bindToLifecycle(LifecycleOwner)} to start the camera. */ +// Begin Signal Custom Code Block @RequiresApi(21) -public final class CameraXView extends ViewGroup { +@SuppressLint("RestrictedApi") +// End Signal Custom Code Block +public final class CameraXView extends FrameLayout { static final String TAG = CameraXView.class.getSimpleName(); static final boolean DEBUG = false; @@ -85,7 +87,7 @@ public final class CameraXView extends ViewGroup { static final int INDEFINITE_VIDEO_SIZE = -1; private static final String EXTRA_SUPER = "super"; - private static final String EXTRA_ZOOM_LEVEL = "zoom_level"; + private static final String EXTRA_ZOOM_RATIO = "zoom_ratio"; private static final String EXTRA_PINCH_TO_ZOOM_ENABLED = "pinch_to_zoom_enabled"; private static final String EXTRA_FLASH = "flash"; private static final String EXTRA_MAX_VIDEO_DURATION = "max_video_duration"; @@ -121,51 +123,31 @@ public final class CameraXView extends ViewGroup { mCameraModule.invalidateView(); } }; - private TextureView mCameraTextureView; - private Size mPreviewSrcSize = new Size(0, 0); + private PreviewView mPreviewView; private ScaleType mScaleType = ScaleType.CENTER_CROP; // For accessibility event private MotionEvent mUpEvent; - private @Nullable Paint mLayerPaint; - public CameraXView(Context context) { + public CameraXView(@NonNull Context context) { this(context, null); } - public CameraXView(Context context, AttributeSet attrs) { + public CameraXView(@NonNull Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } - public CameraXView(Context context, AttributeSet attrs, int defStyle) { + public CameraXView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(context, attrs); } @RequiresApi(21) - public CameraXView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + public CameraXView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, + int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(context, attrs); } - /** Debug logging that can be enabled. */ - private static void log(String msg) { - if (DEBUG) { - Log.i(TAG, msg); - } - } - - /** Utility method for converting an displayRotation int into a human readable string. */ - private static String displayRotationToString(int displayRotation) { - if (displayRotation == Surface.ROTATION_0 || displayRotation == Surface.ROTATION_180) { - return "Portrait-" + (displayRotation * 90); - } else if (displayRotation == Surface.ROTATION_90 - || displayRotation == Surface.ROTATION_270) { - return "Landscape-" + (displayRotation * 90); - } else { - return "Unknown"; - } - } - /** * Binds control of the camera used by this view to the given lifecycle. * @@ -184,21 +166,16 @@ public final class CameraXView extends ViewGroup { * @throws IllegalStateException if camera permissions are not granted. */ @RequiresPermission(permission.CAMERA) - public void bindToLifecycle(LifecycleOwner lifecycleOwner) { + public void bindToLifecycle(@NonNull LifecycleOwner lifecycleOwner) { mCameraModule.bindToLifecycle(lifecycleOwner); } private void init(Context context, @Nullable AttributeSet attrs) { - addView(mCameraTextureView = new TextureView(getContext()), 0 /* view position */); - mCameraTextureView.setLayerPaint(mLayerPaint); + addView(mPreviewView = new PreviewView(getContext()), 0 /* view position */); mCameraModule = new CameraXModule(this); - if (isInEditMode()) { - onPreviewSourceDimensUpdated(640, 480); - } - if (attrs != null) { - TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CameraView); + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CameraXView); setScaleType( ScaleType.fromId( a.getInteger(R.styleable.CameraXView_scaleType, @@ -217,10 +194,10 @@ public final class CameraXView extends ViewGroup { setCameraLensFacing(null); break; case LENS_FACING_FRONT: - setCameraLensFacing(CameraX.LensFacing.FRONT); + setCameraLensFacing(CameraSelector.LENS_FACING_FRONT); break; case LENS_FACING_BACK: - setCameraLensFacing(CameraX.LensFacing.BACK); + setCameraLensFacing(CameraSelector.LENS_FACING_BACK); break; default: // Unhandled event. @@ -229,13 +206,13 @@ public final class CameraXView extends ViewGroup { int flashMode = a.getInt(R.styleable.CameraXView_flash, 0); switch (flashMode) { case FLASH_MODE_AUTO: - setFlash(FlashMode.AUTO); + setFlash(ImageCapture.FLASH_MODE_AUTO); break; case FLASH_MODE_ON: - setFlash(FlashMode.ON); + setFlash(ImageCapture.FLASH_MODE_ON); break; case FLASH_MODE_OFF: - setFlash(FlashMode.OFF); + setFlash(ImageCapture.FLASH_MODE_OFF); break; default: // Unhandled event. @@ -252,12 +229,14 @@ public final class CameraXView extends ViewGroup { } @Override + @NonNull protected LayoutParams generateDefaultLayoutParams() { return new LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); } @Override + @NonNull protected Parcelable onSaveInstanceState() { // TODO(b/113884082): Decide what belongs here or what should be invalidated on // configuration @@ -265,20 +244,21 @@ public final class CameraXView extends ViewGroup { Bundle state = new Bundle(); state.putParcelable(EXTRA_SUPER, super.onSaveInstanceState()); state.putInt(EXTRA_SCALE_TYPE, getScaleType().getId()); - state.putFloat(EXTRA_ZOOM_LEVEL, getZoomLevel()); + state.putFloat(EXTRA_ZOOM_RATIO, getZoomRatio()); state.putBoolean(EXTRA_PINCH_TO_ZOOM_ENABLED, isPinchToZoomEnabled()); - state.putString(EXTRA_FLASH, getFlash().name()); + state.putString(EXTRA_FLASH, FlashModeConverter.nameOf(getFlash())); state.putLong(EXTRA_MAX_VIDEO_DURATION, getMaxVideoDuration()); state.putLong(EXTRA_MAX_VIDEO_SIZE, getMaxVideoSize()); if (getCameraLensFacing() != null) { - state.putString(EXTRA_CAMERA_DIRECTION, getCameraLensFacing().name()); + state.putString(EXTRA_CAMERA_DIRECTION, + LensFacingConverter.nameOf(getCameraLensFacing())); } state.putInt(EXTRA_CAPTURE_MODE, getCaptureMode().getId()); return state; } @Override - protected void onRestoreInstanceState(Parcelable savedState) { + protected void onRestoreInstanceState(@Nullable Parcelable savedState) { // TODO(b/113884082): Decide what belongs here or what should be invalidated on // configuration // change @@ -286,39 +266,22 @@ public final class CameraXView extends ViewGroup { Bundle state = (Bundle) savedState; super.onRestoreInstanceState(state.getParcelable(EXTRA_SUPER)); setScaleType(ScaleType.fromId(state.getInt(EXTRA_SCALE_TYPE))); - setZoomLevel(state.getFloat(EXTRA_ZOOM_LEVEL)); + setZoomRatio(state.getFloat(EXTRA_ZOOM_RATIO)); setPinchToZoomEnabled(state.getBoolean(EXTRA_PINCH_TO_ZOOM_ENABLED)); - setFlash(FlashMode.valueOf(state.getString(EXTRA_FLASH))); + setFlash(FlashModeConverter.valueOf(state.getString(EXTRA_FLASH))); setMaxVideoDuration(state.getLong(EXTRA_MAX_VIDEO_DURATION)); setMaxVideoSize(state.getLong(EXTRA_MAX_VIDEO_SIZE)); String lensFacingString = state.getString(EXTRA_CAMERA_DIRECTION); setCameraLensFacing( TextUtils.isEmpty(lensFacingString) ? null - : CameraX.LensFacing.valueOf(lensFacingString)); + : LensFacingConverter.valueOf(lensFacingString)); setCaptureMode(CaptureMode.fromId(state.getInt(EXTRA_CAPTURE_MODE))); } else { super.onRestoreInstanceState(savedState); } } - /** - * Sets the paint on the preview. - * - *
This only affects the preview, and does not affect captured images/video. - * - * @param paint The paint object to apply to the preview. - * @hide This may not work once {@link android.view.SurfaceView} is supported along with {@link - * TextureView}. - */ - @Override - @RestrictTo(Scope.LIBRARY_GROUP) - public void setLayerPaint(@Nullable Paint paint) { - super.setLayerPaint(paint); - mLayerPaint = paint; - mCameraTextureView.setLayerPaint(paint); - } - @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); @@ -335,33 +298,21 @@ public final class CameraXView extends ViewGroup { dpyMgr.unregisterDisplayListener(mDisplayListener); } + PreviewView getPreviewView() { + return mPreviewView; + } + // TODO(b/124269166): Rethink how we can handle permissions here. @SuppressLint("MissingPermission") @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - int viewWidth = MeasureSpec.getSize(widthMeasureSpec); - int viewHeight = MeasureSpec.getSize(heightMeasureSpec); - - int displayRotation = getDisplay().getRotation(); - - if (mPreviewSrcSize.getHeight() == 0 || mPreviewSrcSize.getWidth() == 0) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec); - mCameraTextureView.measure(viewWidth, viewHeight); - } else { - Size scaled = - calculatePreviewViewDimens( - mPreviewSrcSize, viewWidth, viewHeight, displayRotation, mScaleType); - super.setMeasuredDimension( - Math.min(scaled.getWidth(), viewWidth), - Math.min(scaled.getHeight(), viewHeight)); - mCameraTextureView.measure(scaled.getWidth(), scaled.getHeight()); - } - // Since bindToLifecycle will depend on the measured dimension, only call it when measured // dimension is not 0x0 if (getMeasuredWidth() > 0 && getMeasuredHeight() > 0) { mCameraModule.bindToLifecycleAfterViewMeasured(); } + + super.onMeasure(widthMeasureSpec, heightMeasureSpec); } // TODO(b/124269166): Rethink how we can handle permissions here. @@ -372,114 +323,8 @@ public final class CameraXView extends ViewGroup { // binding to lifecycle mCameraModule.bindToLifecycleAfterViewMeasured(); - // If we don't know the src buffer size yet, set the preview to be the parent size - if (mPreviewSrcSize.getWidth() == 0 || mPreviewSrcSize.getHeight() == 0) { - mCameraTextureView.layout(left, top, right, bottom); - return; - } - - // Compute the preview ui size based on the available width, height, and ui orientation. - int viewWidth = (right - left); - int viewHeight = (bottom - top); - int displayRotation = getDisplay().getRotation(); - Size scaled = - calculatePreviewViewDimens( - mPreviewSrcSize, viewWidth, viewHeight, displayRotation, mScaleType); - - // Compute the center of the view. - int centerX = viewWidth / 2; - int centerY = viewHeight / 2; - - // Compute the left / top / right / bottom values such that preview is centered. - int layoutL = centerX - (scaled.getWidth() / 2); - int layoutT = centerY - (scaled.getHeight() / 2); - int layoutR = layoutL + scaled.getWidth(); - int layoutB = layoutT + scaled.getHeight(); - - // Layout debugging - log("layout: viewWidth: " + viewWidth); - log("layout: viewHeight: " + viewHeight); - log("layout: viewRatio: " + (viewWidth / (float) viewHeight)); - log("layout: sizeWidth: " + mPreviewSrcSize.getWidth()); - log("layout: sizeHeight: " + mPreviewSrcSize.getHeight()); - log( - "layout: sizeRatio: " - + (mPreviewSrcSize.getWidth() / (float) mPreviewSrcSize.getHeight())); - log("layout: scaledWidth: " + scaled.getWidth()); - log("layout: scaledHeight: " + scaled.getHeight()); - log("layout: scaledRatio: " + (scaled.getWidth() / (float) scaled.getHeight())); - log( - "layout: size: " - + scaled - + " (" - + (scaled.getWidth() / (float) scaled.getHeight()) - + " - " - + mScaleType - + "-" - + displayRotationToString(displayRotation) - + ")"); - log("layout: final " + layoutL + ", " + layoutT + ", " + layoutR + ", " + layoutB); - - mCameraTextureView.layout(layoutL, layoutT, layoutR, layoutB); - mCameraModule.invalidateView(); - } - - /** Records the size of the preview's buffers. */ - @UiThread - void onPreviewSourceDimensUpdated(int srcWidth, int srcHeight) { - if (srcWidth != mPreviewSrcSize.getWidth() - || srcHeight != mPreviewSrcSize.getHeight()) { - mPreviewSrcSize = new Size(srcWidth, srcHeight); - requestLayout(); - } - } - - private Size calculatePreviewViewDimens( - Size srcSize, - int parentWidth, - int parentHeight, - int displayRotation, - ScaleType scaleType) { - int inWidth = srcSize.getWidth(); - int inHeight = srcSize.getHeight(); - if (displayRotation == Surface.ROTATION_90 || displayRotation == Surface.ROTATION_270) { - // Need to reverse the width and height since we're in landscape orientation. - inWidth = srcSize.getHeight(); - inHeight = srcSize.getWidth(); - } - - int outWidth = parentWidth; - int outHeight = parentHeight; - if (inWidth != 0 && inHeight != 0) { - float vfRatio = inWidth / (float) inHeight; - float parentRatio = parentWidth / (float) parentHeight; - - switch (scaleType) { - case CENTER_INSIDE: - // Match longest sides together. - if (vfRatio > parentRatio) { - outWidth = parentWidth; - outHeight = Math.round(parentWidth / vfRatio); - } else { - outWidth = Math.round(parentHeight * vfRatio); - outHeight = parentHeight; - } - break; - case CENTER_CROP: - // Match shortest sides together. - if (vfRatio < parentRatio) { - outWidth = parentWidth; - outHeight = Math.round(parentWidth / vfRatio); - } else { - outWidth = Math.round(parentHeight * vfRatio); - outHeight = parentHeight; - } - break; - } - } - - return new Size(outWidth, outHeight); + super.onLayout(changed, left, top, right, bottom); } /** @@ -499,58 +344,12 @@ public final class CameraXView extends ViewGroup { return display.getRotation(); } - @UiThread - SurfaceTexture getSurfaceTexture() { - if (mCameraTextureView != null) { - return mCameraTextureView.getSurfaceTexture(); - } - - return null; - } - - @UiThread - void setSurfaceTexture(SurfaceTexture surfaceTexture) { - if (mCameraTextureView.getSurfaceTexture() != surfaceTexture) { - if (mCameraTextureView.isAvailable()) { - // Remove the old TextureView to properly detach the old SurfaceTexture from the GL - // Context. - removeView(mCameraTextureView); - addView(mCameraTextureView = new TextureView(getContext()), 0); - mCameraTextureView.setLayerPaint(mLayerPaint); - requestLayout(); - } - - mCameraTextureView.setSurfaceTexture(surfaceTexture); - } - } - - @UiThread - Matrix getTransform(Matrix matrix) { - return mCameraTextureView.getTransform(matrix); - } - - @UiThread - int getPreviewWidth() { - return mCameraTextureView.getWidth(); - } - - @UiThread - int getPreviewHeight() { - return mCameraTextureView.getHeight(); - } - - @UiThread - void setTransform(final Matrix matrix) { - if (mCameraTextureView != null) { - mCameraTextureView.setTransform(matrix); - } - } - /** * Returns the scale type used to scale the preview. * * @return The current {@link ScaleType}. */ + @NonNull public ScaleType getScaleType() { return mScaleType; } @@ -562,7 +361,7 @@ public final class CameraXView extends ViewGroup { * * @param scaleType The desired {@link ScaleType}. */ - public void setScaleType(ScaleType scaleType) { + public void setScaleType(@NonNull ScaleType scaleType) { if (scaleType != mScaleType) { mScaleType = scaleType; requestLayout(); @@ -574,6 +373,7 @@ public final class CameraXView extends ViewGroup { * * @return The current {@link CaptureMode}. */ + @NonNull public CaptureMode getCaptureMode() { return mCameraModule.getCaptureMode(); } @@ -585,7 +385,7 @@ public final class CameraXView extends ViewGroup { * * @param captureMode The desired {@link CaptureMode}. */ - public void setCaptureMode(CaptureMode captureMode) { + public void setCaptureMode(@NonNull CaptureMode captureMode) { mCameraModule.setCaptureMode(captureMode); } @@ -601,7 +401,7 @@ public final class CameraXView extends ViewGroup { } /** - * Sets the maximum video duration before {@link OnVideoSavedListener#onVideoSaved(File)} is + * Sets the maximum video duration before {@link VideoCapture.OnVideoSavedCallback#onVideoSaved(FileDescriptor)} is * called automatically. Use {@link #INDEFINITE_VIDEO_DURATION} to disable the timeout. */ private void setMaxVideoDuration(long duration) { @@ -617,7 +417,7 @@ public final class CameraXView extends ViewGroup { } /** - * Sets the maximum video size in bytes before {@link OnVideoSavedListener#onVideoSaved(File)} + * Sets the maximum video size in bytes before {@link VideoCapture.OnVideoSavedCallback#onVideoSaved(FileDescriptor)} * is called automatically. Use {@link #INDEFINITE_VIDEO_SIZE} to disable the size restriction. */ private void setMaxVideoSize(long size) { @@ -625,44 +425,32 @@ public final class CameraXView extends ViewGroup { } /** - * Takes a picture, and calls {@link OnImageCapturedListener#onCaptureSuccess(ImageProxy, int)} + * Takes a picture, and calls {@link OnImageCapturedCallback#onCaptureSuccess(ImageProxy)} * once when done. * - * @param executor The executor in which the listener callback methods will be run. - * @param listener Listener which will receive success or failure callbacks. + * @param executor The executor in which the callback methods will be run. + * @param callback Callback which will receive success or failure callbacks. */ - @SuppressLint("LambdaLast") // Maybe remove after https://issuetracker.google.com/135275901 - public void takePicture(@NonNull Executor executor, @NonNull ImageCapture.OnImageCapturedListener listener) { - mCameraModule.takePicture(executor, listener); + public void takePicture(@NonNull Executor executor, @NonNull OnImageCapturedCallback callback) { + mCameraModule.takePicture(executor, callback); } /** - * Takes a picture and calls {@link OnImageSavedListener#onImageSaved(File)} when done. + * Takes a video and calls the OnVideoSavedCallback when done. * * @param file The destination. - * @param executor The executor in which the listener callback methods will be run. - * @param listener Listener which will receive success or failure callbacks. - */ - @SuppressLint("LambdaLast") // Maybe remove after https://issuetracker.google.com/135275901 - public void takePicture(@NonNull File file, @NonNull Executor executor, - @NonNull ImageCapture.OnImageSavedListener listener) { - mCameraModule.takePicture(file, executor, listener); - } - - /** - * Takes a video and calls the OnVideoSavedListener when done. - * - * @param file The destination. - * @param executor The executor in which the listener callback methods will be run. - * @param listener Listener which will receive success or failure callbacks. + * @param executor The executor in which the callback methods will be run. + * @param callback Callback which will receive success or failure. */ // Begin Signal Custom Code Block @RequiresApi(26) - @SuppressLint("LambdaLast") // Maybe remove after https://issuetracker.google.com/135275901 - public void startRecording(@NonNull FileDescriptor file, @NonNull Executor executor, // End Signal Custom Code Block - @NonNull VideoCapture.OnVideoSavedListener listener) { - mCameraModule.startRecording(file, executor, listener); + public void startRecording(// Begin Signal Custom Code Block + @NonNull FileDescriptor file, + // End Signal Custom Code Block + @NonNull Executor executor, + @NonNull VideoCapture.OnVideoSavedCallback callback) { + mCameraModule.startRecording(file, executor, callback); } /** Stops an in progress video. */ @@ -685,7 +473,7 @@ public final class CameraXView extends ViewGroup { * @throws IllegalStateException if the CAMERA permission is not currently granted. */ @RequiresPermission(permission.CAMERA) - public boolean hasCameraWithLensFacing(CameraX.LensFacing lensFacing) { + public boolean hasCameraWithLensFacing(@CameraSelector.LensFacing int lensFacing) { return mCameraModule.hasCameraWithLensFacing(lensFacing); } @@ -706,7 +494,7 @@ public final class CameraXView extends ViewGroup { * *
If called before {@link #bindToLifecycle(LifecycleOwner)}, this will set the camera to be * used when first bound to the lifecycle. If the specified lensFacing is not supported by the - * device, as determined by {@link #hasCameraWithLensFacing(LensFacing)}, the first supported + * device, as determined by {@link #hasCameraWithLensFacing(int)}, the first supported * lensFacing will be chosen when {@link #bindToLifecycle(LifecycleOwner)} is called. * *
If called with {@code null} AFTER binding to the lifecycle, the behavior would be
@@ -714,36 +502,33 @@ public final class CameraXView extends ViewGroup {
*
* @param lensFacing The desired camera lensFacing.
*/
- public void setCameraLensFacing(@Nullable CameraX.LensFacing lensFacing) {
+ public void setCameraLensFacing(@Nullable Integer lensFacing) {
mCameraModule.setCameraLensFacing(lensFacing);
}
- /** Returns the currently selected {@link LensFacing}. */
+ /** Returns the currently selected lensFacing. */
@Nullable
- public CameraX.LensFacing getCameraLensFacing() {
+ public Integer getCameraLensFacing() {
return mCameraModule.getLensFacing();
}
+ /** Gets the active flash strategy. */
+ @ImageCapture.FlashMode
+ public int getFlash() {
+ return mCameraModule.getFlash();
+ }
+
// Begin Signal Custom Code Block
public boolean hasFlash() {
return mCameraModule.hasFlash();
}
// End Signal Custom Code Block
- /** Gets the active flash strategy. */
- public FlashMode getFlash() {
- return mCameraModule.getFlash();
- }
-
/** Sets the active flash strategy. */
- public void setFlash(@NonNull FlashMode flashMode) {
+ public void setFlash(@ImageCapture.FlashMode int flashMode) {
mCameraModule.setFlash(flashMode);
}
- private int getRelativeCameraOrientation(boolean compensateForMirroring) {
- return mCameraModule.getRelativeCameraOrientation(compensateForMirroring);
- }
-
private long delta() {
return System.currentTimeMillis() - mDownEventTimestamp;
}
@@ -793,42 +578,47 @@ public final class CameraXView extends ViewGroup {
final float y = (mUpEvent != null) ? mUpEvent.getY() : getY() + getHeight() / 2f;
mUpEvent = null;
- TextureViewMeteringPointFactory pointFactory = new TextureViewMeteringPointFactory(
- mCameraTextureView);
+ CameraSelector cameraSelector =
+ new CameraSelector.Builder().requireLensFacing(
+ mCameraModule.getLensFacing()).build();
+
+ DisplayOrientedMeteringPointFactory pointFactory = new DisplayOrientedMeteringPointFactory(
+ getDisplay(), cameraSelector, mPreviewView.getWidth(), mPreviewView.getHeight());
float afPointWidth = 1.0f / 6.0f; // 1/6 total area
float aePointWidth = afPointWidth * 1.5f;
- MeteringPoint afPoint = pointFactory.createPoint(x, y, afPointWidth, 1.0f);
- MeteringPoint aePoint = pointFactory.createPoint(x, y, aePointWidth, 1.0f);
+ MeteringPoint afPoint = pointFactory.createPoint(x, y, afPointWidth);
+ MeteringPoint aePoint = pointFactory.createPoint(x, y, aePointWidth);
- try {
- CameraX.getCameraControl(getCameraLensFacing()).startFocusAndMetering(
- FocusMeteringAction.Builder.from(afPoint, FocusMeteringAction.MeteringMode.AF_ONLY)
- .addPoint(aePoint, FocusMeteringAction.MeteringMode.AE_ONLY)
- .build());
- } catch (CameraInfoUnavailableException e) {
- Log.d(TAG, "cannot access camera", e);
+ Camera camera = mCameraModule.getCamera();
+ if (camera != null) {
+ ListenableFuture Valid zoom values range from 1 to {@link #getMaxZoomLevel()}.
+ * Valid zoom values range from {@link #getMinZoomRatio()} to {@link #getMaxZoomRatio()}.
*
- * @param zoomLevel The requested zoom level.
+ * @param zoomRatio The requested zoom ratio.
*/
- public void setZoomLevel(float zoomLevel) {
- mCameraModule.setZoomLevel(zoomLevel);
+ public void setZoomRatio(float zoomRatio) {
+ mCameraModule.setZoomRatio(zoomRatio);
}
/**
- * Returns the minimum zoom level.
+ * Returns the minimum zoom ratio.
*
- * For most cameras this should return a zoom level of 1. A zoom level of 1 corresponds to a
+ * For most cameras this should return a zoom ratio of 1. A zoom ratio of 1 corresponds to a
* non-zoomed image.
*
- * @return The minimum zoom level.
+ * @return The minimum zoom ratio.
*/
- public float getMinZoomLevel() {
- return mCameraModule.getMinZoomLevel();
+ public float getMinZoomRatio() {
+ return mCameraModule.getMinZoomRatio();
}
/**
- * Returns the maximum zoom level.
+ * Returns the maximum zoom ratio.
*
- * The zoom level corresponds to the ratio between both the widths and heights of a
+ * The zoom ratio corresponds to the ratio between both the widths and heights of a
* non-zoomed image and a maximally zoomed image for the selected camera.
*
- * @return The maximum zoom level.
+ * @return The maximum zoom ratio.
*/
- public float getMaxZoomLevel() {
- return mCameraModule.getMaxZoomLevel();
+ public float getMaxZoomRatio() {
+ return mCameraModule.getMaxZoomRatio();
}
/**
@@ -935,7 +725,7 @@ public final class CameraXView extends ViewGroup {
*/
CENTER_INSIDE(1);
- private int mId;
+ private final int mId;
int getId() {
return mId;
@@ -959,7 +749,7 @@ public final class CameraXView extends ViewGroup {
* The capture mode used by CameraView.
*
* This enum can be used to determine which capture mode will be enabled for {@link
- * CameraView}.
+ * CameraXView}.
*/
public enum CaptureMode {
/** A mode where image capture is enabled. */
@@ -972,7 +762,7 @@ public final class CameraXView extends ViewGroup {
*/
MIXED(2);
- private int mId;
+ private final int mId;
int getId() {
return mId;
@@ -1007,10 +797,6 @@ public final class CameraXView extends ViewGroup {
private class PinchToZoomGestureDetector extends ScaleGestureDetector
implements ScaleGestureDetector.OnScaleGestureListener {
- private static final float SCALE_MULTIPIER = 0.75f;
- private final BaseInterpolator mInterpolator = new DecelerateInterpolator(2f);
- private float mNormalizedScaleFactor = 0;
-
PinchToZoomGestureDetector(Context context) {
this(context, new S());
}
@@ -1022,34 +808,23 @@ public final class CameraXView extends ViewGroup {
@Override
public boolean onScale(ScaleGestureDetector detector) {
- mNormalizedScaleFactor += (detector.getScaleFactor() - 1f) * SCALE_MULTIPIER;
- // Since the scale factor is normalized, it should always be in the range [0, 1]
- mNormalizedScaleFactor = rangeLimit(mNormalizedScaleFactor, 1f, 0);
+ float scale = detector.getScaleFactor();
- // Apply decelerate interpolation. This will cause the differences to seem less
- // pronounced
- // at higher zoom levels.
- float transformedScale = mInterpolator.getInterpolation(mNormalizedScaleFactor);
+ // Speeding up the zoom by 2X.
+ if (scale > 1f) {
+ scale = 1.0f + (scale - 1.0f) * 2;
+ } else {
+ scale = 1.0f - (1.0f - scale) * 2;
+ }
- // Transform back from normalized coordinates to the zoom scale
- float zoomLevel =
- (getMaxZoomLevel() == getMinZoomLevel())
- ? getMinZoomLevel()
- : getMinZoomLevel()
- + transformedScale * (getMaxZoomLevel() - getMinZoomLevel());
-
- setZoomLevel(rangeLimit(zoomLevel, getMaxZoomLevel(), getMinZoomLevel()));
+ float newRatio = getZoomRatio() * scale;
+ newRatio = rangeLimit(newRatio, getMaxZoomRatio(), getMinZoomRatio());
+ setZoomRatio(newRatio);
return true;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
- float initialZoomLevel = getZoomLevel();
- mNormalizedScaleFactor =
- (getMaxZoomLevel() == getMinZoomLevel())
- ? 0
- : (initialZoomLevel - getMinZoomLevel())
- / (getMaxZoomLevel() - getMinZoomLevel());
return true;
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/FlashModeConverter.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/FlashModeConverter.java
new file mode 100644
index 0000000000..39d9f79297
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/FlashModeConverter.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 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 org.thoughtcrime.securesms.mediasend.camerax;
+
+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 androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.camera.core.ImageCapture.FlashMode;
+
+/**
+ * Helper class that defines certain enum-like methods for {@link FlashMode}
+ */
+final class FlashModeConverter {
+
+ private FlashModeConverter() {
+ }
+
+ /**
+ * Returns the {@link FlashMode} constant for the specified name
+ *
+ * @param name The name of the {@link FlashMode} to return
+ * @return The {@link FlashMode} constant for the specified name
+ */
+ @FlashMode
+ public static int valueOf(@Nullable final String name) {
+ if (name == null) {
+ throw new NullPointerException("name cannot be null");
+ }
+
+ switch (name) {
+ case "AUTO":
+ return FLASH_MODE_AUTO;
+ case "ON":
+ return FLASH_MODE_ON;
+ case "OFF":
+ return FLASH_MODE_OFF;
+ default:
+ throw new IllegalArgumentException("Unknown flash mode name " + name);
+ }
+ }
+
+ /**
+ * Returns the name of the {@link FlashMode} constant, exactly as it is declared.
+ *
+ * @param flashMode A {@link FlashMode} constant
+ * @return The name of the {@link FlashMode} constant.
+ */
+ @NonNull
+ public static String nameOf(@FlashMode final int flashMode) {
+ switch (flashMode) {
+ case FLASH_MODE_AUTO:
+ return "AUTO";
+ case FLASH_MODE_ON:
+ return "ON";
+ case FLASH_MODE_OFF:
+ return "OFF";
+ default:
+ throw new IllegalArgumentException("Unknown flash mode " + flashMode);
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/PreviewView.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/PreviewView.java
new file mode 100644
index 0000000000..520c75e0d3
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/PreviewView.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright 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 org.thoughtcrime.securesms.mediasend.camerax;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.hardware.display.DisplayManager;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.Preview;
+
+import org.thoughtcrime.securesms.R;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Custom View that displays camera feed for CameraX's Preview use case.
+ *
+ * This class manages the Surface lifecycle, as well as the preview aspect ratio and
+ * orientation. Internally, it uses either a {@link android.view.TextureView} or
+ * {@link android.view.SurfaceView} to display the camera feed.
+ */
+// Begin Signal Custom Code Block
+@RequiresApi(21)
+// End Signal Custom Code Block
+public class PreviewView extends FrameLayout {
+
+ @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ Implementation mImplementation;
+
+ private ImplementationMode mImplementationMode;
+
+ private final DisplayManager.DisplayListener mDisplayListener =
+ new DisplayManager.DisplayListener() {
+ @Override
+ public void onDisplayAdded(int displayId) {
+ }
+
+ @Override
+ public void onDisplayRemoved(int displayId) {
+ }
+ @Override
+ public void onDisplayChanged(int displayId) {
+ mImplementation.onDisplayChanged();
+ }
+ };
+
+ public PreviewView(@NonNull Context context) {
+ this(context, null);
+ }
+
+ public PreviewView(@NonNull Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public PreviewView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public PreviewView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ final TypedArray attributes = context.getTheme().obtainStyledAttributes(attrs,
+ R.styleable.PreviewView, defStyleAttr, defStyleRes);
+
+ try {
+ final int implementationModeId = attributes.getInteger(
+ R.styleable.PreviewView_implementationMode,
+ ImplementationMode.TEXTURE_VIEW.getId());
+ mImplementationMode = ImplementationMode.fromId(implementationModeId);
+ } finally {
+ attributes.recycle();
+ }
+ setUp();
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ final DisplayManager displayManager =
+ (DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE);
+ if (displayManager != null) {
+ displayManager.registerDisplayListener(mDisplayListener, getHandler());
+ }
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ final DisplayManager displayManager =
+ (DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE);
+ if (displayManager != null) {
+ displayManager.unregisterDisplayListener(mDisplayListener);
+ }
+ }
+
+ private void setUp() {
+ removeAllViews();
+ switch (mImplementationMode) {
+ case SURFACE_VIEW:
+ mImplementation = new SurfaceViewImplementation();
+ break;
+ case TEXTURE_VIEW:
+ mImplementation = new TextureViewImplementation();
+ break;
+ default:
+ throw new IllegalStateException(
+ "Unsupported implementation mode " + mImplementationMode);
+ }
+ mImplementation.init(this);
+ }
+
+ /**
+ * Specifies the {@link ImplementationMode} to use for the preview.
+ *
+ * @param implementationMode Implementation might need to adjust transform by latest display properties such as
+ * display orientation in order to show the preview correctly.
+ */
+ void onDisplayChanged();
+ }
+
+ /**
+ * The implementation mode of a {@link PreviewView}
+ *
+ * Specifies how the Preview surface will be implemented internally: Using a
+ * {@link android.view.SurfaceView} or a {@link android.view.TextureView} (which is the default)
+ *
+ * Using the current app's window to determine whether the device is a natural
+ * portrait-oriented device doesn't work in all scenarios, one example of this is multi-window
+ * mode.
+ * Taking a natural portrait-oriented device in multi-window mode, rotating it 90 degrees (so
+ * that it's in landscape), with the app open, and its window's width being smaller than its
+ * height. Using the app's width and height would determine that the device isn't
+ * naturally portrait-oriented, where in fact it is, which is why it is important to use the
+ * size of the device instead.
+ * Valid values for the relative rotation are {@link Surface#ROTATION_0}, {@link
+ * * Surface#ROTATION_90}, {@link Surface#ROTATION_180}, {@link Surface#ROTATION_270}.
+ */
+ static int rotationDegreesFromSurfaceRotation(int rotationConstant) {
+ switch (rotationConstant) {
+ case Surface.ROTATION_0:
+ return 0;
+ case Surface.ROTATION_90:
+ return 90;
+ case Surface.ROTATION_180:
+ return 180;
+ case Surface.ROTATION_270:
+ return 270;
+ default:
+ throw new UnsupportedOperationException(
+ "Unsupported surface rotation constant: " + rotationConstant);
+ }
+ }
+
+ /** Prevents construction */
+ private SurfaceRotation() {}
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/SurfaceViewImplementation.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/SurfaceViewImplementation.java
new file mode 100644
index 0000000000..7c6963a5cf
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/SurfaceViewImplementation.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright 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 org.thoughtcrime.securesms.mediasend.camerax;
+
+import android.util.Log;
+import android.util.Size;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.UiThread;
+import androidx.camera.core.Preview;
+import androidx.camera.core.SurfaceRequest;
+import androidx.core.content.ContextCompat;
+
+/**
+ * The SurfaceView implementation for {@link PreviewView}.
+ */
+@RequiresApi(21)
+final class SurfaceViewImplementation implements PreviewView.Implementation {
+
+ private static final String TAG = "SurfaceViewPreviewView";
+
+ // Synthetic Accessor
+ @SuppressWarnings("WeakerAccess")
+ TransformableSurfaceView mSurfaceView;
+
+ // Synthetic Accessor
+ @SuppressWarnings("WeakerAccess")
+ final SurfaceRequestCallback mSurfaceRequestCallback =
+ new SurfaceRequestCallback();
+
+ private Preview.SurfaceProvider mSurfaceProvider =
+ new Preview.SurfaceProvider() {
+ @Override
+ public void onSurfaceRequested(@NonNull SurfaceRequest surfaceRequest) {
+ mSurfaceView.post(
+ () -> mSurfaceRequestCallback.setSurfaceRequest(surfaceRequest));
+ }
+ };
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void init(@NonNull FrameLayout parent) {
+ mSurfaceView = new TransformableSurfaceView(parent.getContext());
+ mSurfaceView.setLayoutParams(
+ new FrameLayout.LayoutParams(
+ FrameLayout.LayoutParams.MATCH_PARENT,
+ FrameLayout.LayoutParams.MATCH_PARENT));
+ parent.addView(mSurfaceView);
+ mSurfaceView.getHolder().addCallback(mSurfaceRequestCallback);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @NonNull
+ @Override
+ public Preview.SurfaceProvider getSurfaceProvider() {
+ return mSurfaceProvider;
+ }
+
+ @Override
+ public void onDisplayChanged() {
+
+ }
+
+ /**
+ * The {@link SurfaceHolder.Callback} on mSurfaceView.
+ *
+ * SurfaceView creates Surface on its own before we can do anything. This class makes
+ * sure only the Surface with correct size will be returned to Preview.
+ */
+ class SurfaceRequestCallback implements SurfaceHolder.Callback {
+
+ // Target Surface size. Only complete the SurfaceRequest when the size of the Surface
+ // matches this value.
+ // Guarded by UI thread.
+ @Nullable
+ private Size mTargetSize;
+
+ // SurfaceRequest to set when the target size is met.
+ // Guarded by UI thread.
+ @Nullable
+ private SurfaceRequest mSurfaceRequest;
+
+ // The cached size of the current Surface.
+ // Guarded by UI thread.
+ @Nullable
+ private Size mCurrentSurfaceSize;
+
+ /**
+ * Sets the completer and the size. The completer will only be set if the current size of
+ * the Surface matches the target size.
+ */
+ @UiThread
+ void setSurfaceRequest(@NonNull SurfaceRequest surfaceRequest) {
+ cancelPreviousRequest();
+ mSurfaceRequest = surfaceRequest;
+ Size targetSize = surfaceRequest.getResolution();
+ mTargetSize = targetSize;
+ if (!tryToComplete()) {
+ // The current size is incorrect. Wait for it to change.
+ Log.d(TAG, "Wait for new Surface creation.");
+ mSurfaceView.getHolder().setFixedSize(targetSize.getWidth(),
+ targetSize.getHeight());
+ }
+ }
+
+ /**
+ * Sets the completer if size matches.
+ *
+ * @return true if the completer is set.
+ */
+ @UiThread
+ private boolean tryToComplete() {
+ Surface surface = mSurfaceView.getHolder().getSurface();
+ if (mSurfaceRequest != null && mTargetSize != null && mTargetSize.equals(
+ mCurrentSurfaceSize)) {
+ Log.d(TAG, "Surface set on Preview.");
+ mSurfaceRequest.provideSurface(surface,
+ ContextCompat.getMainExecutor(mSurfaceView.getContext()),
+ (result) -> Log.d(TAG, "Safe to release surface."));
+ mSurfaceRequest = null;
+ mTargetSize = null;
+ return true;
+ }
+ return false;
+ }
+
+ @UiThread
+ private void cancelPreviousRequest() {
+ if (mSurfaceRequest != null) {
+ Log.d(TAG, "Request canceled: " + mSurfaceRequest);
+ mSurfaceRequest.willNotProvideSurface();
+ mSurfaceRequest = null;
+ }
+ mTargetSize = null;
+ }
+
+ @Override
+ public void surfaceCreated(SurfaceHolder surfaceHolder) {
+ Log.d(TAG, "Surface created.");
+ // No-op. Handling surfaceChanged() is enough because it's always called afterwards.
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder surfaceHolder, int format, int width, int height) {
+ Log.d(TAG, "Surface changed. Size: " + width + "x" + height);
+ mCurrentSurfaceSize = new Size(width, height);
+ tryToComplete();
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder surfaceHolder) {
+ Log.d(TAG, "Surface destroyed.");
+ mCurrentSurfaceSize = null;
+ cancelPreviousRequest();
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/TextureViewImplementation.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/TextureViewImplementation.java
new file mode 100644
index 0000000000..7e9213d5f9
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/camerax/TextureViewImplementation.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright 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 org.thoughtcrime.securesms.mediasend.camerax;
+
+import static androidx.camera.core.SurfaceRequest.Result;
+
+import android.annotation.SuppressLint;
+import android.graphics.Point;
+import android.graphics.SurfaceTexture;
+import android.util.Log;
+import android.util.Pair;
+import android.util.Size;
+import android.view.Surface;
+import android.view.TextureView;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.Preview;
+import androidx.camera.core.SurfaceRequest;
+import androidx.camera.core.impl.utils.executor.CameraXExecutors;
+import androidx.camera.core.impl.utils.futures.FutureCallback;
+import androidx.camera.core.impl.utils.futures.Futures;
+import androidx.concurrent.futures.CallbackToFutureAdapter;
+import androidx.core.content.ContextCompat;
+import androidx.core.util.Preconditions;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+/**
+ * The {@link TextureView} implementation for {@link PreviewView}
+ */
+// Begin Signal Custom Code Block
+@RequiresApi(21)
+@SuppressLint("RestrictedApi")
+// End Signal Custom Code Block
+public class TextureViewImplementation implements PreviewView.Implementation {
+
+ private static final String TAG = "TextureViewImpl";
+
+ private FrameLayout mParent;
+ TextureView mTextureView;
+ SurfaceTexture mSurfaceTexture;
+ private Size mResolution;
+ ListenableFuture
+ * The camera produces a preview that depends on its sensor orientation and that has a
+ * specific resolution. In order to display it correctly, this preview must be rotated to
+ * match the UI orientation, and must be scaled up/down to fit inside the view that's
+ * displaying it. This method takes care of doing so while keeping the preview centered.
+ * See message parameter in onError callback or log for more details.
+ */
+ public static final int ERROR_UNKNOWN = 0;
+ /**
+ * An error occurred with encoder state, either when trying to change state or when an
+ * unexpected state change occurred.
+ */
+ public static final int ERROR_ENCODER = 1;
+ /** An error with muxer state such as during creation or when stopping. */
+ public static final int ERROR_MUXER = 2;
+ /**
+ * An error indicating start recording was called when video recording is still in progress.
+ */
+ public static final int ERROR_RECORDING_IN_PROGRESS = 3;
+
/**
* Provides a static configuration with implementation-agnostic options.
*
@@ -131,7 +170,6 @@ public class VideoCapture extends UseCase {
/** For record the first sample written time. */
private final AtomicBoolean mIsFirstVideoSampleWrite = new AtomicBoolean(false);
private final AtomicBoolean mIsFirstAudioSampleWrite = new AtomicBoolean(false);
- private final VideoCaptureConfig.Builder mUseCaseConfigBuilder;
@NonNull
MediaCodec mVideoEncoder;
@@ -147,7 +185,9 @@ public class VideoCapture extends UseCase {
private int mAudioTrackIndex;
/** Surface the camera writes to, which the videoEncoder uses as input. */
Surface mCameraSurface;
+
/** audio raw data */
+ @NonNull
private AudioRecord mAudioRecorder;
private int mAudioBufferSize;
private boolean mIsRecording = false;
@@ -163,7 +203,6 @@ public class VideoCapture extends UseCase {
*/
public VideoCapture(VideoCaptureConfig config) {
super(config);
- mUseCaseConfigBuilder = VideoCaptureConfig.Builder.fromConfig(config);
// video thread start
mVideoHandlerThread.start();
@@ -182,9 +221,6 @@ public class VideoCapture extends UseCase {
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_BIT_RATE, config.getBitRate());
format.setInteger(MediaFormat.KEY_FRAME_RATE, config.getVideoFrameRate());
- // Begin Signal Custom Code Block
- format.setInteger(MediaFormat.KEY_CAPTURE_RATE, config.getVideoFrameRate());
- // End Signal Custom Code Block
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, config.getIFrameInterval());
return format;
@@ -199,9 +235,9 @@ public class VideoCapture extends UseCase {
@Override
@Nullable
@RestrictTo(Scope.LIBRARY_GROUP)
- protected UseCaseConfig.Builder, ?, ?> getDefaultBuilder(LensFacing lensFacing) {
- VideoCaptureConfig defaults = CameraX.getDefaultUseCaseConfig(
- VideoCaptureConfig.class, lensFacing);
+ protected UseCaseConfig.Builder, ?, ?> getDefaultBuilder(@Nullable CameraInfo cameraInfo) {
+ VideoCaptureConfig defaults = CameraX.getDefaultUseCaseConfig(VideoCaptureConfig.class,
+ cameraInfo);
if (defaults != null) {
return VideoCaptureConfig.Builder.fromConfig(defaults);
}
@@ -216,9 +252,9 @@ public class VideoCapture extends UseCase {
*/
@Override
@RestrictTo(Scope.LIBRARY_GROUP)
+ @NonNull
protected Map StartRecording() is asynchronous. User needs to check if any error occurs by setting the
- * {@link OnVideoSavedListener#onError(VideoCaptureError, String, Throwable)}.
+ * {@link OnVideoSavedCallback#onError(int, String, Throwable)}.
*
* @param saveLocation Location to save the video capture
- * @param executor The executor in which the listener callback methods will be run.
- * @param listener Listener to call for the recorded video
+ * @param executor The executor in which the callback methods will be run.
+ * @param callback Callback for when the recorded video saving completion or failure.
*/
- @SuppressLint("LambdaLast") // Maybe remove after https://issuetracker.google.com/135275901
// Begin Signal Custom Code Block
public void startRecording(@NonNull FileDescriptor saveLocation,
- @NonNull Executor executor, @NonNull OnVideoSavedListener listener) {
// End Signal Custom Code Block
+ @NonNull Executor executor, @NonNull OnVideoSavedCallback callback) {
mIsFirstVideoSampleWrite.set(false);
mIsFirstAudioSampleWrite.set(false);
- startRecording(saveLocation, EMPTY_METADATA, executor, listener);
+ startRecording(saveLocation, EMPTY_METADATA, executor, callback);
}
/**
@@ -271,26 +306,26 @@ public class VideoCapture extends UseCase {
* called.
*
* StartRecording() is asynchronous. User needs to check if any error occurs by setting the
- * {@link OnVideoSavedListener#onError(VideoCaptureError, String, Throwable)}.
+ * {@link OnVideoSavedCallback#onError(int, String, Throwable)}.
*
* @param saveLocation Location to save the video capture
* @param metadata Metadata to save with the recorded video
- * @param executor The executor in which the listener callback methods will be run.
- * @param listener Listener to call for the recorded video
+ * @param executor The executor in which the callback methods will be run.
+ * @param callback Callback for when the recorded video saving completion or failure.
*/
- @SuppressLint("LambdaLast") // Maybe remove after https://issuetracker.google.com/135275901
- // Begin Signal Custom Code Block
public void startRecording(
- @NonNull FileDescriptor saveLocation, @NonNull Metadata metadata,
+ // Begin Signal Custom Code Block
+ @NonNull FileDescriptor saveLocation,
+ // End Signal Custom Code Block
+ @NonNull Metadata metadata,
@NonNull Executor executor,
- @NonNull OnVideoSavedListener listener) {
- // End Signal Custom Code Block
+ @NonNull OnVideoSavedCallback callback) {
Log.i(TAG, "startRecording");
- OnVideoSavedListener postListener = new VideoSavedListenerWrapper(executor, listener);
+ OnVideoSavedCallback postListener = new VideoSavedListenerWrapper(executor, callback);
if (!mEndOfAudioVideoSignal.get()) {
postListener.onError(
- VideoCaptureError.RECORDING_IN_PROGRESS, "It is still in video recording!",
+ ERROR_RECORDING_IN_PROGRESS, "It is still in video recording!",
null);
return;
}
@@ -305,12 +340,13 @@ public class VideoCapture extends UseCase {
}
// End Signal Custom Code Block
} catch (IllegalStateException e) {
- postListener.onError(VideoCaptureError.ENCODER_ERROR, "AudioRecorder start fail", e);
+ postListener.onError(ERROR_ENCODER, "AudioRecorder start fail", e);
return;
}
- VideoCaptureConfig config = (VideoCaptureConfig) getUseCaseConfig();
- String cameraId = getCameraIdUnchecked(config);
+ CameraInternal boundCamera = getBoundCamera();
+ String cameraId = getBoundCameraId();
+ Size resolution = getAttachedSurfaceResolution(cameraId);
try {
// video encoder start
Log.i(TAG, "videoEncoder start");
@@ -320,23 +356,15 @@ public class VideoCapture extends UseCase {
mAudioEncoder.start();
} catch (IllegalStateException e) {
- setupEncoder(getAttachedSurfaceResolution(cameraId));
- postListener.onError(VideoCaptureError.ENCODER_ERROR, "Audio/Video encoder start fail",
+ setupEncoder(cameraId, resolution);
+ postListener.onError(ERROR_ENCODER, "Audio/Video encoder start fail",
e);
return;
}
- // Get the relative rotation or default to 0 if the camera info is unavailable
- int relativeRotation = 0;
- try {
- CameraInfoInternal cameraInfoInternal = CameraX.getCameraInfo(cameraId);
- relativeRotation =
- cameraInfoInternal.getSensorRotationDegrees(
- ((ImageOutputConfig) getUseCaseConfig())
- .getTargetRotation(Surface.ROTATION_0));
- } catch (CameraInfoUnavailableException e) {
- Log.e(TAG, "Unable to retrieve camera sensor orientation.", e);
- }
+ CameraInfoInternal cameraInfoInternal = boundCamera.getCameraInfoInternal();
+ int relativeRotation = cameraInfoInternal.getSensorRotationDegrees(
+ ((ImageOutputConfig) getUseCaseConfig()).getTargetRotation(Surface.ROTATION_0));
try {
synchronized (mMuxerLock) {
@@ -355,8 +383,8 @@ public class VideoCapture extends UseCase {
}
}
} catch (IOException e) {
- setupEncoder(getAttachedSurfaceResolution(cameraId));
- postListener.onError(VideoCaptureError.MUXER_ERROR, "MediaMuxer creation failed!", e);
+ setupEncoder(cameraId, resolution);
+ postListener.onError(ERROR_MUXER, "MediaMuxer creation failed!", e);
return;
}
@@ -378,7 +406,8 @@ public class VideoCapture extends UseCase {
new Runnable() {
@Override
public void run() {
- boolean errorOccurred = VideoCapture.this.videoEncode(postListener);
+ boolean errorOccurred = VideoCapture.this.videoEncode(postListener,
+ cameraId, resolution);
if (!errorOccurred) {
postListener.onVideoSaved(saveLocation);
}
@@ -388,11 +417,11 @@ public class VideoCapture extends UseCase {
/**
* Stops recording video, this must be called after {@link
- * VideoCapture#startRecording(File, Metadata, Executor, OnVideoSavedListener)} is called.
+ * VideoCapture#startRecording(File, Metadata, Executor, OnVideoSavedCallback)} is called.
*
* stopRecording() is asynchronous API. User need to check if {@link
- * OnVideoSavedListener#onVideoSaved(File)} or
- * {@link OnVideoSavedListener#onError(VideoCaptureError, String, Throwable)} be called
+ * OnVideoSavedCallback#onVideoSaved(File)} or
+ * {@link OnVideoSavedCallback#onError(int, String, Throwable)} be called
* before startRecording.
*/
public void stopRecording() {
@@ -438,23 +467,17 @@ public class VideoCapture extends UseCase {
return;
}
- final Surface surface = mCameraSurface;
final MediaCodec videoEncoder = mVideoEncoder;
- mDeferrableSurface.setOnSurfaceDetachedListener(
- CameraXExecutors.mainThreadExecutor(),
- new DeferrableSurface.OnSurfaceDetachedListener() {
- @Override
- public void onSurfaceDetached() {
- if (releaseVideoEncoder && videoEncoder != null) {
- videoEncoder.release();
- }
-
- if (surface != null) {
- surface.release();
- }
+ // Calling close should allow termination future to complete and close the surface with
+ // the listener that was added after constructing the DeferrableSurface.
+ mDeferrableSurface.close();
+ mDeferrableSurface.getTerminationFuture().addListener(
+ () -> {
+ if (releaseVideoEncoder && videoEncoder != null) {
+ videoEncoder.release();
}
- });
+ }, CameraXExecutors.mainThreadExecutor());
if (releaseVideoEncoder) {
mVideoEncoder = null;
@@ -473,11 +496,12 @@ public class VideoCapture extends UseCase {
* @param rotation Desired rotation of the output video.
*/
public void setTargetRotation(@RotationValue int rotation) {
- ImageOutputConfig oldConfig = (ImageOutputConfig) getUseCaseConfig();
+ VideoCaptureConfig oldConfig = (VideoCaptureConfig) getUseCaseConfig();
+ VideoCaptureConfig.Builder builder = VideoCaptureConfig.Builder.fromConfig(oldConfig);
int oldRotation = oldConfig.getTargetRotation(ImageOutputConfig.INVALID_ROTATION);
if (oldRotation == ImageOutputConfig.INVALID_ROTATION || oldRotation != rotation) {
- mUseCaseConfigBuilder.setTargetRotation(rotation);
- updateUseCaseConfig(mUseCaseConfigBuilder.build());
+ UseCaseConfigUtil.updateTargetRotationAndRelatedConfigs(builder, rotation);
+ updateUseCaseConfig(builder.getUseCaseConfig());
// TODO(b/122846516): Update session configuration and possibly reconfigure session.
}
@@ -488,7 +512,7 @@ public class VideoCapture extends UseCase {
* audio from selected audio source.
*/
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
- void setupEncoder(Size resolution) {
+ void setupEncoder(@NonNull String cameraId, @NonNull Size resolution) {
VideoCaptureConfig config = (VideoCaptureConfig) getUseCaseConfig();
// video encoder setup
@@ -501,21 +525,32 @@ public class VideoCapture extends UseCase {
if (mCameraSurface != null) {
releaseCameraSurface(false);
}
- mCameraSurface = mVideoEncoder.createInputSurface();
+ Surface cameraSurface = mVideoEncoder.createInputSurface();
+ mCameraSurface = cameraSurface;
SessionConfig.Builder sessionConfigBuilder = SessionConfig.Builder.createFrom(config);
+ if (mDeferrableSurface != null) {
+ mDeferrableSurface.close();
+ }
mDeferrableSurface = new ImmediateSurface(mCameraSurface);
+ mDeferrableSurface.getTerminationFuture().addListener(
+ cameraSurface::release, CameraXExecutors.mainThreadExecutor()
+ );
sessionConfigBuilder.addSurface(mDeferrableSurface);
- String cameraId = getCameraIdUnchecked(config);
-
sessionConfigBuilder.addErrorListener(new SessionConfig.ErrorListener() {
@Override
public void onError(@NonNull SessionConfig sessionConfig,
@NonNull SessionConfig.SessionError error) {
- setupEncoder(resolution);
+ // Ensure the bound camera has not changed before calling setupEncoder.
+ // TODO(b/143915543): Ensure this never gets called by a camera that is not bound
+ // to this use case so we don't need to do this check.
+ if (isCurrentlyBoundCamera(cameraId)) {
+ // Only reset the pipeline when the bound camera is the same.
+ setupEncoder(cameraId, resolution);
+ }
}
});
@@ -620,8 +655,8 @@ public class VideoCapture extends UseCase {
*
* @return returns {@code true} if an error condition occurred, otherwise returns {@code false}
*/
- boolean videoEncode(OnVideoSavedListener videoSavedListener) {
- VideoCaptureConfig config = (VideoCaptureConfig) getUseCaseConfig();
+ boolean videoEncode(@NonNull OnVideoSavedCallback videoSavedCallback, @NonNull String cameraId,
+ @NonNull Size resolution) {
// Main encoding loop. Exits on end of stream.
boolean errorOccurred = false;
boolean videoEos = false;
@@ -638,8 +673,8 @@ public class VideoCapture extends UseCase {
switch (outputBufferId) {
case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
if (mMuxerStarted) {
- videoSavedListener.onError(
- VideoCaptureError.ENCODER_ERROR,
+ videoSavedCallback.onError(
+ ERROR_ENCODER,
"Unexpected change in video encoding format.",
null);
errorOccurred = true;
@@ -656,10 +691,6 @@ public class VideoCapture extends UseCase {
break;
case MediaCodec.INFO_TRY_AGAIN_LATER:
// Timed out. Just wait until next attempt to deque.
- case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
- // Ignore output buffers changed since we dequeue a single buffer instead of
- // multiple
- break;
default:
videoEos = writeVideoEncodedBuffer(outputBufferId);
}
@@ -669,7 +700,7 @@ public class VideoCapture extends UseCase {
Log.i(TAG, "videoEncoder stop");
mVideoEncoder.stop();
} catch (IllegalStateException e) {
- videoSavedListener.onError(VideoCaptureError.ENCODER_ERROR,
+ videoSavedCallback.onError(ERROR_ENCODER,
"Video encoder stop failed!", e);
errorOccurred = true;
}
@@ -686,16 +717,15 @@ public class VideoCapture extends UseCase {
}
}
} catch (IllegalStateException e) {
- videoSavedListener.onError(VideoCaptureError.MUXER_ERROR, "Muxer stop failed!", e);
+ videoSavedCallback.onError(ERROR_MUXER, "Muxer stop failed!", e);
errorOccurred = true;
}
mMuxerStarted = false;
// Do the setup of the videoEncoder at the end of video recording instead of at the start of
// recording because it requires attaching a new Surface. This causes a glitch so we don't
- // want
- // that to incur latency at the start of capture.
- setupEncoder(getAttachedSurfaceResolution(getCameraIdUnchecked(config)));
+ // want that to incur latency at the start of capture.
+ setupEncoder(cameraId, resolution);
notifyReset();
// notify the UI thread that the video recording has finished
@@ -705,7 +735,7 @@ public class VideoCapture extends UseCase {
return errorOccurred;
}
- boolean audioEncode(OnVideoSavedListener videoSavedListener) {
+ boolean audioEncode(OnVideoSavedCallback videoSavedCallback) {
// Audio encoding loop. Exits on end of stream.
boolean audioEos = false;
int outIndex;
@@ -766,14 +796,14 @@ public class VideoCapture extends UseCase {
}
// End Signal Custom Code Block
} catch (IllegalStateException e) {
- videoSavedListener.onError(
- VideoCaptureError.ENCODER_ERROR, "Audio recorder stop failed!", e);
+ videoSavedCallback.onError(
+ ERROR_ENCODER, "Audio recorder stop failed!", e);
}
try {
mAudioEncoder.stop();
} catch (IllegalStateException e) {
- videoSavedListener.onError(VideoCaptureError.ENCODER_ERROR,
+ videoSavedCallback.onError(ERROR_ENCODER,
"Audio encoder stop failed!", e);
}
@@ -889,39 +919,29 @@ public class VideoCapture extends UseCase {
* Describes the error that occurred during video capture operations.
*
* This is a parameter sent to the error callback functions set in listeners such as {@link
- * VideoCapture.OnVideoSavedListener#onError(VideoCaptureError, String, Throwable)}.
+ * VideoCapture.OnVideoSavedCallback#onError(int, String, Throwable)}.
*
* See message parameter in onError callback or log for more details.
+ *
+ * @hide
*/
- public enum VideoCaptureError {
- /**
- * An unknown error occurred.
- *
- * See message parameter in onError callback or log for more details.
- */
- UNKNOWN_ERROR,
- /**
- * An error occurred with encoder state, either when trying to change state or when an
- * unexpected state change occurred.
- */
- ENCODER_ERROR,
- /** An error with muxer state such as during creation or when stopping. */
- MUXER_ERROR,
- /**
- * An error indicating start recording was called when video recording is still in progress.
- */
- RECORDING_IN_PROGRESS
+ @IntDef({ERROR_UNKNOWN, ERROR_ENCODER, ERROR_MUXER, ERROR_RECORDING_IN_PROGRESS})
+ @Retention(RetentionPolicy.SOURCE)
+ @RestrictTo(Scope.LIBRARY_GROUP)
+ public @interface VideoCaptureError {
}
/** Listener containing callbacks for video file I/O events. */
- public interface OnVideoSavedListener {
+ public interface OnVideoSavedCallback {
/** Called when the video has been successfully saved. */
+ // TODO: Should remove file argument to match ImageCapture.OnImageSavedCallback
+ // #onImageSaved()
// Begin Signal Custom Code Block
void onVideoSaved(@NonNull FileDescriptor file);
// End Signal Custom Code Block
/** Called when an error occurs while attempting to save the video. */
- void onError(@NonNull VideoCaptureError videoCaptureError, @NonNull String message,
+ void onError(@VideoCaptureError int videoCaptureError, @NonNull String message,
@Nullable Throwable cause);
}
@@ -936,7 +956,6 @@ public class VideoCapture extends UseCase {
@RestrictTo(Scope.LIBRARY_GROUP)
public static final class Defaults
implements ConfigProviderSURFACE_VIEW
if a {@link android.view.SurfaceView}
+ * should be used to display the camera feed, or
+ * TEXTURE_VIEW
to use a {@link android.view.TextureView}
+ */
+ public void setImplementationMode(@NonNull final ImplementationMode implementationMode) {
+ mImplementationMode = implementationMode;
+ setUp();
+ }
+
+ /**
+ * Returns the implementation mode of the {@link PreviewView}.
+ *
+ * @return SURFACE_VIEW
if the {@link PreviewView} is internally using a
+ * {@link android.view.SurfaceView} to display the camera feed, or TEXTURE_VIEW
+ * if a {@link android.view.TextureView} is being used.
+ */
+ @NonNull
+ public ImplementationMode getImplementationMode() {
+ return mImplementationMode;
+ }
+
+ /**
+ * Gets the {@link Preview.SurfaceProvider} to be used with
+ * {@link Preview#setSurfaceProvider(Executor, Preview.SurfaceProvider)}.
+ */
+ @NonNull
+ public Preview.SurfaceProvider getPreviewSurfaceProvider() {
+ return mImplementation.getSurfaceProvider();
+ }
+
+ /**
+ * Implements this interface to create PreviewView implementation.
+ */
+ interface Implementation {
+
+ /**
+ * Initializes the parent view with sub views.
+ *
+ * @param parent the containing parent {@link FrameLayout}.
+ */
+ void init(@NonNull FrameLayout parent);
+
+ /**
+ * Gets the {@link Preview.SurfaceProvider} to be used with {@link Preview}.
+ */
+ @NonNull
+ Preview.SurfaceProvider getSurfaceProvider();
+
+ /**
+ * Notifies that the display properties have changed.
+ *
+ *