Skip to content

Commit

Permalink
[camera] android-rework part 9: Final implementation of camera class (f…
Browse files Browse the repository at this point in the history
…lutter#4059)

This PR adds the final implementation for the Camera class that incorporates all the features from previous parts.
  • Loading branch information
BeMacized authored Aug 23, 2021
1 parent 6a8681e commit 0a86ac8
Show file tree
Hide file tree
Showing 53 changed files with 2,291 additions and 1,902 deletions.
5 changes: 4 additions & 1 deletion packages/camera/camera/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## NEXT
## 0.9.0

* Complete rewrite of Android plugin to fix many capture, focus, flash, orientation and exposure issues.
* Fixed crash when opening front-facing cameras on some legacy android devices like Sony XZ.
* Android Flash mode works with full precapture sequence.
* Updated Android lint settings.

## 0.8.1+7
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.Lifecycle;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugins.camera.CameraPermissions.PermissionsRegistry;
import io.flutter.view.TextureRegistry;
Expand Down Expand Up @@ -51,7 +53,8 @@ public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registra
registrar.activity(),
registrar.messenger(),
registrar::addRequestPermissionsResultListener,
registrar.view());
registrar.view(),
null);
}

@Override
Expand All @@ -70,18 +73,17 @@ public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
binding.getActivity(),
flutterPluginBinding.getBinaryMessenger(),
binding::addRequestPermissionsResultListener,
flutterPluginBinding.getTextureRegistry());
flutterPluginBinding.getTextureRegistry(),
FlutterLifecycleAdapter.getActivityLifecycle(binding));
}

@Override
public void onDetachedFromActivity() {
if (methodCallHandler == null) {
// Could be on too low of an SDK to have started listening originally.
return;
// Could be on too low of an SDK to have started listening originally.
if (methodCallHandler != null) {
methodCallHandler.stopListening();
methodCallHandler = null;
}

methodCallHandler.stopListening();
methodCallHandler = null;
}

@Override
Expand All @@ -98,14 +100,20 @@ private void maybeStartListening(
Activity activity,
BinaryMessenger messenger,
PermissionsRegistry permissionsRegistry,
TextureRegistry textureRegistry) {
TextureRegistry textureRegistry,
@Nullable Lifecycle lifecycle) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
// If the sdk is less than 21 (min sdk for Camera2) we don't register the plugin.
return;
}

methodCallHandler =
new MethodCallHandlerImpl(
activity, messenger, new CameraPermissions(), permissionsRegistry, textureRegistry);
activity,
messenger,
new CameraPermissions(),
permissionsRegistry,
textureRegistry,
lifecycle);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import android.util.Size;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import io.flutter.embedding.engine.systemchannels.PlatformChannel;
import java.util.Arrays;

/**
Expand Down Expand Up @@ -69,11 +70,32 @@ && supportsDistortionCorrection(cameraProperties)) {
* boundaries.
*/
public static MeteringRectangle convertPointToMeteringRectangle(
@NonNull Size boundaries, double x, double y) {
@NonNull Size boundaries,
double x,
double y,
@NonNull PlatformChannel.DeviceOrientation orientation) {
assert (boundaries.getWidth() > 0 && boundaries.getHeight() > 0);
assert (x >= 0 && x <= 1);
assert (y >= 0 && y <= 1);

// Rotate the coordinates to match the device orientation.
double oldX = x, oldY = y;
switch (orientation) {
case PORTRAIT_UP: // 90 ccw.
y = 1 - oldX;
x = oldY;
break;
case PORTRAIT_DOWN: // 90 cw.
x = 1 - oldY;
y = oldX;
break;
case LANDSCAPE_LEFT:
// No rotation required.
break;
case LANDSCAPE_RIGHT: // 180.
x = 1 - x;
y = 1 - y;
break;
}
// Interpolate the target coordinate.
int targetX = (int) Math.round(x * ((double) (boundaries.getWidth() - 1)));
int targetY = (int) Math.round(y * ((double) (boundaries.getHeight() - 1)));
Expand All @@ -98,7 +120,6 @@ public static MeteringRectangle convertPointToMeteringRectangle(
if (targetY > maxTargetY) {
targetY = maxTargetY;
}

// Build the metering rectangle.
return MeteringRectangleFactory.create(targetX, targetY, targetWidth, targetHeight, 1);
}
Expand Down Expand Up @@ -130,7 +151,7 @@ static class MeteringRectangleFactory {
* @param width width >= 0.
* @param height height >= 0.
* @param meteringWeight weight between {@value MeteringRectangle#METERING_WEIGHT_MIN} and
* {@value MeteringRectangle#METERING_WEIGHT_MAX} inclusively
* {@value MeteringRectangle#METERING_WEIGHT_MAX} inclusively.
* @return new instance of the {@link MeteringRectangle} class.
* @throws IllegalArgumentException if any of the parameters were negative.
*/
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,12 @@

import android.app.Activity;
import android.content.Context;
import android.graphics.ImageFormat;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CameraMetadata;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.CamcorderProfile;
import android.util.Size;
import io.flutter.embedding.engine.systemchannels.PlatformChannel;
import io.flutter.plugins.camera.types.ResolutionPreset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -29,23 +21,24 @@ public final class CameraUtils {

private CameraUtils() {}

static PlatformChannel.DeviceOrientation getDeviceOrientationFromDegrees(int degrees) {
// Round to the nearest 90 degrees.
degrees = (int) (Math.round(degrees / 90.0) * 90) % 360;
// Determine the corresponding device orientation.
switch (degrees) {
case 90:
return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT;
case 180:
return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN;
case 270:
return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT;
case 0:
default:
return PlatformChannel.DeviceOrientation.PORTRAIT_UP;
}
/**
* Gets the {@link CameraManager} singleton.
*
* @param context The context to get the {@link CameraManager} singleton from.
* @return The {@link CameraManager} singleton.
*/
static CameraManager getCameraManager(Context context) {
return (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
}

/**
* Serializes the {@link PlatformChannel.DeviceOrientation} to a string value.
*
* @param orientation The orientation to serialize.
* @return The serialized orientation.
* @throws UnsupportedOperationException when the provided orientation not have a corresponding
* string value.
*/
static String serializeDeviceOrientation(PlatformChannel.DeviceOrientation orientation) {
if (orientation == null)
throw new UnsupportedOperationException("Could not serialize null device orientation.");
Expand All @@ -64,6 +57,15 @@ static String serializeDeviceOrientation(PlatformChannel.DeviceOrientation orien
}
}

/**
* Deserializes a string value to its corresponding {@link PlatformChannel.DeviceOrientation}
* value.
*
* @param orientation The string value to deserialize.
* @return The deserialized orientation.
* @throws UnsupportedOperationException when the provided string value does not have a
* corresponding {@link PlatformChannel.DeviceOrientation}.
*/
static PlatformChannel.DeviceOrientation deserializeDeviceOrientation(String orientation) {
if (orientation == null)
throw new UnsupportedOperationException("Could not deserialize null device orientation.");
Expand All @@ -82,23 +84,13 @@ static PlatformChannel.DeviceOrientation deserializeDeviceOrientation(String ori
}
}

static Size computeBestPreviewSize(String cameraName, ResolutionPreset preset) {
if (preset.ordinal() > ResolutionPreset.high.ordinal()) {
preset = ResolutionPreset.high;
}

CamcorderProfile profile =
getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset);
return new Size(profile.videoFrameWidth, profile.videoFrameHeight);
}

static Size computeBestCaptureSize(StreamConfigurationMap streamConfigurationMap) {
// For still image captures, we use the largest available size.
return Collections.max(
Arrays.asList(streamConfigurationMap.getOutputSizes(ImageFormat.JPEG)),
new CompareSizesByArea());
}

/**
* Gets all the available cameras for the device.
*
* @param activity The current Android activity.
* @return A map of all the available cameras, with their name as their key.
* @throws CameraAccessException when the camera could not be accessed.
*/
public static List<Map<String, Object>> getAvailableCameras(Activity activity)
throws CameraAccessException {
CameraManager cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
Expand Down Expand Up @@ -127,52 +119,4 @@ public static List<Map<String, Object>> getAvailableCameras(Activity activity)
}
return cameras;
}

static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPreset(
String cameraName, ResolutionPreset preset) {
int cameraId = Integer.parseInt(cameraName);
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 static class CompareSizesByArea implements Comparator<Size> {
@Override
public int compare(Size lhs, Size rhs) {
// We cast here to ensure the multiplications won't overflow.
return Long.signum(
(long) lhs.getWidth() * lhs.getHeight() - (long) rhs.getWidth() * rhs.getHeight());
}
}
}
Loading

0 comments on commit 0a86ac8

Please sign in to comment.