From 6954af4594d8d965a3284cd9f9afc9e5efff90da Mon Sep 17 00:00:00 2001 From: Camille Simon <43054281+camsim99@users.noreply.github.com> Date: Fri, 28 Apr 2023 18:21:15 -0700 Subject: [PATCH] [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. --- .../camera_android_camerax/CHANGELOG.md | 1 + .../camerax/CameraAndroidCameraxPlugin.java | 54 +++++--- .../camerax/ProxyLifecycleProvider.java | 90 +++++++++++++ .../CameraAndroidCameraxPluginTest.java | 73 +++++++++++ .../camerax/ProxyLifecycleProviderTest.java | 121 ++++++++++++++++++ .../test/camera_test.dart | 2 +- 6 files changed, 321 insertions(+), 20 deletions(-) create mode 100644 packages/camera/camera_android_camerax/android/src/main/java/io/flutter/plugins/camerax/ProxyLifecycleProvider.java create mode 100644 packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/CameraAndroidCameraxPluginTest.java create mode 100644 packages/camera/camera_android_camerax/android/src/test/java/io/flutter/plugins/camerax/ProxyLifecycleProviderTest.java diff --git a/packages/camera/camera_android_camerax/CHANGELOG.md b/packages/camera/camera_android_camerax/CHANGELOG.md index 1f01b049ea61..12dd55552974 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 8d111a003e12..31f996d26e24 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 final class CameraAndroidCameraxPlugin implements FlutterPlugin, Activity */ 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 @@ void setUp(BinaryMessenger binaryMessenger, Context context, TextureRegistry tex 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 @@ public void onAttachedToActivity(@NonNull ActivityPluginBinding activityPluginBi 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 @@ public void onDetachedFromActivity() { * 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 000000000000..d80d7b32357f --- /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}. + * + *
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 000000000000..a73654d0e69a
--- /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 000000000000..850f552e51eb
--- /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 3d6ea51b4b04..5d08dd65c49c 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:camera_android_camerax/src/instance_manager.dart';
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(