Skip to content

Commit 43a42d1

Browse files
[webview_flutter_android] Updates Dart and Java InstanceManagers (#3282)
[webview_flutter_android] Updates Dart and Java InstanceManagers
1 parent 784291b commit 43a42d1

23 files changed

+541
-68
lines changed

packages/webview_flutter/webview_flutter_android/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 3.4.3
2+
3+
* Updates internal Java InstanceManager to be cleared on hot restart.
4+
15
## 3.4.2
26

37
* Clarifies explanation of endorsement in README.

packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/GeneratedAndroidWebView.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,51 @@ public interface Result<T> {
439439

440440
void error(Throwable error);
441441
}
442+
/**
443+
* Host API for managing the native `InstanceManager`.
444+
*
445+
* <p>Generated interface from Pigeon that represents a handler of messages from Flutter.
446+
*/
447+
public interface InstanceManagerHostApi {
448+
/**
449+
* Clear the native `InstanceManager`.
450+
*
451+
* <p>This is typically only used after a hot restart.
452+
*/
453+
void clear();
454+
455+
/** The codec used by InstanceManagerHostApi. */
456+
static MessageCodec<Object> getCodec() {
457+
return new StandardMessageCodec();
458+
}
459+
/**
460+
* Sets up an instance of `InstanceManagerHostApi` to handle messages through the
461+
* `binaryMessenger`.
462+
*/
463+
static void setup(BinaryMessenger binaryMessenger, InstanceManagerHostApi api) {
464+
{
465+
BasicMessageChannel<Object> channel =
466+
new BasicMessageChannel<>(
467+
binaryMessenger, "dev.flutter.pigeon.InstanceManagerHostApi.clear", getCodec());
468+
if (api != null) {
469+
channel.setMessageHandler(
470+
(message, reply) -> {
471+
ArrayList<Object> wrapped = new ArrayList<Object>();
472+
try {
473+
api.clear();
474+
wrapped.add(0, null);
475+
} catch (Error | RuntimeException exception) {
476+
ArrayList<Object> wrappedError = wrapError(exception);
477+
wrapped = wrappedError;
478+
}
479+
reply.reply(wrapped);
480+
});
481+
} else {
482+
channel.setMessageHandler(null);
483+
}
484+
}
485+
}
486+
}
442487
/**
443488
* Handles methods calls to the native Java Object class.
444489
*

packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InstanceManager.java

Lines changed: 57 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
*/
3131
@SuppressWarnings("unchecked")
3232
public class InstanceManager {
33+
/// Constant returned from #addHostCreatedInstance() if the manager is closed.
34+
public static final int INSTANCE_CLOSED = -1;
35+
3336
// Identifiers are locked to a specific range to avoid collisions with objects
3437
// created simultaneously from Dart.
3538
// Host uses identifiers >= 2^16 and Dart is expected to use values n where,
@@ -87,8 +90,7 @@ private InstanceManager(FinalizationListener finalizationListener) {
8790
*/
8891
@Nullable
8992
public <T> T remove(long identifier) {
90-
if (isClosed()) {
91-
Log.w(TAG, CLOSED_WARNING);
93+
if (assertNotClosed()) {
9294
return null;
9395
}
9496
return (T) strongInstances.remove(identifier);
@@ -97,17 +99,22 @@ public <T> T remove(long identifier) {
9799
/**
98100
* Retrieves the identifier paired with an instance.
99101
*
100-
* <p>If the manager contains `instance`, as a strong or weak reference, the strong reference to
101-
* `instance` will be recreated and will need to be removed again with {@link #remove(long)}.
102+
* <p>If the manager contains a strong reference to `instance`, it will return the identifier
103+
* associated with `instance`. If the manager contains only a weak reference to `instance`, a new
104+
* strong reference to `instance` will be added and will need to be removed again with {@link
105+
* #remove(long)}.
106+
*
107+
* <p>If this method returns a nonnull identifier, this method also expects the Dart
108+
* `InstanceManager` to have, or recreate, a weak reference to the Dart instance the identifier is
109+
* associated with.
102110
*
103111
* @param instance an instance that may be stored in the manager.
104112
* @return the identifier associated with `instance` if the manager contains the value, otherwise
105113
* null if the manager doesn't contain the value or the manager is closed.
106114
*/
107115
@Nullable
108116
public Long getIdentifierForStrongReference(Object instance) {
109-
if (isClosed()) {
110-
Log.w(TAG, CLOSED_WARNING);
117+
if (assertNotClosed()) {
111118
return null;
112119
}
113120
final Long identifier = identifiers.get(instance);
@@ -120,18 +127,18 @@ public Long getIdentifierForStrongReference(Object instance) {
120127
/**
121128
* Adds a new instance that was instantiated from Dart.
122129
*
123-
* <p>If an instance or identifier has already been added, it will be replaced by the new values.
124-
* The Dart InstanceManager is considered the source of truth and has the capability to overwrite
125-
* stored pairs in response to hot restarts.
130+
* <p>The same instance can be added multiple times, but each identifier must be unique. This
131+
* allows two objects that are equivalent (e.g. the `equals` method returns true and their
132+
* hashcodes are equal) to both be added.
126133
*
127-
* <p>If the manager is closed, the addition is ignored.
134+
* <p>If the manager is closed, the addition is ignored and a warning is logged.
128135
*
129136
* @param instance the instance to be stored.
130-
* @param identifier the identifier to be paired with instance. This value must be >= 0.
137+
* @param identifier the identifier to be paired with instance. This value must be >= 0 and
138+
* unique.
131139
*/
132140
public void addDartCreatedInstance(Object instance, long identifier) {
133-
if (isClosed()) {
134-
Log.w(TAG, CLOSED_WARNING);
141+
if (assertNotClosed()) {
135142
return;
136143
}
137144
addInstance(instance, identifier);
@@ -140,13 +147,18 @@ public void addDartCreatedInstance(Object instance, long identifier) {
140147
/**
141148
* Adds a new instance that was instantiated from the host platform.
142149
*
143-
* @param instance the instance to be stored.
150+
* @param instance the instance to be stored. This must be unique to all other added instances.
144151
* @return the unique identifier stored with instance. If the manager is closed, returns -1.
152+
* Otherwise, returns a value >= 0.
145153
*/
146154
public long addHostCreatedInstance(Object instance) {
147-
if (isClosed()) {
148-
Log.w(TAG, CLOSED_WARNING);
149-
return -1;
155+
if (assertNotClosed()) {
156+
return INSTANCE_CLOSED;
157+
}
158+
159+
if (containsInstance(instance)) {
160+
throw new IllegalArgumentException(
161+
String.format("Instance of `%s` has already been added.", instance.getClass()));
150162
}
151163
final long identifier = nextIdentifier++;
152164
addInstance(instance, identifier);
@@ -156,22 +168,22 @@ public long addHostCreatedInstance(Object instance) {
156168
/**
157169
* Retrieves the instance associated with identifier.
158170
*
159-
* @param identifier the identifier paired to an instance.
171+
* @param identifier the identifier associated with an instance.
160172
* @param <T> the expected return type.
161173
* @return the instance associated with `identifier` if the manager contains the value, otherwise
162174
* null if the manager doesn't contain the value or the manager is closed.
163175
*/
164176
@Nullable
165177
public <T> T getInstance(long identifier) {
166-
if (isClosed()) {
167-
Log.w(TAG, CLOSED_WARNING);
178+
if (assertNotClosed()) {
168179
return null;
169180
}
181+
170182
final WeakReference<T> instance = (WeakReference<T>) weakInstances.get(identifier);
171183
if (instance != null) {
172184
return instance.get();
173185
}
174-
return (T) strongInstances.get(identifier);
186+
return null;
175187
}
176188

177189
/**
@@ -182,8 +194,7 @@ public <T> T getInstance(long identifier) {
182194
* `false`.
183195
*/
184196
public boolean containsInstance(Object instance) {
185-
if (isClosed()) {
186-
Log.w(TAG, CLOSED_WARNING);
197+
if (assertNotClosed()) {
187198
return false;
188199
}
189200
return identifiers.containsKey(instance);
@@ -197,14 +208,23 @@ public boolean containsInstance(Object instance) {
197208
public void close() {
198209
handler.removeCallbacks(this::releaseAllFinalizedInstances);
199210
isClosed = true;
211+
clear();
212+
}
213+
214+
/**
215+
* Removes all of the instances from this manager.
216+
*
217+
* <p>The manager will be empty after this call returns.
218+
*/
219+
public void clear() {
200220
identifiers.clear();
201221
weakInstances.clear();
202222
strongInstances.clear();
203223
weakReferencesToIdentifiers.clear();
204224
}
205225

206226
/**
207-
* Whether the manager has released resources and is not longer usable.
227+
* Whether the manager has released resources and is no longer usable.
208228
*
209229
* <p>See {@link #close()}.
210230
*/
@@ -228,12 +248,24 @@ private void releaseAllFinalizedInstances() {
228248

229249
private void addInstance(Object instance, long identifier) {
230250
if (identifier < 0) {
231-
throw new IllegalArgumentException("Identifier must be >= 0.");
251+
throw new IllegalArgumentException(String.format("Identifier must be >= 0: %d", identifier));
252+
}
253+
if (weakInstances.containsKey(identifier)) {
254+
throw new IllegalArgumentException(
255+
String.format("Identifier has already been added: %d", identifier));
232256
}
233257
final WeakReference<Object> weakReference = new WeakReference<>(instance, referenceQueue);
234258
identifiers.put(instance, identifier);
235259
weakInstances.put(identifier, weakReference);
236260
weakReferencesToIdentifiers.put(weakReference, identifier);
237261
strongInstances.put(identifier, instance);
238262
}
263+
264+
private boolean assertNotClosed() {
265+
if (isClosed()) {
266+
Log.w(TAG, CLOSED_WARNING);
267+
return true;
268+
}
269+
return false;
270+
}
239271
}

packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.CookieManagerHostApi;
1818
import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.DownloadListenerHostApi;
1919
import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.FlutterAssetManagerHostApi;
20+
import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.InstanceManagerHostApi;
2021
import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.JavaObjectHostApi;
2122
import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.JavaScriptChannelHostApi;
2223
import io.flutter.plugins.webviewflutter.GeneratedAndroidWebView.WebChromeClientHostApi;
@@ -84,6 +85,8 @@ private void setUp(
8485
new GeneratedAndroidWebView.JavaObjectFlutterApi(binaryMessenger)
8586
.dispose(identifier, reply -> {}));
8687

88+
InstanceManagerHostApi.setup(binaryMessenger, () -> instanceManager.clear());
89+
8790
viewRegistry.registerViewFactory(
8891
"plugins.flutter.io/webview", new FlutterWebViewFactory(instanceManager));
8992

packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/InstanceManagerTest.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,67 @@ public void containsInstanceReturnsFalseWhenClosed() {
108108

109109
assertFalse(instanceManager.containsInstance(object));
110110
}
111+
112+
@Test
113+
public void clear() {
114+
final InstanceManager instanceManager = InstanceManager.open(identifier -> {});
115+
116+
final Object instance = new Object();
117+
118+
instanceManager.addDartCreatedInstance(instance, 0);
119+
assertTrue(instanceManager.containsInstance(instance));
120+
121+
instanceManager.clear();
122+
assertFalse(instanceManager.containsInstance(instance));
123+
124+
instanceManager.close();
125+
}
126+
127+
@Test
128+
public void canAddSameObjectWithAddDartCreatedInstance() {
129+
final InstanceManager instanceManager = InstanceManager.open(identifier -> {});
130+
131+
final Object instance = new Object();
132+
133+
instanceManager.addDartCreatedInstance(instance, 0);
134+
instanceManager.addDartCreatedInstance(instance, 1);
135+
136+
assertTrue(instanceManager.containsInstance(instance));
137+
138+
assertEquals(instanceManager.getInstance(0), instance);
139+
assertEquals(instanceManager.getInstance(1), instance);
140+
141+
instanceManager.close();
142+
}
143+
144+
@Test(expected = IllegalArgumentException.class)
145+
public void cannotAddSameObjectsWithAddHostCreatedInstance() {
146+
final InstanceManager instanceManager = InstanceManager.open(identifier -> {});
147+
148+
final Object instance = new Object();
149+
150+
instanceManager.addHostCreatedInstance(instance);
151+
instanceManager.addHostCreatedInstance(instance);
152+
153+
instanceManager.close();
154+
}
155+
156+
@Test(expected = IllegalArgumentException.class)
157+
public void cannotUseIdentifierLessThanZero() {
158+
final InstanceManager instanceManager = InstanceManager.open(identifier -> {});
159+
160+
instanceManager.addDartCreatedInstance(new Object(), -1);
161+
162+
instanceManager.close();
163+
}
164+
165+
@Test(expected = IllegalArgumentException.class)
166+
public void identifiersMustBeUnique() {
167+
final InstanceManager instanceManager = InstanceManager.open(identifier -> {});
168+
169+
instanceManager.addDartCreatedInstance(new Object(), 0);
170+
instanceManager.addDartCreatedInstance(new Object(), 0);
171+
172+
instanceManager.close();
173+
}
111174
}

packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewClientTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,8 @@ public WebViewClient createWebViewClient(WebViewClientFlutterApiImpl flutterApi)
118118
},
119119
mockFlutterApi);
120120

121-
instanceManager.addDartCreatedInstance(mockWebViewClient, 0);
122-
webViewClientHostApi.setSynchronousReturnValueForShouldOverrideUrlLoading(0L, false);
121+
instanceManager.addDartCreatedInstance(mockWebViewClient, 2);
122+
webViewClientHostApi.setSynchronousReturnValueForShouldOverrideUrlLoading(2L, false);
123123

124124
verify(mockWebViewClient).setReturnValueForShouldOverrideUrlLoading(false);
125125
}

packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -310,9 +310,9 @@ public void destroy() {
310310
}
311311
};
312312

313-
testInstanceManager.addDartCreatedInstance(webView, 0);
313+
testInstanceManager.addDartCreatedInstance(webView, 1);
314314
final JavaObjectHostApiImpl javaObjectHostApi = new JavaObjectHostApiImpl(testInstanceManager);
315-
javaObjectHostApi.dispose(0L);
315+
javaObjectHostApi.dispose(1L);
316316

317317
assertTrue(destroyCalled[0]);
318318
}

packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import 'package:flutter/services.dart';
1818
import 'package:flutter_test/flutter_test.dart';
1919
import 'package:integration_test/integration_test.dart';
2020
import 'package:webview_flutter_android/src/android_webview.dart' as android;
21+
import 'package:webview_flutter_android/src/android_webview.g.dart';
2122
import 'package:webview_flutter_android/src/android_webview_api_impls.dart';
2223
import 'package:webview_flutter_android/src/instance_manager.dart';
2324
import 'package:webview_flutter_android/src/weak_reference_utils.dart';
@@ -111,6 +112,11 @@ Future<void> main() async {
111112
}
112113
});
113114

115+
// Since the InstanceManager of the apis are being changed, the native
116+
// InstanceManager needs to be cleared otherwise an exception will be
117+
// thrown that an identifier is being reused.
118+
await InstanceManagerHostApi().clear();
119+
114120
android.WebView.api = WebViewHostApiImpl(
115121
instanceManager: instanceManager,
116122
);

0 commit comments

Comments
 (0)