/* * 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.view; import android.Manifest.permission; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManager.DisplayListener; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Parcelable; import android.text.TextUtils; import android.util.AttributeSet; import android.view.Display; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.Surface; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.RequiresPermission; import androidx.annotation.RestrictTo; import androidx.annotation.RestrictTo.Scope; import androidx.camera.core.Camera; import androidx.camera.core.CameraSelector; 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.ImageCapture.OnImageSavedCallback; import androidx.camera.core.ImageProxy; import androidx.camera.core.Logger; import androidx.camera.core.MeteringPoint; import androidx.camera.core.MeteringPointFactory; import androidx.camera.core.VideoCapture; import androidx.camera.core.VideoCapture.OnVideoSavedCallback; 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.core.util.Consumer; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LiveData; import com.google.common.util.concurrent.ListenableFuture; import org.signal.core.util.logging.Log; import java.io.File; import java.util.concurrent.Executor; /** * A {@link View} that displays a preview of the camera with methods {@link * #takePicture(Executor, OnImageCapturedCallback)}, * {@link #takePicture(ImageCapture.OutputFileOptions, Executor, OnImageSavedCallback)}, * {@link #startRecording(File , Executor , OnVideoSavedCallback callback)} * 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.
*/
@RequiresApi(21)
@SuppressLint("RestrictedApi")
public final class SignalCameraView extends FrameLayout {
static final String TAG = Log.tag(SignalCameraView.class);
static final int INDEFINITE_VIDEO_DURATION = -1;
static final int INDEFINITE_VIDEO_SIZE = -1;
private static final String EXTRA_SUPER = "super";
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";
private static final String EXTRA_MAX_VIDEO_SIZE = "max_video_size";
private static final String EXTRA_SCALE_TYPE = "scale_type";
private static final String EXTRA_CAMERA_DIRECTION = "camera_direction";
private static final String EXTRA_CAPTURE_MODE = "captureMode";
private static final int LENS_FACING_NONE = 0;
private static final int LENS_FACING_FRONT = 1;
private static final int LENS_FACING_BACK = 2;
private static final int FLASH_MODE_AUTO = 1;
private static final int FLASH_MODE_ON = 2;
private static final int FLASH_MODE_OFF = 4;
// For tap-to-focus
private long mDownEventTimestamp;
// For pinch-to-zoom
private PinchToZoomGestureDetector mPinchToZoomGestureDetector;
private boolean mIsPinchToZoomEnabled = true;
SignalCameraXModule mCameraModule;
private final DisplayManager.DisplayListener mDisplayListener =
new DisplayListener() {
@Override
public void onDisplayAdded(int displayId) {
}
@Override
public void onDisplayRemoved(int displayId) {
}
@Override
public void onDisplayChanged(int displayId) {
mCameraModule.invalidateView();
}
};
private PreviewView mPreviewView;
// For accessibility event
private MotionEvent mUpEvent;
// BEGIN Custom Signal Code Block
private Consumer This links opening/closing the camera to the given lifecycle. The camera will not operate
* unless this method is called with a valid {@link LifecycleOwner} that is not in the {@link
* androidx.lifecycle.Lifecycle.State#DESTROYED} state. Call this method only once camera
* permissions have been obtained.
*
* Once the provided lifecycle has transitioned to a {@link
* androidx.lifecycle.Lifecycle.State#DESTROYED} state, CameraView must be bound to a new
* lifecycle through this method in order to operate the camera.
*
* @param lifecycleOwner The lifecycle that will control this view's camera
* @throws IllegalArgumentException if provided lifecycle is in a {@link
* androidx.lifecycle.Lifecycle.State#DESTROYED} state.
* @throws IllegalStateException if camera permissions are not granted.
*/
// BEGIN Custom Signal Code Block
@RequiresPermission(permission.CAMERA)
public void bindToLifecycle(@NonNull LifecycleOwner lifecycleOwner, Consumer This controls how the view finder should be scaled and positioned within the view.
*
* @param scaleType The desired {@link PreviewView.ScaleType}.
*/
public void setScaleType(@NonNull PreviewView.ScaleType scaleType) {
mPreviewView.setScaleType(scaleType);
}
/**
* Returns the scale type used to scale the preview.
*
* @return The current {@link CaptureMode}.
*/
@NonNull
public CaptureMode getCaptureMode() {
return mCameraModule.getCaptureMode();
}
/**
* Sets the CameraView capture mode
*
* This controls only image or video capture function is enabled or both are enabled.
*
* @param captureMode The desired {@link CaptureMode}.
*/
public void setCaptureMode(@NonNull CaptureMode captureMode) {
mCameraModule.setCaptureMode(captureMode);
}
/**
* Returns the maximum duration of videos, or {@link #INDEFINITE_VIDEO_DURATION} if there is no
* timeout.
*
* @hide Not currently implemented.
*/
@RestrictTo(Scope.LIBRARY_GROUP)
public long getMaxVideoDuration() {
return mCameraModule.getMaxVideoDuration();
}
/**
* Sets the maximum video duration before
* {@link OnVideoSavedCallback#onVideoSaved(VideoCapture.OutputFileResults)} is called
* automatically.
* Use {@link #INDEFINITE_VIDEO_DURATION} to disable the timeout.
*/
private void setMaxVideoDuration(long duration) {
mCameraModule.setMaxVideoDuration(duration);
}
/**
* Returns the maximum size of videos in bytes, or {@link #INDEFINITE_VIDEO_SIZE} if there is no
* timeout.
*/
private long getMaxVideoSize() {
return mCameraModule.getMaxVideoSize();
}
/**
* Sets the maximum video size in bytes before
* {@link OnVideoSavedCallback#onVideoSaved(VideoCapture.OutputFileResults)}
* is called automatically. Use {@link #INDEFINITE_VIDEO_SIZE} to disable the size restriction.
*/
private void setMaxVideoSize(long size) {
mCameraModule.setMaxVideoSize(size);
}
/**
* Takes a picture, and calls {@link OnImageCapturedCallback#onCaptureSuccess(ImageProxy)}
* once when done.
*
* @param executor The executor in which the callback methods will be run.
* @param callback Callback which will receive success or failure callbacks.
*/
public void takePicture(@NonNull Executor executor, @NonNull OnImageCapturedCallback callback) {
mCameraModule.takePicture(executor, callback);
}
/**
* Takes a picture and calls
* {@link OnImageSavedCallback#onImageSaved(ImageCapture.OutputFileResults)} when done.
*
* The value of {@link ImageCapture.Metadata#isReversedHorizontal()} in the
* {@link ImageCapture.OutputFileOptions} will be overwritten based on camera direction. For
* front camera, it will be set to true; for back camera, it will be set to false.
*
* @param outputFileOptions Options to store the newly captured image.
* @param executor The executor in which the callback methods will be run.
* @param callback Callback which will receive success or failure.
*/
public void takePicture(@NonNull ImageCapture.OutputFileOptions outputFileOptions,
@NonNull Executor executor,
@NonNull OnImageSavedCallback callback) {
mCameraModule.takePicture(outputFileOptions, executor, callback);
}
/**
* Takes a video and calls the OnVideoSavedCallback when done.
*
* @param outputFileOptions Options to store the newly captured video.
* @param executor The executor in which the callback methods will be run.
* @param callback Callback which will receive success or failure.
*/
public void startRecording(@NonNull VideoCapture.OutputFileOptions outputFileOptions,
@NonNull Executor executor,
@NonNull OnVideoSavedCallback callback) {
mCameraModule.startRecording(outputFileOptions, executor, callback);
}
/** Stops an in progress video. */
public void stopRecording() {
mCameraModule.stopRecording();
}
/** @return True if currently recording. */
public boolean isRecording() {
return mCameraModule.isRecording();
}
/**
* Queries whether the current device has a camera with the specified direction.
*
* @return True if the device supports the direction.
* @throws IllegalStateException if the CAMERA permission is not currently granted.
*/
@RequiresPermission(permission.CAMERA)
public boolean hasCameraWithLensFacing(@CameraSelector.LensFacing int lensFacing) {
return mCameraModule.hasCameraWithLensFacing(lensFacing);
}
/**
* Toggles between the primary front facing camera and the primary back facing camera.
*
* This will have no effect if not already bound to a lifecycle via {@link
* #bindToLifecycle(LifecycleOwner)}.
*/
public void toggleCamera() {
mCameraModule.toggleCamera();
}
/**
* Sets the desired camera by specifying desired lensFacing.
*
* This will choose the primary camera with the specified camera lensFacing.
*
* 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(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
* equivalent to unbind the use cases without the lifecycle having to be destroyed.
*
* @param lensFacing The desired camera lensFacing.
*/
public void setCameraLensFacing(@Nullable Integer lensFacing) {
mCameraModule.setCameraLensFacing(lensFacing);
}
/** Returns the currently selected lensFacing. */
@Nullable
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
/** Sets the active flash strategy. */
public void setFlash(@ImageCapture.FlashMode int flashMode) {
mCameraModule.setFlash(flashMode);
}
private long delta() {
return System.currentTimeMillis() - mDownEventTimestamp;
}
@Override
public boolean onTouchEvent(@NonNull MotionEvent event) {
// Disable pinch-to-zoom and tap-to-focus while the camera module is paused.
if (mCameraModule.isPaused()) {
return false;
}
// Only forward the event to the pinch-to-zoom gesture detector when pinch-to-zoom is
// enabled.
if (isPinchToZoomEnabled()) {
mPinchToZoomGestureDetector.onTouchEvent(event);
}
if (event.getPointerCount() == 2 && isPinchToZoomEnabled() && isZoomSupported()) {
return true;
}
// Camera focus
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownEventTimestamp = System.currentTimeMillis();
break;
case MotionEvent.ACTION_UP:
if (delta() < ViewConfiguration.getLongPressTimeout()
&& mCameraModule.isBoundToLifecycle()) {
mUpEvent = event;
performClick();
}
break;
default:
// Unhandled event.
return false;
}
return true;
}
/**
* Focus the position of the touch event, or focus the center of the preview for
* accessibility events
*/
@Override
public boolean performClick() {
super.performClick();
final float x = (mUpEvent != null) ? mUpEvent.getX() : getX() + getWidth() / 2f;
final float y = (mUpEvent != null) ? mUpEvent.getY() : getY() + getHeight() / 2f;
mUpEvent = null;
Camera camera = mCameraModule.getCamera();
if (camera != null) {
MeteringPointFactory pointFactory = mPreviewView.getMeteringPointFactory();
float afPointWidth = 1.0f / 6.0f; // 1/6 total area
float aePointWidth = afPointWidth * 1.5f;
MeteringPoint afPoint = pointFactory.createPoint(x, y, afPointWidth);
MeteringPoint aePoint = pointFactory.createPoint(x, y, aePointWidth);
ListenableFuture When enabled, the user can pinch the camera to zoom in/out. This only has an effect if the
* bound camera supports zoom.
*
* @param enabled True to enable pinch-to-zoom.
*/
public void setPinchToZoomEnabled(boolean enabled) {
mIsPinchToZoomEnabled = enabled;
}
/**
* Returns the current zoom ratio.
*
* @return The current zoom ratio.
*/
public float getZoomRatio() {
return mCameraModule.getZoomRatio();
}
/**
* Sets the current zoom ratio.
*
* Valid zoom values range from {@link #getMinZoomRatio()} to {@link #getMaxZoomRatio()}.
*
* @param zoomRatio The requested zoom ratio.
*/
public void setZoomRatio(float zoomRatio) {
mCameraModule.setZoomRatio(zoomRatio);
}
/**
* Returns the minimum zoom ratio.
*
* 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 ratio.
*/
public float getMinZoomRatio() {
return mCameraModule.getMinZoomRatio();
}
/**
* Returns the maximum zoom ratio.
*
* 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 ratio.
*/
public float getMaxZoomRatio() {
return mCameraModule.getMaxZoomRatio();
}
/**
* Returns whether the bound camera supports zooming.
*
* @return True if the camera supports zooming.
*/
public boolean isZoomSupported() {
return mCameraModule.isZoomSupported();
}
/**
* Turns on/off torch.
*
* @param torch True to turn on torch, false to turn off torch.
*/
public void enableTorch(boolean torch) {
mCameraModule.enableTorch(torch);
}
/**
* Returns current torch status.
*
* @return true if torch is on , otherwise false
*/
public boolean isTorchOn() {
return mCameraModule.isTorchOn();
}
/**
* The capture mode used by CameraView.
*
* This enum can be used to determine which capture mode will be enabled for {@link
* SignalCameraView}.
*/
public enum CaptureMode {
/** A mode where image capture is enabled. */
IMAGE(0),
/** A mode where video capture is enabled. */
VIDEO(1),
/**
* A mode where both image capture and video capture are simultaneously enabled. Note that
* this mode may not be available on every device.
*/
MIXED(2);
private final int mId;
int getId() {
return mId;
}
CaptureMode(int id) {
mId = id;
}
static CaptureMode fromId(int id) {
for (CaptureMode f : values()) {
if (f.mId == id) {
return f;
}
}
throw new IllegalArgumentException();
}
}
static class S extends ScaleGestureDetector.SimpleOnScaleGestureListener {
private ScaleGestureDetector.OnScaleGestureListener mListener;
void setRealGestureDetector(ScaleGestureDetector.OnScaleGestureListener l) {
mListener = l;
}
@Override
public boolean onScale(ScaleGestureDetector detector) {
return mListener.onScale(detector);
}
}
private class PinchToZoomGestureDetector extends ScaleGestureDetector
implements ScaleGestureDetector.OnScaleGestureListener {
PinchToZoomGestureDetector(Context context) {
this(context, new S());
}
PinchToZoomGestureDetector(Context context, S s) {
super(context, s);
s.setRealGestureDetector(this);
}
@Override
public boolean onScale(ScaleGestureDetector detector) {
float scale = detector.getScaleFactor();
// Speeding up the zoom by 2X.
if (scale > 1f) {
scale = 1.0f + (scale - 1.0f) * 2;
} else {
scale = 1.0f - (1.0f - scale) * 2;
}
float newRatio = getZoomRatio() * scale;
newRatio = rangeLimit(newRatio, getMaxZoomRatio(), getMinZoomRatio());
setZoomRatio(newRatio);
return true;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
}
}
}