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