diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md index 2bdfa594b3be..ce2fb9046c69 100644 --- a/packages/camera/camera_android_camerax/CHANGELOG.md +++ b/packages/camera/camera_android_camerax/CHANGELOG.md @@ -3,3 +3,4 @@ * Creates camera_android_camerax plugin for development. * Adds CameraInfo class and removes unnecessary code from plugin. * Adds CameraSelector class. +* Adds ProcessCameraProvider class. diff --git a/packages/camera/camera_android_camerax/android/build.gradle b/packages/camera/camera_android_camerax/android/build.gradle index 1772afebe429..a193a298c640 100644 --- a/packages/camera/camera_android_camerax/android/build.gradle +++ b/packages/camera/camera_android_camerax/android/build.gradle @@ -38,13 +38,15 @@ android { dependencies { // CameraX core library using the camera2 implementation must use same version number. - def camerax_version = "1.2.0-beta01" + def camerax_version = "1.2.0-beta02" implementation "androidx.camera:camera-core:${camerax_version}" implementation "androidx.camera:camera-camera2:${camerax_version}" implementation "androidx.camera:camera-lifecycle:${camerax_version}" + implementation 'com.google.guava:guava:28.1-android' testImplementation 'junit:junit:4.13.2' testImplementation 'org.mockito:mockito-inline:4.7.0' testImplementation 'androidx.test:core:1.4.0' + testImplementation 'org.robolectric:robolectric:4.3' } testOptions { unitTests.includeAndroidResources = true diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java index bb5756a7e3b9..b8fbaf539c32 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraAndroidCameraxPlugin.java @@ -15,6 +15,7 @@ public final class CameraAndroidCameraxPlugin implements FlutterPlugin, ActivityAware { private InstanceManager instanceManager; private FlutterPluginBinding pluginBinding; + private ProcessCameraProviderHostApiImpl processCameraProviderHostApi; /** * Initialize this within the {@code #configureFlutterEngine} of a Flutter activity or fragment. @@ -39,6 +40,10 @@ void setUp(BinaryMessenger binaryMessenger, Context context) { binaryMessenger, new JavaObjectHostApiImpl(instanceManager)); GeneratedCameraXLibrary.CameraSelectorHostApi.setup( binaryMessenger, new CameraSelectorHostApiImpl(binaryMessenger, instanceManager)); + processCameraProviderHostApi = + new ProcessCameraProviderHostApiImpl(binaryMessenger, instanceManager, context); + GeneratedCameraXLibrary.ProcessCameraProviderHostApi.setup( + binaryMessenger, processCameraProviderHostApi); } @Override @@ -60,15 +65,33 @@ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { // Activity Lifecycle methods: @Override - public void onAttachedToActivity(@NonNull ActivityPluginBinding activityPluginBinding) {} + public void onAttachedToActivity(@NonNull ActivityPluginBinding activityPluginBinding) { + updateContext(activityPluginBinding.getActivity()); + } @Override - public void onDetachedFromActivityForConfigChanges() {} + public void onDetachedFromActivityForConfigChanges() { + updateContext(pluginBinding.getApplicationContext()); + } @Override public void onReattachedToActivityForConfigChanges( - @NonNull ActivityPluginBinding activityPluginBinding) {} + @NonNull ActivityPluginBinding activityPluginBinding) { + updateContext(activityPluginBinding.getActivity()); + } @Override - public void onDetachedFromActivity() {} + public void onDetachedFromActivity() { + updateContext(pluginBinding.getApplicationContext()); + } + + /** + * Updates context that is used to fetch the corresponding instance of a {@code + * ProcessCameraProvider}. + */ + private void updateContext(Context context) { + if (processCameraProviderHostApi != null) { + processCameraProviderHostApi.setContext(context); + } + } } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraInfoFlutterApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraInfoFlutterApiImpl.java index b5ba9fc1ff3b..c538e420cc7e 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraInfoFlutterApiImpl.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/CameraInfoFlutterApiImpl.java @@ -18,7 +18,6 @@ public CameraInfoFlutterApiImpl( } void create(CameraInfo cameraInfo, Reply reply) { - instanceManager.addHostCreatedInstance(cameraInfo); - create(instanceManager.getIdentifierForStrongReference(cameraInfo), reply); + create(instanceManager.addHostCreatedInstance(cameraInfo), reply); } } diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java index e87a80db030c..041564c3bfcb 100644 --- a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/GeneratedCameraXLibrary.java @@ -22,6 +22,13 @@ /** Generated class from Pigeon. */ @SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"}) public class GeneratedCameraXLibrary { + + public interface Result { + void success(T result); + + void error(Throwable error); + } + private static class JavaObjectHostApiCodec extends StandardMessageCodec { public static final JavaObjectHostApiCodec INSTANCE = new JavaObjectHostApiCodec(); @@ -311,6 +318,133 @@ public void create( } } + private static class ProcessCameraProviderHostApiCodec extends StandardMessageCodec { + public static final ProcessCameraProviderHostApiCodec INSTANCE = + new ProcessCameraProviderHostApiCodec(); + + private ProcessCameraProviderHostApiCodec() {} + } + + /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ + public interface ProcessCameraProviderHostApi { + void getInstance(Result result); + + @NonNull + List getAvailableCameraInfos(@NonNull Long identifier); + + /** The codec used by ProcessCameraProviderHostApi. */ + static MessageCodec getCodec() { + return ProcessCameraProviderHostApiCodec.INSTANCE; + } + + /** + * Sets up an instance of `ProcessCameraProviderHostApi` to handle messages through the + * `binaryMessenger`. + */ + static void setup(BinaryMessenger binaryMessenger, ProcessCameraProviderHostApi api) { + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.ProcessCameraProviderHostApi.getInstance", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + Result resultCallback = + new Result() { + public void success(Long result) { + wrapped.put("result", result); + reply.reply(wrapped); + } + + public void error(Throwable error) { + wrapped.put("error", wrapError(error)); + reply.reply(wrapped); + } + }; + + api.getInstance(resultCallback); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + reply.reply(wrapped); + } + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.ProcessCameraProviderHostApi.getAvailableCameraInfos", + getCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + Map wrapped = new HashMap<>(); + try { + ArrayList args = (ArrayList) message; + Number identifierArg = (Number) args.get(0); + if (identifierArg == null) { + throw new NullPointerException("identifierArg unexpectedly null."); + } + List output = + api.getAvailableCameraInfos( + (identifierArg == null) ? null : identifierArg.longValue()); + wrapped.put("result", output); + } catch (Error | RuntimeException exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + } + } + + private static class ProcessCameraProviderFlutterApiCodec extends StandardMessageCodec { + public static final ProcessCameraProviderFlutterApiCodec INSTANCE = + new ProcessCameraProviderFlutterApiCodec(); + + private ProcessCameraProviderFlutterApiCodec() {} + } + + /** Generated class from Pigeon that represents Flutter messages that can be called from Java. */ + public static class ProcessCameraProviderFlutterApi { + private final BinaryMessenger binaryMessenger; + + public ProcessCameraProviderFlutterApi(BinaryMessenger argBinaryMessenger) { + this.binaryMessenger = argBinaryMessenger; + } + + public interface Reply { + void reply(T reply); + } + + static MessageCodec getCodec() { + return ProcessCameraProviderFlutterApiCodec.INSTANCE; + } + + public void create(@NonNull Long identifierArg, Reply callback) { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.ProcessCameraProviderFlutterApi.create", + getCodec()); + channel.send( + new ArrayList(Arrays.asList(identifierArg)), + channelReply -> { + callback.reply(null); + }); + } + } + private static Map wrapError(Throwable exception) { Map errorMap = new HashMap<>(); errorMap.put("message", exception.toString()); diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProcessCameraProviderFlutterApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProcessCameraProviderFlutterApiImpl.java new file mode 100644 index 000000000000..90c94d0c26cb --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProcessCameraProviderFlutterApiImpl.java @@ -0,0 +1,23 @@ +// 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.camera.lifecycle.ProcessCameraProvider; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ProcessCameraProviderFlutterApi; + +public class ProcessCameraProviderFlutterApiImpl extends ProcessCameraProviderFlutterApi { + public ProcessCameraProviderFlutterApiImpl( + BinaryMessenger binaryMessenger, InstanceManager instanceManager) { + super(binaryMessenger); + this.instanceManager = instanceManager; + } + + private final InstanceManager instanceManager; + + void create(ProcessCameraProvider processCameraProvider, Reply reply) { + create(instanceManager.addHostCreatedInstance(processCameraProvider), reply); + } +} diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProcessCameraProviderHostApiImpl.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProcessCameraProviderHostApiImpl.java new file mode 100644 index 000000000000..19c5eb5b3f70 --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProcessCameraProviderHostApiImpl.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.camerax; + +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.camera.core.CameraInfo; +import androidx.camera.lifecycle.ProcessCameraProvider; +import androidx.core.content.ContextCompat; +import com.google.common.util.concurrent.ListenableFuture; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugins.camerax.GeneratedCameraXLibrary.ProcessCameraProviderHostApi; +import java.util.ArrayList; +import java.util.List; + +public class ProcessCameraProviderHostApiImpl implements ProcessCameraProviderHostApi { + private final BinaryMessenger binaryMessenger; + private final InstanceManager instanceManager; + + private Context context; + + public ProcessCameraProviderHostApiImpl( + BinaryMessenger binaryMessenger, InstanceManager instanceManager, Context context) { + this.binaryMessenger = binaryMessenger; + this.instanceManager = instanceManager; + this.context = context; + } + + /** + * Sets the context that the {@code ProcessCameraProvider} will use to attach the lifecycle of the + * camera to. + * + *

If using the camera plugin in an add-to-app context, ensure that a new instance of the + * {@code ProcessCameraProvider} is fetched via {@code #getInstance} anytime the context changes. + */ + public void setContext(Context context) { + this.context = context; + } + + /** + * Returns the instance of the ProcessCameraProvider to manage the lifecycle of the camera for the + * current {@code Context}. + */ + @Override + public void getInstance(GeneratedCameraXLibrary.Result result) { + ListenableFuture processCameraProviderFuture = + ProcessCameraProvider.getInstance(context); + + processCameraProviderFuture.addListener( + () -> { + try { + // Camera provider is now guaranteed to be available. + ProcessCameraProvider processCameraProvider = processCameraProviderFuture.get(); + + if (!instanceManager.containsInstance(processCameraProvider)) { + final ProcessCameraProviderFlutterApiImpl flutterApi = + new ProcessCameraProviderFlutterApiImpl(binaryMessenger, instanceManager); + flutterApi.create(processCameraProvider, reply -> {}); + } + result.success(instanceManager.getIdentifierForStrongReference(processCameraProvider)); + } catch (Exception e) { + result.error(e); + } + }, + ContextCompat.getMainExecutor(context)); + } + + /** Returns cameras available to the ProcessCameraProvider. */ + @Override + public List getAvailableCameraInfos(@NonNull Long identifier) { + ProcessCameraProvider processCameraProvider = + (ProcessCameraProvider) instanceManager.getInstance(identifier); + + List availableCameras = processCameraProvider.getAvailableCameraInfos(); + List availableCamerasIds = new ArrayList(); + final CameraInfoFlutterApiImpl cameraInfoFlutterApi = + new CameraInfoFlutterApiImpl(binaryMessenger, instanceManager); + + for (CameraInfo cameraInfo : availableCameras) { + cameraInfoFlutterApi.create(cameraInfo, result -> {}); + availableCamerasIds.add(instanceManager.getIdentifierForStrongReference(cameraInfo)); + } + return availableCamerasIds; + } +} diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ProcessCameraProviderTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ProcessCameraProviderTest.java new file mode 100644 index 000000000000..ec321f8dbfea --- /dev/null +++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ProcessCameraProviderTest.java @@ -0,0 +1,106 @@ +// 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.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.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import androidx.camera.core.CameraInfo; +import androidx.camera.lifecycle.ProcessCameraProvider; +import androidx.test.core.app.ApplicationProvider; +import com.google.common.util.concurrent.SettableFuture; +import io.flutter.plugin.common.BinaryMessenger; +import java.util.Arrays; +import java.util.Objects; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; +import org.mockito.stubbing.Answer; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class ProcessCameraProviderTest { + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock public ProcessCameraProvider processCameraProvider; + @Mock public BinaryMessenger mockBinaryMessenger; + + InstanceManager testInstanceManager; + private Context context; + + @Before + public void setUp() { + testInstanceManager = InstanceManager.open(identifier -> {}); + context = ApplicationProvider.getApplicationContext(); + } + + @After + public void tearDown() { + testInstanceManager.close(); + } + + @Test + public void getInstanceTest() { + final ProcessCameraProviderHostApiImpl processCameraProviderHostApi = + new ProcessCameraProviderHostApiImpl(mockBinaryMessenger, testInstanceManager, context); + SettableFuture processCameraProviderFuture = SettableFuture.create(); + processCameraProviderFuture.set(processCameraProvider); + final GeneratedCameraXLibrary.Result mockResult = + mock(GeneratedCameraXLibrary.Result.class); + + testInstanceManager.addDartCreatedInstance(processCameraProvider, 0); + + try (MockedStatic mockedProcessCameraProvider = + Mockito.mockStatic(ProcessCameraProvider.class)) { + mockedProcessCameraProvider + .when(() -> ProcessCameraProvider.getInstance(context)) + .thenAnswer((Answer) invocation -> processCameraProviderFuture); + + processCameraProviderHostApi.getInstance(mockResult); + verify(mockResult).success(0L); + } + } + + @Test + public void getAvailableCameraInfosTest() { + final ProcessCameraProviderHostApiImpl processCameraProviderHostApi = + new ProcessCameraProviderHostApiImpl(mockBinaryMessenger, testInstanceManager, context); + final CameraInfo mockCameraInfo = mock(CameraInfo.class); + + testInstanceManager.addDartCreatedInstance(processCameraProvider, 0); + testInstanceManager.addDartCreatedInstance(mockCameraInfo, 1); + + when(processCameraProvider.getAvailableCameraInfos()).thenReturn(Arrays.asList(mockCameraInfo)); + + assertEquals(processCameraProviderHostApi.getAvailableCameraInfos(0L), Arrays.asList(1L)); + verify(processCameraProvider).getAvailableCameraInfos(); + } + + @Test + public void flutterApiCreateTest() { + final ProcessCameraProviderFlutterApiImpl spyFlutterApi = + spy(new ProcessCameraProviderFlutterApiImpl(mockBinaryMessenger, testInstanceManager)); + + spyFlutterApi.create(processCameraProvider, reply -> {}); + + final long identifier = + Objects.requireNonNull( + testInstanceManager.getIdentifierForStrongReference(processCameraProvider)); + verify(spyFlutterApi).create(eq(identifier), any()); + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax_flutter_api_impls.dart b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax_flutter_api_impls.dart index 576260c0b7b8..9c6564a06c08 100644 --- a/packages/camera/camera_android_camerax/lib/src/android_camera_camerax_flutter_api_impls.dart +++ b/packages/camera/camera_android_camerax/lib/src/android_camera_camerax_flutter_api_impls.dart @@ -6,6 +6,7 @@ import 'camera_info.dart'; import 'camera_selector.dart'; import 'camerax_library.pigeon.dart'; import 'java_object.dart'; +import 'process_camera_provider.dart'; /// Handles initialization of Flutter APIs for the Android CameraX library. class AndroidCameraXCameraFlutterApis { @@ -14,6 +15,7 @@ class AndroidCameraXCameraFlutterApis { JavaObjectFlutterApiImpl? javaObjectFlutterApi, CameraInfoFlutterApiImpl? cameraInfoFlutterApi, CameraSelectorFlutterApiImpl? cameraSelectorFlutterApi, + ProcessCameraProviderFlutterApiImpl? processCameraProviderFlutterApi, }) { this.javaObjectFlutterApi = javaObjectFlutterApi ?? JavaObjectFlutterApiImpl(); @@ -21,6 +23,8 @@ class AndroidCameraXCameraFlutterApis { cameraInfoFlutterApi ?? CameraInfoFlutterApiImpl(); this.cameraSelectorFlutterApi = cameraSelectorFlutterApi ?? CameraSelectorFlutterApiImpl(); + this.processCameraProviderFlutterApi = processCameraProviderFlutterApi ?? + ProcessCameraProviderFlutterApiImpl(); } static bool _haveBeenSetUp = false; @@ -40,12 +44,17 @@ class AndroidCameraXCameraFlutterApis { /// Flutter Api for [CameraSelector]. late final CameraSelectorFlutterApiImpl cameraSelectorFlutterApi; + /// Flutter Api for [ProcessCameraProvider]. + late final ProcessCameraProviderFlutterApiImpl + processCameraProviderFlutterApi; + /// Ensures all the Flutter APIs have been setup to receive calls from native code. void ensureSetUp() { if (!_haveBeenSetUp) { JavaObjectFlutterApi.setup(javaObjectFlutterApi); CameraInfoFlutterApi.setup(cameraInfoFlutterApi); CameraSelectorFlutterApi.setup(cameraSelectorFlutterApi); + ProcessCameraProviderFlutterApi.setup(processCameraProviderFlutterApi); _haveBeenSetUp = true; } } diff --git a/packages/camera/camera_android_camerax/lib/src/camerax_library.pigeon.dart b/packages/camera/camera_android_camerax/lib/src/camerax_library.pigeon.dart index a399001d35b0..c0b052378def 100644 --- a/packages/camera/camera_android_camerax/lib/src/camerax_library.pigeon.dart +++ b/packages/camera/camera_android_camerax/lib/src/camerax_library.pigeon.dart @@ -263,3 +263,112 @@ abstract class CameraSelectorFlutterApi { } } } + +class _ProcessCameraProviderHostApiCodec extends StandardMessageCodec { + const _ProcessCameraProviderHostApiCodec(); +} + +class ProcessCameraProviderHostApi { + /// Constructor for [ProcessCameraProviderHostApi]. 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. + ProcessCameraProviderHostApi({BinaryMessenger? binaryMessenger}) + : _binaryMessenger = binaryMessenger; + + final BinaryMessenger? _binaryMessenger; + + static const MessageCodec codec = + _ProcessCameraProviderHostApiCodec(); + + Future getInstance() async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ProcessCameraProviderHostApi.getInstance', codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send(null) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyMap['result'] as int?)!; + } + } + + Future> getAvailableCameraInfos(int arg_identifier) async { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ProcessCameraProviderHostApi.getAvailableCameraInfos', + codec, + binaryMessenger: _binaryMessenger); + final Map? replyMap = + await channel.send([arg_identifier]) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + ); + } else if (replyMap['error'] != null) { + final Map error = + (replyMap['error'] as Map?)!; + throw PlatformException( + code: (error['code'] as String?)!, + message: error['message'] as String?, + details: error['details'], + ); + } else if (replyMap['result'] == null) { + throw PlatformException( + code: 'null-error', + message: 'Host platform returned null value for non-null return value.', + ); + } else { + return (replyMap['result'] as List?)!.cast(); + } + } +} + +class _ProcessCameraProviderFlutterApiCodec extends StandardMessageCodec { + const _ProcessCameraProviderFlutterApiCodec(); +} + +abstract class ProcessCameraProviderFlutterApi { + static const MessageCodec codec = + _ProcessCameraProviderFlutterApiCodec(); + + void create(int identifier); + static void setup(ProcessCameraProviderFlutterApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ProcessCameraProviderFlutterApi.create', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMessageHandler(null); + } else { + channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ProcessCameraProviderFlutterApi.create was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.ProcessCameraProviderFlutterApi.create was null, expected non-null int.'); + api.create(arg_identifier!); + return; + }); + } + } + } +} diff --git a/packages/camera/camera_android_camerax/lib/src/process_camera_provider.dart b/packages/camera/camera_android_camerax/lib/src/process_camera_provider.dart new file mode 100644 index 000000000000..e2b588d15faa --- /dev/null +++ b/packages/camera/camera_android_camerax/lib/src/process_camera_provider.dart @@ -0,0 +1,119 @@ +// 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'; + +import 'android_camera_camerax_flutter_api_impls.dart'; +import 'camera_info.dart'; +import 'camerax_library.pigeon.dart'; +import 'instance_manager.dart'; +import 'java_object.dart'; + +/// Provides an object to manage the camera. +/// +/// See https://developer.android.com/reference/androidx/camera/lifecycle/ProcessCameraProvider. +class ProcessCameraProvider extends JavaObject { + /// Creates a detached [ProcessCameraProvider]. + ProcessCameraProvider.detached( + {BinaryMessenger? binaryMessenger, InstanceManager? instanceManager}) + : super.detached( + binaryMessenger: binaryMessenger, + instanceManager: instanceManager) { + _api = ProcessCameraProviderHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); + } + + late final ProcessCameraProviderHostApiImpl _api; + + /// Gets an instance of [ProcessCameraProvider]. + static Future getInstance( + {BinaryMessenger? binaryMessenger, InstanceManager? instanceManager}) { + AndroidCameraXCameraFlutterApis.instance.ensureSetUp(); + final ProcessCameraProviderHostApiImpl api = + ProcessCameraProviderHostApiImpl( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + + return api.getInstancefromInstances(); + } + + /// Retrieves the cameras available to the device. + Future> getAvailableCameraInfos() { + return _api.getAvailableCameraInfosFromInstances(this); + } +} + +/// Host API implementation of [ProcessCameraProvider]. +class ProcessCameraProviderHostApiImpl extends ProcessCameraProviderHostApi { + /// Creates a [ProcessCameraProviderHostApiImpl]. + ProcessCameraProviderHostApiImpl( + {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; + + /// Retrieves an instance of a ProcessCameraProvider from the context of + /// the FlutterActivity. + Future getInstancefromInstances() async { + return instanceManager.getInstanceWithWeakReference(await getInstance())! + as ProcessCameraProvider; + } + + /// Retrives the list of CameraInfos corresponding to the available cameras. + Future> getAvailableCameraInfosFromInstances( + ProcessCameraProvider instance) async { + int? identifier = instanceManager.getIdentifier(instance); + identifier ??= instanceManager.addDartCreatedInstance(instance, + onCopy: (ProcessCameraProvider original) { + return ProcessCameraProvider.detached( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + }); + + final List cameraInfos = await getAvailableCameraInfos(identifier); + return (cameraInfos.map((int? id) => + instanceManager.getInstanceWithWeakReference(id!)! as CameraInfo)) + .toList(); + } +} + +/// Flutter API Implementation of [ProcessCameraProvider]. +class ProcessCameraProviderFlutterApiImpl + implements ProcessCameraProviderFlutterApi { + /// Constructs a [ProcessCameraProviderFlutterApiImpl]. + ProcessCameraProviderFlutterApiImpl({ + this.binaryMessenger, + InstanceManager? instanceManager, + }) : 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. + final InstanceManager instanceManager; + + @override + void create(int identifier) { + instanceManager.addHostCreatedInstance( + ProcessCameraProvider.detached( + binaryMessenger: binaryMessenger, instanceManager: instanceManager), + identifier, + onCopy: (ProcessCameraProvider original) { + return ProcessCameraProvider.detached( + binaryMessenger: binaryMessenger, instanceManager: instanceManager); + }, + ); + } +} diff --git a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart index aace7a06b1fd..4d7d96910246 100644 --- a/packages/camera/camera_android_camerax/pigeons/camerax_library.dart +++ b/packages/camera/camera_android_camerax/pigeons/camerax_library.dart @@ -57,3 +57,16 @@ abstract class CameraSelectorHostApi { abstract class CameraSelectorFlutterApi { void create(int identifier, int? lensFacing); } + +@HostApi(dartHostTestHandler: 'TestProcessCameraProviderHostApi') +abstract class ProcessCameraProviderHostApi { + @async + int getInstance(); + + List getAvailableCameraInfos(int identifier); +} + +@FlutterApi() +abstract class ProcessCameraProviderFlutterApi { + void create(int identifier); +} diff --git a/packages/camera/camera_android_camerax/test/process_camera_provider_test.dart b/packages/camera/camera_android_camerax/test/process_camera_provider_test.dart new file mode 100644 index 000000000000..65e7d00ddaea --- /dev/null +++ b/packages/camera/camera_android_camerax/test/process_camera_provider_test.dart @@ -0,0 +1,96 @@ +// 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_info.dart'; +import 'package:camera_android_camerax/src/instance_manager.dart'; +import 'package:camera_android_camerax/src/process_camera_provider.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'process_camera_provider_test.mocks.dart'; +import 'test_camerax_library.pigeon.dart'; + +@GenerateMocks([TestProcessCameraProviderHostApi]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('ProcessCameraProvider', () { + tearDown(() => TestProcessCameraProviderHostApi.setup(null)); + + test('getInstanceTest', () async { + final MockTestProcessCameraProviderHostApi mockApi = + MockTestProcessCameraProviderHostApi(); + TestProcessCameraProviderHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + final ProcessCameraProvider processCameraProvider = + ProcessCameraProvider.detached( + instanceManager: instanceManager, + ); + + instanceManager.addHostCreatedInstance( + processCameraProvider, + 0, + onCopy: (_) => ProcessCameraProvider.detached(), + ); + + when(mockApi.getInstance()).thenAnswer((_) async => 0); + expect( + await ProcessCameraProvider.getInstance( + instanceManager: instanceManager), + equals(processCameraProvider)); + verify(mockApi.getInstance()); + }); + + test('getAvailableCameraInfosTest', () async { + final MockTestProcessCameraProviderHostApi mockApi = + MockTestProcessCameraProviderHostApi(); + TestProcessCameraProviderHostApi.setup(mockApi); + + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + final ProcessCameraProvider processCameraProvider = + ProcessCameraProvider.detached( + instanceManager: instanceManager, + ); + + instanceManager.addHostCreatedInstance( + processCameraProvider, + 0, + onCopy: (_) => ProcessCameraProvider.detached(), + ); + final CameraInfo fakeAvailableCameraInfo = + CameraInfo.detached(instanceManager: instanceManager); + instanceManager.addHostCreatedInstance( + fakeAvailableCameraInfo, + 1, + onCopy: (_) => CameraInfo.detached(), + ); + + when(mockApi.getAvailableCameraInfos(0)).thenReturn([1]); + expect(await processCameraProvider.getAvailableCameraInfos(), + equals([fakeAvailableCameraInfo])); + verify(mockApi.getAvailableCameraInfos(0)); + }); + + test('flutterApiCreateTest', () { + final InstanceManager instanceManager = InstanceManager( + onWeakReferenceRemoved: (_) {}, + ); + final ProcessCameraProviderFlutterApiImpl flutterApi = + ProcessCameraProviderFlutterApiImpl( + instanceManager: instanceManager, + ); + + flutterApi.create(0); + + expect(instanceManager.getInstanceWithWeakReference(0), + isA()); + }); + }); +} diff --git a/packages/camera/camera_android_camerax/test/process_camera_provider_test.mocks.dart b/packages/camera/camera_android_camerax/test/process_camera_provider_test.mocks.dart new file mode 100644 index 000000000000..9fcfe690c062 --- /dev/null +++ b/packages/camera/camera_android_camerax/test/process_camera_provider_test.mocks.dart @@ -0,0 +1,40 @@ +// Mocks generated by Mockito 5.3.0 from annotations +// in camera_android_camerax/test/process_camera_provider_test.dart. +// Do not manually edit this file. + +// 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.pigeon.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 [TestProcessCameraProviderHostApi]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTestProcessCameraProviderHostApi extends _i1.Mock + implements _i2.TestProcessCameraProviderHostApi { + MockTestProcessCameraProviderHostApi() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future getInstance() => + (super.noSuchMethod(Invocation.method(#getInstance, []), + returnValue: _i3.Future.value(0)) as _i3.Future); + @override + List getAvailableCameraInfos(int? identifier) => (super.noSuchMethod( + Invocation.method(#getAvailableCameraInfos, [identifier]), + returnValue: []) as List); +} diff --git a/packages/camera/camera_android_camerax/test/test_camerax_library.pigeon.dart b/packages/camera/camera_android_camerax/test/test_camerax_library.pigeon.dart index b10e14e9d518..2196b73d7fdb 100644 --- a/packages/camera/camera_android_camerax/test/test_camerax_library.pigeon.dart +++ b/packages/camera/camera_android_camerax/test/test_camerax_library.pigeon.dart @@ -135,3 +135,53 @@ abstract class TestCameraSelectorHostApi { } } } + +class _TestProcessCameraProviderHostApiCodec extends StandardMessageCodec { + const _TestProcessCameraProviderHostApiCodec(); +} + +abstract class TestProcessCameraProviderHostApi { + static const MessageCodec codec = + _TestProcessCameraProviderHostApiCodec(); + + Future getInstance(); + List getAvailableCameraInfos(int identifier); + static void setup(TestProcessCameraProviderHostApi? api, + {BinaryMessenger? binaryMessenger}) { + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ProcessCameraProviderHostApi.getInstance', codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + // ignore message + final int output = await api.getInstance(); + return {'result': output}; + }); + } + } + { + final BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.ProcessCameraProviderHostApi.getAvailableCameraInfos', + codec, + binaryMessenger: binaryMessenger); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.ProcessCameraProviderHostApi.getAvailableCameraInfos was null.'); + final List args = (message as List?)!; + final int? arg_identifier = (args[0] as int?); + assert(arg_identifier != null, + 'Argument for dev.flutter.pigeon.ProcessCameraProviderHostApi.getAvailableCameraInfos was null, expected non-null int.'); + final List output = + api.getAvailableCameraInfos(arg_identifier!); + return {'result': output}; + }); + } + } + } +}