Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Concurrent Modification Exception #1091

Merged
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
211 changes: 128 additions & 83 deletions framework/src/org/apache/cordova/PluginManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ Licensed to the Apache Software Foundation (ASF) under one
package org.apache.cordova;

import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;

import org.json.JSONException;

Expand All @@ -40,8 +42,8 @@ public class PluginManager {
private static final int SLOW_EXEC_WARNING_THRESHOLD = Debug.isDebuggerConnected() ? 60 : 16;

// List of service entries
private final LinkedHashMap<String, CordovaPlugin> pluginMap = new LinkedHashMap<String, CordovaPlugin>();
private final LinkedHashMap<String, PluginEntry> entryMap = new LinkedHashMap<String, PluginEntry>();
private final Map<String, CordovaPlugin> pluginMap = Collections.synchronizedMap(new LinkedHashMap<String, CordovaPlugin>());
private final Map<String, PluginEntry> entryMap = Collections.synchronizedMap(new LinkedHashMap<String, PluginEntry>());

private final CordovaInterface ctx;
private final CordovaWebView app;
Expand Down Expand Up @@ -90,13 +92,17 @@ public void init() {
* Create plugins objects that have onload set.
*/
private void startupPlugins() {
for (PluginEntry entry : entryMap.values()) {
// Add a null entry to for each non-startup plugin to avoid ConcurrentModificationException
// When iterating plugins.
if (entry.onload) {
getPlugin(entry.service);
} else {
pluginMap.put(entry.service, null);
synchronized (entryMap) {
for (PluginEntry entry : entryMap.values()) {
// Add a null entry to for each non-startup plugin to avoid ConcurrentModificationException
// When iterating plugins.
if (entry.onload) {
getPlugin(entry.service);
}
else {
LOG.d(TAG, "startupPlugins: put - " + entry.service);
pluginMap.put(entry.service, null);
}
}
}
}
Expand Down Expand Up @@ -169,6 +175,7 @@ public CordovaPlugin getPlugin(String service) {
ret = instantiatePlugin(pe.pluginClass);
}
ret.privateInitialize(service, ctx, app, app.getPreferences());
LOG.d(TAG, "getPlugin - put: " + service);
pluginMap.put(service, ret);
}
return ret;
Expand Down Expand Up @@ -196,6 +203,7 @@ public void addService(PluginEntry entry) {
this.entryMap.put(entry.service, entry);
if (entry.plugin != null) {
entry.plugin.privateInitialize(entry.service, ctx, app, app.getPreferences());
LOG.d(TAG, "addService: put - " + entry.service);
pluginMap.put(entry.service, entry.plugin);
}
}
Expand All @@ -206,9 +214,11 @@ public void addService(PluginEntry entry) {
* @param multitasking Flag indicating if multitasking is turned on for app
*/
public void onPause(boolean multitasking) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onPause(multitasking);
synchronized (this.pluginMap) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onPause(multitasking);
}
}
}
}
Expand All @@ -226,9 +236,11 @@ public void onPause(boolean multitasking) {
*
*/
public boolean onReceivedHttpAuthRequest(CordovaWebView view, ICordovaHttpAuthHandler handler, String host, String realm) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null && plugin.onReceivedHttpAuthRequest(app, handler, host, realm)) {
return true;
synchronized (this.pluginMap) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null && plugin.onReceivedHttpAuthRequest(app, handler, host, realm)) {
return true;
}
}
}
return false;
Expand All @@ -245,9 +257,11 @@ public boolean onReceivedHttpAuthRequest(CordovaWebView view, ICordovaHttpAuthHa
*
*/
public boolean onReceivedClientCertRequest(CordovaWebView view, ICordovaClientCertRequest request) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null && plugin.onReceivedClientCertRequest(app, request)) {
return true;
synchronized (this.pluginMap) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null && plugin.onReceivedClientCertRequest(app, request)) {
return true;
}
}
}
return false;
Expand All @@ -259,9 +273,11 @@ public boolean onReceivedClientCertRequest(CordovaWebView view, ICordovaClientCe
* @param multitasking Flag indicating if multitasking is turned on for app
*/
public void onResume(boolean multitasking) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onResume(multitasking);
synchronized (this.pluginMap) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onResume(multitasking);
}
}
}
}
Expand All @@ -270,9 +286,11 @@ public void onResume(boolean multitasking) {
* Called when the activity is becoming visible to the user.
*/
public void onStart() {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onStart();
synchronized (this.pluginMap) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onStart();
}
}
}
}
Expand All @@ -281,9 +299,11 @@ public void onStart() {
* Called when the activity is no longer visible to the user.
*/
public void onStop() {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onStop();
synchronized (this.pluginMap) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onStop();
}
}
}
}
Expand All @@ -292,9 +312,11 @@ public void onStop() {
* The final call you receive before your activity is destroyed.
*/
public void onDestroy() {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onDestroy();
synchronized (this.pluginMap) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onDestroy();
}
}
}
}
Expand All @@ -307,11 +329,14 @@ public void onDestroy() {
* @return Object to stop propagation or null
*/
public Object postMessage(String id, Object data) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
Object obj = plugin.onMessage(id, data);
if (obj != null) {
return obj;
LOG.d(TAG, "postMessage: " + id);
synchronized (this.pluginMap) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
Object obj = plugin.onMessage(id, data);
if (obj != null) {
return obj;
}
}
}
}
Expand All @@ -322,9 +347,11 @@ public Object postMessage(String id, Object data) {
* Called when the activity receives a new intent.
*/
public void onNewIntent(Intent intent) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onNewIntent(intent);
synchronized (this.pluginMap) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onNewIntent(intent);
}
}
}
}
Expand All @@ -341,12 +368,14 @@ public void onNewIntent(Intent intent) {
* false to block the resource.
*/
public boolean shouldAllowRequest(String url) {
for (PluginEntry entry : this.entryMap.values()) {
CordovaPlugin plugin = pluginMap.get(entry.service);
if (plugin != null) {
Boolean result = plugin.shouldAllowRequest(url);
if (result != null) {
return result;
synchronized (this.entryMap) {
for (PluginEntry entry : this.entryMap.values()) {
CordovaPlugin plugin = pluginMap.get(entry.service);
if (plugin != null) {
Boolean result = plugin.shouldAllowRequest(url);
if (result != null) {
return result;
}
}
}
}
Expand Down Expand Up @@ -379,12 +408,14 @@ public boolean shouldAllowRequest(String url) {
* false to block the navigation.
*/
public boolean shouldAllowNavigation(String url) {
for (PluginEntry entry : this.entryMap.values()) {
CordovaPlugin plugin = pluginMap.get(entry.service);
if (plugin != null) {
Boolean result = plugin.shouldAllowNavigation(url);
if (result != null) {
return result;
synchronized (this.entryMap) {
for (PluginEntry entry : this.entryMap.values()) {
CordovaPlugin plugin = pluginMap.get(entry.service);
if (plugin != null) {
Boolean result = plugin.shouldAllowNavigation(url);
if (result != null) {
return result;
}
}
}
}
Expand All @@ -398,12 +429,14 @@ public boolean shouldAllowNavigation(String url) {
* Called when the webview is requesting the exec() bridge be enabled.
*/
public boolean shouldAllowBridgeAccess(String url) {
for (PluginEntry entry : this.entryMap.values()) {
CordovaPlugin plugin = pluginMap.get(entry.service);
if (plugin != null) {
Boolean result = plugin.shouldAllowBridgeAccess(url);
if (result != null) {
return result;
synchronized (this.entryMap) {
for (PluginEntry entry : this.entryMap.values()) {
CordovaPlugin plugin = pluginMap.get(entry.service);
if (plugin != null) {
Boolean result = plugin.shouldAllowBridgeAccess(url);
if (result != null) {
return result;
}
}
}
}
Expand All @@ -425,12 +458,14 @@ public boolean shouldAllowBridgeAccess(String url) {
* false to block the intent.
*/
public Boolean shouldOpenExternalUrl(String url) {
for (PluginEntry entry : this.entryMap.values()) {
CordovaPlugin plugin = pluginMap.get(entry.service);
if (plugin != null) {
Boolean result = plugin.shouldOpenExternalUrl(url);
if (result != null) {
return result;
synchronized (this.entryMap) {
for (PluginEntry entry : this.entryMap.values()) {
CordovaPlugin plugin = pluginMap.get(entry.service);
if (plugin != null) {
Boolean result = plugin.shouldOpenExternalUrl(url);
if (result != null) {
return result;
}
}
}
}
Expand All @@ -446,32 +481,38 @@ public Boolean shouldOpenExternalUrl(String url) {
* @return Return false to allow the URL to load, return true to prevent the URL from loading.
*/
public boolean onOverrideUrlLoading(String url) {
for (PluginEntry entry : this.entryMap.values()) {
CordovaPlugin plugin = pluginMap.get(entry.service);
if (plugin != null && plugin.onOverrideUrlLoading(url)) {
return true;
synchronized (this.entryMap) {
for (PluginEntry entry : this.entryMap.values()) {
CordovaPlugin plugin = pluginMap.get(entry.service);
if (plugin != null && plugin.onOverrideUrlLoading(url)) {
return true;
}
}
return false;
}
return false;
}

/**
* Called when the app navigates or refreshes.
*/
public void onReset() {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onReset();
synchronized (this.pluginMap) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onReset();
}
}
}
}

Uri remapUri(Uri uri) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
Uri ret = plugin.remapUri(uri);
if (ret != null) {
return ret;
synchronized (this.pluginMap) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
Uri ret = plugin.remapUri(uri);
if (ret != null) {
return ret;
}
}
}
}
Expand Down Expand Up @@ -504,20 +545,24 @@ private CordovaPlugin instantiatePlugin(String className) {
* @param newConfig The new device configuration
*/
public void onConfigurationChanged(Configuration newConfig) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onConfigurationChanged(newConfig);
synchronized (this.pluginMap) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
plugin.onConfigurationChanged(newConfig);
}
}
}
}

public Bundle onSaveInstanceState() {
Bundle state = new Bundle();
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
Bundle pluginState = plugin.onSaveInstanceState();
if(pluginState != null) {
state.putBundle(plugin.getServiceName(), pluginState);
synchronized (this.pluginMap) {
for (CordovaPlugin plugin : this.pluginMap.values()) {
if (plugin != null) {
Bundle pluginState = plugin.onSaveInstanceState();
if (pluginState != null) {
state.putBundle(plugin.getServiceName(), pluginState);
}
}
}
}
Expand Down