[image_picker] Fixes activity leak (#4439)

diff --git a/AUTHORS b/AUTHORS
index f5dc823..41a31ed 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -66,3 +66,4 @@
 Rahul Raj <64.rahulraj@gmail.com>
 Daniel Roek <daniel.roek@gmail.com>
 TheOneWithTheBraid <the-one@with-the-braid.cf>
+Rulong Chen(陈汝龙) <rulong.crl@alibaba-inc.com>
diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md
index 9ab762a..2ba5a2c 100644
--- a/packages/image_picker/image_picker/CHANGELOG.md
+++ b/packages/image_picker/image_picker/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 0.8.4+11
+
+* Fixes Activity leak.
+
 ## 0.8.4+10
 
 * iOS: allows picking images with WebP format.
diff --git a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java b/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java
index 577675b..311ef19 100644
--- a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java
+++ b/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java
@@ -85,11 +85,98 @@
     @Override
     public void onActivityStopped(Activity activity) {
       if (thisActivity == activity) {
-        delegate.saveStateBeforeResult();
+        activityState.getDelegate().saveStateBeforeResult();
       }
     }
   }
 
+  /**
+   * Move all activity-lifetime-bound states into this helper object, so that {@code setup} and
+   * {@code tearDown} would just become constructor and finalize calls of the helper object.
+   */
+  private class ActivityState {
+    private Application application;
+    private Activity activity;
+    private ImagePickerDelegate delegate;
+    private MethodChannel channel;
+    private LifeCycleObserver observer;
+    private ActivityPluginBinding activityBinding;
+
+    // This is null when not using v2 embedding;
+    private Lifecycle lifecycle;
+
+    // Default constructor
+    ActivityState(
+        final Application application,
+        final Activity activity,
+        final BinaryMessenger messenger,
+        final MethodChannel.MethodCallHandler handler,
+        final PluginRegistry.Registrar registrar,
+        final ActivityPluginBinding activityBinding) {
+      this.application = application;
+      this.activity = activity;
+      this.activityBinding = activityBinding;
+
+      delegate = constructDelegate(activity);
+      channel = new MethodChannel(messenger, CHANNEL);
+      channel.setMethodCallHandler(handler);
+      observer = new LifeCycleObserver(activity);
+      if (registrar != null) {
+        // V1 embedding setup for activity listeners.
+        application.registerActivityLifecycleCallbacks(observer);
+        registrar.addActivityResultListener(delegate);
+        registrar.addRequestPermissionsResultListener(delegate);
+      } else {
+        // V2 embedding setup for activity listeners.
+        activityBinding.addActivityResultListener(delegate);
+        activityBinding.addRequestPermissionsResultListener(delegate);
+        lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(activityBinding);
+        lifecycle.addObserver(observer);
+      }
+    }
+
+    // Only invoked by {@link #ImagePickerPlugin(ImagePickerDelegate, Activity)} for testing.
+    ActivityState(final ImagePickerDelegate delegate, final Activity activity) {
+      this.activity = activity;
+      this.delegate = delegate;
+    }
+
+    void release() {
+      if (activityBinding != null) {
+        activityBinding.removeActivityResultListener(delegate);
+        activityBinding.removeRequestPermissionsResultListener(delegate);
+        activityBinding = null;
+      }
+
+      if (lifecycle != null) {
+        lifecycle.removeObserver(observer);
+        lifecycle = null;
+      }
+
+      if (channel != null) {
+        channel.setMethodCallHandler(null);
+        channel = null;
+      }
+
+      if (application != null) {
+        application.unregisterActivityLifecycleCallbacks(observer);
+        application = null;
+      }
+
+      activity = null;
+      observer = null;
+      delegate = null;
+    }
+
+    Activity getActivity() {
+      return activity;
+    }
+
+    ImagePickerDelegate getDelegate() {
+      return delegate;
+    }
+  }
+
   static final String METHOD_CALL_IMAGE = "pickImage";
   static final String METHOD_CALL_MULTI_IMAGE = "pickMultiImage";
   static final String METHOD_CALL_VIDEO = "pickVideo";
@@ -101,15 +188,8 @@
   private static final int SOURCE_CAMERA = 0;
   private static final int SOURCE_GALLERY = 1;
 
-  private MethodChannel channel;
-  private ImagePickerDelegate delegate;
   private FlutterPluginBinding pluginBinding;
-  private ActivityPluginBinding activityBinding;
-  private Application application;
-  private Activity activity;
-  // This is null when not using v2 embedding;
-  private Lifecycle lifecycle;
-  private LifeCycleObserver observer;
+  private ActivityState activityState;
 
   @SuppressWarnings("deprecation")
   public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) {
@@ -137,8 +217,12 @@
 
   @VisibleForTesting
   ImagePickerPlugin(final ImagePickerDelegate delegate, final Activity activity) {
-    this.delegate = delegate;
-    this.activity = activity;
+    activityState = new ActivityState(delegate, activity);
+  }
+
+  @VisibleForTesting
+  final ActivityState getActivityState() {
+    return activityState;
   }
 
   @Override
@@ -153,13 +237,12 @@
 
   @Override
   public void onAttachedToActivity(ActivityPluginBinding binding) {
-    activityBinding = binding;
     setup(
         pluginBinding.getBinaryMessenger(),
         (Application) pluginBinding.getApplicationContext(),
-        activityBinding.getActivity(),
+        binding.getActivity(),
         null,
-        activityBinding);
+        binding);
   }
 
   @Override
@@ -183,37 +266,15 @@
       final Activity activity,
       final PluginRegistry.Registrar registrar,
       final ActivityPluginBinding activityBinding) {
-    this.activity = activity;
-    this.application = application;
-    this.delegate = constructDelegate(activity);
-    channel = new MethodChannel(messenger, CHANNEL);
-    channel.setMethodCallHandler(this);
-    observer = new LifeCycleObserver(activity);
-    if (registrar != null) {
-      // V1 embedding setup for activity listeners.
-      application.registerActivityLifecycleCallbacks(observer);
-      registrar.addActivityResultListener(delegate);
-      registrar.addRequestPermissionsResultListener(delegate);
-    } else {
-      // V2 embedding setup for activity listeners.
-      activityBinding.addActivityResultListener(delegate);
-      activityBinding.addRequestPermissionsResultListener(delegate);
-      lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(activityBinding);
-      lifecycle.addObserver(observer);
-    }
+    activityState =
+        new ActivityState(application, activity, messenger, this, registrar, activityBinding);
   }
 
   private void tearDown() {
-    activityBinding.removeActivityResultListener(delegate);
-    activityBinding.removeRequestPermissionsResultListener(delegate);
-    activityBinding = null;
-    lifecycle.removeObserver(observer);
-    lifecycle = null;
-    delegate = null;
-    channel.setMethodCallHandler(null);
-    channel = null;
-    application.unregisterActivityLifecycleCallbacks(observer);
-    application = null;
+    if (activityState != null) {
+      activityState.release();
+      activityState = null;
+    }
   }
 
   @VisibleForTesting
@@ -273,12 +334,13 @@
 
   @Override
   public void onMethodCall(MethodCall call, MethodChannel.Result rawResult) {
-    if (activity == null) {
+    if (activityState == null || activityState.getActivity() == null) {
       rawResult.error("no_activity", "image_picker plugin requires a foreground activity.", null);
       return;
     }
     MethodChannel.Result result = new MethodResultWrapper(rawResult);
     int imageSource;
+    ImagePickerDelegate delegate = activityState.getDelegate();
     if (call.argument("cameraDevice") != null) {
       CameraDevice device;
       int deviceIntValue = call.argument("cameraDevice");
diff --git a/packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java b/packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java
index 422b8be..ce41343 100644
--- a/packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java
+++ b/packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java
@@ -5,10 +5,13 @@
 package io.flutter.plugins.imagepicker;
 
 import static org.hamcrest.core.IsEqual.equalTo;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyZeroInteractions;
@@ -16,6 +19,11 @@
 
 import android.app.Activity;
 import android.app.Application;
+import androidx.lifecycle.Lifecycle;
+import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding;
+import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
+import io.flutter.embedding.engine.plugins.lifecycle.HiddenLifecycleReference;
+import io.flutter.plugin.common.BinaryMessenger;
 import io.flutter.plugin.common.MethodCall;
 import io.flutter.plugin.common.MethodChannel;
 import java.io.File;
@@ -41,6 +49,9 @@
   @Mock
   io.flutter.plugin.common.PluginRegistry.Registrar mockRegistrar;
 
+  @Mock ActivityPluginBinding mockActivityBinding;
+  @Mock FlutterPluginBinding mockPluginBinding;
+
   @Mock Activity mockActivity;
   @Mock Application mockApplication;
   @Mock ImagePickerDelegate mockImagePickerDelegate;
@@ -52,7 +63,8 @@
   public void setUp() {
     MockitoAnnotations.initMocks(this);
     when(mockRegistrar.context()).thenReturn(mockApplication);
-
+    when(mockActivityBinding.getActivity()).thenReturn(mockActivity);
+    when(mockPluginBinding.getApplicationContext()).thenReturn(mockApplication);
     plugin = new ImagePickerPlugin(mockImagePickerDelegate, mockActivity);
   }
 
@@ -176,6 +188,25 @@
         equalTo(mockDirectory));
   }
 
+  @Test
+  public void onDetachedFromActivity_ShouldReleaseActivityState() {
+    final BinaryMessenger mockBinaryMessenger = mock(BinaryMessenger.class);
+    when(mockPluginBinding.getBinaryMessenger()).thenReturn(mockBinaryMessenger);
+
+    final HiddenLifecycleReference mockLifecycleReference = mock(HiddenLifecycleReference.class);
+    when(mockActivityBinding.getLifecycle()).thenReturn(mockLifecycleReference);
+
+    final Lifecycle mockLifecycle = mock(Lifecycle.class);
+    when(mockLifecycleReference.getLifecycle()).thenReturn(mockLifecycle);
+
+    plugin.onAttachedToEngine(mockPluginBinding);
+    plugin.onAttachedToActivity(mockActivityBinding);
+    assertNotNull(plugin.getActivityState());
+
+    plugin.onDetachedFromActivity();
+    assertNull(plugin.getActivityState());
+  }
+
   private MethodCall buildMethodCall(String method, final int source) {
     final Map<String, Object> arguments = new HashMap<>();
     arguments.put("source", source);
diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml
index 4774711..553599f 100755
--- a/packages/image_picker/image_picker/pubspec.yaml
+++ b/packages/image_picker/image_picker/pubspec.yaml
@@ -3,7 +3,7 @@
   library, and taking new pictures with the camera.
 repository: https://github.com/flutter/plugins/tree/main/packages/image_picker/image_picker
 issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
-version: 0.8.4+10
+version: 0.8.4+11
 
 environment:
   sdk: ">=2.14.0 <3.0.0"