diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md index a11579d528a4..9789e9e80b69 100644 --- a/packages/camera/camera/CHANGELOG.md +++ b/packages/camera/camera/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.6.1 + +* Add flash support for Android and iOS implementations. + ## 0.6.0+2 * Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) 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 306dd447cfb9..27f1355319c1 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 @@ -17,6 +17,8 @@ import android.hardware.camera2.CameraMetadata; import android.hardware.camera2.CaptureFailure; import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.CaptureResult; +import android.hardware.camera2.TotalCaptureResult; import android.hardware.camera2.params.OutputConfiguration; import android.hardware.camera2.params.SessionConfiguration; import android.hardware.camera2.params.StreamConfigurationMap; @@ -34,6 +36,8 @@ import io.flutter.plugin.common.EventChannel; import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugins.camera.media.MediaRecorderBuilder; +import io.flutter.plugins.camera.types.FlashMode; +import io.flutter.plugins.camera.types.ResolutionPreset; import io.flutter.view.TextureRegistry.SurfaceTextureEntry; import java.io.File; import java.io.FileOutputStream; @@ -69,16 +73,8 @@ public class Camera { private CamcorderProfile recordingProfile; private int currentOrientation = ORIENTATION_UNKNOWN; private Context applicationContext; - - // Mirrors camera.dart - public enum ResolutionPreset { - low, - medium, - high, - veryHigh, - ultraHigh, - max, - } + private FlashMode flashMode; + private PictureCaptureRequest pictureCaptureRequest; public Camera( final Activity activity, @@ -97,6 +93,7 @@ public Camera( this.dartMessenger = dartMessenger; this.cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); this.applicationContext = activity.getApplicationContext(); + this.flashMode = FlashMode.auto; orientationEventListener = new OrientationEventListener(activity.getApplicationContext()) { @Override @@ -220,58 +217,125 @@ SurfaceTextureEntry getFlutterTexture() { } public void takePicture(@NonNull final Result result) { + // Only take 1 picture at a time + if (pictureCaptureRequest != null && !pictureCaptureRequest.isFinished()) { + result.error("captureAlreadyActive", "Picture is currently already being captured", null); + return; + } + // Store the result + this.pictureCaptureRequest = new PictureCaptureRequest(result); + + // Create temporary file final File outputDir = applicationContext.getCacheDir(); final File file; try { file = File.createTempFile("CAP", ".jpg", outputDir); } catch (IOException | SecurityException e) { - result.error("cannotCreateFile", e.getMessage(), null); + pictureCaptureRequest.error("cannotCreateFile", e.getMessage(), null); return; } + // Listen for picture being taken pictureImageReader.setOnImageAvailableListener( reader -> { try (Image image = reader.acquireLatestImage()) { ByteBuffer buffer = image.getPlanes()[0].getBuffer(); writeToFile(buffer, file); - result.success(file.getAbsolutePath()); + pictureCaptureRequest.finish(file.getAbsolutePath()); } catch (IOException e) { - result.error("IOError", "Failed saving image", null); + pictureCaptureRequest.error("IOError", "Failed saving image", null); } }, null); + runPicturePreCapture(); + } + + private final CameraCaptureSession.CaptureCallback pictureCaptureCallback = + new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureCompleted( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { + assert (pictureCaptureRequest != null); + switch (pictureCaptureRequest.getState()) { + case awaitingPreCapture: + Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); + // Some devices might return null here, in which case we will also continue. + if (aeState == null + || aeState == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED + || aeState == CaptureRequest.CONTROL_AE_STATE_CONVERGED) { + runPictureCapture(); + } + break; + } + } + + @Override + public void onCaptureFailed( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureFailure failure) { + assert (pictureCaptureRequest != null); + String reason; + 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"; + break; + default: + reason = "Unknown reason"; + } + pictureCaptureRequest.error("captureFailure", reason, null); + } + }; + + private void runPicturePreCapture() { + assert (pictureCaptureRequest != null); + pictureCaptureRequest.setState(PictureCaptureRequest.State.awaitingPreCapture); + + captureRequestBuilder.set( + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); + try { + cameraCaptureSession.capture(captureRequestBuilder.build(), pictureCaptureCallback, null); + captureRequestBuilder.set( + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); + } catch (CameraAccessException e) { + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + } + } + + private void runPictureCapture() { + assert (pictureCaptureRequest != null); + pictureCaptureRequest.setState(PictureCaptureRequest.State.capturing); try { final CaptureRequest.Builder captureBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); captureBuilder.addTarget(pictureImageReader.getSurface()); captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getMediaOrientation()); - - cameraCaptureSession.capture( - captureBuilder.build(), - new CameraCaptureSession.CaptureCallback() { - @Override - public void onCaptureFailed( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureFailure failure) { - String reason; - 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"; - break; - default: - reason = "Unknown reason"; - } - result.error("captureFailure", reason, null); - } - }, - null); + switch (flashMode) { + case off: + captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); + captureBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + break; + case auto: + captureBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); + break; + case always: + default: + captureBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); + break; + } + cameraCaptureSession.capture(captureBuilder.build(), pictureCaptureCallback, null); } catch (CameraAccessException e) { - result.error("cameraAccess", e.getMessage(), null); + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); } } @@ -314,8 +378,7 @@ public void onConfigured(@NonNull CameraCaptureSession session) { return; } cameraCaptureSession = session; - captureRequestBuilder.set( - CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO); + initPreviewCaptureBuilder(); cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); if (onSuccessCallback != null) { onSuccessCallback.run(); @@ -452,6 +515,54 @@ public void resumeVideoRecording(@NonNull final Result result) { result.success(null); } + public void setFlashMode(@NonNull final Result result, FlashMode mode) + throws CameraAccessException { + // Get the flash availability + Boolean flashAvailable; + try { + flashAvailable = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.FLASH_INFO_AVAILABLE); + } catch (CameraAccessException e) { + result.error("setFlashModeFailed", e.getMessage(), null); + return; + } + // Check if flash is available. + if (flashAvailable == null || !flashAvailable) { + result.error("setFlashModeFailed", "Device does not have flash capabilities", null); + return; + } + // Get flash + + this.flashMode = mode; + initPreviewCaptureBuilder(); + this.cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); + result.success(null); + } + + private void initPreviewCaptureBuilder() { + captureRequestBuilder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_MODE_AUTO); + switch (flashMode) { + case off: + captureRequestBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); + captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + break; + case auto: + captureRequestBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); + captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + break; + case always: + default: + captureRequestBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); + captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + break; + } + } + public void startPreview() throws CameraAccessException { if (pictureImageReader == null || pictureImageReader.getSurface() == null) return; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java index a7bb3b7d4914..3b665d6b24f2 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java @@ -10,7 +10,7 @@ import android.hardware.camera2.params.StreamConfigurationMap; import android.media.CamcorderProfile; import android.util.Size; -import io.flutter.plugins.camera.Camera.ResolutionPreset; +import io.flutter.plugins.camera.types.ResolutionPreset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java index 6c2e65e76f9e..12b99b72f642 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java @@ -10,6 +10,7 @@ import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.Result; import io.flutter.plugins.camera.CameraPermissions.PermissionsRegistry; +import io.flutter.plugins.camera.types.FlashMode; import io.flutter.view.TextureRegistry; import java.util.HashMap; import java.util.Map; @@ -122,6 +123,21 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) camera.resumeVideoRecording(result); break; } + case "setFlashMode": + { + String modeStr = call.argument("mode"); + FlashMode mode = FlashMode.getValueForString(modeStr); + if (mode == null) { + result.error("setFlashModeFailed", "Unknown flash mode " + modeStr, null); + return; + } + try { + camera.setFlashMode(result, mode); + } catch (Exception e) { + handleException(e, result); + } + break; + } case "startImageStream": { try { 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 new file mode 100644 index 000000000000..e365f071d9a8 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java @@ -0,0 +1,49 @@ +package io.flutter.plugins.camera; + +import androidx.annotation.Nullable; +import io.flutter.plugin.common.MethodChannel; + +class PictureCaptureRequest { + + enum State { + idle, + awaitingPreCapture, + capturing, + finished, + error, + } + + private final MethodChannel.Result result; + private State state; + + public PictureCaptureRequest(MethodChannel.Result result) { + this.result = result; + state = State.idle; + } + + public void setState(State state) { + if (isFinished()) throw new IllegalStateException("Request has already been finished"); + this.state = state; + } + + public State getState() { + return state; + } + + public boolean isFinished() { + return state == State.finished || state == State.error; + } + + public void finish(String absolutePath) { + if (isFinished()) throw new IllegalStateException("Request has already been finished"); + result.success(absolutePath); + state = State.finished; + } + + public void error( + String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + if (isFinished()) throw new IllegalStateException("Request has already been finished"); + result.error(errorCode, errorMessage, errorDetails); + state = State.error; + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java new file mode 100644 index 000000000000..eddeddc47eab --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java @@ -0,0 +1,16 @@ +package io.flutter.plugins.camera.types; + +// Mirrors flash_mode.dart +public enum FlashMode { + off, + auto, + always; + + public static FlashMode getValueForString(String modeStr) { + try { + return valueOf(modeStr); + } catch (IllegalArgumentException | NullPointerException e) { + return null; + } + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/ResolutionPreset.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/ResolutionPreset.java new file mode 100644 index 000000000000..ffbe2e62095d --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/ResolutionPreset.java @@ -0,0 +1,11 @@ +package io.flutter.plugins.camera.types; + +// Mirrors camera.dart +public enum ResolutionPreset { + low, + medium, + high, + veryHigh, + ultraHigh, + max, +} 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 new file mode 100644 index 000000000000..2b6aa0f25fcf --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java @@ -0,0 +1,93 @@ +package io.flutter.plugins.camera; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import io.flutter.plugin.common.MethodChannel; +import org.junit.Test; + +public class PictureCaptureRequestTest { + + @Test + public void state_is_idle_by_default() { + PictureCaptureRequest req = new PictureCaptureRequest(null); + assertEquals("Default state is idle", req.getState(), PictureCaptureRequest.State.idle); + } + + @Test + public void setState_sets_state() { + PictureCaptureRequest req = new PictureCaptureRequest(null); + req.setState(PictureCaptureRequest.State.awaitingPreCapture); + assertEquals( + "State is awaitingPreCapture", + req.getState(), + PictureCaptureRequest.State.awaitingPreCapture); + req.setState(PictureCaptureRequest.State.capturing); + assertEquals( + "State is awaitingPreCapture", req.getState(), PictureCaptureRequest.State.capturing); + } + + @Test + public void finish_sets_result_and_state() { + // Setup + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + PictureCaptureRequest req = new PictureCaptureRequest(mockResult); + // Act + req.finish("/test/path"); + // Test + verify(mockResult).success("/test/path"); + assertEquals("State is finished", req.getState(), PictureCaptureRequest.State.finished); + } + + @Test + public void isFinished_is_true_When_state_is_finished_or_error() { + // Setup + PictureCaptureRequest req = new PictureCaptureRequest(null); + // Test false states + req.setState(PictureCaptureRequest.State.idle); + assertFalse(req.isFinished()); + req.setState(PictureCaptureRequest.State.awaitingPreCapture); + assertFalse(req.isFinished()); + req.setState(PictureCaptureRequest.State.capturing); + assertFalse(req.isFinished()); + // Test true states + req.setState(PictureCaptureRequest.State.finished); + assertTrue(req.isFinished()); + req = new PictureCaptureRequest(null); // Refresh + req.setState(PictureCaptureRequest.State.error); + assertTrue(req.isFinished()); + } + + @Test(expected = IllegalStateException.class) + public void finish_throws_When_already_finished() { + // Setup + PictureCaptureRequest req = new PictureCaptureRequest(null); + req.setState(PictureCaptureRequest.State.finished); + // Act + req.finish("/test/path"); + } + + @Test + public void error_sets_result_and_state() { + // Setup + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + PictureCaptureRequest req = new PictureCaptureRequest(mockResult); + // Act + req.error("ERROR_CODE", "Error Message", null); + // Test + verify(mockResult).error("ERROR_CODE", "Error Message", null); + assertEquals("State is error", req.getState(), PictureCaptureRequest.State.error); + } + + @Test(expected = IllegalStateException.class) + public void error_throws_When_already_finished() { + // Setup + PictureCaptureRequest req = new PictureCaptureRequest(null); + req.setState(PictureCaptureRequest.State.finished); + // Act + req.error(null, null, null); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FlashModeTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FlashModeTest.java new file mode 100644 index 000000000000..0549e4fc750e --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FlashModeTest.java @@ -0,0 +1,26 @@ +package io.flutter.plugins.camera.types; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class FlashModeTest { + + @Test + public void getValueForString_returns_correct_values() { + assertEquals( + "Returns FlashMode.off for 'off'", FlashMode.getValueForString("off"), FlashMode.off); + assertEquals( + "Returns FlashMode.auto for 'auto'", FlashMode.getValueForString("auto"), FlashMode.auto); + assertEquals( + "Returns FlashMode.always for 'always'", + FlashMode.getValueForString("always"), + FlashMode.always); + } + + @Test + public void getValueForString_returns_null_for_nonexistant_value() { + assertEquals( + "Returns null for 'nonexistant'", FlashMode.getValueForString("nonexistant"), null); + } +} diff --git a/packages/camera/camera/example/android/app/src/main/AndroidManifest.xml b/packages/camera/camera/example/android/app/src/main/AndroidManifest.xml index f37f82306024..f216a7251bcf 100644 --- a/packages/camera/camera/example/android/app/src/main/AndroidManifest.xml +++ b/packages/camera/camera/example/android/app/src/main/AndroidManifest.xml @@ -37,4 +37,5 @@ android:required="true"/> + diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart index e1edc1b06386..847779dade09 100644 --- a/packages/camera/camera/example/lib/main.dart +++ b/packages/camera/camera/example/lib/main.dart @@ -101,6 +101,7 @@ class _CameraExampleHomeState extends State ), ), _captureControlRowWidget(), + _flashModeRowWidget(), _toggleAudioWidget(), Padding( padding: const EdgeInsets.all(5.0), @@ -191,6 +192,43 @@ class _CameraExampleHomeState extends State ); } + /// Display a bar with buttons to change the flash mode + Widget _flashModeRowWidget() { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.max, + children: [ + IconButton( + icon: const Icon(Icons.flash_off), + color: controller?.value?.flashMode == FlashMode.off + ? Colors.orange + : Colors.blue, + onPressed: controller != null + ? () => onFlashModeButtonPressed(FlashMode.off) + : null, + ), + IconButton( + icon: const Icon(Icons.flash_auto), + color: controller?.value?.flashMode == FlashMode.auto + ? Colors.orange + : Colors.blue, + onPressed: controller != null + ? () => onFlashModeButtonPressed(FlashMode.auto) + : null, + ), + IconButton( + icon: const Icon(Icons.flash_on), + color: controller?.value?.flashMode == FlashMode.always + ? Colors.orange + : Colors.blue, + onPressed: controller != null + ? () => onFlashModeButtonPressed(FlashMode.always) + : null, + ), + ], + ); + } + /// Display the control bar with buttons to take pictures and record videos. Widget _captureControlRowWidget() { return Row( @@ -317,6 +355,13 @@ class _CameraExampleHomeState extends State }); } + void onFlashModeButtonPressed(FlashMode mode) { + setFlashMode(mode).then((_) { + if (mounted) setState(() {}); + showInSnackBar('Flash mode set to ${mode.toString().split('.').last}'); + }); + } + void onVideoRecordButtonPressed() { startVideoRecording().then((_) { if (mounted) setState(() {}); @@ -406,6 +451,15 @@ class _CameraExampleHomeState extends State } } + Future setFlashMode(FlashMode mode) async { + try { + await controller.setFlashMode(mode); + } on CameraException catch (e) { + _showCameraException(e); + rethrow; + } + } + Future _startVideoPlayer() async { final VideoPlayerController vController = VideoPlayerController.file(File(videoFile.path)); diff --git a/packages/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m index 9455375b8524..cc70ddf209ce 100644 --- a/packages/camera/camera/ios/Classes/CameraPlugin.m +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -114,6 +114,24 @@ - (UIImageOrientation)getImageRotation { } @end +static AVCaptureFlashMode getFlashModeForString(NSString *mode) { + if ([mode isEqualToString:@"off"]) { + return AVCaptureFlashModeOff; + } else if ([mode isEqualToString:@"auto"]) { + return AVCaptureFlashModeAuto; + } else if ([mode isEqualToString:@"always"]) { + return AVCaptureFlashModeOn; + } else { + NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain + code:NSURLErrorUnknown + userInfo:@{ + NSLocalizedDescriptionKey : [NSString + stringWithFormat:@"Unknown flash mode %@", mode] + }]; + @throw error; + } +} + // Mirrors ResolutionPreset in camera.dart typedef enum { veryLow, @@ -181,6 +199,7 @@ @interface FLTCam : NSObject *)messenger { if (!_isStreamingImages) { FlutterEventChannel *eventChannel = @@ -910,6 +956,8 @@ - (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)re [_camera pauseVideoRecordingWithResult:result]; } else if ([@"resumeVideoRecording" isEqualToString:call.method]) { [_camera resumeVideoRecordingWithResult:result]; + } else if ([@"setFlashMode" isEqualToString:call.method]) { + [_camera setFlashModeWithResult:result mode:call.arguments[@"mode"]]; } else { result(FlutterMethodNotImplemented); } diff --git a/packages/camera/camera/lib/camera.dart b/packages/camera/camera/lib/camera.dart index 6c6d90b9bcee..6c6214e96951 100644 --- a/packages/camera/camera/lib/camera.dart +++ b/packages/camera/camera/lib/camera.dart @@ -11,5 +11,6 @@ export 'package:camera_platform_interface/camera_platform_interface.dart' CameraDescription, CameraException, CameraLensDirection, + FlashMode, ResolutionPreset, XFile; diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart index fcf00245ce7f..2c2ce3b633f1 100644 --- a/packages/camera/camera/lib/src/camera_controller.dart +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -36,6 +36,7 @@ class CameraValue { this.isTakingPicture, this.isStreamingImages, bool isRecordingPaused, + this.flashMode, }) : _isRecordingPaused = isRecordingPaused; /// Creates a new camera controller state for an uninitialized controller. @@ -46,6 +47,7 @@ class CameraValue { isTakingPicture: false, isStreamingImages: false, isRecordingPaused: false, + flashMode: FlashMode.auto, ); /// True after [CameraController.initialize] has completed successfully. @@ -86,6 +88,9 @@ class CameraValue { /// When true [errorDescription] describes the error. bool get hasError => errorDescription != null; + /// The flash mode the camera is currently set to. + final FlashMode flashMode; + /// Creates a modified copy of the object. /// /// Explicitly specified fields get the specified value, all other fields get @@ -98,6 +103,7 @@ class CameraValue { String errorDescription, Size previewSize, bool isRecordingPaused, + FlashMode flashMode, }) { return CameraValue( isInitialized: isInitialized ?? this.isInitialized, @@ -107,6 +113,7 @@ class CameraValue { isTakingPicture: isTakingPicture ?? this.isTakingPicture, isStreamingImages: isStreamingImages ?? this.isStreamingImages, isRecordingPaused: isRecordingPaused ?? _isRecordingPaused, + flashMode: flashMode ?? this.flashMode, ); } @@ -117,7 +124,8 @@ class CameraValue { 'isInitialized: $isInitialized, ' 'errorDescription: $errorDescription, ' 'previewSize: $previewSize, ' - 'isStreamingImages: $isStreamingImages)'; + 'isStreamingImages: $isStreamingImages, ' + 'flashMode: $flashMode)'; } } @@ -468,6 +476,16 @@ class CameraController extends ValueNotifier { } } + /// Sets the flash mode for taking pictures. + Future setFlashMode(FlashMode mode) async { + try { + await CameraPlatform.instance.setFlashMode(_cameraId, mode); + value = value.copyWith(flashMode: mode); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + /// Releases the resources of this camera. @override Future dispose() async { diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml index 3c3e0cea3565..3194fff33684 100644 --- a/packages/camera/camera/pubspec.yaml +++ b/packages/camera/camera/pubspec.yaml @@ -2,13 +2,13 @@ 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.0+2 +version: 0.6.1 homepage: https://github.com/flutter/plugins/tree/master/packages/camera/camera dependencies: flutter: sdk: flutter - camera_platform_interface: ^1.0.0 + camera_platform_interface: ^1.0.4 dev_dependencies: path_provider: ^0.5.0 diff --git a/packages/camera/camera/test/camera_test.dart b/packages/camera/camera/test/camera_test.dart index b129849cd141..c19aa9718f47 100644 --- a/packages/camera/camera/test/camera_test.dart +++ b/packages/camera/camera/test/camera_test.dart @@ -310,6 +310,51 @@ void main() { 'startVideoRecording was called while a camera was streaming images.', ))); }); + + test('setFlashMode() calls $CameraPlatform', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + await cameraController.setFlashMode(FlashMode.always); + + verify(CameraPlatform.instance + .setFlashMode(cameraController.cameraId, FlashMode.always)) + .called(1); + }); + + test('setFlashMode() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + when(CameraPlatform.instance + .setFlashMode(cameraController.cameraId, FlashMode.always)) + .thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error message', + details: null, + ), + ); + + expect( + cameraController.setFlashMode(FlashMode.always), + throwsA(isA().having( + (error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); }); } diff --git a/packages/camera/camera/test/camera_value_test.dart b/packages/camera/camera/test/camera_value_test.dart index 28255eb0a568..06b327cb1c29 100644 --- a/packages/camera/camera/test/camera_value_test.dart +++ b/packages/camera/camera/test/camera_value_test.dart @@ -5,6 +5,7 @@ import 'dart:ui'; import 'package:camera/camera.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -19,6 +20,7 @@ void main() { isRecordingVideo: false, isTakingPicture: false, isStreamingImages: false, + flashMode: FlashMode.auto, ); expect(cameraValue, isA()); @@ -56,6 +58,7 @@ void main() { expect(cameraValue.isRecordingVideo, isFalse); expect(cameraValue.isTakingPicture, isFalse); expect(cameraValue.isStreamingImages, isFalse); + expect(cameraValue.flashMode, FlashMode.auto); }); test('Has aspectRatio after setting size', () { @@ -93,10 +96,11 @@ void main() { isRecordingVideo: false, isTakingPicture: false, isStreamingImages: false, + flashMode: FlashMode.auto, ); expect(cameraValue.toString(), - 'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false)'); + 'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto)'); }); }); }