Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit 60559bb

Browse files
committed
[image_picker] Fixes activity leak
1 parent c0b4032 commit 60559bb

4 files changed

Lines changed: 135 additions & 47 deletions

File tree

packages/image_picker/image_picker/CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
## NEXT
1+
## 0.8.4+5
22

33
* Updates Android compileSdkVersion to 31.
4-
* Fix iOS RunnerUITests search paths.
4+
* Fixes iOS RunnerUITests search paths.
5+
* Fixes Activity leak.
56

67
## 0.8.4+4
78

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

Lines changed: 99 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,92 @@ 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+
private class ActivityState {
94+
private Application application;
95+
private Activity activity;
96+
private ImagePickerDelegate delegate;
97+
private MethodChannel channel;
98+
private LifeCycleObserver observer;
99+
private ActivityPluginBinding activityBinding;
100+
101+
// This is null when not using v2 embedding;
102+
private Lifecycle lifecycle;
103+
104+
ActivityState(
105+
final Application application,
106+
final Activity activity,
107+
final BinaryMessenger messenger,
108+
final MethodChannel.MethodCallHandler handler,
109+
final PluginRegistry.Registrar registrar,
110+
final ActivityPluginBinding activityBinding) {
111+
this.application = application;
112+
this.activity = activity;
113+
this.activityBinding = activityBinding;
114+
115+
delegate = constructDelegate(activity);
116+
channel = new MethodChannel(messenger, CHANNEL);
117+
channel.setMethodCallHandler(handler);
118+
observer = new LifeCycleObserver(activity);
119+
if (registrar != null) {
120+
// V1 embedding setup for activity listeners.
121+
application.registerActivityLifecycleCallbacks(observer);
122+
registrar.addActivityResultListener(delegate);
123+
registrar.addRequestPermissionsResultListener(delegate);
124+
} else {
125+
// V2 embedding setup for activity listeners.
126+
activityBinding.addActivityResultListener(delegate);
127+
activityBinding.addRequestPermissionsResultListener(delegate);
128+
lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(activityBinding);
129+
lifecycle.addObserver(observer);
130+
}
131+
}
132+
133+
ActivityState(final ImagePickerDelegate delegate, final Activity activity) {
134+
this.activity = activity;
135+
this.delegate = delegate;
136+
}
137+
138+
void release() {
139+
if (activityBinding != null) {
140+
activityBinding.removeActivityResultListener(delegate);
141+
activityBinding.removeRequestPermissionsResultListener(delegate);
142+
activityBinding = null;
143+
}
144+
145+
if (lifecycle != null) {
146+
lifecycle.removeObserver(observer);
147+
lifecycle = null;
148+
}
149+
150+
if (channel != null) {
151+
channel.setMethodCallHandler(null);
152+
channel = null;
153+
}
154+
155+
if (application != null) {
156+
application.unregisterActivityLifecycleCallbacks(observer);
157+
application = null;
158+
}
159+
160+
activity = null;
161+
observer = null;
162+
delegate = null;
163+
}
164+
165+
Activity getActivity() {
166+
return activity;
167+
}
168+
169+
ImagePickerDelegate getDelegate() {
170+
return delegate;
171+
}
172+
}
173+
93174
static final String METHOD_CALL_IMAGE = "pickImage";
94175
static final String METHOD_CALL_MULTI_IMAGE = "pickMultiImage";
95176
static final String METHOD_CALL_VIDEO = "pickVideo";
@@ -101,15 +182,8 @@ public void onActivityStopped(Activity activity) {
101182
private static final int SOURCE_CAMERA = 0;
102183
private static final int SOURCE_GALLERY = 1;
103184

104-
private MethodChannel channel;
105-
private ImagePickerDelegate delegate;
106185
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;
186+
private ActivityState activityState;
113187

114188
@SuppressWarnings("deprecation")
115189
public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) {
@@ -137,8 +211,12 @@ public ImagePickerPlugin() {}
137211

138212
@VisibleForTesting
139213
ImagePickerPlugin(final ImagePickerDelegate delegate, final Activity activity) {
140-
this.delegate = delegate;
141-
this.activity = activity;
214+
activityState = new ActivityState(delegate, activity);
215+
}
216+
217+
@VisibleForTesting
218+
final ActivityState getActivityState() {
219+
return activityState;
142220
}
143221

144222
@Override
@@ -153,13 +231,12 @@ public void onDetachedFromEngine(FlutterPluginBinding binding) {
153231

154232
@Override
155233
public void onAttachedToActivity(ActivityPluginBinding binding) {
156-
activityBinding = binding;
157234
setup(
158235
pluginBinding.getBinaryMessenger(),
159236
(Application) pluginBinding.getApplicationContext(),
160-
activityBinding.getActivity(),
237+
binding.getActivity(),
161238
null,
162-
activityBinding);
239+
binding);
163240
}
164241

165242
@Override
@@ -183,37 +260,15 @@ private void setup(
183260
final Activity activity,
184261
final PluginRegistry.Registrar registrar,
185262
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-
}
263+
activityState =
264+
new ActivityState(application, activity, messenger, this, registrar, activityBinding);
204265
}
205266

206267
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;
268+
if (activityState != null) {
269+
activityState.release();
270+
activityState = null;
271+
}
217272
}
218273

219274
@VisibleForTesting
@@ -273,12 +328,13 @@ public void run() {
273328

274329
@Override
275330
public void onMethodCall(MethodCall call, MethodChannel.Result rawResult) {
276-
if (activity == null) {
331+
if (activityState == null || activityState.getActivity() == null) {
277332
rawResult.error("no_activity", "image_picker plugin requires a foreground activity.", null);
278333
return;
279334
}
280335
MethodChannel.Result result = new MethodResultWrapper(rawResult);
281336
int imageSource;
337+
ImagePickerDelegate delegate = activityState.getDelegate();
282338
if (call.argument("cameraDevice") != null) {
283339
CameraDevice device;
284340
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/master/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+4
6+
version: 0.8.4+5
77

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

0 commit comments

Comments
 (0)