[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"