Skip to content

Commit 15801ba

Browse files
committed
Fixes OneSignalPrefs reads that may use old values
* OneSignalPrefs uses a different thread to write, if a read is done before the write thread finishes the read would use old value - This may have resulted in some vary rare race condition issues - Reads are now done from the pending write cache now to prevent any possible issue - This should improve test stability as we did not wait for writes between tests before * OneSignalPrefs now uses a HandlerThread instead of an overkill ScheduledThreadPoolExecutor - This results in only using 1 thread instead of up to 10 as it was configured - Writes are now buffer with a short 200ms delay to prevent extra write I/O
1 parent 32d9143 commit 15801ba

7 files changed

Lines changed: 200 additions & 141 deletions

File tree

Lines changed: 145 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,47 @@
1+
/**
2+
* Modified MIT License
3+
*
4+
* Copyright 2018 OneSignal
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* 1. The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
*
16+
* 2. All copies of substantial portions of the Software may only be used in connection
17+
* with services provided by OneSignal.
18+
*
19+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25+
* THE SOFTWARE.
26+
*/
27+
28+
129
package com.onesignal;
230

331
import android.content.Context;
432
import android.content.SharedPreferences;
5-
import android.support.annotation.NonNull;
6-
7-
import java.util.HashSet;
8-
import java.util.Set;
9-
import java.util.concurrent.ConcurrentHashMap;
10-
import java.util.concurrent.ScheduledThreadPoolExecutor;
11-
import java.util.concurrent.ThreadFactory;
33+
import android.os.Handler;
34+
import android.os.HandlerThread;
1235

13-
/**
14-
* Copyright 2017 OneSignal
15-
* Created by alamgir on 9/20/17.
16-
*/
36+
import java.util.HashMap;
1737

1838
class OneSignalPrefs {
1939

40+
// SharedPreferences Instances
2041
static final String PREFS_ONESIGNAL = OneSignal.class.getSimpleName();
2142
static final String PREFS_PLAYER_PURCHASES = "GTPlayerPurchases";
2243

23-
// PREFERENCES KEYS
44+
// PREFERENCES KEYS
2445
static final String PREFS_OS_LAST_LOCATION_TIME = "OS_LAST_LOCATION_TIME";
2546
static final String PREFS_GT_SOUND_ENABLED = "GT_SOUND_ENABLED";
2647
static final String PREFS_OS_LAST_SESSION_TIME = "OS_LAST_SESSION_TIME";
@@ -45,154 +66,178 @@ class OneSignalPrefs {
4566
static final String PREFS_ONESIGNAL_SYNCED_SUBSCRIPTION = "ONESIGNAL_SYNCED_SUBSCRIPTION";
4667
static final String PREFS_GT_REGISTRATION_ID = "GT_REGISTRATION_ID";
4768

48-
// PLAYER PURCHASE KEYS
69+
// PLAYER PURCHASE KEYS
4970
static final String PREFS_PURCHASE_TOKENS = "purchaseTokens";
5071
static final String PREFS_EXISTING_PURCHASES = "ExistingPurchases";
5172

52-
static ConcurrentHashMap<String,SharedPreferences> preferencesMap = new ConcurrentHashMap<>();
53-
//use a thread pool executor to execute disk writes
54-
private static final ScheduledThreadPoolExecutor prefsExecutor = new ScheduledThreadPoolExecutor(1);
73+
// Buffered writes to apply on WritePrefHandlerThread with a short delay
74+
static HashMap<String, HashMap<String, Object>> prefsToApply;
75+
public static WritePrefHandlerThread prefsHandler;
76+
5577
static {
56-
prefsExecutor.setThreadFactory(new ThreadFactory() {
57-
@Override
58-
public Thread newThread(@NonNull Runnable runnable) {
59-
Thread newThread = new Thread(runnable);
60-
newThread.setName("ONESIGNAL_EXECUTOR_" + newThread.getId());
61-
return newThread;
62-
}
63-
});
78+
initializePool();
6479
}
6580

66-
private static class PrefsWriteRunnable implements Runnable {
81+
public static class WritePrefHandlerThread extends HandlerThread {
82+
public Handler mHandler;
6783

68-
private String prefsName;
69-
private String prefKeyToWrite;
70-
private Object prefValueToWrite;
84+
private static final int WRITE_CALL_DELAY_TO_BUFFER_MS = 200;
85+
private long lastSyncTime = 0L;
7186

72-
PrefsWriteRunnable(String prefsName,
73-
String prefKeyToWrite,
74-
Object prefValueToWrite) {
75-
this.prefsName = prefsName;
76-
this.prefKeyToWrite = prefKeyToWrite;
77-
this.prefValueToWrite = prefValueToWrite;
87+
WritePrefHandlerThread() {
88+
super("OSH_WritePrefs");
89+
start();
90+
mHandler = new Handler(getLooper());
7891
}
7992

80-
@Override
81-
public void run() {
82-
SharedPreferences prefsToWrite = getSharedPrefsByName(prefsName);
93+
void startDelayedWrite() {
94+
synchronized (mHandler) {
95+
mHandler.removeCallbacksAndMessages(null);
96+
if (lastSyncTime == 0)
97+
lastSyncTime = System.currentTimeMillis();
8398

84-
if(prefsToWrite != null) {
85-
SharedPreferences.Editor editor = prefsToWrite.edit();
99+
long delay = lastSyncTime - System.currentTimeMillis() + WRITE_CALL_DELAY_TO_BUFFER_MS;
100+
101+
mHandler.postDelayed(getNewRunnable(), delay);
102+
}
103+
}
86104

87-
if(prefValueToWrite instanceof String) {
88-
editor.putString(prefKeyToWrite,(String)prefValueToWrite);
89-
} else if(prefValueToWrite instanceof Boolean) {
90-
editor.putBoolean(prefKeyToWrite, (Boolean)prefValueToWrite);
91-
} else if(prefValueToWrite instanceof Integer) {
92-
editor.putInt(prefKeyToWrite, (Integer)prefValueToWrite);
93-
} else if(prefValueToWrite instanceof Long) {
94-
editor.putLong(prefKeyToWrite, (Long)prefValueToWrite);
105+
private Runnable getNewRunnable() {
106+
return new Runnable() {
107+
@Override
108+
public void run() {
109+
flushBufferToDisk();
95110
}
96-
OneSignal.Log(OneSignal.LOG_LEVEL.INFO,
97-
"updating prefs: " + prefsName + ", "+ prefKeyToWrite);
111+
};
112+
}
98113

114+
private void flushBufferToDisk() {
115+
for (String pref : prefsToApply.keySet()) {
116+
SharedPreferences prefsToWrite = getSharedPrefsByName(pref);
117+
SharedPreferences.Editor editor = prefsToWrite.edit();
118+
HashMap<String, Object> prefHash = prefsToApply.get(pref);
119+
synchronized (prefHash) {
120+
for (String key : prefHash.keySet()) {
121+
Object value = prefHash.get(key);
122+
if (value instanceof String)
123+
editor.putString(key, (String)value);
124+
else if (value instanceof Boolean)
125+
editor.putBoolean(key, (Boolean)value);
126+
else if (value instanceof Integer)
127+
editor.putInt(key, (Integer)value);
128+
else if (value instanceof Long)
129+
editor.putLong(key, (Long)value);
130+
}
131+
prefHash.clear();
132+
}
99133
editor.apply();
100134
}
101135

136+
lastSyncTime = System.currentTimeMillis();
102137
}
103138
}
104139

140+
public static void initializePool() {
141+
prefsToApply = new HashMap<>();
142+
prefsToApply.put(PREFS_ONESIGNAL, new HashMap<String, Object>());
143+
prefsToApply.put(PREFS_PLAYER_PURCHASES, new HashMap<String, Object>());
144+
145+
prefsHandler = new WritePrefHandlerThread();
146+
}
105147

106-
static void saveString(final String prefsName,final String key,final String value) {
107-
PrefsWriteRunnable saveStringRunnable = new PrefsWriteRunnable(prefsName, key, value);
108-
prefsExecutor.execute(saveStringRunnable);
148+
static void saveString(final String prefsName, final String key, final String value) {
149+
save(prefsName, key, value);
109150
}
110151

111152
static void saveBool(String prefsName, String key, boolean value) {
112-
PrefsWriteRunnable saveBoolRunnable = new PrefsWriteRunnable(prefsName, key, value);
113-
prefsExecutor.execute(saveBoolRunnable);
153+
save(prefsName, key, value);
114154
}
115155

116156
static void saveInt(String prefsName, String key, int value) {
117-
PrefsWriteRunnable saveBoolRunnable = new PrefsWriteRunnable(prefsName, key, value);
118-
prefsExecutor.execute(saveBoolRunnable);
157+
save(prefsName, key, value);
119158
}
120159

121160
static void saveLong(String prefsName, String key, long value) {
122-
PrefsWriteRunnable saveBoolRunnable = new PrefsWriteRunnable(prefsName,key,value);
123-
prefsExecutor.execute(saveBoolRunnable);
161+
save(prefsName, key, value);
124162
}
125163

126-
static boolean hasBool(String prefsName, String key) {
127-
SharedPreferences prefs = getSharedPrefsByName(prefsName);
128-
if(prefs != null)
129-
return getSharedPrefsByName(prefsName).contains(key);
130-
131-
return false;
132-
}
133-
134-
static boolean has(String prefsName, String key) {
135-
SharedPreferences prefs = getSharedPrefsByName(prefsName);
136-
if(prefs != null)
137-
return getSharedPrefsByName(prefsName).contains(key);
138-
139-
return false;
164+
static private void save(String prefsName, String key, Object value) {
165+
HashMap<String, Object> pref = prefsToApply.get(prefsName);
166+
synchronized (pref) {
167+
pref.put(key, value);
168+
}
169+
prefsHandler.startDelayedWrite();
140170
}
141171

142172
static String getString(String prefsName, String key, String defValue) {
143-
SharedPreferences prefs = getSharedPrefsByName(prefsName);
144-
if(prefs != null)
145-
return prefs.getString(key, defValue);
146-
147-
return defValue;
173+
return (String)get(prefsName, key, String.class, defValue);
148174
}
149175

150176
static boolean getBool(String prefsName, String key, boolean defValue) {
151-
SharedPreferences prefs = getSharedPrefsByName(prefsName);
152-
if(prefs != null)
153-
return prefs.getBoolean(key, defValue);
154-
155-
return defValue;
177+
return (Boolean)get(prefsName, key, Boolean.class, defValue);
156178
}
157179

158180
static int getInt(String prefsName, String key, int defValue) {
159-
SharedPreferences prefs = getSharedPrefsByName(prefsName);
160-
if(prefs != null)
161-
return prefs.getInt(key, defValue);
162-
163-
return defValue;
181+
return (Integer)get(prefsName, key, Integer.class, defValue);
164182
}
165183

166184
static long getLong(String prefsName, String key, long defValue) {
185+
return (Long)get(prefsName, key, Long.class, defValue);
186+
}
187+
188+
// If type == Object then this is a contains check
189+
private static Object get(String prefsName, String key, Class type, Object defValue) {
190+
HashMap<String, Object> pref = prefsToApply.get(prefsName);
191+
192+
synchronized (pref) {
193+
if (type.equals(Object.class) && pref.containsKey(key))
194+
return true;
195+
196+
Object cachedValue = pref.get(key);
197+
if (cachedValue != null)
198+
return cachedValue;
199+
}
200+
167201
SharedPreferences prefs = getSharedPrefsByName(prefsName);
168-
if(prefs != null)
169-
return prefs.getLong(key, defValue);
202+
if (prefs != null ) {
203+
if (type.equals(String.class))
204+
return prefs.getString(key, (String)defValue);
205+
else if (type.equals(Boolean.class))
206+
return prefs.getBoolean(key, (Boolean)defValue);
207+
else if (type.equals(Integer.class))
208+
return prefs.getInt(key, (Integer)defValue);
209+
else if (type.equals(Long.class))
210+
return prefs.getLong(key, (Long)defValue);
211+
else if (type.equals(Object.class))
212+
return prefs.contains(key);
213+
214+
return null;
215+
}
170216

171217
return defValue;
172218
}
173219

220+
// TODO: Removes could be optimized as well.
221+
// running applying here for safely for now.
174222
static void remove(String prefsName, String key) {
223+
HashMap<String, Object> pref = prefsToApply.get(prefsName);
224+
synchronized (pref) {
225+
pref.remove(key);
226+
}
227+
175228
SharedPreferences prefs = getSharedPrefsByName(prefsName);
176-
if(prefs != null) {
229+
if (prefs != null) {
177230
SharedPreferences.Editor editor = prefs.edit();
178231
editor.remove(key);
179232
editor.apply();
180233
}
181234
}
182235

183236
private static synchronized SharedPreferences getSharedPrefsByName(String prefsName) {
184-
SharedPreferences prefs;
185-
if(OneSignal.appContext == null)
237+
if (OneSignal.appContext == null)
186238
return null;
187239

188-
if(!preferencesMap.contains(prefsName)) {
189-
prefs = OneSignal.appContext.getSharedPreferences(prefsName, Context.MODE_PRIVATE);
190-
preferencesMap.put(prefsName, prefs);
191-
}
192-
else
193-
prefs = preferencesMap.get(prefsName);
194-
195-
return prefs;
240+
return OneSignal.appContext.getSharedPreferences(prefsName, Context.MODE_PRIVATE);
196241
}
197242

198243
}

OneSignalSDK/onesignal/src/main/java/com/onesignal/PushRegistratorGPS.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
import android.app.PendingIntent.CanceledException;
3636
import android.content.Context;
3737
import android.content.DialogInterface;
38-
import android.content.SharedPreferences;
3938
import android.content.DialogInterface.OnClickListener;
4039
import android.content.pm.PackageInfo;
4140
import android.content.pm.PackageManager;

OneSignalSDK/onesignal/src/main/java/com/onesignal/TrackGooglePurchase.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,10 @@
3737
import org.json.JSONException;
3838
import org.json.JSONObject;
3939

40-
import com.onesignal.OneSignal;
41-
import com.onesignal.OneSignal.IdsAvailableHandler;
42-
4340
import android.content.ComponentName;
4441
import android.content.Context;
4542
import android.content.Intent;
4643
import android.content.ServiceConnection;
47-
import android.content.SharedPreferences;
4844
import android.content.pm.PackageManager;
4945
import android.os.Bundle;
5046
import android.os.IBinder;

OneSignalSDK/unittest/src/test/java/com/onesignal/OneSignalPackagePrivateHelper.java

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -96,22 +96,6 @@ public void run() {
9696
return true;
9797
}
9898

99-
public static void resetRunnables() throws Exception {
100-
RunnableArg runnable = new RunnableArg<UserStateSynchronizer.NetworkHandlerThread>() {
101-
@Override
102-
void run(UserStateSynchronizer.NetworkHandlerThread handlerThread) {
103-
handlerThread.stopScheduledRunnable();
104-
}
105-
};
106-
107-
processNetworkHandles(runnable);
108-
109-
Looper looper = ActivityLifecycleHandler.focusHandlerThread.getHandlerLooper();
110-
if (looper == null) return;
111-
112-
shadowOf(looper).reset();
113-
}
114-
11599
public static void OneSignal_sendPurchases(JSONArray purchases, boolean newAsExisting, OneSignalRestClient.ResponseHandler responseHandler) {
116100
OneSignal.sendPurchases(purchases, newAsExisting, responseHandler);
117101
}
@@ -182,4 +166,6 @@ public static void NotificationOpenedProcessor_processFromContext(Context contex
182166
public static void NotificationSummaryManager_updateSummaryNotificationAfterChildRemoved(Context context, SQLiteDatabase writableDb, String group, boolean dismissed) {
183167
NotificationSummaryManager.updateSummaryNotificationAfterChildRemoved(context, writableDb, group, dismissed);
184168
}
169+
170+
public class OneSignalPrefs extends com.onesignal.OneSignalPrefs {}
185171
}

OneSignalSDK/unittest/src/test/java/com/test/onesignal/GenerateNotificationRunner.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ public void beforeEachTest() throws Exception {
151151

152152
overrideNotificationId = -1;
153153

154-
TestHelpers.betweenTestsCleanup();
154+
TestHelpers.beforeTestInitAndCleanup();
155155

156156
NotificationManager notificationManager = (NotificationManager) blankActivity.getSystemService(Context.NOTIFICATION_SERVICE);
157157
notificationManager.cancelAll();

0 commit comments

Comments
 (0)