diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java index aaac1361eb3d..37bfbf294663 100644 --- a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java @@ -16,7 +16,7 @@ import java.util.HashMap; import java.util.Map; -class DartMessenger { +public class DartMessenger { @NonNull private final Handler handler; @Nullable private MethodChannel cameraChannel; @Nullable private MethodChannel deviceChannel; @@ -48,7 +48,7 @@ enum CameraEventType { this.handler = handler; } - void sendDeviceOrientationChangeEvent(PlatformChannel.DeviceOrientation orientation) { + public void sendDeviceOrientationChangeEvent(PlatformChannel.DeviceOrientation orientation) { assert (orientation != null); this.send( DeviceEventType.ORIENTATION_CHANGED, diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeature.java new file mode 100644 index 000000000000..500f2aa28dc2 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeature.java @@ -0,0 +1,87 @@ +// 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.camera.features.fpsrange; + +import android.hardware.camera2.CaptureRequest; +import android.os.Build; +import android.util.Range; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.features.CameraFeature; + +/** + * Controls the frames per seconds (FPS) range configuration on the {@link android.hardware.camera2} + * API. + */ +public class FpsRangeFeature extends CameraFeature> { + private static final Range MAX_PIXEL4A_RANGE = new Range<>(30, 30); + private Range currentSetting; + + /** + * Creates a new instance of the {@link FpsRangeFeature}. + * + * @param cameraProperties Collection of characteristics for the current camera device. + */ + public FpsRangeFeature(CameraProperties cameraProperties) { + super(cameraProperties); + + if (isPixel4A()) { + // HACK: There is a bug in the Pixel 4A where it cannot support 60fps modes + // even though they are reported as supported by + // `getControlAutoExposureAvailableTargetFpsRanges`. + // For max device compatibility we will keep FPS under 60 even if they report they are + // capable of achieving 60 fps. Highest working FPS is 30. + // https://issuetracker.google.com/issues/189237151 + currentSetting = MAX_PIXEL4A_RANGE; + } else { + Range[] ranges = cameraProperties.getControlAutoExposureAvailableTargetFpsRanges(); + + if (ranges != null) { + for (Range range : ranges) { + int upper = range.getUpper(); + + if (upper >= 10) { + if (currentSetting == null || upper > currentSetting.getUpper()) { + currentSetting = range; + } + } + } + } + } + } + + private boolean isPixel4A() { + return Build.BRAND.equals("google") && Build.MODEL.equals("Pixel 4a"); + } + + @Override + public String getDebugName() { + return "FpsRangeFeature"; + } + + @Override + public Range getValue() { + return currentSetting; + } + + @Override + public void setValue(Range value) { + this.currentSetting = value; + } + + // Always supported + @Override + public boolean checkIsSupported() { + return true; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + if (!checkIsSupported()) { + return; + } + + requestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, currentSetting); + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java new file mode 100644 index 000000000000..67763dde9be4 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java @@ -0,0 +1,176 @@ +// 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.camera.features.resolution; + +import android.hardware.camera2.CaptureRequest; +import android.media.CamcorderProfile; +import android.util.Size; +import androidx.annotation.VisibleForTesting; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.features.CameraFeature; + +/** + * Controls the resolutions configuration on the {@link android.hardware.camera2} API. + * + *

The {@link ResolutionFeature} is responsible for converting the platform independent {@link + * ResolutionPreset} into a {@link android.media.CamcorderProfile} which contains all the properties + * required to configure the resolution using the {@link android.hardware.camera2} API. + */ +public class ResolutionFeature extends CameraFeature { + private Size captureSize; + private Size previewSize; + private CamcorderProfile recordingProfile; + private ResolutionPreset currentSetting; + private int cameraId; + + /** + * Creates a new instance of the {@link ResolutionFeature}. + * + * @param cameraProperties Collection of characteristics for the current camera device. + * @param resolutionPreset Platform agnostic enum containing resolution information. + * @param cameraName Camera identifier of the camera for which to configure the resolution. + */ + public ResolutionFeature( + CameraProperties cameraProperties, ResolutionPreset resolutionPreset, String cameraName) { + super(cameraProperties); + this.currentSetting = resolutionPreset; + try { + this.cameraId = Integer.parseInt(cameraName, 10); + } catch (NumberFormatException e) { + this.cameraId = -1; + return; + } + configureResolution(resolutionPreset, cameraId); + } + + /** + * Gets the {@link android.media.CamcorderProfile} containing the information to configure the + * resolution using the {@link android.hardware.camera2} API. + * + * @return Resolution information to configure the {@link android.hardware.camera2} API. + */ + public CamcorderProfile getRecordingProfile() { + return this.recordingProfile; + } + + /** + * Gets the optimal preview size based on the configured resolution. + * + * @return The optimal preview size. + */ + public Size getPreviewSize() { + return this.previewSize; + } + + /** + * Gets the optimal capture size based on the configured resolution. + * + * @return The optimal capture size. + */ + public Size getCaptureSize() { + return this.captureSize; + } + + @Override + public String getDebugName() { + return "ResolutionFeature"; + } + + @Override + public ResolutionPreset getValue() { + return currentSetting; + } + + @Override + public void setValue(ResolutionPreset value) { + this.currentSetting = value; + configureResolution(currentSetting, cameraId); + } + + @Override + public boolean checkIsSupported() { + return cameraId >= 0; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + // No-op: when setting a resolution there is no need to update the request builder. + } + + @VisibleForTesting + static Size computeBestPreviewSize(int cameraId, ResolutionPreset preset) { + if (preset.ordinal() > ResolutionPreset.high.ordinal()) { + preset = ResolutionPreset.high; + } + + CamcorderProfile profile = + getBestAvailableCamcorderProfileForResolutionPreset(cameraId, preset); + return new Size(profile.videoFrameWidth, profile.videoFrameHeight); + } + + /** + * Gets the best possible {@link android.media.CamcorderProfile} for the supplied {@link + * ResolutionPreset}. + * + * @param cameraId Camera identifier which indicates the device's camera for which to select a + * {@link android.media.CamcorderProfile}. + * @param preset The {@link ResolutionPreset} for which is to be translated to a {@link + * android.media.CamcorderProfile}. + * @return The best possible {@link android.media.CamcorderProfile} that matches the supplied + * {@link ResolutionPreset}. + */ + public static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPreset( + int cameraId, ResolutionPreset preset) { + if (cameraId < 0) { + throw new AssertionError( + "getBestAvailableCamcorderProfileForResolutionPreset can only be used with valid (>=0) camera identifiers."); + } + + switch (preset) { + // All of these cases deliberately fall through to get the best available profile. + case max: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_HIGH)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_HIGH); + } + case ultraHigh: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_2160P)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_2160P); + } + case veryHigh: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_1080P)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_1080P); + } + case high: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_720P)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_720P); + } + case medium: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_480P)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_480P); + } + case low: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_QVGA)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_QVGA); + } + default: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_LOW)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_LOW); + } else { + throw new IllegalArgumentException( + "No capture session available for current capture session."); + } + } + } + + private void configureResolution(ResolutionPreset resolutionPreset, int cameraId) { + if (!checkIsSupported()) { + return; + } + recordingProfile = + getBestAvailableCamcorderProfileForResolutionPreset(cameraId, resolutionPreset); + captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); + previewSize = computeBestPreviewSize(cameraId, resolutionPreset); + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionPreset.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionPreset.java new file mode 100644 index 000000000000..359300305d40 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionPreset.java @@ -0,0 +1,15 @@ +// 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.camera.features.resolution; + +// Mirrors camera.dart +public enum ResolutionPreset { + low, + medium, + high, + veryHigh, + ultraHigh, + max, +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java new file mode 100644 index 000000000000..2a04caad743a --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java @@ -0,0 +1,349 @@ +// 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.camera.features.sensororientation; + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Configuration; +import android.hardware.SensorManager; +import android.provider.Settings; +import android.view.Display; +import android.view.OrientationEventListener; +import android.view.Surface; +import android.view.WindowManager; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation; +import io.flutter.plugins.camera.DartMessenger; + +/** + * Support class to help to determine the media orientation based on the orientation of the device. + */ +public class DeviceOrientationManager { + + private static final IntentFilter orientationIntentFilter = + new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); + + private final Activity activity; + private final DartMessenger messenger; + private final boolean isFrontFacing; + private final int sensorOrientation; + private PlatformChannel.DeviceOrientation lastOrientation; + private OrientationEventListener orientationEventListener; + private BroadcastReceiver broadcastReceiver; + + /** Factory method to create a device orientation manager. */ + public static DeviceOrientationManager create( + @NonNull Activity activity, + @NonNull DartMessenger messenger, + boolean isFrontFacing, + int sensorOrientation) { + return new DeviceOrientationManager(activity, messenger, isFrontFacing, sensorOrientation); + } + + private DeviceOrientationManager( + @NonNull Activity activity, + @NonNull DartMessenger messenger, + boolean isFrontFacing, + int sensorOrientation) { + this.activity = activity; + this.messenger = messenger; + this.isFrontFacing = isFrontFacing; + this.sensorOrientation = sensorOrientation; + } + + /** + * Starts listening to the device's sensors or UI for orientation updates. + * + *

When orientation information is updated the new orientation is send to the client using the + * {@link DartMessenger}. This latest value can also be retrieved through the {@link + * #getMediaOrientation()} accessor. + * + *

If the device's ACCELEROMETER_ROTATION setting is enabled the {@link + * DeviceOrientationManager} will report orientation updates based on the sensor information. If + * the ACCELEROMETER_ROTATION is disabled the {@link DeviceOrientationManager} will fallback to + * the deliver orientation updates based on the UI orientation. + */ + public void start() { + startSensorListener(); + startUIListener(); + } + + /** Stops listening for orientation updates. */ + public void stop() { + stopSensorListener(); + stopUIListener(); + } + + /** + * Returns the last captured orientation in degrees based on sensor or UI information. + * + *

The orientation is returned in degrees and could be one of the following values: + * + *

    + *
  • 0: Indicates the device is currently in portrait. + *
  • 90: Indicates the device is currently in landscape left. + *
  • 180: Indicates the device is currently in portrait down. + *
  • 270: Indicates the device is currently in landscape right. + *
+ * + * @return The last captured orientation in degrees + */ + public int getMediaOrientation() { + return this.getMediaOrientation(this.lastOrientation); + } + + /** + * Returns the device's orientation in degrees based on the supplied {@link + * PlatformChannel.DeviceOrientation} value. + * + *

+ * + *

    + *
  • PORTRAIT_UP: converts to 0 degrees. + *
  • LANDSCAPE_LEFT: converts to 90 degrees. + *
  • PORTRAIT_DOWN: converts to 180 degrees. + *
  • LANDSCAPE_RIGHT: converts to 270 degrees. + *
+ * + * @param orientation The {@link PlatformChannel.DeviceOrientation} value that is to be converted + * into degrees. + * @return The device's orientation in degrees. + */ + public int getMediaOrientation(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 = 90; + break; + case LANDSCAPE_RIGHT: + angle = 270; + break; + } + + if (isFrontFacing) { + angle *= -1; + } + + return (angle + sensorOrientation + 360) % 360; + } + + private void startSensorListener() { + if (orientationEventListener != null) { + return; + } + orientationEventListener = + new OrientationEventListener(activity, SensorManager.SENSOR_DELAY_NORMAL) { + @Override + public void onOrientationChanged(int angle) { + handleSensorOrientationChange(angle); + } + }; + if (orientationEventListener.canDetectOrientation()) { + orientationEventListener.enable(); + } + } + + private void startUIListener() { + if (broadcastReceiver != null) { + return; + } + broadcastReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + handleUIOrientationChange(); + } + }; + activity.registerReceiver(broadcastReceiver, orientationIntentFilter); + broadcastReceiver.onReceive(activity, null); + } + + /** + * Handles orientation changes based on information from the device's sensors. + * + *

This method is visible for testing purposes only and should never be used outside this + * class. + * + * @param angle of the current orientation. + */ + @VisibleForTesting + void handleSensorOrientationChange(int angle) { + if (!isAccelerometerRotationLocked()) { + PlatformChannel.DeviceOrientation orientation = calculateSensorOrientation(angle); + lastOrientation = handleOrientationChange(orientation, lastOrientation, messenger); + } + } + + /** + * Handles orientation changes based on change events triggered by the OrientationIntentFilter. + * + *

This method is visible for testing purposes only and should never be used outside this + * class. + */ + @VisibleForTesting + void handleUIOrientationChange() { + if (isAccelerometerRotationLocked()) { + PlatformChannel.DeviceOrientation orientation = getUIOrientation(); + lastOrientation = handleOrientationChange(orientation, lastOrientation, messenger); + } + } + + /** + * Handles orientation changes coming from either the device's sensors or the + * OrientationIntentFilter. + * + *

This method is visible for testing purposes only and should never be used outside this + * class. + */ + @VisibleForTesting + static DeviceOrientation handleOrientationChange( + DeviceOrientation newOrientation, + DeviceOrientation previousOrientation, + DartMessenger messenger) { + if (!newOrientation.equals(previousOrientation)) { + messenger.sendDeviceOrientationChangeEvent(newOrientation); + } + + return newOrientation; + } + + private void stopSensorListener() { + if (orientationEventListener == null) { + return; + } + orientationEventListener.disable(); + orientationEventListener = null; + } + + private void stopUIListener() { + if (broadcastReceiver == null) { + return; + } + activity.unregisterReceiver(broadcastReceiver); + broadcastReceiver = null; + } + + private boolean isAccelerometerRotationLocked() { + return android.provider.Settings.System.getInt( + activity.getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) + != 1; + } + + /** + * Gets the current user interface orientation. + * + *

This method is visible for testing purposes only and should never be used outside this + * class. + * + * @return The current user interface orientation. + */ + @VisibleForTesting + PlatformChannel.DeviceOrientation getUIOrientation() { + final int rotation = getDisplay().getRotation(); + final int orientation = activity.getResources().getConfiguration().orientation; + + switch (orientation) { + case Configuration.ORIENTATION_PORTRAIT: + if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) { + return PlatformChannel.DeviceOrientation.PORTRAIT_UP; + } else { + return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN; + } + case Configuration.ORIENTATION_LANDSCAPE: + if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) { + return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT; + } else { + return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT; + } + default: + return PlatformChannel.DeviceOrientation.PORTRAIT_UP; + } + } + + /** + * 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. + * + *

This method is visible for testing purposes only and should never be used outside this + * class. + * + * @return The default orientation of the device. + */ + @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; + } + } + + /** + * Gets an instance of the Android {@link android.view.Display}. + * + *

This method is visible for testing purposes only and should never be used outside this + * class. + * + * @return An instance of the Android {@link android.view.Display}. + */ + @SuppressWarnings("deprecation") + @VisibleForTesting + Display getDisplay() { + return ((WindowManager) activity.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeature.java new file mode 100644 index 000000000000..9e316f741805 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeature.java @@ -0,0 +1,105 @@ +// 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.camera.features.sensororientation; + +import android.app.Activity; +import android.hardware.camera2.CameraMetadata; +import android.hardware.camera2.CaptureRequest; +import androidx.annotation.NonNull; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.DartMessenger; +import io.flutter.plugins.camera.features.CameraFeature; +import io.flutter.plugins.camera.features.resolution.ResolutionFeature; + +/** Provides access to the sensor orientation of the camera devices. */ +public class SensorOrientationFeature extends CameraFeature { + private Integer currentSetting = 0; + private final DeviceOrientationManager deviceOrientationListener; + private PlatformChannel.DeviceOrientation lockedCaptureOrientation; + + /** + * Creates a new instance of the {@link ResolutionFeature}. + * + * @param cameraProperties Collection of characteristics for the current camera device. + * @param activity Current Android {@link android.app.Activity}, used to detect UI orientation + * changes. + * @param dartMessenger Instance of a {@link DartMessenger} used to communicate orientation + * updates back to the client. + */ + public SensorOrientationFeature( + @NonNull CameraProperties cameraProperties, + @NonNull Activity activity, + @NonNull DartMessenger dartMessenger) { + super(cameraProperties); + setValue(cameraProperties.getSensorOrientation()); + + boolean isFrontFacing = cameraProperties.getLensFacing() == CameraMetadata.LENS_FACING_FRONT; + deviceOrientationListener = + DeviceOrientationManager.create(activity, dartMessenger, isFrontFacing, currentSetting); + deviceOrientationListener.start(); + } + + @Override + public String getDebugName() { + return "SensorOrientationFeature"; + } + + @Override + public Integer getValue() { + return currentSetting; + } + + @Override + public void setValue(Integer value) { + this.currentSetting = value; + } + + @Override + public boolean checkIsSupported() { + return true; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + // Noop: when setting the sensor orientation there is no need to update the request builder. + } + + /** + * Gets the instance of the {@link DeviceOrientationManager} used to detect orientation changes. + * + * @return The instance of the {@link DeviceOrientationManager}. + */ + public DeviceOrientationManager getDeviceOrientationManager() { + return this.deviceOrientationListener; + } + + /** + * Lock the capture orientation, indicating that the device orientation should not influence the + * capture orientation. + * + * @param orientation The orientation in which to lock the capture orientation. + */ + public void lockCaptureOrientation(PlatformChannel.DeviceOrientation orientation) { + this.lockedCaptureOrientation = orientation; + } + + /** + * Unlock the capture orientation, indicating that the device orientation should be used to + * configure the capture orientation. + */ + public void unlockCaptureOrientation() { + this.lockedCaptureOrientation = null; + } + + /** + * Gets the configured locked capture orientation. + * + * @return The configured locked capture orientation. + */ + public PlatformChannel.DeviceOrientation getLockedCaptureOrientation() { + return this.lockedCaptureOrientation; + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeaturePixel4aTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeaturePixel4aTest.java new file mode 100644 index 000000000000..7b6e70fff5b2 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeaturePixel4aTest.java @@ -0,0 +1,30 @@ +// 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.camera.features.fpsrange; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; + +import android.os.Build; +import android.util.Range; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.utils.TestUtils; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class FpsRangeFeaturePixel4aTest { + @Test + public void ctor_should_initialize_fps_range_with_30_when_device_is_pixel_4a() { + TestUtils.setFinalStatic(Build.class, "BRAND", "google"); + TestUtils.setFinalStatic(Build.class, "MODEL", "Pixel 4a"); + + FpsRangeFeature fpsRangeFeature = new FpsRangeFeature(mock(CameraProperties.class)); + Range range = fpsRangeFeature.getValue(); + assertEquals(30, (int) range.getLower()); + assertEquals(30, (int) range.getUpper()); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java new file mode 100644 index 000000000000..77937b5e87c6 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java @@ -0,0 +1,108 @@ +// 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.camera.features.fpsrange; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +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.hardware.camera2.CaptureRequest; +import android.os.Build; +import android.util.Range; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.utils.TestUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class FpsRangeFeatureTest { + @Before + public void before() { + TestUtils.setFinalStatic(Build.class, "BRAND", "Test Brand"); + TestUtils.setFinalStatic(Build.class, "MODEL", "Test Model"); + } + + @After + public void after() { + TestUtils.setFinalStatic(Build.class, "BRAND", null); + TestUtils.setFinalStatic(Build.class, "MODEL", null); + } + + @Test + public void ctor_should_initialize_fps_range_with_highest_upper_value_from_range_array() { + FpsRangeFeature fpsRangeFeature = createTestInstance(); + assertEquals(13, (int) fpsRangeFeature.getValue().getUpper()); + } + + @Test + public void getDebugName_should_return_the_name_of_the_feature() { + FpsRangeFeature fpsRangeFeature = createTestInstance(); + assertEquals("FpsRangeFeature", fpsRangeFeature.getDebugName()); + } + + @Test + public void getValue_should_return_highest_upper_range_if_not_set() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FpsRangeFeature fpsRangeFeature = createTestInstance(); + + assertEquals(13, (int) fpsRangeFeature.getValue().getUpper()); + } + + @Test + public void getValue_should_echo_the_set_value() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FpsRangeFeature fpsRangeFeature = new FpsRangeFeature(mockCameraProperties); + @SuppressWarnings("unchecked") + Range expectedValue = mock(Range.class); + + fpsRangeFeature.setValue(expectedValue); + Range actualValue = fpsRangeFeature.getValue(); + + assertEquals(expectedValue, actualValue); + } + + @Test + public void checkIsSupported_should_return_true() { + FpsRangeFeature fpsRangeFeature = createTestInstance(); + assertTrue(fpsRangeFeature.checkIsSupported()); + } + + @Test + @SuppressWarnings("unchecked") + public void updateBuilder_should_set_ae_target_fps_range() { + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + FpsRangeFeature fpsRangeFeature = createTestInstance(); + + fpsRangeFeature.updateBuilder(mockBuilder); + + verify(mockBuilder).set(eq(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE), any(Range.class)); + } + + private static FpsRangeFeature createTestInstance() { + @SuppressWarnings("unchecked") + Range rangeOne = mock(Range.class); + @SuppressWarnings("unchecked") + Range rangeTwo = mock(Range.class); + @SuppressWarnings("unchecked") + Range rangeThree = mock(Range.class); + + when(rangeOne.getUpper()).thenReturn(11); + when(rangeTwo.getUpper()).thenReturn(12); + when(rangeThree.getUpper()).thenReturn(13); + + @SuppressWarnings("unchecked") + Range[] ranges = new Range[] {rangeOne, rangeTwo, rangeThree}; + + CameraProperties cameraProperties = mock(CameraProperties.class); + + when(cameraProperties.getControlAutoExposureAvailableTargetFpsRanges()).thenReturn(ranges); + + return new FpsRangeFeature(cameraProperties); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java new file mode 100644 index 000000000000..bb9cb61e1508 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java @@ -0,0 +1,190 @@ +// 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.camera.features.resolution; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; + +import android.media.CamcorderProfile; +import io.flutter.plugins.camera.CameraProperties; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; + +public class ResolutionFeatureTest { + private static final String cameraName = "1"; + private CamcorderProfile mockProfileLow; + private MockedStatic mockedStaticProfile; + + @Before + public void before() { + mockedStaticProfile = mockStatic(CamcorderProfile.class); + mockProfileLow = mock(CamcorderProfile.class); + CamcorderProfile mockProfile = mock(CamcorderProfile.class); + + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_HIGH)) + .thenReturn(true); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_2160P)) + .thenReturn(true); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_1080P)) + .thenReturn(true); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_720P)) + .thenReturn(true); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_480P)) + .thenReturn(true); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_QVGA)) + .thenReturn(true); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_LOW)) + .thenReturn(true); + + mockedStaticProfile + .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_HIGH)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_2160P)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_1080P)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_480P)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_QVGA)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_LOW)) + .thenReturn(mockProfileLow); + } + + @After + public void after() { + mockedStaticProfile.reset(); + mockedStaticProfile.close(); + } + + @Test + public void getDebugName_should_return_the_name_of_the_feature() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ResolutionFeature resolutionFeature = + new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraName); + + assertEquals("ResolutionFeature", resolutionFeature.getDebugName()); + } + + @Test + public void getValue_should_return_initial_value_when_not_set() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ResolutionFeature resolutionFeature = + new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraName); + + assertEquals(ResolutionPreset.max, resolutionFeature.getValue()); + } + + @Test + public void getValue_should_echo_setValue() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ResolutionFeature resolutionFeature = + new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraName); + + resolutionFeature.setValue(ResolutionPreset.high); + + assertEquals(ResolutionPreset.high, resolutionFeature.getValue()); + } + + @Test + public void checkIsSupport_returns_true() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ResolutionFeature resolutionFeature = + new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraName); + + assertTrue(resolutionFeature.checkIsSupported()); + } + + @Test + public void getBestAvailableCamcorderProfileForResolutionPreset_should_fall_through() { + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_HIGH)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_2160P)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_1080P)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_720P)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_480P)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_QVGA)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_LOW)) + .thenReturn(true); + + assertEquals( + mockProfileLow, + ResolutionFeature.getBestAvailableCamcorderProfileForResolutionPreset( + 1, ResolutionPreset.max)); + } + + @Test + public void computeBestPreviewSize_should_use_720P_when_resolution_preset_max() { + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.max); + + mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); + } + + @Test + public void computeBestPreviewSize_should_use_720P_when_resolution_preset_ultraHigh() { + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.ultraHigh); + + mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); + } + + @Test + public void computeBestPreviewSize_should_use_720P_when_resolution_preset_veryHigh() { + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.veryHigh); + + mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); + } + + @Test + public void computeBestPreviewSize_should_use_720P_when_resolution_preset_high() { + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.high); + + mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); + } + + @Test + public void computeBestPreviewSize_should_use_480P_when_resolution_preset_medium() { + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.medium); + + mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_480P)); + } + + @Test + public void computeBestPreviewSize_should_use_QVGA_when_resolution_preset_low() { + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.low); + + mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_QVGA)); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java new file mode 100644 index 000000000000..6e8d04d20e99 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java @@ -0,0 +1,309 @@ +// 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.camera.features.sensororientation; + +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.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.provider.Settings; +import android.view.Display; +import android.view.Surface; +import android.view.WindowManager; +import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation; +import io.flutter.plugins.camera.DartMessenger; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; + +public class DeviceOrientationManagerTest { + private Activity mockActivity; + private DartMessenger mockDartMessenger; + private WindowManager mockWindowManager; + private Display mockDisplay; + private DeviceOrientationManager deviceOrientationManager; + + @Before + public void before() { + mockActivity = mock(Activity.class); + mockDartMessenger = mock(DartMessenger.class); + mockDisplay = mock(Display.class); + mockWindowManager = mock(WindowManager.class); + + when(mockActivity.getSystemService(Context.WINDOW_SERVICE)).thenReturn(mockWindowManager); + when(mockWindowManager.getDefaultDisplay()).thenReturn(mockDisplay); + + deviceOrientationManager = + DeviceOrientationManager.create(mockActivity, mockDartMessenger, false, 0); + } + + @Test + public void getMediaOrientation_when_natural_screen_orientation_equals_portrait_up() { + int degreesPortraitUp = + deviceOrientationManager.getMediaOrientation(DeviceOrientation.PORTRAIT_UP); + int degreesPortraitDown = + deviceOrientationManager.getMediaOrientation(DeviceOrientation.PORTRAIT_DOWN); + int degreesLandscapeLeft = + deviceOrientationManager.getMediaOrientation(DeviceOrientation.LANDSCAPE_LEFT); + int degreesLandscapeRight = + deviceOrientationManager.getMediaOrientation(DeviceOrientation.LANDSCAPE_RIGHT); + + assertEquals(0, degreesPortraitUp); + assertEquals(90, degreesLandscapeLeft); + assertEquals(180, degreesPortraitDown); + assertEquals(270, degreesLandscapeRight); + } + + @Test + public void getMediaOrientation_when_natural_screen_orientation_equals_landscape_left() { + DeviceOrientationManager orientationManager = + DeviceOrientationManager.create(mockActivity, mockDartMessenger, false, 90); + + int degreesPortraitUp = orientationManager.getMediaOrientation(DeviceOrientation.PORTRAIT_UP); + int degreesPortraitDown = + orientationManager.getMediaOrientation(DeviceOrientation.PORTRAIT_DOWN); + int degreesLandscapeLeft = + orientationManager.getMediaOrientation(DeviceOrientation.LANDSCAPE_LEFT); + int degreesLandscapeRight = + orientationManager.getMediaOrientation(DeviceOrientation.LANDSCAPE_RIGHT); + + assertEquals(90, degreesPortraitUp); + assertEquals(180, degreesLandscapeLeft); + assertEquals(270, degreesPortraitDown); + assertEquals(0, degreesLandscapeRight); + } + + @Test + public void getMediaOrientation_should_fallback_to_sensor_orientation_when_orientation_is_null() { + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); + + int degrees = deviceOrientationManager.getMediaOrientation(null); + + assertEquals(90, degrees); + } + + @Test + public void handleSensorOrientationChange_should_send_message_when_sensor_access_is_allowed() { + try (MockedStatic mockedSystem = mockStatic(Settings.System.class)) { + mockedSystem + .when( + () -> + Settings.System.getInt(any(), eq(Settings.System.ACCELEROMETER_ROTATION), eq(0))) + .thenReturn(1); + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); + + deviceOrientationManager.handleSensorOrientationChange(90); + } + + verify(mockDartMessenger, times(1)) + .sendDeviceOrientationChangeEvent(DeviceOrientation.LANDSCAPE_LEFT); + } + + @Test + public void + handleSensorOrientationChange_should_send_message_when_sensor_access_is_not_allowed() { + try (MockedStatic mockedSystem = mockStatic(Settings.System.class)) { + mockedSystem + .when( + () -> + Settings.System.getInt(any(), eq(Settings.System.ACCELEROMETER_ROTATION), eq(0))) + .thenReturn(0); + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); + + deviceOrientationManager.handleSensorOrientationChange(90); + } + + verify(mockDartMessenger, never()).sendDeviceOrientationChangeEvent(any()); + } + + @Test + public void handleUIOrientationChange_should_send_message_when_sensor_access_is_allowed() { + try (MockedStatic mockedSystem = mockStatic(Settings.System.class)) { + mockedSystem + .when( + () -> + Settings.System.getInt(any(), eq(Settings.System.ACCELEROMETER_ROTATION), eq(0))) + .thenReturn(0); + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); + + deviceOrientationManager.handleUIOrientationChange(); + } + + verify(mockDartMessenger, times(1)) + .sendDeviceOrientationChangeEvent(DeviceOrientation.LANDSCAPE_LEFT); + } + + @Test + public void handleUIOrientationChange_should_send_message_when_sensor_access_is_not_allowed() { + try (MockedStatic mockedSystem = mockStatic(Settings.System.class)) { + mockedSystem + .when( + () -> + Settings.System.getInt(any(), eq(Settings.System.ACCELEROMETER_ROTATION), eq(0))) + .thenReturn(1); + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); + + deviceOrientationManager.handleUIOrientationChange(); + } + + verify(mockDartMessenger, never()).sendDeviceOrientationChangeEvent(any()); + } + + @Test + public void handleOrientationChange_should_send_message_when_orientation_is_updated() { + DeviceOrientation previousOrientation = DeviceOrientation.PORTRAIT_UP; + DeviceOrientation newOrientation = DeviceOrientation.LANDSCAPE_LEFT; + + DeviceOrientation orientation = + DeviceOrientationManager.handleOrientationChange( + newOrientation, previousOrientation, mockDartMessenger); + + verify(mockDartMessenger, times(1)).sendDeviceOrientationChangeEvent(newOrientation); + assertEquals(newOrientation, orientation); + } + + @Test + public void handleOrientationChange_should_not_send_message_when_orientation_is_not_updated() { + DeviceOrientation previousOrientation = DeviceOrientation.PORTRAIT_UP; + DeviceOrientation newOrientation = DeviceOrientation.PORTRAIT_UP; + + DeviceOrientation orientation = + DeviceOrientationManager.handleOrientationChange( + newOrientation, previousOrientation, mockDartMessenger); + + verify(mockDartMessenger, never()).sendDeviceOrientationChangeEvent(any()); + assertEquals(newOrientation, orientation); + } + + @Test + public void getUIOrientation() { + // Orientation portrait and rotation of 0 should translate to "PORTRAIT_UP". + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); + DeviceOrientation uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.PORTRAIT_UP, uiOrientation); + + // Orientation portrait and rotation of 90 should translate to "PORTRAIT_UP". + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_90); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.PORTRAIT_UP, uiOrientation); + + // Orientation portrait and rotation of 180 should translate to "PORTRAIT_DOWN". + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_180); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.PORTRAIT_DOWN, uiOrientation); + + // Orientation portrait and rotation of 270 should translate to "PORTRAIT_DOWN". + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_270); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.PORTRAIT_DOWN, uiOrientation); + + // Orientation landscape and rotation of 0 should translate to "LANDSCAPE_LEFT". + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.LANDSCAPE_LEFT, uiOrientation); + + // Orientation landscape and rotation of 90 should translate to "LANDSCAPE_LEFT". + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_90); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.LANDSCAPE_LEFT, uiOrientation); + + // Orientation landscape and rotation of 180 should translate to "LANDSCAPE_RIGHT". + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_180); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.LANDSCAPE_RIGHT, uiOrientation); + + // Orientation landscape and rotation of 270 should translate to "LANDSCAPE_RIGHT". + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_270); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.LANDSCAPE_RIGHT, uiOrientation); + + // Orientation undefined should default to "PORTRAIT_UP". + setUpUIOrientationMocks(Configuration.ORIENTATION_UNDEFINED, Surface.ROTATION_0); + uiOrientation = deviceOrientationManager.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); + + when(mockDisplay.getRotation()).thenReturn(rotation); + + mockConfiguration.orientation = orientation; + when(mockActivity.getResources()).thenReturn(mockResources); + when(mockResources.getConfiguration()).thenReturn(mockConfiguration); + } + + @Test + public void getDisplayTest() { + Display display = deviceOrientationManager.getDisplay(); + + assertEquals(mockDisplay, display); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeatureTest.java new file mode 100644 index 000000000000..ce2bb7bb2670 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeatureTest.java @@ -0,0 +1,126 @@ +// 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.camera.features.sensororientation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.hardware.camera2.CameraMetadata; +import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.DartMessenger; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; + +public class SensorOrientationFeatureTest { + private MockedStatic mockedStaticDeviceOrientationManager; + private Activity mockActivity; + private CameraProperties mockCameraProperties; + private DartMessenger mockDartMessenger; + private DeviceOrientationManager mockDeviceOrientationManager; + + @Before + public void before() { + mockedStaticDeviceOrientationManager = mockStatic(DeviceOrientationManager.class); + mockActivity = mock(Activity.class); + mockCameraProperties = mock(CameraProperties.class); + mockDartMessenger = mock(DartMessenger.class); + mockDeviceOrientationManager = mock(DeviceOrientationManager.class); + + when(mockCameraProperties.getSensorOrientation()).thenReturn(0); + when(mockCameraProperties.getLensFacing()).thenReturn(CameraMetadata.LENS_FACING_BACK); + + mockedStaticDeviceOrientationManager + .when(() -> DeviceOrientationManager.create(mockActivity, mockDartMessenger, false, 0)) + .thenReturn(mockDeviceOrientationManager); + } + + @After + public void after() { + mockedStaticDeviceOrientationManager.close(); + } + + @Test + public void ctor_should_start_device_orientation_manager() { + SensorOrientationFeature sensorOrientationFeature = + new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); + + verify(mockDeviceOrientationManager, times(1)).start(); + } + + @Test + public void getDebugName_should_return_the_name_of_the_feature() { + SensorOrientationFeature sensorOrientationFeature = + new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); + + assertEquals("SensorOrientationFeature", sensorOrientationFeature.getDebugName()); + } + + @Test + public void getValue_should_return_null_if_not_set() { + SensorOrientationFeature sensorOrientationFeature = + new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); + + assertEquals(0, (int) sensorOrientationFeature.getValue()); + } + + @Test + public void getValue_should_echo_setValue() { + SensorOrientationFeature sensorOrientationFeature = + new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); + + sensorOrientationFeature.setValue(90); + + assertEquals(90, (int) sensorOrientationFeature.getValue()); + } + + @Test + public void checkIsSupport_returns_true() { + SensorOrientationFeature sensorOrientationFeature = + new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); + + assertTrue(sensorOrientationFeature.checkIsSupported()); + } + + @Test + public void + getDeviceOrientationManager_should_return_initialized_DartOrientationManager_instance() { + SensorOrientationFeature sensorOrientationFeature = + new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); + + assertEquals( + mockDeviceOrientationManager, sensorOrientationFeature.getDeviceOrientationManager()); + } + + @Test + public void lockCaptureOrientation_should_lock_to_specified_orientation() { + SensorOrientationFeature sensorOrientationFeature = + new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); + + sensorOrientationFeature.lockCaptureOrientation(DeviceOrientation.PORTRAIT_DOWN); + + assertEquals( + DeviceOrientation.PORTRAIT_DOWN, sensorOrientationFeature.getLockedCaptureOrientation()); + } + + @Test + public void unlockCaptureOrientation_should_set_lock_to_null() { + SensorOrientationFeature sensorOrientationFeature = + new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); + + sensorOrientationFeature.unlockCaptureOrientation(); + + assertNull(sensorOrientationFeature.getLockedCaptureOrientation()); + } +}