[camerax] Add `LifecycleOwner` Proxy (#3837)
Adds proxy implementation of `LifecycleOwner` for `Activity`s this plugin may be bound to.
Heavily inspired by [`google_maps_flutter_android`](https://github.com/flutter/packages/blob/main/packages/google_maps_flutter/google_maps_flutter_android/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapsPlugin.java#L121).
Fixes https://github.com/flutter/flutter/issues/125695.
diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md
index 1f01b04..12dd555 100644
--- a/packages/camera/camera_android_camerax/CHANGELOG.md
+++ b/packages/camera/camera_android_camerax/CHANGELOG.md
@@ -19,3 +19,4 @@
* Fixes cast of CameraInfo to fix integration test failure.
* Updates internal Java InstanceManager to only stop finalization callbacks when stopped.
* Implements image streaming.
+* Provides LifecycleOwner implementation for Activities that use the plugin that do not implement it themselves.
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 8d111a0..31f996d 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
@@ -4,8 +4,10 @@
package io.flutter.plugins.camerax;
+import android.app.Activity;
import android.content.Context;
import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.LifecycleOwner;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
@@ -17,10 +19,11 @@
public final class CameraAndroidCameraxPlugin implements FlutterPlugin, ActivityAware {
private InstanceManager instanceManager;
private FlutterPluginBinding pluginBinding;
- private ProcessCameraProviderHostApiImpl processCameraProviderHostApi;
private ImageAnalysisHostApiImpl imageAnalysisHostApiImpl;
- private ImageCaptureHostApiImpl imageCaptureHostApi;
- public SystemServicesHostApiImpl systemServicesHostApi;
+ private ImageCaptureHostApiImpl imageCaptureHostApiImpl;
+ public SystemServicesHostApiImpl systemServicesHostApiImpl;
+
+ @VisibleForTesting ProcessCameraProviderHostApiImpl processCameraProviderHostApiImpl;
/**
* Initialize this within the {@code #configureFlutterEngine} of a Flutter activity or fragment.
@@ -29,7 +32,11 @@
*/
public CameraAndroidCameraxPlugin() {}
- void setUp(BinaryMessenger binaryMessenger, Context context, TextureRegistry textureRegistry) {
+ @VisibleForTesting
+ public void setUp(
+ @NonNull BinaryMessenger binaryMessenger,
+ @NonNull Context context,
+ @NonNull TextureRegistry textureRegistry) {
// Set up instance manager.
instanceManager =
InstanceManager.create(
@@ -47,16 +54,17 @@
binaryMessenger, new CameraSelectorHostApiImpl(binaryMessenger, instanceManager));
GeneratedCameraXLibrary.JavaObjectHostApi.setup(
binaryMessenger, new JavaObjectHostApiImpl(instanceManager));
- processCameraProviderHostApi =
+ processCameraProviderHostApiImpl =
new ProcessCameraProviderHostApiImpl(binaryMessenger, instanceManager, context);
GeneratedCameraXLibrary.ProcessCameraProviderHostApi.setup(
- binaryMessenger, processCameraProviderHostApi);
- systemServicesHostApi = new SystemServicesHostApiImpl(binaryMessenger, instanceManager);
- GeneratedCameraXLibrary.SystemServicesHostApi.setup(binaryMessenger, systemServicesHostApi);
+ binaryMessenger, processCameraProviderHostApiImpl);
+ systemServicesHostApiImpl = new SystemServicesHostApiImpl(binaryMessenger, instanceManager);
+ GeneratedCameraXLibrary.SystemServicesHostApi.setup(binaryMessenger, systemServicesHostApiImpl);
GeneratedCameraXLibrary.PreviewHostApi.setup(
binaryMessenger, new PreviewHostApiImpl(binaryMessenger, instanceManager, textureRegistry));
- imageCaptureHostApi = new ImageCaptureHostApiImpl(binaryMessenger, instanceManager, context);
- GeneratedCameraXLibrary.ImageCaptureHostApi.setup(binaryMessenger, imageCaptureHostApi);
+ imageCaptureHostApiImpl =
+ new ImageCaptureHostApiImpl(binaryMessenger, instanceManager, context);
+ GeneratedCameraXLibrary.ImageCaptureHostApi.setup(binaryMessenger, imageCaptureHostApiImpl);
imageAnalysisHostApiImpl = new ImageAnalysisHostApiImpl(binaryMessenger, instanceManager);
GeneratedCameraXLibrary.ImageAnalysisHostApi.setup(binaryMessenger, imageAnalysisHostApiImpl);
GeneratedCameraXLibrary.AnalyzerHostApi.setup(
@@ -86,10 +94,18 @@
pluginBinding.getApplicationContext(),
pluginBinding.getTextureRegistry());
updateContext(pluginBinding.getApplicationContext());
- processCameraProviderHostApi.setLifecycleOwner(
- (LifecycleOwner) activityPluginBinding.getActivity());
- systemServicesHostApi.setActivity(activityPluginBinding.getActivity());
- systemServicesHostApi.setPermissionsRegistry(
+
+ Activity activity = activityPluginBinding.getActivity();
+
+ if (activity instanceof LifecycleOwner) {
+ processCameraProviderHostApiImpl.setLifecycleOwner((LifecycleOwner) activity);
+ } else {
+ ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity);
+ processCameraProviderHostApiImpl.setLifecycleOwner(proxyLifecycleProvider);
+ }
+
+ systemServicesHostApiImpl.setActivity(activity);
+ systemServicesHostApiImpl.setPermissionsRegistry(
activityPluginBinding::addRequestPermissionsResultListener);
}
@@ -113,12 +129,12 @@
* Updates context that is used to fetch the corresponding instance of a {@code
* ProcessCameraProvider}.
*/
- public void updateContext(Context context) {
- if (processCameraProviderHostApi != null) {
- processCameraProviderHostApi.setContext(context);
+ public void updateContext(@NonNull Context context) {
+ if (processCameraProviderHostApiImpl != null) {
+ processCameraProviderHostApiImpl.setContext(context);
}
- if (imageCaptureHostApi != null) {
- processCameraProviderHostApi.setContext(context);
+ if (imageCaptureHostApiImpl != null) {
+ imageCaptureHostApiImpl.setContext(context);
}
if (imageAnalysisHostApiImpl != null) {
imageAnalysisHostApiImpl.setContext(context);
diff --git a/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProxyLifecycleProvider.java b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProxyLifecycleProvider.java
new file mode 100644
index 0000000..d80d7b3
--- /dev/null
+++ b/packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProxyLifecycleProvider.java
@@ -0,0 +1,90 @@
+// 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.app.Activity;
+import android.app.Application.ActivityLifecycleCallbacks;
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+import androidx.annotation.VisibleForTesting;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.Lifecycle.Event;
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.LifecycleRegistry;
+
+/**
+ * This class provides a custom {@link LifecycleOwner} for the activity driven by {@link
+ * ActivityLifecycleCallbacks}.
+ *
+ * <p>This is used in the case where a direct {@link LifecycleOwner} is not available.
+ */
+public class ProxyLifecycleProvider implements ActivityLifecycleCallbacks, LifecycleOwner {
+
+ @VisibleForTesting @NonNull public LifecycleRegistry lifecycle = new LifecycleRegistry(this);
+ private final int registrarActivityHashCode;
+
+ ProxyLifecycleProvider(@NonNull Activity activity) {
+ this.registrarActivityHashCode = activity.hashCode();
+ activity.getApplication().registerActivityLifecycleCallbacks(this);
+ }
+
+ @Override
+ public void onActivityCreated(@NonNull Activity activity, @NonNull Bundle savedInstanceState) {
+ if (activity.hashCode() != registrarActivityHashCode) {
+ return;
+ }
+ lifecycle.handleLifecycleEvent(Event.ON_CREATE);
+ }
+
+ @Override
+ public void onActivityStarted(@NonNull Activity activity) {
+ if (activity.hashCode() != registrarActivityHashCode) {
+ return;
+ }
+ lifecycle.handleLifecycleEvent(Event.ON_START);
+ }
+
+ @Override
+ public void onActivityResumed(@NonNull Activity activity) {
+ if (activity.hashCode() != registrarActivityHashCode) {
+ return;
+ }
+ lifecycle.handleLifecycleEvent(Event.ON_RESUME);
+ }
+
+ @Override
+ public void onActivityPaused(@NonNull Activity activity) {
+ if (activity.hashCode() != registrarActivityHashCode) {
+ return;
+ }
+ lifecycle.handleLifecycleEvent(Event.ON_PAUSE);
+ }
+
+ @Override
+ public void onActivityStopped(@NonNull Activity activity) {
+ if (activity.hashCode() != registrarActivityHashCode) {
+ return;
+ }
+ lifecycle.handleLifecycleEvent(Event.ON_STOP);
+ }
+
+ @Override
+ public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {}
+
+ @Override
+ public void onActivityDestroyed(@NonNull Activity activity) {
+ if (activity.hashCode() != registrarActivityHashCode) {
+ return;
+ }
+ activity.getApplication().unregisterActivityLifecycleCallbacks(this);
+ lifecycle.handleLifecycleEvent(Event.ON_DESTROY);
+ }
+
+ @NonNull
+ @Override
+ public Lifecycle getLifecycle() {
+ return lifecycle;
+ }
+}
diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraAndroidCameraxPluginTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraAndroidCameraxPluginTest.java
new file mode 100644
index 0000000..a73654d
--- /dev/null
+++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraAndroidCameraxPluginTest.java
@@ -0,0 +1,73 @@
+// 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.Mockito.any;
+import static org.mockito.Mockito.doNothing;
+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 static org.mockito.Mockito.withSettings;
+
+import android.app.Activity;
+import android.app.Application;
+import androidx.lifecycle.LifecycleOwner;
+import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding;
+import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+public class CameraAndroidCameraxPluginTest {
+ @Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
+
+ @Mock ActivityPluginBinding activityPluginBinding;
+ @Mock FlutterPluginBinding flutterPluginBinding;
+
+ @Test
+ public void onAttachedToActivity_setsLifecycleOwnerAsActivityIfLifecycleOwner() {
+ CameraAndroidCameraxPlugin plugin = spy(new CameraAndroidCameraxPlugin());
+ Activity mockActivity =
+ mock(Activity.class, withSettings().extraInterfaces(LifecycleOwner.class));
+ ProcessCameraProviderHostApiImpl mockProcessCameraProviderHostApiImpl =
+ mock(ProcessCameraProviderHostApiImpl.class);
+
+ doNothing().when(plugin).setUp(any(), any(), any());
+ when(activityPluginBinding.getActivity()).thenReturn(mockActivity);
+
+ plugin.processCameraProviderHostApiImpl = mockProcessCameraProviderHostApiImpl;
+ plugin.systemServicesHostApiImpl = mock(SystemServicesHostApiImpl.class);
+
+ plugin.onAttachedToEngine(flutterPluginBinding);
+ plugin.onAttachedToActivity(activityPluginBinding);
+
+ verify(mockProcessCameraProviderHostApiImpl).setLifecycleOwner(any(LifecycleOwner.class));
+ }
+
+ @Test
+ public void
+ onAttachedToActivity_setsLifecycleOwnerAsProxyLifecycleProviderIfActivityNotLifecycleOwner() {
+ CameraAndroidCameraxPlugin plugin = spy(new CameraAndroidCameraxPlugin());
+ Activity mockActivity = mock(Activity.class);
+ ProcessCameraProviderHostApiImpl mockProcessCameraProviderHostApiImpl =
+ mock(ProcessCameraProviderHostApiImpl.class);
+
+ doNothing().when(plugin).setUp(any(), any(), any());
+ when(activityPluginBinding.getActivity()).thenReturn(mockActivity);
+ when(mockActivity.getApplication()).thenReturn(mock(Application.class));
+
+ plugin.processCameraProviderHostApiImpl = mockProcessCameraProviderHostApiImpl;
+ plugin.systemServicesHostApiImpl = mock(SystemServicesHostApiImpl.class);
+
+ plugin.onAttachedToEngine(flutterPluginBinding);
+ plugin.onAttachedToActivity(activityPluginBinding);
+
+ verify(mockProcessCameraProviderHostApiImpl)
+ .setLifecycleOwner(any(ProxyLifecycleProvider.class));
+ }
+}
diff --git a/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ProxyLifecycleProviderTest.java b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ProxyLifecycleProviderTest.java
new file mode 100644
index 0000000..850f552
--- /dev/null
+++ b/packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ProxyLifecycleProviderTest.java
@@ -0,0 +1,121 @@
+// 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.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+import android.app.Activity;
+import android.app.Application;
+import android.os.Bundle;
+import androidx.lifecycle.Lifecycle.Event;
+import androidx.lifecycle.LifecycleRegistry;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+public class ProxyLifecycleProviderTest {
+ @Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
+
+ @Mock Activity activity;
+ @Mock Application application;
+ @Mock LifecycleRegistry mockLifecycleRegistry;
+
+ private final int testHashCode = 27;
+
+ @Before
+ public void setUp() {
+ when(activity.getApplication()).thenReturn(application);
+ }
+
+ @Test
+ public void onActivityCreated_handlesOnCreateEvent() {
+ ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity);
+ Bundle mockBundle = mock(Bundle.class);
+
+ proxyLifecycleProvider.lifecycle = mockLifecycleRegistry;
+
+ proxyLifecycleProvider.onActivityCreated(activity, mockBundle);
+
+ verify(mockLifecycleRegistry).handleLifecycleEvent(Event.ON_CREATE);
+ }
+
+ @Test
+ public void onActivityStarted_handlesOnActivityStartedEvent() {
+ ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity);
+ proxyLifecycleProvider.lifecycle = mockLifecycleRegistry;
+
+ proxyLifecycleProvider.onActivityStarted(activity);
+
+ verify(mockLifecycleRegistry).handleLifecycleEvent(Event.ON_START);
+ }
+
+ @Test
+ public void onActivityResumed_handlesOnActivityResumedEvent() {
+ ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity);
+ proxyLifecycleProvider.lifecycle = mockLifecycleRegistry;
+
+ proxyLifecycleProvider.onActivityResumed(activity);
+
+ verify(mockLifecycleRegistry).handleLifecycleEvent(Event.ON_RESUME);
+ }
+
+ @Test
+ public void onActivityPaused_handlesOnActivityPausedEvent() {
+ ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity);
+ proxyLifecycleProvider.lifecycle = mockLifecycleRegistry;
+
+ proxyLifecycleProvider.onActivityPaused(activity);
+
+ verify(mockLifecycleRegistry).handleLifecycleEvent(Event.ON_PAUSE);
+ }
+
+ @Test
+ public void onActivityStopped_handlesOnActivityStoppedEvent() {
+ ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity);
+ proxyLifecycleProvider.lifecycle = mockLifecycleRegistry;
+
+ proxyLifecycleProvider.onActivityStopped(activity);
+
+ verify(mockLifecycleRegistry).handleLifecycleEvent(Event.ON_STOP);
+ }
+
+ @Test
+ public void onActivityDestroyed_handlesOnActivityDestroyed() {
+ ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity);
+ proxyLifecycleProvider.lifecycle = mockLifecycleRegistry;
+
+ proxyLifecycleProvider.onActivityDestroyed(activity);
+
+ verify(mockLifecycleRegistry).handleLifecycleEvent(Event.ON_DESTROY);
+ }
+
+ @Test
+ public void onActivitySaveInstanceState_doesNotHandleLifecycleEvvent() {
+ ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity);
+ Bundle mockBundle = mock(Bundle.class);
+
+ proxyLifecycleProvider.lifecycle = mockLifecycleRegistry;
+
+ proxyLifecycleProvider.onActivitySaveInstanceState(activity, mockBundle);
+
+ verifyNoInteractions(mockLifecycleRegistry);
+ }
+
+ @Test
+ public void getLifecycle_returnsExpectedLifecycle() {
+ ProxyLifecycleProvider proxyLifecycleProvider = new ProxyLifecycleProvider(activity);
+
+ proxyLifecycleProvider.lifecycle = mockLifecycleRegistry;
+
+ assertEquals(proxyLifecycleProvider.getLifecycle(), mockLifecycleRegistry);
+ }
+}
diff --git a/packages/camera/camera_android_camerax/test/camera_test.dart b/packages/camera/camera_android_camerax/test/camera_test.dart
index 3d6ea51..5d08dd6 100644
--- a/packages/camera/camera_android_camerax/test/camera_test.dart
+++ b/packages/camera/camera_android_camerax/test/camera_test.dart
@@ -7,7 +7,7 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
-import 'camera_info_test.mocks.dart';
+import 'camera_test.mocks.dart';
import 'test_camerax_library.g.dart';
@GenerateMocks(<Type>[TestInstanceManagerHostApi])