diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md index 17e120259871..d2eeaea58468 100644 --- a/packages/camera/camera_android_camerax/CHANGELOG.md +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.6.28 + +* Adds more descriptive error to camera error stream when image capture fails. + ## 0.6.27 * Changes `availableCameras` to get the camera name from `Camera2CameraInfo.getCameraId`. diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXLibrary.g.kt b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXLibrary.g.kt index a2bf38829c44..82c0e0d749de 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXLibrary.g.kt +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraXLibrary.g.kt @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v26.1.4), do not edit directly. +// Autogenerated from Pigeon (v26.1.5), do not edit directly. // See also: https://pub.dev/packages/pigeon @file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") @@ -4257,6 +4257,7 @@ abstract class PigeonApiImageCapture( /** Captures a new still image for in memory access. */ abstract fun takePicture( pigeon_instance: androidx.camera.core.ImageCapture, + systemServicesManager: SystemServicesManager, callback: (Result) -> Unit ) @@ -4331,7 +4332,9 @@ abstract class PigeonApiImageCapture( channel.setMessageHandler { message, reply -> val args = message as List val pigeon_instanceArg = args[0] as androidx.camera.core.ImageCapture - api.takePicture(pigeon_instanceArg) { result: Result -> + val systemServicesManagerArg = args[1] as SystemServicesManager + api.takePicture(pigeon_instanceArg, systemServicesManagerArg) { result: Result + -> val error = result.exceptionOrNull() if (error != null) { reply.reply(CameraXLibraryPigeonUtils.wrapError(error)) diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageCaptureProxyApi.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageCaptureProxyApi.java index 1f7afd3e7140..7bf6bd508a25 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageCaptureProxyApi.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageCaptureProxyApi.java @@ -85,6 +85,7 @@ public void setFlashMode( @Override public void takePicture( @NonNull ImageCapture pigeonInstance, + @NonNull SystemServicesManager systemServicesManager, @NonNull Function1, Unit> callback) { final File outputDir = getPigeonRegistrar().getContext().getCacheDir(); File temporaryCaptureFile; @@ -98,7 +99,7 @@ public void takePicture( final ImageCapture.OutputFileOptions outputFileOptions = createImageCaptureOutputFileOptions(temporaryCaptureFile); final ImageCapture.OnImageSavedCallback onImageSavedCallback = - createOnImageSavedCallback(temporaryCaptureFile, callback); + createOnImageSavedCallback(temporaryCaptureFile, systemServicesManager, callback); pigeonInstance.takePicture( outputFileOptions, Executors.newSingleThreadExecutor(), onImageSavedCallback); @@ -121,7 +122,9 @@ ImageCapture.OutputFileOptions createImageCaptureOutputFileOptions(@NonNull File @NonNull ImageCapture.OnImageSavedCallback createOnImageSavedCallback( - @NonNull File file, @NonNull Function1, Unit> callback) { + @NonNull File file, + @NonNull SystemServicesManager systemServicesManager, + @NonNull Function1, Unit> callback) { return new ImageCapture.OnImageSavedCallback() { @Override public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) { @@ -130,8 +133,32 @@ public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResul @Override public void onError(@NonNull ImageCaptureException exception) { + systemServicesManager.onCameraError( + getImageCaptureExceptionDescription(exception.getImageCaptureError())); ResultCompat.failure(exception, callback); } }; } + + /** + * Returns an error description for each {@link ImageCaptureException} error code. + * + *

See + * https://developer.android.com/reference/androidx/camera/core/ImageCaptureException#getImageCaptureError() + * for details on each error type. + */ + String getImageCaptureExceptionDescription(int imageCaptureErrorCode) { + switch (imageCaptureErrorCode) { + case ImageCapture.ERROR_FILE_IO: + return "An error occurred while attempting to save the captured image to a file."; + case ImageCapture.ERROR_CAPTURE_FAILED: + return "The camera framework failed to fulfill the image capture request."; + case ImageCapture.ERROR_CAMERA_CLOSED: + return "Image capture failed due to the camera being closed."; + case ImageCapture.ERROR_INVALID_CAMERA: + return "The ImageCapture use case was bound to an invalid camera by the Flutter camera plugin. If you see this error, please file an issue if you cannot find one that already exists: https://github.com/flutter/flutter/issues/."; + default: + return "An unknown error has occurred while attempting to take a picture. Check the logs for more details."; + } + } } diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ImageCaptureTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ImageCaptureTest.java index 56d800ad86b3..5c84dff947df 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ImageCaptureTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ImageCaptureTest.java @@ -88,11 +88,13 @@ ImageCapture.OutputFileOptions createImageCaptureOutputFileOptions(@NonNull File @NonNull @Override ImageCapture.OnImageSavedCallback createOnImageSavedCallback( - @NonNull File file, @NonNull Function1, Unit> callback) { + @NonNull File file, + @NonNull SystemServicesManager systemServicesManager, + @NonNull Function1, Unit> callback) { final File mockFile = mock(File.class); when(mockFile.getAbsolutePath()).thenReturn(filename); final ImageCapture.OnImageSavedCallback imageSavedCallback = - super.createOnImageSavedCallback(mockFile, callback); + super.createOnImageSavedCallback(mockFile, systemServicesManager, callback); imageSavedCallback.onImageSaved(mock(ImageCapture.OutputFileResults.class)); return imageSavedCallback; } @@ -114,6 +116,7 @@ ImageCapture.OnImageSavedCallback createOnImageSavedCallback( final String[] result = {null}; api.takePicture( instance, + mock(SystemServicesManager.class), ResultCompat.asCompatCallback( reply -> { result[0] = reply.getOrNull(); @@ -155,6 +158,7 @@ public void takePicture_sendsErrorWhenTemporaryFileCannotBeCreated() { final Throwable[] result = {null}; api.takePicture( instance, + mock(SystemServicesManager.class), ResultCompat.asCompatCallback( reply -> { result[0] = reply.exceptionOrNull(); @@ -167,14 +171,16 @@ public void takePicture_sendsErrorWhenTemporaryFileCannotBeCreated() { } @Test - public void takePicture_onImageSavedCallbackCanSendsError() { + public void takePicture_onImageSavedCallbackSendsErrorAndReportsException() { final ProxyApiRegistrar mockApiRegistrar = mock(ProxyApiRegistrar.class); final Context mockContext = mock(Context.class); final File mockOutputDir = mock(File.class); + final SystemServicesManager mockSystemServicesManager = mock(SystemServicesManager.class); when(mockContext.getCacheDir()).thenReturn(mockOutputDir); when(mockApiRegistrar.getContext()).thenReturn(mockContext); final ImageCaptureException captureException = mock(ImageCaptureException.class); + when(captureException.getImageCaptureError()).thenReturn(ImageCapture.ERROR_CAPTURE_FAILED); final ImageCaptureProxyApi api = new ImageCaptureProxyApi(mockApiRegistrar) { @Override @@ -185,9 +191,11 @@ ImageCapture.OutputFileOptions createImageCaptureOutputFileOptions(@NonNull File @NonNull @Override ImageCapture.OnImageSavedCallback createOnImageSavedCallback( - @NonNull File file, @NonNull Function1, Unit> callback) { + @NonNull File file, + @NonNull SystemServicesManager systemServicesManager, + @NonNull Function1, Unit> callback) { final ImageCapture.OnImageSavedCallback imageSavedCallback = - super.createOnImageSavedCallback(mock(File.class), callback); + super.createOnImageSavedCallback(mock(File.class), systemServicesManager, callback); imageSavedCallback.onError(captureException); return imageSavedCallback; } @@ -209,6 +217,7 @@ ImageCapture.OnImageSavedCallback createOnImageSavedCallback( final Throwable[] result = {null}; api.takePicture( instance, + mockSystemServicesManager, ResultCompat.asCompatCallback( reply -> { result[0] = reply.exceptionOrNull(); @@ -220,6 +229,8 @@ ImageCapture.OnImageSavedCallback createOnImageSavedCallback( any(ImageCapture.OutputFileOptions.class), any(Executor.class), any(ImageCapture.OnImageSavedCallback.class)); + verify(mockSystemServicesManager) + .onCameraError("The camera framework failed to fulfill the image capture request."); assertEquals(result[0], captureException); } } diff --git a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart index 43c96f272692..d09252b62c80 100644 --- a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart +++ b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax.dart @@ -1040,7 +1040,9 @@ class AndroidCameraCameraX extends CameraPlatform { ); } - final String picturePath = await imageCapture!.takePicture(); + final String picturePath = await imageCapture!.takePicture( + systemServicesManager, + ); return XFile(picturePath); } diff --git a/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart b/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart index e850b3e71860..a2fccdb06b93 100644 --- a/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart +++ b/packages/camera/camera_android_camerax/lib/src/camerax_library.g.dart @@ -1,7 +1,7 @@ // Copyright 2013 The Flutter Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v26.1.4), do not edit directly. +// Autogenerated from Pigeon (v26.1.5), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, omit_obvious_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers @@ -5258,7 +5258,9 @@ class ImageCapture extends UseCase { } /// Captures a new still image for in memory access. - Future takePicture() async { + Future takePicture( + SystemServicesManager systemServicesManager, + ) async { final _PigeonInternalProxyApiBaseCodec pigeonChannelCodec = _pigeonVar_codecImageCapture; final BinaryMessenger? pigeonVar_binaryMessenger = pigeon_binaryMessenger; @@ -5270,7 +5272,7 @@ class ImageCapture extends UseCase { binaryMessenger: pigeonVar_binaryMessenger, ); final Future pigeonVar_sendFuture = pigeonVar_channel.send( - [this], + [this, systemServicesManager], ); final pigeonVar_replyList = await pigeonVar_sendFuture as List?; if (pigeonVar_replyList == null) { diff --git a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart index 1b836be11c58..f33db856d4ac 100644 --- a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart +++ b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart @@ -604,7 +604,7 @@ abstract class ImageCapture extends UseCase { /// Captures a new still image for in memory access. @async - String takePicture(); + String takePicture(SystemServicesManager systemServicesManager); /// Sets the desired rotation of the output image. void setTargetRotation(int rotation); diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index 6ab1c8390c95..7471b21c275b 100644 --- a/packages/camera/camera_android_camerax/pubspec.yaml +++ b/packages/camera/camera_android_camerax/pubspec.yaml @@ -2,7 +2,7 @@ name: camera_android_camerax description: Android implementation of the camera plugin using the CameraX library. repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_android_camerax issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 -version: 0.6.27 +version: 0.6.28 environment: sdk: ^3.9.0 diff --git a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart index 9e09c568ff97..0b4033025d6e 100644 --- a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart +++ b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.dart @@ -3854,21 +3854,28 @@ void main() { final mockProcessCameraProvider = MockProcessCameraProvider(); final mockCamera = MockCamera(); final mockCameraInfo = MockCameraInfo(); + final mockImageCapture = MockImageCapture(); const testPicturePath = 'test/absolute/path/to/picture'; // Set directly for test versus calling createCamera. - camera.imageCapture = MockImageCapture(); + camera.imageCapture = mockImageCapture; camera.processCameraProvider = mockProcessCameraProvider; camera.cameraSelector = MockCameraSelector(); // Ignore setting target rotation for this test; tested seprately. camera.captureOrientationLocked = true; - // Tell plugin to create detached camera state observers. + // Tell plugin to create detached camera state observers and mock systemServicesManager. GenericsPigeonOverrides.observerNew = ({required void Function(Observer, T) onChanged}) { return Observer.detached(onChanged: onChanged); }; + PigeonOverrides.systemServicesManager_new = + ({ + required void Function(SystemServicesManager, String) onCameraError, + }) { + return MockSystemServicesManager(); + }; when( mockProcessCameraProvider.isBound(camera.imageCapture), @@ -3884,7 +3891,7 @@ void main() { mockCameraInfo.getCameraState(), ).thenAnswer((_) async => MockLiveCameraState()); when( - camera.imageCapture!.takePicture(), + mockImageCapture.takePicture(argThat(isA())), ).thenAnswer((_) async => testPicturePath); final XFile imageFile = await camera.takePicture(3); @@ -3907,7 +3914,7 @@ void main() { camera.imageCapture = mockImageCapture; camera.processCameraProvider = mockProcessCameraProvider; - // Tell plugin to mock call to get current photo orientation. + // Tell plugin to mock call to get current photo orientation and systemServicesManager. PigeonOverrides.deviceOrientationManager_new = ({ required void Function(DeviceOrientationManager, String) @@ -3919,12 +3926,18 @@ void main() { ).thenAnswer((_) async => defaultTargetRotation); return mockDeviceOrientationManager; }; + PigeonOverrides.systemServicesManager_new = + ({ + required void Function(SystemServicesManager, String) onCameraError, + }) { + return MockSystemServicesManager(); + }; when( mockProcessCameraProvider.isBound(camera.imageCapture), ).thenAnswer((_) async => true); when( - camera.imageCapture!.takePicture(), + mockImageCapture.takePicture(argThat(isA())), ).thenAnswer((_) async => 'test/absolute/path/to/picture'); // Orientation is unlocked and plugin does not need to set default target @@ -3969,6 +3982,14 @@ void main() { // Ignore setting target rotation for this test; tested seprately. camera.captureOrientationLocked = true; + // Tell plugin to mock call to get systemServicesManager. + PigeonOverrides.systemServicesManager_new = + ({ + required void Function(SystemServicesManager, String) onCameraError, + }) { + return MockSystemServicesManager(); + }; + when( mockProcessCameraProvider.isBound(camera.imageCapture), ).thenAnswer((_) async => true); @@ -3995,6 +4016,14 @@ void main() { camera.captureOrientationLocked = true; camera.processCameraProvider = mockProcessCameraProvider; + // Tell plugin to mock call to get current photo orientation and systemServicesManager. + PigeonOverrides.systemServicesManager_new = + ({ + required void Function(SystemServicesManager, String) onCameraError, + }) { + return MockSystemServicesManager(); + }; + when( mockProcessCameraProvider.isBound(camera.imageCapture), ).thenAnswer((_) async => true); diff --git a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart index 2431ccb72a87..1399bcbba6f5 100644 --- a/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart +++ b/packages/camera/camera_android_camerax/test/android_camera_camerax_test.mocks.dart @@ -1444,14 +1444,22 @@ class MockImageCapture extends _i1.Mock implements _i2.ImageCapture { as _i5.Future); @override - _i5.Future takePicture() => + _i5.Future takePicture( + _i2.SystemServicesManager? systemServicesManager, + ) => (super.noSuchMethod( - Invocation.method(#takePicture, []), + Invocation.method(#takePicture, [systemServicesManager]), returnValue: _i5.Future.value( - _i6.dummyValue(this, Invocation.method(#takePicture, [])), + _i6.dummyValue( + this, + Invocation.method(#takePicture, [systemServicesManager]), + ), ), returnValueForMissingStub: _i5.Future.value( - _i6.dummyValue(this, Invocation.method(#takePicture, [])), + _i6.dummyValue( + this, + Invocation.method(#takePicture, [systemServicesManager]), + ), ), ) as _i5.Future);