Skip to content

Commit d69045a

Browse files
authored
Fix the potential ConcurrentModificationException in unregisterListener (#27)
1 parent 28bf88e commit d69045a

File tree

3 files changed

+60
-4
lines changed

3 files changed

+60
-4
lines changed

launchdarkly-android-client/build.gradle

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ apply plugin: 'signing'
44
apply plugin: 'org.ajoberstar.github-pages'
55
// make sure this line comes *after* you apply the Android plugin
66
apply plugin: 'com.getkeepsafe.dexcount'
7+
apply plugin: 'io.codearte.nexus-staging'
78

89
allprojects {
910
group = 'com.launchdarkly'
@@ -77,6 +78,7 @@ buildscript {
7778
classpath 'org.ajoberstar:gradle-git:1.5.0-rc.1'
7879
classpath 'com.github.jengelman.gradle.plugins:shadow:1.2.3'
7980
classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:0.6.3'
81+
classpath "io.codearte.gradle.nexus:gradle-nexus-staging-plugin:0.8.0"
8082
}
8183
}
8284

@@ -117,6 +119,10 @@ signing {
117119
sign configurations.archives
118120
}
119121

122+
nexusStaging {
123+
packageGroup = "com.launchdarkly"
124+
}
125+
120126
uploadArchives {
121127
repositories {
122128
mavenDeployer {

launchdarkly-android-client/src/androidTest/java/com/launchdarkly/android/UserManagerTest.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import android.content.SharedPreferences;
44
import android.support.test.rule.ActivityTestRule;
55
import android.support.test.runner.AndroidJUnit4;
6+
import android.util.Pair;
67

78
import com.google.common.util.concurrent.Futures;
89
import com.google.common.util.concurrent.ListenableFuture;
@@ -18,12 +19,16 @@
1819
import org.junit.runner.RunWith;
1920

2021
import java.util.Arrays;
22+
import java.util.Collection;
2123
import java.util.List;
2224
import java.util.concurrent.ExecutionException;
2325
import java.util.concurrent.Future;
2426

2527
import static com.google.common.util.concurrent.Futures.immediateFailedFuture;
2628
import static junit.framework.Assert.assertEquals;
29+
import static junit.framework.Assert.assertNotNull;
30+
import static junit.framework.Assert.assertFalse;
31+
import static junit.framework.Assert.assertTrue;
2732
import static org.easymock.EasyMock.expect;
2833
import static org.easymock.EasyMock.reset;
2934

@@ -110,6 +115,42 @@ public void TestCanStoreExactly5Users() throws InterruptedException {
110115
assertFlagValue(flagKey, user5);
111116
}
112117

118+
@Test
119+
public void TestRegisterUnregisterListener() {
120+
FeatureFlagChangeListener listener = new FeatureFlagChangeListener() {
121+
@Override
122+
public void onFeatureFlagChange(String flagKey) {
123+
}
124+
};
125+
126+
userManager.registerListener("key", listener);
127+
Collection<Pair<FeatureFlagChangeListener, SharedPreferences.OnSharedPreferenceChangeListener>> listeners = userManager.getListenersByKey("key");
128+
assertNotNull(listeners);
129+
assertFalse(listeners.isEmpty());
130+
131+
userManager.unregisterListener("key", listener);
132+
listeners = userManager.getListenersByKey("key");
133+
assertNotNull(listeners);
134+
assertTrue(listeners.isEmpty());
135+
}
136+
137+
@Test
138+
public void TestUnregisterListenerWithDuplicates() {
139+
FeatureFlagChangeListener listener = new FeatureFlagChangeListener() {
140+
@Override
141+
public void onFeatureFlagChange(String flagKey) {
142+
}
143+
};
144+
145+
userManager.registerListener("key", listener);
146+
userManager.registerListener("key", listener);
147+
userManager.unregisterListener("key", listener);
148+
149+
Collection<Pair<FeatureFlagChangeListener, SharedPreferences.OnSharedPreferenceChangeListener>> listeners = userManager.getListenersByKey("key");
150+
assertNotNull(listeners);
151+
assertTrue(listeners.isEmpty());
152+
}
153+
113154
private Future<Void> setUser(String userKey, JsonObject flags) {
114155
LDUser user = new LDUser.Builder(userKey).build();
115156
ListenableFuture<JsonObject> jsonObjectFuture = Futures.immediateFuture(flags);

launchdarkly-android-client/src/main/java/com/launchdarkly/android/UserManager.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import android.util.Pair;
1111

1212
import com.google.common.base.Function;
13-
import com.google.common.collect.ArrayListMultimap;
13+
import com.google.common.collect.HashMultimap;
1414
import com.google.common.collect.Multimap;
1515
import com.google.common.util.concurrent.FutureCallback;
1616
import com.google.common.util.concurrent.Futures;
@@ -19,10 +19,13 @@
1919
import com.google.gson.JsonObject;
2020

2121
import java.io.File;
22+
23+
import java.util.Collection;
2224
import java.util.Collections;
2325
import java.util.Comparator;
2426
import java.util.Date;
2527
import java.util.HashMap;
28+
import java.util.Iterator;
2629
import java.util.LinkedList;
2730
import java.util.List;
2831
import java.util.Map;
@@ -49,7 +52,7 @@ class UserManager {
4952

5053
private final Application application;
5154
// Maintains references enabling (de)registration of listeners for realtime updates
52-
private final Multimap<String, Pair<FeatureFlagChangeListener, OnSharedPreferenceChangeListener>> listeners = ArrayListMultimap.create();
55+
private final Multimap<String, Pair<FeatureFlagChangeListener, OnSharedPreferenceChangeListener>> listeners = HashMultimap.create();
5356

5457
// The current user- we'll always fetch this user from the response we get from the api
5558
private SharedPreferences currentUserSharedPrefs;
@@ -153,6 +156,10 @@ public Void apply(JsonObject input) {
153156
});
154157
}
155158

159+
Collection<Pair<FeatureFlagChangeListener, OnSharedPreferenceChangeListener>> getListenersByKey(String key) {
160+
return listeners.get(key);
161+
}
162+
156163
void registerListener(final String key, final FeatureFlagChangeListener listener) {
157164
OnSharedPreferenceChangeListener sharedPrefsListener = new OnSharedPreferenceChangeListener() {
158165
@Override
@@ -172,11 +179,13 @@ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, Strin
172179

173180
void unregisterListener(String key, FeatureFlagChangeListener listener) {
174181
synchronized (listeners) {
175-
for (Pair<FeatureFlagChangeListener, OnSharedPreferenceChangeListener> pair : listeners.get(key)) {
182+
Iterator<Pair<FeatureFlagChangeListener, OnSharedPreferenceChangeListener>> it = listeners.get(key).iterator();
183+
while (it.hasNext()) {
184+
Pair<FeatureFlagChangeListener, OnSharedPreferenceChangeListener> pair = it.next();
176185
if (pair.first.equals(listener)) {
177186
Log.d(TAG, "Removing listener for key: [" + key + "]");
178187
activeUserSharedPrefs.unregisterOnSharedPreferenceChangeListener(pair.second);
179-
listeners.remove(key, pair);
188+
it.remove();
180189
}
181190
}
182191
}

0 commit comments

Comments
 (0)