com.leanplum.Leanplum.java Source code

Java tutorial

Introduction

Here is the source code for com.leanplum.Leanplum.java

Source

/*
 * Copyright 2016, Leanplum, Inc. All rights reserved.
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package com.leanplum;

import android.app.Activity;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.location.Location;
import android.os.AsyncTask;
import android.support.v4.app.NotificationCompat;
import android.text.TextUtils;

import com.leanplum.ActionContext.ContextualValues;
import com.leanplum.callbacks.ActionCallback;
import com.leanplum.callbacks.RegisterDeviceCallback;
import com.leanplum.callbacks.RegisterDeviceFinishedCallback;
import com.leanplum.callbacks.StartCallback;
import com.leanplum.callbacks.VariablesChangedCallback;
import com.leanplum.internal.Constants;
import com.leanplum.internal.FileManager;
import com.leanplum.internal.JsonConverter;
import com.leanplum.internal.LeanplumInternal;
import com.leanplum.internal.LeanplumMessageMatchFilter;
import com.leanplum.internal.LeanplumUIEditorWrapper;
import com.leanplum.internal.Log;
import com.leanplum.internal.OsHandler;
import com.leanplum.internal.Registration;
import com.leanplum.internal.Request;
import com.leanplum.internal.Util;
import com.leanplum.internal.Util.DeviceIdInfo;
import com.leanplum.internal.VarCache;
import com.leanplum.messagetemplates.MessageTemplates;

import org.json.JSONArray;
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * Leanplum Android SDK.
 *
 * @author Andrew First, Ben Marten
 */
public class Leanplum {
    public static final int ACTION_KIND_MESSAGE = 1;
    public static final int ACTION_KIND_ACTION = 1 << 1;

    /**
     * Default event name to use for Purchase events.
     */
    public static final String PURCHASE_EVENT_NAME = "Purchase";

    private static final ArrayList<StartCallback> startHandlers = new ArrayList<>();
    private static final ArrayList<VariablesChangedCallback> variablesChangedHandlers = new ArrayList<>();
    private static final ArrayList<VariablesChangedCallback> noDownloadsHandlers = new ArrayList<>();
    private static final ArrayList<VariablesChangedCallback> onceNoDownloadsHandlers = new ArrayList<>();
    private static RegisterDeviceCallback registerDeviceHandler;
    private static RegisterDeviceFinishedCallback registerDeviceFinishedHandler;

    private static LeanplumDeviceIdMode deviceIdMode = LeanplumDeviceIdMode.MD5_MAC_ADDRESS;
    private static String customDeviceId;
    private static boolean userSpecifiedDeviceId;
    private static boolean initializedMessageTemplates = false;
    private static boolean locationCollectionEnabled = true;

    private static ScheduledExecutorService heartbeatExecutor;
    private static final Object heartbeatLock = new Object();

    private static Context context;

    private static Runnable pushStartCallback;

    private Leanplum() {
    }

    /**
     * Optional. Sets the API server. The API path is of the form http[s]://hostname/servletName
     *
     * @param hostName The name of the API host, such as www.leanplum.com
     * @param servletName The name of the API servlet, such as api
     * @param ssl Whether to use SSL
     */
    public static void setApiConnectionSettings(String hostName, String servletName, boolean ssl) {
        if (TextUtils.isEmpty(hostName)) {
            Log.e("setApiConnectionSettings - Empty hostname parameter provided.");
            return;
        }
        if (TextUtils.isEmpty(servletName)) {
            Log.e("setApiConnectionSettings - Empty servletName parameter provided.");
            return;
        }

        Constants.API_HOST_NAME = hostName;
        Constants.API_SERVLET = servletName;
        Constants.API_SSL = ssl;
    }

    /**
     * Optional. Sets the socket server path for Development mode. Path is of the form hostName:port
     *
     * @param hostName The host name of the socket server.
     * @param port The port to connect to.
     */
    public static void setSocketConnectionSettings(String hostName, int port) {
        if (TextUtils.isEmpty(hostName)) {
            Log.e("setSocketConnectionSettings - Empty hostName parameter provided.");
            return;
        }
        if (port < 1 || port > 65535) {
            Log.e("setSocketConnectionSettings - Invalid port parameter provided.");
            return;
        }

        Constants.SOCKET_HOST = hostName;
        Constants.SOCKET_PORT = port;
    }

    /**
     * Optional. By default, Leanplum will hash file variables to determine if they're modified and
     * need to be uploaded to the server. Use this method to override this setting.
     *
     * @param enabled Setting this to false will reduce startup latency in development mode, but it's
     * possible that Leanplum will always have the most up-to-date versions of your resources.
     * (Default: true)
     */
    public static void setFileHashingEnabledInDevelopmentMode(boolean enabled) {
        Constants.hashFilesToDetermineModifications = enabled;
    }

    /**
     * Optional. Whether to enable file uploading in development mode.
     *
     * @param enabled Whether or not files should be uploaded. (Default: true)
     */
    public static void setFileUploadingEnabledInDevelopmentMode(boolean enabled) {
        Constants.enableFileUploadingInDevelopmentMode = enabled;
    }

    /**
     * Optional. Enables verbose logging in development mode.
     */
    public static void enableVerboseLoggingInDevelopmentMode() {
        Constants.enableVerboseLoggingInDevelopmentMode = true;
    }

    /**
     * Optional. Adjusts the network timeouts. The default timeout is 10 seconds for requests, and 15
     * seconds for file downloads.
     */
    public static void setNetworkTimeout(int seconds, int downloadSeconds) {
        if (seconds < 0) {
            Log.e("setNetworkTimeout - Invalid seconds parameter provided.");
            return;
        }
        if (downloadSeconds < 0) {
            Log.e("setNetworkTimeout - Invalid downloadSeconds parameter provided.");
            return;
        }

        Constants.NETWORK_TIMEOUT_SECONDS = seconds;
        Constants.NETWORK_TIMEOUT_SECONDS_FOR_DOWNLOADS = downloadSeconds;
    }

    /**
     * Advanced: Whether new variables can be downloaded mid-session. By default, this is disabled.
     * Currently, if this is enabled, new variables can only be downloaded if a push notification is
     * sent while the app is running, and the notification's metadata hasn't be downloaded yet.
     */
    public static void setCanDownloadContentMidSessionInProductionMode(boolean value) {
        Constants.canDownloadContentMidSessionInProduction = value;
    }

    /**
     * Must call either this or {@link Leanplum#setAppIdForProductionMode} before issuing any calls to
     * the API, including start.
     *
     * @param appId Your app ID.
     * @param accessKey Your development key.
     */
    public static void setAppIdForDevelopmentMode(String appId, String accessKey) {
        if (TextUtils.isEmpty(appId)) {
            Log.e("setAppIdForDevelopmentMode - Empty appId parameter provided.");
            return;
        }
        if (TextUtils.isEmpty(accessKey)) {
            Log.e("setAppIdForDevelopmentMode - Empty accessKey parameter provided.");
            return;
        }

        Constants.isDevelopmentModeEnabled = true;
        Request.setAppId(appId, accessKey);
    }

    /**
     * Must call either this or {@link Leanplum#setAppIdForDevelopmentMode} before issuing any calls
     * to the API, including start.
     *
     * @param appId Your app ID.
     * @param accessKey Your production key.
     */
    public static void setAppIdForProductionMode(String appId, String accessKey) {
        if (TextUtils.isEmpty(appId)) {
            Log.e("setAppIdForProductionMode - Empty appId parameter provided.");
            return;
        }
        if (TextUtils.isEmpty(accessKey)) {
            Log.e("setAppIdForProductionMode - Empty accessKey parameter provided.");
            return;
        }

        Constants.isDevelopmentModeEnabled = false;
        Request.setAppId(appId, accessKey);
    }

    /**
     * Enable interface editing via Leanplum.com Visual Editor.
     */
    @Deprecated
    public static void allowInterfaceEditing() {
        if (Constants.isDevelopmentModeEnabled) {
            throw new LeanplumException("Leanplum UI Editor has moved to a separate package. "
                    + "Please remove this method call and include this line in your build.gradle: "
                    + "compile 'com.leanplum:UIEditor:+'");
        }
    }

    /**
     * Enable screen tracking.
     */
    public static void trackAllAppScreens() {
        LeanplumInternal.enableAutomaticScreenTracking();
    }

    /**
     * Whether screen tracking is enabled or not.
     *
     * @return Boolean - true if enabled
     */
    public static boolean isScreenTrackingEnabled() {
        return LeanplumInternal.getIsScreenTrackingEnabled();
    }

    /**
     * Whether interface editing is enabled or not.
     *
     * @return Boolean - true if enabled
     */
    public static boolean isInterfaceEditingEnabled() {
        return LeanplumUIEditorWrapper.isUIEditorAvailable();
    }

    /**
     * Sets the type of device ID to use. Default: {@link LeanplumDeviceIdMode#MD5_MAC_ADDRESS}
     */
    public static void setDeviceIdMode(LeanplumDeviceIdMode mode) {
        if (mode == null) {
            Log.e("setDeviceIdMode - Invalid mode parameter provided.");
            return;
        }

        deviceIdMode = mode;
        userSpecifiedDeviceId = true;
    }

    /**
     * (Advanced) Sets a custom device ID. Normally, you should use setDeviceIdMode to change the type
     * of device ID provided.
     */
    public static void setDeviceId(String deviceId) {
        if (TextUtils.isEmpty(deviceId)) {
            Log.w("setDeviceId - Empty deviceId parameter provided.");
        }

        customDeviceId = deviceId;
        userSpecifiedDeviceId = true;
    }

    /**
     * Sets the application context. This should be the first call to Leanplum.
     */
    public static void setApplicationContext(Context context) {
        if (context == null) {
            Log.w("setApplicationContext - Null context parameter provided.");
        }

        Leanplum.context = context;
    }

    /**
     * Gets the application context.
     */
    public static Context getContext() {
        if (context == null) {
            Log.e("Your application context is not set. "
                    + "You should call Leanplum.setApplicationContext(this) or "
                    + "LeanplumActivityHelper.enableLifecycleCallbacks(this) in your application's "
                    + "onCreate method, or have your application extend LeanplumApplication.");
        }
        return context;
    }

    /**
     * Called when the device needs to be registered in development mode.
     */
    @Deprecated
    public static void setRegisterDeviceHandler(RegisterDeviceCallback handler,
            RegisterDeviceFinishedCallback finishHandler) {
        if (handler == null && finishHandler == null) {
            Log.w("setRegisterDeviceHandler - Invalid handler parameter provided.");
        }

        registerDeviceHandler = handler;
        registerDeviceFinishedHandler = finishHandler;
    }

    /**
     * Syncs resources between Leanplum and the current app. You should only call this once, and
     * before {@link Leanplum#start}. syncResourcesAsync should be used instead unless file variables
     * need to be defined early
     */
    public static void syncResources() {
        if (Constants.isNoop()) {
            return;
        }
        try {
            FileManager.enableResourceSyncing(null, null, false);
        } catch (Throwable t) {
            Util.handleException(t);
        }
    }

    /**
     * Syncs resources between Leanplum and the current app. You should only call this once, and
     * before {@link Leanplum#start}.
     */
    public static void syncResourcesAsync() {
        if (Constants.isNoop()) {
            return;
        }
        try {
            FileManager.enableResourceSyncing(null, null, true);
        } catch (Throwable t) {
            Util.handleException(t);
        }
    }

    /**
     * Syncs resources between Leanplum and the current app. You should only call this once, and
     * before {@link Leanplum#start}. syncResourcesAsync should be used instead unless file variables
     * need to be defined early
     *
     * @param patternsToInclude Limit paths to only those matching at least one pattern in this list.
     * Supply null to indicate no inclusion patterns. Paths start with the folder name within the res
     * folder, e.g. "layout/main.xml".
     * @param patternsToExclude Exclude paths matching at least one of these patterns. Supply null to
     * indicate no exclusion patterns.
     */
    public static void syncResources(List<String> patternsToInclude, List<String> patternsToExclude) {
        try {
            FileManager.enableResourceSyncing(patternsToInclude, patternsToExclude, false);
        } catch (Throwable t) {
            Util.handleException(t);
        }
    }

    /**
     * Syncs resources between Leanplum and the current app. You should only call this once, and
     * before {@link Leanplum#start}. syncResourcesAsync should be used instead unless file variables
     * need to be defined early
     *
     * @param patternsToInclude Limit paths to only those matching at least one pattern in this list.
     * Supply null to indicate no inclusion patterns. Paths start with the folder name within the res
     * folder, e.g. "layout/main.xml".
     * @param patternsToExclude Exclude paths matching at least one of these patterns. Supply null to
     * indicate no exclusion patterns.
     */
    public static void syncResourcesAsync(List<String> patternsToInclude, List<String> patternsToExclude) {
        try {
            FileManager.enableResourceSyncing(patternsToInclude, patternsToExclude, true);
        } catch (Throwable t) {
            Util.handleException(t);
        }
    }

    /**
     * Returns true if resource syncing is enabled. Resource syncing may not be fully initialized.
     */
    public static boolean isResourceSyncingEnabled() {
        return FileManager.isResourceSyncingEnabled();
    }

    /**
     * Call this when your application starts. This will initiate a call to Leanplum's servers to get
     * the values of the variables used in your app.
     */
    public static void start(Context context) {
        start(context, null, null, null, null);
    }

    /**
     * Call this when your application starts. This will initiate a call to Leanplum's servers to get
     * the values of the variables used in your app.
     */
    public static void start(Context context, StartCallback callback) {
        start(context, null, null, callback, null);
    }

    /**
     * Call this when your application starts. This will initiate a call to Leanplum's servers to get
     * the values of the variables used in your app.
     */
    public static void start(Context context, Map<String, ?> userAttributes) {
        start(context, null, userAttributes, null, null);
    }

    /**
     * Call this when your application starts. This will initiate a call to Leanplum's servers to get
     * the values of the variables used in your app.
     */
    public static void start(Context context, String userId) {
        start(context, userId, null, null, null);
    }

    /**
     * Call this when your application starts. This will initiate a call to Leanplum's servers to get
     * the values of the variables used in your app.
     */
    public static void start(Context context, String userId, StartCallback callback) {
        start(context, userId, null, callback, null);
    }

    /**
     * Call this when your application starts. This will initiate a call to Leanplum's servers to get
     * the values of the variables used in your app.
     */
    public static void start(Context context, String userId, Map<String, ?> userAttributes) {
        start(context, userId, userAttributes, null, null);
    }

    /**
     * Call this when your application starts. This will initiate a call to Leanplum's servers to get
     * the values of the variables used in your app.
     */
    public static synchronized void start(final Context context, String userId, Map<String, ?> attributes,
            StartCallback response) {
        start(context, userId, attributes, response, null);
    }

    static synchronized void start(final Context context, final String userId, final Map<String, ?> attributes,
            StartCallback response, final Boolean isBackground) {
        try {
            OsHandler.getInstance();

            if (context instanceof Activity) {
                LeanplumActivityHelper.currentActivity = (Activity) context;
            }

            // Detect if app is in background automatically if isBackground is not set.
            final boolean actuallyInBackground;
            if (isBackground == null) {
                actuallyInBackground = LeanplumActivityHelper.currentActivity == null
                        || LeanplumActivityHelper.isActivityPaused();
            } else {
                actuallyInBackground = isBackground;
            }

            if (Constants.isNoop()) {
                LeanplumInternal.setHasStarted(true);
                LeanplumInternal.setStartSuccessful(true);
                triggerStartResponse(true);
                triggerVariablesChanged();
                triggerVariablesChangedAndNoDownloadsPending();
                VarCache.applyVariableDiffs(new HashMap<String, Object>(), new HashMap<String, Object>(),
                        VarCache.getUpdateRuleDiffs(), VarCache.getEventRuleDiffs(), new HashMap<String, Object>(),
                        new ArrayList<Map<String, Object>>());
                LeanplumInbox.getInstance().update(new HashMap<String, LeanplumInboxMessage>(), 0, false);
                return;
            }

            if (response != null) {
                addStartResponseHandler(response);
            }

            if (context != null) {
                Leanplum.setApplicationContext(context.getApplicationContext());
            }

            if (LeanplumInternal.hasCalledStart()) {
                if (!actuallyInBackground && LeanplumInternal.hasStartedInBackground()) {
                    // Move to foreground.
                    LeanplumInternal.setStartedInBackground(false);
                    LeanplumInternal.moveToForeground();
                } else {
                    Log.i("Already called start");
                }
                return;
            }

            initializedMessageTemplates = true;
            MessageTemplates.register(Leanplum.getContext());

            LeanplumInternal.setStartedInBackground(actuallyInBackground);

            final Map<String, ?> validAttributes = LeanplumInternal.validateAttributes(attributes, "userAttributes",
                    true);
            LeanplumInternal.setCalledStart(true);

            if (validAttributes != null) {
                LeanplumInternal.getUserAttributeChanges().add(validAttributes);
            }

            Request.loadToken();
            VarCache.setSilent(true);
            VarCache.loadDiffs();
            VarCache.setSilent(false);
            LeanplumInbox.getInstance().load();

            // Setup class members.
            VarCache.onUpdate(new CacheUpdateBlock() {
                @Override
                public void updateCache() {
                    triggerVariablesChanged();
                    if (Request.numPendingDownloads() == 0) {
                        triggerVariablesChangedAndNoDownloadsPending();
                    }
                }
            });
            Request.onNoPendingDownloads(new Request.NoPendingDownloadsCallback() {
                @Override
                public void noPendingDownloads() {
                    triggerVariablesChangedAndNoDownloadsPending();
                }
            });

            // Reduce latency by running the rest of the start call in a background thread.
            Util.executeAsyncTask(new AsyncTask<Void, Void, Void>() {
                @Override
                protected Void doInBackground(Void... params) {
                    try {
                        startHelper(userId, validAttributes, actuallyInBackground);
                    } catch (Throwable t) {
                        Util.handleException(t);
                    }
                    return null;
                }
            });
        } catch (Throwable t) {
            Util.handleException(t);
        }
    }

    private static void startHelper(String userId, final Map<String, ?> attributes, final boolean isBackground) {
        LeanplumPushService.onStart();

        Boolean limitAdTracking = null;
        String deviceId = Request.deviceId();
        if (deviceId == null) {
            if (!userSpecifiedDeviceId && Constants.defaultDeviceId != null) {
                deviceId = Constants.defaultDeviceId;
            } else if (customDeviceId != null) {
                deviceId = customDeviceId;
            } else {
                DeviceIdInfo deviceIdInfo = Util.getDeviceId(deviceIdMode);
                deviceId = deviceIdInfo.id;
                limitAdTracking = deviceIdInfo.limitAdTracking;
            }
            Request.setDeviceId(deviceId);
        }

        if (userId == null) {
            userId = Request.userId();
            if (userId == null) {
                userId = Request.deviceId();
            }
        }
        Request.setUserId(userId);

        // Setup parameters.
        String versionName = Util.getVersionName();
        if (versionName == null) {
            versionName = "";
        }

        TimeZone localTimeZone = TimeZone.getDefault();
        Date now = new Date();
        int timezoneOffsetSeconds = localTimeZone.getOffset(now.getTime()) / 1000;

        HashMap<String, Object> params = new HashMap<>();
        params.put(Constants.Params.INCLUDE_DEFAULTS, Boolean.toString(false));
        if (isBackground) {
            params.put(Constants.Params.BACKGROUND, Boolean.toString(true));
        }
        params.put(Constants.Params.VERSION_NAME, versionName);
        params.put(Constants.Params.DEVICE_NAME, Util.getDeviceName());
        params.put(Constants.Params.DEVICE_MODEL, Util.getDeviceModel());
        params.put(Constants.Params.DEVICE_SYSTEM_NAME, Util.getSystemName());
        params.put(Constants.Params.DEVICE_SYSTEM_VERSION, Util.getSystemVersion());
        params.put(Constants.Keys.TIMEZONE, localTimeZone.getID());
        params.put(Constants.Keys.TIMEZONE_OFFSET_SECONDS, Integer.toString(timezoneOffsetSeconds));
        params.put(Constants.Keys.LOCALE, Util.getLocale());
        params.put(Constants.Keys.COUNTRY, Constants.Values.DETECT);
        params.put(Constants.Keys.REGION, Constants.Values.DETECT);
        params.put(Constants.Keys.CITY, Constants.Values.DETECT);
        params.put(Constants.Keys.LOCATION, Constants.Values.DETECT);
        if (Boolean.TRUE.equals(limitAdTracking)) {
            params.put(Constants.Params.LIMIT_TRACKING, limitAdTracking.toString());
        }
        if (attributes != null) {
            params.put(Constants.Params.USER_ATTRIBUTES, JsonConverter.toJson(attributes));
        }
        if (Constants.isDevelopmentModeEnabled) {
            params.put(Constants.Params.DEV_MODE, Boolean.TRUE.toString());
        }

        // Get the current inbox messages on the device.
        params.put(Constants.Params.INBOX_MESSAGES, LeanplumInbox.getInstance().messagesIds());

        Util.initializePreLeanplumInstall(params);

        // Issue start API call.
        Request req = Request.post(Constants.Methods.START, params);
        req.onApiResponse(new Request.ApiResponseCallback() {
            @Override
            public void response(List<Map<String, Object>> requests, JSONObject response) {
                Leanplum.handleApiResponse(response, requests);
            }
        });

        if (isBackground) {
            req.sendEventually();
        } else {
            req.sendIfConnected();
        }

        LeanplumInternal.triggerStartIssued();
    }

    private static void handleApiResponse(JSONObject response, List<Map<String, Object>> requests) {
        boolean hasStartResponse = false;
        JSONObject lastStartResponse = null;

        // Find and handle the last start response.
        try {
            int numResponses = Request.numResponses(response);
            for (int i = requests.size() - 1; i >= 0; i--) {
                Map<String, Object> request = requests.get(i);
                if (Constants.Methods.START.equals(request.get(Constants.Params.ACTION))) {
                    if (i < numResponses) {
                        lastStartResponse = Request.getResponseAt(response, i);
                    }
                    hasStartResponse = true;
                    break;
                }
            }
        } catch (Throwable t) {
            Util.handleException(t);
        }

        if (hasStartResponse) {
            if (!LeanplumInternal.hasStarted()) {
                Leanplum.handleStartResponse(lastStartResponse);
            }
        }
    }

    private static void handleStartResponse(JSONObject response) {
        boolean success = Request.isResponseSuccess(response);
        if (!success) {
            try {
                LeanplumInternal.setHasStarted(true);
                LeanplumInternal.setStartSuccessful(false);

                // Load the variables that were stored on the device from the last session.
                VarCache.loadDiffs();

                triggerStartResponse(false);
            } catch (Throwable t) {
                Util.handleException(t);
            }
        } else {
            try {
                LeanplumInternal.setHasStarted(true);
                LeanplumInternal.setStartSuccessful(true);

                JSONObject values = response.optJSONObject(Constants.Keys.VARS);
                if (values == null) {
                    Log.e("No variable values were received from the server. "
                            + "Please contact us to investigate.");
                }

                JSONObject messages = response.optJSONObject(Constants.Keys.MESSAGES);
                if (messages == null) {
                    Log.d("No messages received from the server.");
                }

                JSONObject regions = response.optJSONObject(Constants.Keys.REGIONS);
                if (regions == null) {
                    Log.d("No regions received from the server.");
                }

                JSONArray variants = response.optJSONArray(Constants.Keys.VARIANTS);
                if (variants == null) {
                    Log.d("No variants received from the server.");
                }

                String token = response.optString(Constants.Keys.TOKEN, null);
                Request.setToken(token);
                Request.saveToken();

                applyContentInResponse(response, true);

                VarCache.saveUserAttributes();
                triggerStartResponse(true);

                if (response.optBoolean(Constants.Keys.SYNC_INBOX, false)) {
                    LeanplumInbox.getInstance().downloadMessages();
                }

                if (response.optBoolean(Constants.Keys.LOGGING_ENABLED, false)) {
                    Constants.loggingEnabled = true;
                }

                // Allow bidirectional realtime variable updates.
                if (Constants.isDevelopmentModeEnabled) {

                    final Context currentContext = (LeanplumActivityHelper.currentActivity != context
                            && LeanplumActivityHelper.currentActivity != null)
                                    ? LeanplumActivityHelper.currentActivity
                                    : context;

                    // Register device.
                    if (!response.optBoolean(Constants.Keys.IS_REGISTERED) && registerDeviceHandler != null) {
                        registerDeviceHandler.setResponseHandler(new RegisterDeviceCallback.EmailCallback() {
                            @Override
                            public void onResponse(String email) {
                                try {
                                    if (email != null) {
                                        Registration.registerDevice(email, new StartCallback() {
                                            @Override
                                            public void onResponse(boolean success) {
                                                if (registerDeviceFinishedHandler != null) {
                                                    registerDeviceFinishedHandler.setSuccess(success);
                                                    OsHandler.getInstance().post(registerDeviceFinishedHandler);
                                                }
                                                if (success) {
                                                    try {
                                                        LeanplumInternal.onHasStartedAndRegisteredAsDeveloper();
                                                    } catch (Throwable t) {
                                                        Util.handleException(t);
                                                    }
                                                }
                                            }
                                        });
                                    }
                                } catch (Throwable t) {
                                    Util.handleException(t);
                                }
                            }
                        });
                        OsHandler.getInstance().post(registerDeviceHandler);
                    }

                    // Show device is already registered.
                    if (response.optBoolean(Constants.Keys.IS_REGISTERED_FROM_OTHER_APP)) {
                        OsHandler.getInstance().post(new Runnable() {
                            @Override
                            public void run() {
                                try {
                                    NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(
                                            currentContext).setSmallIcon(android.R.drawable.star_on)
                                                    .setContentTitle("Leanplum")
                                                    .setContentText("Your device is registered.");
                                    mBuilder.setContentIntent(PendingIntent.getActivity(
                                            currentContext.getApplicationContext(), 0, new Intent(), 0));
                                    NotificationManager mNotificationManager = (NotificationManager) currentContext
                                            .getSystemService(Context.NOTIFICATION_SERVICE);
                                    // mId allows you to update the notification later on.
                                    mNotificationManager.notify(0, mBuilder.build());
                                } catch (Throwable t) {
                                    Log.i("Device is registered.");
                                }
                            }
                        });
                    }

                    boolean isRegistered = response.optBoolean(Constants.Keys.IS_REGISTERED);

                    // Check for updates.
                    final String latestVersion = response.optString(Constants.Keys.LATEST_VERSION, null);
                    if (isRegistered && latestVersion != null) {
                        Log.i("An update to Leanplum Android SDK, " + latestVersion
                                + ", is available. Go to leanplum.com to download it.");
                    }

                    JSONObject valuesFromCode = response.optJSONObject(Constants.Keys.VARS_FROM_CODE);
                    if (valuesFromCode == null) {
                        valuesFromCode = new JSONObject();
                    }

                    JSONObject actionDefinitions = response.optJSONObject(Constants.Params.ACTION_DEFINITIONS);
                    if (actionDefinitions == null) {
                        actionDefinitions = new JSONObject();
                    }

                    JSONObject fileAttributes = response.optJSONObject(Constants.Params.FILE_ATTRIBUTES);
                    if (fileAttributes == null) {
                        fileAttributes = new JSONObject();
                    }

                    VarCache.setDevModeValuesFromServer(JsonConverter.mapFromJson(valuesFromCode),
                            JsonConverter.mapFromJson(fileAttributes),
                            JsonConverter.mapFromJson(actionDefinitions));

                    if (isRegistered) {
                        LeanplumInternal.onHasStartedAndRegisteredAsDeveloper();
                    }
                }

                LeanplumInternal.moveToForeground();
                startHeartbeat();
            } catch (Throwable t) {
                Util.handleException(t);
            }
        }
    }

    /**
     * Applies the variables, messages, or update rules in a start or getVars response.
     *
     * @param response The response containing content.
     * @param alwaysApply Always apply the content regardless of whether the content changed.
     */
    private static void applyContentInResponse(JSONObject response, boolean alwaysApply) {
        Map<String, Object> values = JsonConverter
                .mapFromJsonOrDefault(response.optJSONObject(Constants.Keys.VARS));
        Map<String, Object> messages = JsonConverter
                .mapFromJsonOrDefault(response.optJSONObject(Constants.Keys.MESSAGES));
        List<Map<String, Object>> updateRules = JsonConverter
                .listFromJsonOrDefault(response.optJSONArray(Constants.Keys.UPDATE_RULES));
        List<Map<String, Object>> eventRules = JsonConverter
                .listFromJsonOrDefault(response.optJSONArray(Constants.Keys.EVENT_RULES));
        Map<String, Object> regions = JsonConverter
                .mapFromJsonOrDefault(response.optJSONObject(Constants.Keys.REGIONS));
        List<Map<String, Object>> variants = JsonConverter
                .listFromJsonOrDefault(response.optJSONArray(Constants.Keys.VARIANTS));

        if (alwaysApply || !values.equals(VarCache.getDiffs()) || !messages.equals(VarCache.getMessageDiffs())
                || !updateRules.equals(VarCache.getUpdateRuleDiffs())
                || !eventRules.equals(VarCache.getEventRuleDiffs()) || !regions.equals(VarCache.regions())) {
            VarCache.applyVariableDiffs(values, messages, updateRules, eventRules, regions, variants);
        }
    }

    /**
     * Used by wrapper SDKs like Unity to override the SDK client name and version.
     */
    static void setClient(String client, String sdkVersion, String defaultDeviceId) {
        Constants.CLIENT = client;
        Constants.LEANPLUM_VERSION = sdkVersion;
        Constants.defaultDeviceId = defaultDeviceId;
    }

    /**
     * Call this when your activity pauses. This is called from LeanplumActivityHelper.
     */
    static void pause() {
        if (Constants.isNoop()) {
            return;
        }
        if (!LeanplumInternal.hasCalledStart()) {
            Log.e("You cannot call pause before calling start");
            return;
        }
        LeanplumInternal.setIsPaused(true);

        if (LeanplumInternal.isPaused()) {
            pauseInternal();
        } else {
            LeanplumInternal.addStartIssuedHandler(new Runnable() {
                @Override
                public void run() {
                    try {
                        pauseInternal();
                    } catch (Throwable t) {
                        Util.handleException(t);
                    }
                }
            });
        }
    }

    private static void pauseInternal() {
        Request.post(Constants.Methods.PAUSE_SESSION, null).sendIfConnected();
        pauseHeartbeat();
    }

    /**
     * Call this when your activity resumes. This is called from LeanplumActivityHelper.
     */
    static void resume() {
        if (Constants.isNoop()) {
            return;
        }
        if (!LeanplumInternal.hasCalledStart()) {
            Log.e("You cannot call resume before calling start");
            return;
        }
        LeanplumInternal.setIsPaused(false);

        if (LeanplumInternal.issuedStart()) {
            resumeInternal();
        } else {
            LeanplumInternal.addStartIssuedHandler(new Runnable() {
                @Override
                public void run() {
                    try {
                        resumeInternal();
                    } catch (Throwable t) {
                        Util.handleException(t);
                    }
                }
            });
        }
    }

    private static void resumeInternal() {
        Request request = Request.post(Constants.Methods.RESUME_SESSION, null);
        if (LeanplumInternal.hasStartedInBackground()) {
            LeanplumInternal.setStartedInBackground(false);
            request.sendIfConnected();
        } else {
            request.sendIfDelayed();
            LeanplumInternal.maybePerformActions("resume", null,
                    LeanplumMessageMatchFilter.LEANPLUM_ACTION_FILTER_ALL, null, null);
        }
        resumeHeartbeat();
    }

    /**
     * Send a heartbeat every 15 minutes while the app is running.
     */
    private static void startHeartbeat() {
        synchronized (heartbeatLock) {
            heartbeatExecutor = Executors.newSingleThreadScheduledExecutor();
            heartbeatExecutor.scheduleAtFixedRate(new Runnable() {
                public void run() {
                    try {
                        Request.post(Constants.Methods.HEARTBEAT, null).sendIfDelayed();
                    } catch (Throwable t) {
                        Util.handleException(t);
                    }
                }
            }, 15, 15, TimeUnit.MINUTES);
        }
    }

    private static void pauseHeartbeat() {
        synchronized (heartbeatLock) {
            if (heartbeatExecutor != null) {
                heartbeatExecutor.shutdown();
            }
        }
    }

    private static void resumeHeartbeat() {
        startHeartbeat();
    }

    /**
     * Call this to explicitly end the session. This should not be used in most cases, so we won't
     * make it public for now.
     */
    static void stop() {
        if (Constants.isNoop()) {
            return;
        }
        if (!LeanplumInternal.hasCalledStart()) {
            Log.e("You cannot call stop before calling start");
            return;
        }

        if (LeanplumInternal.issuedStart()) {
            stopInternal();
        } else {
            LeanplumInternal.addStartIssuedHandler(new Runnable() {
                @Override
                public void run() {
                    try {
                        stopInternal();
                    } catch (Throwable t) {
                        Util.handleException(t);
                    }
                }
            });
        }
    }

    private static void stopInternal() {
        Request.post(Constants.Methods.STOP, null).sendIfConnected();
    }

    /**
     * Whether or not Leanplum has finished starting.
     */
    public static boolean hasStarted() {
        return LeanplumInternal.hasStarted();
    }

    /**
     * Returns an instance to the singleton Newsfeed object.
     *
     * @deprecated use {@link #getInbox} instead
     */
    public static Newsfeed newsfeed() {
        return Newsfeed.getInstance();
    }

    /**
     * Returns an instance to the singleton LeanplumInbox object.
     */
    public static LeanplumInbox getInbox() {
        return LeanplumInbox.getInstance();
    }

    /**
     * Whether or not Leanplum has finished starting and the device is registered as a developer.
     */
    public static boolean hasStartedAndRegisteredAsDeveloper() {
        return LeanplumInternal.hasStartedAndRegisteredAsDeveloper();
    }

    /**
     * Add a callback for when the start call finishes, and variables are returned back from the
     * server.
     */
    public static void addStartResponseHandler(StartCallback handler) {
        if (handler == null) {
            Log.e("addStartResponseHandler - Invalid handler parameter provided.");
            return;
        }

        if (LeanplumInternal.hasStarted()) {
            if (LeanplumInternal.isStartSuccessful()) {
                handler.setSuccess(true);
            }
            handler.run();
        } else {
            synchronized (startHandlers) {
                if (startHandlers.indexOf(handler) == -1) {
                    startHandlers.add(handler);
                }
            }
        }
    }

    /**
     * Removes a start response callback.
     */
    public static void removeStartResponseHandler(StartCallback handler) {
        if (handler == null) {
            Log.e("removeStartResponseHandler - Invalid handler parameter provided.");
            return;
        }

        synchronized (startHandlers) {
            startHandlers.remove(handler);
        }
    }

    private static void triggerStartResponse(boolean success) {
        synchronized (startHandlers) {
            for (StartCallback callback : startHandlers) {
                callback.setSuccess(success);
                OsHandler.getInstance().post(callback);
            }
            startHandlers.clear();
        }
    }

    /**
     * Add a callback for when the variables receive new values from the server. This will be called
     * on start, and also later on if the user is in an experiment that can updated in realtime.
     */
    public static void addVariablesChangedHandler(VariablesChangedCallback handler) {
        if (handler == null) {
            Log.e("addVariablesChangedHandler - Invalid handler parameter provided.");
            return;
        }

        synchronized (variablesChangedHandlers) {
            variablesChangedHandlers.add(handler);
        }
        if (VarCache.hasReceivedDiffs()) {
            handler.variablesChanged();
        }
    }

    /**
     * Removes a variables changed callback.
     */
    public static void removeVariablesChangedHandler(VariablesChangedCallback handler) {
        if (handler == null) {
            Log.e("removeVariablesChangedHandler - Invalid handler parameter provided.");
            return;
        }

        synchronized (variablesChangedHandlers) {
            variablesChangedHandlers.remove(handler);
        }
    }

    private static void triggerVariablesChanged() {
        synchronized (variablesChangedHandlers) {
            for (VariablesChangedCallback callback : variablesChangedHandlers) {
                OsHandler.getInstance().post(callback);
            }
        }
    }

    /**
     * Add a callback for when no more file downloads are pending (either when no files needed to be
     * downloaded or all downloads have been completed).
     */
    public static void addVariablesChangedAndNoDownloadsPendingHandler(VariablesChangedCallback handler) {
        if (handler == null) {
            Log.e("addVariablesChangedAndNoDownloadsPendingHandler - Invalid handler parameter " + "provided.");
            return;
        }

        synchronized (noDownloadsHandlers) {
            noDownloadsHandlers.add(handler);
        }
        if (VarCache.hasReceivedDiffs() && Request.numPendingDownloads() == 0) {
            handler.variablesChanged();
        }
    }

    /**
     * Removes a variables changed and no downloads pending callback.
     */
    public static void removeVariablesChangedAndNoDownloadsPendingHandler(VariablesChangedCallback handler) {
        if (handler == null) {
            Log.e("removeVariablesChangedAndNoDownloadsPendingHandler - Invalid handler parameter " + "provided.");
            return;
        }

        synchronized (noDownloadsHandlers) {
            noDownloadsHandlers.remove(handler);
        }
    }

    /**
     * Add a callback to call ONCE when no more file downloads are pending (either when no files
     * needed to be downloaded or all downloads have been completed).
     */
    public static void addOnceVariablesChangedAndNoDownloadsPendingHandler(VariablesChangedCallback handler) {
        if (handler == null) {
            Log.e("addOnceVariablesChangedAndNoDownloadsPendingHandler - Invalid handler parameter" + " provided.");
            return;
        }

        if (VarCache.hasReceivedDiffs() && Request.numPendingDownloads() == 0) {
            handler.variablesChanged();
        } else {
            synchronized (onceNoDownloadsHandlers) {
                onceNoDownloadsHandlers.add(handler);
            }
        }
    }

    /**
     * Removes a once variables changed and no downloads pending callback.
     */
    public static void removeOnceVariablesChangedAndNoDownloadsPendingHandler(VariablesChangedCallback handler) {
        if (handler == null) {
            Log.e("removeOnceVariablesChangedAndNoDownloadsPendingHandler - Invalid handler"
                    + " parameter provided.");
            return;
        }

        synchronized (onceNoDownloadsHandlers) {
            onceNoDownloadsHandlers.remove(handler);
        }
    }

    static void triggerVariablesChangedAndNoDownloadsPending() {
        synchronized (noDownloadsHandlers) {
            for (VariablesChangedCallback callback : noDownloadsHandlers) {
                OsHandler.getInstance().post(callback);
            }
        }
        synchronized (onceNoDownloadsHandlers) {
            for (VariablesChangedCallback callback : onceNoDownloadsHandlers) {
                OsHandler.getInstance().post(callback);
            }
            onceNoDownloadsHandlers.clear();
        }
    }

    /**
     * Defines an action that is used within Leanplum Marketing Automation. Actions can be set up to
     * get triggered based on app opens, events, and states. Call {@link Leanplum#onAction} to handle
     * the action.
     *
     * @param name The name of the action to register.
     * @param kind Whether to display the action as a message and/or a regular action.
     * @param args User-customizable options for the action.
     */
    public static void defineAction(String name, int kind, ActionArgs args) {
        defineAction(name, kind, args, null, null);
    }

    @Deprecated
    static void defineAction(String name, int kind, ActionArgs args, Map<String, Object> options) {
        defineAction(name, kind, args, options, null);
    }

    /**
     * Defines an action that is used within Leanplum Marketing Automation. Actions can be set up to
     * get triggered based on app opens, events, and states.
     *
     * @param name The name of the action to register.
     * @param kind Whether to display the action as a message and/or a regular action.
     * @param args User-customizable options for the action.
     * @param responder Called when the action is triggered with a context object containing the
     * user-specified options.
     */
    public static void defineAction(String name, int kind, ActionArgs args, ActionCallback responder) {
        defineAction(name, kind, args, null, responder);
    }

    private static void defineAction(String name, int kind, ActionArgs args, Map<String, Object> options,
            ActionCallback responder) {
        if (TextUtils.isEmpty(name)) {
            Log.e("defineAction - Empty name parameter provided.");
            return;
        }
        if (args == null) {
            Log.e("defineAction - Invalid args parameter provided.");
            return;
        }

        try {
            Context context = Leanplum.getContext();
            if (!initializedMessageTemplates) {
                initializedMessageTemplates = true;
                MessageTemplates.register(context);
            }

            if (options == null) {
                options = new HashMap<>();
            }
            LeanplumInternal.getActionHandlers().remove(name);
            VarCache.registerActionDefinition(name, kind, args.getValue(), options);
            if (responder != null) {
                onAction(name, responder);
            }
        } catch (Throwable t) {
            Util.handleException(t);
        }
    }

    /**
     * Adds a callback that handles an action with the given name.
     *
     * @param actionName The name of the type of action to handle.
     * @param handler The callback that runs when the action is triggered.
     */
    public static void onAction(String actionName, ActionCallback handler) {
        if (actionName == null) {
            Log.e("onAction - Invalid actionName parameter provided.");
            return;
        }
        if (handler == null) {
            Log.e("onAction - Invalid handler parameter provided.");
            return;
        }

        List<ActionCallback> handlers = LeanplumInternal.getActionHandlers().get(actionName);
        if (handlers == null) {
            handlers = new ArrayList<>();
            LeanplumInternal.getActionHandlers().put(actionName, handlers);
        }
        handlers.add(handler);
    }

    /**
     * Updates the user ID and adds or modifies user attributes.
     */
    public static void setUserAttributes(final String userId, Map<String, ?> userAttributes) {
        if (Constants.isNoop()) {
            return;
        }
        if (!LeanplumInternal.hasCalledStart()) {
            Log.e("You cannot call setUserAttributes before calling start");
            return;
        }
        try {
            final HashMap<String, Object> params = new HashMap<>();
            if (userId != null) {
                params.put(Constants.Params.NEW_USER_ID, userId);
            }
            if (userAttributes != null) {
                userAttributes = LeanplumInternal.validateAttributes(userAttributes, "userAttributes", true);
                params.put(Constants.Params.USER_ATTRIBUTES, JsonConverter.toJson(userAttributes));
                LeanplumInternal.getUserAttributeChanges().add(userAttributes);
            }

            if (LeanplumInternal.issuedStart()) {
                setUserAttributesInternal(userId, params);
            } else {
                LeanplumInternal.addStartIssuedHandler(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            setUserAttributesInternal(userId, params);
                        } catch (Throwable t) {
                            Util.handleException(t);
                        }
                    }
                });
            }
        } catch (Throwable t) {
            Util.handleException(t);
        }
    }

    private static void setUserAttributesInternal(String userId, HashMap<String, Object> requestArgs) {
        Request.post(Constants.Methods.SET_USER_ATTRIBUTES, requestArgs).send();
        if (userId != null && userId.length() > 0) {
            Request.setUserId(userId);
            if (LeanplumInternal.hasStarted()) {
                VarCache.saveDiffs();
            }
        }
        LeanplumInternal.recordAttributeChanges();
    }

    /**
     * Updates the user ID.
     */
    public static void setUserId(String userId) {
        if (userId == null) {
            Log.e("setUserId - Invalid userId parameter provided.");
            return;
        }

        setUserAttributes(userId, null);
    }

    /**
     * Adds or modifies user attributes.
     */
    public static void setUserAttributes(Map<String, Object> userAttributes) {
        if (userAttributes == null || userAttributes.isEmpty()) {
            Log.e("setUserAttributes - Invalid userAttributes parameter provided (null or empty).");
            return;
        }

        setUserAttributes(null, userAttributes);
    }

    /**
     * Sets the registration ID used for Cloud Messaging.
     */
    static void setRegistrationId(final String registrationId) {
        if (Constants.isNoop()) {
            return;
        }
        pushStartCallback = new Runnable() {
            @Override
            public void run() {
                if (Constants.isNoop()) {
                    return;
                }
                try {
                    HashMap<String, Object> params = new HashMap<>();
                    params.put(Constants.Params.DEVICE_PUSH_TOKEN, registrationId);
                    Request.post(Constants.Methods.SET_DEVICE_ATTRIBUTES, params).send();
                } catch (Throwable t) {
                    Util.handleException(t);
                }
            }
        };
        LeanplumInternal.addStartIssuedHandler(pushStartCallback);
    }

    /**
     * Sets the traffic source info for the current user. Keys in info must be one of: publisherId,
     * publisherName, publisherSubPublisher, publisherSubSite, publisherSubCampaign,
     * publisherSubAdGroup, publisherSubAd.
     */
    public static void setTrafficSourceInfo(Map<String, String> info) {
        if (Constants.isNoop()) {
            return;
        }
        if (!LeanplumInternal.hasCalledStart()) {
            Log.e("You cannot call setTrafficSourceInfo before calling start");
            return;
        }
        if (info == null || info.isEmpty()) {
            Log.e("setTrafficSourceInfo - Invalid info parameter provided (null or empty).");
            return;
        }

        try {
            final HashMap<String, Object> params = new HashMap<>();
            info = LeanplumInternal.validateAttributes(info, "info", false);
            params.put(Constants.Params.TRAFFIC_SOURCE, JsonConverter.toJson(info));
            if (LeanplumInternal.issuedStart()) {
                setTrafficSourceInfoInternal(params);
            } else {
                LeanplumInternal.addStartIssuedHandler(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            setTrafficSourceInfoInternal(params);
                        } catch (Throwable t) {
                            Util.handleException(t);
                        }
                    }
                });
            }
        } catch (Throwable t) {
            Util.handleException(t);
        }
    }

    private static void setTrafficSourceInfoInternal(HashMap<String, Object> params) {
        Request.post(Constants.Methods.SET_TRAFFIC_SOURCE_INFO, params).send();
    }

    /**
     * Logs a particular event in your application. The string can be any value of your choosing, and
     * will show up in the dashboard.
     * <p>
     * <p>To track Purchase events, call {@link Leanplum#trackGooglePlayPurchase} instead for in-app
     * purchases, or use {@link Leanplum#PURCHASE_EVENT_NAME} as the event name for other types of
     * purchases.
     *
     * @param event Name of the event. Event may be empty for message impression events.
     * @param value The value of the event. The value is special in that you can use it for targeting
     * content and messages to users who have a particular lifetime value. For purchase events, the
     * value is the revenue associated with the purchase.
     * @param info Basic context associated with the event, such as the item purchased. info is
     * treated like a default parameter.
     * @param params Key-value pairs with metrics or data associated with the event. Parameters can be
     * strings or numbers. You can use up to 200 different parameter names in your app.
     */
    public static void track(final String event, double value, String info, Map<String, ?> params) {
        LeanplumInternal.track(event, value, info, params, null);
    }

    /**
     * Tracks an in-app purchase as a Purchase event.
     *
     * @param item The name of the item that was purchased.
     * @param priceMicros The price in micros in the user's local currency.
     * @param currencyCode The currency code corresponding to the price.
     * @param purchaseData Purchase data from purchase.getOriginalJson().
     * @param dataSignature Signature from purchase.getSignature().
     */
    public static void trackGooglePlayPurchase(String item, long priceMicros, String currencyCode,
            String purchaseData, String dataSignature) {
        trackGooglePlayPurchase(PURCHASE_EVENT_NAME, item, priceMicros, currencyCode, purchaseData, dataSignature,
                null);
    }

    /**
     * Tracks an in-app purchase as a Purchase event.
     *
     * @param item The name of the item that was purchased.
     * @param priceMicros The price in micros in the user's local currency.
     * @param currencyCode The currency code corresponding to the price.
     * @param purchaseData Purchase data from purchase.getOriginalJson().
     * @param dataSignature Signature from purchase.getSignature().
     * @param params Any additional parameters to track with the event.
     */
    public static void trackGooglePlayPurchase(String item, long priceMicros, String currencyCode,
            String purchaseData, String dataSignature, Map<String, ?> params) {
        trackGooglePlayPurchase(PURCHASE_EVENT_NAME, item, priceMicros, currencyCode, purchaseData, dataSignature,
                params);
    }

    /**
     * Tracks an in-app purchase.
     *
     * @param eventName The name of the event to record the purchase under. Normally, this would be
     * {@link Leanplum#PURCHASE_EVENT_NAME}.
     * @param item The name of the item that was purchased.
     * @param priceMicros The price in micros in the user's local currency.
     * @param currencyCode The currency code corresponding to the price.
     * @param purchaseData Purchase data from purchase.getOriginalJson().
     * @param dataSignature Signature from purchase.getSignature().
     * @param params Any additional parameters to track with the event.
     */
    @SuppressWarnings("SameParameterValue")
    public static void trackGooglePlayPurchase(String eventName, String item, long priceMicros, String currencyCode,
            String purchaseData, String dataSignature, Map<String, ?> params) {
        if (TextUtils.isEmpty(eventName)) {
            Log.w("trackGooglePlayPurchase - Empty eventName parameter provided.");
        }

        final Map<String, String> requestArgs = new HashMap<>();
        requestArgs.put(Constants.Params.GOOGLE_PLAY_PURCHASE_DATA, purchaseData);
        requestArgs.put(Constants.Params.GOOGLE_PLAY_PURCHASE_DATA_SIGNATURE, dataSignature);
        requestArgs.put(Constants.Params.IAP_CURRENCY_CODE, currencyCode);

        Map<String, Object> modifiedParams;
        if (params == null) {
            modifiedParams = new HashMap<>();
        } else {
            modifiedParams = new HashMap<>(params);
        }
        modifiedParams.put(Constants.Params.IAP_ITEM, item);

        LeanplumInternal.track(eventName, priceMicros / 1000000.0, null, modifiedParams, requestArgs);
    }

    /**
     * Logs a particular event in your application. The string can be any value of your choosing, and
     * will show up in the dashboard.
     * <p>
     * <p>To track Purchase events, use {@link Leanplum#PURCHASE_EVENT_NAME}.
     *
     * @param event Name of the event.
     */
    public static void track(String event) {
        track(event, 0.0, "", null);
    }

    /**
     * Logs a particular event in your application. The string can be any value of your choosing, and
     * will show up in the dashboard.
     * <p>
     * <p>To track Purchase events, use {@link Leanplum#PURCHASE_EVENT_NAME}.
     *
     * @param event Name of the event.
     * @param value The value of the event. The value is special in that you can use it for targeting
     * content and messages to users who have a particular lifetime value. For purchase events, the
     * value is the revenue associated with the purchase.
     */
    public static void track(String event, double value) {
        track(event, value, "", null);
    }

    /**
     * Logs a particular event in your application. The string can be any value of your choosing, and
     * will show up in the dashboard.
     * <p>
     * <p>To track Purchase events, use {@link Leanplum#PURCHASE_EVENT_NAME}.
     *
     * @param event Name of the event.
     * @param info Basic context associated with the event, such as the item purchased. info is
     * treated like a default parameter.
     */
    public static void track(String event, String info) {
        track(event, 0.0, info, null);
    }

    /**
     * Logs a particular event in your application. The string can be any value of your choosing, and
     * will show up in the dashboard.
     * <p>
     * <p>To track Purchase events, use {@link Leanplum#PURCHASE_EVENT_NAME}.
     *
     * @param event Name of the event.
     * @param params Key-value pairs with metrics or data associated with the event. Parameters can be
     * strings or numbers. You can use up to 200 different parameter names in your app.
     */
    public static void track(String event, Map<String, ?> params) {
        track(event, 0.0, "", params);
    }

    /**
     * Logs a particular event in your application. The string can be any value of your choosing, and
     * will show up in the dashboard.
     * <p>
     * <p>To track Purchase events, use {@link Leanplum#PURCHASE_EVENT_NAME}.
     *
     * @param event Name of the event.
     * @param value The value of the event. The value is special in that you can use it for targeting
     * content and messages to users who have a particular lifetime value. For purchase events, the
     * value is the revenue associated with the purchase.
     * @param params Key-value pairs with metrics or data associated with the event. Parameters can be
     * strings or numbers. You can use up to 200 different parameter names in your app.
     */
    public static void track(String event, double value, Map<String, ?> params) {
        track(event, value, "", params);
    }

    /**
     * Logs a particular event in your application. The string can be any value of your choosing, and
     * will show up in the dashboard.
     * <p>
     * <p>To track Purchase events, use {@link Leanplum#PURCHASE_EVENT_NAME}.
     *
     * @param event Name of the event.
     * @param value The value of the event. The value is special in that you can use it for targeting
     * content and messages to users who have a particular lifetime value. For purchase events, the
     * value is the revenue associated with the purchase.
     * @param info Basic context associated with the event, such as the item purchased. info is
     * treated like a default parameter.
     */
    public static void track(String event, double value, String info) {
        track(event, value, info, null);
    }

    /**
     * Advances to a particular state in your application. The string can be any value of your
     * choosing, and will show up in the dashboard. A state is a section of your app that the user is
     * currently in.
     *
     * @param state Name of the state. State may be empty for message impression events.
     * @param info Basic context associated with the state, such as the item purchased. info is
     * treated like a default parameter.
     * @param params Key-value pairs with metrics or data associated with the state. Parameters can be
     * strings or numbers. You can use up to 200 different parameter names in your app.
     */
    public static void advanceTo(final String state, String info, final Map<String, ?> params) {
        if (Constants.isNoop()) {
            return;
        }
        if (!LeanplumInternal.hasCalledStart()) {
            Log.e("You cannot call advanceTo before calling start");
            return;
        }

        try {
            final Map<String, Object> requestParams = new HashMap<>();
            requestParams.put(Constants.Params.INFO, info);
            requestParams.put(Constants.Params.STATE, state);
            final Map<String, ?> validatedParams;
            if (params != null) {
                validatedParams = LeanplumInternal.validateAttributes(params, "params", false);
                requestParams.put(Constants.Params.PARAMS, JsonConverter.toJson(validatedParams));
            } else {
                validatedParams = null;
            }

            if (LeanplumInternal.issuedStart()) {
                advanceToInternal(state, validatedParams, requestParams);
            } else {
                LeanplumInternal.addStartIssuedHandler(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            advanceToInternal(state, validatedParams, requestParams);
                        } catch (Throwable t) {
                            Util.handleException(t);
                        }
                    }
                });
            }
        } catch (Throwable t) {
            Util.handleException(t);
        }
    }

    /**
     * Performs the advance API and any actions that are associated with the state.
     *
     * @param state The state name. State may be empty for message impression events.
     * @param params The state parameters.
     * @param requestParams The arguments to send with the API request.
     */
    private static void advanceToInternal(String state, Map<String, ?> params, Map<String, Object> requestParams) {
        Request.post(Constants.Methods.ADVANCE, requestParams).send();

        ContextualValues contextualValues = new ContextualValues();
        contextualValues.parameters = params;

        LeanplumInternal.maybePerformActions("state", state, LeanplumMessageMatchFilter.LEANPLUM_ACTION_FILTER_ALL,
                null, contextualValues);
    }

    /**
     * Advances to a particular state in your application. The string can be any value of your
     * choosing, and will show up in the dashboard. A state is a section of your app that the user is
     * currently in.
     *
     * @param state Name of the state. State may be empty for message impression events.
     */
    public static void advanceTo(String state) {
        advanceTo(state, "", null);
    }

    /**
     * Advances to a particular state in your application. The string can be any value of your
     * choosing, and will show up in the dashboard. A state is a section of your app that the user is
     * currently in.
     *
     * @param state Name of the state. State may be empty for message impression events.
     * @param info Basic context associated with the state, such as the item purchased. info is
     * treated like a default parameter.
     */
    public static void advanceTo(String state, String info) {
        advanceTo(state, info, null);
    }

    /**
     * Advances to a particular state in your application. The string can be any value of your
     * choosing, and will show up in the dashboard. A state is a section of your app that the user is
     * currently in.
     *
     * @param state Name of the state. State may be empty for message impression events.
     * @param params Key-value pairs with metrics or data associated with the state. Parameters can be
     * strings or numbers. You can use up to 200 different parameter names in your app.
     */
    public static void advanceTo(String state, Map<String, ?> params) {
        advanceTo(state, "", params);
    }

    /**
     * Pauses the current state. You can use this if your game has a "pause" mode. You shouldn't call
     * it when someone switches out of your app because that's done automatically.
     */
    public static void pauseState() {
        if (Constants.isNoop()) {
            return;
        }
        if (!LeanplumInternal.hasCalledStart()) {
            Log.e("You cannot call pauseState before calling start");
            return;
        }

        try {
            if (LeanplumInternal.issuedStart()) {
                pauseStateInternal();
            } else {
                LeanplumInternal.addStartIssuedHandler(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            pauseStateInternal();
                        } catch (Throwable t) {
                            Util.handleException(t);
                        }
                    }
                });
            }
        } catch (Throwable t) {
            Util.handleException(t);
        }
    }

    private static void pauseStateInternal() {
        Request.post(Constants.Methods.PAUSE_STATE, new HashMap<String, Object>()).send();
    }

    /**
     * Resumes the current state.
     */
    public static void resumeState() {
        if (Constants.isNoop()) {
            return;
        }
        if (!LeanplumInternal.hasCalledStart()) {
            Log.e("You cannot call resumeState before calling start");
            return;
        }

        try {
            if (LeanplumInternal.issuedStart()) {
                resumeStateInternal();
            } else {
                LeanplumInternal.addStartIssuedHandler(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            resumeStateInternal();
                        } catch (Throwable t) {
                            Util.handleException(t);
                        }
                    }
                });
            }
        } catch (Throwable t) {
            Util.handleException(t);
        }
    }

    private static void resumeStateInternal() {
        Request.post(Constants.Methods.RESUME_STATE, new HashMap<String, Object>()).send();
    }

    /**
     * Forces content to update from the server. If variables have changed, the appropriate callbacks
     * will fire. Use sparingly as if the app is updated, you'll have to deal with potentially
     * inconsistent state or user experience.
     */
    public static void forceContentUpdate() {
        forceContentUpdate(null);
    }

    /**
     * Forces content to update from the server. If variables have changed, the appropriate callbacks
     * will fire. Use sparingly as if the app is updated, you'll have to deal with potentially
     * inconsistent state or user experience.
     *
     * @param callback The callback to invoke when the call completes from the server. The callback
     * will fire regardless of whether the variables have changed.
     */
    @SuppressWarnings("SameParameterValue")
    public static void forceContentUpdate(final VariablesChangedCallback callback) {
        if (Constants.isNoop()) {
            if (callback != null) {
                OsHandler.getInstance().post(callback);
            }
            return;
        }
        try {
            Map<String, Object> params = new HashMap<>();
            params.put(Constants.Params.INCLUDE_DEFAULTS, Boolean.toString(false));
            params.put(Constants.Params.INBOX_MESSAGES, LeanplumInbox.getInstance().messagesIds());
            Request req = Request.post(Constants.Methods.GET_VARS, params);
            req.onResponse(new Request.ResponseCallback() {
                @Override
                public void response(JSONObject response) {
                    try {
                        JSONObject lastResponse = Request.getLastResponse(response);
                        if (lastResponse == null) {
                            Log.e("No response received from the server. Please contact us to investigate.");
                        } else {
                            applyContentInResponse(lastResponse, false);
                            if (lastResponse.optBoolean(Constants.Keys.SYNC_INBOX, false)) {
                                LeanplumInbox.getInstance().downloadMessages();
                            }
                            if (lastResponse.optBoolean(Constants.Keys.LOGGING_ENABLED, false)) {
                                Constants.loggingEnabled = true;
                            }
                        }
                        if (callback != null) {
                            OsHandler.getInstance().post(callback);
                        }
                    } catch (Throwable t) {
                        Util.handleException(t);
                    }
                }
            });
            req.onError(new Request.ErrorCallback() {
                @Override
                public void error(Exception e) {
                    if (callback != null) {
                        OsHandler.getInstance().post(callback);
                    }
                }
            });
            req.sendIfConnected();
        } catch (Throwable t) {
            Util.handleException(t);
        }
    }

    /**
     * This should be your first statement in a unit test. This prevents Leanplum from communicating
     * with the server.
     */
    public static void enableTestMode() {
        Constants.isTestMode = true;
    }

    public static boolean isTestModeEnabled() {
        return Constants.isTestMode;
    }

    /**
     * This should be your first statement in a unit test. This prevents Leanplum from communicating
     * with the server.
     */
    public static void setIsTestModeEnabled(boolean isTestModeEnabled) {
        Constants.isTestMode = isTestModeEnabled;
    }

    /**
     * Gets the path for a particular resource. The resource can be overridden by the server.
     */
    public static String pathForResource(String filename) {
        if (TextUtils.isEmpty(filename)) {
            Log.e("pathForResource - Empty filename parameter provided.");
            return null;
        }

        Var fileVar = Var.defineFile(filename, filename);
        return (fileVar != null) ? fileVar.fileValue() : null;
    }

    /**
     * Traverses the variable structure with the specified path. Path components can be either strings
     * representing keys in a dictionary, or integers representing indices in a list.
     */
    public static Object objectForKeyPath(Object... components) {
        return objectForKeyPathComponents(components);
    }

    /**
     * Traverses the variable structure with the specified path. Path components can be either strings
     * representing keys in a dictionary, or integers representing indices in a list.
     */
    public static Object objectForKeyPathComponents(Object[] pathComponents) {
        try {
            return VarCache.getMergedValueFromComponentArray(pathComponents);
        } catch (Throwable t) {
            Util.handleException(t);
        }
        return null;
    }

    /**
     * Returns information about the active variants for the current user. Each variant will contain
     * an "id" key mapping to the numeric ID of the variant.
     */
    public static List<Map<String, Object>> variants() {
        List<Map<String, Object>> variants = VarCache.variants();
        if (variants == null) {
            return new ArrayList<>();
        }
        return variants;
    }

    /**
     * Returns metadata for all active in-app messages. Recommended only for debugging purposes and
     * advanced use cases.
     */
    public static Map<String, Object> messageMetadata() {
        Map<String, Object> messages = VarCache.messages();
        if (messages == null) {
            return new HashMap<>();
        }
        return messages;
    }

    /**
     * Set location manually. Calls setDeviceLocation with cell type. Best if used in after calling
     * disableLocationCollection.
     *
     * @param location Device location.
     */
    public static void setDeviceLocation(Location location) {
        setDeviceLocation(location, LeanplumLocationAccuracyType.CELL);
    }

    /**
     * Set location manually. Best if used in after calling disableLocationCollection. Useful if you
     * want to apply additional logic before sending in the location.
     *
     * @param location Device location.
     * @param type LeanplumLocationAccuracyType of the location.
     */
    public static void setDeviceLocation(Location location, LeanplumLocationAccuracyType type) {
        if (locationCollectionEnabled) {
            Log.w("Leanplum is automatically collecting device location, so there is no need to "
                    + "call setDeviceLocation. If you prefer to always set location manually, "
                    + "then call disableLocationCollection.");
        }
        LeanplumInternal.setUserLocationAttribute(location, type,
                new LeanplumInternal.locationAttributeRequestsCallback() {
                    @Override
                    public void response(boolean success) {
                        if (success) {
                            Log.d("setUserAttributes with location is successfully called");
                        }
                    }
                });
    }

    /**
     * Disable location collection by setting |locationCollectionEnabled| to false.
     */
    public static void disableLocationCollection() {
        locationCollectionEnabled = false;
    }

    /**
     * Returns whether a customer enabled location collection.
     *
     * @return The value of |locationCollectionEnabled|.
     */
    public static boolean isLocationCollectionEnabled() {
        return locationCollectionEnabled;
    }
}