Java tutorial
/** * Modified MIT License * * Copyright 2015 OneSignal * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * 1. The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * 2. All copies of substantial portions of the Software may only be used in connection * with services provided by OneSignal. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.onesignal; import android.content.Context; import android.content.SharedPreferences; import android.os.Handler; import android.os.HandlerThread; import org.json.JSONException; import org.json.JSONObject; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; class OneSignalStateSynchronizer { private static boolean onSessionDone = false, postSessionCalled = false, waitingForSessionResponse = false; // currentUserState - current know state of the user on OneSignal's server. // toSyncUserState - pending state that will be synced to the OneSignal server. // diff will be generated between currentUserState when sync call is made to the server. private static UserState currentUserState, toSyncUserState; static HashMap<Integer, NetworkHandlerThread> networkHandlerThreads = new HashMap<>(); private static Context appContext; private static final String[] LOCATION_FIELDS = new String[] { "lat", "long", "loc_acc", "loc_type" }; private static final Set<String> LOCATION_FIELDS_SET = new HashSet<String>(Arrays.asList(LOCATION_FIELDS)); static private JSONObject generateJsonDiff(JSONObject cur, JSONObject changedTo, JSONObject baseOutput, Set<String> includeFields) { Iterator<String> keys = changedTo.keys(); String key; Object value; JSONObject output; if (baseOutput != null) output = baseOutput; else output = new JSONObject(); while (keys.hasNext()) { try { key = keys.next(); value = changedTo.get(key); if (cur.has(key)) { if (value instanceof JSONObject) { JSONObject curValue = cur.getJSONObject(key); JSONObject outValue = null; if (baseOutput != null && baseOutput.has(key)) outValue = baseOutput.getJSONObject(key); JSONObject returnedJson = generateJsonDiff(curValue, (JSONObject) value, outValue, includeFields); String returnedJsonStr = returnedJson.toString(); if (!returnedJsonStr.equals("{}")) output.put(key, new JSONObject(returnedJsonStr)); } else if (includeFields != null && includeFields.contains(key)) output.put(key, value); else { Object curValue = cur.get(key); if (!value.equals(curValue)) { // Work around for JSON serializer turning doubles/floats into ints since it drops ending 0's if (curValue instanceof Integer && !"".equals(value)) { if (((Number) curValue).doubleValue() != ((Number) value).doubleValue()) output.put(key, value); } else output.put(key, value); } } } else { if (value instanceof JSONObject) output.put(key, new JSONObject(value.toString())); else output.put(key, value); } } catch (JSONException e) { e.printStackTrace(); } } return output; } private static JSONObject getTagsWithoutDeletedKeys(JSONObject jsonObject) { if (jsonObject.has("tags")) { JSONObject toReturn = new JSONObject(); JSONObject keyValues = jsonObject.optJSONObject("tags"); Iterator<String> keys = keyValues.keys(); String key; Object value; while (keys.hasNext()) { key = keys.next(); try { value = keyValues.get(key); if (!"".equals(value)) toReturn.put(key, value); } catch (Throwable t) { } } return toReturn; } return null; } public static void stopAndPersist() { for (Map.Entry<Integer, OneSignalStateSynchronizer.NetworkHandlerThread> handlerThread : OneSignalStateSynchronizer.networkHandlerThreads .entrySet()) handlerThread.getValue().stopScheduledRunnable(); if (toSyncUserState != null) toSyncUserState.persistState(); } class UserState { private final int UNSUBSCRIBE_VALUE = -2; private String persistKey; JSONObject dependValues, syncValues; private UserState(String inPersistKey, boolean load) { persistKey = inPersistKey; if (load) loadState(); else { dependValues = new JSONObject(); syncValues = new JSONObject(); } } private UserState deepClone(String persistKey) { UserState clonedUserState = new UserState(persistKey, false); try { clonedUserState.dependValues = new JSONObject(dependValues.toString()); clonedUserState.syncValues = new JSONObject(syncValues.toString()); } catch (JSONException e) { e.printStackTrace(); } return clonedUserState; } private void addDependFields() { try { syncValues.put("notification_types", getNotificationTypes()); } catch (JSONException e) { } } private int getNotificationTypes() { try { int subscribableStatus = dependValues.getInt("subscribableStatus"); boolean userSubscribePref = dependValues.getBoolean("userSubscribePref"); return subscribableStatus < UNSUBSCRIBE_VALUE ? subscribableStatus : (userSubscribePref ? 1 : UNSUBSCRIBE_VALUE); } catch (JSONException e) { e.printStackTrace(); } return 1; } private Set<String> getGroupChangeField(JSONObject cur, JSONObject changedTo) { try { if (cur.getDouble("lat") != changedTo.getDouble("lat") || cur.getDouble("long") != changedTo.getDouble("long") || cur.getDouble("loc_acc") != changedTo.getDouble("loc_acc") || cur.getDouble("loc_type") != changedTo.getDouble("loc_type")) return LOCATION_FIELDS_SET; } catch (Throwable t) { return LOCATION_FIELDS_SET; } return null; } private JSONObject generateJsonDiff(UserState newState, boolean isSessionCall) { addDependFields(); newState.addDependFields(); Set<String> includeFields = getGroupChangeField(syncValues, newState.syncValues); JSONObject sendJson = OneSignalStateSynchronizer.generateJsonDiff(syncValues, newState.syncValues, null, includeFields); if (!isSessionCall && sendJson.toString().equals("{}")) return null; try { // This makes sure app_id is in all our REST calls. if (!sendJson.has("app_id")) sendJson.put("app_id", (String) syncValues.opt("app_id")); } catch (JSONException e) { e.printStackTrace(); } return sendJson; } void set(String key, Object value) { try { syncValues.put(key, value); } catch (JSONException e) { e.printStackTrace(); } } void setState(String key, Object value) { try { dependValues.put(key, value); } catch (JSONException e) { e.printStackTrace(); } } private void loadState() { final SharedPreferences prefs = OneSignal.getGcmPreferences(appContext); String dependValuesStr = prefs.getString("ONESIGNAL_USERSTATE_DEPENDVALYES_" + persistKey, null); if (dependValuesStr == null) { dependValues = new JSONObject(); try { int subscribableStatus; boolean userSubscribePref = true; if (persistKey.equals("CURRENT_STATE")) subscribableStatus = prefs.getInt("ONESIGNAL_SUBSCRIPTION", 1); else subscribableStatus = prefs.getInt("ONESIGNAL_SYNCED_SUBSCRIPTION", 1); if (subscribableStatus == UNSUBSCRIBE_VALUE) { subscribableStatus = 1; userSubscribePref = false; } dependValues.put("subscribableStatus", subscribableStatus); dependValues.put("userSubscribePref", userSubscribePref); } catch (JSONException e) { } } else { try { dependValues = new JSONObject(dependValuesStr); } catch (JSONException e) { e.printStackTrace(); } } String syncValuesStr = prefs.getString("ONESIGNAL_USERSTATE_SYNCVALYES_" + persistKey, null); try { if (syncValuesStr == null) { syncValues = new JSONObject(); syncValues.put("identifier", prefs.getString("GT_REGISTRATION_ID", null)); } else syncValues = new JSONObject(syncValuesStr); } catch (JSONException e) { e.printStackTrace(); } } private void persistState() { final SharedPreferences prefs = OneSignal.getGcmPreferences(appContext); SharedPreferences.Editor editor = prefs.edit(); editor.putString("ONESIGNAL_USERSTATE_SYNCVALYES_" + persistKey, syncValues.toString()); editor.putString("ONESIGNAL_USERSTATE_DEPENDVALYES_" + persistKey, dependValues.toString()); editor.commit(); } private void persistStateAfterSync(JSONObject inDependValues, JSONObject inSyncValues) { if (inDependValues != null) OneSignalStateSynchronizer.generateJsonDiff(dependValues, inDependValues, dependValues, null); if (inSyncValues != null) { OneSignalStateSynchronizer.generateJsonDiff(syncValues, inSyncValues, syncValues, null); if (inSyncValues.has("tags")) { JSONObject newTags = new JSONObject(); JSONObject curTags = inSyncValues.optJSONObject("tags"); Iterator<String> keys = curTags.keys(); String key; try { while (keys.hasNext()) { key = keys.next(); if (!"".equals(curTags.optString(key))) newTags.put(key, curTags.optString(key)); } if (newTags.toString().equals("{}")) syncValues.remove("tags"); else syncValues.put("tags", newTags); } catch (Throwable t) { } } } if (inDependValues != null || inSyncValues != null) persistState(); } } static class NetworkHandlerThread extends HandlerThread { private static final int NETWORK_HANDLER_USERSTATE = 0; int mType; Handler mHandler = null; static final int MAX_RETRIES = 3; int currentRetry; NetworkHandlerThread(int type) { super("NetworkHandlerThread"); mType = type; start(); mHandler = new Handler(getLooper()); } public void runNewJob() { currentRetry = 0; mHandler.removeCallbacksAndMessages(null); mHandler.postDelayed(getNewRunnable(), 5000); } private Runnable getNewRunnable() { switch (mType) { case NETWORK_HANDLER_USERSTATE: return new Runnable() { @Override public void run() { syncUserState(false); } }; } return null; } void stopScheduledRunnable() { mHandler.removeCallbacksAndMessages(null); } void doRetry() { if (currentRetry < MAX_RETRIES && !mHandler.hasMessages(0)) { currentRetry++; mHandler.postDelayed(getNewRunnable(), currentRetry * 10000); } } } static void initUserState(Context context) { appContext = context; if (currentUserState != null) return; currentUserState = new OneSignalStateSynchronizer().new UserState("CURRENT_STATE", true); toSyncUserState = new OneSignalStateSynchronizer().new UserState("TOSYNC_STATE", true); } static UserState getNewUserState() { return new OneSignalStateSynchronizer().new UserState("nonPersist", false); } static void syncUserState(boolean fromSyncService) { boolean isSessionCall = !onSessionDone && postSessionCalled && !waitingForSessionResponse; final JSONObject jsonBody = currentUserState.generateJsonDiff(toSyncUserState, isSessionCall); final JSONObject dependDiff = generateJsonDiff(currentUserState.dependValues, toSyncUserState.dependValues, null, null); if (jsonBody == null) { currentUserState.persistStateAfterSync(dependDiff, null); return; } final String userId = OneSignal.getUserId(); toSyncUserState.persistState(); if (onSessionDone || fromSyncService) { OneSignalRestClient.putSync("players/" + userId, jsonBody, new OneSignalRestClient.ResponseHandler() { @Override void onFailure(int statusCode, String response, Throwable throwable) { OneSignal.Log(OneSignal.LOG_LEVEL.WARN, "Failed last request. statusCode: " + statusCode + "\nresponse: " + response); if (response400WithErrorsContaining(statusCode, response, "No user with this id found")) { resetCurrentState(); postNewSyncUserState(); } else getNetworkHandlerThread(NetworkHandlerThread.NETWORK_HANDLER_USERSTATE).doRetry(); } @Override void onSuccess(String response) { currentUserState.persistStateAfterSync(dependDiff, jsonBody); } }); } else if (postSessionCalled) { String urlStr; if (userId == null) urlStr = "players"; else urlStr = "players/" + userId + "/on_session"; waitingForSessionResponse = true; OneSignalRestClient.postSync(urlStr, jsonBody, new OneSignalRestClient.ResponseHandler() { @Override void onFailure(int statusCode, String response, Throwable throwable) { waitingForSessionResponse = false; OneSignal.Log(OneSignal.LOG_LEVEL.WARN, "Failed last request. statusCode: " + statusCode + "\nresponse: " + response); if (response400WithErrorsContaining(statusCode, response, "not a valid device_type")) { resetCurrentState(); postNewSyncUserState(); } else getNetworkHandlerThread(NetworkHandlerThread.NETWORK_HANDLER_USERSTATE).doRetry(); } @Override void onSuccess(String response) { onSessionDone = true; waitingForSessionResponse = false; currentUserState.persistStateAfterSync(dependDiff, jsonBody); try { JSONObject jsonResponse = new JSONObject(response); if (jsonResponse.has("id")) { String userId = jsonResponse.getString("id"); OneSignal.saveUserId(userId); OneSignal.fireIdsAvailableCallback(); OneSignal.Log(OneSignal.LOG_LEVEL.INFO, "Device registered, UserId = " + userId); } else OneSignal.Log(OneSignal.LOG_LEVEL.INFO, "session sent, UserId = " + OneSignal.getUserId()); } catch (Throwable t) { OneSignal.Log(OneSignal.LOG_LEVEL.ERROR, "ERROR parsing on_session or create JSON Response.", t); } } }); } } private static boolean response400WithErrorsContaining(int statusCode, String response, String contains) { if (statusCode == 400 && response != null) { try { JSONObject responseJson = new JSONObject(response); return responseJson.has("errors") && responseJson.optString("errors").contains(contains); } catch (Throwable t) { t.printStackTrace(); } } return false; } private static NetworkHandlerThread getNetworkHandlerThread(Integer type) { if (!networkHandlerThreads.containsKey(type)) networkHandlerThreads.put(type, new NetworkHandlerThread(type)); return networkHandlerThreads.get(type); } private static UserState getUserStateForModification() { if (toSyncUserState == null) toSyncUserState = currentUserState.deepClone("TOSYNC_STATE"); postNewSyncUserState(); return toSyncUserState; } private static void postNewSyncUserState() { getNetworkHandlerThread(NetworkHandlerThread.NETWORK_HANDLER_USERSTATE).runNewJob(); } static void postSession(UserState postSession) { JSONObject toSync = getUserStateForModification().syncValues; generateJsonDiff(toSync, postSession.syncValues, toSync, null); JSONObject dependValues = getUserStateForModification().dependValues; generateJsonDiff(dependValues, postSession.dependValues, dependValues, null); postSessionCalled = true; } static void sendTags(JSONObject newTags) { JSONObject userStateTags = getUserStateForModification().syncValues; try { generateJsonDiff(userStateTags, new JSONObject().put("tags", newTags), userStateTags, null); } catch (JSONException e) { e.printStackTrace(); } } static void setSubscription(boolean enable) { try { getUserStateForModification().dependValues.put("userSubscribePref", enable); } catch (JSONException e) { e.printStackTrace(); } } static void updateIdentifier(String identifier) { UserState userState = getUserStateForModification(); try { userState.syncValues.put("identifier", identifier); } catch (JSONException e) { e.printStackTrace(); } } static void updateLocation(Double lat, Double log, Float accuracy, Integer type) { UserState userState = getUserStateForModification(); try { userState.syncValues.put("lat", lat); userState.syncValues.put("long", log); userState.syncValues.put("loc_acc", accuracy); userState.syncValues.put("loc_type", type); } catch (JSONException e) { e.printStackTrace(); } } static boolean getSubscribed() { return toSyncUserState.getNotificationTypes() > 0; } static String getRegistrationId() { return toSyncUserState.syncValues.optString("identifier", null); } static JSONObject getTags() { return getTagsWithoutDeletedKeys(toSyncUserState.syncValues); } static void resetCurrentState() { onSessionDone = false; OneSignal.saveUserId(null); currentUserState.syncValues = new JSONObject(); currentUserState.persistState(); } }