From bbb41347518e37e6cd13ee266129343f9afc3cd6 Mon Sep 17 00:00:00 2001 From: Camille Simon <43054281+camsim99@users.noreply.github.com> Date: Tue, 2 Jan 2024 11:39:00 -0800 Subject: [PATCH] [camerax] Implement `lockCaptureOrientation` & `unlockCaptureOrientation` (#5285) Implements `lockCaptureOrientation` & `unlockCaptureOrientation` for all camera `UseCase`s. Also fixes small bug concerning not initially setting the target rotation of the `UseCase`s to the requested sensor orientation when `createCamera` is called. Fixes https://github.com/flutter/flutter/issues/125915. --- .../camera_android_camerax/CHANGELOG.md | 4 + .../camera/camera_android_camerax/README.md | 4 - .../camerax/CameraAndroidCameraxPlugin.java | 6 + .../camerax/DeviceOrientationManager.java | 174 +--------- ...eviceOrientationManagerFlutterApiImpl.java | 20 ++ .../DeviceOrientationManagerHostApiImpl.java | 104 ++++++ .../camerax/GeneratedCameraXLibrary.java | 251 ++++++++++++-- .../camerax/ImageAnalysisHostApiImpl.java | 13 +- .../camerax/ImageCaptureHostApiImpl.java | 28 +- .../camerax/PendingRecordingHostApiImpl.java | 9 +- .../plugins/camerax/PreviewHostApiImpl.java | 16 +- .../camerax/SystemServicesFlutterApiImpl.java | 5 - .../camerax/SystemServicesHostApiImpl.java | 44 --- .../camerax/VideoCaptureHostApiImpl.java | 17 +- .../CameraAndroidCameraxPluginTest.java | 2 + .../camerax/DeviceOrientationManagerTest.java | 166 +-------- .../DeviceOrientationManagerWrapperTest.java | 96 ++++++ .../plugins/camerax/ImageAnalysisTest.java | 19 +- .../plugins/camerax/ImageCaptureTest.java | 27 +- .../flutter/plugins/camerax/PreviewTest.java | 14 + .../plugins/camerax/SystemServicesTest.java | 54 --- .../plugins/camerax/VideoCaptureTest.java | 15 + .../example/lib/main.dart | 6 +- .../lib/src/android_camera_camerax.dart | 122 +++++-- ...roid_camera_camerax_flutter_api_impls.dart | 12 + .../lib/src/camerax_library.g.dart | 224 +++++++++--- .../lib/src/camerax_proxy.dart | 40 ++- .../lib/src/device_orientation_manager.dart | 121 +++++++ .../lib/src/image_analysis.dart | 43 ++- .../lib/src/image_capture.dart | 46 ++- .../lib/src/preview.dart | 30 +- .../lib/src/system_services.dart | 65 +--- .../lib/src/video_capture.dart | 22 +- .../pigeons/camerax_library.dart | 29 +- .../camera_android_camerax/pubspec.yaml | 2 +- .../test/android_camera_camerax_test.dart | 324 ++++++++++++++++-- .../android_camera_camerax_test.mocks.dart | 65 ++-- .../test/device_orientation_manager_test.dart | 85 +++++ ...device_orientation_manager_test.mocks.dart | 84 +++++ .../test/image_analysis_test.dart | 48 +++ .../test/image_analysis_test.mocks.dart | 18 + .../test/image_capture_test.dart | 33 +- .../test/image_capture_test.mocks.dart | 18 + .../test/preview_test.dart | 32 +- .../test/preview_test.mocks.dart | 16 + .../test/system_services_test.dart | 42 +-- .../test/system_services_test.mocks.dart | 25 -- .../test/test_camerax_library.g.dart | 199 +++++++++-- .../test/video_capture_test.dart | 26 ++ .../test/video_capture_test.mocks.dart | 16 + 50 files changed, 2071 insertions(+), 810 deletions(-) create mode 100644 packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManagerFlutterApiImpl.java create mode 100644 packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManagerHostApiImpl.java create mode 100644 packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/DeviceOrientationManagerWrapperTest.java create mode 100644 packages/camera/camera_android_camerax/lib/src/device_orientation_manager.dart create mode 100644 packages/camera/camera_android_camerax/test/device_orientation_manager_test.dart create mode 100644 packages/camera/camera_android_camerax/test/device_orientation_manager_test.mocks.dart diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md index e7d7bfd4ea4b..af2488eb6695 100644 --- a/packages/camera/camera_android_camerax/CHANGELOG.md +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.5.0+25 + +* Implements `lockCaptureOrientation` and `unlockCaptureOrientation`. + ## 0.5.0+24 * Updates example app to use non-deprecated video_player method. diff --git a/packages/camera/camera_android_camerax/README.md b/packages/camera/camera_android_camerax/README.md index 073daa16d0eb..3a2e49d7e60d 100644 --- a/packages/camera/camera_android_camerax/README.md +++ b/packages/camera/camera_android_camerax/README.md @@ -31,10 +31,6 @@ dependencies: and thus, the plugin will fall back to 480p if configured with a `ResolutionPreset`. -### Locking/Unlocking capture orientation \[[Issue #125915][125915]\] - -`lockCaptureOrientation` & `unLockCaptureOrientation` are unimplemented. - ### Exposure mode, point, & offset configuration \[[Issue #120468][120468]\] `setExposureMode`, `setExposurePoint`, & `setExposureOffset` are unimplemented. diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java index eeb3c02dd153..c8046db5af6d 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java @@ -27,6 +27,7 @@ public final class CameraAndroidCameraxPlugin implements FlutterPlugin, Activity private ImageCaptureHostApiImpl imageCaptureHostApiImpl; private CameraControlHostApiImpl cameraControlHostApiImpl; public @Nullable SystemServicesHostApiImpl systemServicesHostApiImpl; + public @Nullable DeviceOrientationManagerHostApiImpl deviceOrientationManagerHostApiImpl; @VisibleForTesting public @Nullable ProcessCameraProviderHostApiImpl processCameraProviderHostApiImpl; @@ -71,6 +72,10 @@ public void setUp( systemServicesHostApiImpl = new SystemServicesHostApiImpl(binaryMessenger, instanceManager, context); GeneratedCameraXLibrary.SystemServicesHostApi.setup(binaryMessenger, systemServicesHostApiImpl); + deviceOrientationManagerHostApiImpl = + new DeviceOrientationManagerHostApiImpl(binaryMessenger, instanceManager); + GeneratedCameraXLibrary.DeviceOrientationManagerHostApi.setup( + binaryMessenger, deviceOrientationManagerHostApiImpl); GeneratedCameraXLibrary.PreviewHostApi.setup( binaryMessenger, new PreviewHostApiImpl(binaryMessenger, instanceManager, textureRegistry)); imageCaptureHostApiImpl = @@ -145,6 +150,7 @@ public void onAttachedToActivity(@NonNull ActivityPluginBinding activityPluginBi systemServicesHostApiImpl.setActivity(activity); systemServicesHostApiImpl.setPermissionsRegistry( activityPluginBinding::addRequestPermissionsResultListener); + deviceOrientationManagerHostApiImpl.setActivity(activity); } @Override diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManager.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManager.java index 67cac560db4d..b5281179d728 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManager.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManager.java @@ -14,7 +14,6 @@ import android.view.Surface; import android.view.WindowManager; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import io.flutter.embedding.engine.systemchannels.PlatformChannel; import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation; @@ -85,120 +84,6 @@ public void stop() { broadcastReceiver = null; } - /** - * Returns the device's photo orientation in degrees based on the sensor orientation and the last - * known UI orientation. - * - *

Returns one of 0, 90, 180 or 270. - * - * @return The device's photo orientation in degrees. - */ - public int getPhotoOrientation() { - return this.getPhotoOrientation(this.lastOrientation); - } - - /** - * Returns the device's photo orientation in degrees based on the sensor orientation and the - * supplied {@link PlatformChannel.DeviceOrientation} value. - * - *

Returns one of 0, 90, 180 or 270. - * - * @param orientation The {@link PlatformChannel.DeviceOrientation} value that is to be converted - * into degrees. - * @return The device's photo orientation in degrees. - */ - public int getPhotoOrientation(@Nullable PlatformChannel.DeviceOrientation orientation) { - int angle = 0; - // Fallback to device orientation when the orientation value is null. - if (orientation == null) { - orientation = getUIOrientation(); - } - - switch (orientation) { - case PORTRAIT_UP: - angle = 90; - break; - case PORTRAIT_DOWN: - angle = 270; - break; - case LANDSCAPE_LEFT: - angle = isFrontFacing ? 180 : 0; - break; - case LANDSCAPE_RIGHT: - angle = isFrontFacing ? 0 : 180; - break; - } - - // Sensor orientation is 90 for most devices, or 270 for some devices (eg. Nexus 5X). - // This has to be taken into account so the JPEG is rotated properly. - // For devices with orientation of 90, this simply returns the mapping from ORIENTATIONS. - // For devices with orientation of 270, the JPEG is rotated 180 degrees instead. - return (angle + sensorOrientation + 270) % 360; - } - - /** - * Returns the device's video orientation in clockwise degrees based on the sensor orientation and - * the last known UI orientation. - * - *

Returns one of 0, 90, 180 or 270. - * - * @return The device's video orientation in clockwise degrees. - */ - public int getVideoOrientation() { - return this.getVideoOrientation(this.lastOrientation); - } - - /** - * Returns the device's video orientation in clockwise degrees based on the sensor orientation and - * the supplied {@link PlatformChannel.DeviceOrientation} value. - * - *

Returns one of 0, 90, 180 or 270. - * - *

More details can be found in the official Android documentation: - * https://developer.android.com/reference/android/media/MediaRecorder#setOrientationHint(int) - * - *

See also: - * https://developer.android.com/training/camera2/camera-preview-large-screens#orientation_calculation - * - * @param orientation The {@link PlatformChannel.DeviceOrientation} value that is to be converted - * into degrees. - * @return The device's video orientation in clockwise degrees. - */ - public int getVideoOrientation(@Nullable PlatformChannel.DeviceOrientation orientation) { - int angle = 0; - - // Fallback to device orientation when the orientation value is null. - if (orientation == null) { - orientation = getUIOrientation(); - } - - switch (orientation) { - case PORTRAIT_UP: - angle = 0; - break; - case PORTRAIT_DOWN: - angle = 180; - break; - case LANDSCAPE_LEFT: - angle = 270; - break; - case LANDSCAPE_RIGHT: - angle = 90; - break; - } - - if (isFrontFacing) { - angle *= -1; - } - - return (angle + sensorOrientation + 360) % 360; - } - - /** @return the last received UI orientation. */ - public @Nullable PlatformChannel.DeviceOrientation getLastUIOrientation() { - return this.lastOrientation; - } - /** * Handles orientation changes based on change events triggered by the OrientationIntentFilter. * @@ -241,7 +126,7 @@ static void handleOrientationChange( @SuppressWarnings("deprecation") @VisibleForTesting PlatformChannel.DeviceOrientation getUIOrientation() { - final int rotation = getDisplay().getRotation(); + final int rotation = getDefaultRotation(); final int orientation = activity.getResources().getConfiguration().orientation; switch (orientation) { @@ -265,57 +150,18 @@ PlatformChannel.DeviceOrientation getUIOrientation() { } /** - * Calculates the sensor orientation based on the supplied angle. - * - *

This method is visible for testing purposes only and should never be used outside this - * class. - * - * @param angle Orientation angle. - * @return The sensor orientation based on the supplied angle. - */ - @VisibleForTesting - PlatformChannel.DeviceOrientation calculateSensorOrientation(int angle) { - final int tolerance = 45; - angle += tolerance; - - // Orientation is 0 in the default orientation mode. This is portrait-mode for phones - // and landscape for tablets. We have to compensate for this by calculating the default - // orientation, and apply an offset accordingly. - int defaultDeviceOrientation = getDeviceDefaultOrientation(); - if (defaultDeviceOrientation == Configuration.ORIENTATION_LANDSCAPE) { - angle += 90; - } - // Determine the orientation - angle = angle % 360; - return new PlatformChannel.DeviceOrientation[] { - PlatformChannel.DeviceOrientation.PORTRAIT_UP, - PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, - PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, - PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, - } - [angle / 90]; - } - - /** - * Gets the default orientation of the device. + * Gets default capture rotation for CameraX {@code UseCase}s. * - *

This method is visible for testing purposes only and should never be used outside this - * class. + *

See + * https://developer.android.com/reference/androidx/camera/core/ImageCapture#setTargetRotation(int), + * for instance. * - * @return The default orientation of the device. + * @return The rotation of the screen from its "natural" orientation; one of {@code + * Surface.ROTATION_0}, {@code Surface.ROTATION_90}, {@code Surface.ROTATION_180}, {@code + * Surface.ROTATION_270} */ - @VisibleForTesting - int getDeviceDefaultOrientation() { - Configuration config = activity.getResources().getConfiguration(); - int rotation = getDisplay().getRotation(); - if (((rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) - && config.orientation == Configuration.ORIENTATION_LANDSCAPE) - || ((rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) - && config.orientation == Configuration.ORIENTATION_PORTRAIT)) { - return Configuration.ORIENTATION_LANDSCAPE; - } else { - return Configuration.ORIENTATION_PORTRAIT; - } + int getDefaultRotation() { + return getDisplay().getRotation(); } /** diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManagerFlutterApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManagerFlutterApiImpl.java new file mode 100644 index 000000000000..e3e514cfd780 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManagerFlutterApiImpl.java @@ -0,0 +1,20 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import androidx.annotation.NonNull; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.DeviceOrientationManagerFlutterApi; + +public class DeviceOrientationManagerFlutterApiImpl extends DeviceOrientationManagerFlutterApi { + public DeviceOrientationManagerFlutterApiImpl(@NonNull BinaryMessenger binaryMessenger) { + super(binaryMessenger); + } + + public void sendDeviceOrientationChangedEvent( + @NonNull String orientation, @NonNull Reply reply) { + super.onDeviceOrientationChanged(orientation, reply); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManagerHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManagerHostApiImpl.java new file mode 100644 index 000000000000..e617d53c99cd --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/DeviceOrientationManagerHostApiImpl.java @@ -0,0 +1,104 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import android.app.Activity; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.CameraPermissionsManager.PermissionsRegistry; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.DeviceOrientationManagerHostApi; + +public class DeviceOrientationManagerHostApiImpl implements DeviceOrientationManagerHostApi { + private final BinaryMessenger binaryMessenger; + private final InstanceManager instanceManager; + + @VisibleForTesting public @NonNull CameraXProxy cameraXProxy = new CameraXProxy(); + @VisibleForTesting public @Nullable DeviceOrientationManager deviceOrientationManager; + + @VisibleForTesting + public @NonNull DeviceOrientationManagerFlutterApiImpl deviceOrientationManagerFlutterApiImpl; + + private Activity activity; + private PermissionsRegistry permissionsRegistry; + + public DeviceOrientationManagerHostApiImpl( + @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + this.deviceOrientationManagerFlutterApiImpl = + new DeviceOrientationManagerFlutterApiImpl(binaryMessenger); + } + + public void setActivity(@NonNull Activity activity) { + this.activity = activity; + } + + /** + * Starts listening for device orientation changes using an instance of a {@link + * DeviceOrientationManager}. + * + *

Whenever a change in device orientation is detected by the {@code DeviceOrientationManager}, + * the {@link SystemServicesFlutterApi} will be used to notify the Dart side. + */ + @Override + public void startListeningForDeviceOrientationChange( + @NonNull Boolean isFrontFacing, @NonNull Long sensorOrientation) { + deviceOrientationManager = + cameraXProxy.createDeviceOrientationManager( + activity, + isFrontFacing, + sensorOrientation.intValue(), + (DeviceOrientation newOrientation) -> { + deviceOrientationManagerFlutterApiImpl.sendDeviceOrientationChangedEvent( + serializeDeviceOrientation(newOrientation), reply -> {}); + }); + deviceOrientationManager.start(); + } + + /** Serializes {@code DeviceOrientation} into a String that the Dart side is able to recognize. */ + String serializeDeviceOrientation(DeviceOrientation orientation) { + return orientation.toString(); + } + + /** + * Tells the {@code deviceOrientationManager} to stop listening for orientation updates. + * + *

Has no effect if the {@code deviceOrientationManager} was never created to listen for device + * orientation updates. + */ + @Override + public void stopListeningForDeviceOrientationChange() { + if (deviceOrientationManager != null) { + deviceOrientationManager.stop(); + } + } + + /** + * Gets default capture rotation for CameraX {@code UseCase}s. + * + *

The default capture rotation for CameraX is the rotation of default {@code Display} at the + * time that a {@code UseCase} is bound, but the default {@code Display} does not change in this + * plugin, so this value is {@code Display}-agnostic. + * + *

See + * https://developer.android.com/reference/androidx/camera/core/ImageCapture#setTargetRotation(int) + * for instance for more information on how this default value is used. + */ + @Override + public @NonNull Long getDefaultDisplayRotation() { + int defaultRotation; + try { + defaultRotation = deviceOrientationManager.getDefaultRotation(); + } catch (NullPointerException e) { + throw new IllegalStateException( + "startListeningForDeviceOrientationChange must first be called to subscribe to device orientation changes in order to retrieve default rotation."); + } + + return Long.valueOf(defaultRotation); + } +} 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 603af1f42a02..e8da19753364 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 @@ -1256,11 +1256,6 @@ public interface SystemServicesHostApi { void requestCameraPermissions( @NonNull Boolean enableAudio, @NonNull Result result); - void startListeningForDeviceOrientationChange( - @NonNull Boolean isFrontFacing, @NonNull Long sensorOrientation); - - void stopListeningForDeviceOrientationChange(); - @NonNull String getTempFilePath(@NonNull String prefix, @NonNull String suffix); @@ -1309,7 +1304,85 @@ public void error(Throwable error) { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, - "dev.flutter.pigeon.SystemServicesHostApi.startListeningForDeviceOrientationChange", + "dev.flutter.pigeon.SystemServicesHostApi.getTempFilePath", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + String prefixArg = (String) args.get(0); + String suffixArg = (String) args.get(1); + try { + String output = api.getTempFilePath(prefixArg, suffixArg); + wrapped.add(0, output); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ + public static class SystemServicesFlutterApi { + private final @NonNull BinaryMessenger binaryMessenger; + + public SystemServicesFlutterApi(@NonNull BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + /** Public interface for sending reply. */ + @SuppressWarnings("UnknownNullness") + public interface Reply { + void reply(T reply); + } + /** The codec used by SystemServicesFlutterApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + + public void onCameraError(@NonNull String errorDescriptionArg, @NonNull Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.SystemServicesFlutterApi.onCameraError", + getCodec()); + channel.send( + new ArrayList(Collections.singletonList(errorDescriptionArg)), + channelReply -> callback.reply(null)); + } + } + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface DeviceOrientationManagerHostApi { + + void startListeningForDeviceOrientationChange( + @NonNull Boolean isFrontFacing, @NonNull Long sensorOrientation); + + void stopListeningForDeviceOrientationChange(); + + @NonNull + Long getDefaultDisplayRotation(); + + /** The codec used by DeviceOrientationManagerHostApi. */ + static @NonNull MessageCodec getCodec() { + return new StandardMessageCodec(); + } + /** + * Sets up an instance of `DeviceOrientationManagerHostApi` to handle messages through the + * `binaryMessenger`. + */ + static void setup( + @NonNull BinaryMessenger binaryMessenger, @Nullable DeviceOrientationManagerHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.DeviceOrientationManagerHostApi.startListeningForDeviceOrientationChange", getCodec()); if (api != null) { channel.setMessageHandler( @@ -1337,7 +1410,7 @@ public void error(Throwable error) { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, - "dev.flutter.pigeon.SystemServicesHostApi.stopListeningForDeviceOrientationChange", + "dev.flutter.pigeon.DeviceOrientationManagerHostApi.stopListeningForDeviceOrientationChange", getCodec()); if (api != null) { channel.setMessageHandler( @@ -1360,17 +1433,14 @@ public void error(Throwable error) { BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, - "dev.flutter.pigeon.SystemServicesHostApi.getTempFilePath", + "dev.flutter.pigeon.DeviceOrientationManagerHostApi.getDefaultDisplayRotation", getCodec()); if (api != null) { channel.setMessageHandler( (message, reply) -> { ArrayList wrapped = new ArrayList(); - ArrayList args = (ArrayList) message; - String prefixArg = (String) args.get(0); - String suffixArg = (String) args.get(1); try { - String output = api.getTempFilePath(prefixArg, suffixArg); + Long output = api.getDefaultDisplayRotation(); wrapped.add(0, output); } catch (Throwable exception) { ArrayList wrappedError = wrapError(exception); @@ -1385,10 +1455,10 @@ public void error(Throwable error) { } } /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ - public static class SystemServicesFlutterApi { + public static class DeviceOrientationManagerFlutterApi { private final @NonNull BinaryMessenger binaryMessenger; - public SystemServicesFlutterApi(@NonNull BinaryMessenger argBinaryMessenger) { + public DeviceOrientationManagerFlutterApi(@NonNull BinaryMessenger argBinaryMessenger) { this.binaryMessenger = argBinaryMessenger; } @@ -1397,7 +1467,7 @@ public SystemServicesFlutterApi(@NonNull BinaryMessenger argBinaryMessenger) { public interface Reply { void reply(T reply); } - /** The codec used by SystemServicesFlutterApi. */ + /** The codec used by DeviceOrientationManagerFlutterApi. */ static @NonNull MessageCodec getCodec() { return new StandardMessageCodec(); } @@ -1407,23 +1477,12 @@ public void onDeviceOrientationChanged( BasicMessageChannel channel = new BasicMessageChannel<>( binaryMessenger, - "dev.flutter.pigeon.SystemServicesFlutterApi.onDeviceOrientationChanged", + "dev.flutter.pigeon.DeviceOrientationManagerFlutterApi.onDeviceOrientationChanged", getCodec()); channel.send( new ArrayList(Collections.singletonList(orientationArg)), channelReply -> callback.reply(null)); } - - public void onCameraError(@NonNull String errorDescriptionArg, @NonNull Reply callback) { - BasicMessageChannel channel = - new BasicMessageChannel<>( - binaryMessenger, - "dev.flutter.pigeon.SystemServicesFlutterApi.onCameraError", - getCodec()); - channel.send( - new ArrayList(Collections.singletonList(errorDescriptionArg)), - channelReply -> callback.reply(null)); - } } private static class PreviewHostApiCodec extends StandardMessageCodec { @@ -1466,6 +1525,8 @@ void create( @NonNull ResolutionInfo getResolutionInfo(@NonNull Long identifier); + void setTargetRotation(@NonNull Long identifier, @NonNull Long rotation); + /** The codec used by PreviewHostApi. */ static @NonNull MessageCodec getCodec() { return PreviewHostApiCodec.INSTANCE; @@ -1577,6 +1638,32 @@ static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable PreviewHos channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, "dev.flutter.pigeon.PreviewHostApi.setTargetRotation", getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + Number rotationArg = (Number) args.get(1); + try { + api.setTargetRotation( + (identifierArg == null) ? null : identifierArg.longValue(), + (rotationArg == null) ? null : rotationArg.longValue()); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } } } /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ @@ -1588,6 +1675,8 @@ public interface VideoCaptureHostApi { @NonNull Long getOutput(@NonNull Long identifier); + void setTargetRotation(@NonNull Long identifier, @NonNull Long rotation); + /** The codec used by VideoCaptureHostApi. */ static @NonNull MessageCodec getCodec() { return new StandardMessageCodec(); @@ -1646,6 +1735,34 @@ static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable VideoCaptu channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.VideoCaptureHostApi.setTargetRotation", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + Number rotationArg = (Number) args.get(1); + try { + api.setTargetRotation( + (identifierArg == null) ? null : identifierArg.longValue(), + (rotationArg == null) ? null : rotationArg.longValue()); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } } } /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ @@ -2055,12 +2172,17 @@ public void create(@NonNull Long identifierArg, @NonNull Reply callback) { public interface ImageCaptureHostApi { void create( - @NonNull Long identifier, @Nullable Long flashMode, @Nullable Long resolutionSelectorId); + @NonNull Long identifier, + @Nullable Long targetRotation, + @Nullable Long flashMode, + @Nullable Long resolutionSelectorId); void setFlashMode(@NonNull Long identifier, @NonNull Long flashMode); void takePicture(@NonNull Long identifier, @NonNull Result result); + void setTargetRotation(@NonNull Long identifier, @NonNull Long rotation); + /** The codec used by ImageCaptureHostApi. */ static @NonNull MessageCodec getCodec() { return new StandardMessageCodec(); @@ -2080,11 +2202,13 @@ static void setup(@NonNull BinaryMessenger binaryMessenger, @Nullable ImageCaptu ArrayList wrapped = new ArrayList(); ArrayList args = (ArrayList) message; Number identifierArg = (Number) args.get(0); - Number flashModeArg = (Number) args.get(1); - Number resolutionSelectorIdArg = (Number) args.get(2); + Number targetRotationArg = (Number) args.get(1); + Number flashModeArg = (Number) args.get(2); + Number resolutionSelectorIdArg = (Number) args.get(3); try { api.create( (identifierArg == null) ? null : identifierArg.longValue(), + (targetRotationArg == null) ? null : targetRotationArg.longValue(), (flashModeArg == null) ? null : flashModeArg.longValue(), (resolutionSelectorIdArg == null) ? null @@ -2156,6 +2280,34 @@ public void error(Throwable error) { channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.ImageCaptureHostApi.setTargetRotation", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + Number rotationArg = (Number) args.get(1); + try { + api.setTargetRotation( + (identifierArg == null) ? null : identifierArg.longValue(), + (rotationArg == null) ? null : rotationArg.longValue()); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } } } @@ -2486,12 +2638,17 @@ public void create( /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ public interface ImageAnalysisHostApi { - void create(@NonNull Long identifier, @Nullable Long resolutionSelectorId); + void create( + @NonNull Long identifier, + @Nullable Long targetRotation, + @Nullable Long resolutionSelectorId); void setAnalyzer(@NonNull Long identifier, @NonNull Long analyzerIdentifier); void clearAnalyzer(@NonNull Long identifier); + void setTargetRotation(@NonNull Long identifier, @NonNull Long rotation); + /** The codec used by ImageAnalysisHostApi. */ static @NonNull MessageCodec getCodec() { return new StandardMessageCodec(); @@ -2512,10 +2669,12 @@ static void setup( ArrayList wrapped = new ArrayList(); ArrayList args = (ArrayList) message; Number identifierArg = (Number) args.get(0); - Number resolutionSelectorIdArg = (Number) args.get(1); + Number targetRotationArg = (Number) args.get(1); + Number resolutionSelectorIdArg = (Number) args.get(2); try { api.create( (identifierArg == null) ? null : identifierArg.longValue(), + (targetRotationArg == null) ? null : targetRotationArg.longValue(), (resolutionSelectorIdArg == null) ? null : resolutionSelectorIdArg.longValue()); @@ -2581,6 +2740,34 @@ static void setup( channel.setMessageHandler(null); } } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.ImageAnalysisHostApi.setTargetRotation", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + ArrayList wrapped = new ArrayList(); + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + Number rotationArg = (Number) args.get(1); + try { + api.setTargetRotation( + (identifierArg == null) ? null : identifierArg.longValue(), + (rotationArg == null) ? null : rotationArg.longValue()); + wrapped.add(0, null); + } catch (Throwable exception) { + ArrayList wrappedError = wrapError(exception); + wrapped = wrappedError; + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } } } /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageAnalysisHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageAnalysisHostApiImpl.java index 00bceb76a7e5..f44db11cba1f 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageAnalysisHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageAnalysisHostApiImpl.java @@ -41,9 +41,13 @@ public void setContext(@NonNull Context context) { /** Creates an {@link ImageAnalysis} instance with the target resolution if specified. */ @Override - public void create(@NonNull Long identifier, @Nullable Long resolutionSelectorId) { + public void create( + @NonNull Long identifier, @Nullable Long rotation, @Nullable Long resolutionSelectorId) { ImageAnalysis.Builder imageAnalysisBuilder = cameraXProxy.createImageAnalysisBuilder(); + if (rotation != null) { + imageAnalysisBuilder.setTargetRotation(rotation.intValue()); + } if (resolutionSelectorId != null) { ResolutionSelector resolutionSelector = Objects.requireNonNull(instanceManager.getInstance(resolutionSelectorId)); @@ -75,6 +79,13 @@ public void clearAnalyzer(@NonNull Long identifier) { imageAnalysis.clearAnalyzer(); } + /** Dynamically sets the target rotation of the {@link ImageAnalysis}. */ + @Override + public void setTargetRotation(@NonNull Long identifier, @NonNull Long rotation) { + ImageAnalysis imageAnalysis = getImageAnalysisInstance(identifier); + imageAnalysis.setTargetRotation(rotation.intValue()); + } + /** * Retrieives the {@link ImageAnalysis} instance associated with the specified {@code identifier}. */ diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageCaptureHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageCaptureHostApiImpl.java index 88ec2debb7a8..e17d386632f5 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageCaptureHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ImageCaptureHostApiImpl.java @@ -53,9 +53,15 @@ public void setContext(@NonNull Context context) { */ @Override public void create( - @NonNull Long identifier, @Nullable Long flashMode, @Nullable Long resolutionSelectorId) { + @NonNull Long identifier, + @Nullable Long rotation, + @Nullable Long flashMode, + @Nullable Long resolutionSelectorId) { ImageCapture.Builder imageCaptureBuilder = cameraXProxy.createImageCaptureBuilder(); + if (rotation != null) { + imageCaptureBuilder.setTargetRotation(rotation.intValue()); + } if (flashMode != null) { // This sets the requested flash mode, but may fail silently. imageCaptureBuilder.setFlashMode(flashMode.intValue()); @@ -73,8 +79,7 @@ public void create( /** Sets the flash mode of the {@link ImageCapture} instance with the specified identifier. */ @Override public void setFlashMode(@NonNull Long identifier, @NonNull Long flashMode) { - ImageCapture imageCapture = - (ImageCapture) Objects.requireNonNull(instanceManager.getInstance(identifier)); + ImageCapture imageCapture = getImageCaptureInstance(identifier); imageCapture.setFlashMode(flashMode.intValue()); } @@ -82,8 +87,7 @@ public void setFlashMode(@NonNull Long identifier, @NonNull Long flashMode) { @Override public void takePicture( @NonNull Long identifier, @NonNull GeneratedCameraXLibrary.Result result) { - ImageCapture imageCapture = - (ImageCapture) Objects.requireNonNull(instanceManager.getInstance(identifier)); + ImageCapture imageCapture = getImageCaptureInstance(identifier); final File outputDir = context.getCacheDir(); File temporaryCaptureFile; try { @@ -118,4 +122,18 @@ public void onError(@NonNull ImageCaptureException exception) { } }; } + + /** Dynamically sets the target rotation of the {@link ImageCapture}. */ + @Override + public void setTargetRotation(@NonNull Long identifier, @NonNull Long rotation) { + ImageCapture imageCapture = getImageCaptureInstance(identifier); + imageCapture.setTargetRotation(rotation.intValue()); + } + + /** + * Retrieves the {@link ImageCapture} instance associated with the specified {@code identifier}. + */ + private ImageCapture getImageCaptureInstance(@NonNull Long identifier) { + return Objects.requireNonNull(instanceManager.getInstance(identifier)); + } } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PendingRecordingHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PendingRecordingHostApiImpl.java index 70119fac5d41..d4acebefbffb 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PendingRecordingHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PendingRecordingHostApiImpl.java @@ -75,7 +75,14 @@ public void handleVideoRecordEvent(@NonNull VideoRecordEvent event) { if (event instanceof VideoRecordEvent.Finalize) { VideoRecordEvent.Finalize castedEvent = (VideoRecordEvent.Finalize) event; if (castedEvent.hasError()) { - systemServicesFlutterApi.sendCameraError(castedEvent.getCause().toString(), reply -> {}); + String cameraErrorMessage; + if (castedEvent.getCause() != null) { + cameraErrorMessage = castedEvent.getCause().toString(); + } else { + cameraErrorMessage = + "Error code " + castedEvent.getError() + ": An error occurred while recording video."; + } + systemServicesFlutterApi.sendCameraError(cameraErrorMessage, reply -> {}); } } } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PreviewHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PreviewHostApiImpl.java index 07b581ebf96a..7b1ba1214cdb 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PreviewHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/PreviewHostApiImpl.java @@ -61,7 +61,7 @@ public void create( */ @Override public @NonNull Long setSurfaceProvider(@NonNull Long identifier) { - Preview preview = (Preview) Objects.requireNonNull(instanceManager.getInstance(identifier)); + Preview preview = getPreviewInstance(identifier); flutterSurfaceTexture = textureRegistry.createSurfaceTexture(); SurfaceTexture surfaceTexture = flutterSurfaceTexture.surfaceTexture(); Preview.SurfaceProvider surfaceProvider = createSurfaceProvider(surfaceTexture); @@ -142,7 +142,7 @@ public void releaseFlutterSurfaceTexture() { @Override public @NonNull GeneratedCameraXLibrary.ResolutionInfo getResolutionInfo( @NonNull Long identifier) { - Preview preview = (Preview) Objects.requireNonNull(instanceManager.getInstance(identifier)); + Preview preview = getPreviewInstance(identifier); Size resolution = preview.getResolutionInfo().getResolution(); GeneratedCameraXLibrary.ResolutionInfo.Builder resolutionInfo = @@ -151,4 +151,16 @@ public void releaseFlutterSurfaceTexture() { .setHeight(Long.valueOf(resolution.getHeight())); return resolutionInfo.build(); } + + /** Dynamically sets the target rotation of the {@link Preview}. */ + @Override + public void setTargetRotation(@NonNull Long identifier, @NonNull Long rotation) { + Preview preview = getPreviewInstance(identifier); + preview.setTargetRotation(rotation.intValue()); + } + + /** Retrieves the {@link Preview} instance associated with the specified {@code identifier}. */ + private Preview getPreviewInstance(@NonNull Long identifier) { + return Objects.requireNonNull(instanceManager.getInstance(identifier)); + } } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesFlutterApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesFlutterApiImpl.java index 63158974f43a..2cb4ea1b2275 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesFlutterApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesFlutterApiImpl.java @@ -13,11 +13,6 @@ public SystemServicesFlutterApiImpl(@NonNull BinaryMessenger binaryMessenger) { super(binaryMessenger); } - public void sendDeviceOrientationChangedEvent( - @NonNull String orientation, @NonNull Reply reply) { - super.onDeviceOrientationChanged(orientation, reply); - } - public void sendCameraError(@NonNull String errorDescription, @NonNull Reply reply) { super.onCameraError(errorDescription, reply); } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java index b8f4d6b0c62d..4e0b18069ffb 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/SystemServicesHostApiImpl.java @@ -7,14 +7,11 @@ import android.app.Activity; import android.content.Context; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugins.camerax.CameraPermissionsManager.PermissionsRegistry; import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraPermissionsErrorData; import io.flutter.plugins.camerax.GeneratedCameraXLibrary.Result; -import io.flutter.plugins.camerax.GeneratedCameraXLibrary.SystemServicesFlutterApi; import io.flutter.plugins.camerax.GeneratedCameraXLibrary.SystemServicesHostApi; import java.io.File; import java.io.IOException; @@ -25,7 +22,6 @@ public class SystemServicesHostApiImpl implements SystemServicesHostApi { private Context context; @VisibleForTesting public @NonNull CameraXProxy cameraXProxy = new CameraXProxy(); - @VisibleForTesting public @Nullable DeviceOrientationManager deviceOrientationManager; @VisibleForTesting public @NonNull SystemServicesFlutterApiImpl systemServicesFlutterApi; private Activity activity; @@ -84,46 +80,6 @@ public void requestCameraPermissions( }); } - /** - * Starts listening for device orientation changes using an instance of a {@link - * DeviceOrientationManager}. - * - *

Whenever a change in device orientation is detected by the {@code DeviceOrientationManager}, - * the {@link SystemServicesFlutterApi} will be used to notify the Dart side. - */ - @Override - public void startListeningForDeviceOrientationChange( - @NonNull Boolean isFrontFacing, @NonNull Long sensorOrientation) { - deviceOrientationManager = - cameraXProxy.createDeviceOrientationManager( - activity, - isFrontFacing, - sensorOrientation.intValue(), - (DeviceOrientation newOrientation) -> { - systemServicesFlutterApi.sendDeviceOrientationChangedEvent( - serializeDeviceOrientation(newOrientation), reply -> {}); - }); - deviceOrientationManager.start(); - } - - /** Serializes {@code DeviceOrientation} into a String that the Dart side is able to recognize. */ - String serializeDeviceOrientation(DeviceOrientation orientation) { - return orientation.toString(); - } - - /** - * Tells the {@code deviceOrientationManager} to stop listening for orientation updates. - * - *

Has no effect if the {@code deviceOrientationManager} was never created to listen for device - * orientation updates. - */ - @Override - public void stopListeningForDeviceOrientationChange() { - if (deviceOrientationManager != null) { - deviceOrientationManager.stop(); - } - } - /** Returns a path to be used to create a temp file in the current cache directory. */ @Override @NonNull diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/VideoCaptureHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/VideoCaptureHostApiImpl.java index 7e764cdff4a9..1c849ee09379 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/VideoCaptureHostApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/VideoCaptureHostApiImpl.java @@ -37,8 +37,7 @@ public Long withOutput(@NonNull Long videoOutputId) { @Override @NonNull public Long getOutput(@NonNull Long identifier) { - VideoCapture videoCapture = - Objects.requireNonNull(instanceManager.getInstance(identifier)); + VideoCapture videoCapture = getVideoCaptureInstance(identifier); Recorder recorder = videoCapture.getOutput(); return Objects.requireNonNull(instanceManager.getIdentifierForStrongReference(recorder)); } @@ -49,4 +48,18 @@ public VideoCaptureFlutterApiImpl getVideoCaptureFlutterApiImpl( @NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) { return new VideoCaptureFlutterApiImpl(binaryMessenger, instanceManager); } + + /** Dynamically sets the target rotation of the {@link VideoCapture}. */ + @Override + public void setTargetRotation(@NonNull Long identifier, @NonNull Long rotation) { + VideoCapture videoCapture = getVideoCaptureInstance(identifier); + videoCapture.setTargetRotation(rotation.intValue()); + } + + /** + * Retrieves the {@link VideoCapture} instance associated with the specified {@code identifier}. + */ + private VideoCapture getVideoCaptureInstance(@NonNull Long identifier) { + return Objects.requireNonNull(instanceManager.getInstance(identifier)); + } } diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraAndroidCameraxPluginTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraAndroidCameraxPluginTest.java index 4a2197624b65..58f517edc653 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraAndroidCameraxPluginTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraAndroidCameraxPluginTest.java @@ -44,6 +44,7 @@ public void onAttachedToActivity_setsLifecycleOwnerAsActivityIfLifecycleOwner() plugin.processCameraProviderHostApiImpl = mockProcessCameraProviderHostApiImpl; plugin.liveDataHostApiImpl = mockLiveDataHostApiImpl; plugin.systemServicesHostApiImpl = mock(SystemServicesHostApiImpl.class); + plugin.deviceOrientationManagerHostApiImpl = mock(DeviceOrientationManagerHostApiImpl.class); plugin.onAttachedToEngine(flutterPluginBinding); plugin.onAttachedToActivity(activityPluginBinding); @@ -68,6 +69,7 @@ public void onAttachedToActivity_setsLifecycleOwnerAsActivityIfLifecycleOwner() plugin.processCameraProviderHostApiImpl = mockProcessCameraProviderHostApiImpl; plugin.liveDataHostApiImpl = mockLiveDataHostApiImpl; plugin.systemServicesHostApiImpl = mock(SystemServicesHostApiImpl.class); + plugin.deviceOrientationManagerHostApiImpl = mock(DeviceOrientationManagerHostApiImpl.class); plugin.onAttachedToEngine(flutterPluginBinding); plugin.onAttachedToActivity(activityPluginBinding); diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/DeviceOrientationManagerTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/DeviceOrientationManagerTest.java index 1e2bfba714c7..1bb4077fb6dc 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/DeviceOrientationManagerTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/DeviceOrientationManagerTest.java @@ -50,108 +50,6 @@ public void before() { new DeviceOrientationManager(mockActivity, false, 0, mockDeviceOrientationChangeCallback); } - @Test - public void getVideoOrientation_whenNaturalScreenOrientationEqualsPortraitUp() { - int degreesPortraitUp = - deviceOrientationManager.getVideoOrientation(DeviceOrientation.PORTRAIT_UP); - int degreesPortraitDown = - deviceOrientationManager.getVideoOrientation(DeviceOrientation.PORTRAIT_DOWN); - int degreesLandscapeLeft = - deviceOrientationManager.getVideoOrientation(DeviceOrientation.LANDSCAPE_LEFT); - int degreesLandscapeRight = - deviceOrientationManager.getVideoOrientation(DeviceOrientation.LANDSCAPE_RIGHT); - - assertEquals(0, degreesPortraitUp); - assertEquals(270, degreesLandscapeLeft); - assertEquals(180, degreesPortraitDown); - assertEquals(90, degreesLandscapeRight); - } - - @Test - public void getVideoOrientation_whenNaturalScreenOrientationEqualsLandscapeLeft() { - DeviceOrientationManager orientationManager = - new DeviceOrientationManager(mockActivity, false, 90, mockDeviceOrientationChangeCallback); - - int degreesPortraitUp = orientationManager.getVideoOrientation(DeviceOrientation.PORTRAIT_UP); - int degreesPortraitDown = - orientationManager.getVideoOrientation(DeviceOrientation.PORTRAIT_DOWN); - int degreesLandscapeLeft = - orientationManager.getVideoOrientation(DeviceOrientation.LANDSCAPE_LEFT); - int degreesLandscapeRight = - orientationManager.getVideoOrientation(DeviceOrientation.LANDSCAPE_RIGHT); - - assertEquals(90, degreesPortraitUp); - assertEquals(0, degreesLandscapeLeft); - assertEquals(270, degreesPortraitDown); - assertEquals(180, degreesLandscapeRight); - } - - @Test - public void getVideoOrientation_fallbackToPortraitSensorOrientationWhenOrientationIsNull() { - setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); - - int degrees = deviceOrientationManager.getVideoOrientation(null); - - assertEquals(0, degrees); - } - - @Test - public void getVideoOrientation_fallbackToLandscapeSensorOrientationWhenOrientationIsNull() { - setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); - - DeviceOrientationManager orientationManager = - new DeviceOrientationManager(mockActivity, false, 90, mockDeviceOrientationChangeCallback); - - int degrees = orientationManager.getVideoOrientation(null); - - assertEquals(0, degrees); - } - - @Test - public void getPhotoOrientation_whenNaturalScreenOrientationEqualsPortraitUp() { - int degreesPortraitUp = - deviceOrientationManager.getPhotoOrientation(DeviceOrientation.PORTRAIT_UP); - int degreesPortraitDown = - deviceOrientationManager.getPhotoOrientation(DeviceOrientation.PORTRAIT_DOWN); - int degreesLandscapeLeft = - deviceOrientationManager.getPhotoOrientation(DeviceOrientation.LANDSCAPE_LEFT); - int degreesLandscapeRight = - deviceOrientationManager.getPhotoOrientation(DeviceOrientation.LANDSCAPE_RIGHT); - - assertEquals(0, degreesPortraitUp); - assertEquals(90, degreesLandscapeRight); - assertEquals(180, degreesPortraitDown); - assertEquals(270, degreesLandscapeLeft); - } - - @Test - public void getPhotoOrientation_whenNaturalScreenOrientationEqualsLandscapeLeft() { - DeviceOrientationManager orientationManager = - new DeviceOrientationManager(mockActivity, false, 90, mockDeviceOrientationChangeCallback); - - int degreesPortraitUp = orientationManager.getPhotoOrientation(DeviceOrientation.PORTRAIT_UP); - int degreesPortraitDown = - orientationManager.getPhotoOrientation(DeviceOrientation.PORTRAIT_DOWN); - int degreesLandscapeLeft = - orientationManager.getPhotoOrientation(DeviceOrientation.LANDSCAPE_LEFT); - int degreesLandscapeRight = - orientationManager.getPhotoOrientation(DeviceOrientation.LANDSCAPE_RIGHT); - - assertEquals(90, degreesPortraitUp); - assertEquals(180, degreesLandscapeRight); - assertEquals(270, degreesPortraitDown); - assertEquals(0, degreesLandscapeLeft); - } - - @Test - public void getPhotoOrientation_shouldFallbackToCurrentOrientationWhenOrientationIsNull() { - setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); - - int degrees = deviceOrientationManager.getPhotoOrientation(null); - - assertEquals(270, degrees); - } - @Test public void handleUIOrientationChange_shouldSendMessageWhenSensorAccessIsAllowed() { try (MockedStatic mockedSystem = mockStatic(Settings.System.class)) { @@ -239,60 +137,6 @@ public void getUIOrientation() { assertEquals(DeviceOrientation.PORTRAIT_UP, uiOrientation); } - @Test - public void getDeviceDefaultOrientation() { - setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); - int orientation = deviceOrientationManager.getDeviceDefaultOrientation(); - assertEquals(Configuration.ORIENTATION_PORTRAIT, orientation); - - setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_180); - orientation = deviceOrientationManager.getDeviceDefaultOrientation(); - assertEquals(Configuration.ORIENTATION_PORTRAIT, orientation); - - setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_90); - orientation = deviceOrientationManager.getDeviceDefaultOrientation(); - assertEquals(Configuration.ORIENTATION_LANDSCAPE, orientation); - - setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_270); - orientation = deviceOrientationManager.getDeviceDefaultOrientation(); - assertEquals(Configuration.ORIENTATION_LANDSCAPE, orientation); - - setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); - orientation = deviceOrientationManager.getDeviceDefaultOrientation(); - assertEquals(Configuration.ORIENTATION_LANDSCAPE, orientation); - - setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_180); - orientation = deviceOrientationManager.getDeviceDefaultOrientation(); - assertEquals(Configuration.ORIENTATION_LANDSCAPE, orientation); - - setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_90); - orientation = deviceOrientationManager.getDeviceDefaultOrientation(); - assertEquals(Configuration.ORIENTATION_PORTRAIT, orientation); - - setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_270); - orientation = deviceOrientationManager.getDeviceDefaultOrientation(); - assertEquals(Configuration.ORIENTATION_PORTRAIT, orientation); - } - - @Test - public void calculateSensorOrientation() { - setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); - DeviceOrientation orientation = deviceOrientationManager.calculateSensorOrientation(0); - assertEquals(DeviceOrientation.PORTRAIT_UP, orientation); - - setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); - orientation = deviceOrientationManager.calculateSensorOrientation(90); - assertEquals(DeviceOrientation.LANDSCAPE_LEFT, orientation); - - setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); - orientation = deviceOrientationManager.calculateSensorOrientation(180); - assertEquals(DeviceOrientation.PORTRAIT_DOWN, orientation); - - setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); - orientation = deviceOrientationManager.calculateSensorOrientation(270); - assertEquals(DeviceOrientation.LANDSCAPE_RIGHT, orientation); - } - private void setUpUIOrientationMocks(int orientation, int rotation) { Resources mockResources = mock(Resources.class); Configuration mockConfiguration = mock(Configuration.class); @@ -304,6 +148,16 @@ private void setUpUIOrientationMocks(int orientation, int rotation) { when(mockResources.getConfiguration()).thenReturn(mockConfiguration); } + @Test + public void getDefaultRotation_returnsExpectedValue() { + final int expectedRotation = 90; + when(mockDisplay.getRotation()).thenReturn(expectedRotation); + + final int defaultRotation = deviceOrientationManager.getDefaultRotation(); + + assertEquals(defaultRotation, expectedRotation); + } + @Test public void getDisplayTest() { Display display = deviceOrientationManager.getDisplay(); diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/DeviceOrientationManagerWrapperTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/DeviceOrientationManagerWrapperTest.java new file mode 100644 index 000000000000..26cfd77f1265 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/DeviceOrientationManagerWrapperTest.java @@ -0,0 +1,96 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camerax; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.DeviceOrientationManager.DeviceOrientationChangeCallback; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.DeviceOrientationManagerFlutterApi.Reply; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +public class DeviceOrientationManagerWrapperTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock DeviceOrientationManager mockDeviceOrientationManager; + @Mock public BinaryMessenger mockBinaryMessenger; + @Mock public InstanceManager mockInstanceManager; + + @Test + public void deviceOrientationManagerWrapper_handlesDeviceOrientationChangesAsExpected() { + final DeviceOrientationManagerHostApiImpl hostApi = + new DeviceOrientationManagerHostApiImpl(mockBinaryMessenger, mockInstanceManager); + final CameraXProxy mockCameraXProxy = mock(CameraXProxy.class); + final Activity mockActivity = mock(Activity.class); + final Boolean isFrontFacing = true; + final int sensorOrientation = 90; + + DeviceOrientationManagerFlutterApiImpl flutterApi = + mock(DeviceOrientationManagerFlutterApiImpl.class); + hostApi.deviceOrientationManagerFlutterApiImpl = flutterApi; + + hostApi.cameraXProxy = mockCameraXProxy; + hostApi.setActivity(mockActivity); + when(mockCameraXProxy.createDeviceOrientationManager( + eq(mockActivity), + eq(isFrontFacing), + eq(sensorOrientation), + any(DeviceOrientationChangeCallback.class))) + .thenReturn(mockDeviceOrientationManager); + + final ArgumentCaptor deviceOrientationChangeCallbackCaptor = + ArgumentCaptor.forClass(DeviceOrientationChangeCallback.class); + + hostApi.startListeningForDeviceOrientationChange( + isFrontFacing, Long.valueOf(sensorOrientation)); + + // Test callback method defined in Flutter API is called when device orientation changes. + verify(mockCameraXProxy) + .createDeviceOrientationManager( + eq(mockActivity), + eq(isFrontFacing), + eq(sensorOrientation), + deviceOrientationChangeCallbackCaptor.capture()); + DeviceOrientationChangeCallback deviceOrientationChangeCallback = + deviceOrientationChangeCallbackCaptor.getValue(); + + deviceOrientationChangeCallback.onChange(DeviceOrientation.PORTRAIT_DOWN); + verify(flutterApi) + .sendDeviceOrientationChangedEvent( + eq(DeviceOrientation.PORTRAIT_DOWN.toString()), ArgumentMatchers.>any()); + + // Test that the DeviceOrientationManager starts listening for device orientation changes. + verify(mockDeviceOrientationManager).start(); + + // Test that the DeviceOrientationManager can stop listening for device orientation changes. + hostApi.stopListeningForDeviceOrientationChange(); + verify(mockDeviceOrientationManager).stop(); + } + + @Test + public void getDefaultDisplayRotation_returnsExpectedRotation() { + final DeviceOrientationManagerHostApiImpl hostApi = + new DeviceOrientationManagerHostApiImpl(mockBinaryMessenger, mockInstanceManager); + final int defaultRotation = 180; + + hostApi.deviceOrientationManager = mockDeviceOrientationManager; + when(mockDeviceOrientationManager.getDefaultRotation()).thenReturn(defaultRotation); + + assertEquals(hostApi.getDefaultDisplayRotation(), Long.valueOf(defaultRotation)); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ImageAnalysisTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ImageAnalysisTest.java index 08a9f80b6373..5e4a729dbd23 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ImageAnalysisTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ImageAnalysisTest.java @@ -12,6 +12,7 @@ import static org.mockito.Mockito.when; import android.content.Context; +import android.view.Surface; import androidx.camera.core.ImageAnalysis; import androidx.camera.core.resolutionselector.ResolutionSelector; import androidx.test.core.app.ApplicationProvider; @@ -56,6 +57,7 @@ public void hostApiCreate_createsExpectedImageAnalysisInstanceWithExpectedIdenti final ResolutionSelector mockResolutionSelector = mock(ResolutionSelector.class); final long instanceIdentifier = 0; final long mockResolutionSelectorId = 25; + final int targetRotation = Surface.ROTATION_90; hostApi.cameraXProxy = mockCameraXProxy; instanceManager.addDartCreatedInstance(mockResolutionSelector, mockResolutionSelectorId); @@ -63,8 +65,9 @@ public void hostApiCreate_createsExpectedImageAnalysisInstanceWithExpectedIdenti when(mockCameraXProxy.createImageAnalysisBuilder()).thenReturn(mockImageAnalysisBuilder); when(mockImageAnalysisBuilder.build()).thenReturn(mockImageAnalysis); - hostApi.create(instanceIdentifier, mockResolutionSelectorId); + hostApi.create(instanceIdentifier, Long.valueOf(targetRotation), mockResolutionSelectorId); + verify(mockImageAnalysisBuilder).setTargetRotation(targetRotation); verify(mockImageAnalysisBuilder).setResolutionSelector(mockResolutionSelector); assertEquals(instanceManager.getInstance(instanceIdentifier), mockImageAnalysis); } @@ -98,4 +101,18 @@ public void clearAnalyzer_makesCallToClearAnalyzerOnExpectedImageAnalysisInstanc verify(mockImageAnalysis).clearAnalyzer(); } + + @Test + public void setTargetRotation_makesCallToSetTargetRotation() { + final ImageAnalysisHostApiImpl hostApi = + new ImageAnalysisHostApiImpl(mockBinaryMessenger, instanceManager, context); + final long instanceIdentifier = 32; + final int targetRotation = Surface.ROTATION_180; + + instanceManager.addDartCreatedInstance(mockImageAnalysis, instanceIdentifier); + + hostApi.setTargetRotation(instanceIdentifier, Long.valueOf(targetRotation)); + + verify(mockImageAnalysis).setTargetRotation(targetRotation); + } } 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 df6c8ee74b7f..591037fd97cb 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 @@ -14,6 +14,7 @@ import static org.mockito.Mockito.when; import android.content.Context; +import android.view.Surface; import androidx.camera.core.ImageCapture; import androidx.camera.core.ImageCaptureException; import androidx.camera.core.resolutionselector.ResolutionSelector; @@ -63,18 +64,24 @@ public void create_createsImageCaptureWithCorrectConfiguration() { new ImageCaptureHostApiImpl(mockBinaryMessenger, testInstanceManager, context); final ImageCapture.Builder mockImageCaptureBuilder = mock(ImageCapture.Builder.class); final Long imageCaptureIdentifier = 74L; - final Long flashMode = Long.valueOf(ImageCapture.FLASH_MODE_ON); + final int flashMode = ImageCapture.FLASH_MODE_ON; final ResolutionSelector mockResolutionSelector = mock(ResolutionSelector.class); final long mockResolutionSelectorId = 77; + final int targetRotation = Surface.ROTATION_270; imageCaptureHostApiImpl.cameraXProxy = mockCameraXProxy; testInstanceManager.addDartCreatedInstance(mockResolutionSelector, mockResolutionSelectorId); when(mockCameraXProxy.createImageCaptureBuilder()).thenReturn(mockImageCaptureBuilder); when(mockImageCaptureBuilder.build()).thenReturn(mockImageCapture); - imageCaptureHostApiImpl.create(imageCaptureIdentifier, flashMode, mockResolutionSelectorId); + imageCaptureHostApiImpl.create( + imageCaptureIdentifier, + Long.valueOf(targetRotation), + Long.valueOf(flashMode), + mockResolutionSelectorId); - verify(mockImageCaptureBuilder).setFlashMode(flashMode.intValue()); + verify(mockImageCaptureBuilder).setTargetRotation(targetRotation); + verify(mockImageCaptureBuilder).setFlashMode(flashMode); verify(mockImageCaptureBuilder).setResolutionSelector(mockResolutionSelector); verify(mockImageCaptureBuilder).build(); verify(testInstanceManager).addDartCreatedInstance(mockImageCapture, imageCaptureIdentifier); @@ -197,4 +204,18 @@ public void takePicture_usesExpectedOnImageSavedCallback() { verify(mockResult).error(mockException); } + + @Test + public void setTargetRotation_makesCallToSetTargetRotation() { + final ImageCaptureHostApiImpl hostApi = + new ImageCaptureHostApiImpl(mockBinaryMessenger, testInstanceManager, context); + final long instanceIdentifier = 42; + final int targetRotation = Surface.ROTATION_90; + + testInstanceManager.addDartCreatedInstance(mockImageCapture, instanceIdentifier); + + hostApi.setTargetRotation(instanceIdentifier, Long.valueOf(targetRotation)); + + verify(mockImageCapture).setTargetRotation(targetRotation); + } } diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PreviewTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PreviewTest.java index 37a7c7704135..81b455d7a867 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PreviewTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/PreviewTest.java @@ -217,4 +217,18 @@ public void getResolutionInfo_makesCallToRetrievePreviewResolutionInfo() { assertEquals(resolutionInfo.getWidth(), Long.valueOf(resolutionWidth)); assertEquals(resolutionInfo.getHeight(), Long.valueOf(resolutionHeight)); } + + @Test + public void setTargetRotation_makesCallToSetTargetRotation() { + final PreviewHostApiImpl hostApi = + new PreviewHostApiImpl(mockBinaryMessenger, testInstanceManager, mockTextureRegistry); + final long instanceIdentifier = 52; + final int targetRotation = Surface.ROTATION_180; + + testInstanceManager.addDartCreatedInstance(mockPreview, instanceIdentifier); + + hostApi.setTargetRotation(instanceIdentifier, Long.valueOf(targetRotation)); + + verify(mockPreview).setTargetRotation(targetRotation); + } } diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/SystemServicesTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/SystemServicesTest.java index f905704cbc10..3636629e75f7 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/SystemServicesTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/SystemServicesTest.java @@ -6,7 +6,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; @@ -16,20 +15,16 @@ import android.app.Activity; import android.content.Context; -import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugins.camerax.CameraPermissionsManager.PermissionsRegistry; import io.flutter.plugins.camerax.CameraPermissionsManager.ResultCallback; -import io.flutter.plugins.camerax.DeviceOrientationManager.DeviceOrientationChangeCallback; import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraPermissionsErrorData; import io.flutter.plugins.camerax.GeneratedCameraXLibrary.Result; -import io.flutter.plugins.camerax.GeneratedCameraXLibrary.SystemServicesFlutterApi.Reply; import java.io.File; import java.io.IOException; import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; -import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.MockedStatic; import org.mockito.junit.MockitoJUnit; @@ -96,55 +91,6 @@ public void requestCameraPermissionsTest() { assertEquals(cameraPermissionsErrorData.getDescription(), testErrorDescription); } - @Test - public void deviceOrientationChangeTest() { - final SystemServicesHostApiImpl systemServicesHostApi = - new SystemServicesHostApiImpl(mockBinaryMessenger, mockInstanceManager, mockContext); - final CameraXProxy mockCameraXProxy = mock(CameraXProxy.class); - final Activity mockActivity = mock(Activity.class); - final DeviceOrientationManager mockDeviceOrientationManager = - mock(DeviceOrientationManager.class); - final Boolean isFrontFacing = true; - final int sensorOrientation = 90; - - SystemServicesFlutterApiImpl systemServicesFlutterApi = - mock(SystemServicesFlutterApiImpl.class); - systemServicesHostApi.systemServicesFlutterApi = systemServicesFlutterApi; - - systemServicesHostApi.cameraXProxy = mockCameraXProxy; - systemServicesHostApi.setActivity(mockActivity); - when(mockCameraXProxy.createDeviceOrientationManager( - eq(mockActivity), - eq(isFrontFacing), - eq(sensorOrientation), - any(DeviceOrientationChangeCallback.class))) - .thenReturn(mockDeviceOrientationManager); - - final ArgumentCaptor deviceOrientationChangeCallbackCaptor = - ArgumentCaptor.forClass(DeviceOrientationChangeCallback.class); - - systemServicesHostApi.startListeningForDeviceOrientationChange( - isFrontFacing, Long.valueOf(sensorOrientation)); - - // Test callback method defined in Flutter API is called when device orientation changes. - verify(mockCameraXProxy) - .createDeviceOrientationManager( - eq(mockActivity), - eq(isFrontFacing), - eq(sensorOrientation), - deviceOrientationChangeCallbackCaptor.capture()); - DeviceOrientationChangeCallback deviceOrientationChangeCallback = - deviceOrientationChangeCallbackCaptor.getValue(); - - deviceOrientationChangeCallback.onChange(DeviceOrientation.PORTRAIT_DOWN); - verify(systemServicesFlutterApi) - .sendDeviceOrientationChangedEvent( - eq(DeviceOrientation.PORTRAIT_DOWN.toString()), ArgumentMatchers.>any()); - - // Test that the DeviceOrientationManager starts listening for device orientation changes. - verify(mockDeviceOrientationManager).start(); - } - @Test public void getTempFilePath_returnsCorrectPath() { final SystemServicesHostApiImpl systemServicesHostApi = diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/VideoCaptureTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/VideoCaptureTest.java index 95794334c134..32627fbd6245 100644 --- a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/VideoCaptureTest.java +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/VideoCaptureTest.java @@ -10,6 +10,7 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import android.view.Surface; import androidx.camera.video.Recorder; import androidx.camera.video.VideoCapture; import io.flutter.plugin.common.BinaryMessenger; @@ -78,6 +79,20 @@ public void withOutput_returnsNewVideoCaptureWithAssociatedRecorder() { testInstanceManager.remove(videoCaptureId); } + @Test + public void setTargetRotation_makesCallToSetTargetRotation() { + final VideoCaptureHostApiImpl hostApi = + new VideoCaptureHostApiImpl(mockBinaryMessenger, testInstanceManager); + final long instanceIdentifier = 62; + final int targetRotation = Surface.ROTATION_270; + + testInstanceManager.addDartCreatedInstance(mockVideoCapture, instanceIdentifier); + + hostApi.setTargetRotation(instanceIdentifier, Long.valueOf(targetRotation)); + + verify(mockVideoCapture).setTargetRotation(targetRotation); + } + @Test public void flutterApiCreateTest() { final VideoCaptureFlutterApiImpl spyVideoCaptureFlutterApi = diff --git a/packages/camera/camera_android_camerax/example/lib/main.dart b/packages/camera/camera_android_camerax/example/lib/main.dart index 449899aef7ed..960d50723461 100644 --- a/packages/camera/camera_android_camerax/example/lib/main.dart +++ b/packages/camera/camera_android_camerax/example/lib/main.dart @@ -296,14 +296,16 @@ class _CameraExampleHomeState extends State IconButton( icon: Icon(enableAudio ? Icons.volume_up : Icons.volume_mute), color: Colors.blue, - onPressed: () {}, // TODO(camsim99): Add functionality back here. + onPressed: controller != null ? onAudioModeButtonPressed : null, ), IconButton( icon: Icon(controller?.value.isCaptureOrientationLocked ?? false ? Icons.screen_lock_rotation : Icons.screen_rotation), color: Colors.blue, - onPressed: () {}, // TODO(camsim99): Add functionality back here. + onPressed: controller != null + ? onCaptureOrientationLockButtonPressed + : null, ), ], ), 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 b7f8f8a091d2..fbb42d3664ed 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 @@ -6,6 +6,7 @@ import 'dart:async'; import 'package:async/async.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/services.dart' show DeviceOrientation; import 'package:flutter/widgets.dart'; import 'package:stream_transform/stream_transform.dart'; @@ -17,6 +18,7 @@ import 'camera_selector.dart'; import 'camera_state.dart'; import 'camerax_library.g.dart'; import 'camerax_proxy.dart'; +import 'device_orientation_manager.dart'; import 'exposure_state.dart'; import 'fallback_strategy.dart'; import 'image_analysis.dart'; @@ -155,6 +157,24 @@ class AndroidCameraCameraX extends CameraPlatform { /// set for the camera in use. static const String zoomStateNotSetErrorCode = 'zoomStateNotSet'; + /// Whether or not the capture orientation is locked. + /// + /// Indicates a new target rotation should not be set as it has been locked by + /// [lockCaptureOrientation]. + @visibleForTesting + bool captureOrientationLocked = false; + + /// Whether or not the default rotation for [UseCase]s needs to be set + /// manually because the capture orientation was previously locked. + /// + /// Currently, CameraX provides no way to unset target rotations for + /// [UseCase]s, so once they are set and unset, this plugin must start setting + /// the default orientation manually. + /// + /// See https://developer.android.com/reference/androidx/camera/core/ImageCapture#setTargetRotation(int) + /// for an example on how setting target rotations for [UseCase]s works. + bool shouldSetDefaultRotation = false; + /// Returns list of all available cameras and their descriptions. @override Future> availableCameras() async { @@ -240,20 +260,19 @@ class AndroidCameraCameraX extends CameraPlatform { processCameraProvider!.unbindAll(); // Configure Preview instance. - final int targetRotation = - _getTargetRotation(cameraDescription.sensorOrientation); - preview = proxy.createPreview( - targetRotation: targetRotation, - resolutionSelector: presetResolutionSelector); + preview = proxy.createPreview(presetResolutionSelector, + /* use CameraX default target rotation */ null); final int flutterSurfaceTextureId = await proxy.setPreviewSurfaceProvider(preview!); // Configure ImageCapture instance. - imageCapture = proxy.createImageCapture(presetResolutionSelector); + imageCapture = proxy.createImageCapture(presetResolutionSelector, + /* use CameraX default target rotation */ null); // Configure ImageAnalysis instance. // Defaults to YUV_420_888 image format. - imageAnalysis = proxy.createImageAnalysis(presetResolutionSelector); + imageAnalysis = proxy.createImageAnalysis(presetResolutionSelector, + /* use CameraX default target rotation */ null); // Configure VideoCapture and Recorder instances. recorder = proxy.createRecorder(presetQualitySelector); @@ -370,6 +389,35 @@ class AndroidCameraCameraX extends CameraPlatform { return _cameraEvents(cameraId).whereType(); } + /// Locks the capture orientation. + @override + Future lockCaptureOrientation( + int cameraId, + DeviceOrientation orientation, + ) async { + // Flag that (1) default rotation for UseCases will need to be set manually + // if orientation is ever unlocked and (2) the capture orientation is locked + // and should not be changed until unlocked. + shouldSetDefaultRotation = true; + captureOrientationLocked = true; + + // Get target rotation based on locked orientation. + final int targetLockedRotation = + _getRotationConstantFromDeviceOrientation(orientation); + + // Update UseCases to use target device orientation. + await imageCapture!.setTargetRotation(targetLockedRotation); + await imageAnalysis!.setTargetRotation(targetLockedRotation); + await videoCapture!.setTargetRotation(targetLockedRotation); + } + + /// Unlocks the capture orientation. + @override + Future unlockCaptureOrientation(int cameraId) async { + // Flag that default rotation should be set for UseCases as needed. + captureOrientationLocked = false; + } + /// Gets the minimum supported exposure offset for the selected camera in EV units. /// /// [cameraId] not used. @@ -449,7 +497,8 @@ class AndroidCameraCameraX extends CameraPlatform { /// The ui orientation changed. @override Stream onDeviceOrientationChanged() { - return SystemServices.deviceOrientationChangedStreamController.stream; + return DeviceOrientationManager + .deviceOrientationChangedStreamController.stream; } /// Pause the active preview on the current frame for the selected camera. @@ -493,6 +542,7 @@ class AndroidCameraCameraX extends CameraPlatform { /// [cameraId] is not used. @override Future takePicture(int cameraId) async { + // Set flash mode. if (_currentFlashMode != null) { await imageCapture!.setFlashMode(_currentFlashMode!); } else if (torchEnabled) { @@ -500,6 +550,14 @@ class AndroidCameraCameraX extends CameraPlatform { // been enabled. await imageCapture!.setFlashMode(ImageCapture.flashModeOff); } + + // Set target rotation to default CameraX rotation only if capture + // orientation not locked. + if (!captureOrientationLocked && shouldSetDefaultRotation) { + await imageCapture! + .setTargetRotation(await proxy.getDefaultDisplayRotation()); + } + final String picturePath = await imageCapture!.takePicture(); return XFile(picturePath); } @@ -582,6 +640,13 @@ class AndroidCameraCameraX extends CameraPlatform { .bindToLifecycle(cameraSelector!, [videoCapture!]); } + // Set target rotation to default CameraX rotation only if capture + // orientation not locked. + if (!captureOrientationLocked && shouldSetDefaultRotation) { + await videoCapture! + .setTargetRotation(await proxy.getDefaultDisplayRotation()); + } + videoOutputPath = await SystemServices.getTempFilePath(videoPrefix, '.temp'); pendingRecording = await recorder!.prepareRecording(videoOutputPath!); @@ -654,7 +719,7 @@ class AndroidCameraCameraX extends CameraPlatform { Stream onStreamedFrameAvailable(int cameraId, {CameraImageStreamOptions? options}) { cameraImageDataStreamController = StreamController( - onListen: () => _onFrameStreamListen(cameraId), + onListen: () => _configureImageAnalysis(cameraId), onCancel: _onFrameStreamCancel, ); return cameraImageDataStreamController!.stream; @@ -683,7 +748,14 @@ class AndroidCameraCameraX extends CameraPlatform { /// Configures the [imageAnalysis] instance for image streaming. Future _configureImageAnalysis(int cameraId) async { - // Create Analyzer that can read image data for image streaming. + // Set target rotation to default CameraX rotation only if capture + // orientation not locked. + if (!captureOrientationLocked && shouldSetDefaultRotation) { + await imageAnalysis! + .setTargetRotation(await proxy.getDefaultDisplayRotation()); + } + + // Create and set Analyzer that can read image data for image streaming. final WeakReference weakThis = WeakReference(this); Future analyze(ImageProxy imageProxy) async { @@ -708,7 +780,7 @@ class AndroidCameraCameraX extends CameraPlatform { width: imageProxy.width); weakThis.target!.cameraImageDataStreamController!.add(cameraImageData); - unawaited(imageProxy.close()); + await imageProxy.close(); } final Analyzer analyzer = proxy.createAnalyzer(analyze); @@ -728,12 +800,6 @@ class AndroidCameraCameraX extends CameraPlatform { // Methods for configuring image streaming: - /// The [onListen] callback for the stream controller used for image - /// streaming. - Future _onFrameStreamListen(int cameraId) async { - await _configureImageAnalysis(cameraId); - } - /// The [onCancel] callback for the stream controller used for image /// streaming. /// @@ -816,21 +882,19 @@ class AndroidCameraCameraX extends CameraPlatform { } } - /// Returns [Surface] target rotation constant that maps to specified sensor - /// orientation. - int _getTargetRotation(int sensorOrientation) { - switch (sensorOrientation) { - case 90: + /// Returns [Surface] constant for counter-clockwise degrees of rotation from + /// [DeviceOrientation.portraitUp] required to reach the specified + /// [DeviceOrientation]. + int _getRotationConstantFromDeviceOrientation(DeviceOrientation orientation) { + switch (orientation) { + case DeviceOrientation.portraitUp: + return Surface.ROTATION_0; + case DeviceOrientation.landscapeLeft: return Surface.ROTATION_90; - case 180: + case DeviceOrientation.portraitDown: return Surface.ROTATION_180; - case 270: + case DeviceOrientation.landscapeRight: return Surface.ROTATION_270; - case 0: - return Surface.ROTATION_0; - default: - throw ArgumentError( - '"$sensorOrientation" is not a valid sensor orientation value'); } } diff --git a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax_flutter_api_impls.dart b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax_flutter_api_impls.dart index b59e0df30da1..24f159a1eab5 100644 --- a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax_flutter_api_impls.dart +++ b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax_flutter_api_impls.dart @@ -10,6 +10,7 @@ import 'camera_selector.dart'; import 'camera_state.dart'; import 'camera_state_error.dart'; import 'camerax_library.g.dart'; +import 'device_orientation_manager.dart'; import 'exposure_state.dart'; import 'image_proxy.dart'; import 'java_object.dart'; @@ -34,6 +35,8 @@ class AndroidCameraXCameraFlutterApis { CameraSelectorFlutterApiImpl? cameraSelectorFlutterApiImpl, ProcessCameraProviderFlutterApiImpl? processCameraProviderFlutterApiImpl, SystemServicesFlutterApiImpl? systemServicesFlutterApiImpl, + DeviceOrientationManagerFlutterApiImpl? + deviceOrientationManagerFlutterApiImpl, CameraStateErrorFlutterApiImpl? cameraStateErrorFlutterApiImpl, CameraStateFlutterApiImpl? cameraStateFlutterApiImpl, PendingRecordingFlutterApiImpl? pendingRecordingFlutterApiImpl, @@ -60,6 +63,9 @@ class AndroidCameraXCameraFlutterApis { this.cameraFlutterApiImpl = cameraFlutterApiImpl ?? CameraFlutterApiImpl(); this.systemServicesFlutterApiImpl = systemServicesFlutterApiImpl ?? SystemServicesFlutterApiImpl(); + this.deviceOrientationManagerFlutterApiImpl = + deviceOrientationManagerFlutterApiImpl ?? + DeviceOrientationManagerFlutterApiImpl(); this.cameraStateErrorFlutterApiImpl = cameraStateErrorFlutterApiImpl ?? CameraStateErrorFlutterApiImpl(); this.cameraStateFlutterApiImpl = @@ -117,6 +123,10 @@ class AndroidCameraXCameraFlutterApis { /// Flutter Api implementation for [SystemServices]. late final SystemServicesFlutterApiImpl systemServicesFlutterApiImpl; + /// Flutter Api implementation for [DeviceOrientationManager]. + late final DeviceOrientationManagerFlutterApiImpl + deviceOrientationManagerFlutterApiImpl; + /// Flutter Api implementation for [CameraStateError]. late final CameraStateErrorFlutterApiImpl? cameraStateErrorFlutterApiImpl; @@ -169,6 +179,8 @@ class AndroidCameraXCameraFlutterApis { processCameraProviderFlutterApiImpl); CameraFlutterApi.setup(cameraFlutterApiImpl); SystemServicesFlutterApi.setup(systemServicesFlutterApiImpl); + DeviceOrientationManagerFlutterApi.setup( + deviceOrientationManagerFlutterApiImpl); CameraStateErrorFlutterApi.setup(cameraStateErrorFlutterApiImpl); CameraStateFlutterApi.setup(cameraStateFlutterApiImpl); PendingRecordingFlutterApi.setup(pendingRecordingFlutterApiImpl); 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 4dd5253bd2d6..f1528bbeec87 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 @@ -894,10 +894,77 @@ class SystemServicesHostApi { } } + Future getTempFilePath(String arg_prefix, String arg_suffix) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.SystemServicesHostApi.getTempFilePath', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = + await channel.send([arg_prefix, arg_suffix]) 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 if (replyList[0] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyList[0] as String?)!; + } + } +} + +abstract class SystemServicesFlutterApi { + static const MessageCodec codec = StandardMessageCodec(); + + void onCameraError(String errorDescription); + + static void setup(SystemServicesFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.SystemServicesFlutterApi.onCameraError', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.SystemServicesFlutterApi.onCameraError was null.'); + final List args = (message as List?)!; + final String? arg_errorDescription = (args[0] as String?); + assert(arg_errorDescription != null, + 'Argument for dev.flutter.pigeon.SystemServicesFlutterApi.onCameraError was null, expected non-null String.'); + api.onCameraError(arg_errorDescription!); + return; + }); + } + } + } +} + +class DeviceOrientationManagerHostApi { + /// Constructor for [DeviceOrientationManagerHostApi]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + DeviceOrientationManagerHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = StandardMessageCodec(); + Future startListeningForDeviceOrientationChange( bool arg_isFrontFacing, int arg_sensorOrientation) async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.SystemServicesHostApi.startListeningForDeviceOrientationChange', + 'dev.flutter.pigeon.DeviceOrientationManagerHostApi.startListeningForDeviceOrientationChange', codec, binaryMessenger: _binaryMessenger); final List? replyList = @@ -921,7 +988,7 @@ class SystemServicesHostApi { Future stopListeningForDeviceOrientationChange() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.SystemServicesHostApi.stopListeningForDeviceOrientationChange', + 'dev.flutter.pigeon.DeviceOrientationManagerHostApi.stopListeningForDeviceOrientationChange', codec, binaryMessenger: _binaryMessenger); final List? replyList = await channel.send(null) as List?; @@ -941,12 +1008,12 @@ class SystemServicesHostApi { } } - Future getTempFilePath(String arg_prefix, String arg_suffix) async { + Future getDefaultDisplayRotation() async { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.SystemServicesHostApi.getTempFilePath', codec, + 'dev.flutter.pigeon.DeviceOrientationManagerHostApi.getDefaultDisplayRotation', + codec, binaryMessenger: _binaryMessenger); - final List? replyList = - await channel.send([arg_prefix, arg_suffix]) as List?; + final List? replyList = await channel.send(null) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', @@ -964,23 +1031,21 @@ class SystemServicesHostApi { message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyList[0] as String?)!; + return (replyList[0] as int?)!; } } } -abstract class SystemServicesFlutterApi { +abstract class DeviceOrientationManagerFlutterApi { static const MessageCodec codec = StandardMessageCodec(); void onDeviceOrientationChanged(String orientation); - void onCameraError(String errorDescription); - - static void setup(SystemServicesFlutterApi? api, + static void setup(DeviceOrientationManagerFlutterApi? api, {BinaryMessenger? binaryMessenger}) { { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.SystemServicesFlutterApi.onDeviceOrientationChanged', + 'dev.flutter.pigeon.DeviceOrientationManagerFlutterApi.onDeviceOrientationChanged', codec, binaryMessenger: binaryMessenger); if (api == null) { @@ -988,35 +1053,16 @@ abstract class SystemServicesFlutterApi { } else { channel.setMessageHandler((Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.SystemServicesFlutterApi.onDeviceOrientationChanged was null.'); + 'Argument for dev.flutter.pigeon.DeviceOrientationManagerFlutterApi.onDeviceOrientationChanged was null.'); final List args = (message as List?)!; final String? arg_orientation = (args[0] as String?); assert(arg_orientation != null, - 'Argument for dev.flutter.pigeon.SystemServicesFlutterApi.onDeviceOrientationChanged was null, expected non-null String.'); + 'Argument for dev.flutter.pigeon.DeviceOrientationManagerFlutterApi.onDeviceOrientationChanged was null, expected non-null String.'); api.onDeviceOrientationChanged(arg_orientation!); return; }); } } - { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.SystemServicesFlutterApi.onCameraError', codec, - binaryMessenger: binaryMessenger); - if (api == null) { - channel.setMessageHandler(null); - } else { - channel.setMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.SystemServicesFlutterApi.onCameraError was null.'); - final List args = (message as List?)!; - final String? arg_errorDescription = (args[0] as String?); - assert(arg_errorDescription != null, - 'Argument for dev.flutter.pigeon.SystemServicesFlutterApi.onCameraError was null, expected non-null String.'); - api.onCameraError(arg_errorDescription!); - return; - }); - } - } } } @@ -1151,6 +1197,28 @@ class PreviewHostApi { return (replyList[0] as ResolutionInfo?)!; } } + + Future setTargetRotation(int arg_identifier, int arg_rotation) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PreviewHostApi.setTargetRotation', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_identifier, arg_rotation]) 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; + } + } } class VideoCaptureHostApi { @@ -1216,6 +1284,28 @@ class VideoCaptureHostApi { return (replyList[0] as int?)!; } } + + Future setTargetRotation(int arg_identifier, int arg_rotation) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoCaptureHostApi.setTargetRotation', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_identifier, arg_rotation]) 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 VideoCaptureFlutterApi { @@ -1603,14 +1693,17 @@ class ImageCaptureHostApi { static const MessageCodec codec = StandardMessageCodec(); - Future create(int arg_identifier, int? arg_flashMode, - int? arg_resolutionSelectorId) async { + Future create(int arg_identifier, int? arg_targetRotation, + int? arg_flashMode, int? arg_resolutionSelectorId) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.ImageCaptureHostApi.create', codec, binaryMessenger: _binaryMessenger); - final List? replyList = await channel.send( - [arg_identifier, arg_flashMode, arg_resolutionSelectorId]) - as List?; + final List? replyList = await channel.send([ + arg_identifier, + arg_targetRotation, + arg_flashMode, + arg_resolutionSelectorId + ]) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', @@ -1675,6 +1768,28 @@ class ImageCaptureHostApi { return (replyList[0] as String?)!; } } + + Future setTargetRotation(int arg_identifier, int arg_rotation) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImageCaptureHostApi.setTargetRotation', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_identifier, arg_rotation]) 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; + } + } } class _ResolutionStrategyHostApiCodec extends StandardMessageCodec { @@ -1974,13 +2089,16 @@ class ImageAnalysisHostApi { static const MessageCodec codec = StandardMessageCodec(); - Future create(int arg_identifier, int? arg_resolutionSelectorId) async { + Future create(int arg_identifier, int? arg_targetRotation, + int? arg_resolutionSelectorId) async { final BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.ImageAnalysisHostApi.create', codec, binaryMessenger: _binaryMessenger); - final List? replyList = - await channel.send([arg_identifier, arg_resolutionSelectorId]) - as List?; + final List? replyList = await channel.send([ + arg_identifier, + arg_targetRotation, + arg_resolutionSelectorId + ]) as List?; if (replyList == null) { throw PlatformException( code: 'channel-error', @@ -2042,6 +2160,28 @@ class ImageAnalysisHostApi { return; } } + + Future setTargetRotation(int arg_identifier, int arg_rotation) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImageAnalysisHostApi.setTargetRotation', codec, + binaryMessenger: _binaryMessenger); + final List? replyList = await channel + .send([arg_identifier, arg_rotation]) 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; + } + } } class AnalyzerHostApi { diff --git a/packages/camera/camera_android_camerax/lib/src/camerax_proxy.dart b/packages/camera/camera_android_camerax/lib/src/camerax_proxy.dart index 133d84544d9b..87d3680ff2c3 100644 --- a/packages/camera/camera_android_camerax/lib/src/camerax_proxy.dart +++ b/packages/camera/camera_android_camerax/lib/src/camerax_proxy.dart @@ -8,6 +8,7 @@ import 'analyzer.dart'; import 'camera_selector.dart'; import 'camera_state.dart'; import 'camerax_library.g.dart'; +import 'device_orientation_manager.dart'; import 'fallback_strategy.dart'; import 'image_analysis.dart'; import 'image_capture.dart'; @@ -47,6 +48,7 @@ class CameraXProxy { this.startListeningForDeviceOrientationChange = _startListeningForDeviceOrientationChange, this.setPreviewSurfaceProvider = _setPreviewSurfaceProvider, + this.getDefaultDisplayRotation = _getDefaultDisplayRotation, }); /// Returns a [ProcessCameraProvider] instance. @@ -58,12 +60,14 @@ class CameraXProxy { /// Returns a [Preview] configured with the specified target rotation and /// specified [ResolutionSelector]. Preview Function( - {required int targetRotation, - ResolutionSelector? resolutionSelector}) createPreview; + ResolutionSelector? resolutionSelector, + int? targetRotation, + ) createPreview; /// Returns an [ImageCapture] configured with specified flash mode and /// the specified [ResolutionSelector]. - ImageCapture Function(ResolutionSelector? resolutionSelector) + ImageCapture Function( + ResolutionSelector? resolutionSelector, int? targetRotation) createImageCapture; /// Returns a [Recorder] for use in video capture configured with the @@ -75,7 +79,8 @@ class CameraXProxy { /// Returns an [ImageAnalysis] configured with the specified /// [ResolutionSelector]. - ImageAnalysis Function(ResolutionSelector? resolutionSelector) + ImageAnalysis Function( + ResolutionSelector? resolutionSelector, int? targetRotation) createImageAnalysis; /// Returns an [Analyzer] configured with the specified callback for @@ -128,6 +133,10 @@ class CameraXProxy { /// the ID corresponding to the surface it will provide. Future Function(Preview preview) setPreviewSurfaceProvider; + /// Returns default rotation for [UseCase]s in terms of one of the [Surface] + /// rotation constants. + Future Function() getDefaultDisplayRotation; + static Future _getProcessCameraProvider() { return ProcessCameraProvider.getInstance(); } @@ -145,14 +154,17 @@ class CameraXProxy { } static Preview _createAttachedPreview( - {required int targetRotation, ResolutionSelector? resolutionSelector}) { + ResolutionSelector? resolutionSelector, int? targetRotation) { return Preview( - targetRotation: targetRotation, resolutionSelector: resolutionSelector); + initialTargetRotation: targetRotation, + resolutionSelector: resolutionSelector); } static ImageCapture _createAttachedImageCapture( - ResolutionSelector? resolutionSelector) { - return ImageCapture(resolutionSelector: resolutionSelector); + ResolutionSelector? resolutionSelector, int? targetRotation) { + return ImageCapture( + resolutionSelector: resolutionSelector, + initialTargetRotation: targetRotation); } static Recorder _createAttachedRecorder(QualitySelector? qualitySelector) { @@ -165,8 +177,10 @@ class CameraXProxy { } static ImageAnalysis _createAttachedImageAnalysis( - ResolutionSelector? resolutionSelector) { - return ImageAnalysis(resolutionSelector: resolutionSelector); + ResolutionSelector? resolutionSelector, int? targetRotation) { + return ImageAnalysis( + resolutionSelector: resolutionSelector, + initialTargetRotation: targetRotation); } static Analyzer _createAttachedAnalyzer( @@ -214,11 +228,15 @@ class CameraXProxy { static void _startListeningForDeviceOrientationChange( bool cameraIsFrontFacing, int sensorOrientation) { - SystemServices.startListeningForDeviceOrientationChange( + DeviceOrientationManager.startListeningForDeviceOrientationChange( cameraIsFrontFacing, sensorOrientation); } static Future _setPreviewSurfaceProvider(Preview preview) async { return preview.setSurfaceProvider(); } + + static Future _getDefaultDisplayRotation() async { + return DeviceOrientationManager.getDefaultDisplayRotation(); + } } diff --git a/packages/camera/camera_android_camerax/lib/src/device_orientation_manager.dart b/packages/camera/camera_android_camerax/lib/src/device_orientation_manager.dart new file mode 100644 index 000000000000..10f20232485b --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/device_orientation_manager.dart @@ -0,0 +1,121 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:camera_platform_interface/camera_platform_interface.dart' + show DeviceOrientationChangedEvent; +import 'package:flutter/services.dart'; + +import 'android_camera_camerax_flutter_api_impls.dart'; +import 'camerax_library.g.dart'; + +// Ignoring lint indicating this class only contains static members +// as this class is a wrapper for various Android system services. +// ignore_for_file: avoid_classes_with_only_static_members + +/// Utility class that offers access to Android system services needed for +/// camera usage and other informational streams. +class DeviceOrientationManager { + /// Stream that emits the device orientation whenever it is changed. + /// + /// Values may start being added to the stream once + /// `startListeningForDeviceOrientationChange(...)` is called. + static final StreamController + deviceOrientationChangedStreamController = + StreamController.broadcast(); + + /// Requests that [deviceOrientationChangedStreamController] start + /// emitting values for any change in device orientation. + static void startListeningForDeviceOrientationChange( + bool isFrontFacing, int sensorOrientation, + {BinaryMessenger? binaryMessenger}) { + AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); + final DeviceOrientationManagerHostApi api = + DeviceOrientationManagerHostApi(binaryMessenger: binaryMessenger); + + api.startListeningForDeviceOrientationChange( + isFrontFacing, sensorOrientation); + } + + /// Stops the [deviceOrientationChangedStreamController] from emitting values + /// for changes in device orientation. + static void stopListeningForDeviceOrientationChange( + {BinaryMessenger? binaryMessenger}) { + final DeviceOrientationManagerHostApi api = + DeviceOrientationManagerHostApi(binaryMessenger: binaryMessenger); + + api.stopListeningForDeviceOrientationChange(); + } + + /// Retrieves the default rotation that CameraX uses for [UseCase]s in terms + /// of one of the [Surface] rotation constants. + /// + /// The default rotation that CameraX uses is the rotation of the default + /// display at the time of binding a particular [UseCase], but the default + /// display does not change in the plugin, so this default value is + /// display-agnostic. + /// + /// [startListeningForDeviceOrientationChange] must be called before calling + /// this method. + static Future getDefaultDisplayRotation( + {BinaryMessenger? binaryMessenger}) async { + final DeviceOrientationManagerHostApi api = + DeviceOrientationManagerHostApi(binaryMessenger: binaryMessenger); + + return api.getDefaultDisplayRotation(); + } + + /// Serializes [DeviceOrientation] into a [String]. + static String serializeDeviceOrientation(DeviceOrientation orientation) { + switch (orientation) { + case DeviceOrientation.landscapeLeft: + return 'LANDSCAPE_LEFT'; + case DeviceOrientation.landscapeRight: + return 'LANDSCAPE_RIGHT'; + case DeviceOrientation.portraitDown: + return 'PORTRAIT_DOWN'; + case DeviceOrientation.portraitUp: + return 'PORTRAIT_UP'; + } + } +} + +/// Flutter API implementation of [DeviceOrientationManager]. +class DeviceOrientationManagerFlutterApiImpl + implements DeviceOrientationManagerFlutterApi { + /// Constructs an [DeviceOrientationManagerFlutterApiImpl]. + DeviceOrientationManagerFlutterApiImpl(); + + /// Callback method for any changes in device orientation. + /// + /// Will only be called if + /// `DeviceOrientationManager.startListeningForDeviceOrientationChange(...)` was called + /// to start listening for device orientation updates. + @override + void onDeviceOrientationChanged(String orientation) { + final DeviceOrientation deviceOrientation = + deserializeDeviceOrientation(orientation); + DeviceOrientationManager.deviceOrientationChangedStreamController + .add(DeviceOrientationChangedEvent(deviceOrientation)); + } + + /// Deserializes device orientation in [String] format into a + /// [DeviceOrientation]. + DeviceOrientation deserializeDeviceOrientation(String orientation) { + switch (orientation) { + case 'LANDSCAPE_LEFT': + return DeviceOrientation.landscapeLeft; + case 'LANDSCAPE_RIGHT': + return DeviceOrientation.landscapeRight; + case 'PORTRAIT_DOWN': + return DeviceOrientation.portraitDown; + case 'PORTRAIT_UP': + return DeviceOrientation.portraitUp; + default: + throw ArgumentError( + '"$orientation" is not a valid DeviceOrientation value'); + } + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/image_analysis.dart b/packages/camera/camera_android_camerax/lib/src/image_analysis.dart index e28457c6d192..3dc21ce2a7bc 100644 --- a/packages/camera/camera_android_camerax/lib/src/image_analysis.dart +++ b/packages/camera/camera_android_camerax/lib/src/image_analysis.dart @@ -24,20 +24,23 @@ class ImageAnalysis extends UseCase { ImageAnalysis( {BinaryMessenger? binaryMessenger, InstanceManager? instanceManager, + this.initialTargetRotation, this.resolutionSelector}) : super.detached( binaryMessenger: binaryMessenger, instanceManager: instanceManager) { _api = _ImageAnalysisHostApiImpl( binaryMessenger: binaryMessenger, instanceManager: instanceManager); - _api.createfromInstances(this, resolutionSelector); + _api.createFromInstances(this, initialTargetRotation, resolutionSelector); AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); } - /// Constructs an [ImageAnalysis] that is not automatically attached to a native object. + /// Constructs an [ImageAnalysis] that is not automatically attached to a + /// native object. ImageAnalysis.detached( {BinaryMessenger? binaryMessenger, InstanceManager? instanceManager, + this.initialTargetRotation, this.resolutionSelector}) : super.detached( binaryMessenger: binaryMessenger, @@ -49,18 +52,34 @@ class ImageAnalysis extends UseCase { late final _ImageAnalysisHostApiImpl _api; + /// Initial target rotation of the camera used for the preview stream. + /// + /// Should be specified in terms of one of the [Surface] + /// rotation constants that represents the counter-clockwise degrees of + /// rotation relative to [DeviceOrientation.portraitUp]. + // TODO(camsim99): Remove this parameter. https://github.com/flutter/flutter/issues/140664 + final int? initialTargetRotation; + /// Target resolution of the camera preview stream. /// /// If not set, this [UseCase] will default to the behavior described in: /// https://developer.android.com/reference/androidx/camera/core/ImageAnalysis.Builder#setResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector). final ResolutionSelector? resolutionSelector; + /// Dynamically sets the target rotation of this instance. + /// + /// [rotation] should be specified in terms of one of the [Surface] + /// rotation constants that represents the counter-clockwise degrees of + /// rotation relative to [DeviceOrientation.portraitUp]. + Future setTargetRotation(int rotation) => + _api.setTargetRotationFromInstances(this, rotation); + /// Sets an [Analyzer] to receive and analyze images. Future setAnalyzer(Analyzer analyzer) => - _api.setAnalyzerfromInstances(this, analyzer); + _api.setAnalyzerFromInstances(this, analyzer); /// Removes a previously set [Analyzer]. - Future clearAnalyzer() => _api.clearAnalyzerfromInstances(this); + Future clearAnalyzer() => _api.clearAnalyzerFromInstances(this); } /// Host API implementation of [ImageAnalysis]. @@ -85,27 +104,37 @@ class _ImageAnalysisHostApiImpl extends ImageAnalysisHostApi { /// Creates an [ImageAnalysis] instance with the specified target resolution /// on the native side. - Future createfromInstances( + Future createFromInstances( ImageAnalysis instance, + int? targetRotation, ResolutionSelector? resolutionSelector, ) { return create( instanceManager.addDartCreatedInstance( instance, onCopy: (ImageAnalysis original) => ImageAnalysis.detached( + initialTargetRotation: original.initialTargetRotation, resolutionSelector: original.resolutionSelector, binaryMessenger: binaryMessenger, instanceManager: instanceManager, ), ), + targetRotation, resolutionSelector == null ? null : instanceManager.getIdentifier(resolutionSelector), ); } + /// Dynamically sets the target rotation of [instance] to [rotation]. + Future setTargetRotationFromInstances( + ImageAnalysis instance, int rotation) { + return setTargetRotation( + instanceManager.getIdentifier(instance)!, rotation); + } + /// Sets the [analyzer] to receive and analyze images on the [instance]. - Future setAnalyzerfromInstances( + Future setAnalyzerFromInstances( ImageAnalysis instance, Analyzer analyzer, ) { @@ -116,7 +145,7 @@ class _ImageAnalysisHostApiImpl extends ImageAnalysisHostApi { } /// Removes a previously set analyzer from the [instance]. - Future clearAnalyzerfromInstances( + Future clearAnalyzerFromInstances( ImageAnalysis instance, ) { return clearAnalyzer( diff --git a/packages/camera/camera_android_camerax/lib/src/image_capture.dart b/packages/camera/camera_android_camerax/lib/src/image_capture.dart index 76fd9c4ae5c0..b2f8671cd9f3 100644 --- a/packages/camera/camera_android_camerax/lib/src/image_capture.dart +++ b/packages/camera/camera_android_camerax/lib/src/image_capture.dart @@ -20,6 +20,7 @@ class ImageCapture extends UseCase { ImageCapture({ BinaryMessenger? binaryMessenger, InstanceManager? instanceManager, + this.initialTargetRotation, this.targetFlashMode, this.resolutionSelector, }) : super.detached( @@ -28,13 +29,16 @@ class ImageCapture extends UseCase { ) { _api = ImageCaptureHostApiImpl( binaryMessenger: binaryMessenger, instanceManager: instanceManager); - _api.createFromInstance(this, targetFlashMode, resolutionSelector); + _api.createFromInstance( + this, initialTargetRotation, targetFlashMode, resolutionSelector); } - /// Constructs a [ImageCapture] that is not automatically attached to a native object. + /// Constructs an [ImageCapture] that is not automatically attached to a + /// native object. ImageCapture.detached({ BinaryMessenger? binaryMessenger, InstanceManager? instanceManager, + this.initialTargetRotation, this.targetFlashMode, this.resolutionSelector, }) : super.detached( @@ -47,6 +51,15 @@ class ImageCapture extends UseCase { late final ImageCaptureHostApiImpl _api; + /// Initial target rotation of the camera used for the preview stream. + /// + /// Should be specified in terms of one of the [Surface] + /// rotation constants that represents the counter-clockwise degrees of + /// rotation relative to [DeviceOrientation.portraitUp]. + /// + // TODO(camsim99): Remove this parameter. https://github.com/flutter/flutter/issues/140664 + final int? initialTargetRotation; + /// Flash mode used to take a picture. final int? targetFlashMode; @@ -71,9 +84,17 @@ class ImageCapture extends UseCase { /// See https://developer.android.com/reference/androidx/camera/core/ImageCapture#FLASH_MODE_OFF(). static const int flashModeOff = 2; + /// Dynamically sets the target rotation of this instance. + /// + /// [rotation] should be specified in terms of one of the [Surface] + /// rotation constants that represents the counter-clockwise degrees of + /// rotation relative to [DeviceOrientation.portraitUp]. + Future setTargetRotation(int rotation) => + _api.setTargetRotationFromInstances(this, rotation); + /// Sets the flash mode to use for image capture. Future setFlashMode(int newFlashMode) async { - return _api.setFlashModeFromInstance(this, newFlashMode); + return _api.setFlashModeFromInstances(this, newFlashMode); } /// Takes a picture and returns the absolute path of where the capture image @@ -94,7 +115,7 @@ class ImageCapture extends UseCase { /// See https://developer.android.com/reference/androidx/camera/core/ImageCapture /// for more information. Future takePicture() async { - return _api.takePictureFromInstance(this); + return _api.takePictureFromInstances(this); } } @@ -124,27 +145,36 @@ class ImageCaptureHostApiImpl extends ImageCaptureHostApi { /// Creates an [ImageCapture] instance with the flash mode and target resolution /// if specified. - void createFromInstance(ImageCapture instance, int? targetFlashMode, - ResolutionSelector? resolutionSelector) { + void createFromInstance(ImageCapture instance, int? targetRotation, + int? targetFlashMode, ResolutionSelector? resolutionSelector) { final int identifier = instanceManager.addDartCreatedInstance(instance, onCopy: (ImageCapture original) { return ImageCapture.detached( binaryMessenger: binaryMessenger, instanceManager: instanceManager, + initialTargetRotation: original.initialTargetRotation, targetFlashMode: original.targetFlashMode, resolutionSelector: original.resolutionSelector); }); create( identifier, + targetRotation, targetFlashMode, resolutionSelector == null ? null : instanceManager.getIdentifier(resolutionSelector)); } + /// Dynamically sets the target rotation of [instance] to [rotation]. + Future setTargetRotationFromInstances( + ImageCapture instance, int rotation) { + return setTargetRotation( + instanceManager.getIdentifier(instance)!, rotation); + } + /// Sets the flash mode for the specified [ImageCapture] instance to take /// a picture with. - Future setFlashModeFromInstance( + Future setFlashModeFromInstances( ImageCapture instance, int flashMode) async { final int? identifier = instanceManager.getIdentifier(instance); assert(identifier != null, @@ -154,7 +184,7 @@ class ImageCaptureHostApiImpl extends ImageCaptureHostApi { } /// Takes a picture with the specified [ImageCapture] instance. - Future takePictureFromInstance(ImageCapture instance) async { + Future takePictureFromInstances(ImageCapture instance) async { final int? identifier = instanceManager.getIdentifier(instance); assert(identifier != null, 'No ImageCapture has the identifer of that requested to get the resolution information for.'); diff --git a/packages/camera/camera_android_camerax/lib/src/preview.dart b/packages/camera/camera_android_camerax/lib/src/preview.dart index f0568078bedb..8990313817b6 100644 --- a/packages/camera/camera_android_camerax/lib/src/preview.dart +++ b/packages/camera/camera_android_camerax/lib/src/preview.dart @@ -20,21 +20,21 @@ class Preview extends UseCase { Preview( {BinaryMessenger? binaryMessenger, InstanceManager? instanceManager, - this.targetRotation, + this.initialTargetRotation, this.resolutionSelector}) : super.detached( binaryMessenger: binaryMessenger, instanceManager: instanceManager) { _api = PreviewHostApiImpl( binaryMessenger: binaryMessenger, instanceManager: instanceManager); - _api.createFromInstance(this, targetRotation, resolutionSelector); + _api.createFromInstance(this, initialTargetRotation, resolutionSelector); } /// Constructs a [Preview] that is not automatically attached to a native object. Preview.detached( {BinaryMessenger? binaryMessenger, InstanceManager? instanceManager, - this.targetRotation, + this.initialTargetRotation, this.resolutionSelector}) : super.detached( binaryMessenger: binaryMessenger, @@ -46,7 +46,13 @@ class Preview extends UseCase { late final PreviewHostApiImpl _api; /// Target rotation of the camera used for the preview stream. - final int? targetRotation; + /// + /// Should be specified in terms of one of the [Surface] + /// rotation constants that represents the counter-clockwise degrees of + /// rotation relative to [DeviceOrientation.portraitUp]. + /// + // TODO(camsim99): Remove this parameter. https://github.com/flutter/flutter/issues/140664 + final int? initialTargetRotation; /// Target resolution of the camera preview stream. /// @@ -54,6 +60,14 @@ class Preview extends UseCase { /// https://developer.android.com/reference/androidx/camera/core/Preview.Builder#setResolutionSelector(androidx.camera.core.resolutionselector.ResolutionSelector). final ResolutionSelector? resolutionSelector; + /// Dynamically sets the target rotation of this instance. + /// + /// [rotation] should be specified in terms of one of the [Surface] + /// rotation constants that represents the counter-clockwise degrees of + /// rotation relative to [DeviceOrientation.portraitUp]. + Future setTargetRotation(int rotation) => + _api.setTargetRotationFromInstances(this, rotation); + /// Sets the surface provider for the preview stream. /// /// Returns the ID of the FlutterSurfaceTextureEntry used on the native end @@ -103,7 +117,7 @@ class PreviewHostApiImpl extends PreviewHostApi { return Preview.detached( binaryMessenger: binaryMessenger, instanceManager: instanceManager, - targetRotation: original.targetRotation, + initialTargetRotation: original.initialTargetRotation, resolutionSelector: original.resolutionSelector); }); create( @@ -114,6 +128,12 @@ class PreviewHostApiImpl extends PreviewHostApi { : instanceManager.getIdentifier(resolutionSelector)); } + /// Dynamically sets the target rotation of [instance] to [rotation]. + Future setTargetRotationFromInstances(Preview instance, int rotation) { + return setTargetRotation( + instanceManager.getIdentifier(instance)!, rotation); + } + /// Sets the surface provider of the specified [Preview] instance and returns /// the ID corresponding to the surface it will provide. Future setSurfaceProviderFromInstance(Preview instance) async { diff --git a/packages/camera/camera_android_camerax/lib/src/system_services.dart b/packages/camera/camera_android_camerax/lib/src/system_services.dart index a2513e037662..b75a1cb98035 100644 --- a/packages/camera/camera_android_camerax/lib/src/system_services.dart +++ b/packages/camera/camera_android_camerax/lib/src/system_services.dart @@ -5,10 +5,9 @@ import 'dart:async'; import 'package:camera_platform_interface/camera_platform_interface.dart' - show CameraException, DeviceOrientationChangedEvent; + show CameraException; import 'package:flutter/services.dart'; -import 'android_camera_camerax_flutter_api_impls.dart'; import 'camerax_library.g.dart'; // Ignoring lint indicating this class only contains static members @@ -18,14 +17,6 @@ import 'camerax_library.g.dart'; /// Utility class that offers access to Android system services needed for /// camera usage and other informational streams. class SystemServices { - /// Stream that emits the device orientation whenever it is changed. - /// - /// Values may start being added to the stream once - /// `startListeningForDeviceOrientationChange(...)` is called. - static final StreamController - deviceOrientationChangedStreamController = - StreamController.broadcast(); - /// Stream that emits the errors caused by camera usage on the native side. static final StreamController cameraErrorStreamController = StreamController.broadcast(); @@ -39,29 +30,6 @@ class SystemServices { return api.sendCameraPermissionsRequest(enableAudio); } - /// Requests that [deviceOrientationChangedStreamController] start - /// emitting values for any change in device orientation. - static void startListeningForDeviceOrientationChange( - bool isFrontFacing, int sensorOrientation, - {BinaryMessenger? binaryMessenger}) { - AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); - final SystemServicesHostApi api = - SystemServicesHostApi(binaryMessenger: binaryMessenger); - - api.startListeningForDeviceOrientationChange( - isFrontFacing, sensorOrientation); - } - - /// Stops the [deviceOrientationChangedStreamController] from emitting values - /// for changes in device orientation. - static void stopListeningForDeviceOrientationChange( - {BinaryMessenger? binaryMessenger}) { - final SystemServicesHostApi api = - SystemServicesHostApi(binaryMessenger: binaryMessenger); - - api.stopListeningForDeviceOrientationChange(); - } - /// Returns a file path which was used to create a temporary file. /// Prefix is a part of the file name, and suffix is the file extension. /// @@ -116,37 +84,6 @@ class SystemServicesFlutterApiImpl implements SystemServicesFlutterApi { /// Constructs an [SystemServicesFlutterApiImpl]. SystemServicesFlutterApiImpl(); - /// Callback method for any changes in device orientation. - /// - /// Will only be called if - /// `SystemServices.startListeningForDeviceOrientationChange(...)` was called - /// to start listening for device orientation updates. - @override - void onDeviceOrientationChanged(String orientation) { - final DeviceOrientation deviceOrientation = - deserializeDeviceOrientation(orientation); - SystemServices.deviceOrientationChangedStreamController - .add(DeviceOrientationChangedEvent(deviceOrientation)); - } - - /// Deserializes device orientation in [String] format into a - /// [DeviceOrientation]. - DeviceOrientation deserializeDeviceOrientation(String orientation) { - switch (orientation) { - case 'LANDSCAPE_LEFT': - return DeviceOrientation.landscapeLeft; - case 'LANDSCAPE_RIGHT': - return DeviceOrientation.landscapeRight; - case 'PORTRAIT_DOWN': - return DeviceOrientation.portraitDown; - case 'PORTRAIT_UP': - return DeviceOrientation.portraitUp; - default: - throw ArgumentError( - '"$orientation" is not a valid DeviceOrientation value'); - } - } - /// Callback method for any errors caused by camera usage on the Java side. @override void onCameraError(String errorDescription) { diff --git a/packages/camera/camera_android_camerax/lib/src/video_capture.dart b/packages/camera/camera_android_camerax/lib/src/video_capture.dart index bb657cffc4b4..c3d4403a6ca7 100644 --- a/packages/camera/camera_android_camerax/lib/src/video_capture.dart +++ b/packages/camera/camera_android_camerax/lib/src/video_capture.dart @@ -17,7 +17,8 @@ import 'use_case.dart'; /// See https://developer.android.com/reference/androidx/camera/video/VideoCapture. @immutable class VideoCapture extends UseCase { - /// Creates a VideoCapture that is not automatically attached to a native object. + /// Creates a [VideoCapture] that is not automatically attached to a native + /// object. VideoCapture.detached( {BinaryMessenger? binaryMessenger, InstanceManager? instanceManager}) : super.detached( @@ -28,6 +29,8 @@ class VideoCapture extends UseCase { AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); } + late final VideoCaptureHostApiImpl _api; + /// Creates a [VideoCapture] associated with the given [Recorder]. static Future withOutput(Recorder recorder, {BinaryMessenger? binaryMessenger, InstanceManager? instanceManager}) { @@ -38,12 +41,18 @@ class VideoCapture extends UseCase { return api.withOutputFromInstance(recorder); } + /// Dynamically sets the target rotation of this instance. + /// + /// [rotation] should be specified in terms of one of the [Surface] + /// rotation constants that represents the counter-clockwise degrees of + /// rotation relative to [DeviceOrientation.portraitUp]. + Future setTargetRotation(int rotation) => + _api.setTargetRotationFromInstances(this, rotation); + /// Gets the [Recorder] associated with this VideoCapture. Future getOutput() { return _api.getOutputFromInstance(this); } - - late final VideoCaptureHostApiImpl _api; } /// Host API implementation of [VideoCapture]. @@ -76,6 +85,13 @@ class VideoCaptureHostApiImpl extends VideoCaptureHostApi { .getInstanceWithWeakReference(videoCaptureId)!; } + /// Dynamically sets the target rotation of [instance] to [rotation]. + Future setTargetRotationFromInstances( + VideoCapture instance, int rotation) { + return setTargetRotation( + instanceManager.getIdentifier(instance)!, rotation); + } + /// Gets the [Recorder] associated with the provided [VideoCapture] instance. Future getOutputFromInstance(VideoCapture instance) async { final int? identifier = instanceManager.getIdentifier(instance); diff --git a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart index 22827a803eb9..94285d148df4 100644 --- a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart +++ b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart @@ -211,19 +211,27 @@ abstract class SystemServicesHostApi { @async CameraPermissionsErrorData? requestCameraPermissions(bool enableAudio); + String getTempFilePath(String prefix, String suffix); +} + +@FlutterApi() +abstract class SystemServicesFlutterApi { + void onCameraError(String errorDescription); +} + +@HostApi(dartHostTestHandler: 'TestDeviceOrientationManagerHostApi') +abstract class DeviceOrientationManagerHostApi { void startListeningForDeviceOrientationChange( bool isFrontFacing, int sensorOrientation); void stopListeningForDeviceOrientationChange(); - String getTempFilePath(String prefix, String suffix); + int getDefaultDisplayRotation(); } @FlutterApi() -abstract class SystemServicesFlutterApi { +abstract class DeviceOrientationManagerFlutterApi { void onDeviceOrientationChanged(String orientation); - - void onCameraError(String errorDescription); } @HostApi(dartHostTestHandler: 'TestPreviewHostApi') @@ -235,6 +243,8 @@ abstract class PreviewHostApi { void releaseFlutterSurfaceTexture(); ResolutionInfo getResolutionInfo(int identifier); + + void setTargetRotation(int identifier, int rotation); } @HostApi(dartHostTestHandler: 'TestVideoCaptureHostApi') @@ -242,6 +252,8 @@ abstract class VideoCaptureHostApi { int withOutput(int videoOutputId); int getOutput(int identifier); + + void setTargetRotation(int identifier, int rotation); } @FlutterApi() @@ -294,12 +306,15 @@ abstract class RecordingFlutterApi { @HostApi(dartHostTestHandler: 'TestImageCaptureHostApi') abstract class ImageCaptureHostApi { - void create(int identifier, int? flashMode, int? resolutionSelectorId); + void create(int identifier, int? targetRotation, int? flashMode, + int? resolutionSelectorId); void setFlashMode(int identifier, int flashMode); @async String takePicture(int identifier); + + void setTargetRotation(int identifier, int rotation); } @HostApi(dartHostTestHandler: 'TestResolutionStrategyHostApi') @@ -341,11 +356,13 @@ abstract class ZoomStateFlutterApi { @HostApi(dartHostTestHandler: 'TestImageAnalysisHostApi') abstract class ImageAnalysisHostApi { - void create(int identifier, int? resolutionSelectorId); + void create(int identifier, int? targetRotation, int? resolutionSelectorId); void setAnalyzer(int identifier, int analyzerIdentifier); void clearAnalyzer(int identifier); + + void setTargetRotation(int identifier, int rotation); } @HostApi(dartHostTestHandler: 'TestAnalyzerHostApi') diff --git a/packages/camera/camera_android_camerax/pubspec.yaml b/packages/camera/camera_android_camerax/pubspec.yaml index 2cf3ac3b5d11..e4332e9e54e2 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+24 +version: 0.5.0+25 environment: sdk: ">=3.0.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 100d3040eb9b..2dbda1bba546 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 @@ -15,6 +15,7 @@ import 'package:camera_android_camerax/src/camera_state.dart'; import 'package:camera_android_camerax/src/camera_state_error.dart'; import 'package:camera_android_camerax/src/camerax_library.g.dart'; import 'package:camera_android_camerax/src/camerax_proxy.dart'; +import 'package:camera_android_camerax/src/device_orientation_manager.dart'; import 'package:camera_android_camerax/src/exposure_state.dart'; import 'package:camera_android_camerax/src/fallback_strategy.dart'; import 'package:camera_android_camerax/src/image_analysis.dart'; @@ -31,6 +32,7 @@ import 'package:camera_android_camerax/src/recorder.dart'; import 'package:camera_android_camerax/src/recording.dart'; import 'package:camera_android_camerax/src/resolution_selector.dart'; import 'package:camera_android_camerax/src/resolution_strategy.dart'; +import 'package:camera_android_camerax/src/surface.dart'; import 'package:camera_android_camerax/src/system_services.dart'; import 'package:camera_android_camerax/src/use_case.dart'; import 'package:camera_android_camerax/src/video_capture.dart'; @@ -229,14 +231,11 @@ void main() { return mockBackCameraSelector; } }, - createPreview: ( - {required int targetRotation, - ResolutionSelector? resolutionSelector}) => - mockPreview, - createImageCapture: (_) => mockImageCapture, + createPreview: (_, __) => mockPreview, + createImageCapture: (_, __) => mockImageCapture, createRecorder: (_) => mockRecorder, createVideoCapture: (_) => Future.value(mockVideoCapture), - createImageAnalysis: (_) => mockImageAnalysis, + createImageAnalysis: (_, __) => mockImageAnalysis, createResolutionStrategy: ( {bool highestAvailable = false, Size? boundSize, @@ -347,14 +346,11 @@ void main() { return mockBackCameraSelector; } }, - createPreview: ( - {required int targetRotation, - ResolutionSelector? resolutionSelector}) => - mockPreview, - createImageCapture: (_) => mockImageCapture, + createPreview: (_, __) => mockPreview, + createImageCapture: (_, __) => mockImageCapture, createRecorder: (_) => mockRecorder, createVideoCapture: (_) => Future.value(mockVideoCapture), - createImageAnalysis: (_) => mockImageAnalysis, + createImageAnalysis: (_, __) => mockImageAnalysis, createResolutionStrategy: ( {bool highestAvailable = false, Size? boundSize, @@ -431,18 +427,23 @@ void main() { return mockBackCameraSelector; } }, - createPreview: ( - {required int targetRotation, - ResolutionSelector? resolutionSelector}) => - Preview.detached( - targetRotation: targetRotation, - resolutionSelector: resolutionSelector), - createImageCapture: (ResolutionSelector? resolutionSelector) => - ImageCapture.detached(resolutionSelector: resolutionSelector), + createPreview: + (ResolutionSelector? resolutionSelector, int? targetRotation) => + Preview.detached( + initialTargetRotation: targetRotation, + resolutionSelector: resolutionSelector), + createImageCapture: + (ResolutionSelector? resolutionSelector, int? targetRotation) => + ImageCapture.detached( + resolutionSelector: resolutionSelector, + initialTargetRotation: targetRotation), createRecorder: (_) => mockRecorder, createVideoCapture: (_) => Future.value(mockVideoCapture), - createImageAnalysis: (ResolutionSelector? resolutionSelector) => - ImageAnalysis.detached(resolutionSelector: resolutionSelector), + createImageAnalysis: + (ResolutionSelector? resolutionSelector, int? targetRotation) => + ImageAnalysis.detached( + resolutionSelector: resolutionSelector, + initialTargetRotation: targetRotation), createResolutionStrategy: ( {bool highestAvailable = false, Size? boundSize, int? fallbackRule}) { if (highestAvailable) { @@ -581,15 +582,12 @@ void main() { return mockBackCameraSelector; } }, - createPreview: ( - {required int targetRotation, - ResolutionSelector? resolutionSelector}) => - mockPreview, - createImageCapture: (_) => mockImageCapture, + createPreview: (_, __) => mockPreview, + createImageCapture: (_, __) => mockImageCapture, createRecorder: (QualitySelector? qualitySelector) => Recorder.detached(qualitySelector: qualitySelector), createVideoCapture: (_) => Future.value(mockVideoCapture), - createImageAnalysis: (_) => mockImageAnalysis, + createImageAnalysis: (_, __) => mockImageAnalysis, createResolutionStrategy: ( {bool highestAvailable = false, Size? boundSize, @@ -716,14 +714,11 @@ void main() { return mockBackCameraSelector; } }, - createPreview: ( - {required int targetRotation, - ResolutionSelector? resolutionSelector}) => - mockPreview, - createImageCapture: (_) => mockImageCapture, + createPreview: (_, __) => mockPreview, + createImageCapture: (_, __) => mockImageCapture, createRecorder: (QualitySelector? qualitySelector) => MockRecorder(), createVideoCapture: (_) => Future.value(MockVideoCapture()), - createImageAnalysis: (_) => mockImageAnalysis, + createImageAnalysis: (_, __) => mockImageAnalysis, createResolutionStrategy: ( {bool highestAvailable = false, Size? boundSize, @@ -870,7 +865,8 @@ void main() { const DeviceOrientationChangedEvent testEvent = DeviceOrientationChangedEvent(DeviceOrientation.portraitDown); - SystemServices.deviceOrientationChangedStreamController.add(testEvent); + DeviceOrientationManager.deviceOrientationChangedStreamController + .add(testEvent); expect(await streamQueue.next, testEvent); await streamQueue.cancel(); @@ -1084,6 +1080,9 @@ void main() { camera.videoCapture = MockVideoCapture(); camera.cameraSelector = MockCameraSelector(); + // Ignore setting target rotation for this test; tested seprately. + camera.captureOrientationLocked = true; + const int cameraId = 17; const String outputPath = '/temp/MOV123.temp'; @@ -1124,6 +1123,9 @@ void main() { camera.videoCapture = MockVideoCapture(); camera.cameraSelector = MockCameraSelector(); + // Ignore setting target rotation for this test; tested seprately. + camera.captureOrientationLocked = true; + const int cameraId = 17; const String outputPath = '/temp/MOV123.temp'; @@ -1178,6 +1180,9 @@ void main() { MockTestSystemServicesHostApi(); TestSystemServicesHostApi.setup(mockSystemServicesApi); + // Ignore setting target rotation for this test; tested seprately. + camera.captureOrientationLocked = true; + // Tell plugin to create detached Analyzer for testing. camera.proxy = CameraXProxy( createAnalyzer: @@ -1214,6 +1219,73 @@ void main() { await camera.cameraImageDataStreamController!.close(); }); + test( + 'startVideoCapturing sets VideoCapture target rotation to current video orientation if orientation unlocked', + () async { + // Set up mocks and constants. + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + final MockPendingRecording mockPendingRecording = MockPendingRecording(); + final MockRecording mockRecording = MockRecording(); + final MockVideoCapture mockVideoCapture = MockVideoCapture(); + final TestSystemServicesHostApi mockSystemServicesApi = + MockTestSystemServicesHostApi(); + TestSystemServicesHostApi.setup(mockSystemServicesApi); + const int defaultTargetRotation = Surface.ROTATION_270; + + // Set directly for test versus calling createCamera. + camera.processCameraProvider = MockProcessCameraProvider(); + camera.camera = MockCamera(); + camera.recorder = MockRecorder(); + camera.videoCapture = mockVideoCapture; + camera.cameraSelector = MockCameraSelector(); + + // Tell plugin to mock call to get current video orientation. + camera.proxy = CameraXProxy( + getDefaultDisplayRotation: () => + Future.value(defaultTargetRotation)); + + const int cameraId = 87; + const String outputPath = '/temp/MOV123.temp'; + + // Mock method calls. + when(mockSystemServicesApi.getTempFilePath(camera.videoPrefix, '.temp')) + .thenReturn(outputPath); + when(camera.recorder!.prepareRecording(outputPath)) + .thenAnswer((_) async => mockPendingRecording); + when(mockPendingRecording.start()).thenAnswer((_) async => mockRecording); + when(camera.processCameraProvider!.isBound(camera.videoCapture!)) + .thenAnswer((_) async => true); + + // Orientation is unlocked and plugin does not need to set default target + // rotation manually. + camera.recording = null; + await camera.startVideoCapturing(const VideoCaptureOptions(cameraId)); + verifyNever(mockVideoCapture.setTargetRotation(any)); + + // Orientation is locked and plugin does not need to set default target + // rotation manually. + camera.recording = null; + camera.captureOrientationLocked = true; + await camera.startVideoCapturing(const VideoCaptureOptions(cameraId)); + verifyNever(mockVideoCapture.setTargetRotation(any)); + + // Orientation is locked and plugin does need to set default target + // rotation manually. + camera.recording = null; + camera.captureOrientationLocked = true; + camera.shouldSetDefaultRotation = true; + await camera.startVideoCapturing(const VideoCaptureOptions(cameraId)); + verifyNever(mockVideoCapture.setTargetRotation(any)); + + // Orientation is unlocked and plugin does need to set default target + // rotation manually. + camera.recording = null; + camera.captureOrientationLocked = false; + camera.shouldSetDefaultRotation = true; + await camera.startVideoCapturing(const VideoCaptureOptions(cameraId)); + verify(mockVideoCapture.setTargetRotation(defaultTargetRotation)); + }); + test('pauseVideoRecording pauses the recording', () async { final AndroidCameraCameraX camera = AndroidCameraCameraX(); final MockRecording recording = MockRecording(); @@ -1321,6 +1393,9 @@ void main() { // Set directly for test versus calling createCamera. camera.imageCapture = MockImageCapture(); + // Ignore setting target rotation for this test; tested seprately. + camera.captureOrientationLocked = true; + when(camera.imageCapture!.takePicture()) .thenAnswer((_) async => testPicturePath); @@ -1329,6 +1404,51 @@ void main() { expect(imageFile.path, equals(testPicturePath)); }); + test( + 'takePicture sets ImageCapture target rotation to currrent photo rotation when orientation unlocked', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + final MockImageCapture mockImageCapture = MockImageCapture(); + const int cameraId = 3; + const int defaultTargetRotation = Surface.ROTATION_180; + + // Set directly for test versus calling createCamera. + camera.imageCapture = mockImageCapture; + + // Tell plugin to mock call to get current photo orientation. + camera.proxy = CameraXProxy( + getDefaultDisplayRotation: () => + Future.value(defaultTargetRotation)); + + when(camera.imageCapture!.takePicture()) + .thenAnswer((_) async => 'test/absolute/path/to/picture'); + + // Orientation is unlocked and plugin does not need to set default target + // rotation manually. + await camera.takePicture(cameraId); + verifyNever(mockImageCapture.setTargetRotation(any)); + + // Orientation is locked and plugin does not need to set default target + // rotation manually. + camera.captureOrientationLocked = true; + await camera.takePicture(cameraId); + verifyNever(mockImageCapture.setTargetRotation(any)); + + // Orientation is locked and plugin does need to set default target + // rotation manually. + camera.captureOrientationLocked = true; + camera.shouldSetDefaultRotation = true; + await camera.takePicture(cameraId); + verifyNever(mockImageCapture.setTargetRotation(any)); + + // Orientation is unlocked and plugin does need to set default target + // rotation manually. + camera.captureOrientationLocked = false; + camera.shouldSetDefaultRotation = true; + await camera.takePicture(cameraId); + verify(mockImageCapture.setTargetRotation(defaultTargetRotation)); + }); + test('takePicture turns non-torch flash mode off when torch mode enabled', () async { final AndroidCameraCameraX camera = AndroidCameraCameraX(); @@ -1339,6 +1459,9 @@ void main() { camera.imageCapture = MockImageCapture(); camera.camera = MockCamera(); + // Ignore setting target rotation for this test; tested seprately. + camera.captureOrientationLocked = true; + when(camera.camera!.getCameraControl()) .thenAnswer((_) async => mockCameraControl); @@ -1358,6 +1481,9 @@ void main() { camera.imageCapture = MockImageCapture(); camera.camera = MockCamera(); + // Ignore setting target rotation for this test; tested seprately. + camera.captureOrientationLocked = true; + when(camera.camera!.getCameraControl()) .thenAnswer((_) async => mockCameraControl); @@ -1568,6 +1694,9 @@ void main() { camera.cameraSelector = MockCameraSelector(); camera.imageAnalysis = MockImageAnalysis(); + // Ignore setting target rotation for this test; tested seprately. + camera.captureOrientationLocked = true; + when(mockProcessCameraProvider.bindToLifecycle(any, any)) .thenAnswer((_) => Future.value(mockCamera)); when(mockCamera.getCameraInfo()) @@ -1608,6 +1737,9 @@ void main() { camera.cameraSelector = MockCameraSelector(); camera.imageAnalysis = MockImageAnalysis(); + // Ignore setting target rotation for this test; tested seprately. + camera.captureOrientationLocked = true; + when(mockProcessCameraProvider.bindToLifecycle(any, any)) .thenAnswer((_) => Future.value(mockCamera)); when(mockCamera.getCameraInfo()) @@ -1669,6 +1801,9 @@ void main() { camera.cameraSelector = mockCameraSelector; camera.imageAnalysis = mockImageAnalysis; + // Ignore setting target rotation for this test; tested seprately. + camera.captureOrientationLocked = true; + when(mockProcessCameraProvider.isBound(mockImageAnalysis)) .thenAnswer((_) async => Future.value(false)); when(mockProcessCameraProvider @@ -1725,6 +1860,12 @@ void main() { // Set directly for test versus calling createCamera. camera.imageAnalysis = mockImageAnalysis; + // Ignore setting target rotation for this test; tested seprately. + camera.captureOrientationLocked = true; + + // Tell plugin to create a detached analyzer for testing purposes. + camera.proxy = CameraXProxy(createAnalyzer: (_) => MockAnalyzer()); + final StreamSubscription imageStreamSubscription = camera .onStreamedFrameAvailable(cameraId) .listen((CameraImageData data) {}); @@ -1733,4 +1874,117 @@ void main() { verify(mockImageAnalysis.clearAnalyzer()); }); + + test( + 'onStreamedFrameAvailable sets ImageAnalysis target rotation to current photo orientation when orientation unlocked', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int cameraId = 35; + const int defaultTargetRotation = Surface.ROTATION_90; + final MockImageAnalysis mockImageAnalysis = MockImageAnalysis(); + + // Set directly for test versus calling createCamera. + camera.imageAnalysis = mockImageAnalysis; + + // Tell plugin to create a detached analyzer for testing purposes and mock + // call to get current photo orientation. + camera.proxy = CameraXProxy( + createAnalyzer: (_) => MockAnalyzer(), + getDefaultDisplayRotation: () => + Future.value(defaultTargetRotation)); + + // Orientation is unlocked and plugin does not need to set default target + // rotation manually. + StreamSubscription imageStreamSubscription = camera + .onStreamedFrameAvailable(cameraId) + .listen((CameraImageData data) {}); + await untilCalled(mockImageAnalysis.setAnalyzer(any)); + verifyNever(mockImageAnalysis.setTargetRotation(any)); + await imageStreamSubscription.cancel(); + + // Orientation is locked and plugin does not need to set default target + // rotation manually. + camera.captureOrientationLocked = true; + imageStreamSubscription = camera + .onStreamedFrameAvailable(cameraId) + .listen((CameraImageData data) {}); + await untilCalled(mockImageAnalysis.setAnalyzer(any)); + verifyNever(mockImageAnalysis.setTargetRotation(any)); + await imageStreamSubscription.cancel(); + + // Orientation is locked and plugin does need to set default target + // rotation manually. + camera.captureOrientationLocked = true; + camera.shouldSetDefaultRotation = true; + imageStreamSubscription = camera + .onStreamedFrameAvailable(cameraId) + .listen((CameraImageData data) {}); + await untilCalled(mockImageAnalysis.setAnalyzer(any)); + verifyNever(mockImageAnalysis.setTargetRotation(any)); + await imageStreamSubscription.cancel(); + + // Orientation is unlocked and plugin does need to set default target + // rotation manually. + camera.captureOrientationLocked = false; + camera.shouldSetDefaultRotation = true; + imageStreamSubscription = camera + .onStreamedFrameAvailable(cameraId) + .listen((CameraImageData data) {}); + await untilCalled( + mockImageAnalysis.setTargetRotation(defaultTargetRotation)); + await imageStreamSubscription.cancel(); + }); + + test( + 'lockCaptureOrientation sets capture-related use case target rotations to correct orientation', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int cameraId = 44; + + final MockImageAnalysis mockImageAnalysis = MockImageAnalysis(); + final MockImageCapture mockImageCapture = MockImageCapture(); + final MockVideoCapture mockVideoCapture = MockVideoCapture(); + + // Set directly for test versus calling createCamera. + camera.imageAnalysis = mockImageAnalysis; + camera.imageCapture = mockImageCapture; + camera.videoCapture = mockVideoCapture; + + for (final DeviceOrientation orientation in DeviceOrientation.values) { + int? expectedTargetRotation; + switch (orientation) { + case DeviceOrientation.portraitUp: + expectedTargetRotation = Surface.ROTATION_0; + case DeviceOrientation.landscapeLeft: + expectedTargetRotation = Surface.ROTATION_90; + case DeviceOrientation.portraitDown: + expectedTargetRotation = Surface.ROTATION_180; + case DeviceOrientation.landscapeRight: + expectedTargetRotation = Surface.ROTATION_270; + } + + await camera.lockCaptureOrientation(cameraId, orientation); + + verify(mockImageAnalysis.setTargetRotation(expectedTargetRotation)); + verify(mockImageCapture.setTargetRotation(expectedTargetRotation)); + verify(mockVideoCapture.setTargetRotation(expectedTargetRotation)); + expect(camera.captureOrientationLocked, isTrue); + expect(camera.shouldSetDefaultRotation, isTrue); + + // Reset flags for testing. + camera.captureOrientationLocked = false; + camera.shouldSetDefaultRotation = false; + } + }); + + test( + 'unlockCaptureOrientation sets capture-related use case target rotations to current photo/video orientation', + () async { + final AndroidCameraCameraX camera = AndroidCameraCameraX(); + const int cameraId = 57; + + camera.captureOrientationLocked = true; + await camera.unlockCaptureOrientation(cameraId); + expect(camera.captureOrientationLocked, isFalse); + }); } 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 6dfdd2b4824c..3657cb8ec118 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 @@ -516,6 +516,16 @@ class MockFallbackStrategy extends _i1.Mock implements _i21.FallbackStrategy { /// See the documentation for Mockito's code generation for more information. // ignore: must_be_immutable class MockImageAnalysis extends _i1.Mock implements _i22.ImageAnalysis { + @override + _i16.Future setTargetRotation(int? rotation) => (super.noSuchMethod( + Invocation.method( + #setTargetRotation, + [rotation], + ), + returnValue: _i16.Future.value(), + returnValueForMissingStub: _i16.Future.value(), + ) as _i16.Future); + @override _i16.Future setAnalyzer(_i15.Analyzer? analyzer) => (super.noSuchMethod( Invocation.method( @@ -542,6 +552,16 @@ class MockImageAnalysis extends _i1.Mock implements _i22.ImageAnalysis { /// See the documentation for Mockito's code generation for more information. // ignore: must_be_immutable class MockImageCapture extends _i1.Mock implements _i23.ImageCapture { + @override + _i16.Future setTargetRotation(int? rotation) => (super.noSuchMethod( + Invocation.method( + #setTargetRotation, + [rotation], + ), + returnValue: _i16.Future.value(), + returnValueForMissingStub: _i16.Future.value(), + ) as _i16.Future); + @override _i16.Future setFlashMode(int? newFlashMode) => (super.noSuchMethod( Invocation.method( @@ -708,6 +728,16 @@ class MockPlaneProxy extends _i1.Mock implements _i25.PlaneProxy { /// See the documentation for Mockito's code generation for more information. // ignore: must_be_immutable class MockPreview extends _i1.Mock implements _i28.Preview { + @override + _i16.Future setTargetRotation(int? rotation) => (super.noSuchMethod( + Invocation.method( + #setTargetRotation, + [rotation], + ), + returnValue: _i16.Future.value(), + returnValueForMissingStub: _i16.Future.value(), + ) as _i16.Future); + @override _i16.Future setSurfaceProvider() => (super.noSuchMethod( Invocation.method( @@ -944,6 +974,16 @@ class MockRecording extends _i1.Mock implements _i8.Recording { /// See the documentation for Mockito's code generation for more information. // ignore: must_be_immutable class MockVideoCapture extends _i1.Mock implements _i34.VideoCapture { + @override + _i16.Future setTargetRotation(int? rotation) => (super.noSuchMethod( + Invocation.method( + #setTargetRotation, + [rotation], + ), + returnValue: _i16.Future.value(), + returnValueForMissingStub: _i16.Future.value(), + ) as _i16.Future); + @override _i16.Future<_i11.Recorder> getOutput() => (super.noSuchMethod( Invocation.method( @@ -1185,31 +1225,6 @@ class MockTestSystemServicesHostApi extends _i1.Mock _i16.Future<_i7.CameraPermissionsErrorData?>.value(), ) as _i16.Future<_i7.CameraPermissionsErrorData?>); - @override - void startListeningForDeviceOrientationChange( - bool? isFrontFacing, - int? sensorOrientation, - ) => - super.noSuchMethod( - Invocation.method( - #startListeningForDeviceOrientationChange, - [ - isFrontFacing, - sensorOrientation, - ], - ), - returnValueForMissingStub: null, - ); - - @override - void stopListeningForDeviceOrientationChange() => super.noSuchMethod( - Invocation.method( - #stopListeningForDeviceOrientationChange, - [], - ), - returnValueForMissingStub: null, - ); - @override String getTempFilePath( String? prefix, diff --git a/packages/camera/camera_android_camerax/test/device_orientation_manager_test.dart b/packages/camera/camera_android_camerax/test/device_orientation_manager_test.dart new file mode 100644 index 000000000000..145ceeb278a5 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/device_orientation_manager_test.dart @@ -0,0 +1,85 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:camera_android_camerax/src/device_orientation_manager.dart'; +import 'package:camera_android_camerax/src/surface.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart' + show DeviceOrientationChangedEvent; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'device_orientation_manager_test.mocks.dart'; +import 'test_camerax_library.g.dart'; + +@GenerateMocks( + [TestInstanceManagerHostApi, TestDeviceOrientationManagerHostApi]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + // Mocks the call to clear the native InstanceManager. + TestInstanceManagerHostApi.setup(MockTestInstanceManagerHostApi()); + + group('DeviceOrientationManager', () { + tearDown(() => TestProcessCameraProviderHostApi.setup(null)); + + test( + 'startListeningForDeviceOrientationChange makes request to start listening for new device orientations', + () async { + final MockTestDeviceOrientationManagerHostApi mockApi = + MockTestDeviceOrientationManagerHostApi(); + TestDeviceOrientationManagerHostApi.setup(mockApi); + + DeviceOrientationManager.startListeningForDeviceOrientationChange( + true, 90); + verify(mockApi.startListeningForDeviceOrientationChange(true, 90)); + }); + + test( + 'stopListeningForDeviceOrientationChange makes request to stop listening for new device orientations', + () async { + final MockTestDeviceOrientationManagerHostApi mockApi = + MockTestDeviceOrientationManagerHostApi(); + TestDeviceOrientationManagerHostApi.setup(mockApi); + + DeviceOrientationManager.stopListeningForDeviceOrientationChange(); + verify(mockApi.stopListeningForDeviceOrientationChange()); + }); + + test('getDefaultDisplayRotation retrieves expected rotation', () async { + final MockTestDeviceOrientationManagerHostApi mockApi = + MockTestDeviceOrientationManagerHostApi(); + TestDeviceOrientationManagerHostApi.setup(mockApi); + const int expectedRotation = Surface.ROTATION_180; + + when(mockApi.getDefaultDisplayRotation()).thenReturn(expectedRotation); + + expect(await DeviceOrientationManager.getDefaultDisplayRotation(), + equals(expectedRotation)); + verify(mockApi.getDefaultDisplayRotation()); + }); + + test('onDeviceOrientationChanged adds new orientation to stream', () { + DeviceOrientationManager.deviceOrientationChangedStreamController.stream + .listen((DeviceOrientationChangedEvent event) { + expect(event.orientation, equals(DeviceOrientation.landscapeLeft)); + }); + DeviceOrientationManagerFlutterApiImpl() + .onDeviceOrientationChanged('LANDSCAPE_LEFT'); + }); + + test( + 'onDeviceOrientationChanged throws error if new orientation is invalid', + () { + expect( + () => DeviceOrientationManagerFlutterApiImpl() + .onDeviceOrientationChanged('FAKE_ORIENTATION'), + throwsA(isA().having( + (ArgumentError e) => e.message, + 'message', + '"FAKE_ORIENTATION" is not a valid DeviceOrientation value'))); + }); + }); +} diff --git a/packages/camera/camera_android_camerax/test/device_orientation_manager_test.mocks.dart b/packages/camera/camera_android_camerax/test/device_orientation_manager_test.mocks.dart new file mode 100644 index 000000000000..840125ccc789 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/device_orientation_manager_test.mocks.dart @@ -0,0 +1,84 @@ +// Mocks generated by Mockito 5.4.3 from annotations +// in camera_android_camerax/test/device_orientation_manager_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'package:mockito/mockito.dart' as _i1; + +import 'test_camerax_library.g.dart' as _i2; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +/// A class which mocks [TestInstanceManagerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestInstanceManagerHostApi extends _i1.Mock + implements _i2.TestInstanceManagerHostApi { + MockTestInstanceManagerHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void clear() => super.noSuchMethod( + Invocation.method( + #clear, + [], + ), + returnValueForMissingStub: null, + ); +} + +/// A class which mocks [TestDeviceOrientationManagerHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestDeviceOrientationManagerHostApi extends _i1.Mock + implements _i2.TestDeviceOrientationManagerHostApi { + MockTestDeviceOrientationManagerHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + void startListeningForDeviceOrientationChange( + bool? isFrontFacing, + int? sensorOrientation, + ) => + super.noSuchMethod( + Invocation.method( + #startListeningForDeviceOrientationChange, + [ + isFrontFacing, + sensorOrientation, + ], + ), + returnValueForMissingStub: null, + ); + + @override + void stopListeningForDeviceOrientationChange() => super.noSuchMethod( + Invocation.method( + #stopListeningForDeviceOrientationChange, + [], + ), + returnValueForMissingStub: null, + ); + + @override + int getDefaultDisplayRotation() => (super.noSuchMethod( + Invocation.method( + #getDefaultDisplayRotation, + [], + ), + returnValue: 0, + ) as int); +} diff --git a/packages/camera/camera_android_camerax/test/image_analysis_test.dart b/packages/camera/camera_android_camerax/test/image_analysis_test.dart index 80acd655982e..1db792cf6d37 100644 --- a/packages/camera/camera_android_camerax/test/image_analysis_test.dart +++ b/packages/camera/camera_android_camerax/test/image_analysis_test.dart @@ -7,6 +7,7 @@ import 'package:camera_android_camerax/src/image_analysis.dart'; import 'package:camera_android_camerax/src/image_proxy.dart'; import 'package:camera_android_camerax/src/instance_manager.dart'; import 'package:camera_android_camerax/src/resolution_selector.dart'; +import 'package:camera_android_camerax/src/surface.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; @@ -30,6 +31,24 @@ void main() { TestImageAnalysisHostApi.setup(null); }); + test('detached create does not call create on the Java side', () { + final MockTestImageAnalysisHostApi mockApi = + MockTestImageAnalysisHostApi(); + TestImageAnalysisHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + + ImageAnalysis.detached( + initialTargetRotation: Surface.ROTATION_270, + resolutionSelector: MockResolutionSelector(), + instanceManager: instanceManager, + ); + + verifyNever(mockApi.create(argThat(isA()), argThat(isA()), + argThat(isA()))); + }); test('create calls create on the Java side', () { final MockTestImageAnalysisHostApi mockApi = MockTestImageAnalysisHostApi(); @@ -39,6 +58,7 @@ void main() { onWeakReferenceRemoved: (_) {}, ); + const int targetRotation = Surface.ROTATION_90; final MockResolutionSelector mockResolutionSelector = MockResolutionSelector(); const int mockResolutionSelectorId = 24; @@ -50,15 +70,43 @@ void main() { }); final ImageAnalysis instance = ImageAnalysis( + initialTargetRotation: targetRotation, resolutionSelector: mockResolutionSelector, instanceManager: instanceManager, ); verify(mockApi.create( argThat(equals(instanceManager.getIdentifier(instance))), + argThat(equals(targetRotation)), argThat(equals(mockResolutionSelectorId)))); }); + test( + 'setTargetRotation makes call to set target rotation for ImageAnalysis instance', + () async { + final MockTestImageAnalysisHostApi mockApi = + MockTestImageAnalysisHostApi(); + TestImageAnalysisHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + const int targetRotation = Surface.ROTATION_180; + final ImageAnalysis imageAnalysis = ImageAnalysis.detached( + instanceManager: instanceManager, + ); + instanceManager.addHostCreatedInstance( + imageAnalysis, + 0, + onCopy: (_) => ImageAnalysis.detached(instanceManager: instanceManager), + ); + + await imageAnalysis.setTargetRotation(targetRotation); + + verify(mockApi.setTargetRotation( + instanceManager.getIdentifier(imageAnalysis), targetRotation)); + }); + test('setAnalyzer makes call to set analyzer on ImageAnalysis instance', () async { final MockTestImageAnalysisHostApi mockApi = diff --git a/packages/camera/camera_android_camerax/test/image_analysis_test.mocks.dart b/packages/camera/camera_android_camerax/test/image_analysis_test.mocks.dart index 132fceddeb1d..5f91b8a20634 100644 --- a/packages/camera/camera_android_camerax/test/image_analysis_test.mocks.dart +++ b/packages/camera/camera_android_camerax/test/image_analysis_test.mocks.dart @@ -33,6 +33,7 @@ class MockTestImageAnalysisHostApi extends _i1.Mock @override void create( int? identifier, + int? targetRotation, int? resolutionSelectorId, ) => super.noSuchMethod( @@ -40,6 +41,7 @@ class MockTestImageAnalysisHostApi extends _i1.Mock #create, [ identifier, + targetRotation, resolutionSelectorId, ], ), @@ -70,6 +72,22 @@ class MockTestImageAnalysisHostApi extends _i1.Mock ), returnValueForMissingStub: null, ); + + @override + void setTargetRotation( + int? identifier, + int? rotation, + ) => + super.noSuchMethod( + Invocation.method( + #setTargetRotation, + [ + identifier, + rotation, + ], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [TestInstanceManagerHostApi]. diff --git a/packages/camera/camera_android_camerax/test/image_capture_test.dart b/packages/camera/camera_android_camerax/test/image_capture_test.dart index c50314ed3cb3..e9e9f0c18558 100644 --- a/packages/camera/camera_android_camerax/test/image_capture_test.dart +++ b/packages/camera/camera_android_camerax/test/image_capture_test.dart @@ -5,6 +5,7 @@ import 'package:camera_android_camerax/src/image_capture.dart'; import 'package:camera_android_camerax/src/instance_manager.dart'; import 'package:camera_android_camerax/src/resolution_selector.dart'; +import 'package:camera_android_camerax/src/surface.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; @@ -35,12 +36,13 @@ void main() { ); ImageCapture.detached( instanceManager: instanceManager, + initialTargetRotation: Surface.ROTATION_180, targetFlashMode: ImageCapture.flashModeOn, resolutionSelector: MockResolutionSelector(), ); verifyNever(mockApi.create(argThat(isA()), argThat(isA()), - argThat(isA()))); + argThat(isA()), argThat(isA()))); }); test('create calls create on the Java side', () async { @@ -50,6 +52,8 @@ void main() { final InstanceManager instanceManager = InstanceManager( onWeakReferenceRemoved: (_) {}, ); + + const int targetRotation = Surface.ROTATION_270; const int targetFlashMode = ImageCapture.flashModeAuto; final MockResolutionSelector mockResolutionSelector = MockResolutionSelector(); @@ -63,12 +67,14 @@ void main() { ImageCapture( instanceManager: instanceManager, + initialTargetRotation: targetRotation, targetFlashMode: targetFlashMode, resolutionSelector: mockResolutionSelector, ); verify(mockApi.create( argThat(isA()), + argThat(equals(targetRotation)), argThat(equals(targetFlashMode)), argThat(equals(mockResolutionSelectorId)))); }); @@ -97,6 +103,31 @@ void main() { instanceManager.getIdentifier(imageCapture), flashMode)); }); + test( + 'setTargetRotation makes call to set target rotation for ImageCapture instance', + () async { + final MockTestImageCaptureHostApi mockApi = MockTestImageCaptureHostApi(); + TestImageCaptureHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + const int targetRotation = Surface.ROTATION_180; + final ImageCapture imageCapture = ImageCapture.detached( + instanceManager: instanceManager, + ); + instanceManager.addHostCreatedInstance( + imageCapture, + 0, + onCopy: (_) => ImageCapture.detached(instanceManager: instanceManager), + ); + + await imageCapture.setTargetRotation(targetRotation); + + verify(mockApi.setTargetRotation( + instanceManager.getIdentifier(imageCapture), targetRotation)); + }); + test('takePicture makes call to capture still image', () async { final MockTestImageCaptureHostApi mockApi = MockTestImageCaptureHostApi(); TestImageCaptureHostApi.setup(mockApi); diff --git a/packages/camera/camera_android_camerax/test/image_capture_test.mocks.dart b/packages/camera/camera_android_camerax/test/image_capture_test.mocks.dart index 1cd374b75f48..35db22af6516 100644 --- a/packages/camera/camera_android_camerax/test/image_capture_test.mocks.dart +++ b/packages/camera/camera_android_camerax/test/image_capture_test.mocks.dart @@ -36,6 +36,7 @@ class MockTestImageCaptureHostApi extends _i1.Mock @override void create( int? identifier, + int? targetRotation, int? flashMode, int? resolutionSelectorId, ) => @@ -44,6 +45,7 @@ class MockTestImageCaptureHostApi extends _i1.Mock #create, [ identifier, + targetRotation, flashMode, resolutionSelectorId, ], @@ -81,6 +83,22 @@ class MockTestImageCaptureHostApi extends _i1.Mock ), )), ) as _i3.Future); + + @override + void setTargetRotation( + int? identifier, + int? rotation, + ) => + super.noSuchMethod( + Invocation.method( + #setTargetRotation, + [ + identifier, + rotation, + ], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [TestInstanceManagerHostApi]. diff --git a/packages/camera/camera_android_camerax/test/preview_test.dart b/packages/camera/camera_android_camerax/test/preview_test.dart index 29f862cda4c7..cda1252d538e 100644 --- a/packages/camera/camera_android_camerax/test/preview_test.dart +++ b/packages/camera/camera_android_camerax/test/preview_test.dart @@ -6,6 +6,7 @@ import 'package:camera_android_camerax/src/camerax_library.g.dart'; import 'package:camera_android_camerax/src/instance_manager.dart'; import 'package:camera_android_camerax/src/preview.dart'; import 'package:camera_android_camerax/src/resolution_selector.dart'; +import 'package:camera_android_camerax/src/surface.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; @@ -33,7 +34,7 @@ void main() { ); Preview.detached( instanceManager: instanceManager, - targetRotation: 90, + initialTargetRotation: Surface.ROTATION_90, resolutionSelector: MockResolutionSelector(), ); @@ -48,7 +49,7 @@ void main() { final InstanceManager instanceManager = InstanceManager( onWeakReferenceRemoved: (_) {}, ); - const int targetRotation = 90; + const int targetRotation = Surface.ROTATION_90; final MockResolutionSelector mockResolutionSelector = MockResolutionSelector(); const int mockResolutionSelectorId = 24; @@ -61,7 +62,7 @@ void main() { Preview( instanceManager: instanceManager, - targetRotation: targetRotation, + initialTargetRotation: targetRotation, resolutionSelector: mockResolutionSelector, ); @@ -71,6 +72,31 @@ void main() { argThat(equals(mockResolutionSelectorId)))); }); + test( + 'setTargetRotation makes call to set target rotation for Preview instance', + () async { + final MockTestPreviewHostApi mockApi = MockTestPreviewHostApi(); + TestPreviewHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + const int targetRotation = Surface.ROTATION_180; + final Preview preview = Preview.detached( + instanceManager: instanceManager, + ); + instanceManager.addHostCreatedInstance( + preview, + 0, + onCopy: (_) => Preview.detached(instanceManager: instanceManager), + ); + + await preview.setTargetRotation(targetRotation); + + verify(mockApi.setTargetRotation( + instanceManager.getIdentifier(preview), targetRotation)); + }); + test( 'setSurfaceProvider makes call to set surface provider for preview instance', () async { diff --git a/packages/camera/camera_android_camerax/test/preview_test.mocks.dart b/packages/camera/camera_android_camerax/test/preview_test.mocks.dart index 98b4f2648885..5b54483751d7 100644 --- a/packages/camera/camera_android_camerax/test/preview_test.mocks.dart +++ b/packages/camera/camera_android_camerax/test/preview_test.mocks.dart @@ -111,6 +111,22 @@ class MockTestPreviewHostApi extends _i1.Mock ), ), ) as _i2.ResolutionInfo); + + @override + void setTargetRotation( + int? identifier, + int? rotation, + ) => + super.noSuchMethod( + Invocation.method( + #setTargetRotation, + [ + identifier, + rotation, + ], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [ResolutionSelector]. diff --git a/packages/camera/camera_android_camerax/test/system_services_test.dart b/packages/camera/camera_android_camerax/test/system_services_test.dart index 414097b60152..030f9aee5b26 100644 --- a/packages/camera/camera_android_camerax/test/system_services_test.dart +++ b/packages/camera/camera_android_camerax/test/system_services_test.dart @@ -6,8 +6,7 @@ import 'package:camera_android_camerax/src/camerax_library.g.dart' show CameraPermissionsErrorData; import 'package:camera_android_camerax/src/system_services.dart'; import 'package:camera_platform_interface/camera_platform_interface.dart' - show CameraException, DeviceOrientationChangedEvent; -import 'package:flutter/services.dart'; + show CameraException; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; @@ -62,45 +61,6 @@ void main() { verify(mockApi.requestCameraPermissions(true)); }); - test('startListeningForDeviceOrientationChangeTest', () async { - final MockTestSystemServicesHostApi mockApi = - MockTestSystemServicesHostApi(); - TestSystemServicesHostApi.setup(mockApi); - - SystemServices.startListeningForDeviceOrientationChange(true, 90); - verify(mockApi.startListeningForDeviceOrientationChange(true, 90)); - }); - - test('stopListeningForDeviceOrientationChangeTest', () async { - final MockTestSystemServicesHostApi mockApi = - MockTestSystemServicesHostApi(); - TestSystemServicesHostApi.setup(mockApi); - - SystemServices.stopListeningForDeviceOrientationChange(); - verify(mockApi.stopListeningForDeviceOrientationChange()); - }); - - test('onDeviceOrientationChanged adds new orientation to stream', () { - SystemServices.deviceOrientationChangedStreamController.stream - .listen((DeviceOrientationChangedEvent event) { - expect(event.orientation, equals(DeviceOrientation.landscapeLeft)); - }); - SystemServicesFlutterApiImpl() - .onDeviceOrientationChanged('LANDSCAPE_LEFT'); - }); - - test( - 'onDeviceOrientationChanged throws error if new orientation is invalid', - () { - expect( - () => SystemServicesFlutterApiImpl() - .onDeviceOrientationChanged('FAKE_ORIENTATION'), - throwsA(isA().having( - (ArgumentError e) => e.message, - 'message', - '"FAKE_ORIENTATION" is not a valid DeviceOrientation value'))); - }); - test('onCameraError adds new error to stream', () { const String testErrorDescription = 'Test error description!'; SystemServices.cameraErrorStreamController.stream diff --git a/packages/camera/camera_android_camerax/test/system_services_test.mocks.dart b/packages/camera/camera_android_camerax/test/system_services_test.mocks.dart index bc3b48f24c02..298842c11513 100644 --- a/packages/camera/camera_android_camerax/test/system_services_test.mocks.dart +++ b/packages/camera/camera_android_camerax/test/system_services_test.mocks.dart @@ -63,31 +63,6 @@ class MockTestSystemServicesHostApi extends _i1.Mock returnValue: _i3.Future<_i4.CameraPermissionsErrorData?>.value(), ) as _i3.Future<_i4.CameraPermissionsErrorData?>); - @override - void startListeningForDeviceOrientationChange( - bool? isFrontFacing, - int? sensorOrientation, - ) => - super.noSuchMethod( - Invocation.method( - #startListeningForDeviceOrientationChange, - [ - isFrontFacing, - sensorOrientation, - ], - ), - returnValueForMissingStub: null, - ); - - @override - void stopListeningForDeviceOrientationChange() => super.noSuchMethod( - Invocation.method( - #stopListeningForDeviceOrientationChange, - [], - ), - returnValueForMissingStub: null, - ); - @override String getTempFilePath( String? prefix, 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 4773effcb732..35d9d333c0cb 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 @@ -508,11 +508,6 @@ abstract class TestSystemServicesHostApi { Future requestCameraPermissions( bool enableAudio); - void startListeningForDeviceOrientationChange( - bool isFrontFacing, int sensorOrientation); - - void stopListeningForDeviceOrientationChange(); - String getTempFilePath(String prefix, String suffix); static void setup(TestSystemServicesHostApi? api, @@ -543,7 +538,49 @@ abstract class TestSystemServicesHostApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.SystemServicesHostApi.startListeningForDeviceOrientationChange', + 'dev.flutter.pigeon.SystemServicesHostApi.getTempFilePath', 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.SystemServicesHostApi.getTempFilePath was null.'); + final List args = (message as List?)!; + final String? arg_prefix = (args[0] as String?); + assert(arg_prefix != null, + 'Argument for dev.flutter.pigeon.SystemServicesHostApi.getTempFilePath was null, expected non-null String.'); + final String? arg_suffix = (args[1] as String?); + assert(arg_suffix != null, + 'Argument for dev.flutter.pigeon.SystemServicesHostApi.getTempFilePath was null, expected non-null String.'); + final String output = api.getTempFilePath(arg_prefix!, arg_suffix!); + return [output]; + }); + } + } + } +} + +abstract class TestDeviceOrientationManagerHostApi { + static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => + TestDefaultBinaryMessengerBinding.instance; + static const MessageCodec codec = StandardMessageCodec(); + + void startListeningForDeviceOrientationChange( + bool isFrontFacing, int sensorOrientation); + + void stopListeningForDeviceOrientationChange(); + + int getDefaultDisplayRotation(); + + static void setup(TestDeviceOrientationManagerHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.DeviceOrientationManagerHostApi.startListeningForDeviceOrientationChange', codec, binaryMessenger: binaryMessenger); if (api == null) { @@ -554,14 +591,14 @@ abstract class TestSystemServicesHostApi { .setMockDecodedMessageHandler(channel, (Object? message) async { assert(message != null, - 'Argument for dev.flutter.pigeon.SystemServicesHostApi.startListeningForDeviceOrientationChange was null.'); + 'Argument for dev.flutter.pigeon.DeviceOrientationManagerHostApi.startListeningForDeviceOrientationChange was null.'); final List args = (message as List?)!; final bool? arg_isFrontFacing = (args[0] as bool?); assert(arg_isFrontFacing != null, - 'Argument for dev.flutter.pigeon.SystemServicesHostApi.startListeningForDeviceOrientationChange was null, expected non-null bool.'); + 'Argument for dev.flutter.pigeon.DeviceOrientationManagerHostApi.startListeningForDeviceOrientationChange was null, expected non-null bool.'); final int? arg_sensorOrientation = (args[1] as int?); assert(arg_sensorOrientation != null, - 'Argument for dev.flutter.pigeon.SystemServicesHostApi.startListeningForDeviceOrientationChange was null, expected non-null int.'); + 'Argument for dev.flutter.pigeon.DeviceOrientationManagerHostApi.startListeningForDeviceOrientationChange was null, expected non-null int.'); api.startListeningForDeviceOrientationChange( arg_isFrontFacing!, arg_sensorOrientation!); return []; @@ -570,7 +607,7 @@ abstract class TestSystemServicesHostApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.SystemServicesHostApi.stopListeningForDeviceOrientationChange', + 'dev.flutter.pigeon.DeviceOrientationManagerHostApi.stopListeningForDeviceOrientationChange', codec, binaryMessenger: binaryMessenger); if (api == null) { @@ -588,7 +625,8 @@ abstract class TestSystemServicesHostApi { } { final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.SystemServicesHostApi.getTempFilePath', codec, + 'dev.flutter.pigeon.DeviceOrientationManagerHostApi.getDefaultDisplayRotation', + codec, binaryMessenger: binaryMessenger); if (api == null) { _testBinaryMessengerBinding!.defaultBinaryMessenger @@ -597,16 +635,8 @@ abstract class TestSystemServicesHostApi { _testBinaryMessengerBinding!.defaultBinaryMessenger .setMockDecodedMessageHandler(channel, (Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.SystemServicesHostApi.getTempFilePath was null.'); - final List args = (message as List?)!; - final String? arg_prefix = (args[0] as String?); - assert(arg_prefix != null, - 'Argument for dev.flutter.pigeon.SystemServicesHostApi.getTempFilePath was null, expected non-null String.'); - final String? arg_suffix = (args[1] as String?); - assert(arg_suffix != null, - 'Argument for dev.flutter.pigeon.SystemServicesHostApi.getTempFilePath was null, expected non-null String.'); - final String output = api.getTempFilePath(arg_prefix!, arg_suffix!); + // ignore message + final int output = api.getDefaultDisplayRotation(); return [output]; }); } @@ -650,6 +680,8 @@ abstract class TestPreviewHostApi { ResolutionInfo getResolutionInfo(int identifier); + void setTargetRotation(int identifier, int rotation); + static void setup(TestPreviewHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -738,6 +770,31 @@ abstract class TestPreviewHostApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.PreviewHostApi.setTargetRotation', 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.PreviewHostApi.setTargetRotation 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.PreviewHostApi.setTargetRotation was null, expected non-null int.'); + final int? arg_rotation = (args[1] as int?); + assert(arg_rotation != null, + 'Argument for dev.flutter.pigeon.PreviewHostApi.setTargetRotation was null, expected non-null int.'); + api.setTargetRotation(arg_identifier!, arg_rotation!); + return []; + }); + } + } } } @@ -750,6 +807,8 @@ abstract class TestVideoCaptureHostApi { int getOutput(int identifier); + void setTargetRotation(int identifier, int rotation); + static void setup(TestVideoCaptureHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -796,6 +855,31 @@ abstract class TestVideoCaptureHostApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoCaptureHostApi.setTargetRotation', 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.VideoCaptureHostApi.setTargetRotation 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.VideoCaptureHostApi.setTargetRotation was null, expected non-null int.'); + final int? arg_rotation = (args[1] as int?); + assert(arg_rotation != null, + 'Argument for dev.flutter.pigeon.VideoCaptureHostApi.setTargetRotation was null, expected non-null int.'); + api.setTargetRotation(arg_identifier!, arg_rotation!); + return []; + }); + } + } } } @@ -1059,12 +1143,15 @@ abstract class TestImageCaptureHostApi { TestDefaultBinaryMessengerBinding.instance; static const MessageCodec codec = StandardMessageCodec(); - void create(int identifier, int? flashMode, int? resolutionSelectorId); + void create(int identifier, int? targetRotation, int? flashMode, + int? resolutionSelectorId); void setFlashMode(int identifier, int flashMode); Future takePicture(int identifier); + void setTargetRotation(int identifier, int rotation); + static void setup(TestImageCaptureHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -1084,9 +1171,11 @@ abstract class TestImageCaptureHostApi { final int? arg_identifier = (args[0] as int?); assert(arg_identifier != null, 'Argument for dev.flutter.pigeon.ImageCaptureHostApi.create was null, expected non-null int.'); - final int? arg_flashMode = (args[1] as int?); - final int? arg_resolutionSelectorId = (args[2] as int?); - api.create(arg_identifier!, arg_flashMode, arg_resolutionSelectorId); + final int? arg_targetRotation = (args[1] as int?); + final int? arg_flashMode = (args[2] as int?); + final int? arg_resolutionSelectorId = (args[3] as int?); + api.create(arg_identifier!, arg_targetRotation, arg_flashMode, + arg_resolutionSelectorId); return []; }); } @@ -1138,6 +1227,31 @@ abstract class TestImageCaptureHostApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImageCaptureHostApi.setTargetRotation', 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.ImageCaptureHostApi.setTargetRotation 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.ImageCaptureHostApi.setTargetRotation was null, expected non-null int.'); + final int? arg_rotation = (args[1] as int?); + assert(arg_rotation != null, + 'Argument for dev.flutter.pigeon.ImageCaptureHostApi.setTargetRotation was null, expected non-null int.'); + api.setTargetRotation(arg_identifier!, arg_rotation!); + return []; + }); + } + } } } @@ -1285,12 +1399,14 @@ abstract class TestImageAnalysisHostApi { TestDefaultBinaryMessengerBinding.instance; static const MessageCodec codec = StandardMessageCodec(); - void create(int identifier, int? resolutionSelectorId); + void create(int identifier, int? targetRotation, int? resolutionSelectorId); void setAnalyzer(int identifier, int analyzerIdentifier); void clearAnalyzer(int identifier); + void setTargetRotation(int identifier, int rotation); + static void setup(TestImageAnalysisHostApi? api, {BinaryMessenger? binaryMessenger}) { { @@ -1310,8 +1426,10 @@ abstract class TestImageAnalysisHostApi { final int? arg_identifier = (args[0] as int?); assert(arg_identifier != null, 'Argument for dev.flutter.pigeon.ImageAnalysisHostApi.create was null, expected non-null int.'); - final int? arg_resolutionSelectorId = (args[1] as int?); - api.create(arg_identifier!, arg_resolutionSelectorId); + final int? arg_targetRotation = (args[1] as int?); + final int? arg_resolutionSelectorId = (args[2] as int?); + api.create( + arg_identifier!, arg_targetRotation, arg_resolutionSelectorId); return []; }); } @@ -1363,6 +1481,31 @@ abstract class TestImageAnalysisHostApi { }); } } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ImageAnalysisHostApi.setTargetRotation', 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.ImageAnalysisHostApi.setTargetRotation 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.ImageAnalysisHostApi.setTargetRotation was null, expected non-null int.'); + final int? arg_rotation = (args[1] as int?); + assert(arg_rotation != null, + 'Argument for dev.flutter.pigeon.ImageAnalysisHostApi.setTargetRotation was null, expected non-null int.'); + api.setTargetRotation(arg_identifier!, arg_rotation!); + return []; + }); + } + } } } diff --git a/packages/camera/camera_android_camerax/test/video_capture_test.dart b/packages/camera/camera_android_camerax/test/video_capture_test.dart index 560c48ffd592..6954b352ade3 100644 --- a/packages/camera/camera_android_camerax/test/video_capture_test.dart +++ b/packages/camera/camera_android_camerax/test/video_capture_test.dart @@ -5,6 +5,7 @@ import 'package:camera_android_camerax/src/camerax_library.g.dart'; import 'package:camera_android_camerax/src/instance_manager.dart'; import 'package:camera_android_camerax/src/recorder.dart'; +import 'package:camera_android_camerax/src/surface.dart'; import 'package:camera_android_camerax/src/video_capture.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; @@ -50,6 +51,31 @@ void main() { verify(mockApi.withOutput(mockRecorderId)); }); + test( + 'setTargetRotation makes call to set target rotation for VideoCapture instance', + () async { + final MockTestVideoCaptureHostApi mockApi = MockTestVideoCaptureHostApi(); + TestVideoCaptureHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + const int targetRotation = Surface.ROTATION_180; + final VideoCapture videoCapture = VideoCapture.detached( + instanceManager: instanceManager, + ); + instanceManager.addHostCreatedInstance( + videoCapture, + 0, + onCopy: (_) => VideoCapture.detached(instanceManager: instanceManager), + ); + + await videoCapture.setTargetRotation(targetRotation); + + verify(mockApi.setTargetRotation( + instanceManager.getIdentifier(videoCapture), targetRotation)); + }); + test('getOutput calls the Java side and returns correct Recorder', () async { final MockTestVideoCaptureHostApi mockApi = MockTestVideoCaptureHostApi(); TestVideoCaptureHostApi.setup(mockApi); diff --git a/packages/camera/camera_android_camerax/test/video_capture_test.mocks.dart b/packages/camera/camera_android_camerax/test/video_capture_test.mocks.dart index f2f8e54b64bd..cebddab7fe3f 100644 --- a/packages/camera/camera_android_camerax/test/video_capture_test.mocks.dart +++ b/packages/camera/camera_android_camerax/test/video_capture_test.mocks.dart @@ -61,6 +61,22 @@ class MockTestVideoCaptureHostApi extends _i1.Mock ), returnValue: 0, ) as int); + + @override + void setTargetRotation( + int? identifier, + int? rotation, + ) => + super.noSuchMethod( + Invocation.method( + #setTargetRotation, + [ + identifier, + rotation, + ], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [TestInstanceManagerHostApi].