diff --git a/cameraview/src/androidTest/AndroidManifest.xml b/cameraview/src/androidTest/AndroidManifest.xml index 90600623..36b84bf2 100644 --- a/cameraview/src/androidTest/AndroidManifest.xml +++ b/cameraview/src/androidTest/AndroidManifest.xml @@ -4,4 +4,8 @@ + + + + \ No newline at end of file diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockPreview.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockPreview.java index eb2b1b8c..a0afd571 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockPreview.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockPreview.java @@ -2,28 +2,35 @@ package com.otaliastudios.cameraview; import android.content.Context; +import android.support.annotation.NonNull; import android.view.Surface; import android.view.View; import android.view.ViewGroup; -public class MockPreview extends Preview { +public class MockPreview extends Preview { - public MockPreview(Context context, ViewGroup parent) { + MockPreview(Context context, ViewGroup parent) { super(context, parent); } + @NonNull + @Override + protected View onCreateView(Context context, ViewGroup parent) { + return new View(context); + } + @Override Surface getSurface() { return null; } @Override - View getView() { + Class getOutputClass() { return null; } @Override - Class getOutputClass() { + Void getOutput() { return null; } diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/PreviewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/PreviewTest.java new file mode 100644 index 00000000..ea515d20 --- /dev/null +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/PreviewTest.java @@ -0,0 +1,137 @@ +package com.otaliastudios.cameraview; + + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.rule.ActivityTestRule; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import java.util.concurrent.Semaphore; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import static org.junit.Assert.*; + +import static org.mockito.Mockito.*; + +public abstract class PreviewTest { + + protected abstract Preview createPreview(Context context, ViewGroup parent); + + private Preview preview; + private ViewGroup parent; + private Preview.SurfaceCallback callback; + private final Semaphore lock = new Semaphore(1, true); + + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(TestActivity.class); + + @Before + public void setUp() { + Context context = rule.getActivity(); + parent = new FrameLayout(context); + preview = createPreview(context, parent); + callback = mock(Preview.SurfaceCallback.class); + preview.setSurfaceCallback(callback); + InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { + @Override + public void run() { + rule.getActivity().setContentView(parent); + } + }); + } + + @Test + public void testDefaults() { + assertNotNull(preview.getView()); + assertEquals(parent.getChildAt(0), preview.getView()); + assertNotNull(preview.getOutputClass()); + } + + @Test + public void testDesiredSize() { + preview.setDesiredSize(160, 90); + assertEquals(160, preview.getDesiredSize().getWidth()); + assertEquals(90, preview.getDesiredSize().getHeight()); + } + + @Test + public void testSurfaceSize() { + preview.onSurfaceAvailable(500, 1000); + assertEquals(500, preview.getSurfaceSize().getWidth()); + assertEquals(1000, preview.getSurfaceSize().getHeight()); + + preview.onSurfaceSizeChanged(50, 100); + assertEquals(50, preview.getSurfaceSize().getWidth()); + assertEquals(100, preview.getSurfaceSize().getHeight()); + + preview.onSurfaceDestroyed(); + assertEquals(0, preview.getSurfaceSize().getWidth()); + assertEquals(0, preview.getSurfaceSize().getHeight()); + } + + @Test + public void testCallbacks() { + preview.onSurfaceAvailable(500, 1000); + verify(callback, times(1)).onSurfaceAvailable(); + + preview.onSurfaceSizeChanged(50, 100); + verify(callback, times(1)).onSurfaceChanged(); + } + + @Test + public void testCropCenter() throws Exception { + preview.setCropListener(new Preview.CropListener() { + @Override + public void onPreCrop() { + try { + lock.acquire(); + } catch (Exception e) { + e.printStackTrace(); + }; + } + + @Override + public void onPostCrop() { + lock.release(); + } + }); + + // If aspect ratio is different, there should be a crop. + Size s = new Size(1000, 1000); + preview.onSurfaceAvailable(s.getWidth(), s.getHeight()); + + // Not cropping. + preview.setDesiredSize(100, 100); // Wait... + lock.acquire(); + assertEquals(AspectRatio.of(100, 100), getScaledAspectRatio()); + assertFalse(preview.isCropping()); + lock.release(); + + // Cropping. + preview.setDesiredSize(160, 50); // Wait... + lock.acquire(); + assertEquals(AspectRatio.of(160, 50), getScaledAspectRatio()); + assertTrue(preview.isCropping()); + lock.release(); + + // Not cropping. + preview.onSurfaceSizeChanged(1600, 500); // Wait... + lock.acquire(); + assertEquals(AspectRatio.of(160, 50), getScaledAspectRatio()); + assertFalse(preview.isCropping()); + lock.release(); + } + + private AspectRatio getScaledAspectRatio() { + Size size = preview.getSurfaceSize(); + int newWidth = (int) ((float) size.getWidth() * preview.getView().getScaleX()); + int newHeight = (int) ((float) size.getHeight() * preview.getView().getScaleY()); + return AspectRatio.of(newWidth, newHeight); + } +} diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/SurfaceViewPreviewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/SurfaceViewPreviewTest.java new file mode 100644 index 00000000..4e47e9a2 --- /dev/null +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/SurfaceViewPreviewTest.java @@ -0,0 +1,19 @@ +package com.otaliastudios.cameraview; + + +import android.content.Context; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; +import android.view.ViewGroup; + +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class SurfaceViewPreviewTest extends PreviewTest { + + @Override + protected Preview createPreview(Context context, ViewGroup parent) { + return new SurfaceViewPreview(context, parent); + } +} diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/TestActivity.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/TestActivity.java new file mode 100644 index 00000000..8108c7d3 --- /dev/null +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/TestActivity.java @@ -0,0 +1,7 @@ +package com.otaliastudios.cameraview; + + +import android.app.Activity; + +public class TestActivity extends Activity { +} diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/TextureViewPreviewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/TextureViewPreviewTest.java new file mode 100644 index 00000000..a18143a4 --- /dev/null +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/TextureViewPreviewTest.java @@ -0,0 +1,19 @@ +package com.otaliastudios.cameraview; + + +import android.content.Context; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; +import android.view.ViewGroup; + +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class TextureViewPreviewTest extends PreviewTest { + + @Override + protected Preview createPreview(Context context, ViewGroup parent) { + return new TextureViewPreview(context, parent); + } +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java index 3f8c79d7..a039ada2 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java @@ -2,6 +2,7 @@ package com.otaliastudios.cameraview; import android.graphics.PointF; import android.graphics.Rect; +import android.graphics.SurfaceTexture; import android.graphics.YuvImage; import android.hardware.Camera; import android.location.Location; @@ -110,12 +111,14 @@ class Camera1 extends CameraController { // These can happen at different times but we want to end up here. private void setup() { try { + Object output = mPreview.getOutput(); if (mPreview.getOutputClass() == SurfaceHolder.class) { - mCamera.setPreviewDisplay(mPreview.getSurfaceHolder()); + mCamera.setPreviewDisplay((SurfaceHolder) output); } else { - mCamera.setPreviewTexture(mPreview.getSurfaceTexture()); + mCamera.setPreviewTexture((SurfaceTexture) output); } } catch (IOException e) { + LOG.e("Error while trying to setup Camera1.", e); throw new RuntimeException(e); } @@ -450,8 +453,10 @@ class Camera1 extends CameraController { @Override boolean shouldFlipSizes() { + int offset = computeSensorToDisplayOffset(); LOG.i("shouldFlip:", "mDeviceOrientation=", mDeviceOrientation, "mSensorOffset=", mSensorOffset); - return (mDeviceOrientation + mSensorOffset) % 180 != 0; + LOG.i("shouldFlip:", "sensorToDisplay=", offset); + return offset % 180 != 0; } @Override diff --git a/cameraview/src/main/views/com/otaliastudios/cameraview/Preview.java b/cameraview/src/main/views/com/otaliastudios/cameraview/Preview.java index 6e0c94bb..2cd9fdc0 100644 --- a/cameraview/src/main/views/com/otaliastudios/cameraview/Preview.java +++ b/cameraview/src/main/views/com/otaliastudios/cameraview/Preview.java @@ -1,15 +1,14 @@ package com.otaliastudios.cameraview; import android.content.Context; -import android.graphics.SurfaceTexture; +import android.support.annotation.NonNull; import android.view.Surface; -import android.view.SurfaceHolder; import android.view.View; import android.view.ViewGroup; -abstract class Preview { +abstract class Preview { - private final static CameraLogger LOG = CameraLogger.create(Preview.class.getSimpleName()); + protected final static CameraLogger LOG = CameraLogger.create(Preview.class.getSimpleName()); // This is used to notify CameraImpl to recompute its camera Preview size. // After that, CameraView will need a new layout pass to adapt to the Preview size. @@ -19,6 +18,7 @@ abstract class Preview { } private SurfaceCallback mSurfaceCallback; + private T mView; // As far as I can see, these are the view/surface dimensions. // This live in the 'View' orientation. @@ -29,19 +29,25 @@ abstract class Preview { private int mDesiredWidth; private int mDesiredHeight; - Preview(Context context, ViewGroup parent) {} + Preview(Context context, ViewGroup parent) { + mView = onCreateView(context, parent); + } + + @NonNull + protected abstract T onCreateView(Context context, ViewGroup parent); abstract Surface getSurface(); - abstract View getView(); - abstract Class getOutputClass(); - abstract boolean isReady(); - SurfaceHolder getSurfaceHolder() { - return null; - } - SurfaceTexture getSurfaceTexture() { - return null; + + @NonNull + final T getView() { + return mView; } + abstract Class getOutputClass(); + + abstract Output getOutput(); + + abstract boolean isReady(); // As far as I can see, these are the actual preview dimensions, as set in CameraParameters. // This is called by the CameraImpl. @@ -53,6 +59,10 @@ abstract class Preview { crop(); } + final Size getDesiredSize() { + return new Size(mDesiredWidth, mDesiredHeight); + } + final Size getSurfaceSize() { return new Size(mSurfaceWidth, mSurfaceHeight); } @@ -87,7 +97,6 @@ abstract class Preview { protected final void onSurfaceDestroyed() { mSurfaceWidth = 0; mSurfaceHeight = 0; - crop(); } @@ -100,11 +109,16 @@ abstract class Preview { * However that should be already managed by the framework. */ private final void crop() { + onPreCrop(); getView().post(new Runnable() { @Override public void run() { - if (mDesiredHeight == 0 || mDesiredWidth == 0) return; - if (mSurfaceHeight == 0 || mSurfaceWidth == 0) return; + if (mDesiredHeight == 0 || mDesiredWidth == 0 || + mSurfaceHeight == 0 || mSurfaceWidth == 0) { + onPostCrop(); + return; + } + float scaleX = 1f, scaleY = 1f; AspectRatio current = AspectRatio.of(mSurfaceWidth, mSurfaceHeight); AspectRatio target = AspectRatio.of(mDesiredWidth, mDesiredHeight); @@ -119,6 +133,7 @@ abstract class Preview { getView().setScaleY(scaleY); LOG.i("crop:", "applied scaleX=", scaleX); LOG.i("crop:", "applied scaleY=", scaleY); + onPostCrop(); } }); } @@ -132,4 +147,25 @@ abstract class Preview { final boolean isCropping() { return getView().getScaleX() > 1 || getView().getScaleY() > 1; } + + + // Utils for testing. + interface CropListener { + void onPreCrop(); + void onPostCrop(); + } + + private CropListener cropListener; + + private void onPreCrop() { + if (cropListener != null) cropListener.onPreCrop(); + } + + private void onPostCrop() { + if (cropListener != null) cropListener.onPostCrop(); + } + + void setCropListener(CropListener listener) { + cropListener = listener; + } } diff --git a/cameraview/src/main/views/com/otaliastudios/cameraview/SurfaceViewPreview.java b/cameraview/src/main/views/com/otaliastudios/cameraview/SurfaceViewPreview.java index 808934ba..10968073 100644 --- a/cameraview/src/main/views/com/otaliastudios/cameraview/SurfaceViewPreview.java +++ b/cameraview/src/main/views/com/otaliastudios/cameraview/SurfaceViewPreview.java @@ -1,6 +1,7 @@ package com.otaliastudios.cameraview; import android.content.Context; +import android.support.annotation.NonNull; import android.view.Surface; import android.view.SurfaceHolder; import android.view.SurfaceView; @@ -8,20 +9,24 @@ import android.view.View; import android.view.ViewGroup; // This is not used. -class SurfaceViewPreview extends Preview { +class SurfaceViewPreview extends Preview { - private final SurfaceView mSurfaceView; SurfaceViewPreview(Context context, ViewGroup parent) { super(context, parent); - final View view = View.inflate(context, R.layout.surface_view, parent); // MATCH_PARENT - mSurfaceView = (SurfaceView) view.findViewById(R.id.surface_view); - final SurfaceHolder holder = mSurfaceView.getHolder(); + } + + @NonNull + @Override + protected SurfaceView onCreateView(Context context, ViewGroup parent) { + final View root = View.inflate(context, R.layout.surface_view, parent); // MATCH_PARENT + SurfaceView surface = root.findViewById(R.id.surface_view); + final SurfaceHolder holder = surface.getHolder(); holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); holder.addCallback(new SurfaceHolder.Callback() { @Override public void surfaceCreated(SurfaceHolder holder) { - onSurfaceAvailable(mSurfaceView.getWidth(), mSurfaceView.getHeight()); + onSurfaceAvailable(getView().getWidth(), getView().getHeight()); } @Override @@ -34,31 +39,27 @@ class SurfaceViewPreview extends Preview { onSurfaceDestroyed(); } }); + return surface; } @Override Surface getSurface() { - return getSurfaceHolder().getSurface(); - } - - @Override - View getView() { - return mSurfaceView; + return getOutput().getSurface(); } @Override - SurfaceHolder getSurfaceHolder() { - return mSurfaceView.getHolder(); + SurfaceHolder getOutput() { + return getView().getHolder(); } @Override - Class getOutputClass() { + Class getOutputClass() { return SurfaceHolder.class; } @Override boolean isReady() { - return mSurfaceView.getWidth() != 0 && mSurfaceView.getHeight() != 0; + return getView().getWidth() != 0 && getView().getHeight() != 0; } } diff --git a/cameraview/src/main/views/com/otaliastudios/cameraview/TextureViewPreview.java b/cameraview/src/main/views/com/otaliastudios/cameraview/TextureViewPreview.java index 77fd90b0..2e8cdf2d 100644 --- a/cameraview/src/main/views/com/otaliastudios/cameraview/TextureViewPreview.java +++ b/cameraview/src/main/views/com/otaliastudios/cameraview/TextureViewPreview.java @@ -3,22 +3,26 @@ package com.otaliastudios.cameraview; import android.annotation.TargetApi; import android.content.Context; import android.graphics.SurfaceTexture; +import android.support.annotation.NonNull; import android.view.Surface; import android.view.TextureView; import android.view.View; import android.view.ViewGroup; -@TargetApi(14) -class TextureViewPreview extends Preview { +class TextureViewPreview extends Preview { - private final TextureView mTextureView; private Surface mSurface; TextureViewPreview(Context context, ViewGroup parent) { super(context, parent); - final View view = View.inflate(context, R.layout.texture_view, parent); // MATCH_PARENT - mTextureView = (TextureView) view.findViewById(R.id.texture_view); - mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { + } + + @NonNull + @Override + protected TextureView onCreateView(Context context, ViewGroup parent) { + final View root = View.inflate(context, R.layout.texture_view, parent); // MATCH_PARENT + TextureView texture = root.findViewById(R.id.texture_view); + texture.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { @Override public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { @@ -40,42 +44,38 @@ class TextureViewPreview extends Preview { public void onSurfaceTextureUpdated(SurfaceTexture surface) { } }); + return texture; } @Override Surface getSurface() { if (mSurface == null) { // Check if valid? - mSurface = new Surface(getSurfaceTexture()); + mSurface = new Surface(getOutput()); } return mSurface; } @Override - View getView() { - return mTextureView; - } - - @Override - Class getOutputClass() { + Class getOutputClass() { return SurfaceTexture.class; } @Override boolean isReady() { - return getSurfaceTexture() != null; + return getOutput() != null; } @Override - SurfaceTexture getSurfaceTexture() { - return mTextureView.getSurfaceTexture(); + SurfaceTexture getOutput() { + return getView().getSurfaceTexture(); } @TargetApi(15) @Override void setDesiredSize(int width, int height) { super.setDesiredSize(width, height); - if (mTextureView.getSurfaceTexture() != null) { - mTextureView.getSurfaceTexture().setDefaultBufferSize(width, height); + if (getView().getSurfaceTexture() != null) { + getView().getSurfaceTexture().setDefaultBufferSize(width, height); } }