Skip to content

Commit 6ff862f

Browse files
authored
[image_picker] Fixes activity leak (flutter#4439)
1 parent 20e231b commit 6ff862f

5 files changed

Lines changed: 143 additions & 45 deletions

File tree

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,3 +66,4 @@ Alex Li <[email protected]>
6666
Rahul Raj <[email protected]>
6767
Daniel Roek <[email protected]>
6868
TheOneWithTheBraid <[email protected]>
69+
Rulong Chen(陈汝龙) <[email protected]>

packages/image_picker/image_picker/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.8.4+11
2+
3+
* Fixes Activity leak.
4+
15
## 0.8.4+10
26

37
* iOS: allows picking images with WebP format.

packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java

Lines changed: 105 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,98 @@ public void onActivityDestroyed(Activity activity) {
8585
@Override
8686
public void onActivityStopped(Activity activity) {
8787
if (thisActivity == activity) {
88-
delegate.saveStateBeforeResult();
88+
activityState.getDelegate().saveStateBeforeResult();
8989
}
9090
}
9191
}
9292

93+
/**
94+
* Move all activity-lifetime-bound states into this helper object, so that {@code setup} and
95+
* {@code tearDown} would just become constructor and finalize calls of the helper object.
96+
*/
97+
private class ActivityState {
98+
private Application application;
99+
private Activity activity;
100+
private ImagePickerDelegate delegate;
101+
private MethodChannel channel;
102+
private LifeCycleObserver observer;
103+
private ActivityPluginBinding activityBinding;
104+
105+
// This is null when not using v2 embedding;
106+
private Lifecycle lifecycle;
107+
108+
// Default constructor
109+
ActivityState(
110+
final Application application,
111+
final Activity activity,
112+
final BinaryMessenger messenger,
113+
final MethodChannel.MethodCallHandler handler,
114+
final PluginRegistry.Registrar registrar,
115+
final ActivityPluginBinding activityBinding) {
116+
this.application = application;
117+
this.activity = activity;
118+
this.activityBinding = activityBinding;
119+
120+
delegate = constructDelegate(activity);
121+
channel = new MethodChannel(messenger, CHANNEL);
122+
channel.setMethodCallHandler(handler);
123+
observer = new LifeCycleObserver(activity);
124+
if (registrar != null) {
125+
// V1 embedding setup for activity listeners.
126+
application.registerActivityLifecycleCallbacks(observer);
127+
registrar.addActivityResultListener(delegate);
128+
registrar.addRequestPermissionsResultListener(delegate);
129+
} else {
130+
// V2 embedding setup for activity listeners.
131+
activityBinding.addActivityResultListener(delegate);
132+
activityBinding.addRequestPermissionsResultListener(delegate);
133+
lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(activityBinding);
134+
lifecycle.addObserver(observer);
135+
}
136+
}
137+
138+
// Only invoked by {@link #ImagePickerPlugin(ImagePickerDelegate, Activity)} for testing.
139+
ActivityState(final ImagePickerDelegate delegate, final Activity activity) {
140+
this.activity = activity;
141+
this.delegate = delegate;
142+
}
143+
144+
void release() {
145+
if (activityBinding != null) {
146+
activityBinding.removeActivityResultListener(delegate);
147+
activityBinding.removeRequestPermissionsResultListener(delegate);
148+
activityBinding = null;
149+
}
150+
151+
if (lifecycle != null) {
152+
lifecycle.removeObserver(observer);
153+
lifecycle = null;
154+
}
155+
156+
if (channel != null) {
157+
channel.setMethodCallHandler(null);
158+
channel = null;
159+
}
160+
161+
if (application != null) {
162+
application.unregisterActivityLifecycleCallbacks(observer);
163+
application = null;
164+
}
165+
166+
activity = null;
167+
observer = null;
168+
delegate = null;
169+
}
170+
171+
Activity getActivity() {
172+
return activity;
173+
}
174+
175+
ImagePickerDelegate getDelegate() {
176+
return delegate;
177+
}
178+
}
179+
93180
static final String METHOD_CALL_IMAGE = "pickImage";
94181
static final String METHOD_CALL_MULTI_IMAGE = "pickMultiImage";
95182
static final String METHOD_CALL_VIDEO = "pickVideo";
@@ -101,15 +188,8 @@ public void onActivityStopped(Activity activity) {
101188
private static final int SOURCE_CAMERA = 0;
102189
private static final int SOURCE_GALLERY = 1;
103190

104-
private MethodChannel channel;
105-
private ImagePickerDelegate delegate;
106191
private FlutterPluginBinding pluginBinding;
107-
private ActivityPluginBinding activityBinding;
108-
private Application application;
109-
private Activity activity;
110-
// This is null when not using v2 embedding;
111-
private Lifecycle lifecycle;
112-
private LifeCycleObserver observer;
192+
private ActivityState activityState;
113193

114194
@SuppressWarnings("deprecation")
115195
public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) {
@@ -137,8 +217,12 @@ public ImagePickerPlugin() {}
137217

138218
@VisibleForTesting
139219
ImagePickerPlugin(final ImagePickerDelegate delegate, final Activity activity) {
140-
this.delegate = delegate;
141-
this.activity = activity;
220+
activityState = new ActivityState(delegate, activity);
221+
}
222+
223+
@VisibleForTesting
224+
final ActivityState getActivityState() {
225+
return activityState;
142226
}
143227

144228
@Override
@@ -153,13 +237,12 @@ public void onDetachedFromEngine(FlutterPluginBinding binding) {
153237

154238
@Override
155239
public void onAttachedToActivity(ActivityPluginBinding binding) {
156-
activityBinding = binding;
157240
setup(
158241
pluginBinding.getBinaryMessenger(),
159242
(Application) pluginBinding.getApplicationContext(),
160-
activityBinding.getActivity(),
243+
binding.getActivity(),
161244
null,
162-
activityBinding);
245+
binding);
163246
}
164247

165248
@Override
@@ -183,37 +266,15 @@ private void setup(
183266
final Activity activity,
184267
final PluginRegistry.Registrar registrar,
185268
final ActivityPluginBinding activityBinding) {
186-
this.activity = activity;
187-
this.application = application;
188-
this.delegate = constructDelegate(activity);
189-
channel = new MethodChannel(messenger, CHANNEL);
190-
channel.setMethodCallHandler(this);
191-
observer = new LifeCycleObserver(activity);
192-
if (registrar != null) {
193-
// V1 embedding setup for activity listeners.
194-
application.registerActivityLifecycleCallbacks(observer);
195-
registrar.addActivityResultListener(delegate);
196-
registrar.addRequestPermissionsResultListener(delegate);
197-
} else {
198-
// V2 embedding setup for activity listeners.
199-
activityBinding.addActivityResultListener(delegate);
200-
activityBinding.addRequestPermissionsResultListener(delegate);
201-
lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(activityBinding);
202-
lifecycle.addObserver(observer);
203-
}
269+
activityState =
270+
new ActivityState(application, activity, messenger, this, registrar, activityBinding);
204271
}
205272

206273
private void tearDown() {
207-
activityBinding.removeActivityResultListener(delegate);
208-
activityBinding.removeRequestPermissionsResultListener(delegate);
209-
activityBinding = null;
210-
lifecycle.removeObserver(observer);
211-
lifecycle = null;
212-
delegate = null;
213-
channel.setMethodCallHandler(null);
214-
channel = null;
215-
application.unregisterActivityLifecycleCallbacks(observer);
216-
application = null;
274+
if (activityState != null) {
275+
activityState.release();
276+
activityState = null;
277+
}
217278
}
218279

219280
@VisibleForTesting
@@ -273,12 +334,13 @@ public void run() {
273334

274335
@Override
275336
public void onMethodCall(MethodCall call, MethodChannel.Result rawResult) {
276-
if (activity == null) {
337+
if (activityState == null || activityState.getActivity() == null) {
277338
rawResult.error("no_activity", "image_picker plugin requires a foreground activity.", null);
278339
return;
279340
}
280341
MethodChannel.Result result = new MethodResultWrapper(rawResult);
281342
int imageSource;
343+
ImagePickerDelegate delegate = activityState.getDelegate();
282344
if (call.argument("cameraDevice") != null) {
283345
CameraDevice device;
284346
int deviceIntValue = call.argument("cameraDevice");

packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,25 @@
55
package io.flutter.plugins.imagepicker;
66

77
import static org.hamcrest.core.IsEqual.equalTo;
8+
import static org.junit.Assert.assertNotNull;
9+
import static org.junit.Assert.assertNull;
810
import static org.junit.Assert.assertThat;
911
import static org.junit.Assert.assertTrue;
1012
import static org.mockito.ArgumentMatchers.any;
1113
import static org.mockito.ArgumentMatchers.eq;
14+
import static org.mockito.Mockito.mock;
1215
import static org.mockito.Mockito.times;
1316
import static org.mockito.Mockito.verify;
1417
import static org.mockito.Mockito.verifyZeroInteractions;
1518
import static org.mockito.Mockito.when;
1619

1720
import android.app.Activity;
1821
import android.app.Application;
22+
import androidx.lifecycle.Lifecycle;
23+
import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding;
24+
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
25+
import io.flutter.embedding.engine.plugins.lifecycle.HiddenLifecycleReference;
26+
import io.flutter.plugin.common.BinaryMessenger;
1927
import io.flutter.plugin.common.MethodCall;
2028
import io.flutter.plugin.common.MethodChannel;
2129
import java.io.File;
@@ -41,6 +49,9 @@ public class ImagePickerPluginTest {
4149
@Mock
4250
io.flutter.plugin.common.PluginRegistry.Registrar mockRegistrar;
4351

52+
@Mock ActivityPluginBinding mockActivityBinding;
53+
@Mock FlutterPluginBinding mockPluginBinding;
54+
4455
@Mock Activity mockActivity;
4556
@Mock Application mockApplication;
4657
@Mock ImagePickerDelegate mockImagePickerDelegate;
@@ -52,7 +63,8 @@ public class ImagePickerPluginTest {
5263
public void setUp() {
5364
MockitoAnnotations.initMocks(this);
5465
when(mockRegistrar.context()).thenReturn(mockApplication);
55-
66+
when(mockActivityBinding.getActivity()).thenReturn(mockActivity);
67+
when(mockPluginBinding.getApplicationContext()).thenReturn(mockApplication);
5668
plugin = new ImagePickerPlugin(mockImagePickerDelegate, mockActivity);
5769
}
5870

@@ -176,6 +188,25 @@ public void constructDelegate_ShouldUseInternalCacheDirectory() {
176188
equalTo(mockDirectory));
177189
}
178190

191+
@Test
192+
public void onDetachedFromActivity_ShouldReleaseActivityState() {
193+
final BinaryMessenger mockBinaryMessenger = mock(BinaryMessenger.class);
194+
when(mockPluginBinding.getBinaryMessenger()).thenReturn(mockBinaryMessenger);
195+
196+
final HiddenLifecycleReference mockLifecycleReference = mock(HiddenLifecycleReference.class);
197+
when(mockActivityBinding.getLifecycle()).thenReturn(mockLifecycleReference);
198+
199+
final Lifecycle mockLifecycle = mock(Lifecycle.class);
200+
when(mockLifecycleReference.getLifecycle()).thenReturn(mockLifecycle);
201+
202+
plugin.onAttachedToEngine(mockPluginBinding);
203+
plugin.onAttachedToActivity(mockActivityBinding);
204+
assertNotNull(plugin.getActivityState());
205+
206+
plugin.onDetachedFromActivity();
207+
assertNull(plugin.getActivityState());
208+
}
209+
179210
private MethodCall buildMethodCall(String method, final int source) {
180211
final Map<String, Object> arguments = new HashMap<>();
181212
arguments.put("source", source);

packages/image_picker/image_picker/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ description: Flutter plugin for selecting images from the Android and iOS image
33
library, and taking new pictures with the camera.
44
repository: https://github.com/flutter/plugins/tree/main/packages/image_picker/image_picker
55
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22
6-
version: 0.8.4+10
6+
version: 0.8.4+11
77

88
environment:
99
sdk: ">=2.14.0 <3.0.0"

0 commit comments

Comments
 (0)