Skip to content

Commit 0a86ac8

Browse files
authored
[camera] android-rework part 9: Final implementation of camera class (flutter#4059)
This PR adds the final implementation for the Camera class that incorporates all the features from previous parts.
1 parent 6a8681e commit 0a86ac8

File tree

53 files changed

+2291
-1902
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+2291
-1902
lines changed

packages/camera/camera/CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1-
## NEXT
1+
## 0.9.0
22

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

58
## 0.8.1+7

packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java

Lines changed: 583 additions & 692 deletions
Large diffs are not rendered by default.

packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88
import android.os.Build;
99
import androidx.annotation.NonNull;
1010
import androidx.annotation.Nullable;
11+
import androidx.lifecycle.Lifecycle;
1112
import io.flutter.embedding.engine.plugins.FlutterPlugin;
1213
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
1314
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
15+
import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter;
1416
import io.flutter.plugin.common.BinaryMessenger;
1517
import io.flutter.plugins.camera.CameraPermissions.PermissionsRegistry;
1618
import io.flutter.view.TextureRegistry;
@@ -51,7 +53,8 @@ public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registra
5153
registrar.activity(),
5254
registrar.messenger(),
5355
registrar::addRequestPermissionsResultListener,
54-
registrar.view());
56+
registrar.view(),
57+
null);
5558
}
5659

5760
@Override
@@ -70,18 +73,17 @@ public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
7073
binding.getActivity(),
7174
flutterPluginBinding.getBinaryMessenger(),
7275
binding::addRequestPermissionsResultListener,
73-
flutterPluginBinding.getTextureRegistry());
76+
flutterPluginBinding.getTextureRegistry(),
77+
FlutterLifecycleAdapter.getActivityLifecycle(binding));
7478
}
7579

7680
@Override
7781
public void onDetachedFromActivity() {
78-
if (methodCallHandler == null) {
79-
// Could be on too low of an SDK to have started listening originally.
80-
return;
82+
// Could be on too low of an SDK to have started listening originally.
83+
if (methodCallHandler != null) {
84+
methodCallHandler.stopListening();
85+
methodCallHandler = null;
8186
}
82-
83-
methodCallHandler.stopListening();
84-
methodCallHandler = null;
8587
}
8688

8789
@Override
@@ -98,14 +100,20 @@ private void maybeStartListening(
98100
Activity activity,
99101
BinaryMessenger messenger,
100102
PermissionsRegistry permissionsRegistry,
101-
TextureRegistry textureRegistry) {
103+
TextureRegistry textureRegistry,
104+
@Nullable Lifecycle lifecycle) {
102105
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
103106
// If the sdk is less than 21 (min sdk for Camera2) we don't register the plugin.
104107
return;
105108
}
106109

107110
methodCallHandler =
108111
new MethodCallHandlerImpl(
109-
activity, messenger, new CameraPermissions(), permissionsRegistry, textureRegistry);
112+
activity,
113+
messenger,
114+
new CameraPermissions(),
115+
permissionsRegistry,
116+
textureRegistry,
117+
lifecycle);
110118
}
111119
}

packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import android.util.Size;
1212
import androidx.annotation.NonNull;
1313
import androidx.annotation.VisibleForTesting;
14+
import io.flutter.embedding.engine.systemchannels.PlatformChannel;
1415
import java.util.Arrays;
1516

1617
/**
@@ -69,11 +70,32 @@ && supportsDistortionCorrection(cameraProperties)) {
6970
* boundaries.
7071
*/
7172
public static MeteringRectangle convertPointToMeteringRectangle(
72-
@NonNull Size boundaries, double x, double y) {
73+
@NonNull Size boundaries,
74+
double x,
75+
double y,
76+
@NonNull PlatformChannel.DeviceOrientation orientation) {
7377
assert (boundaries.getWidth() > 0 && boundaries.getHeight() > 0);
7478
assert (x >= 0 && x <= 1);
7579
assert (y >= 0 && y <= 1);
76-
80+
// Rotate the coordinates to match the device orientation.
81+
double oldX = x, oldY = y;
82+
switch (orientation) {
83+
case PORTRAIT_UP: // 90 ccw.
84+
y = 1 - oldX;
85+
x = oldY;
86+
break;
87+
case PORTRAIT_DOWN: // 90 cw.
88+
x = 1 - oldY;
89+
y = oldX;
90+
break;
91+
case LANDSCAPE_LEFT:
92+
// No rotation required.
93+
break;
94+
case LANDSCAPE_RIGHT: // 180.
95+
x = 1 - x;
96+
y = 1 - y;
97+
break;
98+
}
7799
// Interpolate the target coordinate.
78100
int targetX = (int) Math.round(x * ((double) (boundaries.getWidth() - 1)));
79101
int targetY = (int) Math.round(y * ((double) (boundaries.getHeight() - 1)));
@@ -98,7 +120,6 @@ public static MeteringRectangle convertPointToMeteringRectangle(
98120
if (targetY > maxTargetY) {
99121
targetY = maxTargetY;
100122
}
101-
102123
// Build the metering rectangle.
103124
return MeteringRectangleFactory.create(targetX, targetY, targetWidth, targetHeight, 1);
104125
}
@@ -130,7 +151,7 @@ static class MeteringRectangleFactory {
130151
* @param width width >= 0.
131152
* @param height height >= 0.
132153
* @param meteringWeight weight between {@value MeteringRectangle#METERING_WEIGHT_MIN} and
133-
* {@value MeteringRectangle#METERING_WEIGHT_MAX} inclusively
154+
* {@value MeteringRectangle#METERING_WEIGHT_MAX} inclusively.
134155
* @return new instance of the {@link MeteringRectangle} class.
135156
* @throws IllegalArgumentException if any of the parameters were negative.
136157
*/

packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegions.java

Lines changed: 0 additions & 76 deletions
This file was deleted.

packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java

Lines changed: 32 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,12 @@
66

77
import android.app.Activity;
88
import android.content.Context;
9-
import android.graphics.ImageFormat;
109
import android.hardware.camera2.CameraAccessException;
1110
import android.hardware.camera2.CameraCharacteristics;
1211
import android.hardware.camera2.CameraManager;
1312
import android.hardware.camera2.CameraMetadata;
14-
import android.hardware.camera2.params.StreamConfigurationMap;
15-
import android.media.CamcorderProfile;
16-
import android.util.Size;
1713
import io.flutter.embedding.engine.systemchannels.PlatformChannel;
18-
import io.flutter.plugins.camera.types.ResolutionPreset;
1914
import java.util.ArrayList;
20-
import java.util.Arrays;
21-
import java.util.Collections;
22-
import java.util.Comparator;
2315
import java.util.HashMap;
2416
import java.util.List;
2517
import java.util.Map;
@@ -29,23 +21,24 @@ public final class CameraUtils {
2921

3022
private CameraUtils() {}
3123

32-
static PlatformChannel.DeviceOrientation getDeviceOrientationFromDegrees(int degrees) {
33-
// Round to the nearest 90 degrees.
34-
degrees = (int) (Math.round(degrees / 90.0) * 90) % 360;
35-
// Determine the corresponding device orientation.
36-
switch (degrees) {
37-
case 90:
38-
return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT;
39-
case 180:
40-
return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN;
41-
case 270:
42-
return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT;
43-
case 0:
44-
default:
45-
return PlatformChannel.DeviceOrientation.PORTRAIT_UP;
46-
}
24+
/**
25+
* Gets the {@link CameraManager} singleton.
26+
*
27+
* @param context The context to get the {@link CameraManager} singleton from.
28+
* @return The {@link CameraManager} singleton.
29+
*/
30+
static CameraManager getCameraManager(Context context) {
31+
return (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
4732
}
4833

34+
/**
35+
* Serializes the {@link PlatformChannel.DeviceOrientation} to a string value.
36+
*
37+
* @param orientation The orientation to serialize.
38+
* @return The serialized orientation.
39+
* @throws UnsupportedOperationException when the provided orientation not have a corresponding
40+
* string value.
41+
*/
4942
static String serializeDeviceOrientation(PlatformChannel.DeviceOrientation orientation) {
5043
if (orientation == null)
5144
throw new UnsupportedOperationException("Could not serialize null device orientation.");
@@ -64,6 +57,15 @@ static String serializeDeviceOrientation(PlatformChannel.DeviceOrientation orien
6457
}
6558
}
6659

60+
/**
61+
* Deserializes a string value to its corresponding {@link PlatformChannel.DeviceOrientation}
62+
* value.
63+
*
64+
* @param orientation The string value to deserialize.
65+
* @return The deserialized orientation.
66+
* @throws UnsupportedOperationException when the provided string value does not have a
67+
* corresponding {@link PlatformChannel.DeviceOrientation}.
68+
*/
6769
static PlatformChannel.DeviceOrientation deserializeDeviceOrientation(String orientation) {
6870
if (orientation == null)
6971
throw new UnsupportedOperationException("Could not deserialize null device orientation.");
@@ -82,23 +84,13 @@ static PlatformChannel.DeviceOrientation deserializeDeviceOrientation(String ori
8284
}
8385
}
8486

85-
static Size computeBestPreviewSize(String cameraName, ResolutionPreset preset) {
86-
if (preset.ordinal() > ResolutionPreset.high.ordinal()) {
87-
preset = ResolutionPreset.high;
88-
}
89-
90-
CamcorderProfile profile =
91-
getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset);
92-
return new Size(profile.videoFrameWidth, profile.videoFrameHeight);
93-
}
94-
95-
static Size computeBestCaptureSize(StreamConfigurationMap streamConfigurationMap) {
96-
// For still image captures, we use the largest available size.
97-
return Collections.max(
98-
Arrays.asList(streamConfigurationMap.getOutputSizes(ImageFormat.JPEG)),
99-
new CompareSizesByArea());
100-
}
101-
87+
/**
88+
* Gets all the available cameras for the device.
89+
*
90+
* @param activity The current Android activity.
91+
* @return A map of all the available cameras, with their name as their key.
92+
* @throws CameraAccessException when the camera could not be accessed.
93+
*/
10294
public static List<Map<String, Object>> getAvailableCameras(Activity activity)
10395
throws CameraAccessException {
10496
CameraManager cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
@@ -127,52 +119,4 @@ public static List<Map<String, Object>> getAvailableCameras(Activity activity)
127119
}
128120
return cameras;
129121
}
130-
131-
static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPreset(
132-
String cameraName, ResolutionPreset preset) {
133-
int cameraId = Integer.parseInt(cameraName);
134-
switch (preset) {
135-
// All of these cases deliberately fall through to get the best available profile.
136-
case max:
137-
if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_HIGH)) {
138-
return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_HIGH);
139-
}
140-
case ultraHigh:
141-
if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_2160P)) {
142-
return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_2160P);
143-
}
144-
case veryHigh:
145-
if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_1080P)) {
146-
return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_1080P);
147-
}
148-
case high:
149-
if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_720P)) {
150-
return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_720P);
151-
}
152-
case medium:
153-
if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_480P)) {
154-
return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_480P);
155-
}
156-
case low:
157-
if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_QVGA)) {
158-
return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_QVGA);
159-
}
160-
default:
161-
if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_LOW)) {
162-
return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_LOW);
163-
} else {
164-
throw new IllegalArgumentException(
165-
"No capture session available for current capture session.");
166-
}
167-
}
168-
}
169-
170-
private static class CompareSizesByArea implements Comparator<Size> {
171-
@Override
172-
public int compare(Size lhs, Size rhs) {
173-
// We cast here to ensure the multiplications won't overflow.
174-
return Long.signum(
175-
(long) lhs.getWidth() * lhs.getHeight() - (long) rhs.getWidth() * rhs.getHeight());
176-
}
177-
}
178122
}

0 commit comments

Comments
 (0)