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 &
script:
- ./gradlew clean testDebugUnitTest connectedCheck coverageReport
- ./gradlew clean testDebugUnitTest connectedCheck mergedCoverageReport
after_success:
- bash <(curl -s https://codecov.io/bash)
- bash <(curl -s https://codecov.io/bash) -s "*/build/reports/mergedCoverageReport/"
cache:
directories:

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

@ -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.content.Context;
import android.hardware.Camera;
import android.location.Location;
import android.os.Looper;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.MediumTest;
import android.support.test.runner.AndroidJUnit4;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
@ -25,15 +27,17 @@ import static android.view.ViewGroup.LayoutParams.*;
@RunWith(AndroidJUnit4.class)
@MediumTest
public class CameraViewTest {
public class CameraViewTest extends BaseTest {
private CameraView cameraView;
private MockCameraController mockController;
private Preview mockPreview;
private boolean hasPermissions;
@Before
public void setUp() {
InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
ui(new Runnable() {
@Override
public void run() {
Context context = InstrumentationRegistry.getContext();
@ -49,6 +53,11 @@ public class CameraViewTest {
mockPreview = new MockPreview(context, container);
return mockPreview;
}
@Override
protected boolean checkPermissions(SessionType sessionType) {
return hasPermissions;
}
};
}
});
@ -59,12 +68,13 @@ public class CameraViewTest {
cameraView = null;
mockController = null;
mockPreview = null;
hasPermissions = false;
}
//region testDefaults
@Test
public void testNotStarted() {
public void testNullBeforeStart() {
assertFalse(cameraView.isStarted());
assertNull(cameraView.getCameraOptions());
assertNull(cameraView.getExtraProperties());
@ -96,6 +106,23 @@ public class CameraViewTest {
assertEquals(cameraView.getGestureAction(Gesture.SCROLL_VERTICAL), GestureAction.DEFAULT_SCROLL_VERTICAL);
}
@Test
public void testStartWithPermissions() {
hasPermissions = true;
cameraView.start();
assertTrue(cameraView.isStarted());
cameraView.stop();
assertFalse(cameraView.isStarted());
}
@Test
public void testStartWithoutPermissions() {
hasPermissions = false;
cameraView.start();
assertFalse(cameraView.isStarted());
}
//endregion
//region testGesture
@ -145,6 +172,94 @@ public class CameraViewTest {
//endregion
//region testGestureAction
@Test
public void testGestureAction_capture() {
MotionEvent event = MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0);
ui(new Runnable() {
@Override
public void run() {
cameraView.mTapGestureLayout = new TapGestureLayout(cameraView.getContext()) {
public boolean onTouchEvent(MotionEvent event) { return true; }
};
cameraView.mTapGestureLayout.setGestureType(Gesture.TAP);
}
});
cameraView.mapGesture(Gesture.TAP, GestureAction.CAPTURE);
cameraView.dispatchTouchEvent(event);
assertTrue(mockController.mPictureCaptured);
}
@Test
public void testGestureAction_focus() {
MotionEvent event = MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0);
ui(new Runnable() {
@Override
public void run() {
cameraView.mTapGestureLayout = new TapGestureLayout(cameraView.getContext()) {
public boolean onTouchEvent(MotionEvent event) { return true; }
};
cameraView.mTapGestureLayout.setGestureType(Gesture.TAP);
}
});
mockController.mFocusStarted = false;
cameraView.mapGesture(Gesture.TAP, GestureAction.FOCUS);
cameraView.dispatchTouchEvent(event);
assertTrue(mockController.mFocusStarted);
// Try with FOCUS_WITH_MARKER
mockController.mFocusStarted = false;
cameraView.mapGesture(Gesture.TAP, GestureAction.FOCUS_WITH_MARKER);
cameraView.dispatchTouchEvent(event);
assertTrue(mockController.mFocusStarted);
}
@Test
public void testGestureAction_zoom() {
MotionEvent event = MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0);
ui(new Runnable() {
@Override
public void run() {
cameraView.mPinchGestureLayout = new PinchGestureLayout(cameraView.getContext()) {
public boolean onTouchEvent(MotionEvent event) { return true; }
};
cameraView.mPinchGestureLayout.setGestureType(Gesture.PINCH);
}
});
mockController.mZoomChanged = false;
cameraView.mapGesture(Gesture.PINCH, GestureAction.ZOOM);
cameraView.dispatchTouchEvent(event);
assertTrue(mockController.mZoomChanged);
}
@Test
public void testGestureAction_exposureCorrection() {
// This needs a valid CameraOptions value.
CameraOptions o = mock(CameraOptions.class);
when(o.getExposureCorrectionMinValue()).thenReturn(-10f);
when(o.getExposureCorrectionMaxValue()).thenReturn(10f);
mockController.setMockCameraOptions(o);
MotionEvent event = MotionEvent.obtain(0L, 0L, 0, 0f, 0f, 0);
ui(new Runnable() {
@Override
public void run() {
cameraView.mScrollGestureLayout = new ScrollGestureLayout(cameraView.getContext()) {
public boolean onTouchEvent(MotionEvent event) { return true; }
};
cameraView.mScrollGestureLayout.setGestureType(Gesture.SCROLL_HORIZONTAL);
}
});
mockController.mExposureCorrectionChanged = false;
cameraView.mapGesture(Gesture.SCROLL_HORIZONTAL, GestureAction.EXPOSURE_CORRECTION);
cameraView.dispatchTouchEvent(event);
assertTrue(mockController.mExposureCorrectionChanged);
}
//endregion
//region testMeasure
private void mockPreviewSize() {
@ -268,11 +383,34 @@ public class CameraViewTest {
//endregion
// TODO: test setLocation
//region testLocation
// TODO: test touch events
@Test
public void testSetLocation() {
cameraView.setLocation(50d, -50d);
assertEquals(50d, mockController.mLocation.getLatitude(), 0);
assertEquals(-50d, mockController.mLocation.getLongitude(), 0);
assertEquals(0, mockController.mLocation.getAltitude(), 0);
assertEquals("Unknown", mockController.mLocation.getProvider());
assertEquals(System.currentTimeMillis(), mockController.mLocation.getTime(), 1000f);
Location source = new Location("Provider");
source.setTime(5000);
source.setLatitude(10d);
source.setLongitude(-10d);
source.setAltitude(50d);
cameraView.setLocation(source);
Location other = mockController.mLocation;
assertEquals(10d, other.getLatitude(), 0d);
assertEquals(-10d, other.getLongitude(), 0d);
assertEquals(50d, other.getAltitude(), 0d);
assertEquals("Provider", other.getProvider());
assertEquals(5000, other.getTime());
}
//endregion
// TODO: test permissions and start() stop() isStarted()
// TODO: test permissions
// TODO: test CameraCallbacks

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

@ -2,33 +2,36 @@ package com.otaliastudios.cameraview;
import android.content.Context;
import android.support.annotation.NonNull;
import android.view.Surface;
import android.view.View;
import android.view.ViewGroup;
public class MockPreview extends Preview {
public class MockPreview extends Preview<View, Void> {
public MockPreview(Context context, ViewGroup parent) {
super(context, parent);
MockPreview(Context context, ViewGroup parent) {
super(context, parent, null);
}
@NonNull
@Override
Surface getSurface() {
return null;
protected View onCreateView(Context context, ViewGroup parent) {
return new View(context);
}
@Override
View getView() {
Surface getSurface() {
return null;
}
@Override
Class getOutputClass() {
Class<Void> getOutputClass() {
return null;
}
@Override
boolean isReady() {
return true;
Void getOutput() {
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.Rect;
import android.graphics.SurfaceTexture;
import android.graphics.YuvImage;
import android.hardware.Camera;
import android.location.Location;
@ -110,12 +111,14 @@ class Camera1 extends CameraController {
// These can happen at different times but we want to end up here.
private void setup() {
try {
Object output = mPreview.getOutput();
if (mPreview.getOutputClass() == SurfaceHolder.class) {
mCamera.setPreviewDisplay(mPreview.getSurfaceHolder());
mCamera.setPreviewDisplay((SurfaceHolder) output);
} else {
mCamera.setPreviewTexture(mPreview.getSurfaceTexture());
mCamera.setPreviewTexture((SurfaceTexture) output);
}
} catch (IOException e) {
LOG.e("Error while trying to setup Camera1.", e);
throw new RuntimeException(e);
}
@ -450,8 +453,10 @@ class Camera1 extends CameraController {
@Override
boolean shouldFlipSizes() {
int offset = computeSensorToDisplayOffset();
LOG.i("shouldFlip:", "mDeviceOrientation=", mDeviceOrientation, "mSensorOffset=", mSensorOffset);
return (mDeviceOrientation + mSensorOffset) % 180 != 0;
LOG.i("shouldFlip:", "sensorToDisplay=", offset);
return offset % 180 != 0;
}
@Override

@ -159,7 +159,7 @@ public class CameraView extends FrameLayout {
protected Preview instantiatePreview(Context context, ViewGroup container) {
return new TextureViewPreview(context, container);
return new TextureViewPreview(context, container, null);
}
@ -504,7 +504,7 @@ public class CameraView extends FrameLayout {
* @return true if we can go on, false otherwise.
*/
@SuppressLint("NewApi")
private boolean checkPermissions(SessionType sessionType) {
protected boolean checkPermissions(SessionType sessionType) {
checkPermissionsManifestOrThrow(sessionType);
boolean api23 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
int cameraCheck, audioCheck;
@ -537,7 +537,7 @@ public class CameraView extends FrameLayout {
/**
* If mSessionType == SESSION_TYPE_VIDEO we will ask for RECORD_AUDIO permission.
* If the developer did not add this to its manifest, throw and fire warnings.
* (Hoping this is not cought elsewhere... we should test).
* (Hoping this is not caught elsewhere... we should test).
*/
private void checkPermissionsManifestOrThrow(SessionType sessionType) {
if (sessionType == SessionType.VIDEO) {

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

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

@ -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;
}
// For tests.
void setGestureType(Gesture type) {
mType = type;
}
public final PointF[] getPoints() {
return mPoints;
}

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

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

@ -1,15 +1,17 @@
package com.otaliastudios.cameraview;
import android.content.Context;
import android.graphics.SurfaceTexture;
import android.support.annotation.NonNull;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.View;
import android.view.ViewGroup;
abstract class Preview {
abstract class Preview<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.
// After that, CameraView will need a new layout pass to adapt to the Preview size.
@ -19,6 +21,7 @@ abstract class Preview {
}
private SurfaceCallback mSurfaceCallback;
private T mView;
// As far as I can see, these are the view/surface dimensions.
// This live in the 'View' orientation.
@ -29,19 +32,24 @@ abstract class Preview {
private int mDesiredWidth;
private int mDesiredHeight;
Preview(Context context, ViewGroup parent) {}
Preview(Context context, ViewGroup parent, SurfaceCallback callback) {
mView = onCreateView(context, parent);
mSurfaceCallback = callback;
}
@NonNull
protected abstract T onCreateView(Context context, ViewGroup parent);
abstract Surface getSurface();
abstract View getView();
abstract Class getOutputClass();
abstract boolean isReady();
SurfaceHolder getSurfaceHolder() {
return null;
}
SurfaceTexture getSurfaceTexture() {
return null;
@NonNull
final T getView() {
return mView;
}
abstract Class<Output> getOutputClass();
abstract Output getOutput();
// As far as I can see, these are the actual preview dimensions, as set in CameraParameters.
// This is called by the CameraImpl.
@ -53,12 +61,20 @@ abstract class Preview {
crop();
}
final Size getDesiredSize() {
return new Size(mDesiredWidth, mDesiredHeight);
}
final Size getSurfaceSize() {
return new Size(mSurfaceWidth, mSurfaceHeight);
}
final void setSurfaceCallback(SurfaceCallback callback) {
mSurfaceCallback = callback;
// If surface already available, dispatch.
if (mSurfaceWidth != 0 || mSurfaceHeight != 0) {
mSurfaceCallback.onSurfaceAvailable();
}
}
@ -83,13 +99,14 @@ abstract class Preview {
}
}
protected final void onSurfaceDestroyed() {
mSurfaceWidth = 0;
mSurfaceHeight = 0;
crop();
}
final boolean isReady() {
return mSurfaceWidth > 0 && mSurfaceHeight > 0;
}
/**
* Here we must crop the visible part by applying a > 1 scale to one of our
@ -100,11 +117,16 @@ abstract class Preview {
* However that should be already managed by the framework.
*/
private final void crop() {
mCropTask.start();
getView().post(new Runnable() {
@Override
public void run() {
if (mDesiredHeight == 0 || mDesiredWidth == 0) return;
if (mSurfaceHeight == 0 || mSurfaceWidth == 0) return;
if (mDesiredHeight == 0 || mDesiredWidth == 0 ||
mSurfaceHeight == 0 || mSurfaceWidth == 0) {
mCropTask.end(null);
return;
}
float scaleX = 1f, scaleY = 1f;
AspectRatio current = AspectRatio.of(mSurfaceWidth, mSurfaceHeight);
AspectRatio target = AspectRatio.of(mDesiredWidth, mDesiredHeight);
@ -119,6 +141,7 @@ abstract class Preview {
getView().setScaleY(scaleY);
LOG.i("crop:", "applied scaleX=", scaleX);
LOG.i("crop:", "applied scaleY=", scaleY);
mCropTask.end(null);
}
});
}
@ -130,6 +153,7 @@ abstract class Preview {
* @return true if cropping
*/
final boolean isCropping() {
return getView().getScaleX() > 1 || getView().getScaleY() > 1;
// Account for some error
return getView().getScaleX() > 1.02f || getView().getScaleY() > 1.02f;
}
}

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

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

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

Loading…
Cancel
Save