diff --git a/.travis.yml b/.travis.yml index 116cf6f1..3a3cd42b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,29 +39,34 @@ install: - export EMULATOR="system-images;android-$EMULATOR_API;$EMULATOR_TAG;$EMULATOR_ABI" - echo yes | sdkmanager "platforms;android-$EMULATOR_API" # Install sdk - echo yes | sdkmanager "$EMULATOR" # Install system image + - sdkmanager --list || true # Check everything is updated # Create adn start emulator - echo no | avdmanager create avd -n test -k "$EMULATOR" -f # Create emulator - - emulator -avd test -no-window -camera-back emulated -camera-front emulated -memory 2048 & # Launch + - which emulator # ensure we are using the right emulator (home/emulator/) + - emulator -avd test -no-window -camera-back emulated -camera-front emulated -memory 2048 -writable-system & # Launch - adb wait-for-device # Wait for adb process - - sdkmanager --list || true # Check everything is updated + - adb remount # Mount as writable before_script: + # Wait for emulator - android-wait-for-emulator # Wait for emulator ready to interact - adb shell settings put global window_animation_scale 0 & # Disable animations - adb shell settings put global transition_animation_scale 0 & # Disable animations - adb shell settings put global animator_duration_scale 0 & # Disable animations + + # Unlock and configure logs. + # Would be great to use -v color to adb logcat but looks not supported on travis. - sleep 20 # Sleep 20 seconds just in case - - adb shell input keyevent 82 & # Dispatch event + - adb shell input keyevent 82 & # Dispatch unlock event + - adb logcat -c # Clear logcat until now + - adb logcat Test:V TestRunner:V CameraView:V CameraController:V Camera1:V WorkerHandler:V *:E & + # - export LOGCAT_PID=$! # Save PID of the logcat process. Should kill later with kill $LOGCAT_PID + + script: - - ./gradlew clean testDebugUnitTest - - adb logcat -c - - adb logcat -v color Test:V TestRunner:V CameraView:V CameraController:V Camera1:V WorkerHandler:V *:S & - - export LOGCAT_PID=$! - - ./gradlew connectedCheck - - kill $LOGCAT_PID - - ./gradlew mergedCoverageReport + - ./gradlew clean testDebugUnitTest connectedCheck mergedCoverageReport after_success: - bash <(curl -s https://codecov.io/bash) -s "*/build/reports/mergedCoverageReport/" diff --git a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java index 44159727..7ea10157 100644 --- a/cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java +++ b/cameraview/src/androidTest/java/com/otaliastudios/cameraview/IntegrationTest.java @@ -1,11 +1,13 @@ package com.otaliastudios.cameraview; +import android.content.Context; import android.graphics.Bitmap; import android.graphics.PointF; import android.support.test.filters.MediumTest; import android.support.test.rule.ActivityTestRule; import android.support.test.runner.AndroidJUnit4; +import android.view.ViewGroup; import org.junit.After; import org.junit.Before; @@ -103,7 +105,7 @@ public class IntegrationTest extends BaseTest { final Task pic = new Task<>(); pic.listen(); doEndTask(pic, 0).when(listener).onPictureTaken(any(byte[].class)); - byte[] result = pic.await(2000); + byte[] result = pic.await(5000); if (expectSuccess) { assertNotNull("Can take picture", result); } else { @@ -143,7 +145,7 @@ public class IntegrationTest extends BaseTest { } @Test - // TODO: This works great on the device but crashes on the emulator. + // This works great on the device but crashes often on the emulator. // There must be something wrong with the emulated camera... // Like stopPreview() and release() are not really sync calls? public void testConcurrentCalls() throws Exception { @@ -160,7 +162,7 @@ public class IntegrationTest extends BaseTest { assertTrue("Handles concurrent calls to start & stop, " + latch.getCount(), did); } - //-@Test + @Test public void testStartInitializesOptions() { assertNull(camera.getCameraOptions()); assertNull(camera.getExtraProperties()); @@ -175,7 +177,7 @@ public class IntegrationTest extends BaseTest { //region test Facing/SessionType // Test things that should reset the camera. - //-@Test + @Test public void testSetFacing() throws Exception { camera.start(); CameraOptions o = waitForOpen(true); @@ -193,7 +195,7 @@ public class IntegrationTest extends BaseTest { } } - //-@Test + @Test public void testSetSessionType() throws Exception { camera.setSessionType(SessionType.PICTURE); camera.start(); @@ -216,7 +218,7 @@ public class IntegrationTest extends BaseTest { //region test Set Parameters // When camera is open, parameters will be set only if supported. - //-@Test + @Test public void testSetZoom() { camera.start(); CameraOptions options = waitForOpen(true); @@ -227,7 +229,7 @@ public class IntegrationTest extends BaseTest { assertEquals(can ? newValue : oldValue, camera.getZoom(), 0f); } - //-@Test + @Test public void testSetExposureCorrection() { camera.start(); CameraOptions options = waitForOpen(true); @@ -238,7 +240,7 @@ public class IntegrationTest extends BaseTest { assertEquals(can ? newValue : oldValue, camera.getExposureCorrection(), 0f); } - //-@Test + @Test public void testSetFlash() { camera.start(); CameraOptions options = waitForOpen(true); @@ -255,7 +257,7 @@ public class IntegrationTest extends BaseTest { } } - //-@Test + @Test public void testSetWhiteBalance() { camera.start(); CameraOptions options = waitForOpen(true); @@ -272,7 +274,7 @@ public class IntegrationTest extends BaseTest { } } - //-@Test + @Test public void testSetHdr() { camera.start(); CameraOptions options = waitForOpen(true); @@ -289,7 +291,7 @@ public class IntegrationTest extends BaseTest { } } - //-@Test + @Test public void testSetAudio() { // TODO: when permissions are managed, check that Audio.ON triggers the audio permission camera.start(); @@ -301,7 +303,7 @@ public class IntegrationTest extends BaseTest { } } - //-@Test + @Test public void testSetLocation() { camera.start(); waitForOpen(true); @@ -329,7 +331,7 @@ public class IntegrationTest extends BaseTest { camera.setVideoQuality(VideoQuality.LOWEST); } - //-@Test + @Test public void testSetVideoQuality_whileInPictureSessionType() { camera.setSessionType(SessionType.PICTURE); camera.setVideoQuality(VideoQuality.HIGHEST); @@ -339,7 +341,7 @@ public class IntegrationTest extends BaseTest { assertEquals(camera.getVideoQuality(), VideoQuality.LOWEST); } - //-@Test + @Test public void testSetVideoQuality_whileNotStarted() { camera.setVideoQuality(VideoQuality.HIGHEST); assertEquals(camera.getVideoQuality(), VideoQuality.HIGHEST); @@ -347,7 +349,7 @@ public class IntegrationTest extends BaseTest { assertEquals(camera.getVideoQuality(), VideoQuality.LOWEST); } - //-@Test + @Test public void testSetVideoQuality_shouldRecompute() { // If video quality changes bring to a new capture size, // this might bring to a new aspect ratio, @@ -382,7 +384,7 @@ public class IntegrationTest extends BaseTest { waitForVideo(true); // waits 2000 } - //-@Test + @Test public void testEndVideo_withoutStarting() { camera.setSessionType(SessionType.VIDEO); camera.start(); @@ -396,7 +398,7 @@ public class IntegrationTest extends BaseTest { //region startAutoFocus // TODO: won't test onStopAutoFocus because that is not guaranteed to be called - //-@Test + @Test public void testStartAutoFocus() { camera.start(); CameraOptions o = waitForOpen(true); @@ -412,13 +414,13 @@ public class IntegrationTest extends BaseTest { //region capture - //-@Test + @Test public void testCapturePicture_beforeStarted() { camera.capturePicture(); waitForPicture(false); } - //-@Test + @Test public void testCapturePicture_concurrentCalls() throws Exception { // Second take should fail. camera.start(); @@ -434,7 +436,7 @@ public class IntegrationTest extends BaseTest { assertEquals(latch.getCount(), 1); } - //-@Test + @Test public void testCapturePicture_size() throws Exception { camera.setCropOutput(false); camera.start(); @@ -450,13 +452,13 @@ public class IntegrationTest extends BaseTest { assertTrue(b.getHeight() == size.getHeight() || b.getHeight() == size.getWidth()); } - //-@Test + @Test public void testCaptureSnapshot_beforeStarted() { camera.captureSnapshot(); waitForPicture(false); } - //-@Test + @Test public void testCaptureSnapshot_concurrentCalls() throws Exception { // Second take should fail. camera.start(); @@ -472,7 +474,7 @@ public class IntegrationTest extends BaseTest { assertEquals(latch.getCount(), 1); } - //-@Test + @Test public void testCaptureSnapshot_size() throws Exception { camera.setCropOutput(false); camera.start(); diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java index a1975322..9cf02e59 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/Camera1.java @@ -66,15 +66,17 @@ class Camera1 extends CameraController { */ @Override public void onSurfaceAvailable() { - LOG.i("onSurfaceAvailable, size is", mPreview.getSurfaceSize()); + LOG.i("onSurfaceAvailable:", "Size is", mPreview.getSurfaceSize()); if (!shouldSetup()) return; mHandler.post(new Runnable() { @Override public void run() { if (!shouldSetup()) return; + LOG.i("onSurfaceAvailable:", "Inside handler. About to bind."); try { setup(); } catch (Exception e) { + LOG.w("onSurfaceAvailable:", "Exception while binding camera to preview.", e); throw new RuntimeException(e); } } @@ -92,10 +94,13 @@ class Camera1 extends CameraController { // Compute a new camera preview size. Size newSize = computePreviewSize(); if (!newSize.equals(mPreviewSize)) { + LOG.i("onSurfaceChanged:", "Computed a new preview size. Dispatching."); mPreviewSize = newSize; mCameraCallbacks.onCameraPreviewSizeChanged(); synchronized (mLock) { + LOG.i("onSurfaceChanged:", "Stopping preview."); mCamera.stopPreview(); + LOG.i("onSurfaceChanged:", "Stopped preview."); Camera.Parameters params = mCamera.getParameters(); params.setPreviewSize(mPreviewSize.getWidth(), mPreviewSize.getHeight()); mCamera.setParameters(params); @@ -105,7 +110,9 @@ class Camera1 extends CameraController { invertPreviewSizes ? mPreviewSize.getHeight() : mPreviewSize.getWidth(), invertPreviewSizes ? mPreviewSize.getWidth() : mPreviewSize.getHeight() ); + LOG.i("onSurfaceChanged:", "Restarting preview."); mCamera.startPreview(); + LOG.i("onSurfaceChanged:", "Restarted preview."); } } } @@ -118,6 +125,7 @@ class Camera1 extends CameraController { // These can happen at different times but we want to end up here. @WorkerThread private void setup() throws Exception { + LOG.i("setup:", "Started"); Object output = mPreview.getOutput(); if (mPreview.getOutputClass() == SurfaceHolder.class) { mCamera.setPreviewDisplay((SurfaceHolder) output); @@ -128,6 +136,7 @@ class Camera1 extends CameraController { boolean invertPreviewSizes = shouldFlipSizes(); mCaptureSize = computeCaptureSize(); mPreviewSize = computePreviewSize(); + LOG.i("setup:", "Dispatching onCameraPreviewSizeChanged."); mCameraCallbacks.onCameraPreviewSizeChanged(); mPreview.setDesiredSize( invertPreviewSizes ? mPreviewSize.getHeight() : mPreviewSize.getWidth(), @@ -139,8 +148,9 @@ class Camera1 extends CameraController { params.setPictureSize(mCaptureSize.getWidth(), mCaptureSize.getHeight()); // <- allowed mCamera.setParameters(params); } - + LOG.i("setup:", "Starting preview with startPreview()."); mCamera.startPreview(); + LOG.i("setup:", "Started preview with startPreview()."); mIsSetup = true; } @@ -149,6 +159,7 @@ class Camera1 extends CameraController { @Override void onStart() throws Exception { if (isCameraAvailable()) { + LOG.w("onStart:", "Camera not available. Should not happen."); onStop(); // Should not happen. } if (collectCameraId()) { @@ -156,6 +167,7 @@ class Camera1 extends CameraController { // Set parameters that might have been set before the camera was opened. synchronized (mLock) { + LOG.i("onStart:", "Applying default parameters."); Camera.Parameters params = mCamera.getParameters(); mExtraProperties = new ExtraProperties(params); mOptions = new CameraOptions(params); @@ -170,6 +182,7 @@ class Camera1 extends CameraController { // Try starting preview. mCamera.setDisplayOrientation(computeSensorToDisplayOffset()); // <- not allowed during preview if (shouldSetup()) setup(); + LOG.i("onStart:", "Ended"); } } @@ -177,19 +190,27 @@ class Camera1 extends CameraController { @Override void onStop() throws Exception { Exception error = null; + LOG.i("onStop:", "About to clean up."); mHandler.get().removeCallbacks(mPostFocusResetRunnable); if (isCameraAvailable()) { + LOG.i("onStop:", "Clean up.", "Ending video?", mIsCapturingVideo); if (mIsCapturingVideo) endVideo(); try { + LOG.i("onStop:", "Clean up.", "Stopping preview."); mCamera.stopPreview(); + LOG.i("onStop:", "Clean up.", "Stopped preview."); } catch (Exception e) { + LOG.w("onStop:", "Clean up.", "Exception while stopping preview."); error = e; } try { + LOG.i("onStop:", "Clean up.", "Releasing camera."); mCamera.release(); + LOG.i("onStop:", "Clean up.", "Released camera."); } catch (Exception e) { + LOG.w("onStop:", "Clean up.", "Exception while releasing camera."); error = e; } } @@ -392,8 +413,8 @@ class Camera1 extends CameraController { } onSurfaceChanged(); } - LOG.i("captureSize: "+mCaptureSize); - LOG.i("previewSize: "+mPreviewSize); + LOG.i("setVideoQuality:", "captureSize:", mCaptureSize); + LOG.i("setVideoQuality:", "previewSize:", mPreviewSize); } } @@ -484,8 +505,8 @@ class Camera1 extends CameraController { @Override boolean shouldFlipSizes() { int offset = computeSensorToDisplayOffset(); - LOG.i("shouldFlip:", "mDeviceOrientation=", mDeviceOrientation, "mSensorOffset=", mSensorOffset); - LOG.i("shouldFlip:", "sensorToDisplay=", offset); + LOG.i("shouldFlipSizes:", "mDeviceOrientation=", mDeviceOrientation, "mSensorOffset=", mSensorOffset); + LOG.i("shouldFlipSizes:", "sensorToDisplay=", offset); return offset % 180 != 0; } @@ -545,7 +566,7 @@ class Camera1 extends CameraController { // Choose the max size. List captureSizes = sizesFromList(params.getSupportedPictureSizes()); Size maxSize = Collections.max(captureSizes); - LOG.i("computeCaptureSize:", "computed", maxSize); + LOG.i("size:", "computeCaptureSize:", "computed", maxSize); return Collections.max(captureSizes); } else { // Choose according to developer choice in setVideoQuality. @@ -554,7 +575,7 @@ class Camera1 extends CameraController { List captureSizes = sizesFromList(params.getSupportedPictureSizes()); CamcorderProfile profile = getCamcorderProfile(mVideoQuality); AspectRatio targetRatio = AspectRatio.of(profile.videoFrameWidth, profile.videoFrameHeight); - LOG.i("computeCaptureSize:", "videoQuality:", mVideoQuality, "targetRatio:", targetRatio); + LOG.i("size:", "computeCaptureSize:", "videoQuality:", mVideoQuality, "targetRatio:", targetRatio); return matchSize(captureSizes, targetRatio, new Size(0, 0), true); } } @@ -564,7 +585,7 @@ class Camera1 extends CameraController { List previewSizes = sizesFromList(params.getSupportedPreviewSizes()); AspectRatio targetRatio = AspectRatio.of(mCaptureSize.getWidth(), mCaptureSize.getHeight()); Size biggerThan = mPreview.getSurfaceSize(); - LOG.i("computePreviewSize:", "targetRatio:", targetRatio, "surface size:", biggerThan); + LOG.i("size:", "computePreviewSize:", "targetRatio:", targetRatio, "surface size:", biggerThan); return matchSize(previewSizes, targetRatio, biggerThan, false); } @@ -586,7 +607,7 @@ class Camera1 extends CameraController { mMediaRecorder.start(); return true; } catch (Exception e) { - LOG.e("Error while starting MediaRecorder.", e); + LOG.e("Error while starting MediaRecorder. Swallowing.", e); mVideoFile = null; mCamera.lock(); endVideo(); @@ -608,7 +629,7 @@ class Camera1 extends CameraController { } 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); + LOG.w("Error while closing media recorder. Swallowing", e); } mMediaRecorder = null; } @@ -781,8 +802,8 @@ class Camera1 extends CameraController { double theta = ((double) displayToSensor) * Math.PI / 180; double sensorClickX = viewClickX * Math.cos(theta) - viewClickY * Math.sin(theta); double sensorClickY = viewClickX * Math.sin(theta) + viewClickY * Math.cos(theta); - LOG.i("viewClickX:", viewClickX, "viewClickY:", viewClickY); - LOG.i("sensorClickX:", sensorClickX, "sensorClickY:", sensorClickY); + LOG.i("focus:", "viewClickX:", viewClickX, "viewClickY:", viewClickY); + LOG.i("focus:", "sensorClickX:", sensorClickX, "sensorClickY:", sensorClickY); // Compute the rect bounds. Rect rect1 = computeMeteringArea(sensorClickX, sensorClickY, 150d); @@ -803,7 +824,7 @@ class Camera1 extends CameraController { int bottom = (int) Math.min(centerY + delta, 1000); int left = (int) Math.max(centerX - delta, -1000); int right = (int) Math.min(centerX + delta, 1000); - LOG.i("metering area:", "top:", top, "left:", left, "bottom:", bottom, "right:", right); + LOG.i("focus:", "computeMeteringArea:", "top:", top, "left:", left, "bottom:", bottom, "right:", right); return new Rect(left, top, right, bottom); } @@ -822,7 +843,7 @@ class Camera1 extends CameraController { for (Camera.Size size : sizes) { result.add(new Size(size.width, size.height)); } - LOG.i("sizesFromList:", result); + LOG.i("size:", "sizesFromList:", result); return result; } @@ -852,8 +873,8 @@ class Camera1 extends CameraController { } } - LOG.i("matchSize:", "found consistent:", consistent.size()); - LOG.i("matchSize:", "found big enough and consistent:", bigEnoughAndConsistent.size()); + LOG.i("size:", "matchSize:", "found consistent:", consistent.size()); + LOG.i("size:", "matchSize:", "found big enough and consistent:", bigEnoughAndConsistent.size()); Size result; if (biggestPossible) { if (bigEnoughAndConsistent.size() > 0) { @@ -872,7 +893,7 @@ class Camera1 extends CameraController { result = Collections.max(sizes); } } - LOG.i("matchSize:", "returning result", result); + LOG.i("size", "matchSize:", "returning result", result); return result; } diff --git a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java index 95935b4a..7bde91da 100644 --- a/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java +++ b/cameraview/src/main/java/com/otaliastudios/cameraview/CameraController.java @@ -53,7 +53,9 @@ abstract class CameraController implements Preview.SurfaceCallback { public void uncaughtException(Thread thread, Throwable throwable) { // Something went wrong. Thread is terminated (about to?). // Move to other thread and stop resources. + LOG.w("Interrupting thread, due to exception.", throwable); thread.interrupt(); + LOG.w("Interrupted thread. Posting a stopImmediately.", ss()); mHandler = WorkerHandler.get("CameraViewController"); mHandler.post(new Runnable() { @Override @@ -67,16 +69,29 @@ abstract class CameraController implements Preview.SurfaceCallback { //region Start&Stop + private String ss() { + switch (mState) { + case STATE_STOPPING: return "STATE_STOPPING"; + case STATE_STOPPED: return "STATE_STOPPED"; + case STATE_STARTING: return "STATE_STARTING"; + case STATE_STARTED: return "STATE_STARTED"; + } + return "null"; + } + // Starts the preview asynchronously. final void start() { + LOG.i("Start:", "posting runnable. State:", ss()); mHandler.post(new Runnable() { @Override public void run() { try { - int a = mState; + LOG.i("Start:", "executing. State:", ss()); if (mState >= STATE_STARTING) return; mState = STATE_STARTING; + LOG.i("Start:", "about to call onStart()", ss()); onStart(); + LOG.i("Start:", "returned from onStart().", "Dispatching.", ss()); mState = STATE_STARTED; mCameraCallbacks.dispatchOnCameraOpened(mOptions); @@ -90,14 +105,17 @@ abstract class CameraController implements Preview.SurfaceCallback { // Stops the preview asynchronously. final void stop() { + LOG.i("Stop:", "posting runnable. State:", ss()); mHandler.post(new Runnable() { @Override public void run() { try { - int a = mState; + LOG.i("Stop:", "executing. State:", ss()); if (mState <= STATE_STOPPED) return; mState = STATE_STOPPING; + LOG.i("Stop:", "about to call onStop()"); onStop(); + LOG.i("Stop:", "returned from onStop().", "Dispatching."); mState = STATE_STOPPED; mCameraCallbacks.dispatchOnCameraClosed(); @@ -113,31 +131,40 @@ abstract class CameraController implements Preview.SurfaceCallback { void stopImmediately() { try { // Don't check, try stop again. + LOG.i("Stop immediately. State was:", ss()); mState = STATE_STOPPING; onStop(); mState = STATE_STOPPED; + LOG.i("Stop immediately. Stopped. State is:", ss()); } catch (Exception e) { // Do nothing. + LOG.i("Stop immediately. Exception while stopping.", e); mState = STATE_STOPPED; } } // Forces a restart. protected final void restart() { + LOG.i("Restart:", "posting runnable"); mHandler.post(new Runnable() { @Override public void run() { try { + LOG.i("Restart:", "executing. Needs stopping:", mState > STATE_STOPPED, ss()); // Don't stop if stopped. if (mState > STATE_STOPPED) { mState = STATE_STOPPING; onStop(); mState = STATE_STOPPED; + LOG.i("Restart:", "stopped. Dispatching.", ss()); mCameraCallbacks.dispatchOnCameraClosed(); } + + LOG.i("Restart: about to start. State:", ss()); mState = STATE_STARTING; onStart(); mState = STATE_STARTED; + LOG.i("Restart: returned from start. Dispatching. State:", ss()); mCameraCallbacks.dispatchOnCameraOpened(mOptions); } catch (Exception e) {