mirror of
https://github.com/flutter/packages.git
synced 2025-07-01 15:23:25 +08:00
[camerax] Implements torch mode (#4903)
Implements the torch flash mode. Also wraps classes necessary for the implementation (a method in `Camera`, `CameraControl`). Fixes https://github.com/flutter/flutter/issues/120715. Fixes https://github.com/flutter/flutter/issues/115846. Part of https://github.com/flutter/flutter/issues/115847.
This commit is contained in:
@ -1,3 +1,7 @@
|
||||
## 0.5.0+19
|
||||
|
||||
* Implements torch flash mode.
|
||||
|
||||
## 0.5.0+18
|
||||
|
||||
* Implements `startVideoCapturing`.
|
||||
|
@ -35,10 +35,6 @@ and thus, the plugin will fall back to 480p if configured with a
|
||||
|
||||
`lockCaptureOrientation` & `unLockCaptureOrientation` are unimplemented.
|
||||
|
||||
### Torch mode \[[Issue #120715][120715]\]
|
||||
|
||||
Calling `setFlashMode` with mode `FlashMode.torch` currently does nothing.
|
||||
|
||||
### Exposure mode, point, & offset configuration \[[Issue #120468][120468]\]
|
||||
|
||||
`setExposureMode`, `setExposurePoint`, & `setExposureOffset` are unimplemented.
|
||||
|
@ -25,6 +25,7 @@ public final class CameraAndroidCameraxPlugin implements FlutterPlugin, Activity
|
||||
private VideoCaptureHostApiImpl videoCaptureHostApiImpl;
|
||||
private ImageAnalysisHostApiImpl imageAnalysisHostApiImpl;
|
||||
private ImageCaptureHostApiImpl imageCaptureHostApiImpl;
|
||||
private CameraControlHostApiImpl cameraControlHostApiImpl;
|
||||
public @Nullable SystemServicesHostApiImpl systemServicesHostApiImpl;
|
||||
|
||||
@VisibleForTesting
|
||||
@ -81,7 +82,8 @@ public final class CameraAndroidCameraxPlugin implements FlutterPlugin, Activity
|
||||
GeneratedCameraXLibrary.LiveDataHostApi.setup(binaryMessenger, liveDataHostApiImpl);
|
||||
GeneratedCameraXLibrary.ObserverHostApi.setup(
|
||||
binaryMessenger, new ObserverHostApiImpl(binaryMessenger, instanceManager));
|
||||
imageAnalysisHostApiImpl = new ImageAnalysisHostApiImpl(binaryMessenger, instanceManager);
|
||||
imageAnalysisHostApiImpl =
|
||||
new ImageAnalysisHostApiImpl(binaryMessenger, instanceManager, context);
|
||||
GeneratedCameraXLibrary.ImageAnalysisHostApi.setup(binaryMessenger, imageAnalysisHostApiImpl);
|
||||
GeneratedCameraXLibrary.AnalyzerHostApi.setup(
|
||||
binaryMessenger, new AnalyzerHostApiImpl(binaryMessenger, instanceManager));
|
||||
@ -107,6 +109,8 @@ public final class CameraAndroidCameraxPlugin implements FlutterPlugin, Activity
|
||||
binaryMessenger, new FallbackStrategyHostApiImpl(instanceManager));
|
||||
GeneratedCameraXLibrary.QualitySelectorHostApi.setup(
|
||||
binaryMessenger, new QualitySelectorHostApiImpl(instanceManager));
|
||||
cameraControlHostApiImpl = new CameraControlHostApiImpl(instanceManager, context);
|
||||
GeneratedCameraXLibrary.CameraControlHostApi.setup(binaryMessenger, cameraControlHostApiImpl);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -128,7 +132,6 @@ public final class CameraAndroidCameraxPlugin implements FlutterPlugin, Activity
|
||||
Activity activity = activityPluginBinding.getActivity();
|
||||
|
||||
setUp(pluginBinding.getBinaryMessenger(), activity, pluginBinding.getTextureRegistry());
|
||||
updateContext(activity);
|
||||
|
||||
if (activity instanceof LifecycleOwner) {
|
||||
processCameraProviderHostApiImpl.setLifecycleOwner((LifecycleOwner) activity);
|
||||
@ -183,5 +186,8 @@ public final class CameraAndroidCameraxPlugin implements FlutterPlugin, Activity
|
||||
if (imageAnalysisHostApiImpl != null) {
|
||||
imageAnalysisHostApiImpl.setContext(context);
|
||||
}
|
||||
if (cameraControlHostApiImpl != null) {
|
||||
cameraControlHostApiImpl.setContext(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,29 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package io.flutter.plugins.camerax;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.camera.core.CameraControl;
|
||||
import io.flutter.plugin.common.BinaryMessenger;
|
||||
import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraControlFlutterApi;
|
||||
|
||||
public class CameraControlFlutterApiImpl extends CameraControlFlutterApi {
|
||||
private final @NonNull InstanceManager instanceManager;
|
||||
|
||||
public CameraControlFlutterApiImpl(
|
||||
@NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) {
|
||||
super(binaryMessenger);
|
||||
this.instanceManager = instanceManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link CameraControl} instance in Dart. {@code reply} is not used so it can be empty.
|
||||
*/
|
||||
void create(CameraControl cameraControl, Reply<Void> reply) {
|
||||
if (!instanceManager.containsInstance(cameraControl)) {
|
||||
create(instanceManager.addHostCreatedInstance(cameraControl), reply);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package io.flutter.plugins.camerax;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.camera.core.CameraControl;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraControlHostApi;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Host API implementation for {@link CameraControl}.
|
||||
*
|
||||
* <p>This class handles instantiating and adding native object instances that are attached to a
|
||||
* Dart instance or handle method calls on the associated native class or an instance of the class.
|
||||
*/
|
||||
public class CameraControlHostApiImpl implements CameraControlHostApi {
|
||||
private final InstanceManager instanceManager;
|
||||
private final CameraControlProxy proxy;
|
||||
|
||||
/** Proxy for constructors and static method of {@link CameraControl}. */
|
||||
@VisibleForTesting
|
||||
public static class CameraControlProxy {
|
||||
Context context;
|
||||
|
||||
/** Enables or disables the torch of the specified {@link CameraControl} instance. */
|
||||
@NonNull
|
||||
public void enableTorch(
|
||||
@NonNull CameraControl cameraControl,
|
||||
@NonNull Boolean torch,
|
||||
@NonNull GeneratedCameraXLibrary.Result<Void> result) {
|
||||
ListenableFuture<Void> enableTorchFuture = cameraControl.enableTorch(torch);
|
||||
|
||||
Futures.addCallback(
|
||||
enableTorchFuture,
|
||||
new FutureCallback<Void>() {
|
||||
public void onSuccess(Void voidResult) {
|
||||
result.success(null);
|
||||
}
|
||||
|
||||
public void onFailure(Throwable t) {
|
||||
result.error(t);
|
||||
}
|
||||
},
|
||||
ContextCompat.getMainExecutor(context));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an {@link CameraControlHostApiImpl}.
|
||||
*
|
||||
* @param instanceManager maintains instances stored to communicate with attached Dart objects
|
||||
*/
|
||||
public CameraControlHostApiImpl(
|
||||
@NonNull InstanceManager instanceManager, @NonNull Context context) {
|
||||
this(instanceManager, new CameraControlProxy(), context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs an {@link CameraControlHostApiImpl}.
|
||||
*
|
||||
* @param instanceManager maintains instances stored to communicate with attached Dart objects
|
||||
* @param proxy proxy for constructors and static method of {@link CameraControl}
|
||||
* @param context {@link Context} used to retrieve {@code Executor} used to enable torch mode
|
||||
*/
|
||||
@VisibleForTesting
|
||||
CameraControlHostApiImpl(
|
||||
@NonNull InstanceManager instanceManager,
|
||||
@NonNull CameraControlProxy proxy,
|
||||
@NonNull Context context) {
|
||||
this.instanceManager = instanceManager;
|
||||
this.proxy = proxy;
|
||||
proxy.context = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the context that the {@code ProcessCameraProvider} will use to enable/disable torch mode.
|
||||
*
|
||||
* <p>If using the camera plugin in an add-to-app context, ensure that a new instance of the
|
||||
* {@code CameraControl} is fetched via {@code #enableTorch} anytime the context changes.
|
||||
*/
|
||||
public void setContext(@NonNull Context context) {
|
||||
this.proxy.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enableTorch(
|
||||
@NonNull Long identifier,
|
||||
@NonNull Boolean torch,
|
||||
@NonNull GeneratedCameraXLibrary.Result<Void> result) {
|
||||
proxy.enableTorch(
|
||||
Objects.requireNonNull(instanceManager.getInstance(identifier)), torch, result);
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@ package io.flutter.plugins.camerax;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.camera.core.Camera;
|
||||
import androidx.camera.core.CameraControl;
|
||||
import androidx.camera.core.CameraInfo;
|
||||
import io.flutter.plugin.common.BinaryMessenger;
|
||||
import io.flutter.plugins.camerax.GeneratedCameraXLibrary.CameraHostApi;
|
||||
@ -28,14 +29,33 @@ public class CameraHostApiImpl implements CameraHostApi {
|
||||
@Override
|
||||
@NonNull
|
||||
public Long getCameraInfo(@NonNull Long identifier) {
|
||||
Camera camera = (Camera) Objects.requireNonNull(instanceManager.getInstance(identifier));
|
||||
Camera camera = getCameraInstance(identifier);
|
||||
CameraInfo cameraInfo = camera.getCameraInfo();
|
||||
|
||||
if (!instanceManager.containsInstance(cameraInfo)) {
|
||||
CameraInfoFlutterApiImpl cameraInfoFlutterApiImpl =
|
||||
new CameraInfoFlutterApiImpl(binaryMessenger, instanceManager);
|
||||
cameraInfoFlutterApiImpl.create(cameraInfo, reply -> {});
|
||||
}
|
||||
CameraInfoFlutterApiImpl cameraInfoFlutterApiImpl =
|
||||
new CameraInfoFlutterApiImpl(binaryMessenger, instanceManager);
|
||||
cameraInfoFlutterApiImpl.create(cameraInfo, reply -> {});
|
||||
return instanceManager.getIdentifierForStrongReference(cameraInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the {@link CameraControl} instance that provides access to asynchronous operations
|
||||
* like zoom and focus & metering on the {@link Camera} instance with the specified identifier.
|
||||
*/
|
||||
@Override
|
||||
@NonNull
|
||||
public Long getCameraControl(@NonNull Long identifier) {
|
||||
Camera camera = getCameraInstance(identifier);
|
||||
CameraControl cameraControl = camera.getCameraControl();
|
||||
|
||||
CameraControlFlutterApiImpl cameraControlFlutterApiImpl =
|
||||
new CameraControlFlutterApiImpl(binaryMessenger, instanceManager);
|
||||
cameraControlFlutterApiImpl.create(cameraControl, reply -> {});
|
||||
return instanceManager.getIdentifierForStrongReference(cameraControl);
|
||||
}
|
||||
|
||||
/** Retrieives the {@link Camera} instance associated with the specified {@code identifier}. */
|
||||
private Camera getCameraInstance(@NonNull Long identifier) {
|
||||
return (Camera) Objects.requireNonNull(instanceManager.getInstance(identifier));
|
||||
}
|
||||
}
|
||||
|
@ -1136,6 +1136,9 @@ public class GeneratedCameraXLibrary {
|
||||
@NonNull
|
||||
Long getCameraInfo(@NonNull Long identifier);
|
||||
|
||||
@NonNull
|
||||
Long getCameraControl(@NonNull Long identifier);
|
||||
|
||||
/** The codec used by CameraHostApi. */
|
||||
static @NonNull MessageCodec<Object> getCodec() {
|
||||
return new StandardMessageCodec();
|
||||
@ -1166,6 +1169,31 @@ public class GeneratedCameraXLibrary {
|
||||
channel.setMessageHandler(null);
|
||||
}
|
||||
}
|
||||
{
|
||||
BasicMessageChannel<Object> channel =
|
||||
new BasicMessageChannel<>(
|
||||
binaryMessenger, "dev.flutter.pigeon.CameraHostApi.getCameraControl", 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.getCameraControl(
|
||||
(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. */
|
||||
@ -3203,4 +3231,82 @@ public class GeneratedCameraXLibrary {
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
|
||||
public interface CameraControlHostApi {
|
||||
|
||||
void enableTorch(
|
||||
@NonNull Long identifier, @NonNull Boolean torch, @NonNull Result<Void> result);
|
||||
|
||||
/** The codec used by CameraControlHostApi. */
|
||||
static @NonNull MessageCodec<Object> getCodec() {
|
||||
return new StandardMessageCodec();
|
||||
}
|
||||
/**
|
||||
* Sets up an instance of `CameraControlHostApi` to handle messages through the
|
||||
* `binaryMessenger`.
|
||||
*/
|
||||
static void setup(
|
||||
@NonNull BinaryMessenger binaryMessenger, @Nullable CameraControlHostApi api) {
|
||||
{
|
||||
BasicMessageChannel<Object> channel =
|
||||
new BasicMessageChannel<>(
|
||||
binaryMessenger, "dev.flutter.pigeon.CameraControlHostApi.enableTorch", 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);
|
||||
Boolean torchArg = (Boolean) args.get(1);
|
||||
Result<Void> resultCallback =
|
||||
new Result<Void>() {
|
||||
public void success(Void result) {
|
||||
wrapped.add(0, null);
|
||||
reply.reply(wrapped);
|
||||
}
|
||||
|
||||
public void error(Throwable error) {
|
||||
ArrayList<Object> wrappedError = wrapError(error);
|
||||
reply.reply(wrappedError);
|
||||
}
|
||||
};
|
||||
|
||||
api.enableTorch(
|
||||
(identifierArg == null) ? null : identifierArg.longValue(),
|
||||
torchArg,
|
||||
resultCallback);
|
||||
});
|
||||
} else {
|
||||
channel.setMessageHandler(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/** Generated class from Pigeon that represents Flutter messages that can be called from Java. */
|
||||
public static class CameraControlFlutterApi {
|
||||
private final @NonNull BinaryMessenger binaryMessenger;
|
||||
|
||||
public CameraControlFlutterApi(@NonNull BinaryMessenger argBinaryMessenger) {
|
||||
this.binaryMessenger = argBinaryMessenger;
|
||||
}
|
||||
|
||||
/** Public interface for sending reply. */
|
||||
@SuppressWarnings("UnknownNullness")
|
||||
public interface Reply<T> {
|
||||
void reply(T reply);
|
||||
}
|
||||
/** The codec used by CameraControlFlutterApi. */
|
||||
static @NonNull MessageCodec<Object> getCodec() {
|
||||
return new StandardMessageCodec();
|
||||
}
|
||||
|
||||
public void create(@NonNull Long identifierArg, @NonNull Reply<Void> callback) {
|
||||
BasicMessageChannel<Object> channel =
|
||||
new BasicMessageChannel<>(
|
||||
binaryMessenger, "dev.flutter.pigeon.CameraControlFlutterApi.create", getCodec());
|
||||
channel.send(
|
||||
new ArrayList<Object>(Collections.singletonList(identifierArg)),
|
||||
channelReply -> callback.reply(null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,9 +24,12 @@ public class ImageAnalysisHostApiImpl implements ImageAnalysisHostApi {
|
||||
@VisibleForTesting @NonNull public CameraXProxy cameraXProxy = new CameraXProxy();
|
||||
|
||||
public ImageAnalysisHostApiImpl(
|
||||
@NonNull BinaryMessenger binaryMessenger, @NonNull InstanceManager instanceManager) {
|
||||
@NonNull BinaryMessenger binaryMessenger,
|
||||
@NonNull InstanceManager instanceManager,
|
||||
@NonNull Context context) {
|
||||
this.binaryMessenger = binaryMessenger;
|
||||
this.instanceManager = instanceManager;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,111 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package io.flutter.plugins.camerax;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.camera.core.CameraControl;
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import io.flutter.plugin.common.BinaryMessenger;
|
||||
import java.util.Objects;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.MockitoJUnit;
|
||||
import org.mockito.junit.MockitoRule;
|
||||
|
||||
public class CameraControlTest {
|
||||
@Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
|
||||
|
||||
@Mock public BinaryMessenger mockBinaryMessenger;
|
||||
@Mock public CameraControl cameraControl;
|
||||
|
||||
InstanceManager testInstanceManager;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
testInstanceManager = InstanceManager.create(identifier -> {});
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
testInstanceManager.stopFinalizationListener();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void enableTorch_turnsTorchModeOnAndOffAsExpected() {
|
||||
try (MockedStatic<Futures> mockedFutures = Mockito.mockStatic(Futures.class)) {
|
||||
final CameraControlHostApiImpl cameraControlHostApiImpl =
|
||||
new CameraControlHostApiImpl(testInstanceManager, mock(Context.class));
|
||||
final Long cameraControlIdentifier = 88L;
|
||||
final boolean enableTorch = true;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
final ListenableFuture<Void> enableTorchFuture = mock(ListenableFuture.class);
|
||||
|
||||
testInstanceManager.addDartCreatedInstance(cameraControl, cameraControlIdentifier);
|
||||
|
||||
when(cameraControl.enableTorch(true)).thenReturn(enableTorchFuture);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
final ArgumentCaptor<FutureCallback<Void>> futureCallbackCaptor =
|
||||
ArgumentCaptor.forClass(FutureCallback.class);
|
||||
|
||||
// Test turning on torch mode.
|
||||
@SuppressWarnings("unchecked")
|
||||
final GeneratedCameraXLibrary.Result<Void> successfulMockResult =
|
||||
mock(GeneratedCameraXLibrary.Result.class);
|
||||
cameraControlHostApiImpl.enableTorch(
|
||||
cameraControlIdentifier, enableTorch, successfulMockResult);
|
||||
mockedFutures.verify(
|
||||
() -> Futures.addCallback(eq(enableTorchFuture), futureCallbackCaptor.capture(), any()));
|
||||
mockedFutures.clearInvocations();
|
||||
|
||||
FutureCallback<Void> successfulEnableTorchCallback = futureCallbackCaptor.getValue();
|
||||
|
||||
successfulEnableTorchCallback.onSuccess(mock(Void.class));
|
||||
verify(successfulMockResult).success(null);
|
||||
|
||||
// Test turning off torch mode.
|
||||
@SuppressWarnings("unchecked")
|
||||
final GeneratedCameraXLibrary.Result<Void> failedMockResult =
|
||||
mock(GeneratedCameraXLibrary.Result.class);
|
||||
final Throwable testThrowable = new Throwable();
|
||||
cameraControlHostApiImpl.enableTorch(cameraControlIdentifier, enableTorch, failedMockResult);
|
||||
mockedFutures.verify(
|
||||
() -> Futures.addCallback(eq(enableTorchFuture), futureCallbackCaptor.capture(), any()));
|
||||
|
||||
FutureCallback<Void> failedEnableTorchCallback = futureCallbackCaptor.getValue();
|
||||
|
||||
failedEnableTorchCallback.onFailure(testThrowable);
|
||||
verify(failedMockResult).error(testThrowable);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flutterApiCreate_makesCallToCreateInstanceOnDartSide() {
|
||||
final CameraControlFlutterApiImpl spyFlutterApi =
|
||||
spy(new CameraControlFlutterApiImpl(mockBinaryMessenger, testInstanceManager));
|
||||
|
||||
spyFlutterApi.create(cameraControl, reply -> {});
|
||||
|
||||
final long cameraControlIdentifier =
|
||||
Objects.requireNonNull(testInstanceManager.getIdentifierForStrongReference(cameraControl));
|
||||
verify(spyFlutterApi).create(eq(cameraControlIdentifier), any());
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import androidx.camera.core.Camera;
|
||||
import androidx.camera.core.CameraControl;
|
||||
import androidx.camera.core.CameraInfo;
|
||||
import io.flutter.plugin.common.BinaryMessenger;
|
||||
import java.util.Objects;
|
||||
@ -59,6 +60,23 @@ public class CameraTest {
|
||||
verify(camera).getCameraInfo();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getCameraControl_retrievesExpectedCameraControlInstance() {
|
||||
final CameraHostApiImpl cameraHostApiImpl =
|
||||
new CameraHostApiImpl(mockBinaryMessenger, testInstanceManager);
|
||||
final CameraControl mockCameraControl = mock(CameraControl.class);
|
||||
final Long cameraIdentifier = 43L;
|
||||
final Long mockCameraControlIdentifier = 79L;
|
||||
|
||||
testInstanceManager.addDartCreatedInstance(camera, cameraIdentifier);
|
||||
testInstanceManager.addDartCreatedInstance(mockCameraControl, mockCameraControlIdentifier);
|
||||
|
||||
when(camera.getCameraControl()).thenReturn(mockCameraControl);
|
||||
|
||||
assertEquals(cameraHostApiImpl.getCameraControl(cameraIdentifier), mockCameraControlIdentifier);
|
||||
verify(camera).getCameraControl();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flutterApiCreate_makesCallToCreateInstanceOnDartSide() {
|
||||
final CameraFlutterApiImpl spyFlutterApi =
|
||||
|
@ -50,7 +50,7 @@ public class ImageAnalysisTest {
|
||||
@Test
|
||||
public void hostApiCreate_createsExpectedImageAnalysisInstanceWithExpectedIdentifier() {
|
||||
final ImageAnalysisHostApiImpl hostApi =
|
||||
new ImageAnalysisHostApiImpl(mockBinaryMessenger, instanceManager);
|
||||
new ImageAnalysisHostApiImpl(mockBinaryMessenger, instanceManager, context);
|
||||
final CameraXProxy mockCameraXProxy = mock(CameraXProxy.class);
|
||||
final ImageAnalysis.Builder mockImageAnalysisBuilder = mock(ImageAnalysis.Builder.class);
|
||||
final ResolutionSelector mockResolutionSelector = mock(ResolutionSelector.class);
|
||||
@ -72,8 +72,7 @@ public class ImageAnalysisTest {
|
||||
@Test
|
||||
public void setAnalyzer_makesCallToSetAnalyzerOnExpectedImageAnalysisInstance() {
|
||||
final ImageAnalysisHostApiImpl hostApi =
|
||||
new ImageAnalysisHostApiImpl(mockBinaryMessenger, instanceManager);
|
||||
hostApi.setContext(context);
|
||||
new ImageAnalysisHostApiImpl(mockBinaryMessenger, instanceManager, context);
|
||||
|
||||
final ImageAnalysis.Analyzer mockAnalyzer = mock(ImageAnalysis.Analyzer.class);
|
||||
final long analyzerIdentifier = 10;
|
||||
@ -90,7 +89,7 @@ public class ImageAnalysisTest {
|
||||
@Test
|
||||
public void clearAnalyzer_makesCallToClearAnalyzerOnExpectedImageAnalysisInstance() {
|
||||
final ImageAnalysisHostApiImpl hostApi =
|
||||
new ImageAnalysisHostApiImpl(mockBinaryMessenger, instanceManager);
|
||||
new ImageAnalysisHostApiImpl(mockBinaryMessenger, instanceManager, context);
|
||||
final long instanceIdentifier = 22;
|
||||
|
||||
instanceManager.addDartCreatedInstance(mockImageAnalysis, instanceIdentifier);
|
||||
|
@ -274,7 +274,7 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
|
||||
IconButton(
|
||||
icon: const Icon(Icons.flash_on),
|
||||
color: Colors.blue,
|
||||
onPressed: () {}, // TODO(camsim99): Add functionality back here.
|
||||
onPressed: controller != null ? onFlashModeButtonPressed : null,
|
||||
),
|
||||
// The exposure and focus mode are currently not supported on the web.
|
||||
...!kIsWeb
|
||||
@ -326,28 +326,36 @@ class _CameraExampleHomeState extends State<CameraExampleHome>
|
||||
color: controller?.value.flashMode == FlashMode.off
|
||||
? Colors.orange
|
||||
: Colors.blue,
|
||||
onPressed: () {}, // TODO(camsim99): Add functionality back here.
|
||||
onPressed: controller != null
|
||||
? () => onSetFlashModeButtonPressed(FlashMode.off)
|
||||
: null,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.flash_auto),
|
||||
color: controller?.value.flashMode == FlashMode.auto
|
||||
? Colors.orange
|
||||
: Colors.blue,
|
||||
onPressed: () {}, // TODO(camsim99): Add functionality back here.
|
||||
onPressed: controller != null
|
||||
? () => onSetFlashModeButtonPressed(FlashMode.auto)
|
||||
: null,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.flash_on),
|
||||
color: controller?.value.flashMode == FlashMode.always
|
||||
? Colors.orange
|
||||
: Colors.blue,
|
||||
onPressed: () {}, // TODO(camsim99): Add functionality back here.
|
||||
onPressed: controller != null
|
||||
? () => onSetFlashModeButtonPressed(FlashMode.always)
|
||||
: null,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.highlight),
|
||||
color: controller?.value.flashMode == FlashMode.torch
|
||||
? Colors.orange
|
||||
: Colors.blue,
|
||||
onPressed: () {}, // TODO(camsim99): Add functionality back here.
|
||||
onPressed: controller != null
|
||||
? () => onSetFlashModeButtonPressed(FlashMode.torch)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -11,6 +11,7 @@ import 'package:stream_transform/stream_transform.dart';
|
||||
|
||||
import 'analyzer.dart';
|
||||
import 'camera.dart';
|
||||
import 'camera_control.dart';
|
||||
import 'camera_info.dart';
|
||||
import 'camera_selector.dart';
|
||||
import 'camera_state.dart';
|
||||
@ -111,6 +112,10 @@ class AndroidCameraCameraX extends CameraPlatform {
|
||||
/// The flash mode currently configured for [imageCapture].
|
||||
int? _currentFlashMode;
|
||||
|
||||
/// Whether or not torch flash mode has been enabled for the [camera].
|
||||
@visibleForTesting
|
||||
bool torchEnabled = false;
|
||||
|
||||
/// The [ImageAnalysis] instance that can be configured to analyze individual
|
||||
/// frames.
|
||||
ImageAnalysis? imageAnalysis;
|
||||
@ -483,14 +488,37 @@ class AndroidCameraCameraX extends CameraPlatform {
|
||||
Future<XFile> takePicture(int cameraId) async {
|
||||
if (_currentFlashMode != null) {
|
||||
await imageCapture!.setFlashMode(_currentFlashMode!);
|
||||
} else if (torchEnabled) {
|
||||
// Ensure any previously set flash modes are unset when torch mode has
|
||||
// been enabled.
|
||||
await imageCapture!.setFlashMode(ImageCapture.flashModeOff);
|
||||
}
|
||||
final String picturePath = await imageCapture!.takePicture();
|
||||
return XFile(picturePath);
|
||||
}
|
||||
|
||||
/// Sets the flash mode for the selected camera.
|
||||
///
|
||||
/// When the [FlashMode.torch] is enabled, any previously set [FlashMode] with
|
||||
/// this method will be disabled, just as with any other [FlashMode]; while
|
||||
/// this is not default native Android behavior as defined by the CameraX API,
|
||||
/// this behavior is compliant with the plugin platform interface.
|
||||
///
|
||||
/// This method combines the notion of setting the flash mode of the
|
||||
/// [imageCapture] UseCase and enabling the camera torch, as described
|
||||
/// by https://developer.android.com/reference/androidx/camera/core/ImageCapture
|
||||
/// and https://developer.android.com/reference/androidx/camera/core/CameraControl#enableTorch(boolean),
|
||||
/// respectively.
|
||||
@override
|
||||
Future<void> setFlashMode(int cameraId, FlashMode mode) async {
|
||||
CameraControl? cameraControl;
|
||||
// Turn off torch mode if it is enabled and not being redundantly set.
|
||||
if (mode != FlashMode.torch && torchEnabled) {
|
||||
cameraControl = await camera!.getCameraControl();
|
||||
await cameraControl.enableTorch(false);
|
||||
torchEnabled = false;
|
||||
}
|
||||
|
||||
switch (mode) {
|
||||
case FlashMode.off:
|
||||
_currentFlashMode = ImageCapture.flashModeOff;
|
||||
@ -502,7 +530,14 @@ class AndroidCameraCameraX extends CameraPlatform {
|
||||
_currentFlashMode = ImageCapture.flashModeOn;
|
||||
break;
|
||||
case FlashMode.torch:
|
||||
// TODO(camsim99): Implement torch mode when CameraControl is wrapped.
|
||||
_currentFlashMode = null;
|
||||
if (torchEnabled) {
|
||||
// Torch mode enabled already.
|
||||
return;
|
||||
}
|
||||
cameraControl = await camera!.getCameraControl();
|
||||
await cameraControl.enableTorch(true);
|
||||
torchEnabled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
import 'analyzer.dart';
|
||||
import 'camera.dart';
|
||||
import 'camera_control.dart';
|
||||
import 'camera_info.dart';
|
||||
import 'camera_selector.dart';
|
||||
import 'camera_state.dart';
|
||||
@ -26,27 +27,27 @@ import 'zoom_state.dart';
|
||||
/// Handles initialization of Flutter APIs for the Android CameraX library.
|
||||
class AndroidCameraXCameraFlutterApis {
|
||||
/// Creates a [AndroidCameraXCameraFlutterApis].
|
||||
AndroidCameraXCameraFlutterApis({
|
||||
JavaObjectFlutterApiImpl? javaObjectFlutterApiImpl,
|
||||
CameraFlutterApiImpl? cameraFlutterApiImpl,
|
||||
CameraInfoFlutterApiImpl? cameraInfoFlutterApiImpl,
|
||||
CameraSelectorFlutterApiImpl? cameraSelectorFlutterApiImpl,
|
||||
ProcessCameraProviderFlutterApiImpl? processCameraProviderFlutterApiImpl,
|
||||
SystemServicesFlutterApiImpl? systemServicesFlutterApiImpl,
|
||||
CameraStateErrorFlutterApiImpl? cameraStateErrorFlutterApiImpl,
|
||||
CameraStateFlutterApiImpl? cameraStateFlutterApiImpl,
|
||||
PendingRecordingFlutterApiImpl? pendingRecordingFlutterApiImpl,
|
||||
RecordingFlutterApiImpl? recordingFlutterApiImpl,
|
||||
RecorderFlutterApiImpl? recorderFlutterApiImpl,
|
||||
VideoCaptureFlutterApiImpl? videoCaptureFlutterApiImpl,
|
||||
ExposureStateFlutterApiImpl? exposureStateFlutterApiImpl,
|
||||
ZoomStateFlutterApiImpl? zoomStateFlutterApiImpl,
|
||||
LiveDataFlutterApiImpl? liveDataFlutterApiImpl,
|
||||
ObserverFlutterApiImpl? observerFlutterApiImpl,
|
||||
ImageProxyFlutterApiImpl? imageProxyFlutterApiImpl,
|
||||
PlaneProxyFlutterApiImpl? planeProxyFlutterApiImpl,
|
||||
AnalyzerFlutterApiImpl? analyzerFlutterApiImpl,
|
||||
}) {
|
||||
AndroidCameraXCameraFlutterApis(
|
||||
{JavaObjectFlutterApiImpl? javaObjectFlutterApiImpl,
|
||||
CameraFlutterApiImpl? cameraFlutterApiImpl,
|
||||
CameraInfoFlutterApiImpl? cameraInfoFlutterApiImpl,
|
||||
CameraSelectorFlutterApiImpl? cameraSelectorFlutterApiImpl,
|
||||
ProcessCameraProviderFlutterApiImpl? processCameraProviderFlutterApiImpl,
|
||||
SystemServicesFlutterApiImpl? systemServicesFlutterApiImpl,
|
||||
CameraStateErrorFlutterApiImpl? cameraStateErrorFlutterApiImpl,
|
||||
CameraStateFlutterApiImpl? cameraStateFlutterApiImpl,
|
||||
PendingRecordingFlutterApiImpl? pendingRecordingFlutterApiImpl,
|
||||
RecordingFlutterApiImpl? recordingFlutterApiImpl,
|
||||
RecorderFlutterApiImpl? recorderFlutterApiImpl,
|
||||
VideoCaptureFlutterApiImpl? videoCaptureFlutterApiImpl,
|
||||
ExposureStateFlutterApiImpl? exposureStateFlutterApiImpl,
|
||||
ZoomStateFlutterApiImpl? zoomStateFlutterApiImpl,
|
||||
LiveDataFlutterApiImpl? liveDataFlutterApiImpl,
|
||||
ObserverFlutterApiImpl? observerFlutterApiImpl,
|
||||
ImageProxyFlutterApiImpl? imageProxyFlutterApiImpl,
|
||||
PlaneProxyFlutterApiImpl? planeProxyFlutterApiImpl,
|
||||
AnalyzerFlutterApiImpl? analyzerFlutterApiImpl,
|
||||
CameraControlFlutterApiImpl? cameraControlFlutterApiImpl}) {
|
||||
this.javaObjectFlutterApiImpl =
|
||||
javaObjectFlutterApiImpl ?? JavaObjectFlutterApiImpl();
|
||||
this.cameraInfoFlutterApiImpl =
|
||||
@ -85,6 +86,8 @@ class AndroidCameraXCameraFlutterApis {
|
||||
imageProxyFlutterApiImpl ?? ImageProxyFlutterApiImpl();
|
||||
this.planeProxyFlutterApiImpl =
|
||||
planeProxyFlutterApiImpl ?? PlaneProxyFlutterApiImpl();
|
||||
this.cameraControlFlutterApiImpl =
|
||||
cameraControlFlutterApiImpl ?? CameraControlFlutterApiImpl();
|
||||
}
|
||||
|
||||
static bool _haveBeenSetUp = false;
|
||||
@ -153,6 +156,9 @@ class AndroidCameraXCameraFlutterApis {
|
||||
/// Flutter Api implementation for [PlaneProxy].
|
||||
late final PlaneProxyFlutterApiImpl planeProxyFlutterApiImpl;
|
||||
|
||||
/// Flutter Api implementation for [CameraControl].
|
||||
late final CameraControlFlutterApiImpl cameraControlFlutterApiImpl;
|
||||
|
||||
/// Ensures all the Flutter APIs have been setup to receive calls from native code.
|
||||
void ensureSetUp() {
|
||||
if (!_haveBeenSetUp) {
|
||||
@ -176,6 +182,7 @@ class AndroidCameraXCameraFlutterApis {
|
||||
PlaneProxyFlutterApi.setup(planeProxyFlutterApiImpl);
|
||||
LiveDataFlutterApi.setup(liveDataFlutterApiImpl);
|
||||
ObserverFlutterApi.setup(observerFlutterApiImpl);
|
||||
CameraControlFlutterApi.setup(cameraControlFlutterApiImpl);
|
||||
_haveBeenSetUp = true;
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import 'package:flutter/services.dart' show BinaryMessenger;
|
||||
import 'package:meta/meta.dart' show immutable;
|
||||
|
||||
import 'android_camera_camerax_flutter_api_impls.dart';
|
||||
import 'camera_control.dart';
|
||||
import 'camera_info.dart';
|
||||
import 'camerax_library.g.dart';
|
||||
import 'instance_manager.dart';
|
||||
@ -23,27 +24,33 @@ class Camera extends JavaObject {
|
||||
: super.detached(
|
||||
binaryMessenger: binaryMessenger,
|
||||
instanceManager: instanceManager) {
|
||||
_api = CameraHostApiImpl(
|
||||
_api = _CameraHostApiImpl(
|
||||
binaryMessenger: binaryMessenger, instanceManager: instanceManager);
|
||||
AndroidCameraXCameraFlutterApis.instance.ensureSetUp();
|
||||
}
|
||||
|
||||
late final CameraHostApiImpl _api;
|
||||
late final _CameraHostApiImpl _api;
|
||||
|
||||
/// Retrieve the [CameraInfo] instance that contains information about this
|
||||
/// Retrieves the [CameraInfo] instance that contains information about this
|
||||
/// instance.
|
||||
Future<CameraInfo> getCameraInfo() async {
|
||||
return _api.getCameraInfoFromInstance(this);
|
||||
}
|
||||
|
||||
/// Retrieves the [CameraControl] instance that provides asynchronous
|
||||
/// operations like zoom and focus & metering for this instance.
|
||||
Future<CameraControl> getCameraControl() async {
|
||||
return _api.getCameraControlFromInstance(this);
|
||||
}
|
||||
}
|
||||
|
||||
/// Host API implementation of [Camera].
|
||||
class CameraHostApiImpl extends CameraHostApi {
|
||||
/// Constructs a [CameraHostApiImpl].
|
||||
class _CameraHostApiImpl extends CameraHostApi {
|
||||
/// Constructs a [_CameraHostApiImpl].
|
||||
///
|
||||
/// An [instanceManager] is typically passed when a copy of an instance
|
||||
/// contained by an [InstanceManager] is being created.
|
||||
CameraHostApiImpl({this.binaryMessenger, InstanceManager? instanceManager})
|
||||
_CameraHostApiImpl({this.binaryMessenger, InstanceManager? instanceManager})
|
||||
: super(binaryMessenger: binaryMessenger) {
|
||||
this.instanceManager = instanceManager ?? JavaObject.globalInstanceManager;
|
||||
}
|
||||
@ -57,7 +64,7 @@ class CameraHostApiImpl extends CameraHostApi {
|
||||
/// Maintains instances stored to communicate with native language objects.
|
||||
late final InstanceManager instanceManager;
|
||||
|
||||
/// Gets the [CameraInfo] associated with the specified instance of [Camera].
|
||||
/// Gets the [CameraInfo] associated with the specified [Camera] instance.
|
||||
Future<CameraInfo> getCameraInfoFromInstance(Camera instance) async {
|
||||
final int identifier = instanceManager.getIdentifier(instance)!;
|
||||
final int cameraInfoId = await getCameraInfo(identifier);
|
||||
@ -65,6 +72,15 @@ class CameraHostApiImpl extends CameraHostApi {
|
||||
return instanceManager
|
||||
.getInstanceWithWeakReference<CameraInfo>(cameraInfoId)!;
|
||||
}
|
||||
|
||||
/// Gets the [CameraControl] associated with the specified [Camera] instance.
|
||||
Future<CameraControl> getCameraControlFromInstance(Camera instance) async {
|
||||
final int identifier = instanceManager.getIdentifier(instance)!;
|
||||
final int cameraControlId = await getCameraControl(identifier);
|
||||
|
||||
return instanceManager
|
||||
.getInstanceWithWeakReference<CameraControl>(cameraControlId)!;
|
||||
}
|
||||
}
|
||||
|
||||
/// Flutter API implementation of [Camera].
|
||||
|
@ -0,0 +1,109 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:flutter/services.dart' show BinaryMessenger, PlatformException;
|
||||
import 'package:meta/meta.dart' show immutable;
|
||||
|
||||
import 'android_camera_camerax_flutter_api_impls.dart';
|
||||
import 'camerax_library.g.dart';
|
||||
import 'instance_manager.dart';
|
||||
import 'java_object.dart';
|
||||
import 'system_services.dart';
|
||||
|
||||
/// The interface that provides asynchronous operations like zoom and focus &
|
||||
/// metering, which affects output of all [UseCase]s currently bound to the
|
||||
/// corresponding [Camera] instance.
|
||||
///
|
||||
/// See https://developer.android.com/reference/androidx/camera/core/CameraControl.
|
||||
@immutable
|
||||
class CameraControl extends JavaObject {
|
||||
/// Constructs a [CameraControl] that is not automatically attached to a native object.
|
||||
CameraControl.detached(
|
||||
{BinaryMessenger? binaryMessenger, InstanceManager? instanceManager})
|
||||
: super.detached(
|
||||
binaryMessenger: binaryMessenger,
|
||||
instanceManager: instanceManager) {
|
||||
_api = _CameraControlHostApiImpl(
|
||||
binaryMessenger: binaryMessenger, instanceManager: instanceManager);
|
||||
AndroidCameraXCameraFlutterApis.instance.ensureSetUp();
|
||||
}
|
||||
|
||||
late final _CameraControlHostApiImpl _api;
|
||||
|
||||
/// Enables or disables the torch of related [Camera] instance.
|
||||
Future<void> enableTorch(bool torch) async {
|
||||
return _api.enableTorchFromInstance(this, torch);
|
||||
}
|
||||
}
|
||||
|
||||
/// Host API implementation of [CameraControl].
|
||||
class _CameraControlHostApiImpl extends CameraControlHostApi {
|
||||
/// Constructs a [_CameraControlHostApiImpl].
|
||||
///
|
||||
/// An [instanceManager] is typically passed when a copy of an instance
|
||||
/// contained by an [InstanceManager] is being created.
|
||||
_CameraControlHostApiImpl(
|
||||
{this.binaryMessenger, InstanceManager? instanceManager})
|
||||
: super(binaryMessenger: binaryMessenger) {
|
||||
this.instanceManager = instanceManager ?? JavaObject.globalInstanceManager;
|
||||
}
|
||||
|
||||
/// Receives binary data across the Flutter platform barrier.
|
||||
///
|
||||
/// If it is null, the default BinaryMessenger will be used which routes to
|
||||
/// the host platform.
|
||||
final BinaryMessenger? binaryMessenger;
|
||||
|
||||
/// Maintains instances stored to communicate with native language objects.
|
||||
late final InstanceManager instanceManager;
|
||||
|
||||
/// Enables or disables the torch for the specified [CameraControl] instance.
|
||||
Future<void> enableTorchFromInstance(
|
||||
CameraControl instance, bool torch) async {
|
||||
final int identifier = instanceManager.getIdentifier(instance)!;
|
||||
try {
|
||||
await enableTorch(identifier, torch);
|
||||
} on PlatformException catch (e) {
|
||||
SystemServices.cameraErrorStreamController
|
||||
.add(e.message ?? 'The camera was unable to change torch modes.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Flutter API implementation of [CameraControl].
|
||||
class CameraControlFlutterApiImpl extends CameraControlFlutterApi {
|
||||
/// Constructs a [CameraControlFlutterApiImpl].
|
||||
///
|
||||
/// If [binaryMessenger] is null, the default [BinaryMessenger] will be used,
|
||||
/// which routes to the host platform.
|
||||
///
|
||||
/// An [instanceManager] is typically passed when a copy of an instance
|
||||
/// contained by an [InstanceManager] is being created. If left null, it
|
||||
/// will default to the global instance defined in [JavaObject].
|
||||
CameraControlFlutterApiImpl({
|
||||
BinaryMessenger? binaryMessenger,
|
||||
InstanceManager? instanceManager,
|
||||
}) : _binaryMessenger = binaryMessenger,
|
||||
_instanceManager = instanceManager ?? JavaObject.globalInstanceManager;
|
||||
|
||||
/// Receives binary data across the Flutter platform barrier.
|
||||
final BinaryMessenger? _binaryMessenger;
|
||||
|
||||
/// Maintains instances stored to communicate with native language objects.
|
||||
final InstanceManager _instanceManager;
|
||||
|
||||
@override
|
||||
void create(int identifier) {
|
||||
_instanceManager.addHostCreatedInstance(
|
||||
CameraControl.detached(
|
||||
binaryMessenger: _binaryMessenger, instanceManager: _instanceManager),
|
||||
identifier,
|
||||
onCopy: (CameraControl original) {
|
||||
return CameraControl.detached(
|
||||
binaryMessenger: _binaryMessenger,
|
||||
instanceManager: _instanceManager);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -780,6 +780,33 @@ class CameraHostApi {
|
||||
return (replyList[0] as int?)!;
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> getCameraControl(int arg_identifier) async {
|
||||
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
|
||||
'dev.flutter.pigeon.CameraHostApi.getCameraControl', codec,
|
||||
binaryMessenger: _binaryMessenger);
|
||||
final List<Object?>? replyList =
|
||||
await channel.send(<Object?>[arg_identifier]) as List<Object?>?;
|
||||
if (replyList == null) {
|
||||
throw PlatformException(
|
||||
code: 'channel-error',
|
||||
message: 'Unable to establish connection on channel.',
|
||||
);
|
||||
} else if (replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: replyList[0]! as String,
|
||||
message: replyList[1] as String?,
|
||||
details: replyList[2],
|
||||
);
|
||||
} else if (replyList[0] == null) {
|
||||
throw PlatformException(
|
||||
code: 'null-error',
|
||||
message: 'Host platform returned null value for non-null return value.',
|
||||
);
|
||||
} else {
|
||||
return (replyList[0] as int?)!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class CameraFlutterApi {
|
||||
@ -2626,3 +2653,65 @@ class FallbackStrategyHostApi {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CameraControlHostApi {
|
||||
/// Constructor for [CameraControlHostApi]. The [binaryMessenger] named argument is
|
||||
/// available for dependency injection. If it is left null, the default
|
||||
/// BinaryMessenger will be used which routes to the host platform.
|
||||
CameraControlHostApi({BinaryMessenger? binaryMessenger})
|
||||
: _binaryMessenger = binaryMessenger;
|
||||
final BinaryMessenger? _binaryMessenger;
|
||||
|
||||
static const MessageCodec<Object?> codec = StandardMessageCodec();
|
||||
|
||||
Future<void> enableTorch(int arg_identifier, bool arg_torch) async {
|
||||
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
|
||||
'dev.flutter.pigeon.CameraControlHostApi.enableTorch', codec,
|
||||
binaryMessenger: _binaryMessenger);
|
||||
final List<Object?>? replyList = await channel
|
||||
.send(<Object?>[arg_identifier, arg_torch]) as List<Object?>?;
|
||||
if (replyList == null) {
|
||||
throw PlatformException(
|
||||
code: 'channel-error',
|
||||
message: 'Unable to establish connection on channel.',
|
||||
);
|
||||
} else if (replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: replyList[0]! as String,
|
||||
message: replyList[1] as String?,
|
||||
details: replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class CameraControlFlutterApi {
|
||||
static const MessageCodec<Object?> codec = StandardMessageCodec();
|
||||
|
||||
void create(int identifier);
|
||||
|
||||
static void setup(CameraControlFlutterApi? api,
|
||||
{BinaryMessenger? binaryMessenger}) {
|
||||
{
|
||||
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
|
||||
'dev.flutter.pigeon.CameraControlFlutterApi.create', codec,
|
||||
binaryMessenger: binaryMessenger);
|
||||
if (api == null) {
|
||||
channel.setMessageHandler(null);
|
||||
} else {
|
||||
channel.setMessageHandler((Object? message) async {
|
||||
assert(message != null,
|
||||
'Argument for dev.flutter.pigeon.CameraControlFlutterApi.create was null.');
|
||||
final List<Object?> args = (message as List<Object?>?)!;
|
||||
final int? arg_identifier = (args[0] as int?);
|
||||
assert(arg_identifier != null,
|
||||
'Argument for dev.flutter.pigeon.CameraControlFlutterApi.create was null, expected non-null int.');
|
||||
api.create(arg_identifier!);
|
||||
return;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -197,6 +197,8 @@ abstract class ProcessCameraProviderFlutterApi {
|
||||
@HostApi(dartHostTestHandler: 'TestCameraHostApi')
|
||||
abstract class CameraHostApi {
|
||||
int getCameraInfo(int identifier);
|
||||
|
||||
int getCameraControl(int identifier);
|
||||
}
|
||||
|
||||
@FlutterApi()
|
||||
@ -417,3 +419,14 @@ abstract class FallbackStrategyHostApi {
|
||||
void create(int identifier, VideoQuality quality,
|
||||
VideoResolutionFallbackRule fallbackRule);
|
||||
}
|
||||
|
||||
@HostApi(dartHostTestHandler: 'TestCameraControlHostApi')
|
||||
abstract class CameraControlHostApi {
|
||||
@async
|
||||
void enableTorch(int identifier, bool torch);
|
||||
}
|
||||
|
||||
@FlutterApi()
|
||||
abstract class CameraControlFlutterApi {
|
||||
void create(int identifier);
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ name: camera_android_camerax
|
||||
description: Android implementation of the camera plugin using the CameraX library.
|
||||
repository: https://github.com/flutter/packages/tree/main/packages/camera/camera_android_camerax
|
||||
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22
|
||||
version: 0.5.0+18
|
||||
version: 0.5.0+19
|
||||
|
||||
environment:
|
||||
sdk: ">=2.19.0 <4.0.0"
|
||||
|
@ -8,6 +8,7 @@ import 'package:async/async.dart';
|
||||
import 'package:camera_android_camerax/camera_android_camerax.dart';
|
||||
import 'package:camera_android_camerax/src/analyzer.dart';
|
||||
import 'package:camera_android_camerax/src/camera.dart';
|
||||
import 'package:camera_android_camerax/src/camera_control.dart';
|
||||
import 'package:camera_android_camerax/src/camera_info.dart';
|
||||
import 'package:camera_android_camerax/src/camera_selector.dart';
|
||||
import 'package:camera_android_camerax/src/camera_state.dart';
|
||||
@ -46,6 +47,7 @@ import 'test_camerax_library.g.dart';
|
||||
@GenerateNiceMocks(<MockSpec<Object>>[
|
||||
MockSpec<Camera>(),
|
||||
MockSpec<CameraInfo>(),
|
||||
MockSpec<CameraControl>(),
|
||||
MockSpec<CameraImageData>(),
|
||||
MockSpec<CameraSelector>(),
|
||||
MockSpec<ExposureState>(),
|
||||
@ -1025,12 +1027,35 @@ void main() {
|
||||
expect(imageFile.path, equals(testPicturePath));
|
||||
});
|
||||
|
||||
test('setFlashMode configures ImageCapture with expected flash mode',
|
||||
test('takePicture turns non-torch flash mode off when torch mode enabled',
|
||||
() async {
|
||||
final AndroidCameraCameraX camera = AndroidCameraCameraX();
|
||||
const int cameraId = 77;
|
||||
final MockCameraControl mockCameraControl = MockCameraControl();
|
||||
|
||||
camera.imageCapture = MockImageCapture();
|
||||
camera.camera = MockCamera();
|
||||
|
||||
when(camera.camera!.getCameraControl())
|
||||
.thenAnswer((_) async => mockCameraControl);
|
||||
|
||||
await camera.setFlashMode(cameraId, FlashMode.torch);
|
||||
await camera.takePicture(cameraId);
|
||||
verify(camera.imageCapture!.setFlashMode(ImageCapture.flashModeOff));
|
||||
});
|
||||
|
||||
test(
|
||||
'setFlashMode configures ImageCapture with expected non-torch flash mode',
|
||||
() async {
|
||||
final AndroidCameraCameraX camera = AndroidCameraCameraX();
|
||||
const int cameraId = 22;
|
||||
final MockCameraControl mockCameraControl = MockCameraControl();
|
||||
|
||||
camera.imageCapture = MockImageCapture();
|
||||
camera.camera = MockCamera();
|
||||
|
||||
when(camera.camera!.getCameraControl())
|
||||
.thenAnswer((_) async => mockCameraControl);
|
||||
|
||||
for (final FlashMode flashMode in FlashMode.values) {
|
||||
await camera.setFlashMode(cameraId, flashMode);
|
||||
@ -1047,19 +1072,68 @@ void main() {
|
||||
expectedFlashMode = ImageCapture.flashModeOn;
|
||||
break;
|
||||
case FlashMode.torch:
|
||||
// TODO(camsim99): Test torch mode when implemented.
|
||||
expectedFlashMode = null;
|
||||
break;
|
||||
}
|
||||
|
||||
if (expectedFlashMode == null) {
|
||||
// Torch mode enabled and won't be used for configuring image capture.
|
||||
continue;
|
||||
}
|
||||
|
||||
verifyNever(mockCameraControl.enableTorch(true));
|
||||
expect(camera.torchEnabled, isFalse);
|
||||
await camera.takePicture(cameraId);
|
||||
verify(camera.imageCapture!.setFlashMode(expectedFlashMode));
|
||||
}
|
||||
});
|
||||
|
||||
test('setFlashMode turns on torch mode as expected', () async {
|
||||
final AndroidCameraCameraX camera = AndroidCameraCameraX();
|
||||
const int cameraId = 44;
|
||||
final MockCameraControl mockCameraControl = MockCameraControl();
|
||||
|
||||
camera.camera = MockCamera();
|
||||
|
||||
when(camera.camera!.getCameraControl())
|
||||
.thenAnswer((_) async => mockCameraControl);
|
||||
|
||||
await camera.setFlashMode(cameraId, FlashMode.torch);
|
||||
|
||||
verify(mockCameraControl.enableTorch(true));
|
||||
expect(camera.torchEnabled, isTrue);
|
||||
});
|
||||
|
||||
test('setFlashMode turns off torch mode when non-torch flash modes set',
|
||||
() async {
|
||||
final AndroidCameraCameraX camera = AndroidCameraCameraX();
|
||||
const int cameraId = 33;
|
||||
final MockCameraControl mockCameraControl = MockCameraControl();
|
||||
|
||||
camera.camera = MockCamera();
|
||||
|
||||
when(camera.camera!.getCameraControl())
|
||||
.thenAnswer((_) async => mockCameraControl);
|
||||
|
||||
for (final FlashMode flashMode in FlashMode.values) {
|
||||
camera.torchEnabled = true;
|
||||
await camera.setFlashMode(cameraId, flashMode);
|
||||
|
||||
switch (flashMode) {
|
||||
case FlashMode.off:
|
||||
case FlashMode.auto:
|
||||
case FlashMode.always:
|
||||
verify(mockCameraControl.enableTorch(false));
|
||||
expect(camera.torchEnabled, isFalse);
|
||||
break;
|
||||
case FlashMode.torch:
|
||||
verifyNever(mockCameraControl.enableTorch(true));
|
||||
expect(camera.torchEnabled, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('getMinExposureOffset returns expected exposure offset', () async {
|
||||
final AndroidCameraCameraX camera = AndroidCameraCameraX();
|
||||
final MockCameraInfo mockCameraInfo = MockCameraInfo();
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,67 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:camera_android_camerax/src/camera_control.dart';
|
||||
import 'package:camera_android_camerax/src/instance_manager.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
import 'camera_control_test.mocks.dart';
|
||||
import 'test_camerax_library.g.dart';
|
||||
|
||||
@GenerateMocks(<Type>[TestCameraControlHostApi, TestInstanceManagerHostApi])
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Mocks the call to clear the native InstanceManager.
|
||||
TestInstanceManagerHostApi.setup(MockTestInstanceManagerHostApi());
|
||||
|
||||
group('CameraControl', () {
|
||||
tearDown(() => TestCameraHostApi.setup(null));
|
||||
|
||||
test('enableTorch makes call on Java side to enable torch', () async {
|
||||
final MockTestCameraControlHostApi mockApi =
|
||||
MockTestCameraControlHostApi();
|
||||
TestCameraControlHostApi.setup(mockApi);
|
||||
|
||||
final InstanceManager instanceManager = InstanceManager(
|
||||
onWeakReferenceRemoved: (_) {},
|
||||
);
|
||||
|
||||
final CameraControl cameraControl = CameraControl.detached(
|
||||
instanceManager: instanceManager,
|
||||
);
|
||||
const int cameraControlIdentifier = 22;
|
||||
|
||||
instanceManager.addHostCreatedInstance(
|
||||
cameraControl,
|
||||
cameraControlIdentifier,
|
||||
onCopy: (_) => CameraControl.detached(instanceManager: instanceManager),
|
||||
);
|
||||
|
||||
const bool enableTorch = true;
|
||||
await cameraControl.enableTorch(enableTorch);
|
||||
|
||||
verify(mockApi.enableTorch(cameraControlIdentifier, enableTorch));
|
||||
});
|
||||
|
||||
test('flutterApiCreate makes call to add instance to instance manager', () {
|
||||
final InstanceManager instanceManager = InstanceManager(
|
||||
onWeakReferenceRemoved: (_) {},
|
||||
);
|
||||
final CameraControlFlutterApiImpl flutterApi =
|
||||
CameraControlFlutterApiImpl(
|
||||
instanceManager: instanceManager,
|
||||
);
|
||||
const int cameraControlIdentifier = 67;
|
||||
|
||||
flutterApi.create(cameraControlIdentifier);
|
||||
|
||||
expect(
|
||||
instanceManager.getInstanceWithWeakReference(cameraControlIdentifier),
|
||||
isA<CameraControl>());
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
// Mocks generated by Mockito 5.4.1 from annotations
|
||||
// in camera_android_camerax/test/camera_control_test.dart.
|
||||
// Do not manually edit this file.
|
||||
|
||||
// @dart=2.19
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'dart:async' as _i3;
|
||||
|
||||
import 'package:mockito/mockito.dart' as _i1;
|
||||
|
||||
import 'test_camerax_library.g.dart' as _i2;
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: avoid_redundant_argument_values
|
||||
// ignore_for_file: avoid_setters_without_getters
|
||||
// ignore_for_file: comment_references
|
||||
// ignore_for_file: implementation_imports
|
||||
// ignore_for_file: invalid_use_of_visible_for_testing_member
|
||||
// ignore_for_file: prefer_const_constructors
|
||||
// ignore_for_file: unnecessary_parenthesis
|
||||
// ignore_for_file: camel_case_types
|
||||
// ignore_for_file: subtype_of_sealed_class
|
||||
|
||||
/// A class which mocks [TestCameraControlHostApi].
|
||||
///
|
||||
/// See the documentation for Mockito's code generation for more information.
|
||||
class MockTestCameraControlHostApi extends _i1.Mock
|
||||
implements _i2.TestCameraControlHostApi {
|
||||
MockTestCameraControlHostApi() {
|
||||
_i1.throwOnMissingStub(this);
|
||||
}
|
||||
|
||||
@override
|
||||
_i3.Future<void> enableTorch(
|
||||
int? identifier,
|
||||
bool? torch,
|
||||
) =>
|
||||
(super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#enableTorch,
|
||||
[
|
||||
identifier,
|
||||
torch,
|
||||
],
|
||||
),
|
||||
returnValue: _i3.Future<void>.value(),
|
||||
returnValueForMissingStub: _i3.Future<void>.value(),
|
||||
) as _i3.Future<void>);
|
||||
}
|
||||
|
||||
/// A class which mocks [TestInstanceManagerHostApi].
|
||||
///
|
||||
/// See the documentation for Mockito's code generation for more information.
|
||||
class MockTestInstanceManagerHostApi extends _i1.Mock
|
||||
implements _i2.TestInstanceManagerHostApi {
|
||||
MockTestInstanceManagerHostApi() {
|
||||
_i1.throwOnMissingStub(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void clear() => super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#clear,
|
||||
[],
|
||||
),
|
||||
returnValueForMissingStub: null,
|
||||
);
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:camera_android_camerax/src/camera.dart';
|
||||
import 'package:camera_android_camerax/src/camera_control.dart';
|
||||
import 'package:camera_android_camerax/src/camera_info.dart';
|
||||
import 'package:camera_android_camerax/src/instance_manager.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@ -54,6 +55,39 @@ void main() {
|
||||
verify(mockApi.getCameraInfo(cameraIdentifier));
|
||||
});
|
||||
|
||||
test('getCameraControl makes call to retrieve expected CameraControl',
|
||||
() async {
|
||||
final MockTestCameraHostApi mockApi = MockTestCameraHostApi();
|
||||
TestCameraHostApi.setup(mockApi);
|
||||
|
||||
final InstanceManager instanceManager = InstanceManager(
|
||||
onWeakReferenceRemoved: (_) {},
|
||||
);
|
||||
|
||||
final Camera camera = Camera.detached(
|
||||
instanceManager: instanceManager,
|
||||
);
|
||||
const int cameraIdentifier = 42;
|
||||
final CameraControl cameraControl = CameraControl.detached();
|
||||
const int cameraControlIdentifier = 8;
|
||||
instanceManager.addHostCreatedInstance(
|
||||
camera,
|
||||
cameraIdentifier,
|
||||
onCopy: (_) => Camera.detached(instanceManager: instanceManager),
|
||||
);
|
||||
instanceManager.addHostCreatedInstance(
|
||||
cameraControl,
|
||||
cameraControlIdentifier,
|
||||
onCopy: (_) => CameraControl.detached(instanceManager: instanceManager),
|
||||
);
|
||||
|
||||
when(mockApi.getCameraControl(cameraIdentifier))
|
||||
.thenAnswer((_) => cameraControlIdentifier);
|
||||
|
||||
expect(await camera.getCameraControl(), equals(cameraControl));
|
||||
verify(mockApi.getCameraControl(cameraIdentifier));
|
||||
});
|
||||
|
||||
test('flutterApiCreate makes call to add instance to instance manager', () {
|
||||
final InstanceManager instanceManager = InstanceManager(
|
||||
onWeakReferenceRemoved: (_) {},
|
||||
|
@ -36,6 +36,14 @@ class MockTestCameraHostApi extends _i1.Mock implements _i2.TestCameraHostApi {
|
||||
),
|
||||
returnValue: 0,
|
||||
) as int);
|
||||
@override
|
||||
int getCameraControl(int? identifier) => (super.noSuchMethod(
|
||||
Invocation.method(
|
||||
#getCameraControl,
|
||||
[identifier],
|
||||
),
|
||||
returnValue: 0,
|
||||
) as int);
|
||||
}
|
||||
|
||||
/// A class which mocks [TestInstanceManagerHostApi].
|
||||
|
@ -426,6 +426,8 @@ abstract class TestCameraHostApi {
|
||||
|
||||
int getCameraInfo(int identifier);
|
||||
|
||||
int getCameraControl(int identifier);
|
||||
|
||||
static void setup(TestCameraHostApi? api,
|
||||
{BinaryMessenger? binaryMessenger}) {
|
||||
{
|
||||
@ -450,6 +452,28 @@ abstract class TestCameraHostApi {
|
||||
});
|
||||
}
|
||||
}
|
||||
{
|
||||
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
|
||||
'dev.flutter.pigeon.CameraHostApi.getCameraControl', codec,
|
||||
binaryMessenger: binaryMessenger);
|
||||
if (api == null) {
|
||||
_testBinaryMessengerBinding!.defaultBinaryMessenger
|
||||
.setMockDecodedMessageHandler<Object?>(channel, null);
|
||||
} else {
|
||||
_testBinaryMessengerBinding!.defaultBinaryMessenger
|
||||
.setMockDecodedMessageHandler<Object?>(channel,
|
||||
(Object? message) async {
|
||||
assert(message != null,
|
||||
'Argument for dev.flutter.pigeon.CameraHostApi.getCameraControl was null.');
|
||||
final List<Object?> args = (message as List<Object?>?)!;
|
||||
final int? arg_identifier = (args[0] as int?);
|
||||
assert(arg_identifier != null,
|
||||
'Argument for dev.flutter.pigeon.CameraHostApi.getCameraControl was null, expected non-null int.');
|
||||
final int output = api.getCameraControl(arg_identifier!);
|
||||
return <Object?>[output];
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1721,3 +1745,40 @@ abstract class TestFallbackStrategyHostApi {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class TestCameraControlHostApi {
|
||||
static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding =>
|
||||
TestDefaultBinaryMessengerBinding.instance;
|
||||
static const MessageCodec<Object?> codec = StandardMessageCodec();
|
||||
|
||||
Future<void> enableTorch(int identifier, bool torch);
|
||||
|
||||
static void setup(TestCameraControlHostApi? api,
|
||||
{BinaryMessenger? binaryMessenger}) {
|
||||
{
|
||||
final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
|
||||
'dev.flutter.pigeon.CameraControlHostApi.enableTorch', codec,
|
||||
binaryMessenger: binaryMessenger);
|
||||
if (api == null) {
|
||||
_testBinaryMessengerBinding!.defaultBinaryMessenger
|
||||
.setMockDecodedMessageHandler<Object?>(channel, null);
|
||||
} else {
|
||||
_testBinaryMessengerBinding!.defaultBinaryMessenger
|
||||
.setMockDecodedMessageHandler<Object?>(channel,
|
||||
(Object? message) async {
|
||||
assert(message != null,
|
||||
'Argument for dev.flutter.pigeon.CameraControlHostApi.enableTorch was null.');
|
||||
final List<Object?> args = (message as List<Object?>?)!;
|
||||
final int? arg_identifier = (args[0] as int?);
|
||||
assert(arg_identifier != null,
|
||||
'Argument for dev.flutter.pigeon.CameraControlHostApi.enableTorch was null, expected non-null int.');
|
||||
final bool? arg_torch = (args[1] as bool?);
|
||||
assert(arg_torch != null,
|
||||
'Argument for dev.flutter.pigeon.CameraControlHostApi.enableTorch was null, expected non-null bool.');
|
||||
await api.enableTorch(arg_identifier!, arg_torch!);
|
||||
return <Object?>[];
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user