com.onesignal.OneSignal.java Source code

Java tutorial

Introduction

Here is the source code for com.onesignal.OneSignal.java

Source

/**
 * 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 java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.UUID;

import org.json.*;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.util.Log;

import com.stericson.RootTools.internal.RootToolsInternalMethods;

import com.onesignal.OneSignalDbContract.NotificationTable;

public class OneSignal {

    public enum LOG_LEVEL {
        NONE, FATAL, ERROR, WARN, INFO, DEBUG, VERBOSE
    }

    static final long MIN_ON_FOCUS_TIME = 60;

    public interface NotificationOpenedHandler {
        /**
         * Callback to implement in your app to handle when a notification is
         * opened from the Android status bar or a new one comes in while the app is running.
         *
         * @param message
         *           The message string the user seen/should see in the Android status bar.
         * @param additionalData
         *           The additionalData key value pair section you entered in on onesignal.com.
         * @param isActive
         *           Was the app in the foreground when the notification was received.
         */
        void notificationOpened(String message, JSONObject additionalData, boolean isActive);
    }

    public interface IdsAvailableHandler {
        void idsAvailable(String userId, String registrationId);
    }

    public interface GetTagsHandler {
        void tagsAvailable(JSONObject tags);
    }

    public interface PostNotificationResponseHandler {
        void onSuccess(JSONObject response);

        void onFailure(JSONObject response);
    }

    public static class Builder {
        Context mContext;
        NotificationOpenedHandler mNotificationOpenedHandler;
        boolean mPromptLocation;

        private Builder() {
        }

        private Builder(Context context) {
            mContext = context;
        }

        public Builder setNotificationOpenedHandler(NotificationOpenedHandler handler) {
            mNotificationOpenedHandler = handler;
            return this;
        }

        public Builder setAutoPromptLocation(boolean enable) {
            mPromptLocation = enable;
            return this;
        }

        public void init() {
            OneSignal.init(this);
        }
    }

    /**
     * Tag used on log messages.
     */
    static final String TAG = "OneSignal";

    static String appId, mGoogleProjectNumber;
    static Context appContext;

    private static boolean startedRegistration;

    private static LOG_LEVEL visualLogLevel = LOG_LEVEL.NONE;
    private static LOG_LEVEL logCatLevel = LOG_LEVEL.WARN;

    private static String userId = null;
    private static int subscribableStatus = 1;

    private static NotificationOpenedHandler notificationOpenedHandler;

    static boolean initDone;
    private static boolean foreground;

    private static IdsAvailableHandler idsAvailableHandler;

    private static long lastTrackedTime = 1, unSentActiveTime = -1;

    private static TrackGooglePurchase trackGooglePurchase;
    private static TrackAmazonPurchase trackAmazonPurchase;

    public static final String VERSION = "020102";

    private static AdvertisingIdentifierProvider mainAdIdProvider = new AdvertisingIdProviderGPS();

    private static int deviceType;
    public static String sdkType = "native";

    private static OSUtils osUtils;

    private static boolean ranSessionInitThread;

    private static String lastRegistrationId;
    private static boolean registerForPushFired, locationFired;
    private static Double lastLocLat, lastLocLong;
    private static Float lastLocAcc;
    private static Integer lastLocType;
    private static OneSignal.Builder mInitBuilder;

    static Collection<JSONArray> unprocessedOpenedNotifis = new ArrayList<JSONArray>();

    public static OneSignal.Builder startInit(Context context) {
        return new OneSignal.Builder(context);
    }

    private static void init(OneSignal.Builder inBuilder) {
        mInitBuilder = inBuilder;

        Context context = mInitBuilder.mContext;
        mInitBuilder.mContext = null; // Clear to prevent leaks.

        try {
            ApplicationInfo ai = context.getPackageManager().getApplicationInfo(context.getPackageName(),
                    PackageManager.GET_META_DATA);
            Bundle bundle = ai.metaData;
            OneSignal.init(context, bundle.getString("onesignal_google_project_number").substring(4),
                    bundle.getString("onesignal_app_id"), mInitBuilder.mNotificationOpenedHandler);
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }

    public static void init(Context context, String googleProjectNumber, String oneSignalAppId) {
        init(context, googleProjectNumber, oneSignalAppId, null);
    }

    public static void init(Context context, String googleProjectNumber, String oneSignalAppId,
            NotificationOpenedHandler inNotificationOpenedHandler) {
        if (mInitBuilder == null)
            mInitBuilder = new OneSignal.Builder();

        osUtils = new OSUtils();

        deviceType = osUtils.getDeviceType();

        // START: Init validation
        try {
            UUID.fromString(oneSignalAppId);
        } catch (Throwable t) {
            Log(LOG_LEVEL.FATAL,
                    "OneSignal AppId format is invalid.\nExample: 'b2f7f966-d8cc-11e4-bed1-df8f05be55ba'\n", t);
            return;
        }

        if ("b2f7f966-d8cc-11e4-bed1-df8f05be55ba".equals(oneSignalAppId)
                || "5eb5a37e-b458-11e3-ac11-000c2940e62c".equals(oneSignalAppId))
            Log(LOG_LEVEL.WARN,
                    "OneSignal Example AppID detected, please update to your app's id found on OneSignal.com");

        if (deviceType == 1) {
            try {
                Double.parseDouble(googleProjectNumber);
                if (googleProjectNumber.length() < 8 || googleProjectNumber.length() > 16)
                    throw new IllegalArgumentException(
                            "Google Project number (Sender_ID) should be a 10 to 14 digit number in length.");
            } catch (Throwable t) {
                Log(LOG_LEVEL.FATAL,
                        "Google Project number (Sender_ID) format is invalid. Please use the 10 to 14 digit number found in the Google Developer Console for your project.\nExample: '703322744261'\n",
                        t);
                subscribableStatus = -6;
            }

            try {
                Class.forName("com.google.android.gms.gcm.GoogleCloudMessaging");
            } catch (ClassNotFoundException e) {
                Log(LOG_LEVEL.FATAL,
                        "The GCM Google Play services client library was not found. Please make sure to include it in your project.",
                        e);
                subscribableStatus = -4;
            }
        }

        mGoogleProjectNumber = googleProjectNumber;

        try {
            Class.forName("android.support.v4.view.MenuCompat");
            try {
                Class.forName("android.support.v4.content.WakefulBroadcastReceiver");
                Class.forName("android.support.v4.app.NotificationManagerCompat");
            } catch (ClassNotFoundException e) {
                Log(LOG_LEVEL.FATAL,
                        "The included Android Support Library v4 is to old or incomplete. Please update your project's android-support-v4.jar to the latest revision.",
                        e);
                subscribableStatus = -5;
            }
        } catch (ClassNotFoundException e) {
            Log(LOG_LEVEL.FATAL,
                    "Could not find the Android Support Library v4. Please make sure android-support-v4.jar has been correctly added to your project.",
                    e);
            subscribableStatus = -3;
        }

        if (initDone) {
            if (context != null)
                appContext = context.getApplicationContext();

            if (inNotificationOpenedHandler != null)
                notificationOpenedHandler = inNotificationOpenedHandler;

            if (notificationOpenedHandler != null)
                fireCallbackForOpenedNotifications();

            return;
        }

        // END: Init validation
        boolean contextIsActivity = (context instanceof Activity);

        foreground = contextIsActivity;
        appId = oneSignalAppId;
        appContext = context.getApplicationContext();
        if (contextIsActivity)
            ActivityLifecycleHandler.curActivity = (Activity) context;
        else
            ActivityLifecycleHandler.nextResumeIsFirstActivity = true;
        notificationOpenedHandler = inNotificationOpenedHandler;
        lastTrackedTime = SystemClock.elapsedRealtime();

        OneSignalStateSynchronizer.initUserState(appContext);
        appContext.startService(new Intent(appContext, SyncService.class));

        if (android.os.Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB_MR2)
            ((Application) appContext).registerActivityLifecycleCallbacks(new ActivityLifecycleListener());
        else
            ActivityLifecycleListenerCompat.startListener();

        try {
            Class.forName("com.amazon.device.iap.PurchasingListener");
            trackAmazonPurchase = new TrackAmazonPurchase(appContext);
        } catch (ClassNotFoundException e) {
        }

        // Re-register user if the app id changed, this might happen when a dev is testing.
        String oldAppId = getSavedAppId();
        if (oldAppId != null) {
            if (!oldAppId.equals(appId)) {
                Log(LOG_LEVEL.DEBUG, "APP ID changed, clearing user id as it is no longer valid.");
                SaveAppId(appId);
                OneSignalStateSynchronizer.resetCurrentState();
            }
        } else
            SaveAppId(appId);

        if (foreground || getUserId() == null)
            startRegistrationOrOnSession();

        if (notificationOpenedHandler != null)
            fireCallbackForOpenedNotifications();

        if (TrackGooglePurchase.CanTrack(appContext))
            trackGooglePurchase = new TrackGooglePurchase(appContext);

        initDone = true;
    }

    private static void startRegistrationOrOnSession() {
        if (startedRegistration)
            return;

        startedRegistration = true;

        PushRegistrator pushRegistrator;
        if (deviceType == 2)
            pushRegistrator = new PushRegistratorADM();
        else
            pushRegistrator = new PushRegistratorGPS();

        pushRegistrator.registerForPush(appContext, mGoogleProjectNumber, new PushRegistrator.RegisteredHandler() {
            @Override
            public void complete(String id) {
                lastRegistrationId = id;
                registerForPushFired = true;
                registerUser();
            }
        });

        LocationGMS.getLocation(appContext, mInitBuilder.mPromptLocation, new LocationGMS.LocationHandler() {
            @Override
            public void complete(Double lat, Double log, Float accuracy, Integer type) {
                lastLocLat = lat;
                lastLocLong = log;
                lastLocAcc = accuracy;
                lastLocType = type;
                locationFired = true;
                registerUser();
            }
        });
    }

    private static void fireCallbackForOpenedNotifications() {
        for (JSONArray dataArray : unprocessedOpenedNotifis)
            runNotificationOpenedCallback(dataArray, false);

        unprocessedOpenedNotifis.clear();
    }

    private static void updateRegistrationId() {
        String orgRegId = OneSignalStateSynchronizer.getRegistrationId();
        if (lastRegistrationId != null && !lastRegistrationId.equals(orgRegId)) {
            OneSignalStateSynchronizer.updateIdentifier(lastRegistrationId);
            fireIdsAvailableCallback();
        }
    }

    public static void setLogLevel(LOG_LEVEL inLogCatLevel, LOG_LEVEL inVisualLogLevel) {
        logCatLevel = inLogCatLevel;
        visualLogLevel = inVisualLogLevel;
    }

    public static void setLogLevel(int inLogCatLevel, int inVisualLogLevel) {
        setLogLevel(getLogLevel(inLogCatLevel), getLogLevel(inVisualLogLevel));
    }

    private static OneSignal.LOG_LEVEL getLogLevel(int level) {
        switch (level) {
        case 0:
            return OneSignal.LOG_LEVEL.NONE;
        case 1:
            return OneSignal.LOG_LEVEL.FATAL;
        case 2:
            return OneSignal.LOG_LEVEL.ERROR;
        case 3:
            return OneSignal.LOG_LEVEL.WARN;
        case 4:
            return OneSignal.LOG_LEVEL.INFO;
        case 5:
            return OneSignal.LOG_LEVEL.DEBUG;
        case 6:
            return OneSignal.LOG_LEVEL.VERBOSE;
        }

        if (level < 0)
            return OneSignal.LOG_LEVEL.NONE;
        return OneSignal.LOG_LEVEL.VERBOSE;
    }

    private static boolean atLogLevel(LOG_LEVEL level) {
        return level.compareTo(visualLogLevel) < 1 || level.compareTo(logCatLevel) < 1;
    }

    static void Log(LOG_LEVEL level, String message) {
        Log(level, message, null);
    }

    static void Log(final LOG_LEVEL level, String message, Throwable throwable) {
        if (level.compareTo(logCatLevel) < 1) {
            if (level == LOG_LEVEL.VERBOSE)
                Log.v(TAG, message, throwable);
            else if (level == LOG_LEVEL.DEBUG)
                Log.d(TAG, message, throwable);
            else if (level == LOG_LEVEL.INFO)
                Log.i(TAG, message, throwable);
            else if (level == LOG_LEVEL.WARN)
                Log.w(TAG, message, throwable);
            else if (level == LOG_LEVEL.ERROR || level == LOG_LEVEL.FATAL)
                Log.e(TAG, message, throwable);
        }

        if (level.compareTo(visualLogLevel) < 1 && ActivityLifecycleHandler.curActivity != null) {
            try {
                String fullMessage = message + "\n";
                if (throwable != null) {
                    fullMessage += throwable.getMessage();
                    StringWriter sw = new StringWriter();
                    PrintWriter pw = new PrintWriter(sw);
                    throwable.printStackTrace(pw);
                    fullMessage += sw.toString();
                }

                final String finalFullMessage = fullMessage;
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        if (ActivityLifecycleHandler.curActivity != null)
                            new AlertDialog.Builder(ActivityLifecycleHandler.curActivity).setTitle(level.toString())
                                    .setMessage(finalFullMessage).show();
                    }
                });
            } catch (Throwable t) {
                Log.e(TAG, "Error showing logging message.", t);
            }
        }
    }

    private static void logHttpError(String errorString, int statusCode, Throwable throwable,
            String errorResponse) {
        String jsonError = "";
        if (errorResponse != null && atLogLevel(LOG_LEVEL.INFO))
            jsonError = "\n" + errorResponse + "\n";
        Log(LOG_LEVEL.WARN, "HTTP code: " + statusCode + " " + errorString + jsonError, throwable);
    }

    /**
     * Now automatically tracked, remove from your Activities.
     *
     * @deprecated Automatically tracked.
     * @Deprecated Automatically tracked.
     */
    public static void onPaused() {
        Log(LOG_LEVEL.INFO,
                "Deprecated! onPaused is now tracked automatically, please remove calls to OneSignal.onPaused() and OneSignal.onResume().");
    }

    static void onAppLostFocus(boolean onlySave) {
        foreground = false;

        if (!initDone)
            return;

        if (trackAmazonPurchase != null)
            trackAmazonPurchase.checkListener();

        if (lastTrackedTime == -1)
            return;

        long time_elapsed = (long) (((SystemClock.elapsedRealtime() - lastTrackedTime) / 1000d) + 0.5d);
        lastTrackedTime = SystemClock.elapsedRealtime();
        if (time_elapsed < 0 || time_elapsed > 604800)
            return;
        if (appContext == null) {
            Log(LOG_LEVEL.ERROR, "Android Context not found, please call OneSignal.init when your app starts.");
            return;
        }

        long unSentActiveTime = GetUnsentActiveTime();
        long totalTimeActive = unSentActiveTime + time_elapsed;

        if (onlySave || totalTimeActive < MIN_ON_FOCUS_TIME || getUserId() == null) {
            SaveUnsentActiveTime(totalTimeActive);
            return;
        }

        sendOnFocus(totalTimeActive, true);
    }

    static void sendOnFocus(long totalTimeActive, boolean synchronous) {
        JSONObject jsonBody = new JSONObject();
        try {
            jsonBody.put("app_id", appId);
            jsonBody.put("state", "ping");
            jsonBody.put("active_time", totalTimeActive);
            addNetType(jsonBody);

            String url = "players/" + getUserId() + "/on_focus";
            OneSignalRestClient.ResponseHandler responseHandler = new OneSignalRestClient.ResponseHandler() {
                @Override
                void onFailure(int statusCode, String response, Throwable throwable) {
                    logHttpError("sending on_focus Failed", statusCode, throwable, response);
                }

                @Override
                void onSuccess(String response) {
                    SaveUnsentActiveTime(0);
                }
            };

            if (synchronous)
                OneSignalRestClient.postSync(url, jsonBody, responseHandler);
            else
                OneSignalRestClient.post(url, jsonBody, responseHandler);
        } catch (Throwable t) {
            Log(LOG_LEVEL.ERROR, "Generating on_focus:JSON Failed.", t);
        }
    }

    /**
     * Now automatically tracked, remove from your Activities.
     *
     * @deprecated Automatically tracked.
     * @Deprecated Automatically tracked.
     */
    public static void onResumed() {
        Log(LOG_LEVEL.INFO,
                "Deprecated! onResumed is now tracked automatically, please remove calls to OneSignal.onPaused() and OneSignal.onResume().");
    }

    static void onAppFocus() {
        foreground = true;
        lastTrackedTime = SystemClock.elapsedRealtime();

        startRegistrationOrOnSession();

        if (trackGooglePurchase != null)
            trackGooglePurchase.trackIAP();
    }

    static boolean isForeground() {
        return foreground;
    }

    private static void addNetType(JSONObject jsonObj) {
        try {
            jsonObj.put("net_type", osUtils.getNetType());
        } catch (Throwable t) {
        }
    }

    private static int getTimeZoneOffset() {
        TimeZone timezone = Calendar.getInstance().getTimeZone();
        int offset = timezone.getRawOffset();

        if (timezone.inDaylightTime(new Date()))
            offset = offset + timezone.getDSTSavings();

        return offset / 1000;
    }

    private static void registerUser() {
        Log(LOG_LEVEL.DEBUG,
                "registerUser: registerForPushFired:" + registerForPushFired + ", locationFired: " + locationFired);

        if (!registerForPushFired || !locationFired)
            return;

        if (ranSessionInitThread) {
            updateRegistrationId();
            return;
        }

        ranSessionInitThread = true;

        new Thread(new Runnable() {
            public void run() {
                OneSignalStateSynchronizer.UserState userState = OneSignalStateSynchronizer.getNewUserState();

                String packageName = appContext.getPackageName();
                PackageManager packageManager = appContext.getPackageManager();

                userState.set("app_id", appId);
                userState.set("identifier", lastRegistrationId);

                String adId = mainAdIdProvider.getIdentifier(appContext);
                // "... must use the advertising ID (when available on a device) in lieu of any other device identifiers ..."
                // https://play.google.com/about/developer-content-policy.html
                if (adId == null)
                    adId = new AdvertisingIdProviderFallback().getIdentifier(appContext);
                userState.set("ad_id", adId);
                userState.set("device_os", Build.VERSION.RELEASE);
                userState.set("timezone", getTimeZoneOffset());
                userState.set("language", Locale.getDefault().getLanguage());
                userState.set("sdk", VERSION);
                userState.set("sdk_type", sdkType);
                userState.set("android_package", packageName);
                userState.set("device_model", Build.MODEL);
                userState.set("device_type", deviceType);
                userState.setState("subscribableStatus", subscribableStatus);

                try {
                    userState.set("game_version", packageManager.getPackageInfo(packageName, 0).versionCode);
                } catch (PackageManager.NameNotFoundException e) {
                }

                List<PackageInfo> packList = packageManager.getInstalledPackages(0);
                int count = -1;
                for (int i = 0; i < packList.size(); i++)
                    count += ((packList.get(i).applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0) ? 1 : 0;
                userState.set("pkgc", count);
                userState.set("net_type", osUtils.getNetType());
                userState.set("carrier", osUtils.getCarrierName());
                userState.set("rooted", RootToolsInternalMethods.isRooted());

                userState.set("lat", lastLocLat);
                userState.set("long", lastLocLong);
                userState.set("loc_acc", lastLocAcc);
                userState.set("loc_type", lastLocType);

                OneSignalStateSynchronizer.postSession(userState);
            }
        }).start();
    }

    public static void sendTag(String key, String value) {
        try {
            sendTags(new JSONObject().put(key, value));
        } catch (JSONException t) {
            t.printStackTrace();
        }
    }

    public static void sendTags(String jsonString) {
        try {
            sendTags(new JSONObject(jsonString));
        } catch (JSONException t) {
            Log(LOG_LEVEL.ERROR, "Generating JSONObject for sendTags failed!", t);
        }
    }

    public static void sendTags(JSONObject keyValues) {
        if (appContext == null) {
            Log(LOG_LEVEL.ERROR,
                    "You must initialize OneSignal before modifying tags! Omitting this tag operation.");
            return;
        }

        if (keyValues == null)
            return;

        JSONObject existingKeys = OneSignalStateSynchronizer.getTags();

        JSONObject toSend = new JSONObject();

        Iterator<String> keys = keyValues.keys();
        String key;
        Object value;

        while (keys.hasNext()) {
            key = keys.next();
            try {
                value = keyValues.get(key);
                if (value instanceof JSONArray || value instanceof JSONObject)
                    Log(LOG_LEVEL.ERROR, "Omitting key '" + key + "'! sendTags DO NOT supported nested values!");
                else if (keyValues.isNull(key) || "".equals(value)) {
                    if (existingKeys.has(key))
                        toSend.put(key, "");
                } else
                    toSend.put(key, value.toString());
            } catch (Throwable t) {
            }
        }

        if (!toSend.toString().equals("{}"))
            OneSignalStateSynchronizer.sendTags(toSend);
    }

    public static void postNotification(String json, final PostNotificationResponseHandler handler) {
        try {
            postNotification(new JSONObject(json), handler);
        } catch (JSONException e) {
            Log(LOG_LEVEL.ERROR, "Invalid postNotification JSON format: " + json);
        }
    }

    public static void postNotification(JSONObject json, final PostNotificationResponseHandler handler) {
        try {
            json.put("app_id", getSavedAppId());

            OneSignalRestClient.post("notifications/", json, new OneSignalRestClient.ResponseHandler() {
                @Override
                public void onSuccess(String response) {
                    Log(LOG_LEVEL.DEBUG,
                            "HTTP create notification success: " + (response != null ? response : "null"));
                    if (handler != null) {
                        try {
                            JSONObject jsonObject = new JSONObject(response);
                            if (jsonObject.has("errors"))
                                handler.onFailure(jsonObject);
                            else
                                handler.onSuccess(new JSONObject(response));
                        } catch (Throwable t) {
                            t.printStackTrace();
                        }
                    }
                }

                @Override
                void onFailure(int statusCode, String response, Throwable throwable) {
                    logHttpError("create notification failed", statusCode, throwable, response);

                    if (statusCode == 0)
                        response = "{'error': 'HTTP no response error'}";

                    if (handler != null) {
                        try {
                            handler.onFailure(new JSONObject(response));
                        } catch (Throwable t) {
                            handler.onFailure(null);
                        }
                    }
                }
            });
        } catch (JSONException e) {
            Log(LOG_LEVEL.ERROR, "HTTP create notification json exception!", e);
            if (handler != null) {
                try {
                    handler.onFailure(new JSONObject("{'error': 'HTTP create notification json exception!'}"));
                } catch (JSONException e1) {
                    e1.printStackTrace();
                }
            }
        }
    }

    public static void getTags(final GetTagsHandler getTagsHandler) {
        if (appContext == null) {
            Log(LOG_LEVEL.ERROR, "You must initialize OneSignal before getting tags! Omitting this tag operation.");
            return;
        }

        JSONObject tags = OneSignalStateSynchronizer.getTags();
        if (tags == null || tags.toString().equals("{}"))
            getTagsHandler.tagsAvailable(null);
        else
            getTagsHandler.tagsAvailable(OneSignalStateSynchronizer.getTags());
    }

    public static void deleteTag(String key) {
        Collection<String> tempList = new ArrayList<String>(1);
        tempList.add(key);
        deleteTags(tempList);
    }

    public static void deleteTags(Collection<String> keys) {
        try {
            JSONObject jsonTags = new JSONObject();
            for (String key : keys)
                jsonTags.put(key, "");

            sendTags(jsonTags);
        } catch (Throwable t) {
            Log(LOG_LEVEL.ERROR, "Failed to generate JSON for deleteTags.", t);
        }
    }

    public static void deleteTags(String jsonArrayString) {
        try {
            JSONObject jsonTags = new JSONObject();
            JSONArray jsonArray = new JSONArray(jsonArrayString);

            for (int i = 0; i < jsonArray.length(); i++)
                jsonTags.put(jsonArray.getString(i), "");

            sendTags(jsonTags);
        } catch (Throwable t) {
            Log(LOG_LEVEL.ERROR, "Failed to generate JSON for deleteTags.", t);
        }
    }

    public static void idsAvailable(IdsAvailableHandler inIdsAvailableHandler) {
        idsAvailableHandler = inIdsAvailableHandler;

        if (getUserId() != null)
            internalFireIdsAvailableCallback();
    }

    static void fireIdsAvailableCallback() {
        if (idsAvailableHandler != null) {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    internalFireIdsAvailableCallback();
                }
            });
        }
    }

    private static void internalFireIdsAvailableCallback() {
        if (idsAvailableHandler == null)
            return;

        String regId = OneSignalStateSynchronizer.getRegistrationId();
        if (!OneSignalStateSynchronizer.getSubscribed())
            regId = null;

        String userId = getUserId();
        if (userId == null)
            return;

        idsAvailableHandler.idsAvailable(userId, regId);

        if (regId != null)
            idsAvailableHandler = null;
    }

    static void sendPurchases(JSONArray purchases, boolean newAsExisting,
            OneSignalRestClient.ResponseHandler responseHandler) {
        if (getUserId() == null)
            return;

        try {
            JSONObject jsonBody = new JSONObject();
            jsonBody.put("app_id", appId);
            if (newAsExisting)
                jsonBody.put("existing", true);
            jsonBody.put("purchases", purchases);

            OneSignalRestClient.post("players/" + getUserId() + "/on_purchase", jsonBody, responseHandler);
        } catch (Throwable t) {
            Log(LOG_LEVEL.ERROR, "Failed to generate JSON for sendPurchases.", t);
        }
    }

    private static boolean openURLFromNotification(Context context, JSONArray dataArray) {
        int jsonArraySize = dataArray.length();

        boolean urlOpened = false;

        for (int i = 0; i < jsonArraySize; i++) {
            try {
                JSONObject data = dataArray.getJSONObject(i);
                if (!data.has("custom"))
                    continue;

                JSONObject customJSON = new JSONObject(data.getString("custom"));

                if (customJSON.has("u")) {
                    String url = customJSON.getString("u");
                    if (!url.contains("://"))
                        url = "http://" + url;

                    Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
                    intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET
                            | Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
                    context.startActivity(intent);
                    urlOpened = true;
                }
            } catch (Throwable t) {
                Log(LOG_LEVEL.ERROR,
                        "Error parsing JSON item " + i + "/" + jsonArraySize + " for launching a web URL.", t);
            }
        }

        return urlOpened;
    }

    private static void runNotificationOpenedCallback(final JSONArray dataArray, final boolean isActive) {
        if (notificationOpenedHandler == null) {
            unprocessedOpenedNotifis.add(dataArray);
            return;
        }

        int jsonArraySize = dataArray.length();

        JSONObject completeAdditionalData = null;
        String firstMessage = null;

        for (int i = 0; i < jsonArraySize; i++) {
            try {
                JSONObject data = dataArray.getJSONObject(i);

                JSONObject additionalDataJSON = null;

                // Summary notifications will not have custom set
                if (data.has("custom")) {
                    JSONObject customJSON = new JSONObject(data.getString("custom"));
                    additionalDataJSON = new JSONObject();

                    if (customJSON.has("a"))
                        additionalDataJSON = customJSON.getJSONObject("a");

                    if (data.has("title"))
                        additionalDataJSON.put("title", data.getString("title"));

                    if (customJSON.has("u"))
                        additionalDataJSON.put("launchURL", customJSON.getString("u"));

                    if (data.has("sound"))
                        additionalDataJSON.put("sound", data.getString("sound"));

                    if (data.has("sicon"))
                        additionalDataJSON.put("smallIcon", data.getString("sicon"));

                    if (data.has("licon"))
                        additionalDataJSON.put("largeIcon", data.getString("licon"));

                    if (data.has("bicon"))
                        additionalDataJSON.put("bigPicture", data.getString("bicon"));

                    if (additionalDataJSON.equals(new JSONObject()))
                        additionalDataJSON = null;
                }

                if (firstMessage == null) {
                    completeAdditionalData = additionalDataJSON;
                    firstMessage = data.getString("alert");
                } else {
                    if (completeAdditionalData == null)
                        completeAdditionalData = new JSONObject();
                    if (!completeAdditionalData.has("stacked_notifications"))
                        completeAdditionalData.put("stacked_notifications", new JSONArray());

                    additionalDataJSON.put("message", data.getString("alert"));

                    completeAdditionalData.getJSONArray("stacked_notifications").put(additionalDataJSON);
                }
            } catch (Throwable t) {
                Log(LOG_LEVEL.ERROR, "Error parsing JSON item " + i + "/" + jsonArraySize + " for callback.", t);
            }
        }

        fireNotificationOpenedHandler(firstMessage, completeAdditionalData, isActive);
    }

    private static void fireNotificationOpenedHandler(final String message, final JSONObject additionalDataJSON,
            final boolean isActive) {
        if (Looper.getMainLooper().getThread() == Thread.currentThread()) // isUIThread
            notificationOpenedHandler.notificationOpened(message, additionalDataJSON, isActive);
        else {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    notificationOpenedHandler.notificationOpened(message, additionalDataJSON, isActive);
                }
            });
        }
    }

    // Called when receiving GCM message when app is open, in focus, and is not set to display when active.
    static void handleNotificationOpened(JSONArray data) {
        sendNotificationOpened(appContext, data);
        runNotificationOpenedCallback(data, true);
    }

    // Called when opening a notification when the app is suspended in the background, from alert type notification, or when it is dead
    public static void handleNotificationOpened(Context inContext, JSONArray data, boolean fromAlert) {
        sendNotificationOpened(inContext, data);

        boolean urlOpened = openURLFromNotification(inContext, data);

        runNotificationOpenedCallback(data, false);

        // Open/Resume app when opening the notification.
        if (!fromAlert && !urlOpened)
            fireIntentFromNotificationOpen(inContext, data);
    }

    private static void fireIntentFromNotificationOpen(Context inContext, JSONArray data) {
        PackageManager packageManager = inContext.getPackageManager();

        boolean isCustom = false;

        Intent intent = new Intent().setAction("com.onesignal.NotificationOpened.RECEIVE")
                .setPackage(inContext.getPackageName());

        List<ResolveInfo> resolveInfo = packageManager.queryBroadcastReceivers(intent,
                PackageManager.GET_INTENT_FILTERS);
        if (resolveInfo.size() > 0) {
            intent.putExtra("onesignal_data", data.toString());
            inContext.sendBroadcast(intent);
            isCustom = true;
        }

        // Calling startActivity() from outside of an Activity  context requires the FLAG_ACTIVITY_NEW_TASK flag.
        resolveInfo = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
        if (resolveInfo.size() > 0) {
            if (!isCustom)
                intent.putExtra("onesignal_data", data.toString());
            isCustom = true;
            intent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
            inContext.startActivity(intent);
        }

        if (!isCustom) {
            try {
                ApplicationInfo ai = inContext.getPackageManager().getApplicationInfo(inContext.getPackageName(),
                        PackageManager.GET_META_DATA);
                Bundle bundle = ai.metaData;
                String defaultStr = bundle.getString("com.onesignal.NotificationOpened.DEFAULT");
                isCustom = "DISABLE".equals(defaultStr);
            } catch (Throwable t) {
                Log(LOG_LEVEL.ERROR, "", t);
            }
        }

        if (!isCustom) {
            Intent launchIntent = inContext.getPackageManager()
                    .getLaunchIntentForPackage(inContext.getPackageName());

            if (launchIntent != null) {
                launchIntent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
                inContext.startActivity(launchIntent);
            }
        }
    }

    private static void sendNotificationOpened(Context inContext, JSONArray dataArray) {
        for (int i = 0; i < dataArray.length(); i++) {
            try {
                JSONObject data = dataArray.getJSONObject(i);

                // Summary notifications do not always have a custom field.
                if (!data.has("custom"))
                    continue;

                JSONObject customJson = new JSONObject(data.getString("custom"));

                // ... they also never have a OneSignal notification id.
                if (!customJson.has("i"))
                    continue;

                String notificationId = customJson.getString("i");

                JSONObject jsonBody = new JSONObject();
                jsonBody.put("app_id", getSavedAppId(inContext));
                jsonBody.put("player_id", getSavedUserId(inContext));
                jsonBody.put("opened", true);

                OneSignalRestClient.put("notifications/" + notificationId, jsonBody,
                        new OneSignalRestClient.ResponseHandler() {
                            @Override
                            void onFailure(int statusCode, String response, Throwable throwable) {
                                logHttpError("sending Notification Opened Failed", statusCode, throwable, response);
                            }
                        });
            } catch (Throwable t) { // JSONException and UnsupportedEncodingException
                Log(LOG_LEVEL.ERROR, "Failed to generate JSON to send notification opened.", t);
            }
        }
    }

    private static void SaveAppId(String appId) {
        if (appContext == null)
            return;
        final SharedPreferences prefs = getGcmPreferences(appContext);
        SharedPreferences.Editor editor = prefs.edit();
        editor.putString("GT_APP_ID", appId);
        editor.commit();
    }

    static String getSavedAppId() {
        return getSavedAppId(appContext);
    }

    private static String getSavedAppId(Context inContext) {
        if (inContext == null)
            return "";

        final SharedPreferences prefs = getGcmPreferences(inContext);
        return prefs.getString("GT_APP_ID", null);
    }

    private static String getSavedUserId(Context inContext) {
        if (inContext == null)
            return "";
        final SharedPreferences prefs = getGcmPreferences(inContext);
        return prefs.getString("GT_PLAYER_ID", null);
    }

    static String getUserId() {
        if (userId == null && appContext != null) {
            final SharedPreferences prefs = getGcmPreferences(appContext);
            userId = prefs.getString("GT_PLAYER_ID", null);
        }
        return userId;
    }

    static void saveUserId(String inUserId) {
        userId = inUserId;
        if (appContext == null)
            return;
        final SharedPreferences prefs = getGcmPreferences(appContext);
        SharedPreferences.Editor editor = prefs.edit();
        editor.putString("GT_PLAYER_ID", userId);
        editor.commit();
    }

    // If true(default) - Device will always vibrate unless the device is in silent mode.
    // If false - Device will only vibrate when the device is set on it's vibrate only mode.
    public static void enableVibrate(boolean enable) {
        if (appContext == null)
            return;
        final SharedPreferences prefs = getGcmPreferences(appContext);
        SharedPreferences.Editor editor = prefs.edit();
        editor.putBoolean("GT_VIBRATE_ENABLED", enable);
        editor.commit();
    }

    static boolean getVibrate(Context context) {
        final SharedPreferences prefs = getGcmPreferences(context);
        return prefs.getBoolean("GT_VIBRATE_ENABLED", true);
    }

    // If true(default) - Sound plays when receiving notification. Vibrates when device is on vibrate only mode.
    // If false - Only vibrates unless EnableVibrate(false) was set.
    public static void enableSound(boolean enable) {
        if (appContext == null)
            return;
        final SharedPreferences prefs = getGcmPreferences(appContext);
        SharedPreferences.Editor editor = prefs.edit();
        editor.putBoolean("GT_SOUND_ENABLED", enable);
        editor.commit();
    }

    static boolean getSoundEnabled(Context context) {
        final SharedPreferences prefs = getGcmPreferences(context);
        return prefs.getBoolean("GT_SOUND_ENABLED", true);
    }

    public static void enableNotificationsWhenActive(boolean enable) {
        if (appContext == null)
            return;
        final SharedPreferences prefs = getGcmPreferences(appContext);
        SharedPreferences.Editor editor = prefs.edit();
        editor.putBoolean("ONESIGNAL_ALWAYS_SHOW_NOTIF", enable);
        editor.commit();
    }

    static boolean getNotificationsWhenActiveEnabled(Context context) {
        final SharedPreferences prefs = getGcmPreferences(context);
        return prefs.getBoolean("ONESIGNAL_ALWAYS_SHOW_NOTIF", false);
    }

    public static void enableInAppAlertNotification(boolean enable) {
        if (appContext == null)
            return;
        final SharedPreferences prefs = getGcmPreferences(appContext);
        SharedPreferences.Editor editor = prefs.edit();
        editor.putBoolean("ONESIGNAL_INAPP_ALERT", enable);
        editor.commit();
    }

    static boolean getInAppAlertNotificationEnabled(Context context) {
        final SharedPreferences prefs = getGcmPreferences(context);
        return prefs.getBoolean("ONESIGNAL_INAPP_ALERT", false);
    }

    public static void setSubscription(boolean enable) {
        if (appContext == null) {
            Log(LOG_LEVEL.ERROR, "OneSignal.init has not been called. Could not set subscription.");
            return;
        }

        OneSignalStateSynchronizer.setSubscription(enable);
    }

    public static void promptLocation() {
        if (appContext == null) {
            Log(LOG_LEVEL.ERROR, "OneSignal.init has not been called. Could not prompt for location.");
            return;
        }

        LocationGMS.getLocation(appContext, true, new LocationGMS.LocationHandler() {
            @Override
            public void complete(Double lat, Double log, Float accuracy, Integer type) {
                if (lat != null && log != null)
                    OneSignalStateSynchronizer.updateLocation(lat, log, accuracy, type);
            }
        });
    }

    public static void removeNotificationOpenedHandler() {
        notificationOpenedHandler = null;
    }

    static long GetUnsentActiveTime() {
        if (unSentActiveTime == -1 && appContext != null) {
            final SharedPreferences prefs = getGcmPreferences(appContext);
            unSentActiveTime = prefs.getLong("GT_UNSENT_ACTIVE_TIME", 0);
        }

        Log(LOG_LEVEL.INFO, "GetUnsentActiveTime: " + unSentActiveTime);

        return unSentActiveTime;
    }

    private static void SaveUnsentActiveTime(long time) {
        unSentActiveTime = time;
        if (appContext == null)
            return;

        Log(LOG_LEVEL.INFO, "SaveUnsentActiveTime: " + unSentActiveTime);

        final SharedPreferences prefs = getGcmPreferences(appContext);
        SharedPreferences.Editor editor = prefs.edit();
        editor.putLong("GT_UNSENT_ACTIVE_TIME", time);
        editor.commit();
    }

    static SharedPreferences getGcmPreferences(Context context) {
        return context.getSharedPreferences(OneSignal.class.getSimpleName(), Context.MODE_PRIVATE);
    }

    static boolean isDuplicateNotification(String id, Context context) {
        if (id == null || "".equals(id))
            return false;

        OneSignalDbHelper dbHelper = new OneSignalDbHelper(context);
        SQLiteDatabase readableDb = dbHelper.getReadableDatabase();

        String[] retColumn = { NotificationTable.COLUMN_NAME_NOTIFICATION_ID };
        String[] whereArgs = { id };

        Cursor cursor = readableDb.query(NotificationTable.TABLE_NAME, retColumn,
                NotificationTable.COLUMN_NAME_NOTIFICATION_ID + " = ?", // Where String
                whereArgs, null, null, null);

        boolean exists = cursor.moveToFirst();
        cursor.close();
        readableDb.close();

        if (exists) {
            Log(LOG_LEVEL.DEBUG, "Duplicate GCM message received, skipping processing. " + id);
            return true;
        }

        return false;
    }

    static void runOnUiThread(Runnable action) {
        Handler handler = new Handler(Looper.getMainLooper());
        handler.post(action);
    }

    static boolean isValidAndNotDuplicated(Context context, Bundle bundle) {
        if (bundle.isEmpty())
            return false;

        try {
            if (bundle.containsKey("custom")) {
                JSONObject customJSON = new JSONObject(bundle.getString("custom"));

                if (customJSON.has("i"))
                    return !OneSignal.isDuplicateNotification(customJSON.getString("i"), context);
                else
                    Log(LOG_LEVEL.DEBUG, "Not a OneSignal formatted GCM message. No 'i' field in custom.");
            } else
                Log(LOG_LEVEL.DEBUG, "Not a OneSignal formatted GCM message. No 'custom' field in the bundle.");
        } catch (Throwable t) {
            Log(LOG_LEVEL.DEBUG, "Could not parse bundle for duplicate, probably not a OneSignal notification.", t);
        }

        return false;
    }
}