diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md index 995e9170688..f556c8a9872 100644 --- a/packages/camera/camera_android_camerax/CHANGELOG.md +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.0+20 + +* Implements `setZoomLevel`. + ## 0.5.0+19 * Implements torch flash mode. diff --git a/packages/camera/camera_android_camerax/README.md b/packages/camera/camera_android_camerax/README.md index a9a3ddc305f..073daa16d0e 100644 --- a/packages/camera/camera_android_camerax/README.md +++ b/packages/camera/camera_android_camerax/README.md @@ -43,10 +43,6 @@ and thus, the plugin will fall back to 480p if configured with a `setFocusMode` & `setFocusPoint` are unimplemented. -### Zoom configuration \[[Issue #125371][125371]\] - -`setZoomLevel` is unimplemented. - ### Setting maximum duration and stream options for video capture Calling `startVideoCapturing` with `VideoCaptureOptions` configured with diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraControlHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraControlHostApiImpl.java index 8241fdaad9a..e70714a8bff 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraControlHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraControlHostApiImpl.java @@ -51,6 +51,29 @@ public void onFailure(Throwable t) { }, ContextCompat.getMainExecutor(context)); } + + /** Sets the zoom ratio of the specified {@link CameraControl} instance. */ + @NonNull + public void setZoomRatio( + @NonNull CameraControl cameraControl, + @NonNull Double ratio, + @NonNull GeneratedCameraXLibrary.Result result) { + float ratioAsFloat = ratio.floatValue(); + ListenableFuture setZoomRatioFuture = cameraControl.setZoomRatio(ratioAsFloat); + + Futures.addCallback( + setZoomRatioFuture, + new FutureCallback() { + public void onSuccess(Void voidResult) { + result.success(null); + } + + public void onFailure(Throwable t) { + result.error(t); + } + }, + ContextCompat.getMainExecutor(context)); + } } /** @@ -81,7 +104,8 @@ public CameraControlHostApiImpl( } /** - * Sets the context that the {@code ProcessCameraProvider} will use to enable/disable torch mode. + * Sets the context that the {@code ProcessCameraProvider} will use to enable/disable torch mode + * and set the zoom ratio. * *

If using the camera plugin in an add-to-app context, ensure that a new instance of the * {@code CameraControl} is fetched via {@code #enableTorch} anytime the context changes. @@ -98,4 +122,13 @@ public void enableTorch( proxy.enableTorch( Objects.requireNonNull(instanceManager.getInstance(identifier)), torch, result); } + + @Override + public void setZoomRatio( + @NonNull Long identifier, + @NonNull Double ratio, + @NonNull GeneratedCameraXLibrary.Result result) { + proxy.setZoomRatio( + Objects.requireNonNull(instanceManager.getInstance(identifier)), ratio, result); + } } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java index 224ba11e038..603af1f42a0 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java @@ -3237,6 +3237,9 @@ public interface CameraControlHostApi { void enableTorch( @NonNull Long identifier, @NonNull Boolean torch, @NonNull Result result); + void setZoomRatio( + @NonNull Long identifier, @NonNull Double ratio, @NonNull Result result); + /** The codec used by CameraControlHostApi. */ static @NonNull MessageCodec getCodec() { return new StandardMessageCodec(); @@ -3280,6 +3283,41 @@ public void error(Throwable error) { channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.CameraControlHostApi.setZoomRatio", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + Double ratioArg = (Double) args.get(1); + Result resultCallback = + new Result() { + public void success(Void result) { + wrapped.add(0, null); + reply.reply(wrapped); + } + + public void error(Throwable error) { + ArrayList wrappedError = wrapError(error); + reply.reply(wrappedError); + } + }; + + api.setZoomRatio( + (identifierArg == null) ? null : identifierArg.longValue(), + ratioArg, + resultCallback); + }); + } else { + channel.setMessageHandler(null); + } + } } } /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraControlTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraControlTest.java index 5e17f644136..a01e3db8238 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraControlTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraControlTest.java @@ -97,6 +97,57 @@ public void enableTorch_turnsTorchModeOnAndOffAsExpected() { } } + @Test + public void setZoomRatio_setsZoomAsExpected() { + try (MockedStatic mockedFutures = Mockito.mockStatic(Futures.class)) { + final CameraControlHostApiImpl cameraControlHostApiImpl = + new CameraControlHostApiImpl(testInstanceManager, mock(Context.class)); + final Long cameraControlIdentifier = 33L; + final Double zoomRatio = 0.2D; + + @SuppressWarnings("unchecked") + final ListenableFuture setZoomRatioFuture = mock(ListenableFuture.class); + + testInstanceManager.addDartCreatedInstance(cameraControl, cameraControlIdentifier); + + when(cameraControl.setZoomRatio(zoomRatio.floatValue())).thenReturn(setZoomRatioFuture); + + @SuppressWarnings("unchecked") + final ArgumentCaptor> futureCallbackCaptor = + ArgumentCaptor.forClass(FutureCallback.class); + + // Test successful behavior. + @SuppressWarnings("unchecked") + final GeneratedCameraXLibrary.Result successfulMockResult = + mock(GeneratedCameraXLibrary.Result.class); + cameraControlHostApiImpl.setZoomRatio( + cameraControlIdentifier, zoomRatio, successfulMockResult); + mockedFutures.verify( + () -> Futures.addCallback(eq(setZoomRatioFuture), futureCallbackCaptor.capture(), any())); + mockedFutures.clearInvocations(); + + FutureCallback successfulSetZoomRatioCallback = futureCallbackCaptor.getValue(); + + successfulSetZoomRatioCallback.onSuccess(mock(Void.class)); + verify(successfulMockResult).success(null); + + // Test failed behavior. + @SuppressWarnings("unchecked") + final GeneratedCameraXLibrary.Result failedMockResult = + mock(GeneratedCameraXLibrary.Result.class); + final Throwable testThrowable = new Throwable(); + cameraControlHostApiImpl.setZoomRatio(cameraControlIdentifier, zoomRatio, failedMockResult); + mockedFutures.verify( + () -> Futures.addCallback(eq(setZoomRatioFuture), futureCallbackCaptor.capture(), any())); + mockedFutures.clearInvocations(); + + FutureCallback failedSetZoomRatioCallback = futureCallbackCaptor.getValue(); + + failedSetZoomRatioCallback.onFailure(testThrowable); + verify(failedMockResult).error(testThrowable); + } + } + @Test public void flutterApiCreate_makesCallToCreateInstanceOnDartSide() { final CameraControlFlutterApiImpl spyFlutterApi = diff --git a/packages/camera/camera_android_camerax/example/lib/main.dart b/packages/camera/camera_android_camerax/example/lib/main.dart index ec6fb512eeb..a4434e7d5a9 100644 --- a/packages/camera/camera_android_camerax/example/lib/main.dart +++ b/packages/camera/camera_android_camerax/example/lib/main.dart @@ -392,12 +392,8 @@ class _CameraExampleHomeState extends State style: styleAuto, onPressed: () {}, // TODO(camsim99): Add functionality back here. - onLongPress: () { - if (controller != null) { - controller!.setExposurePoint(null); - showInSnackBar('Resetting exposure point'); - } - }, + onLongPress: + () {}, // TODO(camsim99): Add functionality back here., child: const Text('AUTO'), ), TextButton( @@ -470,12 +466,8 @@ class _CameraExampleHomeState extends State style: styleAuto, onPressed: () {}, // TODO(camsim99): Add functionality back here. - onLongPress: () { - if (controller != null) { - controller!.setFocusPoint(null); - } - showInSnackBar('Resetting focus point'); - }, + onLongPress: + () {}, // TODO(camsim99): Add functionality back here. child: const Text('AUTO'), ), TextButton( 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 8b06bf61a91..0c14b279b02 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 @@ -439,6 +439,17 @@ class AndroidCameraCameraX extends CameraPlatform { return zoomState.minZoomRatio; } + /// Set the zoom level for the selected camera. + /// + /// The supplied [zoom] value should be between the minimum and the maximum + /// supported zoom level returned by `getMinZoomLevel` and `getMaxZoomLevel`. + /// Throws a `CameraException` when an illegal zoom level is supplied. + @override + Future setZoomLevel(int cameraId, double zoom) async { + final CameraControl cameraControl = await camera!.getCameraControl(); + await cameraControl.setZoomRatio(zoom); + } + /// The ui orientation changed. @override Stream onDeviceOrientationChanged() { diff --git a/packages/camera/camera_android_camerax/lib/src/camera_control.dart b/packages/camera/camera_android_camerax/lib/src/camera_control.dart index b30195ce928..57b84772be3 100644 --- a/packages/camera/camera_android_camerax/lib/src/camera_control.dart +++ b/packages/camera/camera_android_camerax/lib/src/camera_control.dart @@ -32,9 +32,22 @@ class CameraControl extends JavaObject { late final _CameraControlHostApiImpl _api; /// Enables or disables the torch of related [Camera] instance. + /// + /// If the torch mode was unable to be changed, an error message will be + /// added to [SystemServices.cameraErrorStreamController]. Future enableTorch(bool torch) async { return _api.enableTorchFromInstance(this, torch); } + + /// Sets zoom of related [Camera] by ratio. + /// + /// Ratio should be between what the `minZoomRatio` and `maxZoomRatio` of the + /// [ZoomState] of the [CameraInfo] instance that is retrievable from the same + /// [Camera] instance; otherwise, an error message will be added to + /// [SystemServices.cameraErrorStreamController]. + Future setZoomRatio(double ratio) async { + return _api.setZoomRatioFromInstance(this, ratio); + } } /// Host API implementation of [CameraControl]. @@ -69,6 +82,18 @@ class _CameraControlHostApiImpl extends CameraControlHostApi { .add(e.message ?? 'The camera was unable to change torch modes.'); } } + + /// Sets zoom of specified [CameraControl] instance by ratio. + Future setZoomRatioFromInstance( + CameraControl instance, double ratio) async { + final int identifier = instanceManager.getIdentifier(instance)!; + try { + await setZoomRatio(identifier, ratio); + } on PlatformException catch (e) { + SystemServices.cameraErrorStreamController.add(e.message ?? + 'Zoom ratio was unable to be set. If ratio was not out of range, newer value may have been set; otherwise, the camera may be closed.'); + } + } } /// Flutter API implementation of [CameraControl]. 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 4fc4d1573ef..4dd5253bd2d 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 @@ -2685,6 +2685,28 @@ class CameraControlHostApi { return; } } + + Future setZoomRatio(int arg_identifier, double arg_ratio) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CameraControlHostApi.setZoomRatio', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_identifier, arg_ratio]) as List?; + if (replyList == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyList.length > 1) { + throw PlatformException( + code: replyList[0]! as String, + message: replyList[1] as String?, + details: replyList[2], + ); + } else { + return; + } + } } abstract class CameraControlFlutterApi { diff --git a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart index 2c5b9c2a77c..22827a803eb 100644 --- a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart +++ b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart @@ -424,6 +424,9 @@ abstract class FallbackStrategyHostApi { abstract class CameraControlHostApi { @async void enableTorch(int identifier, bool torch); + + @async + void setZoomRatio(int identifier, double ratio); } @FlutterApi() diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index 238d94c8c96..6e231e43852 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.5.0+19 +version: 0.5.0+20 environment: sdk: ">=2.19.0 <4.0.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 10c8e5b8723..e96b4c57f1f 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 @@ -1219,6 +1219,22 @@ void main() { expect(await camera.getMinZoomLevel(55), minZoomRatio); }); + test('setZoomLevel sets zoom ratio as expected', () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int cameraId = 44; + const double zoomRatio = 0.3; + final MockCameraControl mockCameraControl = MockCameraControl(); + + camera.camera = MockCamera(); + + when(camera.camera!.getCameraControl()) + .thenAnswer((_) async => mockCameraControl); + + await camera.setZoomLevel(cameraId, zoomRatio); + + verify(mockCameraControl.setZoomRatio(zoomRatio)); + }); + test( 'onStreamedFrameAvailable emits CameraImageData when picked up from CameraImageData stream controller', () async { 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 f275ea7021a..4fae31e0639 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 @@ -368,6 +368,15 @@ class MockCameraControl extends _i1.Mock implements _i3.CameraControl { returnValue: _i15.Future.value(), returnValueForMissingStub: _i15.Future.value(), ) as _i15.Future); + @override + _i15.Future setZoomRatio(double? ratio) => (super.noSuchMethod( + Invocation.method( + #setZoomRatio, + [ratio], + ), + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); } /// A class which mocks [CameraImageData]. diff --git a/packages/camera/camera_android_camerax/test/camera_control_test.dart b/packages/camera/camera_android_camerax/test/camera_control_test.dart index 99acc94b4bc..77a9e0c0327 100644 --- a/packages/camera/camera_android_camerax/test/camera_control_test.dart +++ b/packages/camera/camera_android_camerax/test/camera_control_test.dart @@ -47,6 +47,32 @@ void main() { verify(mockApi.enableTorch(cameraControlIdentifier, enableTorch)); }); + test('setZoomRatio makes call on Java side to set zoom ratio', () async { + final MockTestCameraControlHostApi mockApi = + MockTestCameraControlHostApi(); + TestCameraControlHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + final CameraControl cameraControl = CameraControl.detached( + instanceManager: instanceManager, + ); + const int cameraControlIdentifier = 45; + + instanceManager.addHostCreatedInstance( + cameraControl, + cameraControlIdentifier, + onCopy: (_) => CameraControl.detached(instanceManager: instanceManager), + ); + + const double zoom = 0.2; + await cameraControl.setZoomRatio(zoom); + + verify(mockApi.setZoomRatio(cameraControlIdentifier, zoom)); + }); + test('flutterApiCreate makes call to add instance to instance manager', () { final InstanceManager instanceManager = InstanceManager( onWeakReferenceRemoved: (_) {}, diff --git a/packages/camera/camera_android_camerax/test/camera_control_test.mocks.dart b/packages/camera/camera_android_camerax/test/camera_control_test.mocks.dart index 0f43f0c4348..519fc623669 100644 --- a/packages/camera/camera_android_camerax/test/camera_control_test.mocks.dart +++ b/packages/camera/camera_android_camerax/test/camera_control_test.mocks.dart @@ -47,6 +47,22 @@ class MockTestCameraControlHostApi extends _i1.Mock returnValue: _i3.Future.value(), returnValueForMissingStub: _i3.Future.value(), ) as _i3.Future); + @override + _i3.Future setZoomRatio( + int? identifier, + double? ratio, + ) => + (super.noSuchMethod( + Invocation.method( + #setZoomRatio, + [ + identifier, + ratio, + ], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); } /// A class which mocks [TestInstanceManagerHostApi]. diff --git a/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart b/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart index 88e44ae6119..4773effcb73 100644 --- a/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart +++ b/packages/camera/camera_android_camerax/test/test_camerax_library.g.dart @@ -1753,6 +1753,8 @@ abstract class TestCameraControlHostApi { Future enableTorch(int identifier, bool torch); + Future setZoomRatio(int identifier, double ratio); + static void setup(TestCameraControlHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -1780,5 +1782,30 @@ abstract class TestCameraControlHostApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.CameraControlHostApi.setZoomRatio', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, null); + } else { + _testBinaryMessengerBinding!.defaultBinaryMessenger + .setMockDecodedMessageHandler(channel, + (Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.CameraControlHostApi.setZoomRatio was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.CameraControlHostApi.setZoomRatio was null, expected non-null int.'); + final double? arg_ratio = (args[1] as double?); + assert(arg_ratio != null, + 'Argument for dev.flutter.pigeon.CameraControlHostApi.setZoomRatio was null, expected non-null double.'); + await api.setZoomRatio(arg_identifier!, arg_ratio!); + return []; + }); + } + } } }