diff --git a/.travis.yml b/.travis.yml
index c7c60469..115669ba 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -30,7 +30,7 @@ android:
before_script:
- echo no | android create avd --force --name test --target android-$EMULATOR_API --abi $EMULATOR_ABI
- - emulator -avd test -no-audio -no-window &
+ - emulator -avd test -no-audio -no-window -camera-back emulated -camera-front emulated &
- android-wait-for-emulator
- adb shell input keyevent 82 &
diff --git a/README.md b/README.md
index d123e79a..9c58db39 100644
--- a/README.md
+++ b/README.md
@@ -422,6 +422,7 @@ Other APIs not mentioned above are provided, and are well documented and comment
|`toggleFlash()`|Toggles the flash value between `Flash.OFF`, `Flash.ON`, and `Flash.AUTO`.|
|`setLocation(Location)`|Sets location data to be appended to picture/video metadata.|
|`setLocation(double, double)`|Sets latitude and longitude to be appended to picture/video metadata.|
+|`getLocation()`|Retrieves location data previously applied with setLocation().|
|`startAutoFocus(float, float)`|Starts an autofocus process at the given coordinates, with respect to the view dimensions.|
|`getPreviewSize()`|Returns the size of the preview surface. If CameraView was not constrained in its layout phase (e.g. it was `wrap_content`), this will return the same aspect ratio of CameraView.|
|`getSnapshotSize()`|Returns `getPreviewSize()`, since a snapshot is a preview frame.|
diff --git a/cameraview/src/androidTest/AndroidManifest.xml b/cameraview/src/androidTest/AndroidManifest.xml
index 1795db1e..a5482493 100644
--- a/cameraview/src/androidTest/AndroidManifest.xml
+++ b/cameraview/src/androidTest/AndroidManifest.xml
@@ -6,6 +6,7 @@
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseTest.java
index bcaa0f69..589a8d0c 100644
--- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseTest.java
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/BaseTest.java
@@ -9,6 +9,7 @@ import android.os.Handler;
import android.os.Looper;
import android.support.test.InstrumentationRegistry;
import android.support.test.annotation.UiThreadTest;
+import android.support.test.espresso.core.internal.deps.guava.collect.ObjectArrays;
import android.support.test.rule.ActivityTestRule;
import android.view.View;
@@ -16,12 +17,15 @@ import org.junit.Before;
import org.junit.Rule;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
+import org.mockito.stubbing.Stubber;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
+import java.util.concurrent.CountDownLatch;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -75,4 +79,36 @@ public class BaseTest {
});
return y;
}
+
+ public static Stubber doCountDown(final CountDownLatch latch) {
+ return doAnswer(new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ latch.countDown();
+ return null;
+ }
+ });
+ }
+
+ public static Stubber doEndTask(final Task task, final T response) {
+ return doAnswer(new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ task.end(response);
+ return null;
+ }
+ });
+ }
+
+ public static Stubber doEndTask(final Task task, final int withReturnArgument) {
+ return doAnswer(new Answer() {
+ @Override
+ public Object answer(InvocationOnMock invocation) throws Throwable {
+ Object o = invocation.getArguments()[withReturnArgument];
+ //noinspection unchecked
+ task.end(o);
+ return null;
+ }
+ });
+ }
}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraControllerIntegrationTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraControllerIntegrationTest.java
new file mode 100644
index 00000000..a7482168
--- /dev/null
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraControllerIntegrationTest.java
@@ -0,0 +1,305 @@
+package com.otaliastudios.cameraview;
+
+
+import android.content.Context;
+import android.support.test.filters.MediumTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static org.mockito.Mockito.*;
+import static org.junit.Assert.*;
+
+
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class CameraControllerIntegrationTest extends BaseTest {
+
+ @Rule
+ public ActivityTestRule rule = new ActivityTestRule<>(TestActivity.class);
+
+ private CameraView camera;
+ private Camera1 controller;
+ private CameraListener listener;
+
+ @Before
+ public void setUp() {
+ ui(new Runnable() {
+ @Override
+ public void run() {
+ camera = new CameraView(rule.getActivity()) {
+
+ @Override
+ protected CameraController instantiateCameraController(CameraCallbacks callbacks, Preview preview) {
+ controller = new Camera1(callbacks, preview);
+ return controller;
+ }
+ };
+
+ listener = mock(CameraListener.class);
+ camera.addCameraListener(listener);
+ rule.getActivity().inflate(camera);
+ }
+ });
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ camera.stopCapturingVideo();
+ camera.stop();
+ Thread.sleep(800); // Just to be sure it's released before next test.
+ }
+
+ private CameraOptions waitForOpen(boolean expectSuccess) {
+ final Task open = new Task<>();
+ open.listen();
+ doEndTask(open, 0).when(listener).onCameraOpened(any(CameraOptions.class));
+ CameraOptions result = open.await(1000);
+ if (expectSuccess) {
+ assertNotNull("Can open", result);
+ } else {
+ assertNull("Should not open", result);
+ }
+ return result;
+ }
+
+ private Boolean waitForClose(boolean expectSuccess) {
+ final Task close = new Task<>();
+ close.listen();
+ doEndTask(close, true).when(listener).onCameraClosed();
+ Boolean result = close.await(1000);
+ if (expectSuccess) {
+ assertNotNull("Can close", result);
+ } else {
+ assertNull("Should not close", result);
+ }
+ return result;
+ }
+
+ //region test open/close
+
+ @Test
+ public void testOpenClose() {
+ assertFalse(controller.isCameraAvailable());
+
+ camera.start();
+ waitForOpen(true);
+ assertTrue(controller.isCameraAvailable());
+
+ camera.stop();
+ waitForClose(true);
+ assertFalse(controller.isCameraAvailable());
+ }
+
+ @Test
+ public void testOpenTwice() {
+ camera.start();
+ waitForOpen(true);
+ waitForOpen(false);
+ }
+
+ @Test
+ public void testCloseTwice() {
+ camera.stop();
+ waitForClose(false);
+ }
+
+ @Test
+ public void testConcurrentCalls() throws Exception {
+ final CountDownLatch latch = new CountDownLatch(4);
+ doCountDown(latch).when(listener).onCameraOpened(any(CameraOptions.class));
+ doCountDown(latch).when(listener).onCameraClosed();
+
+ camera.start();
+ camera.stop();
+ camera.start();
+ camera.stop();
+
+ boolean did = latch.await(4, TimeUnit.SECONDS);
+ assertTrue("Handles concurrent calls to start & stop", did);
+ }
+
+ @Test
+ public void testStartInitializesOptions() {
+ assertNull(camera.getCameraOptions());
+ assertNull(camera.getExtraProperties());
+ camera.start();
+ waitForOpen(true);
+ assertNotNull(camera.getCameraOptions());
+ assertNotNull(camera.getExtraProperties());
+ }
+
+ //endregion
+
+ //region test Facing/SessionType
+ // Test things that should reset the camera.
+
+ @Test
+ public void testSetFacing() throws Exception {
+ camera.setFacing(Facing.BACK);
+ camera.start();
+ waitForOpen(true);
+
+ // set facing should call stop and start again.
+ final CountDownLatch latch = new CountDownLatch(2);
+ doCountDown(latch).when(listener).onCameraOpened(any(CameraOptions.class));
+ doCountDown(latch).when(listener).onCameraClosed();
+
+ camera.setFacing(Facing.FRONT);
+
+ boolean did = latch.await(2, TimeUnit.SECONDS);
+ assertTrue("Handles setFacing while active", did);
+ assertEquals(camera.getFacing(), Facing.FRONT);
+ }
+
+ @Test
+ public void testSetSessionType() throws Exception {
+ camera.setSessionType(SessionType.PICTURE);
+ camera.start();
+ waitForOpen(true);
+
+ // set session type should call stop and start again.
+ final CountDownLatch latch = new CountDownLatch(2);
+ doCountDown(latch).when(listener).onCameraOpened(any(CameraOptions.class));
+ doCountDown(latch).when(listener).onCameraClosed();
+
+ camera.setSessionType(SessionType.VIDEO);
+
+ boolean did = latch.await(2, TimeUnit.SECONDS);
+ assertTrue("Handles setSessionType while active", did);
+ assertEquals(camera.getSessionType(), SessionType.VIDEO);
+ }
+
+ //endregion
+
+ //region test Set Parameters
+ // When camera is open, parameters will be set only if supported.
+
+ @Test
+ public void testSetZoom() {
+ camera.start();
+ CameraOptions options = waitForOpen(true);
+ boolean can = options.isZoomSupported();
+ float oldValue = camera.getZoom();
+ float newValue = 0.65f;
+ camera.setZoom(newValue);
+ assertEquals(can ? newValue : oldValue, camera.getZoom(), 0f);
+ }
+
+ @Test
+ public void testSetExposureCorrection() {
+ camera.start();
+ CameraOptions options = waitForOpen(true);
+ boolean can = options.isExposureCorrectionSupported();
+ float oldValue = camera.getExposureCorrection();
+ float newValue = options.getExposureCorrectionMaxValue();
+ camera.setExposureCorrection(newValue);
+ assertEquals(can ? newValue : oldValue, camera.getExposureCorrection(), 0f);
+ }
+
+ @Test
+ public void testSetFlash() {
+ camera.start();
+ CameraOptions options = waitForOpen(true);
+ Flash[] values = Flash.values();
+ Flash oldValue = camera.getFlash();
+ for (Flash value : values) {
+ camera.setFlash(value);
+ if (options.supports(value)) {
+ assertEquals(camera.getFlash(), value);
+ } else {
+ assertEquals(camera.getFlash(), oldValue);
+ }
+ }
+ }
+
+ @Test
+ public void testSetWhiteBalance() {
+ camera.start();
+ CameraOptions options = waitForOpen(true);
+ WhiteBalance[] values = WhiteBalance.values();
+ WhiteBalance oldValue = camera.getWhiteBalance();
+ for (WhiteBalance value : values) {
+ camera.setWhiteBalance(value);
+ if (options.supports(value)) {
+ assertEquals(camera.getWhiteBalance(), value);
+ } else {
+ assertEquals(camera.getWhiteBalance(), oldValue);
+ }
+ }
+ }
+
+ @Test
+ public void testSetHdr() {
+ camera.start();
+ CameraOptions options = waitForOpen(true);
+ WhiteBalance[] values = WhiteBalance.values();
+ WhiteBalance oldValue = camera.getWhiteBalance();
+ for (WhiteBalance value : values) {
+ camera.setWhiteBalance(value);
+ if (options.supports(value)) {
+ assertEquals(camera.getWhiteBalance(), value);
+ } else {
+ assertEquals(camera.getWhiteBalance(), oldValue);
+ }
+ }
+ }
+
+ //endregion
+
+ //region testSetVideoQuality
+ // This can be tricky because can trigger layout changes.
+
+ @Test(expected = IllegalStateException.class)
+ public void testSetVideoQuality_whileRecording() {
+ camera.setSessionType(SessionType.VIDEO);
+ camera.setVideoQuality(VideoQuality.HIGHEST);
+ camera.start();
+ waitForOpen(true);
+ camera.startCapturingVideo(null);
+ camera.setVideoQuality(VideoQuality.LOWEST);
+ }
+
+ @Test
+ public void testSetVideoQuality_whileInPictureSessionType() {
+ camera.setSessionType(SessionType.PICTURE);
+ camera.setVideoQuality(VideoQuality.HIGHEST);
+ camera.start();
+ waitForOpen(true);
+ camera.setVideoQuality(VideoQuality.LOWEST);
+ assertEquals(camera.getVideoQuality(), VideoQuality.LOWEST);
+ }
+
+ @Test
+ public void testSetVideoQuality_whileNotStarted() {
+ camera.setVideoQuality(VideoQuality.HIGHEST);
+ assertEquals(camera.getVideoQuality(), VideoQuality.HIGHEST);
+ camera.setVideoQuality(VideoQuality.LOWEST);
+ assertEquals(camera.getVideoQuality(), VideoQuality.LOWEST);
+ }
+
+ @Test
+ public void testSetVideoQuality_shouldRecompute() {
+ // If video quality changes bring to a new capture size,
+ // this might bring to a new aspect ratio,
+ // which might bring to a new preview size. No idea how to test.
+ assertTrue(true);
+ }
+
+ //endregion
+}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java
index 105f6f3c..7865a81f 100644
--- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java
@@ -93,6 +93,7 @@ public class CameraViewTest extends BaseTest {
assertEquals(cameraView.getSessionType(), SessionType.DEFAULT);
assertEquals(cameraView.getHdr(), Hdr.DEFAULT);
assertEquals(cameraView.getVideoQuality(), VideoQuality.DEFAULT);
+ assertEquals(cameraView.getLocation(), null);
// Self managed
assertEquals(cameraView.getExposureCorrection(), 0f, 0f);
@@ -410,7 +411,7 @@ public class CameraViewTest extends BaseTest {
source.setLongitude(-10d);
source.setAltitude(50d);
cameraView.setLocation(source);
- Location other = mockController.mLocation;
+ Location other = cameraView.getLocation();
assertEquals(10d, other.getLatitude(), 0d);
assertEquals(-10d, other.getLongitude(), 0d);
assertEquals(50d, other.getAltitude(), 0d);
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java
index ce150fd2..19200f76 100644
--- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/MockCameraController.java
@@ -10,7 +10,6 @@ import java.io.File;
public class MockCameraController extends CameraController {
- Location mLocation;
boolean mPictureCaptured;
boolean mFocusStarted;
boolean mZoomChanged;
@@ -110,7 +109,7 @@ public class MockCameraController extends CameraController {
}
@Override
- boolean isCameraOpened() {
+ boolean isCameraAvailable() {
return true;
}
diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/WorkerHandlerTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/WorkerHandlerTest.java
index d0d52762..b179e3e2 100644
--- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/WorkerHandlerTest.java
+++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/WorkerHandlerTest.java
@@ -21,4 +21,20 @@ public class WorkerHandlerTest {
assertTrue(w1 == w1a);
assertFalse(w1 == w2);
}
+
+ @Test
+ public void testStaticRun() {
+ final Task task = new Task<>();
+ task.listen();
+ Runnable action = new Runnable() {
+ @Override
+ public void run() {
+ task.end(true);
+ }
+ };
+ WorkerHandler.run(action);
+ Boolean result = task.await(500);
+ assertNotNull(result);
+ assertTrue(result);
+ }
}
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java
index a039ada2..f32b5f24 100644
--- a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java
+++ b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java
@@ -9,11 +9,9 @@ import android.location.Location;
import android.media.CamcorderProfile;
import android.media.MediaRecorder;
import android.os.Build;
-import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread;
-import android.util.Log;
import android.view.SurfaceHolder;
import java.io.File;
@@ -36,13 +34,11 @@ class Camera1 extends CameraController {
private int mSensorOffset;
- private Location mLocation;
-
private final int mPostFocusResetDelay = 3000;
private Runnable mPostFocusResetRunnable = new Runnable() {
@Override
public void run() {
- if (!isCameraOpened()) return;
+ if (!isCameraAvailable()) return;
mCamera.cancelAutoFocus();
synchronized (mLock) {
Camera.Parameters params = mCamera.getParameters();
@@ -104,7 +100,7 @@ class Camera1 extends CameraController {
}
private boolean shouldSetup() {
- return isCameraOpened() && mPreview.isReady() && !mIsSetup;
+ return isCameraAvailable() && mPreview.isReady() && !mIsSetup;
}
// The act of binding an "open" camera to a "ready" preview.
@@ -144,7 +140,7 @@ class Camera1 extends CameraController {
@WorkerThread
@Override
void onStart() {
- if (isCameraOpened()) onStop();
+ if (isCameraAvailable()) onStop();
if (collectCameraId()) {
mCamera = Camera.open(mCameraId);
@@ -172,7 +168,7 @@ class Camera1 extends CameraController {
@Override
void onStop() {
mHandler.get().removeCallbacks(mPostFocusResetRunnable);
- if (isCameraOpened()) {
+ if (isCameraAvailable()) {
if (mIsCapturingVideo) endVideo();
mCamera.stopPreview();
mCamera.release();
@@ -205,7 +201,7 @@ class Camera1 extends CameraController {
void setSessionType(SessionType sessionType) {
if (sessionType != mSessionType) {
mSessionType = sessionType;
- if (isCameraOpened()) {
+ if (isCameraAvailable()) {
start();
}
}
@@ -215,7 +211,7 @@ class Camera1 extends CameraController {
void setLocation(Location location) {
Location oldLocation = mLocation;
mLocation = location;
- if (isCameraOpened()) {
+ if (isCameraAvailable()) {
synchronized (mLock) {
Camera.Parameters params = mCamera.getParameters();
if (mergeLocation(params, oldLocation)) mCamera.setParameters(params);
@@ -243,7 +239,7 @@ class Camera1 extends CameraController {
void setFacing(Facing facing) {
if (facing != mFacing) {
mFacing = facing;
- if (collectCameraId() && isCameraOpened()) {
+ if (collectCameraId() && isCameraAvailable()) {
start();
}
}
@@ -253,7 +249,7 @@ class Camera1 extends CameraController {
void setWhiteBalance(WhiteBalance whiteBalance) {
WhiteBalance old = mWhiteBalance;
mWhiteBalance = whiteBalance;
- if (isCameraOpened()) {
+ if (isCameraAvailable()) {
synchronized (mLock) {
Camera.Parameters params = mCamera.getParameters();
if (mergeWhiteBalance(params, old)) mCamera.setParameters(params);
@@ -274,7 +270,7 @@ class Camera1 extends CameraController {
void setHdr(Hdr hdr) {
Hdr old = mHdr;
mHdr = hdr;
- if (isCameraOpened()) {
+ if (isCameraAvailable()) {
synchronized (mLock) {
Camera.Parameters params = mCamera.getParameters();
if (mergeHdr(params, old)) mCamera.setParameters(params);
@@ -295,7 +291,7 @@ class Camera1 extends CameraController {
void setFlash(Flash flash) {
Flash old = mFlash;
mFlash = flash;
- if (isCameraOpened()) {
+ if (isCameraAvailable()) {
synchronized (mLock) {
Camera.Parameters params = mCamera.getParameters();
if (mergeFlash(params, old)) mCamera.setParameters(params);
@@ -348,7 +344,7 @@ class Camera1 extends CameraController {
}
mVideoQuality = videoQuality;
- if (isCameraOpened() && mSessionType == SessionType.VIDEO) {
+ if (isCameraAvailable() && mSessionType == SessionType.VIDEO) {
// Change capture size to a size that fits the video aspect ratio.
Size oldSize = mCaptureSize;
mCaptureSize = computeCaptureSize();
@@ -370,7 +366,7 @@ class Camera1 extends CameraController {
@Override
boolean capturePicture() {
if (mIsCapturingImage) return false;
- if (!isCameraOpened()) return false;
+ if (!isCameraAvailable()) return false;
if (mSessionType == SessionType.VIDEO && mIsCapturingVideo) {
if (!mOptions.isVideoSnapshotSupported()) return false;
}
@@ -410,7 +406,7 @@ class Camera1 extends CameraController {
@Override
boolean captureSnapshot() {
- if (!isCameraOpened()) return false;
+ if (!isCameraAvailable()) return false;
if (mIsCapturingImage) return false;
// This won't work while capturing a video.
// Switch to capturePicture.
@@ -435,7 +431,7 @@ class Camera1 extends CameraController {
final int postWidth = flip ? preHeight : preWidth;
final int postHeight = flip ? preWidth : preHeight;
final int format = params.getPreviewFormat();
- new Thread(new Runnable() {
+ WorkerHandler.run(new Runnable() {
@Override
public void run() {
@@ -445,7 +441,7 @@ class Camera1 extends CameraController {
mCameraCallbacks.processSnapshot(yuv, consistentWithView, exifFlip);
mIsCapturingImage = false;
}
- }).start();
+ });
}
});
return true;
@@ -460,7 +456,7 @@ class Camera1 extends CameraController {
}
@Override
- boolean isCameraOpened() {
+ boolean isCameraAvailable() {
return mCamera != null;
}
@@ -548,7 +544,7 @@ class Camera1 extends CameraController {
boolean startVideo(@NonNull File videoFile) {
mVideoFile = videoFile;
if (mIsCapturingVideo) return false;
- if (!isCameraOpened()) return false;
+ if (!isCameraAvailable()) return false;
Camera.Parameters params = mCamera.getParameters();
params.setVideoStabilization(false);
if (mSessionType == SessionType.VIDEO) {
@@ -573,9 +569,17 @@ class Camera1 extends CameraController {
boolean endVideo() {
if (mIsCapturingVideo) {
mIsCapturingVideo = false;
- mMediaRecorder.stop();
- mMediaRecorder.release();
- mMediaRecorder = null;
+ if (mMediaRecorder != null) {
+ try {
+ mMediaRecorder.stop();
+ mMediaRecorder.release();
+ } catch (Exception e) {
+ // This can happen if endVideo() is called right after startVideo().
+ // We don't care.
+ LOG.w("Error while closing media recorder.", e);
+ }
+ mMediaRecorder = null;
+ }
if (mVideoFile != null) {
mCameraCallbacks.dispatchOnVideoTaken(mVideoFile);
mVideoFile = null;
@@ -655,7 +659,7 @@ class Camera1 extends CameraController {
@Override
boolean setZoom(float zoom) {
- if (!isCameraOpened()) return false;
+ if (!isCameraAvailable()) return false;
if (!mOptions.isZoomSupported()) return false;
synchronized (mLock) {
Camera.Parameters params = mCamera.getParameters();
@@ -669,7 +673,7 @@ class Camera1 extends CameraController {
@Override
boolean setExposureCorrection(float EVvalue) {
- if (!isCameraOpened()) return false;
+ if (!isCameraAvailable()) return false;
if (!mOptions.isExposureCorrectionSupported()) return false;
float max = mOptions.getExposureCorrectionMaxValue();
float min = mOptions.getExposureCorrectionMinValue();
@@ -689,7 +693,7 @@ class Camera1 extends CameraController {
@Override
boolean startAutoFocus(@Nullable final Gesture gesture, PointF point) {
- if (!isCameraOpened()) return false;
+ if (!isCameraAvailable()) return false;
if (!mOptions.isAutoFocusSupported()) return false;
final PointF p = new PointF(point.x, point.y); // copy.
List meteringAreas2 = computeMeteringAreas(p.x, p.y);
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera2.java b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera2.java
index 18a2d487..b80d0737 100644
--- a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera2.java
+++ b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera2.java
@@ -11,15 +11,12 @@ import android.hardware.camera2.CameraManager;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.location.Location;
import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread;
-import android.util.Log;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
-import java.util.TreeSet;
@TargetApi(21)
class Camera2 extends CameraController {
@@ -90,15 +87,6 @@ class Camera2 extends CameraController {
}
- @Override
- void onDisplayOffset(int displayOrientation) {
-
- }
-
- @Override
- void onDeviceOrientation(int deviceOrientation) {
-
- }
@Override
void setFacing(Facing facing) {
@@ -131,7 +119,7 @@ class Camera2 extends CameraController {
// }
// }
- if (mFacing == facing && isCameraOpened()) {
+ if (mFacing == facing && isCameraAvailable()) {
stop();
start();
}
@@ -194,7 +182,7 @@ class Camera2 extends CameraController {
}
@Override
- boolean isCameraOpened() {
+ boolean isCameraAvailable() {
return mCamera != null;
}
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java
index 0432fcbc..252474a6 100644
--- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java
+++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java
@@ -19,6 +19,7 @@ abstract class CameraController implements Preview.SurfaceCallback {
protected VideoQuality mVideoQuality;
protected SessionType mSessionType;
protected Hdr mHdr;
+ protected Location mLocation;
protected Size mCaptureSize;
protected Size mPreviewSize;
@@ -68,6 +69,11 @@ abstract class CameraController implements Preview.SurfaceCallback {
@WorkerThread
abstract void onStop();
+ // Returns whether the camera is available (started),
+ // so we can start setting parameters on it.
+ // Preview surface might still be off at this point.
+ abstract boolean isCameraAvailable();
+
//endregion
//region Rotation callbacks
@@ -85,24 +91,34 @@ abstract class CameraController implements Preview.SurfaceCallback {
//region Abstract setParameters
+ // Should restart the session if active.
+ abstract void setSessionType(SessionType sessionType);
+
+ // Should restart the session if active.
+ abstract void setFacing(Facing facing);
+
+ // If opened and supported, apply and return true.
abstract boolean setZoom(float zoom);
+ // If opened and supported, apply and return true.
abstract boolean setExposureCorrection(float EVvalue);
- abstract void setFacing(Facing facing);
-
+ // If closed, keep. If opened, check supported and apply.
abstract void setFlash(Flash flash);
+ // If closed, keep. If opened, check supported and apply.
abstract void setWhiteBalance(WhiteBalance whiteBalance);
- abstract void setVideoQuality(VideoQuality videoQuality);
-
- abstract void setSessionType(SessionType sessionType);
-
+ // If closed, keep. If opened, check supported and apply.
abstract void setHdr(Hdr hdr);
+ // If closed, keep. If opened, check supported and apply.
abstract void setLocation(Location location);
+ // Throw if capturing. If in video session, recompute capture size, and, if needed, preview size.
+ abstract void setVideoQuality(VideoQuality videoQuality);
+
+
//endregion
//region APIs
@@ -115,11 +131,8 @@ abstract class CameraController implements Preview.SurfaceCallback {
abstract boolean endVideo();
-
abstract boolean shouldFlipSizes(); // Wheter the Sizes should be flipped to match the view orientation.
- abstract boolean isCameraOpened();
-
abstract boolean startAutoFocus(@Nullable Gesture gesture, PointF point);
//endregion
@@ -160,6 +173,10 @@ abstract class CameraController implements Preview.SurfaceCallback {
return mHdr;
}
+ final Location getLocation() {
+ return mLocation;
+ }
+
final Size getCaptureSize() {
return mCaptureSize;
}
diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
index ae89b7df..c7b05c78 100644
--- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
+++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
@@ -16,16 +16,12 @@ import android.location.Location;
import android.media.MediaActionSound;
import android.os.Build;
import android.os.Handler;
-import android.os.HandlerThread;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
-import android.util.Log;
-import android.view.Display;
import android.view.MotionEvent;
import android.view.ViewGroup;
-import android.view.WindowManager;
import android.widget.FrameLayout;
import java.io.ByteArrayOutputStream;
@@ -59,17 +55,19 @@ public class CameraView extends FrameLayout {
private HashMap mGestureMap = new HashMap<>(4);
// Components
- CameraCallbacks mCameraCallbacks;
+ /* for tests */ CameraCallbacks mCameraCallbacks;
private OrientationHelper mOrientationHelper;
private CameraController mCameraController;
private Preview mPreviewImpl;
+ private ArrayList mListeners = new ArrayList<>(2);
+
// Views
GridLinesLayout mGridLinesLayout;
PinchGestureLayout mPinchGestureLayout;
TapGestureLayout mTapGestureLayout;
ScrollGestureLayout mScrollGestureLayout;
- private boolean mIsStarted;
+ private boolean mStartCalled;
private boolean mKeepScreenOn;
// Threading
@@ -113,7 +111,7 @@ public class CameraView extends FrameLayout {
a.recycle();
// Components
- mCameraCallbacks = new CameraCallbacks();
+ mCameraCallbacks = new Callbacks();
mPreviewImpl = instantiatePreview(context, this);
mCameraController = instantiateCameraController(mCameraCallbacks, mPreviewImpl);
mUiHandler = new Handler(Looper.getMainLooper());
@@ -129,7 +127,7 @@ public class CameraView extends FrameLayout {
addView(mTapGestureLayout);
addView(mScrollGestureLayout);
- mIsStarted = false;
+ mStartCalled = false;
// Apply self managed
setCropOutput(cropOutput);
@@ -408,7 +406,7 @@ public class CameraView extends FrameLayout {
@Override
public boolean onTouchEvent(MotionEvent event) {
- if (!mCameraController.isCameraOpened()) return true;
+ if (!mCameraController.isCameraAvailable()) return true;
// Pass to our own GestureLayouts
CameraOptions options = mCameraController.getCameraOptions(); // Non null
@@ -478,7 +476,7 @@ public class CameraView extends FrameLayout {
* @return whether the camera has started
*/
public boolean isStarted() {
- return mIsStarted;
+ return mStartCalled;
}
@@ -487,13 +485,13 @@ public class CameraView extends FrameLayout {
* This should be called onResume(), or when you are ready with permissions.
*/
public void start() {
- if (mIsStarted || !isEnabled()) {
+ if (mStartCalled || !isEnabled()) {
// Already started, do nothing.
return;
}
if (checkPermissions(getSessionType())) {
- mIsStarted = true;
+ mStartCalled = true;
// Update display orientation for current CameraController
mOrientationHelper.enable(getContext());
mCameraController.start();
@@ -567,17 +565,17 @@ public class CameraView extends FrameLayout {
* This should be called onPause().
*/
public void stop() {
- if (!mIsStarted) {
+ if (!mStartCalled) {
// Already stopped, do nothing.
return;
}
- mIsStarted = false;
+ mStartCalled = false;
mCameraController.stop();
}
public void destroy() {
// TODO: this is not strictly needed
- mCameraCallbacks.clearListeners(); // Release inner listener.
+ clearCameraListeners(); // Release
}
//endregion
@@ -745,6 +743,16 @@ public class CameraView extends FrameLayout {
}
+ /**
+ * Retrieves the location previously applied with setLocation().
+ *
+ * @return the current location, if any.
+ */
+ @Nullable
+ public Location getLocation() {
+ return mCameraController.getLocation();
+ }
+
/**
* Sets desired white balance to current camera session.
*
@@ -892,7 +900,7 @@ public class CameraView extends FrameLayout {
*/
public void setSessionType(SessionType sessionType) {
- if (sessionType == getSessionType() || !mIsStarted) {
+ if (sessionType == getSessionType() || !mStartCalled) {
// Check did took place, or will happen on start().
mCameraController.setSessionType(sessionType);
@@ -1003,10 +1011,8 @@ public class CameraView extends FrameLayout {
*/
@Deprecated
public void setCameraListener(CameraListener cameraListener) {
- mCameraCallbacks.clearListeners();
- if (cameraListener != null) {
- mCameraCallbacks.addListener(cameraListener);
- }
+ mListeners.clear();
+ addCameraListener(cameraListener);
}
@@ -1018,7 +1024,7 @@ public class CameraView extends FrameLayout {
*/
public void addCameraListener(CameraListener cameraListener) {
if (cameraListener != null) {
- mCameraCallbacks.addListener(cameraListener);
+ mListeners.add(cameraListener);
}
}
@@ -1030,7 +1036,7 @@ public class CameraView extends FrameLayout {
*/
public void removeCameraListener(CameraListener cameraListener) {
if (cameraListener != null) {
- mCameraCallbacks.removeListener(cameraListener);
+ mListeners.remove(cameraListener);
}
}
@@ -1040,7 +1046,7 @@ public class CameraView extends FrameLayout {
* to camera events.
*/
public void clearCameraListeners() {
- mCameraCallbacks.clearListeners();
+ mListeners.clear();
}
@@ -1103,8 +1109,13 @@ public class CameraView extends FrameLayout {
file = new File(getContext().getExternalFilesDir(null), "video.mp4");
}
if (mCameraController.startVideo(file)) {
- mKeepScreenOn = getKeepScreenOn();
- if (!mKeepScreenOn) setKeepScreenOn(true);
+ mUiHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mKeepScreenOn = getKeepScreenOn();
+ if (!mKeepScreenOn) setKeepScreenOn(true);
+ }
+ });
}
}
@@ -1143,7 +1154,12 @@ public class CameraView extends FrameLayout {
*/
public void stopCapturingVideo() {
if (mCameraController.endVideo()) {
- if (getKeepScreenOn() != mKeepScreenOn) setKeepScreenOn(mKeepScreenOn);
+ mUiHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (getKeepScreenOn() != mKeepScreenOn) setKeepScreenOn(mKeepScreenOn);
+ }
+ });
}
}
@@ -1218,20 +1234,31 @@ public class CameraView extends FrameLayout {
}
}
+ interface CameraCallbacks extends OrientationHelper.Callbacks {
+ void dispatchOnCameraOpened(CameraOptions options);
+ void dispatchOnCameraClosed();
+ void onCameraPreviewSizeChanged();
+ void processImage(byte[] jpeg, boolean consistentWithView, boolean flipHorizontally);
+ void processSnapshot(YuvImage image, boolean consistentWithView, boolean flipHorizontally);
+ void dispatchOnVideoTaken(File file);
+ void dispatchOnFocusStart(@Nullable Gesture trigger, PointF where);
+ void dispatchOnFocusEnd(@Nullable Gesture trigger, boolean success, PointF where);
+ void dispatchOnZoomChanged(final float newValue, final PointF[] fingers);
+ void dispatchOnExposureCorrectionChanged(float newValue, float[] bounds, PointF[] fingers);
+ }
- class CameraCallbacks implements OrientationHelper.Callbacks {
+ private class Callbacks implements CameraCallbacks {
// Outer listeners
- private ArrayList mListeners = new ArrayList<>(2);
private CameraLogger mLogger = CameraLogger.create(CameraCallbacks.class.getSimpleName());
// Orientation TODO: move this logic into OrientationHelper
private Integer mDisplayOffset;
private Integer mDeviceOrientation;
- CameraCallbacks() {}
-
+ Callbacks() {}
+ @Override
public void dispatchOnCameraOpened(final CameraOptions options) {
mLogger.i("dispatchOnCameraOpened", options);
mUiHandler.post(new Runnable() {
@@ -1244,7 +1271,7 @@ public class CameraView extends FrameLayout {
});
}
-
+ @Override
public void dispatchOnCameraClosed() {
mLogger.i("dispatchOnCameraClosed");
mUiHandler.post(new Runnable() {
@@ -1257,7 +1284,7 @@ public class CameraView extends FrameLayout {
});
}
-
+ @Override
public void onCameraPreviewSizeChanged() {
mLogger.i("onCameraPreviewSizeChanged");
// Camera preview size, as returned by getPreviewSize(), has changed.
@@ -1290,6 +1317,7 @@ public class CameraView extends FrameLayout {
* @param flipHorizontally whether this picture should be flipped horizontally after decoding,
* because it was taken with the front camera.
*/
+ @Override
public void processImage(final byte[] jpeg, final boolean consistentWithView, final boolean flipHorizontally) {
mLogger.i("processImage");
mWorkerHandler.post(new Runnable() {
@@ -1311,7 +1339,7 @@ public class CameraView extends FrameLayout {
});
}
-
+ @Override
public void processSnapshot(final YuvImage yuv, final boolean consistentWithView, boolean flipHorizontally) {
mLogger.i("processSnapshot");
mWorkerHandler.post(new Runnable() {
@@ -1349,7 +1377,7 @@ public class CameraView extends FrameLayout {
});
}
-
+ @Override
public void dispatchOnVideoTaken(final File video) {
mLogger.i("dispatchOnVideoTaken", video);
mUiHandler.post(new Runnable() {
@@ -1362,7 +1390,7 @@ public class CameraView extends FrameLayout {
});
}
-
+ @Override
public void dispatchOnFocusStart(@Nullable final Gesture gesture, final PointF point) {
mLogger.i("dispatchOnFocusStart", gesture, point);
mUiHandler.post(new Runnable() {
@@ -1379,7 +1407,7 @@ public class CameraView extends FrameLayout {
});
}
-
+ @Override
public void dispatchOnFocusEnd(@Nullable final Gesture gesture, final boolean success,
final PointF point) {
mLogger.i("dispatchOnFocusEnd", gesture, success, point);
@@ -1437,7 +1465,7 @@ public class CameraView extends FrameLayout {
});
}
-
+ @Override
public void dispatchOnZoomChanged(final float newValue, final PointF[] fingers) {
mLogger.i("dispatchOnZoomChanged", newValue);
mUiHandler.post(new Runnable() {
@@ -1450,7 +1478,7 @@ public class CameraView extends FrameLayout {
});
}
-
+ @Override
public void dispatchOnExposureCorrectionChanged(final float newValue,
final float[] bounds,
final PointF[] fingers) {
@@ -1464,24 +1492,6 @@ public class CameraView extends FrameLayout {
}
});
}
-
-
- private void addListener(@NonNull CameraListener cameraListener) {
- mLogger.i("addListener");
- mListeners.add(cameraListener);
- }
-
-
- private void removeListener(@NonNull CameraListener cameraListener) {
- mLogger.i("removeListener");
- mListeners.remove(cameraListener);
- }
-
-
- private void clearListeners() {
- mLogger.i("clearListeners");
- mListeners.clear();
- }
}
//endregion
diff --git a/cameraview/src/main/utils/com/otaliastudios/cameraview/CameraUtils.java b/cameraview/src/main/utils/com/otaliastudios/cameraview/CameraUtils.java
index f15c88c7..ed6ba69c 100644
--- a/cameraview/src/main/utils/com/otaliastudios/cameraview/CameraUtils.java
+++ b/cameraview/src/main/utils/com/otaliastudios/cameraview/CameraUtils.java
@@ -68,7 +68,7 @@ public class CameraUtils {
*/
public static void decodeBitmap(final byte[] source, final BitmapCallback callback) {
final Handler ui = new Handler();
- new Thread(new Runnable() {
+ WorkerHandler.run(new Runnable() {
@Override
public void run() {
final Bitmap bitmap = decodeBitmap(source);
@@ -79,7 +79,7 @@ public class CameraUtils {
}
});
}
- }).start();
+ });
}
diff --git a/cameraview/src/main/utils/com/otaliastudios/cameraview/WorkerHandler.java b/cameraview/src/main/utils/com/otaliastudios/cameraview/WorkerHandler.java
index 4d7cd897..c58280bf 100644
--- a/cameraview/src/main/utils/com/otaliastudios/cameraview/WorkerHandler.java
+++ b/cameraview/src/main/utils/com/otaliastudios/cameraview/WorkerHandler.java
@@ -36,6 +36,13 @@ class WorkerHandler {
return handler;
}
+ // Handy util to perform action in a fallback thread.
+ // Not to be used for long-running operations since they will
+ // block the fallback thread.
+ public static void run(Runnable action) {
+ get("FallbackCameraThread").post(action);
+ }
+
private HandlerThread mThread;
private Handler mHandler;
diff --git a/codecov.yml b/codecov.yml
index c738a49c..8bbf156c 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -6,7 +6,7 @@ coverage:
status:
project:
default:
- target: 40%
+ target: 50%
patch:
default:
target: 60%