From 739645451a69d37566a80188093c165497de7a23 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Wed, 13 Jan 2021 15:24:23 +0100 Subject: [PATCH 1/3] Ignore type 0 capture fails on Android, and added capture timeout. --- packages/camera/camera/CHANGELOG.md | 4 +++ .../io/flutter/plugins/camera/Camera.java | 5 +++- .../plugins/camera/PictureCaptureRequest.java | 28 +++++++++++++++++++ packages/camera/camera/pubspec.yaml | 2 +- 4 files changed, 37 insertions(+), 2 deletions(-) diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index 1a2b03d93a6a..59404a32d17e 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.6.6+1 + +* Fixes picture captures causing a crash on some Huawei devices. + ## 0.6.6 * Adds auto focus support for Android and iOS implementations. diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java index 3fc702a2a879..b37e5701caf0 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -464,17 +464,20 @@ public void onCaptureFailed( return; } String reason; + boolean fatalFailure = false; switch (failure.getReason()) { case CaptureFailure.REASON_ERROR: reason = "An error happened in the framework"; break; case CaptureFailure.REASON_FLUSHED: reason = "The capture has failed due to an abortCaptures() call"; + fatalFailure = true; break; default: reason = "Unknown reason"; } - pictureCaptureRequest.error("captureFailure", reason, null); + Log.w("Camera", "pictureCaptureCallback.onCaptureFailed(): " + reason); + if (fatalFailure) pictureCaptureRequest.error("captureFailure", reason, null); } private void processCapture(CaptureResult result) { diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java index 189f2f1490dc..7609c4ce7e9c 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -4,6 +4,7 @@ package io.flutter.plugins.camera; +import android.os.Handler; import androidx.annotation.Nullable; import io.flutter.plugin.common.MethodChannel; @@ -19,17 +20,25 @@ enum State { error, } + private static final int REQUEST_TIMEOUT = 5000; + private final Handler handler; private final MethodChannel.Result result; private State state; public PictureCaptureRequest(MethodChannel.Result result) { this.result = result; state = State.idle; + this.handler = new Handler(); } public void setState(State state) { if (isFinished()) throw new IllegalStateException("Request has already been finished"); this.state = state; + if (state != State.idle && state != State.finished && state != State.error) { + this.resetTimeout(); + } else { + clearTimeout(); + } } public State getState() { @@ -42,6 +51,7 @@ public boolean isFinished() { public void finish(String absolutePath) { if (isFinished()) throw new IllegalStateException("Request has already been finished"); + clearTimeout(); result.success(absolutePath); state = State.finished; } @@ -49,7 +59,25 @@ public void finish(String absolutePath) { public void error( String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { if (isFinished()) throw new IllegalStateException("Request has already been finished"); + clearTimeout(); result.error(errorCode, errorMessage, errorDetails); state = State.error; } + + private final Runnable timeoutCallback = + new Runnable() { + @Override + public void run() { + error("captureTimeout", "Picture capture request timed out", state.toString()); + } + }; + + private void resetTimeout() { + clearTimeout(); + handler.postDelayed(timeoutCallback, REQUEST_TIMEOUT); + } + + private void clearTimeout() { + handler.removeCallbacks(timeoutCallback); + } } diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 0b21497b5462..9581aba73d1c 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -2,7 +2,7 @@ name: camera description: A Flutter plugin for getting information about and controlling the camera on Android and iOS. Supports previewing the camera feed, capturing images, capturing video, and streaming image buffers to dart. -version: 0.6.6 +version: 0.6.6+1 homepage: https://github.com/flutter/plugins/tree/master/packages/camera/camera dependencies: From cd3549f04a73923602a0402c2b6dce7b5c91a2f2 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Wed, 13 Jan 2021 16:28:40 +0100 Subject: [PATCH 2/3] Update handler constructor --- .../java/io/flutter/plugins/camera/PictureCaptureRequest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java index 7609c4ce7e9c..cc7e86bdb2e5 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -5,6 +5,8 @@ package io.flutter.plugins.camera; import android.os.Handler; +import android.os.Looper; + import androidx.annotation.Nullable; import io.flutter.plugin.common.MethodChannel; @@ -28,7 +30,7 @@ enum State { public PictureCaptureRequest(MethodChannel.Result result) { this.result = result; state = State.idle; - this.handler = new Handler(); + this.handler = new Handler(Looper.getMainLooper()); } public void setState(State state) { From cff09eccbcb17394038e5bf777ea097015cebfe3 Mon Sep 17 00:00:00 2001 From: "Bodhi Mulders (BeMacized)" Date: Wed, 13 Jan 2021 16:57:36 +0100 Subject: [PATCH 3/3] Added unit tests --- .../plugins/camera/PictureCaptureRequest.java | 55 +++++++++++-------- .../camera/PictureCaptureRequestTest.java | 51 +++++++++++++++++ 2 files changed, 84 insertions(+), 22 deletions(-) diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java index cc7e86bdb2e5..396f782a2a08 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -6,7 +6,6 @@ import android.os.Handler; import android.os.Looper; - import androidx.annotation.Nullable; import io.flutter.plugin.common.MethodChannel; @@ -22,24 +21,35 @@ enum State { error, } - private static final int REQUEST_TIMEOUT = 5000; - private final Handler handler; + private final Runnable timeoutCallback = + new Runnable() { + @Override + public void run() { + error("captureTimeout", "Picture capture request timed out", state.toString()); + } + }; + private final MethodChannel.Result result; + private final TimeoutHandler timeoutHandler; private State state; public PictureCaptureRequest(MethodChannel.Result result) { + this(result, new TimeoutHandler()); + } + + public PictureCaptureRequest(MethodChannel.Result result, TimeoutHandler timeoutHandler) { this.result = result; - state = State.idle; - this.handler = new Handler(Looper.getMainLooper()); + this.state = State.idle; + this.timeoutHandler = timeoutHandler; } public void setState(State state) { if (isFinished()) throw new IllegalStateException("Request has already been finished"); this.state = state; if (state != State.idle && state != State.finished && state != State.error) { - this.resetTimeout(); + this.timeoutHandler.resetTimeout(timeoutCallback); } else { - clearTimeout(); + this.timeoutHandler.clearTimeout(timeoutCallback); } } @@ -53,7 +63,7 @@ public boolean isFinished() { public void finish(String absolutePath) { if (isFinished()) throw new IllegalStateException("Request has already been finished"); - clearTimeout(); + this.timeoutHandler.clearTimeout(timeoutCallback); result.success(absolutePath); state = State.finished; } @@ -61,25 +71,26 @@ public void finish(String absolutePath) { public void error( String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { if (isFinished()) throw new IllegalStateException("Request has already been finished"); - clearTimeout(); + this.timeoutHandler.clearTimeout(timeoutCallback); result.error(errorCode, errorMessage, errorDetails); state = State.error; } - private final Runnable timeoutCallback = - new Runnable() { - @Override - public void run() { - error("captureTimeout", "Picture capture request timed out", state.toString()); - } - }; + static class TimeoutHandler { + private static final int REQUEST_TIMEOUT = 5000; + private final Handler handler; - private void resetTimeout() { - clearTimeout(); - handler.postDelayed(timeoutCallback, REQUEST_TIMEOUT); - } + TimeoutHandler() { + this.handler = new Handler(Looper.getMainLooper()); + } - private void clearTimeout() { - handler.removeCallbacks(timeoutCallback); + public void resetTimeout(Runnable runnable) { + clearTimeout(runnable); + handler.postDelayed(runnable, REQUEST_TIMEOUT); + } + + public void clearTimeout(Runnable runnable) { + handler.removeCallbacks(runnable); + } } } diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java index 3ede0b7abe3a..3252b3e111c4 100644 --- a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java @@ -7,7 +7,10 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import io.flutter.plugin.common.MethodChannel; @@ -38,6 +41,32 @@ public void setState_sets_state() { "State is awaitingPreCapture", req.getState(), PictureCaptureRequest.State.capturing); } + @Test + public void setState_resets_timeout() { + PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = + mock(PictureCaptureRequest.TimeoutHandler.class); + PictureCaptureRequest req = new PictureCaptureRequest(null, mockTimeoutHandler); + req.setState(PictureCaptureRequest.State.focusing); + req.setState(PictureCaptureRequest.State.preCapture); + req.setState(PictureCaptureRequest.State.waitingPreCaptureReady); + req.setState(PictureCaptureRequest.State.capturing); + verify(mockTimeoutHandler, times(4)).resetTimeout(any()); + verify(mockTimeoutHandler, never()).clearTimeout(any()); + } + + @Test + public void setState_clears_timeout() { + PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = + mock(PictureCaptureRequest.TimeoutHandler.class); + PictureCaptureRequest req = new PictureCaptureRequest(null, mockTimeoutHandler); + req.setState(PictureCaptureRequest.State.idle); + req.setState(PictureCaptureRequest.State.finished); + req = new PictureCaptureRequest(null, mockTimeoutHandler); + req.setState(PictureCaptureRequest.State.error); + verify(mockTimeoutHandler, never()).resetTimeout(any()); + verify(mockTimeoutHandler, times(3)).clearTimeout(any()); + } + @Test public void finish_sets_result_and_state() { // Setup @@ -50,6 +79,17 @@ public void finish_sets_result_and_state() { assertEquals("State is finished", req.getState(), PictureCaptureRequest.State.finished); } + @Test + public void finish_clears_timeout() { + PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = + mock(PictureCaptureRequest.TimeoutHandler.class); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + PictureCaptureRequest req = new PictureCaptureRequest(mockResult, mockTimeoutHandler); + req.finish("/test/path"); + verify(mockTimeoutHandler, never()).resetTimeout(any()); + verify(mockTimeoutHandler).clearTimeout(any()); + } + @Test public void isFinished_is_true_When_state_is_finished_or_error() { // Setup @@ -90,6 +130,17 @@ public void error_sets_result_and_state() { assertEquals("State is error", req.getState(), PictureCaptureRequest.State.error); } + @Test + public void error_clears_timeout() { + PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = + mock(PictureCaptureRequest.TimeoutHandler.class); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + PictureCaptureRequest req = new PictureCaptureRequest(mockResult, mockTimeoutHandler); + req.error("ERROR_CODE", "Error Message", null); + verify(mockTimeoutHandler, never()).resetTimeout(any()); + verify(mockTimeoutHandler).clearTimeout(any()); + } + @Test(expected = IllegalStateException.class) public void error_throws_When_already_finished() { // Setup