From 41f9f51ef98f6d8fa6afcdde56917c8fa4722d59 Mon Sep 17 00:00:00 2001 From: Mattia Iavarone Date: Mon, 4 Sep 2017 01:56:29 +0200 Subject: [PATCH] Add instrumentation tests and codecov support (#18) * Add instrumentation tests * Run androidTests on api 22 * Fix lint errors * Nits * Add codecov support * Shield --- .travis.yml | 21 +- README.md | 4 + cameraview/build.gradle | 72 +++++ .../cameraview/AspectRatioTest.java | 58 ++++ .../cameraview/CameraOptions1Test.java | 132 +++++++++ .../cameraview/CameraViewTest.java | 279 ++++++++++++++++++ .../cameraview/ExtraProperties1Test.java} | 7 +- .../otaliastudios/cameraview/Mapper1Test.java | 6 + .../cameraview/MockCameraController.java | 124 ++++++++ .../otaliastudios/cameraview/MockPreview.java | 34 +++ .../otaliastudios/cameraview/CameraView.java | 70 ++++- .../cameraview/GestureLayout.java | 4 + 12 files changed, 796 insertions(+), 15 deletions(-) create mode 100644 cameraview/src/androidTest/java/com/otaliastudios/cameraview/AspectRatioTest.java create mode 100644 cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraOptions1Test.java create mode 100644 cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java rename cameraview/src/{test/java/com/otaliastudios/cameraview/ExtraPropertiesTest.java => androidTest/java/com/otaliastudios/cameraview/ExtraProperties1Test.java} (78%) rename cameraview/src/{test => androidTest}/java/com/otaliastudios/cameraview/Mapper1Test.java (94%) create mode 100644 cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java create mode 100644 cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockPreview.java diff --git a/.travis.yml b/.travis.yml index d5cce1f7..410e1a0f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,12 @@ sudo: false jdk: - oraclejdk8 +env: + global: + # Run android tests on api level 22 + - EMULATOR_API=22 + - EMULATOR_ABI=armeabi-v7a + android: components: - tools @@ -17,9 +23,22 @@ android: - build-tools-26.0.1 - android-26 - doc-26 + # Android tests + - android-$EMULATOR_API + - sys-img-$EMULATOR_ABI-android-$EMULATOR_API + # sys-img-x86-google_apis-26 + +before_script: + - echo no | android create avd --force --name test --target android-$EMULATOR_API --abi $EMULATOR_ABI + - emulator -avd test -no-audio -no-window & + - android-wait-for-emulator + - adb shell input keyevent 82 & script: - - ./gradlew clean connectedCheck testDebugUnitTest + - ./gradlew clean testDebugUnitTest connectedCheck coverageReport + +after_success: + - bash <(curl -s https://codecov.io/bash) cache: directories: diff --git a/README.md b/README.md index a5aad9b9..6997c48c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ +[![Build Status](https://travis-ci.org/natario1/CameraView.svg?branch=master)](https://travis-ci.org/natario1/CameraView) +[![Code Coverage](https://codecov.io/gh/natario1/CameraView/branch/master/graph/badge.svg)](https://codecov.io/gh/natario1/CameraView) +

@@ -409,6 +412,7 @@ Other APIs not mentioned above are provided, and are well documented and comment |------|-----------| |`isStarted()`|Returns true if `start()` was called succesfully. This does not mean that camera is open or showing preview.| |`mapGesture(Gesture, GestureAction)`|Maps a certain gesture to a certain action. No-op if the action is not supported.| +|`getGestureAction(Gesture)`|Returns the action currently mapped to the given gesture.| |`clearGesture(Gesture)`|Clears any action mapped to the given gesture.| |`getCameraOptions()`|If camera was started, returns non-null object with information about what is supported.| |`getExtraProperties()`|If camera was started, returns non-null object with extra information about the camera sensor. Not very useful at the moment.| diff --git a/cameraview/build.gradle b/cameraview/build.gradle index f7f2d0de..7face5d3 100644 --- a/cameraview/build.gradle +++ b/cameraview/build.gradle @@ -6,6 +6,8 @@ apply plugin: 'com.jfrog.bintray' version = '1.2.0' group = 'com.otaliastudios' +//region android dependencies + android { compileSdkVersion rootProject.ext.compileSdkVersion buildToolsVersion rootProject.ext.buildToolsVersion @@ -15,8 +17,14 @@ android { targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 versionName project.version + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } + buildTypes { + debug { + testCoverageEnabled true + } + release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' @@ -34,10 +42,18 @@ dependencies { testCompile 'junit:junit:4.12' testCompile 'org.mockito:mockito-core:1.10.19' + androidTestCompile 'com.android.support.test:runner:1.0.1' + androidTestCompile 'com.android.support.test:rules:1.0.1' + androidTestCompile 'com.google.dexmaker:dexmaker:1.2' + androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2' + compile "com.android.support:exifinterface:$supportLibVersion" compile "com.android.support:support-annotations:$supportLibVersion" } +//endregion + +//region bintray install { repositories.mavenInstaller { @@ -104,6 +120,10 @@ bintray { } } +//endregion + +//region javadoc and sources + // From official sample https://github.com/bintray/bintray-examples/blob/master/gradle-aar-example/build.gradle task sourcesJar(type: Jar) { classifier = 'sources' @@ -134,6 +154,58 @@ artifacts { archives sourcesJar } +//endregion + +//region code coverage + +// 1. running androidTests with connectedCheck will generate an .ec file +// in build/outputs/code-coverage/connected, plus the XML result in +// in build/reports/coverage/debug/report.xml . + +// 2. running unit tests with testDebugUnitTest will just generate the .exec file. +// The JacocoReport task from the jacoco plugin can create the XML report out of it. + +// to have a unified report, we just pass both the .exec and the .ec file +// to the jacoco task, so we get a unified XML report with total coverage. +// Reference: https://medium.com/@rafael_toledo/setting-up-an-unified-coverage-report-in-android-with-jacoco-robolectric-and-espresso-ffe239aaf3fa + +apply plugin: 'jacoco' + +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/") +} + +task coverageReport(type: JacocoReport) { + dependsOn "testDebugUnitTest" + dependsOn "connectedCheck" + + def testData = "jacoco/testDebugUnitTest.exec" + def androidTestData = "outputs/code-coverage/connected/*coverage.ec" + executionData = fileTree(dir: "$buildDir", includes: [testData, androidTestData]) + + // Sources. + sourceDirectories = android.sourceSets.main.java.sourceFiles + // Add BuildConfig and R. + additionalSourceDirs = files([ + "$buildDir/generated/source/buildConfig/debug", + "$buildDir/generated/source/r/debug" + ]) + + // Classes. + def debugDir = "$buildDir/intermediates/classes/debug" + def filter = ['**/R.class', '**/R$*.class', '**/*$ViewInjector*.*', + '**/BuildConfig.*', '**/Manifest*.*'] + classDirectories = fileTree(dir: debugDir, excludes: filter); + + reports.xml.enabled = true + reports.html.enabled = true +} + +//endregion + // export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_101.jdk/Contents/Home // To deploy ./gradlew bintrayUpload diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/AspectRatioTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/AspectRatioTest.java new file mode 100644 index 00000000..715a95b5 --- /dev/null +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/AspectRatioTest.java @@ -0,0 +1,58 @@ +package com.otaliastudios.cameraview; + + +import android.annotation.TargetApi; +import android.hardware.Camera; +import android.hardware.camera2.CameraCharacteristics; +import android.os.Parcel; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; +import android.util.SizeF; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class AspectRatioTest { + + @Test + public void testConstructor() { + AspectRatio ratio = AspectRatio.of(50, 10); + assertEquals(ratio.getX(), 5f, 0); + assertEquals(ratio.getY(), 1f, 0); + assertEquals(ratio.toString(), "5:1"); + } + + @Test + public void testEquals() { + AspectRatio ratio = AspectRatio.of(50, 10); + AspectRatio ratio1 = AspectRatio.of(5, 1); + assertTrue(ratio.equals(ratio1)); + + Size size = new Size(500, 100); + assertTrue(ratio.matches(size)); + } + + @Test + public void testInverse() { + AspectRatio ratio = AspectRatio.of(50, 10); + AspectRatio inverse = ratio.inverse(); + assertEquals(inverse.getX(), 1f, 0); + assertEquals(inverse.getY(), 5f, 0); + } + + @Test + public void testParcelable() { + AspectRatio ratio = AspectRatio.of(50, 10); + Parcel parcel = Parcel.obtain(); + ratio.writeToParcel(parcel, ratio.describeContents()); + + parcel.setDataPosition(0); + AspectRatio other = AspectRatio.CREATOR.createFromParcel(parcel); + assertEquals(ratio, other); + } + +} diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraOptions1Test.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraOptions1Test.java new file mode 100644 index 00000000..fbf68a7a --- /dev/null +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraOptions1Test.java @@ -0,0 +1,132 @@ +package com.otaliastudios.cameraview; + + +import android.hardware.Camera; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; +import android.test.ActivityInstrumentationTestCase2; +import android.test.InstrumentationTestCase; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.mockito.Mockito.*; +import static org.junit.Assert.*; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class CameraOptions1Test { + + @Test + public void testEmpty() { + CameraOptions o = new CameraOptions(mock(Camera.Parameters.class)); + assertTrue(o.getSupportedWhiteBalance().isEmpty()); + assertTrue(o.getSupportedFlash().isEmpty()); + assertTrue(o.getSupportedHdr().isEmpty()); + assertFalse(o.isAutoFocusSupported()); + assertFalse(o.isExposureCorrectionSupported()); + assertFalse(o.isVideoSnapshotSupported()); + assertFalse(o.isZoomSupported()); + assertEquals(o.getExposureCorrectionMaxValue(), 0f, 0); + assertEquals(o.getExposureCorrectionMinValue(), 0f, 0); + } + + @Test + public void testFacing() { + Set supported = new HashSet<>(); + Camera.CameraInfo cameraInfo = new Camera.CameraInfo(); + for (int i = 0, count = Camera.getNumberOfCameras(); i < count; i++) { + Camera.getCameraInfo(i, cameraInfo); + supported.add(cameraInfo.facing); + } + + CameraOptions o = new CameraOptions(mock(Camera.Parameters.class)); + Mapper m = new Mapper.Mapper1(); + Set s = o.getSupportedFacing(); + assertEquals(o.getSupportedFacing().size(), supported.size()); + for (Facing facing : s) { + assertTrue(supported.contains(m.map(facing))); + } + } + + @Test + public void testWhiteBalance() { + Camera.Parameters params = mock(Camera.Parameters.class); + when(params.getSupportedWhiteBalance()).thenReturn(Arrays.asList( + Camera.Parameters.WHITE_BALANCE_AUTO, // Supported + Camera.Parameters.WHITE_BALANCE_CLOUDY_DAYLIGHT, // Supported + Camera.Parameters.WHITE_BALANCE_SHADE // Not supported + )); + + CameraOptions o = new CameraOptions(params); + assertEquals(o.getSupportedWhiteBalance().size(), 2); + assertTrue(o.getSupportedWhiteBalance().contains(WhiteBalance.AUTO)); + assertTrue(o.getSupportedWhiteBalance().contains(WhiteBalance.CLOUDY)); + assertTrue(o.supports(WhiteBalance.AUTO)); + assertTrue(o.supports(WhiteBalance.CLOUDY)); + } + + @Test + public void testFlash() { + Camera.Parameters params = mock(Camera.Parameters.class); + when(params.getSupportedFlashModes()).thenReturn(Arrays.asList( + Camera.Parameters.FLASH_MODE_AUTO, // Supported + Camera.Parameters.FLASH_MODE_TORCH, // Supported + Camera.Parameters.FLASH_MODE_RED_EYE // Not supported + )); + + CameraOptions o = new CameraOptions(params); + assertEquals(o.getSupportedFlash().size(), 2); + assertTrue(o.getSupportedFlash().contains(Flash.AUTO)); + assertTrue(o.getSupportedFlash().contains(Flash.TORCH)); + assertTrue(o.supports(Flash.AUTO)); + assertTrue(o.supports(Flash.TORCH)); + } + + @Test + public void testHdr() { + Camera.Parameters params = mock(Camera.Parameters.class); + when(params.getSupportedSceneModes()).thenReturn(Arrays.asList( + Camera.Parameters.SCENE_MODE_AUTO, // Supported + Camera.Parameters.SCENE_MODE_HDR, // Supported + Camera.Parameters.SCENE_MODE_FIREWORKS // Not supported + )); + + CameraOptions o = new CameraOptions(params); + assertEquals(o.getSupportedHdr().size(), 2); + assertTrue(o.getSupportedHdr().contains(Hdr.OFF)); + assertTrue(o.getSupportedHdr().contains(Hdr.ON)); + assertTrue(o.supports(Hdr.OFF)); + assertTrue(o.supports(Hdr.ON)); + } + + @Test + public void testBooleanFlags() { + Camera.Parameters params = mock(Camera.Parameters.class); + when(params.isVideoSnapshotSupported()).thenReturn(true); + when(params.isZoomSupported()).thenReturn(true); + when(params.getSupportedFocusModes()).thenReturn(Arrays.asList(Camera.Parameters.FOCUS_MODE_AUTO)); + CameraOptions o = new CameraOptions(params); + assertTrue(o.isVideoSnapshotSupported()); + assertTrue(o.isZoomSupported()); + assertTrue(o.isAutoFocusSupported()); + } + + @Test + public void testExposureCorrection() { + Camera.Parameters params = mock(Camera.Parameters.class); + when(params.getMaxExposureCompensation()).thenReturn(10); + when(params.getMinExposureCompensation()).thenReturn(-10); + when(params.getExposureCompensationStep()).thenReturn(0.5f); + CameraOptions o = new CameraOptions(params); + assertTrue(o.isExposureCorrectionSupported()); + assertEquals(o.getExposureCorrectionMinValue(), -10f * 0.5f, 0f); + assertEquals(o.getExposureCorrectionMaxValue(), 10f * 0.5f, 0f); + } + +} diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java new file mode 100644 index 00000000..4e8c94d5 --- /dev/null +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java @@ -0,0 +1,279 @@ +package com.otaliastudios.cameraview; + + +import android.app.Instrumentation; +import android.content.Context; +import android.hardware.Camera; +import android.os.Looper; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.MediumTest; +import android.support.test.runner.AndroidJUnit4; +import android.view.View; +import android.view.ViewGroup; + +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.*; + +import static android.view.View.MeasureSpec.*; +import static android.view.ViewGroup.LayoutParams.*; + +@RunWith(AndroidJUnit4.class) +@MediumTest +public class CameraViewTest { + + private CameraView cameraView; + private MockCameraController mockController; + private Preview mockPreview; + + @Before + public void setUp() { + InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { + @Override + public void run() { + Context context = InstrumentationRegistry.getContext(); + cameraView = new CameraView(context) { + @Override + protected CameraController instantiateCameraController(CameraCallbacks callbacks, Preview preview) { + mockController = new MockCameraController(callbacks, preview); + return mockController; + } + + @Override + protected Preview instantiatePreview(Context context, ViewGroup container) { + mockPreview = new MockPreview(context, container); + return mockPreview; + } + }; + } + }); + } + + @After + public void tearDown() { + cameraView = null; + mockController = null; + mockPreview = null; + } + + //region testDefaults + + @Test + public void testNotStarted() { + assertFalse(cameraView.isStarted()); + assertNull(cameraView.getCameraOptions()); + assertNull(cameraView.getExtraProperties()); + assertNull(cameraView.getPreviewSize()); + assertNull(cameraView.getCaptureSize()); + assertNull(cameraView.getSnapshotSize()); + } + + @Test + public void testDefaults() { + // CameraController + assertEquals(cameraView.getFlash(), Flash.DEFAULT); + assertEquals(cameraView.getFacing(), Facing.DEFAULT); + assertEquals(cameraView.getGrid(), Grid.DEFAULT); + assertEquals(cameraView.getWhiteBalance(), WhiteBalance.DEFAULT); + assertEquals(cameraView.getSessionType(), SessionType.DEFAULT); + assertEquals(cameraView.getHdr(), Hdr.DEFAULT); + assertEquals(cameraView.getVideoQuality(), VideoQuality.DEFAULT); + + // Self managed + assertEquals(cameraView.getExposureCorrection(), 0f, 0f); + assertEquals(cameraView.getZoom(), 0f, 0f); + assertEquals(cameraView.getCropOutput(), CameraView.DEFAULT_CROP_OUTPUT); + assertEquals(cameraView.getJpegQuality(), CameraView.DEFAULT_JPEG_QUALITY); + assertEquals(cameraView.getGestureAction(Gesture.TAP), GestureAction.DEFAULT_TAP); + assertEquals(cameraView.getGestureAction(Gesture.LONG_TAP), GestureAction.DEFAULT_LONG_TAP); + assertEquals(cameraView.getGestureAction(Gesture.PINCH), GestureAction.DEFAULT_PINCH); + assertEquals(cameraView.getGestureAction(Gesture.SCROLL_HORIZONTAL), GestureAction.DEFAULT_SCROLL_HORIZONTAL); + assertEquals(cameraView.getGestureAction(Gesture.SCROLL_VERTICAL), GestureAction.DEFAULT_SCROLL_VERTICAL); + } + + //endregion + + //region testGesture + + @Test + public void testGesture_mapAndClear() { + // Assignable + cameraView.mapGesture(Gesture.PINCH, GestureAction.ZOOM); + assertEquals(cameraView.getGestureAction(Gesture.PINCH), GestureAction.ZOOM); + + // Not assignable: This is like clearing + cameraView.mapGesture(Gesture.PINCH, GestureAction.CAPTURE); + assertEquals(cameraView.getGestureAction(Gesture.PINCH), GestureAction.NONE); + + // Test clearing + cameraView.mapGesture(Gesture.PINCH, GestureAction.ZOOM); + cameraView.clearGesture(Gesture.PINCH); + assertEquals(cameraView.getGestureAction(Gesture.PINCH), GestureAction.NONE); + } + + @Test + public void testGesture_enablingDisablingLayouts() { + cameraView.clearGesture(Gesture.TAP); + cameraView.clearGesture(Gesture.LONG_TAP); + cameraView.clearGesture(Gesture.PINCH); + cameraView.clearGesture(Gesture.SCROLL_HORIZONTAL); + cameraView.clearGesture(Gesture.SCROLL_VERTICAL); + + // PinchGestureLayout + cameraView.mapGesture(Gesture.PINCH, GestureAction.ZOOM); + assertTrue(cameraView.mPinchGestureLayout.enabled()); + cameraView.clearGesture(Gesture.PINCH); + assertFalse(cameraView.mPinchGestureLayout.enabled()); + + // TapGestureLayout + cameraView.mapGesture(Gesture.TAP, GestureAction.CAPTURE); + assertTrue(cameraView.mTapGestureLayout.enabled()); + cameraView.clearGesture(Gesture.TAP); + assertFalse(cameraView.mPinchGestureLayout.enabled()); + + // ScrollGestureLayout + cameraView.mapGesture(Gesture.SCROLL_HORIZONTAL, GestureAction.ZOOM); + assertTrue(cameraView.mScrollGestureLayout.enabled()); + cameraView.clearGesture(Gesture.SCROLL_HORIZONTAL); + assertFalse(cameraView.mScrollGestureLayout.enabled()); + } + + //endregion + + //region testMeasure + + private void mockPreviewSize() { + Size size = new Size(900, 1600); + mockController.setMockPreviewSize(size); + } + + @Test + public void testMeasure_matchParentBoth() { + mockPreviewSize(); + + // Respect parent/layout constraints on both dimensions. + cameraView.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); + cameraView.measure( + makeMeasureSpec(500, EXACTLY), + makeMeasureSpec(500, EXACTLY)); + assertEquals(cameraView.getMeasuredWidth(), 500); + assertEquals(cameraView.getMeasuredHeight(), 500); + + // Even if the parent ViewGroup passes AT_MOST + cameraView.measure( + makeMeasureSpec(500, AT_MOST), + makeMeasureSpec(500, AT_MOST)); + assertEquals(cameraView.getMeasuredWidth(), 500); + assertEquals(cameraView.getMeasuredHeight(), 500); + + cameraView.measure( + makeMeasureSpec(500, EXACTLY), + makeMeasureSpec(500, AT_MOST)); + assertEquals(cameraView.getMeasuredWidth(), 500); + assertEquals(cameraView.getMeasuredHeight(), 500); + } + + @Test + public void testMeasure_wrapContentBoth() { + mockPreviewSize(); + + // Respect parent constraints, but fit aspect ratio. + // Fit into a 160x160 parent so we espect final width to be 90. + cameraView.setLayoutParams(new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); + cameraView.measure( + makeMeasureSpec(160, AT_MOST), + makeMeasureSpec(160, AT_MOST)); + assertEquals(cameraView.getMeasuredWidth(), 90); + assertEquals(cameraView.getMeasuredHeight(), 160); + } + + @Test + public void testMeasure_wrapContentSingle() { + mockPreviewSize(); + + // Respect MATCH_PARENT on height, change width to fit the aspect ratio. + cameraView.setLayoutParams(new ViewGroup.LayoutParams(WRAP_CONTENT, MATCH_PARENT)); + cameraView.measure( + makeMeasureSpec(160, AT_MOST), + makeMeasureSpec(160, AT_MOST)); + assertEquals(cameraView.getMeasuredWidth(), 90); + assertEquals(cameraView.getMeasuredHeight(), 160); + + // Respect MATCH_PARENT on width. Enlarge height trying to fit aspect ratio as much as possible. + cameraView.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT)); + cameraView.measure( + makeMeasureSpec(160, AT_MOST), + makeMeasureSpec(160, AT_MOST)); + assertEquals(cameraView.getMeasuredWidth(), 160); + assertEquals(cameraView.getMeasuredHeight(), 160); + } + + @Test + public void testMeasure_scrollableContainer() { + mockPreviewSize(); + + // Assume a vertical scroll view. It will pass UNSPECIFIED as height. + // We respect MATCH_PARENT on width (160), and enlarge height to match the aspect ratio. + cameraView.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, WRAP_CONTENT)); + cameraView.measure( + makeMeasureSpec(160, AT_MOST), + makeMeasureSpec(0, UNSPECIFIED)); + assertEquals(cameraView.getMeasuredWidth(), 160); + assertEquals(cameraView.getMeasuredHeight(), 160f * (16f / 9f), 1f); // Leave a margin + + // Assume a view scrolling in both dimensions. It will pass UNSPECIFIED. + // In this case we must fit the exact preview dimension. + cameraView.setLayoutParams(new ViewGroup.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); + cameraView.measure( + makeMeasureSpec(0, UNSPECIFIED), + makeMeasureSpec(0, UNSPECIFIED)); + assertEquals(cameraView.getMeasuredWidth(), 900); + assertEquals(cameraView.getMeasuredHeight(), 1600); + } + + //endregion + + //region Zoom, ExposureCorrection + + @Test + public void testZoom() { + cameraView.setZoom(0.5f); + assertEquals(cameraView.getZoom(), 0.5f, 0f); + cameraView.setZoom(-10f); + assertEquals(cameraView.getZoom(), 0f, 0f); + cameraView.setZoom(10f); + assertEquals(cameraView.getZoom(), 1f, 0f); + } + + @Test + public void testExposureCorrection() { + // This needs a valid CameraOptions value. + CameraOptions o = mock(CameraOptions.class); + when(o.getExposureCorrectionMinValue()).thenReturn(-10f); + when(o.getExposureCorrectionMaxValue()).thenReturn(10f); + mockController.setMockCameraOptions(o); + + cameraView.setExposureCorrection(5f); + assertEquals(cameraView.getExposureCorrection(), 5f, 0f); + cameraView.setExposureCorrection(-100f); + assertEquals(cameraView.getExposureCorrection(), -10f, 0f); + cameraView.setExposureCorrection(100f); + assertEquals(cameraView.getExposureCorrection(), 10f, 0f); + } + + //endregion + + // TODO: test setLocation + + // TODO: test touch events + + // TODO: test permissions and start() stop() isStarted() + + // TODO: test CameraCallbacks + +} diff --git a/cameraview/src/test/java/com/otaliastudios/cameraview/ExtraPropertiesTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/ExtraProperties1Test.java similarity index 78% rename from cameraview/src/test/java/com/otaliastudios/cameraview/ExtraPropertiesTest.java rename to cameraview/src/androidTest/java/com/otaliastudios/cameraview/ExtraProperties1Test.java index 166b182e..9a45eca7 100644 --- a/cameraview/src/test/java/com/otaliastudios/cameraview/ExtraPropertiesTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/ExtraProperties1Test.java @@ -4,15 +4,20 @@ package com.otaliastudios.cameraview; import android.annotation.TargetApi; import android.hardware.Camera; import android.hardware.camera2.CameraCharacteristics; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; import android.util.SizeF; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.Mockito; import static org.mockito.Mockito.*; import static org.junit.Assert.*; -public class ExtraPropertiesTest { +@RunWith(AndroidJUnit4.class) +@SmallTest +public class ExtraProperties1Test { @Test public void testConstructor1() { diff --git a/cameraview/src/test/java/com/otaliastudios/cameraview/Mapper1Test.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/Mapper1Test.java similarity index 94% rename from cameraview/src/test/java/com/otaliastudios/cameraview/Mapper1Test.java rename to cameraview/src/androidTest/java/com/otaliastudios/cameraview/Mapper1Test.java index bbbaec78..71a93434 100644 --- a/cameraview/src/test/java/com/otaliastudios/cameraview/Mapper1Test.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/Mapper1Test.java @@ -2,11 +2,17 @@ package com.otaliastudios.cameraview; import android.hardware.Camera; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; import org.junit.Test; +import org.junit.runner.RunWith; import static org.junit.Assert.*; + +@RunWith(AndroidJUnit4.class) +@SmallTest public class Mapper1Test { private Mapper mapper = new Mapper.Mapper1(); diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java new file mode 100644 index 00000000..113bc7ea --- /dev/null +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java @@ -0,0 +1,124 @@ +package com.otaliastudios.cameraview; + + +import android.graphics.PointF; +import android.location.Location; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.io.File; + +public class MockCameraController extends CameraController { + + MockCameraController(CameraView.CameraCallbacks callback, Preview preview) { + super(callback, preview); + } + + void setMockCameraOptions(CameraOptions options) { + mOptions = options; + } + + void setMockPreviewSize(Size size) { + mPreviewSize = size; + } + + @Override + void onStart() { + + } + + @Override + void onStop() { + + } + + @Override + boolean setZoom(float zoom) { + return true; + } + + @Override + boolean setExposureCorrection(float EVvalue) { + return true; + } + + @Override + void setFacing(Facing facing) { + mFacing = facing; + } + + @Override + void setFlash(Flash flash) { + mFlash = flash; + } + + @Override + void setWhiteBalance(WhiteBalance whiteBalance) { + mWhiteBalance = whiteBalance; + } + + @Override + void setVideoQuality(VideoQuality videoQuality) { + mVideoQuality = videoQuality; + } + + @Override + void setSessionType(SessionType sessionType) { + mSessionType = sessionType; + } + + @Override + void setHdr(Hdr hdr) { + mHdr = hdr; + } + + @Override + void setLocation(Location location) { + + } + + @Override + boolean capturePicture() { + return true; + } + + @Override + boolean captureSnapshot() { + return true; + } + + @Override + boolean startVideo(@NonNull File file) { + return true; + } + + @Override + boolean endVideo() { + return true; + } + + @Override + boolean shouldFlipSizes() { + return false; + } + + @Override + boolean isCameraOpened() { + return true; + } + + @Override + boolean startAutoFocus(@Nullable Gesture gesture, PointF point) { + return true; + } + + @Override + public void onSurfaceChanged() { + + } + + @Override + public void onSurfaceAvailable() { + + } +} diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockPreview.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockPreview.java new file mode 100644 index 00000000..eb2b1b8c --- /dev/null +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockPreview.java @@ -0,0 +1,34 @@ +package com.otaliastudios.cameraview; + + +import android.content.Context; +import android.view.Surface; +import android.view.View; +import android.view.ViewGroup; + +public class MockPreview extends Preview { + + public MockPreview(Context context, ViewGroup parent) { + super(context, parent); + } + + @Override + Surface getSurface() { + return null; + } + + @Override + View getView() { + return null; + } + + @Override + Class getOutputClass() { + return null; + } + + @Override + boolean isReady() { + return true; + } +} diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java index 278bd790..83d11199 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java @@ -1,6 +1,7 @@ package com.otaliastudios.cameraview; import android.Manifest; +import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; @@ -44,8 +45,8 @@ public class CameraView extends FrameLayout { private final static String TAG = CameraView.class.getSimpleName(); public final static int PERMISSION_REQUEST_CODE = 16; - private final static int DEFAULT_JPEG_QUALITY = 100; - private final static boolean DEFAULT_CROP_OUTPUT = false; + final static int DEFAULT_JPEG_QUALITY = 100; + final static boolean DEFAULT_CROP_OUTPUT = false; // Self managed parameters private int mJpegQuality; @@ -61,10 +62,10 @@ public class CameraView extends FrameLayout { private Preview mPreviewImpl; // Views - private GridLinesLayout mGridLinesLayout; - private PinchGestureLayout mPinchGestureLayout; - private TapGestureLayout mTapGestureLayout; - private ScrollGestureLayout mScrollGestureLayout; + GridLinesLayout mGridLinesLayout; + PinchGestureLayout mPinchGestureLayout; + TapGestureLayout mTapGestureLayout; + ScrollGestureLayout mScrollGestureLayout; private boolean mIsStarted; private boolean mKeepScreenOn; @@ -98,6 +99,7 @@ public class CameraView extends FrameLayout { WhiteBalance whiteBalance = WhiteBalance.fromValue(a.getInteger(R.styleable.CameraView_cameraWhiteBalance, WhiteBalance.DEFAULT.value())); VideoQuality videoQuality = VideoQuality.fromValue(a.getInteger(R.styleable.CameraView_cameraVideoQuality, VideoQuality.DEFAULT.value())); SessionType sessionType = SessionType.fromValue(a.getInteger(R.styleable.CameraView_cameraSessionType, SessionType.DEFAULT.value())); + Hdr hdr = Hdr.fromValue(a.getInteger(R.styleable.CameraView_cameraHdr, Hdr.DEFAULT.value())); // Gestures GestureAction tapGesture = GestureAction.fromValue(a.getInteger(R.styleable.CameraView_cameraGestureTap, GestureAction.DEFAULT_TAP.value())); @@ -109,8 +111,8 @@ public class CameraView extends FrameLayout { // Components mCameraCallbacks = new CameraCallbacks(); - mPreviewImpl = new TextureViewPreview(context, this); - mCameraController = new Camera1(mCameraCallbacks, mPreviewImpl); + mPreviewImpl = instantiatePreview(context, this); + mCameraController = instantiateCameraController(mCameraCallbacks, mPreviewImpl); mUiHandler = new Handler(Looper.getMainLooper()); mWorkerHandler = new WorkerHandler("CameraViewWorker"); @@ -137,6 +139,7 @@ public class CameraView extends FrameLayout { setVideoQuality(videoQuality); setWhiteBalance(whiteBalance); setGrid(grid); + setHdr(hdr); // Apply gestures mapGesture(Gesture.TAP, tapGesture); @@ -151,6 +154,17 @@ public class CameraView extends FrameLayout { } } + + protected Preview instantiatePreview(Context context, ViewGroup container) { + return new TextureViewPreview(context, container); + } + + + protected CameraController instantiateCameraController(CameraCallbacks callbacks, Preview preview) { + return new Camera1(callbacks, preview); + } + + @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); @@ -364,7 +378,18 @@ public class CameraView extends FrameLayout { * @param gesture which gesture to clear */ public void clearGesture(@NonNull Gesture gesture) { - mGestureMap.put(gesture, GestureAction.NONE); + mapGesture(gesture, GestureAction.NONE); + } + + + /** + * Returns the action currently mapped to the given gesture. + * + * @param gesture which gesture to inspect + * @return mapped action + */ + public GestureAction getGestureAction(@NonNull Gesture gesture) { + return mGestureMap.get(gesture); } @@ -472,6 +497,7 @@ public class CameraView extends FrameLayout { * Throws if session = audio and manifest did not add the microphone permissions. * @return true if we can go on, false otherwise. */ + @SuppressLint("NewApi") private boolean checkPermissions(SessionType sessionType) { checkPermissionsManifestOrThrow(sessionType); boolean api23 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; @@ -480,9 +506,7 @@ public class CameraView extends FrameLayout { cameraCheck = PackageManager.PERMISSION_GRANTED; audioCheck = PackageManager.PERMISSION_GRANTED; } else { - //noinspection all cameraCheck = getContext().checkSelfPermission(Manifest.permission.CAMERA); - //noinspection all audioCheck = getContext().checkSelfPermission(Manifest.permission.RECORD_AUDIO); } switch (sessionType) { @@ -922,12 +946,21 @@ public class CameraView extends FrameLayout { */ public void setJpegQuality(int jpegQuality) { if (jpegQuality <= 0 || jpegQuality > 100) { - throw new IllegalArgumentException("JPEG quality should be > 0 and <= 0"); + throw new IllegalArgumentException("JPEG quality should be > 0 and <= 100"); } mJpegQuality = jpegQuality; } + /** + * Gets the JPEG compression quality for image outputs. + * @return a 0-100 integer + */ + public int getJpegQuality() { + return mJpegQuality; + } + + /** * Whether we should crop the picture output to match CameraView aspect ratio. * This is only relevant if CameraView dimensions were somehow constrained @@ -942,6 +975,17 @@ public class CameraView extends FrameLayout { } + /** + * Returns whether we should crop the picture output to match CameraView aspect ratio. + * + * @see #setCropOutput(boolean) + * @return whether we crop + */ + public boolean getCropOutput() { + return mCropOutput; + } + + /** * Sets a {@link CameraListener} instance to be notified of all * interesting events that will happen during the camera lifecycle. @@ -1158,7 +1202,7 @@ public class CameraView extends FrameLayout { private MediaActionSound mSound; private final boolean mUseSounds = Build.VERSION.SDK_INT >= 16; - @SuppressWarnings("all") + @SuppressLint("NewApi") private void sound(int soundType) { if (mUseSounds) { if (mSound == null) mSound = new MediaActionSound(); diff --git a/cameraview/src/main/views/com/otaliastudios/cameraview/GestureLayout.java b/cameraview/src/main/views/com/otaliastudios/cameraview/GestureLayout.java index ad7cc975..5553b2da 100644 --- a/cameraview/src/main/views/com/otaliastudios/cameraview/GestureLayout.java +++ b/cameraview/src/main/views/com/otaliastudios/cameraview/GestureLayout.java @@ -29,6 +29,10 @@ abstract class GestureLayout extends FrameLayout { mEnabled = enable; } + public boolean enabled() { + return mEnabled; + } + public abstract boolean onTouchEvent(MotionEvent event); public final Gesture getGestureType() {