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
pull/36/head
Mattia Iavarone 7 years ago committed by GitHub
parent 982ed3d094
commit 66077c1eaa
  1. 4
      .travis.yml
  2. 7
      cameraview/build.gradle
  3. 13
      cameraview/src/androidTest/AndroidManifest.xml
  4. 41
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseTest.java
  5. 60
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraUtilsTest.java
  6. 150
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java
  7. 92
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CropHelperTest.java
  8. 62
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/GestureLayoutTest.java
  9. 78
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/GridLinesLayoutTest.java
  10. 14
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java
  11. 21
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockPreview.java
  12. 97
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/OrientationHelperTest.java
  13. 86
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/PinchGestureLayoutTest.java
  14. 157
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/PreviewTest.java
  15. 97
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/ScrollGestureLayoutTest.java
  16. 19
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/SurfaceViewPreviewTest.java
  17. 81
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/TapGestureLayoutTest.java
  18. 75
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/TestActivity.java
  19. 43
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/TextureViewPreviewTest.java
  20. 11
      cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java
  21. 6
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  22. 4
      cameraview/src/main/res/layout/surface_view.xml
  23. 9
      cameraview/src/main/utils/com/otaliastudios/cameraview/OrientationHelper.java
  24. 72
      cameraview/src/main/utils/com/otaliastudios/cameraview/Task.java
  25. 5
      cameraview/src/main/views/com/otaliastudios/cameraview/GestureLayout.java
  26. 10
      cameraview/src/main/views/com/otaliastudios/cameraview/GridLinesLayout.java
  27. 2
      cameraview/src/main/views/com/otaliastudios/cameraview/PinchGestureLayout.java
  28. 60
      cameraview/src/main/views/com/otaliastudios/cameraview/Preview.java
  29. 40
      cameraview/src/main/views/com/otaliastudios/cameraview/SurfaceViewPreview.java
  30. 4
      cameraview/src/main/views/com/otaliastudios/cameraview/TapGestureLayout.java
  31. 43
      cameraview/src/main/views/com/otaliastudios/cameraview/TextureViewPreview.java
  32. 10
      codecov.yml

@ -35,10 +35,10 @@ before_script:
- adb shell input keyevent 82 & - adb shell input keyevent 82 &
script: script:
- ./gradlew clean testDebugUnitTest connectedCheck coverageReport - ./gradlew clean testDebugUnitTest connectedCheck mergedCoverageReport
after_success: after_success:
- bash <(curl -s https://codecov.io/bash) - bash <(curl -s https://codecov.io/bash) -s "*/build/reports/mergedCoverageReport/"
cache: cache:
directories: directories:

@ -46,6 +46,7 @@ dependencies {
androidTestCompile 'com.android.support.test:rules:1.0.1' androidTestCompile 'com.android.support.test:rules:1.0.1'
androidTestCompile 'com.google.dexmaker:dexmaker:1.2' androidTestCompile 'com.google.dexmaker:dexmaker:1.2'
androidTestCompile 'com.google.dexmaker:dexmaker-mockito: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:exifinterface:$supportLibVersion"
compile "com.android.support:support-annotations:$supportLibVersion" compile "com.android.support:support-annotations:$supportLibVersion"
@ -175,14 +176,15 @@ artifacts {
apply plugin: 'jacoco' apply plugin: 'jacoco'
def reportsDirectory = "$buildDir/reports/"
jacoco { jacoco {
// Using a different version might get 0 coverage reports. // Using a different version might get 0 coverage reports.
// No time to investigate now. // No time to investigate now.
toolVersion = "0.7.6.201602180812" 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 "testDebugUnitTest"
dependsOn "connectedCheck" dependsOn "connectedCheck"
@ -206,6 +208,7 @@ task coverageReport(type: JacocoReport) {
reports.xml.enabled = true reports.xml.enabled = true
reports.html.enabled = true reports.html.enabled = true
reports.xml.destination = "$reportsDirectory/mergedCoverageReport/report.xml"
} }
//endregion //endregion

@ -0,0 +1,13 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.otaliastudios.cameraview">
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<application>
<activity
android:hardwareAccelerated="true"
android:name=".TestActivity"/>
</application>
</manifest>

@ -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();
}
}

@ -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
}
}

@ -4,10 +4,12 @@ package com.otaliastudios.cameraview;
import android.app.Instrumentation; import android.app.Instrumentation;
import android.content.Context; import android.content.Context;
import android.hardware.Camera; import android.hardware.Camera;
import android.location.Location;
import android.os.Looper; import android.os.Looper;
import android.support.test.InstrumentationRegistry; import android.support.test.InstrumentationRegistry;
import android.support.test.filters.MediumTest; import android.support.test.filters.MediumTest;
import android.support.test.runner.AndroidJUnit4; import android.support.test.runner.AndroidJUnit4;
import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -25,15 +27,17 @@ import static android.view.ViewGroup.LayoutParams.*;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@MediumTest @MediumTest
public class CameraViewTest { public class CameraViewTest extends BaseTest {
private CameraView cameraView; private CameraView cameraView;
private MockCameraController mockController; private MockCameraController mockController;
private Preview mockPreview; private Preview mockPreview;
private boolean hasPermissions;
@Before @Before
public void setUp() { public void setUp() {
InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() { ui(new Runnable() {
@Override @Override
public void run() { public void run() {
Context context = InstrumentationRegistry.getContext(); Context context = InstrumentationRegistry.getContext();
@ -49,6 +53,11 @@ public class CameraViewTest {
mockPreview = new MockPreview(context, container); mockPreview = new MockPreview(context, container);
return mockPreview; return mockPreview;
} }
@Override
protected boolean checkPermissions(SessionType sessionType) {
return hasPermissions;
}
}; };
} }
}); });
@ -59,12 +68,13 @@ public class CameraViewTest {
cameraView = null; cameraView = null;
mockController = null; mockController = null;
mockPreview = null; mockPreview = null;
hasPermissions = false;
} }
//region testDefaults //region testDefaults
@Test @Test
public void testNotStarted() { public void testNullBeforeStart() {
assertFalse(cameraView.isStarted()); assertFalse(cameraView.isStarted());
assertNull(cameraView.getCameraOptions()); assertNull(cameraView.getCameraOptions());
assertNull(cameraView.getExtraProperties()); assertNull(cameraView.getExtraProperties());
@ -96,6 +106,23 @@ public class CameraViewTest {
assertEquals(cameraView.getGestureAction(Gesture.SCROLL_VERTICAL), GestureAction.DEFAULT_SCROLL_VERTICAL); 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 //endregion
//region testGesture //region testGesture
@ -145,6 +172,94 @@ public class CameraViewTest {
//endregion //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 //region testMeasure
private void mockPreviewSize() { private void mockPreviewSize() {
@ -268,11 +383,34 @@ public class CameraViewTest {
//endregion //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 // TODO: test CameraCallbacks

@ -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<Boolean>() {
@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);
}
}
}

@ -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<T extends GestureLayout> extends BaseTest {
protected abstract T create(Context context);
@Rule
public ActivityTestRule<TestActivity> rule = new ActivityTestRule<>(TestActivity.class);
protected T layout;
protected Task<Gesture> 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.<View>is(layout)).inRoot(any(Root.class));
}
}

@ -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<TestActivity> 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));
}
}

@ -10,6 +10,12 @@ import java.io.File;
public class MockCameraController extends CameraController { public class MockCameraController extends CameraController {
Location mLocation;
boolean mPictureCaptured;
boolean mFocusStarted;
boolean mZoomChanged;
boolean mExposureCorrectionChanged;
MockCameraController(CameraView.CameraCallbacks callback, Preview preview) { MockCameraController(CameraView.CameraCallbacks callback, Preview preview) {
super(callback, preview); super(callback, preview);
} }
@ -24,21 +30,21 @@ public class MockCameraController extends CameraController {
@Override @Override
void onStart() { void onStart() {
} }
@Override @Override
void onStop() { void onStop() {
} }
@Override @Override
boolean setZoom(float zoom) { boolean setZoom(float zoom) {
mZoomChanged = true;
return true; return true;
} }
@Override @Override
boolean setExposureCorrection(float EVvalue) { boolean setExposureCorrection(float EVvalue) {
mExposureCorrectionChanged = true;
return true; return true;
} }
@ -74,11 +80,12 @@ public class MockCameraController extends CameraController {
@Override @Override
void setLocation(Location location) { void setLocation(Location location) {
mLocation = location;
} }
@Override @Override
boolean capturePicture() { boolean capturePicture() {
mPictureCaptured = true;
return true; return true;
} }
@ -109,6 +116,7 @@ public class MockCameraController extends CameraController {
@Override @Override
boolean startAutoFocus(@Nullable Gesture gesture, PointF point) { boolean startAutoFocus(@Nullable Gesture gesture, PointF point) {
mFocusStarted = true;
return true; return true;
} }

@ -2,33 +2,36 @@ package com.otaliastudios.cameraview;
import android.content.Context; import android.content.Context;
import android.support.annotation.NonNull;
import android.view.Surface; import android.view.Surface;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
public class MockPreview extends Preview { public class MockPreview extends Preview<View, Void> {
public MockPreview(Context context, ViewGroup parent) { MockPreview(Context context, ViewGroup parent) {
super(context, parent); super(context, parent, null);
} }
@NonNull
@Override @Override
Surface getSurface() { protected View onCreateView(Context context, ViewGroup parent) {
return null; return new View(context);
} }
@Override @Override
View getView() { Surface getSurface() {
return null; return null;
} }
@Override @Override
Class getOutputClass() { Class<Void> getOutputClass() {
return null; return null;
} }
@Override @Override
boolean isReady() { Void getOutput() {
return true; return null;
} }
} }

@ -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);
}
}

@ -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<PinchGestureLayout> {
@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);
}
}
}

@ -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<TestActivity> rule = new ActivityTestRule<>(TestActivity.class);
protected Preview preview;
protected Size surfaceSize;
private Preview.SurfaceCallback callback;
private Task<Boolean> 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();
}
}

@ -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<ScrollGestureLayout> {
@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);
}
}

@ -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);
}
}

@ -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<TapGestureLayout> {
@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);
}
}

@ -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();
}
}

@ -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();
}
}

@ -2,6 +2,7 @@ package com.otaliastudios.cameraview;
import android.graphics.PointF; import android.graphics.PointF;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.SurfaceTexture;
import android.graphics.YuvImage; import android.graphics.YuvImage;
import android.hardware.Camera; import android.hardware.Camera;
import android.location.Location; 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. // These can happen at different times but we want to end up here.
private void setup() { private void setup() {
try { try {
Object output = mPreview.getOutput();
if (mPreview.getOutputClass() == SurfaceHolder.class) { if (mPreview.getOutputClass() == SurfaceHolder.class) {
mCamera.setPreviewDisplay(mPreview.getSurfaceHolder()); mCamera.setPreviewDisplay((SurfaceHolder) output);
} else { } else {
mCamera.setPreviewTexture(mPreview.getSurfaceTexture()); mCamera.setPreviewTexture((SurfaceTexture) output);
} }
} catch (IOException e) { } catch (IOException e) {
LOG.e("Error while trying to setup Camera1.", e);
throw new RuntimeException(e); throw new RuntimeException(e);
} }
@ -450,8 +453,10 @@ class Camera1 extends CameraController {
@Override @Override
boolean shouldFlipSizes() { boolean shouldFlipSizes() {
int offset = computeSensorToDisplayOffset();
LOG.i("shouldFlip:", "mDeviceOrientation=", mDeviceOrientation, "mSensorOffset=", mSensorOffset); LOG.i("shouldFlip:", "mDeviceOrientation=", mDeviceOrientation, "mSensorOffset=", mSensorOffset);
return (mDeviceOrientation + mSensorOffset) % 180 != 0; LOG.i("shouldFlip:", "sensorToDisplay=", offset);
return offset % 180 != 0;
} }
@Override @Override

@ -159,7 +159,7 @@ public class CameraView extends FrameLayout {
protected Preview instantiatePreview(Context context, ViewGroup container) { 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. * @return true if we can go on, false otherwise.
*/ */
@SuppressLint("NewApi") @SuppressLint("NewApi")
private boolean checkPermissions(SessionType sessionType) { protected boolean checkPermissions(SessionType sessionType) {
checkPermissionsManifestOrThrow(sessionType); checkPermissionsManifestOrThrow(sessionType);
boolean api23 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; boolean api23 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
int cameraCheck, audioCheck; 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 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. * 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) { private void checkPermissionsManifestOrThrow(SessionType sessionType) {
if (sessionType == SessionType.VIDEO) { if (sessionType == SessionType.VIDEO) {

@ -3,8 +3,8 @@
<SurfaceView <SurfaceView
android:id="@+id/surface_view" android:id="@+id/surface_view"
android:gravity="center"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent"
android:gravity="center" />
</merge> </merge>

@ -19,11 +19,12 @@ class OrientationHelper {
DISPLAY_ORIENTATIONS.put(Surface.ROTATION_270, 270); DISPLAY_ORIENTATIONS.put(Surface.ROTATION_270, 270);
} }
private final OrientationEventListener mListener; final OrientationEventListener mListener;
Display mDisplay;
private final Callbacks mCallbacks; private final Callbacks mCallbacks;
private Display mDisplay; int mLastKnownDisplayOffset = -1;
private int mLastKnownDisplayOffset = -1; int mLastOrientation = -1;
private int mLastOrientation = -1;
interface Callbacks { interface Callbacks {
void onDisplayOffsetChanged(int displayOffset); void onDisplayOffsetChanged(int displayOffset);

@ -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<T> {
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;
}
}

@ -39,6 +39,11 @@ abstract class GestureLayout extends FrameLayout {
return mType; return mType;
} }
// For tests.
void setGestureType(Gesture type) {
mType = type;
}
public final PointF[] getPoints() { public final PointF[] getPoints() {
return mPoints; return mPoints;
} }

@ -16,10 +16,12 @@ class GridLinesLayout extends View {
private Grid gridMode; private Grid gridMode;
private final Drawable horiz; Drawable horiz;
private final Drawable vert; Drawable vert;
private final float width; private final float width;
Task<Void> drawTask = new Task<>();
private final static float GOLDEN_RATIO_INV = 0.61803398874989f; private final static float GOLDEN_RATIO_INV = 0.61803398874989f;
public GridLinesLayout(@NonNull Context context) { public GridLinesLayout(@NonNull Context context) {
@ -46,7 +48,7 @@ class GridLinesLayout extends View {
public void setGridMode(Grid gridMode) { public void setGridMode(Grid gridMode) {
this.gridMode = gridMode; this.gridMode = gridMode;
invalidate(); postInvalidate();
} }
private int getLineCount() { private int getLineCount() {
@ -75,6 +77,7 @@ class GridLinesLayout extends View {
@Override @Override
protected void onDraw(Canvas canvas) { protected void onDraw(Canvas canvas) {
super.onDraw(canvas); super.onDraw(canvas);
drawTask.start();
int count = getLineCount(); int count = getLineCount();
for (int n = 0; n < count; n++) { for (int n = 0; n < count; n++) {
float pos = getLinePosition(n); float pos = getLinePosition(n);
@ -89,5 +92,6 @@ class GridLinesLayout extends View {
vert.draw(canvas); vert.draw(canvas);
canvas.translate(- pos * getWidth(), 0); canvas.translate(- pos * getWidth(), 0);
} }
drawTask.end(null);
} }
} }

@ -14,7 +14,7 @@ class PinchGestureLayout extends GestureLayout {
private final static float ADD_SENSITIVITY = 2f; private final static float ADD_SENSITIVITY = 2f;
private ScaleGestureDetector mDetector; ScaleGestureDetector mDetector;
private boolean mNotify; private boolean mNotify;
private float mAdditionFactor = 0; private float mAdditionFactor = 0;

@ -1,15 +1,17 @@
package com.otaliastudios.cameraview; package com.otaliastudios.cameraview;
import android.content.Context; import android.content.Context;
import android.graphics.SurfaceTexture; import android.support.annotation.NonNull;
import android.view.Surface; import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
abstract class Preview { abstract class Preview<T extends View, Output> {
private final static CameraLogger LOG = CameraLogger.create(Preview.class.getSimpleName()); protected final static CameraLogger LOG = CameraLogger.create(Preview.class.getSimpleName());
// Used for testing.
Task<Void> mCropTask = new Task<>();
// This is used to notify CameraImpl to recompute its camera Preview size. // 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. // 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 SurfaceCallback mSurfaceCallback;
private T mView;
// As far as I can see, these are the view/surface dimensions. // As far as I can see, these are the view/surface dimensions.
// This live in the 'View' orientation. // This live in the 'View' orientation.
@ -29,19 +32,24 @@ abstract class Preview {
private int mDesiredWidth; private int mDesiredWidth;
private int mDesiredHeight; 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 Surface getSurface();
abstract View getView();
abstract Class getOutputClass(); @NonNull
abstract boolean isReady(); final T getView() {
SurfaceHolder getSurfaceHolder() { return mView;
return null;
}
SurfaceTexture getSurfaceTexture() {
return null;
} }
abstract Class<Output> getOutputClass();
abstract Output getOutput();
// As far as I can see, these are the actual preview dimensions, as set in CameraParameters. // As far as I can see, these are the actual preview dimensions, as set in CameraParameters.
// This is called by the CameraImpl. // This is called by the CameraImpl.
@ -53,12 +61,20 @@ abstract class Preview {
crop(); crop();
} }
final Size getDesiredSize() {
return new Size(mDesiredWidth, mDesiredHeight);
}
final Size getSurfaceSize() { final Size getSurfaceSize() {
return new Size(mSurfaceWidth, mSurfaceHeight); return new Size(mSurfaceWidth, mSurfaceHeight);
} }
final void setSurfaceCallback(SurfaceCallback callback) { final void setSurfaceCallback(SurfaceCallback callback) {
mSurfaceCallback = 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() { protected final void onSurfaceDestroyed() {
mSurfaceWidth = 0; mSurfaceWidth = 0;
mSurfaceHeight = 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 * 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. * However that should be already managed by the framework.
*/ */
private final void crop() { private final void crop() {
mCropTask.start();
getView().post(new Runnable() { getView().post(new Runnable() {
@Override @Override
public void run() { public void run() {
if (mDesiredHeight == 0 || mDesiredWidth == 0) return; if (mDesiredHeight == 0 || mDesiredWidth == 0 ||
if (mSurfaceHeight == 0 || mSurfaceWidth == 0) return; mSurfaceHeight == 0 || mSurfaceWidth == 0) {
mCropTask.end(null);
return;
}
float scaleX = 1f, scaleY = 1f; float scaleX = 1f, scaleY = 1f;
AspectRatio current = AspectRatio.of(mSurfaceWidth, mSurfaceHeight); AspectRatio current = AspectRatio.of(mSurfaceWidth, mSurfaceHeight);
AspectRatio target = AspectRatio.of(mDesiredWidth, mDesiredHeight); AspectRatio target = AspectRatio.of(mDesiredWidth, mDesiredHeight);
@ -119,6 +141,7 @@ abstract class Preview {
getView().setScaleY(scaleY); getView().setScaleY(scaleY);
LOG.i("crop:", "applied scaleX=", scaleX); LOG.i("crop:", "applied scaleX=", scaleX);
LOG.i("crop:", "applied scaleY=", scaleY); LOG.i("crop:", "applied scaleY=", scaleY);
mCropTask.end(null);
} }
}); });
} }
@ -130,6 +153,7 @@ abstract class Preview {
* @return true if cropping * @return true if cropping
*/ */
final boolean isCropping() { final boolean isCropping() {
return getView().getScaleX() > 1 || getView().getScaleY() > 1; // Account for some error
return getView().getScaleX() > 1.02f || getView().getScaleY() > 1.02f;
} }
} }

@ -1,6 +1,7 @@
package com.otaliastudios.cameraview; package com.otaliastudios.cameraview;
import android.content.Context; import android.content.Context;
import android.support.annotation.NonNull;
import android.view.Surface; import android.view.Surface;
import android.view.SurfaceHolder; import android.view.SurfaceHolder;
import android.view.SurfaceView; import android.view.SurfaceView;
@ -8,20 +9,24 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
// This is not used. // This is not used.
class SurfaceViewPreview extends Preview { class SurfaceViewPreview extends Preview<SurfaceView, SurfaceHolder> {
private final SurfaceView mSurfaceView;
SurfaceViewPreview(Context context, ViewGroup parent) { SurfaceViewPreview(Context context, ViewGroup parent, SurfaceCallback callback) {
super(context, parent); super(context, parent, callback);
final View view = View.inflate(context, R.layout.surface_view, parent); // MATCH_PARENT }
mSurfaceView = (SurfaceView) view.findViewById(R.id.surface_view);
final SurfaceHolder holder = mSurfaceView.getHolder(); @NonNull
@Override
protected SurfaceView onCreateView(Context context, ViewGroup parent) {
final View root = View.inflate(context, R.layout.surface_view, parent); // MATCH_PARENT
SurfaceView surface = root.findViewById(R.id.surface_view);
final SurfaceHolder holder = surface.getHolder();
holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
holder.addCallback(new SurfaceHolder.Callback() { holder.addCallback(new SurfaceHolder.Callback() {
@Override @Override
public void surfaceCreated(SurfaceHolder holder) { public void surfaceCreated(SurfaceHolder holder) {
onSurfaceAvailable(mSurfaceView.getWidth(), mSurfaceView.getHeight()); onSurfaceAvailable(getView().getWidth(), getView().getHeight());
} }
@Override @Override
@ -34,31 +39,22 @@ class SurfaceViewPreview extends Preview {
onSurfaceDestroyed(); onSurfaceDestroyed();
} }
}); });
return surface;
} }
@Override @Override
Surface getSurface() { Surface getSurface() {
return getSurfaceHolder().getSurface(); return getOutput().getSurface();
} }
@Override @Override
View getView() { SurfaceHolder getOutput() {
return mSurfaceView; return getView().getHolder();
} }
@Override @Override
SurfaceHolder getSurfaceHolder() { Class<SurfaceHolder> getOutputClass() {
return mSurfaceView.getHolder();
}
@Override
Class getOutputClass() {
return SurfaceHolder.class; return SurfaceHolder.class;
} }
@Override
boolean isReady() {
return mSurfaceView.getWidth() != 0 && mSurfaceView.getHeight() != 0;
}
} }

@ -58,8 +58,8 @@ class TapGestureLayout extends GestureLayout {
// Views to draw the focus marker. // Views to draw the focus marker.
LayoutInflater.from(getContext()).inflate(R.layout.layout_focus_marker, this); LayoutInflater.from(getContext()).inflate(R.layout.layout_focus_marker, this);
mFocusMarkerContainer = (FrameLayout) findViewById(R.id.focusMarkerContainer); mFocusMarkerContainer = findViewById(R.id.focusMarkerContainer);
mFocusMarkerFill = (ImageView) findViewById(R.id.fill); mFocusMarkerFill = findViewById(R.id.fill);
} }

@ -3,22 +3,26 @@ package com.otaliastudios.cameraview;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.Context; import android.content.Context;
import android.graphics.SurfaceTexture; import android.graphics.SurfaceTexture;
import android.support.annotation.NonNull;
import android.view.Surface; import android.view.Surface;
import android.view.TextureView; import android.view.TextureView;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@TargetApi(14) class TextureViewPreview extends Preview<TextureView, SurfaceTexture> {
class TextureViewPreview extends Preview {
private final TextureView mTextureView;
private Surface mSurface; private Surface mSurface;
TextureViewPreview(Context context, ViewGroup parent) { TextureViewPreview(Context context, ViewGroup parent, SurfaceCallback callback) {
super(context, parent); super(context, parent, callback);
final View view = View.inflate(context, R.layout.texture_view, parent); // MATCH_PARENT }
mTextureView = (TextureView) view.findViewById(R.id.texture_view);
mTextureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { @NonNull
@Override
protected TextureView onCreateView(Context context, ViewGroup parent) {
final View root = View.inflate(context, R.layout.texture_view, parent); // MATCH_PARENT
TextureView texture = root.findViewById(R.id.texture_view);
texture.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
@Override @Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
@ -40,42 +44,33 @@ class TextureViewPreview extends Preview {
public void onSurfaceTextureUpdated(SurfaceTexture surface) { public void onSurfaceTextureUpdated(SurfaceTexture surface) {
} }
}); });
return texture;
} }
@Override @Override
Surface getSurface() { Surface getSurface() {
if (mSurface == null) { // Check if valid? if (mSurface == null) { // Check if valid?
mSurface = new Surface(getSurfaceTexture()); mSurface = new Surface(getOutput());
} }
return mSurface; return mSurface;
} }
@Override @Override
View getView() { Class<SurfaceTexture> getOutputClass() {
return mTextureView;
}
@Override
Class getOutputClass() {
return SurfaceTexture.class; return SurfaceTexture.class;
} }
@Override @Override
boolean isReady() { SurfaceTexture getOutput() {
return getSurfaceTexture() != null; return getView().getSurfaceTexture();
}
@Override
SurfaceTexture getSurfaceTexture() {
return mTextureView.getSurfaceTexture();
} }
@TargetApi(15) @TargetApi(15)
@Override @Override
void setDesiredSize(int width, int height) { void setDesiredSize(int width, int height) {
super.setDesiredSize(width, height); super.setDesiredSize(width, height);
if (mTextureView.getSurfaceTexture() != null) { if (getView().getSurfaceTexture() != null) {
mTextureView.getSurfaceTexture().setDefaultBufferSize(width, height); getView().getSurfaceTexture().setDefaultBufferSize(width, height);
} }
} }

@ -6,16 +6,12 @@ coverage:
status: status:
project: project:
default: default:
enabled: yes target: 40%
target: 30%
patch: patch:
default: default:
enabled: yes
target: 60% target: 60%
changes: changes: no
default:
enabled: no
comment: comment:
# diff, changes, footer, reach, flags, suggestions, # diff, changes, footer, reach, flags, suggestions
layout: "header, files" layout: "header, files"

Loading…
Cancel
Save