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([TestInstanceManagerHostApi])