Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[camerax] Add fix for camera preview rotation on landscape-oriented devices and set up fix for Impeller support #7044

Merged
merged 25 commits into from
Jul 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/camera/camera_android_camerax/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 1,10 @@
## 0.6.6

* Adds logic to support building a camera preview with Android `Surface`s not backed by a `SurfaceTexture`
to which CameraX cannot not automatically apply the transformation required to achieve the correct rotation.
* Adds fix for incorrect camera preview rotation on naturally landscape-oriented devices.
* Updates example app's minimum supported SDK version to Flutter 3.22/Dart 3.4.

## 0.6.5 6

* Updates Guava version to 33.2.1.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,26 29,30 @@ public class Camera2CameraInfoHostApiImpl implements Camera2CameraInfoHostApi {

/** Proxy for methods of {@link Camera2CameraInfo}. */
@VisibleForTesting
@OptIn(markerClass = ExperimentalCamera2Interop.class)
public static class Camera2CameraInfoProxy {

@NonNull
@OptIn(markerClass = ExperimentalCamera2Interop.class)
public Camera2CameraInfo createFrom(@NonNull CameraInfo cameraInfo) {
return Camera2CameraInfo.from(cameraInfo);
}

@NonNull
@OptIn(markerClass = ExperimentalCamera2Interop.class)
public Integer getSupportedHardwareLevel(@NonNull Camera2CameraInfo camera2CameraInfo) {
return camera2CameraInfo.getCameraCharacteristic(
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL);
}

@NonNull
@OptIn(markerClass = ExperimentalCamera2Interop.class)
public String getCameraId(@NonNull Camera2CameraInfo camera2CameraInfo) {
return camera2CameraInfo.getCameraId();
}

@NonNull
public Long getSensorOrientation(@NonNull Camera2CameraInfo camera2CameraInfo) {
return Long.valueOf(
camera2CameraInfo.getCameraCharacteristic(CameraCharacteristics.SENSOR_ORIENTATION));
}
}

/**
Expand Down Expand Up @@ -105,6 109,12 @@ public String getCameraId(@NonNull Long identifier) {
return proxy.getCameraId(getCamera2CameraInfoInstance(identifier));
}

@Override
@NonNull
public Long getSensorOrientation(@NonNull Long identifier) {
return proxy.getSensorOrientation(getCamera2CameraInfoInstance(identifier));
}

private Camera2CameraInfo getCamera2CameraInfoInstance(@NonNull Long identifier) {
return Objects.requireNonNull(instanceManager.getInstance(identifier));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 52,7 @@ interface DeviceOrientationChangeCallback {
* Starts listening to the device's sensors or UI for orientation updates.
*
* <p>When orientation information is updated, the callback method of the {@link
* DeviceOrientationChangeCallback} is called with the new orientation. This latest value can also
* be retrieved through the {@link #getVideoOrientation()} accessor.
* DeviceOrientationChangeCallback} is called with the new orientation.
*
* <p>If the device's ACCELEROMETER_ROTATION setting is enabled the {@link
* DeviceOrientationManager} will report orientation updates based on the sensor information. If
Expand Down Expand Up @@ -124,7 123,7 @@ static void handleOrientationChange(
*/
// Configuration.ORIENTATION_SQUARE is deprecated.
@SuppressWarnings("deprecation")
@VisibleForTesting
@NonNull
PlatformChannel.DeviceOrientation getUIOrientation() {
final int rotation = getDefaultRotation();
final int orientation = activity.getResources().getConfiguration().orientation;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 95,8 @@ public void stopListeningForDeviceOrientationChange() {
* for instance for more information on how this default value is used.
*/
@Override
public @NonNull Long getDefaultDisplayRotation() {
@NonNull
public Long getDefaultDisplayRotation() {
int defaultRotation;
try {
defaultRotation = deviceOrientationManager.getDefaultRotation();
Expand All @@ -106,4 107,11 @@ public void stopListeningForDeviceOrientationChange() {

return Long.valueOf(defaultRotation);
}

/** Gets current UI orientation based on the current device orientation and rotation. */
@Override
@NonNull
public String getUiOrientation() {
return serializeDeviceOrientation(deviceOrientationManager.getUIOrientation());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1441,6 1441,9 @@ void requestCameraPermissions(
@NonNull
String getTempFilePath(@NonNull String prefix, @NonNull String suffix);

@NonNull
Boolean isPreviewPreTransformed();

/** The codec used by SystemServicesHostApi. */
static @NonNull MessageCodec<Object> getCodec() {
return SystemServicesHostApiCodec.INSTANCE;
Expand Down Expand Up @@ -1508,6 1511,29 @@ public void error(Throwable error) {
channel.setMessageHandler(null);
}
}
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger,
"dev.flutter.pigeon.SystemServicesHostApi.isPreviewPreTransformed",
getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
ArrayList<Object> wrapped = new ArrayList<Object>();
try {
Boolean output = api.isPreviewPreTransformed();
wrapped.add(0, output);
} catch (Throwable exception) {
ArrayList<Object> 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. */
Expand Down Expand Up @@ -1550,6 1576,9 @@ void startListeningForDeviceOrientationChange(
@NonNull
Long getDefaultDisplayRotation();

@NonNull
String getUiOrientation();

/** The codec used by DeviceOrientationManagerHostApi. */
static @NonNull MessageCodec<Object> getCodec() {
return new StandardMessageCodec();
Expand Down Expand Up @@ -1634,6 1663,29 @@ static void setup(
channel.setMessageHandler(null);
}
}
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger,
"dev.flutter.pigeon.DeviceOrientationManagerHostApi.getUiOrientation",
getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
ArrayList<Object> wrapped = new ArrayList<Object>();
try {
String output = api.getUiOrientation();
wrapped.add(0, output);
} catch (Throwable exception) {
ArrayList<Object> 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. */
Expand Down Expand Up @@ -4389,6 4441,9 @@ public interface Camera2CameraInfoHostApi {
@NonNull
String getCameraId(@NonNull Long identifier);

@NonNull
Long getSensorOrientation(@NonNull Long identifier);

/** The codec used by Camera2CameraInfoHostApi. */
static @NonNull MessageCodec<Object> getCodec() {
return new StandardMessageCodec();
Expand Down Expand Up @@ -4481,6 4536,33 @@ static void setup(
channel.setMessageHandler(null);
}
}
{
BasicMessageChannel<Object> channel =
new BasicMessageChannel<>(
binaryMessenger,
"dev.flutter.pigeon.Camera2CameraInfoHostApi.getSensorOrientation",
getCodec());
if (api != null) {
channel.setMessageHandler(
(message, reply) -> {
ArrayList<Object> wrapped = new ArrayList<Object>();
ArrayList<Object> args = (ArrayList<Object>) message;
Number identifierArg = (Number) args.get(0);
try {
Long output =
api.getSensorOrientation(
(identifierArg == null) ? null : identifierArg.longValue());
wrapped.add(0, output);
} catch (Throwable exception) {
ArrayList<Object> 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. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 6,7 @@

import android.app.Activity;
import android.content.Context;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
Expand Down Expand Up @@ -103,4 104,18 @@ public String getTempFilePath(@NonNull String prefix, @NonNull String suffix) {
null);
}
}

/**
* Returns whether or not a {@code SurfaceTexture} backs the {@code Surface} provided to CameraX
* to build the camera preview. If it is backed by a {@code Surface}, then the transformation
* needed to correctly rotate the preview has already been applied.
*
* <p>This is determined by the engine, who uses {@code SurfaceTexture}s on Android SDKs 29 and
* below.
*/
@Override
@NonNull
public Boolean isPreviewPreTransformed() {
return Build.VERSION.SDK_INT <= 29;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 93,16 @@ public void getDefaultDisplayRotation_returnsExpectedRotation() {

assertEquals(hostApi.getDefaultDisplayRotation(), Long.valueOf(defaultRotation));
}

@Test
public void getUiOrientation_returnsExpectedOrientation() {
final DeviceOrientationManagerHostApiImpl hostApi =
new DeviceOrientationManagerHostApiImpl(mockBinaryMessenger, mockInstanceManager);
final DeviceOrientation uiOrientation = DeviceOrientation.LANDSCAPE_LEFT;

hostApi.deviceOrientationManager = mockDeviceOrientationManager;
when(mockDeviceOrientationManager.getUIOrientation()).thenReturn(uiOrientation);

assertEquals(hostApi.getUiOrientation(), uiOrientation.toString());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 5,9 @@
package io.flutter.plugins.camerax;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
Expand All @@ -24,12 26,16 @@
import java.io.IOException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;

@RunWith(RobolectricTestRunner.class)
public class SystemServicesTest {
@Rule public MockitoRule mockitoRule = MockitoJUnit.rule();

Expand Down Expand Up @@ -130,4 136,28 @@ public void getTempFilePath_throwsRuntimeExceptionOnIOException() {

mockedStaticFile.close();
}

@Test
@Config(sdk = 28)
public void isPreviewPreTransformed_returnsTrueWhenRunningBelowSdk29() {
final SystemServicesHostApiImpl systemServicesHostApi =
new SystemServicesHostApiImpl(mockBinaryMessenger, mockInstanceManager, mockContext);
assertTrue(systemServicesHostApi.isPreviewPreTransformed());
}

@Test
@Config(sdk = 29)
public void isPreviewPreTransformed_returnsTrueWhenRunningSdk29() {
final SystemServicesHostApiImpl systemServicesHostApi =
new SystemServicesHostApiImpl(mockBinaryMessenger, mockInstanceManager, mockContext);
assertTrue(systemServicesHostApi.isPreviewPreTransformed());
}

@Test
@Config(sdk = 30)
public void isPreviewPreTransformed_returnsFalseWhenRunningAboveSdk29() {
final SystemServicesHostApiImpl systemServicesHostApi =
new SystemServicesHostApiImpl(mockBinaryMessenger, mockInstanceManager, mockContext);
assertFalse(systemServicesHostApi.isPreviewPreTransformed());
}
}
4 changes: 2 additions & 2 deletions packages/camera/camera_android_camerax/example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 3,8 @@ description: Demonstrates how to use the camera_android_camerax plugin.
publish_to: 'none'

environment:
sdk: ^3.2.0
flutter: ">=3.16.0"
sdk: ^3.4.0
flutter: ">=3.22.0"

dependencies:
camera_android_camerax:
Expand Down
Loading