/*
* Copyright 2022 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.viewfinder;
import static androidx.camera.viewfinder.internal.utils.TransformUtils.createTransformInfo;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.display.DisplayManager;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.view.Display;
import android.view.Surface;
import android.view.SurfaceView;
import android.view.TextureView;
import android.view.View;
import android.widget.FrameLayout;
import androidx.annotation.AnyThread;
import androidx.annotation.ColorRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.UiThread;
import androidx.annotation.VisibleForTesting;
import androidx.camera.viewfinder.internal.quirk.DeviceQuirks;
import androidx.camera.viewfinder.internal.quirk.SurfaceViewNotCroppedByParentQuirk;
import androidx.camera.viewfinder.internal.quirk.SurfaceViewStretchedQuirk;
import androidx.camera.viewfinder.internal.surface.ViewfinderSurfaceProvider;
import androidx.camera.viewfinder.internal.utils.Logger;
import androidx.camera.viewfinder.internal.utils.Threads;
import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;
import com.google.common.util.concurrent.ListenableFuture;
/**
* Base viewfinder widget that can display the camera feed for Camera2.
*
* <p> It internally uses either a {@link TextureView} or {@link SurfaceView} to display the
* camera feed, and applies required transformations on them to correctly display the viewfinder,
* this involves correcting their aspect ratio, scale and rotation.
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public final class CameraViewfinder extends FrameLayout {
private static final String TAG = "CameraViewFinder";
@ColorRes private static final int DEFAULT_BACKGROUND_COLOR = android.R.color.black;
private static final ImplementationMode DEFAULT_IMPL_MODE = ImplementationMode.PERFORMANCE;
// Synthetic access
@SuppressWarnings("WeakerAccess")
@NonNull
final ViewfinderTransformation mViewfinderTransformation = new ViewfinderTransformation();
@SuppressWarnings("WeakerAccess")
@NonNull
private final DisplayRotationListener mDisplayRotationListener = new DisplayRotationListener();
@NonNull
private final Looper mRequiredLooper = Looper.myLooper();
@NonNull ImplementationMode mImplementationMode;
// Synthetic access
@SuppressWarnings("WeakerAccess")
@Nullable
ViewfinderImplementation mImplementation;
// Synthetic access
@SuppressWarnings("WeakerAccess")
@Nullable
ViewfinderSurfaceRequest mCurrentSurfaceRequest;
private final OnLayoutChangeListener mOnLayoutChangeListener =
(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
boolean isSizeChanged =
right - left != oldRight - oldLeft || bottom - top != oldBottom - oldTop;
if (isSizeChanged) {
redrawViewfinder();
}
};
// Synthetic access
@SuppressWarnings("WeakerAccess")
final ViewfinderSurfaceProvider mSurfaceProvider = new ViewfinderSurfaceProvider() {
@Override
@AnyThread
public void onSurfaceRequested(@NonNull ViewfinderSurfaceRequest surfaceRequest) {
if (!Threads.isMainThread()) {
// In short term, throwing exception to guarantee onSurfaceRequest is
// called on main thread. In long term, user should be able to specify an
// executor to run this function.
throw new IllegalStateException("onSurfaceRequested must be called on the main "
+ "thread");
}
Logger.d(TAG, "Surface requested by Viewfinder.");
if (surfaceRequest.getImplementationMode() != null) {
mImplementationMode = surfaceRequest.getImplementationMode();
}
mImplementation = shouldUseTextureView(mImplementationMode)
? new TextureViewImplementation(
CameraViewfinder.this, mViewfinderTransformation)
: new SurfaceViewImplementation(
CameraViewfinder.this, mViewfinderTransformation);
mImplementation.onSurfaceRequested(surfaceRequest);
Display display = getDisplay();
if (display != null) {
mViewfinderTransformation.setTransformationInfo(
createTransformInfo(surfaceRequest.getResolution(),
display,
surfaceRequest.getLensFacing()
== CameraCharacteristics.LENS_FACING_FRONT,
surfaceRequest.getSensorOrientation()),
surfaceRequest.getResolution(),
surfaceRequest.getLensFacing()
== CameraCharacteristics.LENS_FACING_FRONT);
redrawViewfinder();
}
}
};
@UiThread
public CameraViewfinder(@NonNull Context context) {
this(context, null);
}
@UiThread
public CameraViewfinder(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
@UiThread
public CameraViewfinder(@NonNull Context context,
@Nullable AttributeSet attrs,
int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
@UiThread
public CameraViewfinder(@NonNull Context context,
@Nullable AttributeSet attrs,
int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
final TypedArray attributes = context.getTheme().obtainStyledAttributes(attrs,
R.styleable.Viewfinder, defStyleAttr, defStyleRes);
ViewCompat.saveAttributeDataForStyleable(this, context, R.styleable.Viewfinder, attrs,
attributes, defStyleAttr, defStyleRes);
try {
final int scaleTypeId = attributes.getInteger(
R.styleable.Viewfinder_scaleType,
mViewfinderTransformation.getScaleType().getId());
setScaleType(ScaleType.fromId(scaleTypeId));
int implementationModeId =
attributes.getInteger(R.styleable.Viewfinder_implementationMode,
DEFAULT_IMPL_MODE.getId());
mImplementationMode = ImplementationMode.fromId(implementationModeId);
} finally {
attributes.recycle();
}
// Set background only if it wasn't already set. A default background prevents the content
// behind the viewfinder from being visible before the viewfinder starts streaming.
if (getBackground() == null) {
setBackgroundColor(ContextCompat.getColor(getContext(), DEFAULT_BACKGROUND_COLOR));
}
}
/**
* Returns the {@link ImplementationMode}.
*
* <p> For each {@link ViewfinderSurfaceRequest} sent to {@link CameraViewfinder}, the
* {@link ImplementationMode} set in the {@link ViewfinderSurfaceRequest} will be used first.
* If it's not set, the {@code app:implementationMode} in the layout xml will be used. If
* it's not set in the layout xml, the default value {@link ImplementationMode#PERFORMANCE}
* will be used. Each {@link ViewfinderSurfaceRequest sent to {@link CameraViewfinder} can
* override the {@link ImplementationMode} once it has set the
* {@link ImplementationMode}.
*
* @return The {@link ImplementationMode} for {@link CameraViewfinder}.
*/
@UiThread
@NonNull
public ImplementationMode getImplementationMode() {
checkUiThread();
return mImplementationMode;
}
/**
* Applies a {@link ScaleType} to the viewfinder.
*
* <p> This value can also be set in the layout XML file via the {@code app:scaleType}
* attribute.
*
* <p> The default value is {@link ScaleType#FILL_CENTER}.
*
* <p> This method should be called after {@link CameraViewfinder} is inflated and can be
* called before or after
* {@link CameraViewfinder#requestSurfaceAsync(ViewfinderSurfaceRequest)}. The
* {@link ScaleType} to set will be effective immediately after the method is called.
*
* @param scaleType The {@link ScaleType} to apply to the viewfinder.
* @attr name app:scaleType
*/
@UiThread
public void setScaleType(@NonNull final ScaleType scaleType) {
checkUiThread();
mViewfinderTransformation.setScaleType(scaleType);
redrawViewfinder();
}
/**
* Returns the {@link ScaleType} currently applied to the viewfinder.
*
* <p> The default value is {@link ScaleType#FILL_CENTER}.
*
* @return The {@link ScaleType} currently applied to the viewfinder.
*/
@UiThread
@NonNull
public ScaleType getScaleType() {
checkUiThread();
return mViewfinderTransformation.getScaleType();
}
/**
* Requests surface by sending a {@link ViewfinderSurfaceRequest}.
*
* <p> Only one request can be handled at the same time. If requesting a surface with
* the same {@link ViewfinderSurfaceRequest}, the previous requested surface will be returned.
* If requesting a surface with a new {@link ViewfinderSurfaceRequest}, the previous
* requested surface will be released and a new surface will be requested.
*
* <p> The result is a {@link ListenableFuture} of {@link Surface}, which provides the
* functionality to attach listeners and propagate exceptions.
*
* <pre>
* ViewfinderSurfaceRequest request = new ViewfinderSurfaceRequest(
* new Size(width, height), cameraManager.getCameraCharacteristics(cameraId));
*
* ListenableFuture<Surface> surfaceListenableFuture =
* mCameraViewFinder.requestSurfaceAsync(request);
*
* Futures.addCallback(surfaceListenableFuture, new FutureCallback<Surface>() {
* {@literal @}Override
* public void onSuccess({@literal @}Nullable Surface surface) {
* if (surface != null) {
* createCaptureSession(surface);
* }
* }
*
* {@literal @}Override
* public void onFailure(Throwable t) {}
* }, ContextCompat.getMainExecutor(getContext()));
* </pre>
*
* @param surfaceRequest The {@link ViewfinderSurfaceRequest} to get a surface.
* @return The requested surface.
*
* @see ViewfinderSurfaceRequest
*/
@UiThread
@NonNull
public ListenableFuture<Surface> requestSurfaceAsync(
@NonNull ViewfinderSurfaceRequest surfaceRequest) {
checkUiThread();
if (mCurrentSurfaceRequest != null
&& surfaceRequest.equals(mCurrentSurfaceRequest)) {
return mCurrentSurfaceRequest.getViewfinderSurface().getSurface();
}
if (mCurrentSurfaceRequest != null) {
mCurrentSurfaceRequest.markSurfaceSafeToRelease();
}
ListenableFuture<Surface> surfaceListenableFuture =
surfaceRequest.getViewfinderSurface().getSurface();
mCurrentSurfaceRequest = surfaceRequest;
provideSurfaceIfReady();
return surfaceListenableFuture;
}
/**
* Returns a {@link Bitmap} representation of the content displayed on the
* {@link CameraViewfinder}, or {@code null} if the camera viewfinder hasn't started yet.
* <p>
* The returned {@link Bitmap} uses the {@link Bitmap.Config#ARGB_8888} pixel format and its
* dimensions are the same as this view's.
* <p>
* <strong>Do not</strong> invoke this method from a drawing method
* ({@link View#onDraw(Canvas)} for instance).
* <p>
* If an error occurs during the copy, an empty {@link Bitmap} will be returned.
*
* @return A {@link Bitmap.Config#ARGB_8888} {@link Bitmap} representing the content
* displayed on the {@link CameraViewfinder}, or null if the camera viewfinder hasn't started
* yet.
*/
@UiThread
@Nullable
public Bitmap getBitmap() {
checkUiThread();
return mImplementation == null ? null : mImplementation.getBitmap();
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
addOnLayoutChangeListener(mOnLayoutChangeListener);
if (mImplementation != null) {
mImplementation.onAttachedToWindow();
}
startListeningToDisplayChange();
// TODO: need to handle incomplete surface request if request is received before view
// attached to window.
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
removeOnLayoutChangeListener(mOnLayoutChangeListener);
if (mImplementation != null) {
mImplementation.onDetachedFromWindow();
}
if (mCurrentSurfaceRequest != null) {
mCurrentSurfaceRequest.markSurfaceSafeToRelease();
mCurrentSurfaceRequest = null;
}
stopListeningToDisplayChange();
}
@VisibleForTesting
static boolean shouldUseTextureView(@NonNull final ImplementationMode implementationMode) {
boolean hasSurfaceViewQuirk = DeviceQuirks.get(SurfaceViewStretchedQuirk.class) != null
|| DeviceQuirks.get(SurfaceViewNotCroppedByParentQuirk.class) != null;
if (Build.VERSION.SDK_INT <= 24 || hasSurfaceViewQuirk) {
// Force to use TextureView when the device is running android 7.0 and below, legacy
// level or SurfaceView has quirks.
Logger.d(TAG, "Implementation mode to set is not supported, forcing to use "
+ "TextureView, because transform APIs are not supported on these devices.");
return true;
}
switch (implementationMode) {
case COMPATIBLE:
return true;
case PERFORMANCE:
return false;
default:
throw new IllegalArgumentException(
"Invalid implementation mode: " + implementationMode);
}
}
// Synthetic access
@SuppressWarnings("WeakerAccess")
void redrawViewfinder() {
if (mImplementation != null) {
mImplementation.redrawViewfinder();
}
}
private boolean provideSurfaceIfReady() {
final ViewfinderSurfaceRequest surfaceRequest = mCurrentSurfaceRequest;
final ViewfinderSurfaceProvider surfaceProvider = mSurfaceProvider;
if (surfaceProvider != null && surfaceRequest != null) {
surfaceProvider.onSurfaceRequested(surfaceRequest);
return true;
}
return false;
}
/**
* Checks if the current thread is the same UI thread on which the class was constructed.
*
* @see <a href = go/android-api-guidelines/concurrency#uithread></a>
*/
private void checkUiThread() {
// Ignore mRequiredLooper == null because this can be called from the super
// class constructor before the class's own constructor has run.
if (mRequiredLooper != null && Looper.myLooper() != mRequiredLooper) {
Throwable throwable = new Throwable(
"A method was called on thread '" + Thread.currentThread().getName()
+ "'. All methods must be called on the same thread. (Expected Looper "
+ mRequiredLooper + ", but called on " + Looper.myLooper() + ".");
throw new RuntimeException(throwable);
}
}
/**
* The implementation mode of a {@link CameraViewfinder}.
*
* <p> User preference on how the {@link CameraViewfinder} should render the viewfinder.
* {@link CameraViewfinder} displays the viewfinder with either a {@link SurfaceView} or a
* {@link TextureView}. A {@link SurfaceView} is generally better than a {@link TextureView}
* when it comes to certain key metrics, including power and latency. On the other hand,
* {@link TextureView} is better supported by a wider range of devices. The option is used by
* {@link CameraViewfinder} to decide what is the best internal implementation given the device
* capabilities and user configurations.
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public enum ImplementationMode {
/**
* Use a {@link SurfaceView} for the viewfinder when possible. A SurfaceView has somewhat
* lower latency and less performance and power overhead than a TextureView. Use this
*
* If the device doesn't support {@link SurfaceView}, {@link CameraViewfinder} will fall
* back to use a {@link TextureView} instead.
*
* <p>{@link CameraViewfinder} falls back to {@link TextureView} when the API level is 24 or
* lower, the camera hardware support level is
* {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY}.
*
*/
PERFORMANCE(0),
/**
* Use a {@link TextureView} for the viewfinder.
*/
COMPATIBLE(1);
private final int mId;
ImplementationMode(int id) {
mId = id;
}
int getId() {
return mId;
}
static ImplementationMode fromId(int id) {
for (ImplementationMode implementationMode : values()) {
if (implementationMode.mId == id) {
return implementationMode;
}
}
throw new IllegalArgumentException("Unknown implementation mode id " + id);
}
}
/** Options for scaling the viewfinder vis-à-vis its container {@link CameraViewfinder}. */
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public enum ScaleType {
/**
* Scale the viewfinder, maintaining the source aspect ratio, so it fills the entire
* {@link CameraViewfinder}, and align it to the start of the view, which is the top left
* corner in a left-to-right (LTR) layout, or the top right corner in a right-to-left
* (RTL) layout.
* <p>
* This may cause the viewfinder to be cropped if the camera viewfinder aspect ratio does
* not match that of its container {@link CameraViewfinder}.
*/
FILL_START(0),
/**
* Scale the viewfinder, maintaining the source aspect ratio, so it fills the entire
* {@link CameraViewfinder}, and center it in the view.
* <p>
* This may cause the viewfinder to be cropped if the camera viewfinder aspect ratio does
* not match that of its container {@link CameraViewfinder}.
*/
FILL_CENTER(1),
/**
* Scale the viewfinder, maintaining the source aspect ratio, so it fills the entire
* {@link CameraViewfinder}, and align it to the end of the view, which is the bottom right
* corner in a left-to-right (LTR) layout, or the bottom left corner in a right-to-left
* (RTL) layout.
* <p>
* This may cause the viewfinder to be cropped if the camera viewfinder aspect ratio does
* not match that of its container {@link CameraViewfinder}.
*/
FILL_END(2),
/**
* Scale the viewfinder, maintaining the source aspect ratio, so it is entirely contained
* within the {@link CameraViewfinder}, and align it to the start of the view, which is the
* top left corner in a left-to-right (LTR) layout, or the top right corner in a
* right-to-left (RTL) layout. The background area not covered by the viewfinder stream
* will be black or the background of the {@link CameraViewfinder}
* <p>
* Both dimensions of the viewfinder will be equal or less than the corresponding dimensions
* of its container {@link CameraViewfinder}.
*/
FIT_START(3),
/**
* Scale the viewfinder, maintaining the source aspect ratio, so it is entirely contained
* within the {@link CameraViewfinder}, and center it inside the view. The background
* area not covered by the viewfinder stream will be black or the background of the
* {@link CameraViewfinder}.
* <p>
* Both dimensions of the viewfinder will be equal or less than the corresponding dimensions
* of its container {@link CameraViewfinder}.
*/
FIT_CENTER(4),
/**
* Scale the viewfinder, maintaining the source aspect ratio, so it is entirely contained
* within the {@link CameraViewfinder}, and align it to the end of the view, which is the
* bottom right corner in a left-to-right (LTR) layout, or the bottom left corner in a
* right-to-left (RTL) layout. The background area not covered by the viewfinder stream
* will be black or the background of the {@link CameraViewfinder}.
* <p>
* Both dimensions of the viewfinder will be equal or less than the corresponding dimensions
* of its container {@link CameraViewfinder}.
*/
FIT_END(5);
private final int mId;
ScaleType(int id) {
mId = id;
}
int getId() {
return mId;
}
static ScaleType fromId(int id) {
for (ScaleType scaleType : values()) {
if (scaleType.mId == id) {
return scaleType;
}
}
throw new IllegalArgumentException("Unknown scale type id " + id);
}
}
private void startListeningToDisplayChange() {
DisplayManager displayManager = getDisplayManager();
if (displayManager == null) {
return;
}
displayManager.registerDisplayListener(mDisplayRotationListener,
new Handler(Looper.getMainLooper()));
}
private void stopListeningToDisplayChange() {
DisplayManager displayManager = getDisplayManager();
if (displayManager == null) {
return;
}
displayManager.unregisterDisplayListener(mDisplayRotationListener);
}
@Nullable
private DisplayManager getDisplayManager() {
Context context = getContext();
if (context == null) {
return null;
}
return (DisplayManager) context.getApplicationContext()
.getSystemService(Context.DISPLAY_SERVICE);
}
/**
* Listener for display rotation changes.
*
* <p> When the device is rotated 180° from side to side, the activity is not
* destroyed and recreated. This class is necessary to make sure preview's target rotation
* gets updated when that happens.
*/
// Synthetic access
@SuppressWarnings("WeakerAccess")
class DisplayRotationListener implements DisplayManager.DisplayListener {
@Override
public void onDisplayAdded(int displayId) {
}
@Override
public void onDisplayRemoved(int displayId) {
}
@Override
public void onDisplayChanged(int displayId) {
Display display = getDisplay();
if (display != null && display.getDisplayId() == displayId) {
ViewfinderSurfaceRequest surfaceRequest = mCurrentSurfaceRequest;
if (surfaceRequest != null) {
mViewfinderTransformation.updateTransformInfo(
createTransformInfo(surfaceRequest.getResolution(),
display,
surfaceRequest.getLensFacing()
== CameraCharacteristics.LENS_FACING_FRONT,
surfaceRequest.getSensorOrientation()));
redrawViewfinder();
}
}
}
}
}