From 66077c1eaab21c21e33866e8c311c2a43ecce86f Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Thu, 7 Sep 2017 18:51:25 +0200 Subject: [PATCH] Improve coverage (#22) * New tests * Try adjust codecov * CropHelper and CameraUtils tests * Set target coverage to 40% * OrientationHelperTest * Preview tests * Fix SurfaceView * Introduce Task for async tests * Gesture tests * Improve Gesture Tests * Add wake lock * Fix view size * Add build info logs * Revert info * Try fix bug * Test timeout * Try fix tests * Fix TextureView tests if not hardware accelerated * Fix bug --- .travis.yml | 4 +- cameraview/build.gradle | 7 +- .../src/androidTest/AndroidManifest.xml | 13 ++ .../otaliastudios/cameraview/BaseTest.java | 41 +++++ .../cameraview/CameraUtilsTest.java | 60 +++++++ .../cameraview/CameraViewTest.java | 150 ++++++++++++++++- .../cameraview/CropHelperTest.java | 92 ++++++++++ .../cameraview/GestureLayoutTest.java | 62 +++++++ .../cameraview/GridLinesLayoutTest.java | 78 +++++++++ .../cameraview/MockCameraController.java | 14 +- .../otaliastudios/cameraview/MockPreview.java | 21 ++- .../cameraview/OrientationHelperTest.java | 97 +++++++++++ .../cameraview/PinchGestureLayoutTest.java | 86 ++++++++++ .../otaliastudios/cameraview/PreviewTest.java | 157 ++++++++++++++++++ .../cameraview/ScrollGestureLayoutTest.java | 97 +++++++++++ .../cameraview/SurfaceViewPreviewTest.java | 19 +++ .../cameraview/TapGestureLayoutTest.java | 81 +++++++++ .../cameraview/TestActivity.java | 75 +++++++++ .../cameraview/TextureViewPreviewTest.java | 43 +++++ .../com/otaliastudios/cameraview/Camera1.java | 11 +- .../otaliastudios/cameraview/CameraView.java | 6 +- .../src/main/res/layout/surface_view.xml | 4 +- .../cameraview/OrientationHelper.java | 9 +- .../com/otaliastudios/cameraview/Task.java | 72 ++++++++ .../cameraview/GestureLayout.java | 5 + .../cameraview/GridLinesLayout.java | 10 +- .../cameraview/PinchGestureLayout.java | 2 +- .../com/otaliastudios/cameraview/Preview.java | 60 +++++-- .../cameraview/SurfaceViewPreview.java | 40 ++--- .../cameraview/TapGestureLayout.java | 4 +- .../cameraview/TextureViewPreview.java | 43 +++-- codecov.yml | 10 +- 32 files changed, 1362 insertions(+), 111 deletions(-) create mode 100644 cameraview/src/androidTest/AndroidManifest.xml create mode 100644 cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseTest.java create mode 100644 cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraUtilsTest.java create mode 100644 cameraview/src/androidTest/java/com/otaliastudios/cameraview/CropHelperTest.java create mode 100644 cameraview/src/androidTest/java/com/otaliastudios/cameraview/GestureLayoutTest.java create mode 100644 cameraview/src/androidTest/java/com/otaliastudios/cameraview/GridLinesLayoutTest.java create mode 100644 cameraview/src/androidTest/java/com/otaliastudios/cameraview/OrientationHelperTest.java create mode 100644 cameraview/src/androidTest/java/com/otaliastudios/cameraview/PinchGestureLayoutTest.java create mode 100644 cameraview/src/androidTest/java/com/otaliastudios/cameraview/PreviewTest.java create mode 100644 cameraview/src/androidTest/java/com/otaliastudios/cameraview/ScrollGestureLayoutTest.java create mode 100644 cameraview/src/androidTest/java/com/otaliastudios/cameraview/SurfaceViewPreviewTest.java create mode 100644 cameraview/src/androidTest/java/com/otaliastudios/cameraview/TapGestureLayoutTest.java create mode 100644 cameraview/src/androidTest/java/com/otaliastudios/cameraview/TestActivity.java create mode 100644 cameraview/src/androidTest/java/com/otaliastudios/cameraview/TextureViewPreviewTest.java create mode 100644 cameraview/src/main/utils/com/otaliastudios/cameraview/Task.java diff --git a/.travis.yml b/.travis.yml index 410e1a0f..c7c60469 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,10 +35,10 @@ before_script: - adb shell input keyevent 82 & script: - - ./gradlew clean testDebugUnitTest connectedCheck coverageReport + - ./gradlew clean testDebugUnitTest connectedCheck mergedCoverageReport after_success: - - bash <(curl -s https://codecov.io/bash) + - bash <(curl -s https://codecov.io/bash) -s "*/build/reports/mergedCoverageReport/" cache: directories: diff --git a/cameraview/build.gradle b/cameraview/build.gradle index fd3a2c94..987db39e 100644 --- a/cameraview/build.gradle +++ b/cameraview/build.gradle @@ -46,6 +46,7 @@ dependencies { androidTestCompile 'com.android.support.test:rules:1.0.1' androidTestCompile 'com.google.dexmaker:dexmaker:1.2' androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2' + androidTestCompile 'com.android.support.test.espresso:espresso-core:3.0.1' compile "com.android.support:exifinterface:$supportLibVersion" compile "com.android.support:support-annotations:$supportLibVersion" @@ -175,14 +176,15 @@ artifacts { apply plugin: 'jacoco' +def reportsDirectory = "$buildDir/reports/" jacoco { // Using a different version might get 0 coverage reports. // No time to investigate now. toolVersion = "0.7.6.201602180812" - reportsDir = file("$buildDir/reports/jacoco/merged/") + reportsDir = file(reportsDirectory) } -task coverageReport(type: JacocoReport) { +task mergedCoverageReport(type: JacocoReport) { dependsOn "testDebugUnitTest" dependsOn "connectedCheck" @@ -206,6 +208,7 @@ task coverageReport(type: JacocoReport) { reports.xml.enabled = true reports.html.enabled = true + reports.xml.destination = "$reportsDirectory/mergedCoverageReport/report.xml" } //endregion diff --git a/cameraview/src/androidTest/AndroidManifest.xml b/cameraview/src/androidTest/AndroidManifest.xml new file mode 100644 index 00000000..1795db1e --- /dev/null +++ b/cameraview/src/androidTest/AndroidManifest.xml @@ -0,0 +1,13 @@ + + + + + + + + + + \ No newline at end of file diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseTest.java new file mode 100644 index 00000000..7f505479 --- /dev/null +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseTest.java @@ -0,0 +1,41 @@ +package com.otaliastudios.cameraview; + + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.support.test.InstrumentationRegistry; +import android.support.test.annotation.UiThreadTest; +import android.support.test.rule.ActivityTestRule; +import android.view.View; + +import org.junit.Before; +import org.junit.Rule; + +public class BaseTest { + + public static void ui(Runnable runnable) { + InstrumentationRegistry.getInstrumentation().runOnMainSync(runnable); + } + + public static void uiAsync(Runnable runnable) { + new Handler(Looper.getMainLooper()).post(runnable); + } + + public static Context context() { + return InstrumentationRegistry.getInstrumentation().getContext(); + } + + public static void uiRequestLayout(final View view) { + ui(new Runnable() { + @Override + public void run() { + view.requestLayout(); + } + }); + } + + public static void waitUi() { + InstrumentationRegistry.getInstrumentation().waitForIdleSync(); + } +} diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraUtilsTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraUtilsTest.java new file mode 100644 index 00000000..92115c96 --- /dev/null +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraUtilsTest.java @@ -0,0 +1,60 @@ +package com.otaliastudios.cameraview; + + +import android.annotation.TargetApi; +import android.app.Instrumentation; +import android.content.Context; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SmallTest; +import android.support.test.internal.runner.InstrumentationConnection; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.ByteArrayOutputStream; + +import static org.mockito.Mockito.*; +import static org.junit.Assert.*; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class CameraUtilsTest { + + @Test + public void testHasCameras() { + Context context = mock(Context.class); + PackageManager pm = mock(PackageManager.class); + when(context.getPackageManager()).thenReturn(pm); + when(pm.hasSystemFeature(anyString())).thenReturn(true); + assertTrue(CameraUtils.hasCameras(context)); + + when(pm.hasSystemFeature(anyString())).thenReturn(false); + assertFalse(CameraUtils.hasCameras(context)); + } + + @Test + public void testDecodeBitmap() { + int w = 100, h = 200, color = Color.WHITE; + Bitmap source = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); + source.setPixel(0, 0, color); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + + // Using lossy JPG we can't have strict comparison of values after compression. + source.compress(Bitmap.CompressFormat.PNG, 100, os); + + // No orientation. + Bitmap other = CameraUtils.decodeBitmap(os.toByteArray()); + assertEquals(100, w); + assertEquals(200, h); + assertEquals(color, other.getPixel(0, 0)); + assertEquals(0, other.getPixel(0, h-1)); + assertEquals(0, other.getPixel(w-1, 0)); + assertEquals(0, other.getPixel(w-1, h-1)); + + // TODO: improve when we add EXIF writing to byte arrays + } +} diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java index 4e8c94d5..9e2f645d 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java @@ -4,10 +4,12 @@ package com.otaliastudios.cameraview; import android.app.Instrumentation; import android.content.Context; import android.hardware.Camera; +import android.location.Location; import android.os.Looper; import android.support.test.InstrumentationRegistry; import android.support.test.filters.MediumTest; import android.support.test.runner.AndroidJUnit4; +import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; @@ -25,15 +27,17 @@ import static android.view.ViewGroup.LayoutParams.*; @RunWith(AndroidJUnit4.class) @MediumTest -public class CameraViewTest { +public class CameraViewTest extends BaseTest { private CameraView cameraView; private MockCameraController mockController; private Preview mockPreview; + private boolean hasPermissions; + @Before public void setUp() { - InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { + ui(new Runnable() { @Override public void run() { Context context = InstrumentationRegistry.getContext(); @@ -49,6 +53,11 @@ public class CameraViewTest { mockPreview = new MockPreview(context, container); return mockPreview; } + + @Override + protected boolean checkPermissions(SessionType sessionType) { + return hasPermissions; + } }; } }); @@ -59,12 +68,13 @@ public class CameraViewTest { cameraView = null; mockController = null; mockPreview = null; + hasPermissions = false; } //region testDefaults @Test - public void testNotStarted() { + public void testNullBeforeStart() { assertFalse(cameraView.isStarted()); assertNull(cameraView.getCameraOptions()); assertNull(cameraView.getExtraProperties()); @@ -96,6 +106,23 @@ public class CameraViewTest { assertEquals(cameraView.getGestureAction(Gesture.SCROLL_VERTICAL), GestureAction.DEFAULT_SCROLL_VERTICAL); } + @Test + public void testStartWithPermissions() { + hasPermissions = true; + cameraView.start(); + assertTrue(cameraView.isStarted()); + + cameraView.stop(); + assertFalse(cameraView.isStarted()); + } + + @Test + public void testStartWithoutPermissions() { + hasPermissions = false; + cameraView.start(); + assertFalse(cameraView.isStarted()); + } + //endregion //region testGesture @@ -145,6 +172,94 @@ public class CameraViewTest { //endregion + //region testGestureAction + + @Test + public void testGestureAction_capture() { + MotionEvent event = MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0); + ui(new Runnable() { + @Override + public void run() { + cameraView.mTapGestureLayout = new TapGestureLayout(cameraView.getContext()) { + public boolean onTouchEvent(MotionEvent event) { return true; } + + }; + cameraView.mTapGestureLayout.setGestureType(Gesture.TAP); + } + }); + cameraView.mapGesture(Gesture.TAP, GestureAction.CAPTURE); + cameraView.dispatchTouchEvent(event); + assertTrue(mockController.mPictureCaptured); + } + + @Test + public void testGestureAction_focus() { + MotionEvent event = MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0); + ui(new Runnable() { + @Override + public void run() { + cameraView.mTapGestureLayout = new TapGestureLayout(cameraView.getContext()) { + public boolean onTouchEvent(MotionEvent event) { return true; } + }; + cameraView.mTapGestureLayout.setGestureType(Gesture.TAP); + } + }); + mockController.mFocusStarted = false; + cameraView.mapGesture(Gesture.TAP, GestureAction.FOCUS); + cameraView.dispatchTouchEvent(event); + assertTrue(mockController.mFocusStarted); + + // Try with FOCUS_WITH_MARKER + mockController.mFocusStarted = false; + cameraView.mapGesture(Gesture.TAP, GestureAction.FOCUS_WITH_MARKER); + cameraView.dispatchTouchEvent(event); + assertTrue(mockController.mFocusStarted); + } + + @Test + public void testGestureAction_zoom() { + MotionEvent event = MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0); + ui(new Runnable() { + @Override + public void run() { + cameraView.mPinchGestureLayout = new PinchGestureLayout(cameraView.getContext()) { + public boolean onTouchEvent(MotionEvent event) { return true; } + }; + cameraView.mPinchGestureLayout.setGestureType(Gesture.PINCH); + } + }); + mockController.mZoomChanged = false; + cameraView.mapGesture(Gesture.PINCH, GestureAction.ZOOM); + cameraView.dispatchTouchEvent(event); + assertTrue(mockController.mZoomChanged); + } + + @Test + public void testGestureAction_exposureCorrection() { + // This needs a valid CameraOptions value. + CameraOptions o = mock(CameraOptions.class); + when(o.getExposureCorrectionMinValue()).thenReturn(-10f); + when(o.getExposureCorrectionMaxValue()).thenReturn(10f); + mockController.setMockCameraOptions(o); + + MotionEvent event = MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0); + ui(new Runnable() { + @Override + public void run() { + cameraView.mScrollGestureLayout = new ScrollGestureLayout(cameraView.getContext()) { + public boolean onTouchEvent(MotionEvent event) { return true; } + }; + cameraView.mScrollGestureLayout.setGestureType(Gesture.SCROLL_HORIZONTAL); + } + }); + mockController.mExposureCorrectionChanged = false; + cameraView.mapGesture(Gesture.SCROLL_HORIZONTAL, GestureAction.EXPOSURE_CORRECTION); + cameraView.dispatchTouchEvent(event); + assertTrue(mockController.mExposureCorrectionChanged); + } + + //endregion + //region testMeasure private void mockPreviewSize() { @@ -268,11 +383,34 @@ public class CameraViewTest { //endregion - // TODO: test setLocation + //region testLocation - // TODO: test touch events + @Test + public void testSetLocation() { + cameraView.setLocation(50d, -50d); + assertEquals(50d, mockController.mLocation.getLatitude(), 0); + assertEquals(-50d, mockController.mLocation.getLongitude(), 0); + assertEquals(0, mockController.mLocation.getAltitude(), 0); + assertEquals("Unknown", mockController.mLocation.getProvider()); + assertEquals(System.currentTimeMillis(), mockController.mLocation.getTime(), 1000f); + + Location source = new Location("Provider"); + source.setTime(5000); + source.setLatitude(10d); + source.setLongitude(-10d); + source.setAltitude(50d); + cameraView.setLocation(source); + Location other = mockController.mLocation; + assertEquals(10d, other.getLatitude(), 0d); + assertEquals(-10d, other.getLongitude(), 0d); + assertEquals(50d, other.getAltitude(), 0d); + assertEquals("Provider", other.getProvider()); + assertEquals(5000, other.getTime()); + } + + //endregion - // TODO: test permissions and start() stop() isStarted() + // TODO: test permissions // TODO: test CameraCallbacks diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CropHelperTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CropHelperTest.java new file mode 100644 index 00000000..993f3bd9 --- /dev/null +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CropHelperTest.java @@ -0,0 +1,92 @@ +package com.otaliastudios.cameraview; + + +import android.content.Context; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.graphics.Rect; +import android.graphics.YuvImage; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class CropHelperTest { + + @Test + public void testCropFromYuv() { + testCropFromYuv(1600, 1600, AspectRatio.of(16, 9)); + testCropFromYuv(1600, 1600, AspectRatio.of(9, 16)); + } + + private void testCropFromYuv(final int w, final int h, final AspectRatio target) { + final boolean wider = target.toFloat() > ((float) w / (float) h); + + // Not sure how to test YuvImages... + YuvImage i = mock(YuvImage.class); + when(i.getWidth()).thenReturn(w); + when(i.getHeight()).thenReturn(h); + when(i.compressToJpeg(any(Rect.class), anyInt(), any(OutputStream.class))).thenAnswer(new Answer() { + @Override + public Boolean answer(InvocationOnMock iom) throws Throwable { + Object[] args = iom.getArguments(); + Rect rect = (Rect) args[0]; + + // Assert. + AspectRatio ratio = AspectRatio.of(rect.width(), rect.height()); + assertEquals(target, ratio); + if (wider) { // width must match. + assertEquals(rect.width(), w); + } else { + assertEquals(rect.height(), h); + } + return true; + } + }); + CropHelper.cropToJpeg(i, target, 100); + } + + @Test + public void testCropFromJpeg() { + testCropFromJpeg(1600, 1600, AspectRatio.of(16, 9)); + testCropFromJpeg(1600, 1600, AspectRatio.of(9, 16)); + } + + private void testCropFromJpeg(int w, int h, AspectRatio target) { + final boolean wider = target.toFloat() > ((float) w / (float) h); + + Bitmap source = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); + ByteArrayOutputStream os = new ByteArrayOutputStream(); + source.compress(Bitmap.CompressFormat.JPEG, 100, os); + byte[] b = CropHelper.cropToJpeg(os.toByteArray(), target, 100); + Bitmap result = BitmapFactory.decodeByteArray(b, 0, b.length); + + // Assert. + AspectRatio ratio = AspectRatio.of(result.getWidth(), result.getHeight()); + assertEquals(target, ratio); + if (wider) { // width must match. + assertEquals(result.getWidth(), w); + } else { + assertEquals(result.getHeight(), h); + } + } +} diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/GestureLayoutTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/GestureLayoutTest.java new file mode 100644 index 00000000..e6c2a5cf --- /dev/null +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/GestureLayoutTest.java @@ -0,0 +1,62 @@ +package com.otaliastudios.cameraview; + + +import android.annotation.TargetApi; +import android.content.Context; +import android.support.test.espresso.Espresso; +import android.support.test.espresso.Root; +import android.support.test.espresso.ViewAssertion; +import android.support.test.espresso.ViewInteraction; +import android.support.test.espresso.assertion.ViewAssertions; +import android.support.test.espresso.matcher.ViewMatchers; +import android.support.test.rule.ActivityTestRule; +import android.view.MotionEvent; +import android.view.View; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Rule; + +import static android.support.test.espresso.Espresso.onView; +import static org.hamcrest.Matchers.any; + +@TargetApi(17) +public abstract class GestureLayoutTest extends BaseTest { + + protected abstract T create(Context context); + + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(TestActivity.class); + + protected T layout; + protected Task touch; + + @Before + public void setUp() { + ui(new Runnable() { + @Override + public void run() { + TestActivity a = rule.getActivity(); + layout = create(a); + layout.enable(true); + a.inflate(layout); + + touch = new Task<>(); + layout.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View view, MotionEvent motionEvent) { + boolean found = layout.onTouchEvent(motionEvent); + if (found) touch.end(layout.getGestureType()); + return true; + } + }); + } + }); + } + + protected final ViewInteraction onLayout() { + return onView(Matchers.is(layout)).inRoot(any(Root.class)); + } +} diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/GridLinesLayoutTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/GridLinesLayoutTest.java new file mode 100644 index 00000000..8869ce10 --- /dev/null +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/GridLinesLayoutTest.java @@ -0,0 +1,78 @@ +package com.otaliastudios.cameraview; + + +import android.graphics.Canvas; +import android.graphics.drawable.ColorDrawable; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.MediumTest; +import android.support.test.rule.ActivityTestRule; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.mockito.Mockito.*; + +@RunWith(AndroidJUnit4.class) +@MediumTest +public class GridLinesLayoutTest extends BaseTest { + + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(TestActivity.class); + + private GridLinesLayout layout; + + @Before + public void setUp() { + ui(new Runnable() { + @Override + public void run() { + TestActivity a = rule.getActivity(); + layout = new GridLinesLayout(a); + layout.setGridMode(Grid.OFF); + a.getContentView().addView(layout); + layout.vert = mock(ColorDrawable.class); + layout.horiz = mock(ColorDrawable.class); + } + }); + } + + @Test + public void testOff() { + layout.drawTask.listen(); + layout.setGridMode(Grid.OFF); + layout.drawTask.await(); + verify(layout.vert, never()).draw(any(Canvas.class)); + verify(layout.horiz, never()).draw(any(Canvas.class)); + } + + @Test + public void test3x3() { + layout.drawTask.listen(); + layout.setGridMode(Grid.DRAW_3X3); + layout.drawTask.await(); + verify(layout.vert, times(2)).draw(any(Canvas.class)); + verify(layout.horiz, times(2)).draw(any(Canvas.class)); + } + + @Test + public void testPhi() { + layout.drawTask.listen(); + layout.setGridMode(Grid.DRAW_PHI); + layout.drawTask.await(); + verify(layout.vert, times(2)).draw(any(Canvas.class)); + verify(layout.horiz, times(2)).draw(any(Canvas.class)); + } + + @Test + public void test4x4() { + layout.drawTask.listen(); + layout.setGridMode(Grid.DRAW_4X4); + layout.drawTask.await(); + verify(layout.vert, times(3)).draw(any(Canvas.class)); + verify(layout.horiz, times(3)).draw(any(Canvas.class)); + } + +} diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java index 113bc7ea..ce150fd2 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java @@ -10,6 +10,12 @@ import java.io.File; public class MockCameraController extends CameraController { + Location mLocation; + boolean mPictureCaptured; + boolean mFocusStarted; + boolean mZoomChanged; + boolean mExposureCorrectionChanged; + MockCameraController(CameraView.CameraCallbacks callback, Preview preview) { super(callback, preview); } @@ -24,21 +30,21 @@ public class MockCameraController extends CameraController { @Override void onStart() { - } @Override void onStop() { - } @Override boolean setZoom(float zoom) { + mZoomChanged = true; return true; } @Override boolean setExposureCorrection(float EVvalue) { + mExposureCorrectionChanged = true; return true; } @@ -74,11 +80,12 @@ public class MockCameraController extends CameraController { @Override void setLocation(Location location) { - + mLocation = location; } @Override boolean capturePicture() { + mPictureCaptured = true; return true; } @@ -109,6 +116,7 @@ public class MockCameraController extends CameraController { @Override boolean startAutoFocus(@Nullable Gesture gesture, PointF point) { + mFocusStarted = true; return true; } diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockPreview.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockPreview.java index eb2b1b8c..e011c617 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockPreview.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockPreview.java @@ -2,33 +2,36 @@ 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) { - super(context, parent); + MockPreview(Context context, ViewGroup parent) { + super(context, parent, null); } + @NonNull @Override - Surface getSurface() { - return null; + protected View onCreateView(Context context, ViewGroup parent) { + return new View(context); } @Override - View getView() { + Surface getSurface() { return null; } @Override - Class getOutputClass() { + Class getOutputClass() { return null; } @Override - boolean isReady() { - return true; + Void getOutput() { + return null; } + } diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/OrientationHelperTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/OrientationHelperTest.java new file mode 100644 index 00000000..19a6aa83 --- /dev/null +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/OrientationHelperTest.java @@ -0,0 +1,97 @@ +package com.otaliastudios.cameraview; + + +import android.annotation.TargetApi; +import android.app.Instrumentation; +import android.app.UiAutomation; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; +import android.view.OrientationEventListener; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class OrientationHelperTest extends BaseTest { + + private OrientationHelper helper; + private OrientationHelper.Callbacks callbacks; + + @Before + public void setUp() { + ui(new Runnable() { + @Override + public void run() { + callbacks = mock(OrientationHelper.Callbacks.class); + helper = new OrientationHelper(context(), callbacks); + } + }); + } + + @After + public void tearDown() { + callbacks = null; + helper = null; + } + + @Test + public void testEnable() { + assertNotNull(helper.mListener); + assertNull(helper.mDisplay); + + helper.enable(context()); + assertNotNull(helper.mListener); + assertNotNull(helper.mDisplay); + + // Ensure nothing bad if called twice. + helper.enable(context()); + assertNotNull(helper.mListener); + assertNotNull(helper.mDisplay); + + helper.disable(); + assertNotNull(helper.mListener); + assertNull(helper.mDisplay); + + verify(callbacks, atLeastOnce()).onDisplayOffsetChanged(anyInt()); + } + + @Test + public void testRotation() { + helper.enable(context()); + + reset(callbacks); // Reset counts. + helper.mListener.onOrientationChanged(OrientationEventListener.ORIENTATION_UNKNOWN); + assertEquals(helper.mLastOrientation, 0); + helper.mListener.onOrientationChanged(10); + assertEquals(helper.mLastOrientation, 0); + helper.mListener.onOrientationChanged(-10); + assertEquals(helper.mLastOrientation, 0); + helper.mListener.onOrientationChanged(44); + assertEquals(helper.mLastOrientation, 0); + helper.mListener.onOrientationChanged(360); + assertEquals(helper.mLastOrientation, 0); + + // Callback called just once. + verify(callbacks, times(1)).onDeviceOrientationChanged(0); + + helper.mListener.onOrientationChanged(90); + helper.mListener.onOrientationChanged(91); + assertEquals(helper.mLastOrientation, 90); + verify(callbacks, times(1)).onDeviceOrientationChanged(90); + + helper.mListener.onOrientationChanged(180); + assertEquals(helper.mLastOrientation, 180); + verify(callbacks, times(1)).onDeviceOrientationChanged(180); + + helper.mListener.onOrientationChanged(270); + assertEquals(helper.mLastOrientation, 270); + verify(callbacks, times(1)).onDeviceOrientationChanged(270); + } +} diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/PinchGestureLayoutTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/PinchGestureLayoutTest.java new file mode 100644 index 00000000..d1af6e1d --- /dev/null +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/PinchGestureLayoutTest.java @@ -0,0 +1,86 @@ +package com.otaliastudios.cameraview; + + +import android.content.Context; +import android.support.test.espresso.Espresso; +import android.support.test.espresso.UiController; +import android.support.test.espresso.ViewAction; +import android.support.test.espresso.action.CoordinatesProvider; +import android.support.test.espresso.action.GeneralLocation; +import android.support.test.espresso.action.GeneralSwipeAction; +import android.support.test.espresso.action.MotionEvents; +import android.support.test.espresso.action.PrecisionDescriber; +import android.support.test.espresso.action.Press; +import android.support.test.espresso.action.Swipe; +import android.support.test.espresso.action.ViewActions; +import android.support.test.espresso.assertion.ViewAssertions; +import android.support.test.espresso.matcher.ViewMatchers; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; +import android.view.View; + +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.TimeUnit; + +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class PinchGestureLayoutTest extends GestureLayoutTest { + + @Override + protected PinchGestureLayout create(Context context) { + return new PinchGestureLayout(context); + } + + @Test + public void testDefaults() { + assertEquals(layout.getGestureType(), Gesture.PINCH); + assertEquals(layout.getPoints().length, 2); + assertEquals(layout.getPoints()[0].x, 0, 0); + assertEquals(layout.getPoints()[0].y, 0, 0); + assertEquals(layout.getPoints()[1].x, 0, 0); + assertEquals(layout.getPoints()[1].y, 0, 0); + } + + // TODO: test pinch open + // TODO: test pinch close + // TODO: test pinch disabled + + // Possible approach: mimic pinch gesture and let espresso test. + // Too lazy to do this now, but it's possible. + // https://stackoverflow.com/questions/11523423/how-to-generate-zoom-pinch-gesture-for-testing-for-android + + public abstract class PinchViewAction implements ViewAction { + } + + private void testPinch(ViewAction action, boolean increasing) { + touch.listen(); + touch.start(); + onLayout().perform(action); + Gesture found = touch.await(10000, TimeUnit.MILLISECONDS); + assertNotNull(found); + + // How will this move our parameter? + float curr = 0.5f, min = 0f, max = 1f; + float newValue = layout.scaleValue(curr, min, max); + if (increasing) { + assertTrue(newValue > curr); + assertTrue(newValue <= max); + } else { + assertTrue(newValue < curr); + assertTrue(newValue >= min); + } + } +} 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..8fbad240 --- /dev/null +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/PreviewTest.java @@ -0,0 +1,157 @@ +package com.otaliastudios.cameraview; + + +import android.content.Context; +import android.support.test.rule.ActivityTestRule; +import android.view.ViewGroup; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + + +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.*; + +import static org.mockito.Mockito.*; + +public abstract class PreviewTest extends BaseTest { + + protected abstract Preview createPreview(Context context, ViewGroup parent, Preview.SurfaceCallback callback); + + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(TestActivity.class); + + protected Preview preview; + protected Size surfaceSize; + private Preview.SurfaceCallback callback; + private Task availability; + + @Before + public void setUp() { + availability = new Task<>(); + availability.listen(); + + ui(new Runnable() { + @Override + public void run() { + TestActivity a = rule.getActivity(); + surfaceSize = a.getContentSize(); + + callback = mock(Preview.SurfaceCallback.class); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + if (availability != null) availability.end(true); + return null; + } + }).when(callback).onSurfaceAvailable(); + preview = createPreview(a, a.getContentView(), callback); + } + }); + } + + // Wait for surface to be available. + protected void ensureAvailable() { + assertNotNull(availability.await(2, TimeUnit.SECONDS)); + } + + // Trigger a destroy. + protected void ensureDestroyed() { + ui(new Runnable() { + @Override + public void run() { + rule.getActivity().getContentView().removeView(preview.getView()); + } + }); + waitUi(); + } + + @After + public void tearDown() { + preview = null; + callback = null; + surfaceSize = null; + availability = null; + } + + @Test + public void testDefaults() { + ensureAvailable(); + assertTrue(preview.isReady()); + assertNotNull(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 testSurfaceAvailable() { + ensureAvailable(); + verify(callback, times(1)).onSurfaceAvailable(); + assertEquals(surfaceSize.getWidth(), preview.getSurfaceSize().getWidth()); + assertEquals(surfaceSize.getHeight(), preview.getSurfaceSize().getHeight()); + } + + @Test + public void testSurfaceDestroyed() { + ensureAvailable(); + ensureDestroyed(); + assertEquals(0, preview.getSurfaceSize().getWidth()); + assertEquals(0, preview.getSurfaceSize().getHeight()); + } + + @Test + public void testCropCenter() throws Exception { + ensureAvailable(); + + // This is given by the activity, it's the fixed size. + float view = getViewAspectRatio(); + + // If we apply a desired size with same aspect ratio, there should be no crop. + setDesiredAspectRatio(view); + assertFalse(preview.isCropping()); + + // If we apply a different aspect ratio, there should be cropping. + float desired = view * 1.2f; + setDesiredAspectRatio(desired); + assertTrue(preview.isCropping()); + + // Since desired is 'desired', let's fake a new view size that is consistent with it. + // Ensure crop is not happening anymore. + preview.mCropTask.listen(); + preview.onSurfaceSizeChanged((int) (50f * desired), 50); // Wait... + preview.mCropTask.await(); + assertEquals(desired, getViewAspectRatioWithScale(), 0.01f); + assertFalse(preview.isCropping()); + } + + private void setDesiredAspectRatio(float desiredAspectRatio) { + preview.mCropTask.listen(); + preview.setDesiredSize((int) (10f * desiredAspectRatio), 10); // Wait... + preview.mCropTask.await(); + assertEquals(desiredAspectRatio, getViewAspectRatioWithScale(), 0.01f); + + } + + private float getViewAspectRatio() { + Size size = preview.getSurfaceSize(); + return AspectRatio.of(size.getWidth(), size.getHeight()).toFloat(); + } + + private float getViewAspectRatioWithScale() { + 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).toFloat(); + } +} diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/ScrollGestureLayoutTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/ScrollGestureLayoutTest.java new file mode 100644 index 00000000..ad5e8433 --- /dev/null +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/ScrollGestureLayoutTest.java @@ -0,0 +1,97 @@ +package com.otaliastudios.cameraview; + + +import android.content.Context; +import android.support.test.espresso.ViewAction; +import android.support.test.espresso.assertion.ViewAssertions; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import org.hamcrest.Matchers; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.TimeUnit; + +import static android.support.test.espresso.Espresso.onView; +import static android.support.test.espresso.action.ViewActions.click; +import static android.support.test.espresso.action.ViewActions.swipeDown; +import static android.support.test.espresso.action.ViewActions.swipeLeft; +import static android.support.test.espresso.action.ViewActions.swipeRight; +import static android.support.test.espresso.action.ViewActions.swipeUp; +import static android.support.test.espresso.matcher.ViewMatchers.withId; +import static junit.framework.Assert.assertNotNull; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class ScrollGestureLayoutTest extends GestureLayoutTest { + + @Override + protected ScrollGestureLayout create(Context context) { + return new ScrollGestureLayout(context); + } + + @Test + public void testDefaults() { + assertNull(layout.getGestureType()); + assertEquals(layout.getPoints().length, 2); + assertEquals(layout.getPoints()[0].x, 0, 0); + assertEquals(layout.getPoints()[0].y, 0, 0); + assertEquals(layout.getPoints()[1].x, 0, 0); + assertEquals(layout.getPoints()[1].y, 0, 0); + } + + @Test + public void testScrollDisabled() { + layout.enable(false); + touch.listen(); + touch.start(); + onLayout().perform(swipeUp()); + Gesture found = touch.await(500, TimeUnit.MILLISECONDS); + assertNull(found); + } + + private void testScroll(ViewAction scroll, Gesture expected, boolean increasing) { + touch.listen(); + touch.start(); + onLayout().perform(scroll); + Gesture found = touch.await(500, TimeUnit.MILLISECONDS); + assertEquals(found, expected); + + // How will this move our parameter? + float curr = 0.5f, min = 0f, max = 1f; + float newValue = layout.scaleValue(curr, min, max); + if (increasing) { + assertTrue(newValue > curr); + assertTrue(newValue <= max); + } else { + assertTrue(newValue < curr); + assertTrue(newValue >= min); + } + } + + @Test + public void testScrollLeft() { + testScroll(swipeLeft(), Gesture.SCROLL_HORIZONTAL, false); + } + + @Test + public void testScrollRight() { + testScroll(swipeRight(), Gesture.SCROLL_HORIZONTAL, true); + } + + @Test + public void testScrollUp() { + testScroll(swipeUp(), Gesture.SCROLL_VERTICAL, true); + } + + @Test + public void testScrollDown() { + testScroll(swipeDown(), Gesture.SCROLL_VERTICAL, false); + } + + +} 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..afa18eb2 --- /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, Preview.SurfaceCallback callback) { + return new SurfaceViewPreview(context, parent, callback); + } +} diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/TapGestureLayoutTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/TapGestureLayoutTest.java new file mode 100644 index 00000000..cf976b5f --- /dev/null +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/TapGestureLayoutTest.java @@ -0,0 +1,81 @@ +package com.otaliastudios.cameraview; + + +import android.content.Context; +import android.support.test.espresso.action.GeneralClickAction; +import android.support.test.espresso.action.GeneralLocation; +import android.support.test.espresso.action.Press; +import android.support.test.espresso.action.Tap; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; +import android.view.InputDevice; +import android.view.MotionEvent; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.TimeUnit; + +import static android.support.test.espresso.Espresso.*; +import static android.support.test.espresso.matcher.ViewMatchers.*; +import static android.support.test.espresso.action.ViewActions.*; +import static org.junit.Assert.*; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class TapGestureLayoutTest extends GestureLayoutTest { + + @Override + protected TapGestureLayout create(Context context) { + return new TapGestureLayout(context); + } + + @Test + public void testDefaults() { + assertNull(layout.getGestureType()); + assertEquals(layout.getPoints().length, 1); + assertEquals(layout.getPoints()[0].x, 0, 0); + assertEquals(layout.getPoints()[0].y, 0, 0); + } + + @Test + public void testTap() { + touch.listen(); + touch.start(); + GeneralClickAction a = new GeneralClickAction( + Tap.SINGLE, GeneralLocation.CENTER, Press.FINGER, + InputDevice.SOURCE_UNKNOWN, MotionEvent.BUTTON_PRIMARY); + onLayout().perform(a); + Gesture found = touch.await(500, TimeUnit.MILLISECONDS); + + assertEquals(found, Gesture.TAP); + Size size = rule.getActivity().getContentSize(); + assertEquals(layout.getPoints()[0].x, (size.getWidth() / 2f), 1f); + assertEquals(layout.getPoints()[0].y, (size.getHeight() / 2f), 1f); + } + + @Test + public void testTapWhileDisabled() { + layout.enable(false); + touch.listen(); + touch.start(); + onLayout().perform(click()); + Gesture found = touch.await(500, TimeUnit.MILLISECONDS); + assertNull(found); + } + + @Test + public void testLongTap() { + touch.listen(); + touch.start(); + GeneralClickAction a = new GeneralClickAction( + Tap.LONG, GeneralLocation.CENTER, Press.FINGER, + InputDevice.SOURCE_UNKNOWN, MotionEvent.BUTTON_PRIMARY); + onLayout().perform(a); + Gesture found = touch.await(500, TimeUnit.MILLISECONDS); + assertEquals(found, Gesture.LONG_TAP); + Size size = rule.getActivity().getContentSize(); + assertEquals(layout.getPoints()[0].x, (size.getWidth() / 2f), 1f); + assertEquals(layout.getPoints()[0].y, (size.getHeight() / 2f), 1f); + } +} 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..92c9c69b --- /dev/null +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/TestActivity.java @@ -0,0 +1,75 @@ +package com.otaliastudios.cameraview; + + +import android.app.Activity; +import android.app.KeyguardManager; +import android.content.Context; +import android.graphics.Point; +import android.os.Bundle; +import android.os.PersistableBundle; +import android.support.annotation.Nullable; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.FrameLayout; + +import static android.view.ViewGroup.LayoutParams.*; + +public class TestActivity extends Activity { + + private ViewGroup content; + private Size contentSize = new Size(1000, 1000); + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + wakeScreen(); + + // Match parent decor view. + FrameLayout root = new FrameLayout(this); + root.setKeepScreenOn(true); + root.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); + + // Inner content view with fixed size. + // We want it to be fully visible or expresso will crash. + Point size = new Point(); + getWindowManager().getDefaultDisplay().getSize(size); + int width = Math.min(size.x, size.y); + int height = Math.min(size.x, size.y); + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( + width, height, Gravity.CENTER); + content = new FrameLayout(this); + content.setLayoutParams(params); + contentSize = new Size(width, height); + + // Add. + root.addView(content); + setContentView(root); + } + + public void wakeScreen() { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED + | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD + | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON + | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + | WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON); + } + + public Size getContentSize() { + return contentSize; + } + + public ViewGroup getContentView() { + return content; + } + + public void inflate(View child) { + inflate(child, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); + } + + public void inflate(View child, ViewGroup.LayoutParams params) { + content.addView(child, params); + content.requestLayout(); + } +} 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..39391063 --- /dev/null +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/TextureViewPreviewTest.java @@ -0,0 +1,43 @@ +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, Preview.SurfaceCallback callback) { + return new TextureViewPreview(context, parent, callback); + } + + @Override + protected void ensureAvailable() { + if (isHardwareAccelerated()) { + super.ensureAvailable(); + } else { + preview.onSurfaceAvailable( + surfaceSize.getWidth(), + surfaceSize.getHeight()); + } + } + + @Override + protected void ensureDestroyed() { + super.ensureDestroyed(); + if (!isHardwareAccelerated()) { + // Ensure it is called. + preview.onSurfaceDestroyed(); + } + } + + private boolean isHardwareAccelerated() { + return preview.getView().isHardwareAccelerated(); + } +} 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/java/com/otaliastudios/cameraview/CameraView.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java index 4e4de4ea..e79066a0 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java @@ -159,7 +159,7 @@ public class CameraView extends FrameLayout { protected Preview instantiatePreview(Context context, ViewGroup container) { - return new TextureViewPreview(context, container); + return new TextureViewPreview(context, container, null); } @@ -504,7 +504,7 @@ public class CameraView extends FrameLayout { * @return true if we can go on, false otherwise. */ @SuppressLint("NewApi") - private boolean checkPermissions(SessionType sessionType) { + protected boolean checkPermissions(SessionType sessionType) { checkPermissionsManifestOrThrow(sessionType); boolean api23 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; int cameraCheck, audioCheck; @@ -537,7 +537,7 @@ public class CameraView extends FrameLayout { /** * If mSessionType == SESSION_TYPE_VIDEO we will ask for RECORD_AUDIO permission. * If the developer did not add this to its manifest, throw and fire warnings. - * (Hoping this is not cought elsewhere... we should test). + * (Hoping this is not caught elsewhere... we should test). */ private void checkPermissionsManifestOrThrow(SessionType sessionType) { if (sessionType == SessionType.VIDEO) { diff --git a/cameraview/src/main/res/layout/surface_view.xml b/cameraview/src/main/res/layout/surface_view.xml index 42fb3f23..3db4f4ea 100644 --- a/cameraview/src/main/res/layout/surface_view.xml +++ b/cameraview/src/main/res/layout/surface_view.xml @@ -3,8 +3,8 @@ + android:layout_height="match_parent" + android:gravity="center" /> diff --git a/cameraview/src/main/utils/com/otaliastudios/cameraview/OrientationHelper.java b/cameraview/src/main/utils/com/otaliastudios/cameraview/OrientationHelper.java index 53f9aaea..ba63b7f2 100644 --- a/cameraview/src/main/utils/com/otaliastudios/cameraview/OrientationHelper.java +++ b/cameraview/src/main/utils/com/otaliastudios/cameraview/OrientationHelper.java @@ -19,11 +19,12 @@ class OrientationHelper { DISPLAY_ORIENTATIONS.put(Surface.ROTATION_270, 270); } - private final OrientationEventListener mListener; + final OrientationEventListener mListener; + Display mDisplay; + private final Callbacks mCallbacks; - private Display mDisplay; - private int mLastKnownDisplayOffset = -1; - private int mLastOrientation = -1; + int mLastKnownDisplayOffset = -1; + int mLastOrientation = -1; interface Callbacks { void onDisplayOffsetChanged(int displayOffset); diff --git a/cameraview/src/main/utils/com/otaliastudios/cameraview/Task.java b/cameraview/src/main/utils/com/otaliastudios/cameraview/Task.java new file mode 100644 index 00000000..16ef3150 --- /dev/null +++ b/cameraview/src/main/utils/com/otaliastudios/cameraview/Task.java @@ -0,0 +1,72 @@ +package com.otaliastudios.cameraview; + +import android.support.annotation.NonNull; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * A naive implementation of {@link java.util.concurrent.CountDownLatch} + * to help in testing. + */ +class Task { + + private CountDownLatch mLatch; + private T mResult; + private int mCount; + + Task() { + } + + private boolean listening() { + return mLatch != null; + } + + void listen() { + if (listening()) throw new RuntimeException("Should not happen."); + mResult = null; + mLatch = new CountDownLatch(1); + } + + void start() { + if (!listening()) mCount++; + } + + void end(T result) { + if (mCount > 0) { + mCount--; + return; + } + + if (listening()) { // Should be always true. + mResult = result; + mLatch.countDown(); + } + } + + T await() { + try { + mLatch.await(); + } catch (Exception e) { + e.printStackTrace(); + } + T result = mResult; + mResult = null; + mLatch = null; + return result; + } + + T await(long time, @NonNull TimeUnit unit) { + try { + mLatch.await(time, unit); + } catch (Exception e) { + e.printStackTrace(); + } + T result = mResult; + mResult = null; + mLatch = null; + return result; + } + + +} diff --git a/cameraview/src/main/views/com/otaliastudios/cameraview/GestureLayout.java b/cameraview/src/main/views/com/otaliastudios/cameraview/GestureLayout.java index 5553b2da..6f1fcc36 100644 --- a/cameraview/src/main/views/com/otaliastudios/cameraview/GestureLayout.java +++ b/cameraview/src/main/views/com/otaliastudios/cameraview/GestureLayout.java @@ -39,6 +39,11 @@ abstract class GestureLayout extends FrameLayout { return mType; } + // For tests. + void setGestureType(Gesture type) { + mType = type; + } + public final PointF[] getPoints() { return mPoints; } diff --git a/cameraview/src/main/views/com/otaliastudios/cameraview/GridLinesLayout.java b/cameraview/src/main/views/com/otaliastudios/cameraview/GridLinesLayout.java index d8f16e4f..0d7784ea 100644 --- a/cameraview/src/main/views/com/otaliastudios/cameraview/GridLinesLayout.java +++ b/cameraview/src/main/views/com/otaliastudios/cameraview/GridLinesLayout.java @@ -16,10 +16,12 @@ class GridLinesLayout extends View { private Grid gridMode; - private final Drawable horiz; - private final Drawable vert; + Drawable horiz; + Drawable vert; private final float width; + Task drawTask = new Task<>(); + private final static float GOLDEN_RATIO_INV = 0.61803398874989f; public GridLinesLayout(@NonNull Context context) { @@ -46,7 +48,7 @@ class GridLinesLayout extends View { public void setGridMode(Grid gridMode) { this.gridMode = gridMode; - invalidate(); + postInvalidate(); } private int getLineCount() { @@ -75,6 +77,7 @@ class GridLinesLayout extends View { @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); + drawTask.start(); int count = getLineCount(); for (int n = 0; n < count; n++) { float pos = getLinePosition(n); @@ -89,5 +92,6 @@ class GridLinesLayout extends View { vert.draw(canvas); canvas.translate(- pos * getWidth(), 0); } + drawTask.end(null); } } diff --git a/cameraview/src/main/views/com/otaliastudios/cameraview/PinchGestureLayout.java b/cameraview/src/main/views/com/otaliastudios/cameraview/PinchGestureLayout.java index 574c5e5a..08c0cd62 100644 --- a/cameraview/src/main/views/com/otaliastudios/cameraview/PinchGestureLayout.java +++ b/cameraview/src/main/views/com/otaliastudios/cameraview/PinchGestureLayout.java @@ -14,7 +14,7 @@ class PinchGestureLayout extends GestureLayout { private final static float ADD_SENSITIVITY = 2f; - private ScaleGestureDetector mDetector; + ScaleGestureDetector mDetector; private boolean mNotify; private float mAdditionFactor = 0; diff --git a/cameraview/src/main/views/com/otaliastudios/cameraview/Preview.java b/cameraview/src/main/views/com/otaliastudios/cameraview/Preview.java index 6e0c94bb..b8b0e58d 100644 --- a/cameraview/src/main/views/com/otaliastudios/cameraview/Preview.java +++ b/cameraview/src/main/views/com/otaliastudios/cameraview/Preview.java @@ -1,15 +1,17 @@ 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()); + + // Used for testing. + Task mCropTask = new Task<>(); // 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 +21,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 +32,24 @@ abstract class Preview { private int mDesiredWidth; private int mDesiredHeight; - Preview(Context context, ViewGroup parent) {} + Preview(Context context, ViewGroup parent, SurfaceCallback callback) { + mView = onCreateView(context, parent); + mSurfaceCallback = callback; + } + + @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(); // As far as I can see, these are the actual preview dimensions, as set in CameraParameters. // This is called by the CameraImpl. @@ -53,12 +61,20 @@ abstract class Preview { crop(); } + final Size getDesiredSize() { + return new Size(mDesiredWidth, mDesiredHeight); + } + final Size getSurfaceSize() { return new Size(mSurfaceWidth, mSurfaceHeight); } final void setSurfaceCallback(SurfaceCallback callback) { mSurfaceCallback = callback; + // If surface already available, dispatch. + if (mSurfaceWidth != 0 || mSurfaceHeight != 0) { + mSurfaceCallback.onSurfaceAvailable(); + } } @@ -83,13 +99,14 @@ abstract class Preview { } } - protected final void onSurfaceDestroyed() { mSurfaceWidth = 0; mSurfaceHeight = 0; - crop(); } + final boolean isReady() { + return mSurfaceWidth > 0 && mSurfaceHeight > 0; + } /** * Here we must crop the visible part by applying a > 1 scale to one of our @@ -100,11 +117,16 @@ abstract class Preview { * However that should be already managed by the framework. */ private final void crop() { + mCropTask.start(); 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) { + mCropTask.end(null); + return; + } + float scaleX = 1f, scaleY = 1f; AspectRatio current = AspectRatio.of(mSurfaceWidth, mSurfaceHeight); AspectRatio target = AspectRatio.of(mDesiredWidth, mDesiredHeight); @@ -119,6 +141,7 @@ abstract class Preview { getView().setScaleY(scaleY); LOG.i("crop:", "applied scaleX=", scaleX); LOG.i("crop:", "applied scaleY=", scaleY); + mCropTask.end(null); } }); } @@ -130,6 +153,7 @@ abstract class Preview { * @return true if cropping */ final boolean isCropping() { - return getView().getScaleX() > 1 || getView().getScaleY() > 1; + // Account for some error + return getView().getScaleX() > 1.02f || getView().getScaleY() > 1.02f; } } diff --git a/cameraview/src/main/views/com/otaliastudios/cameraview/SurfaceViewPreview.java b/cameraview/src/main/views/com/otaliastudios/cameraview/SurfaceViewPreview.java index 808934ba..ed5e3f60 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(); + SurfaceViewPreview(Context context, ViewGroup parent, SurfaceCallback callback) { + super(context, parent, callback); + } + + @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,22 @@ class SurfaceViewPreview extends Preview { onSurfaceDestroyed(); } }); + return surface; } @Override Surface getSurface() { - return getSurfaceHolder().getSurface(); + return getOutput().getSurface(); } @Override - View getView() { - return mSurfaceView; + SurfaceHolder getOutput() { + return getView().getHolder(); } @Override - SurfaceHolder getSurfaceHolder() { - return mSurfaceView.getHolder(); - } - - @Override - Class getOutputClass() { + Class getOutputClass() { return SurfaceHolder.class; } - @Override - boolean isReady() { - return mSurfaceView.getWidth() != 0 && mSurfaceView.getHeight() != 0; - } - } diff --git a/cameraview/src/main/views/com/otaliastudios/cameraview/TapGestureLayout.java b/cameraview/src/main/views/com/otaliastudios/cameraview/TapGestureLayout.java index 541dd5fd..bf2965cf 100644 --- a/cameraview/src/main/views/com/otaliastudios/cameraview/TapGestureLayout.java +++ b/cameraview/src/main/views/com/otaliastudios/cameraview/TapGestureLayout.java @@ -58,8 +58,8 @@ class TapGestureLayout extends GestureLayout { // Views to draw the focus marker. LayoutInflater.from(getContext()).inflate(R.layout.layout_focus_marker, this); - mFocusMarkerContainer = (FrameLayout) findViewById(R.id.focusMarkerContainer); - mFocusMarkerFill = (ImageView) findViewById(R.id.fill); + mFocusMarkerContainer = findViewById(R.id.focusMarkerContainer); + mFocusMarkerFill = findViewById(R.id.fill); } diff --git a/cameraview/src/main/views/com/otaliastudios/cameraview/TextureViewPreview.java b/cameraview/src/main/views/com/otaliastudios/cameraview/TextureViewPreview.java index 77fd90b0..9bff5d16 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() { + TextureViewPreview(Context context, ViewGroup parent, SurfaceCallback callback) { + super(context, parent, callback); + } + + @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,33 @@ 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; - } - - @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); } } diff --git a/codecov.yml b/codecov.yml index 0da8aaf5..c738a49c 100644 --- a/codecov.yml +++ b/codecov.yml @@ -6,16 +6,12 @@ coverage: status: project: default: - enabled: yes - target: 30% + target: 40% patch: default: - enabled: yes target: 60% - changes: - default: - enabled: no + changes: no comment: - # diff, changes, footer, reach, flags, suggestions, + # diff, changes, footer, reach, flags, suggestions layout: "header, files"