Bug 1438716 - Upgrade Leanplum SDK. r?nechen draft
authorAndrei Lazar <andrei.a.lazar@softvision.ro>
Wed, 30 May 2018 17:09:30 +0300
changeset 817971 0bc1f163e9e3118d808a4e4f49479928eff27db6
parent 817970 e7429a79a2e453e0df0364d1e6cb41ee6fd21db2
child 817972 aad8ccc7c64c773d2a2336b332e57dae6c7ea7ab
child 818693 300e61abfe8c70d59941913eeb9f19379785a271
push id116231
push userplingurar@mozilla.com
push dateFri, 13 Jul 2018 19:23:06 +0000
reviewersnechen
bugs1438716
milestone63.0a1
Bug 1438716 - Upgrade Leanplum SDK. r?nechen Upgraded to version 3.0.2 from official repository while keeping the internal changes. This upgrade encapsulates some major changes as well as bug fixes. MozReview-Commit-ID: DMOEIKnw0nJ
mobile/android/thirdparty/com/leanplum/ActionContext.java
mobile/android/thirdparty/com/leanplum/Leanplum.java
mobile/android/thirdparty/com/leanplum/LeanplumCloudMessagingProvider.java
mobile/android/thirdparty/com/leanplum/LeanplumEditorMode.java
mobile/android/thirdparty/com/leanplum/LeanplumGcmProvider.java
mobile/android/thirdparty/com/leanplum/LeanplumInbox.java
mobile/android/thirdparty/com/leanplum/LeanplumInboxMessage.java
mobile/android/thirdparty/com/leanplum/LeanplumLocalPushListenerService.java
mobile/android/thirdparty/com/leanplum/LeanplumManualProvider.java
mobile/android/thirdparty/com/leanplum/LeanplumNotificationChannel.java
mobile/android/thirdparty/com/leanplum/LeanplumNotificationHelper.java
mobile/android/thirdparty/com/leanplum/LeanplumPushListenerService.java
mobile/android/thirdparty/com/leanplum/LeanplumPushReceiver.java
mobile/android/thirdparty/com/leanplum/LeanplumPushService.java
mobile/android/thirdparty/com/leanplum/LeanplumResources.java
mobile/android/thirdparty/com/leanplum/LeanplumUIEditor.java
mobile/android/thirdparty/com/leanplum/Newsfeed.java
mobile/android/thirdparty/com/leanplum/Var.java
mobile/android/thirdparty/com/leanplum/activities/LeanplumAccountAuthenticatorActivity.java
mobile/android/thirdparty/com/leanplum/activities/LeanplumActivity.java
mobile/android/thirdparty/com/leanplum/activities/LeanplumActivityGroup.java
mobile/android/thirdparty/com/leanplum/activities/LeanplumAliasActivity.java
mobile/android/thirdparty/com/leanplum/activities/LeanplumAppCompatActivity.java
mobile/android/thirdparty/com/leanplum/activities/LeanplumExpandableListActivity.java
mobile/android/thirdparty/com/leanplum/activities/LeanplumFragmentActivity.java
mobile/android/thirdparty/com/leanplum/activities/LeanplumLauncherActivity.java
mobile/android/thirdparty/com/leanplum/activities/LeanplumListActivity.java
mobile/android/thirdparty/com/leanplum/activities/LeanplumNativeActivity.java
mobile/android/thirdparty/com/leanplum/activities/LeanplumPreferenceActivity.java
mobile/android/thirdparty/com/leanplum/activities/LeanplumTabActivity.java
mobile/android/thirdparty/com/leanplum/callbacks/InboxSyncedCallback.java
mobile/android/thirdparty/com/leanplum/internal/ActionManager.java
mobile/android/thirdparty/com/leanplum/internal/CollectionUtil.java
mobile/android/thirdparty/com/leanplum/internal/Constants.java
mobile/android/thirdparty/com/leanplum/internal/FileManager.java
mobile/android/thirdparty/com/leanplum/internal/LeanplumEventCallbackManager.java
mobile/android/thirdparty/com/leanplum/internal/LeanplumEventDataManager.java
mobile/android/thirdparty/com/leanplum/internal/LeanplumInternal.java
mobile/android/thirdparty/com/leanplum/internal/LeanplumManifestHelper.java
mobile/android/thirdparty/com/leanplum/internal/Log.java
mobile/android/thirdparty/com/leanplum/internal/Registration.java
mobile/android/thirdparty/com/leanplum/internal/Request.java
mobile/android/thirdparty/com/leanplum/internal/ResourceQualifiers.java
mobile/android/thirdparty/com/leanplum/internal/Socket.java
mobile/android/thirdparty/com/leanplum/internal/SocketIOClient.java
mobile/android/thirdparty/com/leanplum/internal/Util.java
mobile/android/thirdparty/com/leanplum/internal/VarCache.java
mobile/android/thirdparty/com/leanplum/internal/WebSocketClient.java
mobile/android/thirdparty/com/leanplum/messagetemplates/BaseMessageDialog.java
mobile/android/thirdparty/com/leanplum/messagetemplates/BaseMessageOptions.java
mobile/android/thirdparty/com/leanplum/messagetemplates/HTMLOptions.java
mobile/android/thirdparty/com/leanplum/messagetemplates/HTMLTemplate.java
mobile/android/thirdparty/com/leanplum/messagetemplates/MessageTemplates.java
mobile/android/thirdparty/com/leanplum/messagetemplates/WebInterstitial.java
mobile/android/thirdparty/com/leanplum/messagetemplates/WebInterstitialOptions.java
mobile/android/thirdparty/com/leanplum/utils/BuildUtil.java
mobile/android/thirdparty/com/leanplum/utils/SharedPreferencesUtil.java
--- a/mobile/android/thirdparty/com/leanplum/ActionContext.java
+++ b/mobile/android/thirdparty/com/leanplum/ActionContext.java
@@ -19,16 +19,17 @@
  * under the License.
  */
 
 package com.leanplum;
 
 import android.support.annotation.NonNull;
 import android.text.TextUtils;
 
+import com.leanplum.callbacks.VariablesChangedCallback;
 import com.leanplum.internal.ActionManager;
 import com.leanplum.internal.BaseActionContext;
 import com.leanplum.internal.CollectionUtil;
 import com.leanplum.internal.Constants;
 import com.leanplum.internal.FileManager;
 import com.leanplum.internal.JsonConverter;
 import com.leanplum.internal.LeanplumInternal;
 import com.leanplum.internal.Log;
@@ -220,17 +221,17 @@ public class ActionContext extends BaseA
     try {
       return fillTemplate(object.toString());
     } catch (Throwable t) {
       Util.handleException(t);
       return object.toString();
     }
   }
 
-  private String fillTemplate(String value) {
+  public String fillTemplate(String value) {
     if (contextualValues == null || value == null || !value.contains("##")) {
       return value;
     }
     if (contextualValues.parameters != null) {
       Map<String, ?> parameters = contextualValues.parameters;
       for (Map.Entry<String, ?> entry : parameters.entrySet()) {
         String placeholder = "##Parameter " + entry.getKey() + "##";
         value = value.replace(placeholder, "" + entry.getValue());
@@ -360,64 +361,118 @@ public class ActionContext extends BaseA
       return;
     }
 
     // Checks if action "Chain to Existing Message" started.
     if (!isChainToExistingMessageStarted(args, name)) {
       // Try to start action "Chain to a new Message".
       Object messageAction = args.get(Constants.Values.ACTION_ARG);
       if (messageAction != null) {
-        createActionContextForMessageId(messageAction.toString(), args, messageId, name);
+        createActionContextForMessageId(messageAction.toString(), args, messageId, name, false);
       }
     }
   }
 
   /**
    * Return true if here was an action for this message and we started it.
    */
-  private boolean createActionContextForMessageId(String messageAction, Map<String, Object>
-      messageArgs, String messageId, String name) {
+  private boolean createActionContextForMessageId(String messageAction,
+      Map<String, Object> messageArgs, String messageId, String name, Boolean chained) {
     try {
-      ActionContext actionContext = new ActionContext(messageAction,
+      final ActionContext actionContext = new ActionContext(messageAction,
           messageArgs, messageId);
       actionContext.contextualValues = contextualValues;
       actionContext.preventRealtimeUpdating = preventRealtimeUpdating;
       actionContext.isRooted = isRooted;
-      actionContext.parentContext = this;
       actionContext.key = name;
-      LeanplumInternal.triggerAction(actionContext);
+      if (chained) {
+        LeanplumInternal.triggerAction(actionContext, new VariablesChangedCallback() {
+          @Override
+          public void variablesChanged() {
+            try {
+              ActionManager.getInstance().recordMessageImpression(actionContext.getMessageId());
+            } catch (Throwable t) {
+              Util.handleException(t);
+            }
+          }
+        });
+      } else {
+        actionContext.parentContext = this;
+        LeanplumInternal.triggerAction(actionContext);
+      }
       return true;
     } catch (Throwable t) {
       Util.handleException(t);
     }
     return false;
   }
 
   /**
-   * Return true if here was action "Chain to Existing Message" and we started it.
+   * Return true if there was action "Chain to Existing Message" and we started it.
    */
-  private boolean isChainToExistingMessageStarted(Map<String, Object> args, String name) {
+  private boolean isChainToExistingMessageStarted(Map<String, Object> args, final String name) {
     if (args == null) {
       return false;
     }
 
-    String messageId = (String) args.get(Constants.Values.CHAIN_MESSAGE_ARG);
-    Object actionType = args.get(Constants.Values.ACTION_ARG);
-    if (messageId != null && Constants.Values.CHAIN_MESSAGE_ACTION_NAME.equals(actionType)) {
-      Map<String, Object> messages = VarCache.messages();
-      if (messages != null && messages.containsKey(messageId)) {
-        Map<String, Object> message = CollectionUtil.uncheckedCast(messages.get(messageId));
-        if (message != null) {
-          Map<String, Object> messageArgs = CollectionUtil.uncheckedCast(
+    final String messageId = getChainedMessageId(args);
+    if (!shouldForceContentUpdateForChainedMessage(args)) {
+      return executeChainedMessage(messageId, VarCache.messages(), name);
+    } else {
+      // message may not on the device yet, so we need to fetch it.
+      Leanplum.forceContentUpdate(new VariablesChangedCallback() {
+        @Override
+        public void variablesChanged() {
+          executeChainedMessage(messageId, VarCache.messages(), name);
+        }
+      });
+    }
+    return false;
+  }
+
+  /**
+   * Return true if there is a chained message in the actionMap that is not yet loaded onto the device.
+   */
+  static boolean shouldForceContentUpdateForChainedMessage(Map<String, Object> actionMap) {
+    if (actionMap == null) {
+      return false;
+    }
+    String chainedMessageId = getChainedMessageId(actionMap);
+    if (chainedMessageId != null
+            && (VarCache.messages() == null || !VarCache.messages().containsKey(chainedMessageId))) {
+        return true;
+    }
+    return false;
+  }
+
+  /**
+   * Extract chained messageId from parent message's actionMap.  If it doesn't exist then return null.
+   */
+  static String getChainedMessageId(Map<String, Object> actionMap) {
+    if (actionMap != null) {
+      if (Constants.Values.CHAIN_MESSAGE_ACTION_NAME.equals(actionMap.get(Constants.Values.ACTION_ARG))) {
+        return (String) actionMap.get(Constants.Values.CHAIN_MESSAGE_ARG);
+      }
+    }
+    return null;
+  }
+
+
+  private boolean executeChainedMessage(String messageId, Map<String, Object> messages,
+                                        String name) {
+    if (messages == null) {
+      return false;
+    }
+    Map<String, Object> message = CollectionUtil.uncheckedCast(messages.get(messageId));
+    if (message != null) {
+      Map<String, Object> messageArgs = CollectionUtil.uncheckedCast(
               message.get(Constants.Keys.VARS));
-          Object messageAction = message.get("action");
-          return messageAction != null && createActionContextForMessageId(messageAction.toString(),
-              messageArgs, messageId, name);
-        }
-      }
+      Object messageAction = message.get("action");
+      return messageAction != null && createActionContextForMessageId(messageAction.toString(),
+              messageArgs, messageId, name, true);
     }
     return false;
   }
 
   /**
    * Prefix given event with all parent actionContext names to while filtering out the string
    * "action" (used in ExperimentVariable names but filtered out from event names).
    *
--- a/mobile/android/thirdparty/com/leanplum/Leanplum.java
+++ b/mobile/android/thirdparty/com/leanplum/Leanplum.java
@@ -35,27 +35,30 @@ import com.leanplum.ActionContext.Contex
 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.LeanplumEventDataManager;
 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 com.leanplum.utils.BuildUtil;
+import com.leanplum.utils.SharedPreferencesUtil;
 
 import org.json.JSONArray;
 import org.json.JSONObject;
 
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
@@ -81,28 +84,25 @@ public class Leanplum {
 
   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 final Object heartbeatLock = new Object();
   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() {
   }
 
   /**
@@ -240,28 +240,16 @@ public class Leanplum {
       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.
@@ -303,16 +291,30 @@ public class Leanplum {
       Log.w("setDeviceId - Empty deviceId parameter provided.");
     }
 
     customDeviceId = deviceId;
     userSpecifiedDeviceId = true;
   }
 
   /**
+   * Gets the deviceId in the current Leanplum session. This should only be called after
+   * {@link Leanplum#start}.
+   *
+   * @return String Returns the deviceId in the current Leanplum session.
+   */
+  public static String getDeviceId() {
+    if (!LeanplumInternal.hasCalledStart()) {
+      Log.e("Leanplum.start() must be called before calling getDeviceId.");
+      return null;
+    }
+    return Request.deviceId();
+  }
+
+  /**
    * 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;
@@ -327,30 +329,16 @@ public class Leanplum {
           + "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;
     }
@@ -568,17 +556,17 @@ public class Leanplum {
       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>() {
+      Util.executeAsyncTask(true, new AsyncTask<Void, Void, Void>() {
         @Override
         protected Void doInBackground(Void... params) {
           try {
             startHelper(userId, validAttributes, actuallyInBackground);
           } catch (Throwable t) {
             Util.handleException(t);
           }
           return null;
@@ -586,16 +574,17 @@ public class Leanplum {
       });
     } catch (Throwable t) {
       Util.handleException(t);
     }
   }
 
   private static void startHelper(
       String userId, final Map<String, ?> attributes, final boolean isBackground) {
+    LeanplumEventDataManager.init(context);
     LeanplumPushService.onStart();
 
     Boolean limitAdTracking = null;
     String deviceId = Request.deviceId();
     if (deviceId == null) {
       if (!userSpecifiedDeviceId && Constants.defaultDeviceId != null) {
         deviceId = Constants.defaultDeviceId;
       } else if (customDeviceId != null) {
@@ -621,26 +610,30 @@ public class Leanplum {
     if (versionName == null) {
       versionName = "";
     }
 
     TimeZone localTimeZone = TimeZone.getDefault();
     Date now = new Date();
     int timezoneOffsetSeconds = localTimeZone.getOffset(now.getTime()) / 1000;
 
+    String registrationId = SharedPreferencesUtil.getString(context,
+        Constants.Defaults.LEANPLUM_PUSH, Constants.Defaults.PROPERTY_REGISTRATION_ID);
+
     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.Params.DEVICE_PUSH_TOKEN, registrationId);
     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)) {
@@ -654,226 +647,264 @@ public class Leanplum {
     }
 
     // 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() {
+    final Request request = Request.post(Constants.Methods.START, params);
+    request.onApiResponse(new Request.ApiResponseCallback() {
       @Override
-      public void response(List<Map<String, Object>> requests, JSONObject response) {
-        Leanplum.handleApiResponse(response, requests);
+      public void response(List<Map<String, Object>> requests, JSONObject response, int countOfEvents) {
+        Leanplum.handleApiResponse(response, requests, request, countOfEvents);
       }
     });
 
     if (isBackground) {
-      req.sendEventually();
+      request.sendEventually();
     } else {
-      req.sendIfConnected();
+      request.sendIfConnected();
     }
 
     LeanplumInternal.triggerStartIssued();
   }
 
-  private static void handleApiResponse(JSONObject response, List<Map<String, Object>> requests) {
+  private static void handleApiResponse(JSONObject response, List<Map<String, Object>> requests,
+      final Request request, int countOfUnsentRequests) {
     boolean hasStartResponse = false;
     JSONObject lastStartResponse = null;
 
     // Find and handle the last start response.
     try {
-      int numResponses = Request.numResponses(response);
+      // Checks if START event inside the current batch. If database index of START event bigger
+      // then a number of count of events that we got from the database - decrease START event
+      // database index.
+      if (request.getDataBaseIndex() >= countOfUnsentRequests) {
+        request.setDataBaseIndex(request.getDataBaseIndex() - countOfUnsentRequests);
+        return;
+      }
+
+      final int responseCount = 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) {
+        Map<String, Object> currentRequest = requests.get(i);
+        if (Constants.Methods.START.equals(currentRequest.get(Constants.Params.ACTION))) {
+          if (i < responseCount) {
             lastStartResponse = Request.getResponseAt(response, i);
           }
           hasStartResponse = true;
           break;
         }
       }
     } catch (Throwable t) {
       Util.handleException(t);
     }
 
     if (hasStartResponse) {
       if (!LeanplumInternal.hasStarted()) {
+        // Set start response to null.
+        request.onApiResponse(null);
         Leanplum.handleStartResponse(lastStartResponse);
       }
     }
   }
 
-  private static void handleStartResponse(JSONObject response) {
-    boolean success = Request.isResponseSuccess(response);
-    if (!success) {
-      try {
-        LeanplumInternal.setHasStarted(true);
-        LeanplumInternal.setStartSuccessful(false);
+  private static void handleStartResponse(final JSONObject response) {
+    Util.executeAsyncTask(false, new AsyncTask<Void, Void, Void>() {
+      @Override
+      protected Void doInBackground(Void... params) {
+        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();
+            // 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);
+            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 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 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.");
+            }
 
-        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.");
+            }
 
-        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();
+            if (BuildUtil.isNotificationChannelSupported(context)) {
+              // Get notification channels and groups.
+              JSONArray notificationChannels = response.optJSONArray(
+                  Constants.Keys.NOTIFICATION_CHANNELS);
+              JSONArray notificationGroups = response.optJSONArray(
+                  Constants.Keys.NOTIFICATION_GROUPS);
+              String defaultNotificationChannel = response.optString(
+                  Constants.Keys.DEFAULT_NOTIFICATION_CHANNEL);
 
-        applyContentInResponse(response, true);
+              // Configure notification channels and groups
+              LeanplumNotificationChannel.configureNotificationGroups(
+                  context, notificationGroups);
+              LeanplumNotificationChannel.configureNotificationChannels(
+                  context, notificationChannels);
+              LeanplumNotificationChannel.configureDefaultNotificationChannel(
+                  context, defaultNotificationChannel);
+            }
 
-        VarCache.saveUserAttributes();
-        triggerStartResponse(true);
+            String token = response.optString(Constants.Keys.TOKEN, null);
+            Request.setToken(token);
+            Request.saveToken();
 
-        if (response.optBoolean(Constants.Keys.SYNC_INBOX, false)) {
-          LeanplumInbox.getInstance().downloadMessages();
-        }
+            applyContentInResponse(response, true);
 
-        if (response.optBoolean(Constants.Keys.LOGGING_ENABLED, false)) {
-          Constants.loggingEnabled = true;
-        }
+            VarCache.saveUserAttributes();
+            triggerStartResponse(true);
+
+            if (response.optBoolean(Constants.Keys.SYNC_INBOX, false)) {
+              LeanplumInbox.getInstance().downloadMessages();
+            }
 
-        // Allow bidirectional realtime variable updates.
-        if (Constants.isDevelopmentModeEnabled) {
+            if (response.optBoolean(Constants.Keys.LOGGING_ENABLED, false)) {
+              Constants.loggingEnabled = true;
+            }
 
-          final Context currentContext = (
-              LeanplumActivityHelper.currentActivity != context &&
-                  LeanplumActivityHelper.currentActivity != null) ?
-              LeanplumActivityHelper.currentActivity
-              : context;
+            // 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);
+              // 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);
+                    }
                   }
-                } catch (Throwable t) {
-                  Util.handleException(t);
-                }
+                });
+                OsHandler.getInstance().post(registerDeviceHandler);
               }
-            });
-            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)
+              // 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 builder =
+                          LeanplumNotificationHelper.getDefaultCompatNotificationBuilder(context,
+                              BuildUtil.isNotificationChannelSupported(context));
+                      if (builder == null) {
+                        return;
+                      }
+                      builder.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.");
-                }
+                      builder.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, builder.build());
+                    } catch (Throwable t) {
+                      Log.i("Device is registered.");
+                    }
+                  }
+                });
               }
-            });
-          }
 
-          boolean isRegistered = response.optBoolean(Constants.Keys.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.");
-          }
+              // 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 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 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();
-          }
+              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));
+              VarCache.setDevModeValuesFromServer(
+                  JsonConverter.mapFromJson(valuesFromCode),
+                  JsonConverter.mapFromJson(fileAttributes),
+                  JsonConverter.mapFromJson(actionDefinitions));
 
-          if (isRegistered) {
-            LeanplumInternal.onHasStartedAndRegisteredAsDeveloper();
+              if (isRegistered) {
+                LeanplumInternal.onHasStartedAndRegisteredAsDeveloper();
+              }
+            }
+            LeanplumInternal.moveToForeground();
+            startHeartbeat();
+          } catch (Throwable t) {
+            Util.handleException(t);
           }
         }
-
-        LeanplumInternal.moveToForeground();
-        startHeartbeat();
-      } catch (Throwable t) {
-        Util.handleException(t);
+        return null;
       }
-    }
+    });
   }
 
   /**
    * 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.
    */
@@ -917,19 +948,18 @@ public class Leanplum {
   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()) {
+    if (LeanplumInternal.issuedStart()) {
       pauseInternal();
     } else {
       LeanplumInternal.addStartIssuedHandler(new Runnable() {
         @Override
         public void run() {
           try {
             pauseInternal();
           } catch (Throwable t) {
@@ -938,30 +968,30 @@ public class Leanplum {
         }
       });
     }
   }
 
   private static void pauseInternal() {
     Request.post(Constants.Methods.PAUSE_SESSION, null).sendIfConnected();
     pauseHeartbeat();
+    LeanplumInternal.setIsPaused(true);
   }
 
   /**
    * 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 {
@@ -980,16 +1010,17 @@ public class Leanplum {
       LeanplumInternal.setStartedInBackground(false);
       request.sendIfConnected();
     } else {
       request.sendIfDelayed();
       LeanplumInternal.maybePerformActions("resume", null,
           LeanplumMessageMatchFilter.LEANPLUM_ACTION_FILTER_ALL, null, null);
     }
     resumeHeartbeat();
+    LeanplumInternal.setIsPaused(false);
   }
 
   /**
    * Send a heartbeat every 15 minutes while the app is running.
    */
   private static void startHeartbeat() {
     synchronized (heartbeatLock) {
       heartbeatExecutor = Executors.newSingleThreadScheduledExecutor();
@@ -1053,25 +1084,16 @@ public class Leanplum {
   /**
    * 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.
@@ -1265,22 +1287,16 @@ public class Leanplum {
    * @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
@@ -1512,16 +1528,45 @@ public class Leanplum {
    * 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);
   }
 
   /**
+   * Manually track purchase event with currency code in your application. It is advised to use
+   * {@link Leanplum#trackGooglePlayPurchase} instead for in-app purchases.
+   *
+   * @param event Name of the event.
+   * @param value The value of the event. Can be price.
+   * @param currencyCode The currency code corresponding to the price.
+   * @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 trackPurchase(final String event, double value, String currencyCode,
+      Map<String, ?> params) {
+    try {
+      if (TextUtils.isEmpty(event)) {
+        Log.w("trackPurchase - Empty event parameter provided.");
+      }
+
+      final Map<String, String> requestArgs = new HashMap<>();
+      if (!TextUtils.isEmpty(currencyCode)) {
+        requestArgs.put(Constants.Params.IAP_CURRENCY_CODE, currencyCode);
+      }
+
+      LeanplumInternal.track(event, value, null, params, requestArgs);
+    } catch (Throwable t) {
+      Log.e("trackPurchase - Failed to track purchase event.");
+      Util.handleException(t);
+    }
+  }
+
+  /**
    * 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().
    */
@@ -1879,45 +1924,46 @@ public class Leanplum {
       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) {
+            if (response == 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)) {
+              applyContentInResponse(response, false);
+              if (response.optBoolean(Constants.Keys.SYNC_INBOX, false)) {
                 LeanplumInbox.getInstance().downloadMessages();
+              } else {
+                LeanplumInbox.getInstance().triggerInboxSyncedWithStatus(true);
               }
-              if (lastResponse.optBoolean(Constants.Keys.LOGGING_ENABLED, false)) {
+              if (response.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.onError(new Request.ErrorCallback() {
+        @Override
+        public void error(Exception e) {
+          if (callback != null) {
+            OsHandler.getInstance().post(callback);
+          }
+          LeanplumInbox.getInstance().triggerInboxSyncedWithStatus(false);
+        }
+      });
       req.sendIfConnected();
     } catch (Throwable t) {
       Util.handleException(t);
     }
   }
 
   /**
    * This should be your first statement in a unit test. This prevents Leanplum from communicating
--- a/mobile/android/thirdparty/com/leanplum/LeanplumCloudMessagingProvider.java
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumCloudMessagingProvider.java
@@ -28,73 +28,85 @@ import com.leanplum.internal.Log;
 import com.leanplum.utils.SharedPreferencesUtil;
 
 /**
  * Leanplum Cloud Messaging provider.
  *
  * @author Anna Orlova
  */
 abstract class LeanplumCloudMessagingProvider {
-  static final String PUSH_REGISTRATION_SERVICE = "com.leanplum.LeanplumPushRegistrationService";
-  static final String PUSH_RECEIVER = "com.leanplum.LeanplumPushReceiver";
+  private static String registrationId;
 
-  private static String registrationId;
+  /**
+   * Gets the registration Id associated with current messaging provider.
+   *
+   * @return Registration Id.
+   */
+  static String getCurrentRegistrationId() {
+    return registrationId;
+  }
+
+  /**
+   * Sends the registration ID to the server over HTTP.
+   */
+  private static void sendRegistrationIdToBackend(String registrationId) {
+    Leanplum.setRegistrationId(registrationId);
+  }
 
   /**
    * Registration app for Cloud Messaging.
    *
    * @return String - registration id for app.
    */
   public abstract String getRegistrationId();
 
   /**
-   * Verifies that Android Manifest is set up correctly.
+   * Whether Messaging Provider is initialized correctly.
    *
-   * @return true If Android Manifest is set up correctly.
+   * @return True if provider is initialized, false otherwise.
    */
-  public abstract boolean isManifestSetUp();
+  public abstract boolean isInitialized();
 
-  public abstract boolean isInitialized();
+  /**
+   * Whether app manifest is setup correctly.
+   *
+   * @return True if manifest is setup, false otherwise.
+   */
+  public abstract boolean isManifestSetup();
 
   /**
    * Unregister from cloud messaging.
    */
   public abstract void unregister();
 
-  static String getCurrentRegistrationId() {
-    return registrationId;
-  }
-
+  /**
+   * Callback should be invoked when Registration ID is received from provider.
+   *
+   * @param context The application context.
+   * @param registrationId Registration Id.
+   */
   void onRegistrationIdReceived(Context context, String registrationId) {
     if (registrationId == null) {
       Log.w("Registration ID is undefined.");
       return;
     }
     LeanplumCloudMessagingProvider.registrationId = registrationId;
     // Check if received push notification token is different from stored one and send new one to
     // server.
     if (!LeanplumCloudMessagingProvider.registrationId.equals(SharedPreferencesUtil.getString(
         context, Constants.Defaults.LEANPLUM_PUSH, Constants.Defaults.PROPERTY_REGISTRATION_ID))) {
       Log.i("Device registered for push notifications with registration token", registrationId);
       storePreferences(context.getApplicationContext());
+      sendRegistrationIdToBackend(LeanplumCloudMessagingProvider.registrationId);
     }
-    // Send push token on every launch for not missed token when user force quit the app.
-    sendRegistrationIdToBackend(LeanplumCloudMessagingProvider.registrationId);
-  }
-
-  /**
-   * Sends the registration ID to the server over HTTP.
-   */
-  private static void sendRegistrationIdToBackend(String registrationId) {
-    Leanplum.setRegistrationId(registrationId);
   }
 
   /**
    * Stores the registration ID in the application's {@code SharedPreferences}.
    *
-   * @param context application's context.
+   * @param context The application context.
    */
   public void storePreferences(Context context) {
     Log.v("Saving the registration ID in the shared preferences.");
     SharedPreferencesUtil.setString(context, Constants.Defaults.LEANPLUM_PUSH,
         Constants.Defaults.PROPERTY_REGISTRATION_ID, registrationId);
   }
 }
--- a/mobile/android/thirdparty/com/leanplum/LeanplumEditorMode.java
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumEditorMode.java
@@ -20,17 +20,20 @@
  */
 
 package com.leanplum;
 
 /**
  * Enum for describing the Editor Mode.
  *
  * @author Ben Marten
+ * @deprecated {@link LeanplumEditorMode} will be made private in future releases, since it is not
+ * intended to be public API.
  */
+@Deprecated
 public enum LeanplumEditorMode {
   LP_EDITOR_MODE_INTERFACE(0),
   LP_EDITOR_MODE_EVENT(1);
 
   private final int value;
 
   /**
    * Creates a new EditorMode enum with given value.
--- a/mobile/android/thirdparty/com/leanplum/LeanplumGcmProvider.java
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumGcmProvider.java
@@ -42,45 +42,36 @@ import java.util.Collections;
  */
 class LeanplumGcmProvider extends LeanplumCloudMessagingProvider {
   private static final String ERROR_TIMEOUT = "TIMEOUT";
   private static final String ERROR_INVALID_SENDER = "INVALID_SENDER";
   private static final String ERROR_AUTHENTICATION_FAILED = "AUTHENTICATION_FAILED";
   private static final String ERROR_PHONE_REGISTRATION_ERROR = "PHONE_REGISTRATION_ERROR";
   private static final String ERROR_TOO_MANY_REGISTRATIONS = "TOO_MANY_REGISTRATIONS";
 
-  private static final String SEND_PERMISSION = "com.google.android.c2dm.permission.SEND";
-  private static final String RECEIVE_PERMISSION = "com.google.android.c2dm.permission.RECEIVE";
-  private static final String RECEIVE_ACTION = "com.google.android.c2dm.intent.RECEIVE";
-  private static final String REGISTRATION_ACTION = "com.google.android.c2dm.intent.REGISTRATION";
-  private static final String INSTANCE_ID_ACTION = "com.google.android.gms.iid.InstanceID";
-  private static final String PUSH_LISTENER_SERVICE = "com.leanplum.LeanplumPushListenerService";
-  private static final String GCM_RECEIVER = "com.google.android.gms.gcm.GcmReceiver";
-  private static final String PUSH_INSTANCE_ID_SERVICE =
-      "com.leanplum.LeanplumPushInstanceIDService";
-
   private static String senderIds;
 
+  /**
+   * Sets GCM sender id.
+   *
+   * @param senderId Sender id.
+   */
   static void setSenderId(String senderId) {
     senderIds = senderId;
   }
 
-  /**
-   * Stores the GCM sender ID in the application's {@code SharedPreferences}.
-   *
-   * @param context application's context.
-   */
   @Override
   public void storePreferences(Context context) {
     super.storePreferences(context);
     Log.v("Saving GCM sender ID");
     SharedPreferencesUtil.setString(context, Constants.Defaults.LEANPLUM_PUSH,
         Constants.Defaults.PROPERTY_SENDER_IDS, senderIds);
   }
 
+  @Override
   public String getRegistrationId() {
     String registrationId = null;
     try {
       InstanceID instanceID = InstanceID.getInstance(Leanplum.getContext());
       if (senderIds == null || instanceID == null) {
         Log.w("There was a problem setting up GCM, please make sure you follow instructions " +
             "on how to set it up.");
         return null;
@@ -112,59 +103,69 @@ class LeanplumGcmProvider extends Leanpl
       Log.w("There was a problem setting up GCM, please make sure you follow instructions " +
           "on how to set it up. Please verify that you are using correct version of " +
           "Google Play Services and Android Support Library v4.");
       Util.handleException(t);
     }
     return registrationId;
   }
 
+  @Override
   public boolean isInitialized() {
     return senderIds != null || getCurrentRegistrationId() != null;
   }
 
-  public boolean isManifestSetUp() {
+  @Override
+  public boolean isManifestSetup() {
     Context context = Leanplum.getContext();
     if (context == null) {
       return false;
     }
-
-    boolean hasPermissions = LeanplumManifestHelper.checkPermission(RECEIVE_PERMISSION, false, true)
-        && (LeanplumManifestHelper.checkPermission(context.getPackageName() +
-        ".gcm.permission.C2D_MESSAGE", true, false) || LeanplumManifestHelper.checkPermission(
-        context.getPackageName() + ".permission.C2D_MESSAGE", true, true));
+    try {
+      boolean hasPermissions = LeanplumManifestHelper.checkPermission(LeanplumManifestHelper.GCM_RECEIVE_PERMISSION, false, true)
+          && (LeanplumManifestHelper.checkPermission(context.getPackageName() + ".gcm.permission.C2D_MESSAGE", true, false)
+          || LeanplumManifestHelper.checkPermission(context.getPackageName() + ".permission.C2D_MESSAGE", true, true));
 
-    boolean hasGcmReceiver = LeanplumManifestHelper.checkComponent(
-        LeanplumManifestHelper.getReceivers(), GCM_RECEIVER, true, SEND_PERMISSION,
-        Arrays.asList(RECEIVE_ACTION, REGISTRATION_ACTION), context.getPackageName());
-    boolean hasPushReceiver = LeanplumManifestHelper.checkComponent(
-        LeanplumManifestHelper.getReceivers(), PUSH_RECEIVER, false, null,
-        Collections.singletonList(PUSH_LISTENER_SERVICE), null);
+      boolean hasGcmReceiver = LeanplumManifestHelper.checkComponent(
+          LeanplumManifestHelper.ApplicationComponent.RECEIVER, LeanplumManifestHelper.GCM_RECEIVER,
+          true, LeanplumManifestHelper.GCM_SEND_PERMISSION, Arrays.asList(LeanplumManifestHelper.GCM_RECEIVE_ACTION,
+              LeanplumManifestHelper.GCM_REGISTRATION_ACTION), context.getPackageName());
+      boolean hasPushReceiver = LeanplumManifestHelper.checkComponent(LeanplumManifestHelper.ApplicationComponent.RECEIVER,
+          LeanplumManifestHelper.LP_PUSH_RECEIVER, false, null,
+          Collections.singletonList(LeanplumManifestHelper.LP_PUSH_LISTENER_SERVICE), context.getPackageName());
+
+      boolean hasReceivers = hasGcmReceiver && hasPushReceiver;
 
-    boolean hasReceivers = hasGcmReceiver && hasPushReceiver;
+      boolean hasPushListenerService = LeanplumManifestHelper.checkComponent(
+          LeanplumManifestHelper.ApplicationComponent.SERVICE,
+          LeanplumManifestHelper.LP_PUSH_LISTENER_SERVICE, false, null,
+          Collections.singletonList(LeanplumManifestHelper.GCM_RECEIVE_ACTION), context.getPackageName());
+      boolean hasInstanceIdService = LeanplumManifestHelper.checkComponent(
+          LeanplumManifestHelper.ApplicationComponent.SERVICE,
+          LeanplumManifestHelper.LP_PUSH_INSTANCE_ID_SERVICE, false, null,
+          Collections.singletonList(LeanplumManifestHelper.GCM_INSTANCE_ID_ACTION), context.getPackageName());
+      boolean hasRegistrationService = LeanplumManifestHelper.checkComponent(
+          LeanplumManifestHelper.ApplicationComponent.SERVICE,
+          LeanplumManifestHelper.LP_PUSH_REGISTRATION_SERVICE, false, null, null, context.getPackageName());
 
-    boolean hasPushListenerService = LeanplumManifestHelper.checkComponent(
-        LeanplumManifestHelper.getServices(), PUSH_LISTENER_SERVICE, false, null,
-        Collections.singletonList(RECEIVE_ACTION), null);
-    boolean hasPushInstanceIDService = LeanplumManifestHelper.checkComponent(
-        LeanplumManifestHelper.getServices(), PUSH_INSTANCE_ID_SERVICE, false, null,
-        Collections.singletonList(INSTANCE_ID_ACTION), null);
-    boolean hasPushRegistrationService = LeanplumManifestHelper.checkComponent(
-        LeanplumManifestHelper.getServices(), PUSH_REGISTRATION_SERVICE, false, null, null, null);
+      boolean hasServices = hasPushListenerService && hasInstanceIdService && hasRegistrationService;
 
-    boolean hasServices = hasPushListenerService && hasPushInstanceIDService
-        && hasPushRegistrationService;
-
-    return hasPermissions && hasReceivers && hasServices;
+      if (hasPermissions && hasReceivers && hasServices) {
+        Log.i("Google Cloud Messaging is setup correctly.");
+        return true;
+      }
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+    Log.e("Failed to setup Google Cloud Messaging, check your manifest configuration.");
+    return false;
   }
 
-  /**
-   * Unregister from GCM.
-   */
+  @Override
   public void unregister() {
     try {
       InstanceID.getInstance(Leanplum.getContext()).deleteInstanceID();
-      Log.i("Application was unregistred from GCM.");
+      Log.i("Application was unregistered from GCM.");
     } catch (Exception e) {
       Log.e("Failed to unregister from GCM.");
     }
   }
 }
--- a/mobile/android/thirdparty/com/leanplum/LeanplumInbox.java
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumInbox.java
@@ -20,25 +20,27 @@
  */
 
 package com.leanplum;
 
 import android.content.Context;
 import android.content.SharedPreferences;
 
 import com.leanplum.callbacks.InboxChangedCallback;
+import com.leanplum.callbacks.InboxSyncedCallback;
 import com.leanplum.callbacks.VariablesChangedCallback;
 import com.leanplum.internal.AESCrypt;
 import com.leanplum.internal.CollectionUtil;
 import com.leanplum.internal.Constants;
 import com.leanplum.internal.JsonConverter;
 import com.leanplum.internal.Log;
 import com.leanplum.internal.OsHandler;
 import com.leanplum.internal.Request;
 import com.leanplum.internal.Util;
+import com.leanplum.utils.SharedPreferencesUtil;
 
 import org.json.JSONObject;
 
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.Date;
 import java.util.HashMap;
@@ -49,52 +51,171 @@ import java.util.Map;
 import java.util.Set;
 
 /**
  * Inbox class.
  *
  * @author Aleksandar Gyorev, Anna Orlova
  */
 public class LeanplumInbox {
+  private static LeanplumInbox instance = new LeanplumInbox();
+
+  static Set<String> downloadedImageUrls;
   static boolean isInboxImagePrefetchingEnabled = true;
-  /**
-   * Should be like this until Newsfeed is removed for backward compatibility.
-   */
-  static Newsfeed instance = new Newsfeed();
-  static Set<String> downloadedImageUrls;
 
-  // Inbox properties.
   private int unreadCount;
   private Map<String, LeanplumInboxMessage> messages;
   private boolean didLoad = false;
-  private List<InboxChangedCallback> changedCallbacks;
-  private Object updatingLock = new Object();
 
-  LeanplumInbox() {
+  private final List<InboxChangedCallback> changedCallbacks;
+  private final List<InboxSyncedCallback> syncedCallbacks;
+  private final Object updatingLock = new Object();
+
+  protected LeanplumInbox() {
     this.unreadCount = 0;
     this.messages = new HashMap<>();
     this.didLoad = false;
     this.changedCallbacks = new ArrayList<>();
+    this.syncedCallbacks = new ArrayList<>();
     downloadedImageUrls = new HashSet<>();
   }
 
   /**
+   * Disable prefetching images.
+   */
+  public static void disableImagePrefetching() {
+    isInboxImagePrefetchingEnabled = false;
+  }
+
+  /**
+   * Returns the number of all inbox messages on the device.
+   */
+  public int count() {
+    return messages.size();
+  }
+
+  /**
+   * Returns the number of the unread inbox messages on the device.
+   */
+  public int unreadCount() {
+    return unreadCount;
+  }
+
+  /**
+   * Returns the identifiers of all inbox messages on the device sorted in ascending
+   * chronological order, i.e. the id of the oldest message is the first one, and the most recent
+   * one is the last one in the array.
+   */
+  public List<String> messagesIds() {
+    List<String> messageIds = new ArrayList<>(messages.keySet());
+    try {
+      Collections.sort(messageIds, new Comparator<String>() {
+        @Override
+        public int compare(String firstMessageId, String secondMessageId) {
+          // Message that is null will be moved to the back of the list.
+          LeanplumInboxMessage firstMessage = messageForId(firstMessageId);
+          if (firstMessage == null) {
+            return -1;
+          }
+          LeanplumInboxMessage secondMessage = messageForId(secondMessageId);
+          if (secondMessage == null) {
+            return 1;
+          }
+          // Message with null date will be moved to the back of the list.
+          Date firstDate = firstMessage.getDeliveryTimestamp();
+          if (firstDate == null) {
+            return -1;
+          }
+          Date secondDate = secondMessage.getDeliveryTimestamp();
+          if (secondDate == null) {
+            return 1;
+          }
+          return firstDate.compareTo(secondDate);
+        }
+      });
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+    return messageIds;
+  }
+
+  /**
+   * Returns a List containing all of the inbox messages sorted chronologically ascending (i.e.
+   * the oldest first and the newest last).
+   */
+  public List<LeanplumInboxMessage> allMessages() {
+    return allMessages(new ArrayList<LeanplumInboxMessage>());
+  }
+
+  /**
+   * Returns a List containing all of the unread inbox messages sorted chronologically ascending
+   * (i.e. the oldest first and the newest last).
+   */
+  public List<LeanplumInboxMessage> unreadMessages() {
+    return unreadMessages(new ArrayList<LeanplumInboxMessage>());
+  }
+
+  /**
+   * Returns the inbox messages associated with the given getMessageId identifier.
+   */
+  public LeanplumInboxMessage messageForId(String messageId) {
+    return messages.get(messageId);
+  }
+
+  /**
+   * Add a callback for when the inbox receives new values from the server.
+   */
+  public void addChangedHandler(InboxChangedCallback handler) {
+    synchronized (changedCallbacks) {
+      changedCallbacks.add(handler);
+    }
+    if (this.didLoad) {
+      handler.inboxChanged();
+    }
+  }
+
+  /**
+   * Removes a inbox changed callback.
+   */
+  public void removeChangedHandler(InboxChangedCallback handler) {
+    synchronized (changedCallbacks) {
+      changedCallbacks.remove(handler);
+    }
+  }
+
+  /**
+   * Add a callback for when the forceContentUpdate was called.
+   *
+   * @param handler InboxSyncedCallback callback that need to be added.
+   */
+  public void addSyncedHandler(InboxSyncedCallback handler) {
+    synchronized (syncedCallbacks) {
+      syncedCallbacks.add(handler);
+    }
+  }
+
+
+  /**
+   * Removes a inbox synced callback.
+   *
+   * @param handler InboxSyncedCallback callback that need to be removed.
+   */
+  public void removeSyncedHandler(InboxSyncedCallback handler) {
+    synchronized (syncedCallbacks) {
+      syncedCallbacks.remove(handler);
+    }
+  }
+
+  /**
    * Static 'getInstance' method.
    */
   static LeanplumInbox getInstance() {
     return instance;
   }
 
-  /**
-   * Disable prefetching images.
-   */
-  public static void disableImagePrefetching() {
-    isInboxImagePrefetchingEnabled = false;
-  }
-
   boolean isInboxImagePrefetchingEnabled() {
     return isInboxImagePrefetchingEnabled;
   }
 
   void updateUnreadCount(int unreadCount) {
     this.unreadCount = unreadCount;
     save();
     triggerChanged();
@@ -141,16 +262,29 @@ public class LeanplumInbox {
   void triggerChanged() {
     synchronized (changedCallbacks) {
       for (InboxChangedCallback callback : changedCallbacks) {
         OsHandler.getInstance().post(callback);
       }
     }
   }
 
+  /**
+   * Trigger InboxSyncedCallback with status of forceContentUpdate.
+   * @param success True if forceContentUpdate was successful.
+   */
+  void triggerInboxSyncedWithStatus(boolean success) {
+    synchronized (changedCallbacks) {
+      for (InboxSyncedCallback callback : syncedCallbacks) {
+        callback.setSuccess(success);
+        OsHandler.getInstance().post(callback);
+      }
+    }
+  }
+
   void load() {
     if (Constants.isNoop()) {
       return;
     }
     Context context = Leanplum.getContext();
     SharedPreferences defaults = context.getSharedPreferences(
         "__leanplum__", Context.MODE_PRIVATE);
     if (Request.token() == null) {
@@ -193,41 +327,36 @@ public class LeanplumInbox {
     }
     Context context = Leanplum.getContext();
     SharedPreferences defaults = context.getSharedPreferences(
         "__leanplum__", Context.MODE_PRIVATE);
     SharedPreferences.Editor editor = defaults.edit();
     Map<String, Object> messages = new HashMap<>();
     for (Map.Entry<String, LeanplumInboxMessage> entry : this.messages.entrySet()) {
       String messageId = entry.getKey();
-      NewsfeedMessage newsfeedMessage = entry.getValue();
-      Map<String, Object> data = newsfeedMessage.toJsonMap();
+      LeanplumInboxMessage inboxMessage = entry.getValue();
+      Map<String, Object> data = inboxMessage.toJsonMap();
       messages.put(messageId, data);
     }
     String messagesJson = JsonConverter.toJson(messages);
     AESCrypt aesContext = new AESCrypt(Request.appId(), Request.token());
     editor.putString(Constants.Defaults.INBOX_KEY, aesContext.encrypt(messagesJson));
-    try {
-      editor.apply();
-    } catch (NoSuchMethodError e) {
-      editor.commit();
-    }
+    SharedPreferencesUtil.commitChanges(editor);
   }
 
   void downloadMessages() {
     if (Constants.isNoop()) {
       return;
     }
 
-    Request req = Request.post(Constants.Methods.GET_INBOX_MESSAGES, null);
+    final Request req = Request.post(Constants.Methods.GET_INBOX_MESSAGES, null);
     req.onResponse(new Request.ResponseCallback() {
       @Override
-      public void response(JSONObject responses) {
+      public void response(JSONObject response) {
         try {
-          JSONObject response = Request.getLastResponse(responses);
           if (response == null) {
             Log.e("No inbox response received from the server.");
             return;
           }
 
           JSONObject messagesDict = response.optJSONObject(Constants.Keys.INBOX_MESSAGES);
           if (messagesDict == null) {
             Log.e("No inbox messages found in the response from the server.", response);
@@ -258,148 +387,71 @@ public class LeanplumInbox {
                 unreadCount++;
               }
               messages.put(messageId, message);
             }
           }
 
           if (!willDownladImages) {
             update(messages, unreadCount, true);
+            triggerInboxSyncedWithStatus(true);
             return;
           }
 
           final int totalUnreadCount = unreadCount;
           Leanplum.addOnceVariablesChangedAndNoDownloadsPendingHandler(
               new VariablesChangedCallback() {
                 @Override
                 public void variablesChanged() {
                   update(messages, totalUnreadCount, true);
+                  triggerInboxSyncedWithStatus(true);
                 }
               });
         } catch (Throwable t) {
+          triggerInboxSyncedWithStatus(false);
           Util.handleException(t);
         }
       }
     });
+    req.onError(new Request.ErrorCallback() {
+      @Override
+      public void error(Exception e) {
+        triggerInboxSyncedWithStatus(false);
+      }
+    });
     req.sendIfConnected();
   }
 
   /**
-   * Returns the number of all inbox messages on the device.
-   */
-  public int count() {
-    return messages.size();
-  }
-
-  /**
-   * Returns the number of the unread inbox messages on the device.
-   */
-  public int unreadCount() {
-    return unreadCount;
-  }
-
-  /**
-   * Returns the identifiers of all inbox messages on the device sorted in ascending
-   * chronological order, i.e. the id of the oldest message is the first one, and the most recent
-   * one is the last one in the array.
-   */
-  public List<String> messagesIds() {
-    List<String> messageIds = new ArrayList<>(messages.keySet());
-    try {
-      Collections.sort(messageIds, new Comparator<String>() {
-        @Override
-        public int compare(String firstMessage, String secondMessage) {
-          Date firstDate = messageForId(firstMessage).getDeliveryTimestamp();
-          Date secondDate = messageForId(secondMessage).getDeliveryTimestamp();
-          return firstDate.compareTo(secondDate);
-        }
-      });
-    } catch (Throwable t) {
-      Util.handleException(t);
-    }
-    return messageIds;
-  }
-
-  /**
-   * Have to stay as is because of backward compatibility + generics super-sub incompatibility
-   * (http://www.angelikalanger.com/GenericsFAQ/FAQSections/ParameterizedTypes.html#Topic2).
-   * <p>
-   * Returns a List containing all of the newsfeed messages sorted chronologically ascending (i.e.
+   * Returns a List containing all of the inbox messages sorted chronologically ascending (i.e.
    * the oldest first and the newest last).
    */
-  public List<NewsfeedMessage> allMessages() {
-    return allMessages(new ArrayList<NewsfeedMessage>());
-  }
-
-  /**
-   * Have to stay as is because of backward compatibility + generics super-sub incompatibility
-   * (http://www.angelikalanger.com/GenericsFAQ/FAQSections/ParameterizedTypes.html#Topic2).
-   * <p>
-   * Returns a List containing all of the unread newsfeed messages sorted chronologically ascending
-   * (i.e. the oldest first and the newest last).
-   */
-  public List<NewsfeedMessage> unreadMessages() {
-    return unreadMessages(new ArrayList<NewsfeedMessage>());
-  }
-
-  /**
-   * Suggested workaround for generics to be used with {@link LeanplumInbox#getInstance()} although
-   * only LeanplumInboxMessage could be an instance of NewsfeedMessage.
-   */
-  private <T extends NewsfeedMessage> List<T> allMessages(List<T> messages) {
+  private List<LeanplumInboxMessage> allMessages(List<LeanplumInboxMessage> messages) {
     if (messages == null) {
       messages = new ArrayList<>();
     }
     try {
       for (String messageId : messagesIds()) {
-        messages.add((T) messageForId(messageId));
+        messages.add(messageForId(messageId));
       }
     } catch (Throwable t) {
       Util.handleException(t);
     }
     return messages;
   }
 
   /**
-   * Suggested workaround for generics to be used with {@link LeanplumInbox#getInstance()} although
-   * only LeanplumInboxMessage could be an instance of NewsfeedMessage.
+   * Returns a List containing all of the unread inbox messages sorted chronologically ascending
+   * (i.e. the oldest first and the newest last).
    */
-  private <T extends NewsfeedMessage> List<T> unreadMessages(List<T> unreadMessages) {
+  private List<LeanplumInboxMessage> unreadMessages(List<LeanplumInboxMessage> unreadMessages) {
     if (unreadMessages == null) {
       unreadMessages = new ArrayList<>();
     }
     List<LeanplumInboxMessage> messages = allMessages(null);
     for (LeanplumInboxMessage message : messages) {
       if (!message.isRead()) {
-        unreadMessages.add((T) message);
+        unreadMessages.add(message);
       }
     }
     return unreadMessages;
   }
-
-  /**
-   * Returns the inbox messages associated with the given getMessageId identifier.
-   */
-  public LeanplumInboxMessage messageForId(String messageId) {
-    return messages.get(messageId);
-  }
-
-  /**
-   * Add a callback for when the inbox receives new values from the server.
-   */
-  public void addChangedHandler(InboxChangedCallback handler) {
-    synchronized (changedCallbacks) {
-      changedCallbacks.add(handler);
-    }
-    if (this.didLoad) {
-      handler.inboxChanged();
-    }
-  }
-
-  /**
-   * Removes a inbox changed callback.
-   */
-  public void removeChangedHandler(InboxChangedCallback handler) {
-    synchronized (changedCallbacks) {
-      changedCallbacks.remove(handler);
-    }
-  }
-}
+}
\ No newline at end of file
--- a/mobile/android/thirdparty/com/leanplum/LeanplumInboxMessage.java
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumInboxMessage.java
@@ -21,78 +21,63 @@
 
 package com.leanplum;
 
 import android.net.Uri;
 import android.text.TextUtils;
 
 import com.leanplum.internal.CollectionUtil;
 import com.leanplum.internal.Constants;
+import com.leanplum.internal.JsonConverter;
 import com.leanplum.internal.Log;
+import com.leanplum.internal.Request;
 import com.leanplum.internal.Util;
 
 import org.json.JSONObject;
 
 import java.io.File;
+import java.util.Date;
+import java.util.HashMap;
 import java.util.Map;
 
 import static com.leanplum.internal.FileManager.DownloadFileResult;
 import static com.leanplum.internal.FileManager.fileExistsAtPath;
 import static com.leanplum.internal.FileManager.fileValue;
 import static com.leanplum.internal.FileManager.maybeDownloadFile;
 
 /**
  * LeanplumInboxMessage class.
  *
  * @author Anna Orlova
  */
-public class LeanplumInboxMessage extends NewsfeedMessage {
+public class LeanplumInboxMessage {
+  private String messageId;
+  private Long deliveryTimestamp;
+  private Long expirationTimestamp;
+  private boolean isRead;
+  private ActionContext context;
   private String imageUrl;
   private String imageFileName;
 
   private LeanplumInboxMessage(String messageId, Long deliveryTimestamp, Long expirationTimestamp,
       boolean isRead, ActionContext context) {
-    super(messageId, deliveryTimestamp, expirationTimestamp, isRead, context);
+    this.messageId = messageId;
+    this.deliveryTimestamp = deliveryTimestamp;
+    this.expirationTimestamp = expirationTimestamp;
+    this.isRead = isRead;
+    this.context = context;
     imageUrl = context.stringNamed(Constants.Keys.INBOX_IMAGE);
     if (imageUrl != null) {
       try {
         imageFileName = Util.sha256(imageUrl);
       } catch (Exception ignored) {
       }
     }
   }
 
-  static LeanplumInboxMessage createFromJsonMap(String messageId, Map<String, Object> map) {
-    Map<String, Object> messageData = CollectionUtil.uncheckedCast(map.get(Constants.Keys
-        .MESSAGE_DATA));
-    Long deliveryTimestamp = CollectionUtil.uncheckedCast(map.get(Constants.Keys
-        .DELIVERY_TIMESTAMP));
-    Long expirationTimestamp = CollectionUtil.uncheckedCast(map.get(Constants.Keys
-        .EXPIRATION_TIMESTAMP));
-    Boolean isRead = CollectionUtil.uncheckedCast(map.get(Constants.Keys.IS_READ));
-    return constructMessage(messageId, deliveryTimestamp, expirationTimestamp,
-        isRead != null ? isRead : false, messageData);
-  }
-
-  static LeanplumInboxMessage constructMessage(String messageId, Long deliveryTimestamp,
-      Long expirationTimestamp, boolean isRead, Map<String, Object> actionArgs) {
-    if (!isValidMessageId(messageId)) {
-      Log.e("Malformed inbox messageId: " + messageId);
-      return null;
-    }
-
-    String[] messageIdParts = messageId.split("##");
-    ActionContext context = new ActionContext((String) actionArgs.get(Constants.Values.ACTION_ARG),
-        actionArgs, messageIdParts[0]);
-    context.preventRealtimeUpdating();
-    context.update();
-    return new LeanplumInboxMessage(messageId, deliveryTimestamp, expirationTimestamp, isRead,
-        context);
-  }
-
   /**
    * Returns the image file path of the inbox message. Can be null.
    */
   public String getImageFilePath() {
     String path = fileValue(imageFileName);
     if (fileExistsAtPath(path)) {
       return new File(path).getAbsolutePath();
     }
@@ -121,40 +106,190 @@ public class LeanplumInboxMessage extend
   }
 
   /**
    * Returns the data of the inbox message. Advanced use only.
    */
   public JSONObject getData() {
     JSONObject object = null;
     try {
-      String dataString = getContext().stringNamed(Constants.Keys.DATA);
-      if (!TextUtils.isEmpty(dataString)) {
-        object = new JSONObject(dataString);
-      }
-    } catch (Exception e) {
+      Map<String, ?> mapData =
+          CollectionUtil.uncheckedCast(getContext().objectNamed(Constants.Keys.DATA));
+      object = JsonConverter.mapToJsonObject(mapData);
+    } catch (Throwable t) {
       Log.w("Unable to parse JSONObject for Data field of inbox message.");
     }
     return object;
   }
 
   /**
+   * Returns the message identifier of the newsfeed message.
+   */
+  public String getMessageId() {
+    return messageId;
+  }
+
+  /**
+   * Returns the title of the inbox message.
+   */
+  public String getTitle() {
+    return context.stringNamed(Constants.Keys.TITLE);
+  }
+
+  /**
+   * Returns the subtitle of the inbox message.
+   */
+  public String getSubtitle() {
+    return context.stringNamed(Constants.Keys.SUBTITLE);
+  }
+
+  /**
+   * Returns the delivery timestamp of the inbox message,
+   * or null if delivery timestamp is not present.
+   */
+  public Date getDeliveryTimestamp() {
+    if (deliveryTimestamp == null) {
+      return null;
+    }
+    return new Date(deliveryTimestamp);
+  }
+
+
+  /**
+   * Return the expiration timestamp of the inbox message.
+   */
+  public Date getExpirationTimestamp() {
+    if (expirationTimestamp == null) {
+      return null;
+    }
+    return new Date(expirationTimestamp);
+  }
+
+  /**
+   * Returns 'true' if the inbox message is read.
+   */
+  public boolean isRead() {
+    return isRead;
+  }
+
+  /**
+   * Read the inbox message, marking it as read and invoking its open action.
+   */
+  public void read() {
+    try {
+      if (Constants.isNoop()) {
+        return;
+      }
+
+      if (!this.isRead) {
+        setIsRead(true);
+
+        int unreadCount = LeanplumInbox.getInstance().unreadCount() - 1;
+        LeanplumInbox.getInstance().updateUnreadCount(unreadCount);
+
+        Map<String, Object> params = new HashMap<>();
+        params.put(Constants.Params.INBOX_MESSAGE_ID, messageId);
+        Request req = Request.post(Constants.Methods.MARK_INBOX_MESSAGE_AS_READ,
+            params);
+        req.send();
+      }
+      this.context.runTrackedActionNamed(Constants.Values.DEFAULT_PUSH_ACTION);
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+  }
+
+  /**
+   * Remove the inbox message from the inbox.
+   */
+  public void remove() {
+    try {
+      LeanplumInbox.getInstance().removeMessage(messageId);
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+  }
+
+  static LeanplumInboxMessage createFromJsonMap(String messageId, Map<String, Object> map) {
+    Map<String, Object> messageData = CollectionUtil.uncheckedCast(map.get(Constants.Keys
+        .MESSAGE_DATA));
+    Long deliveryTimestamp = CollectionUtil.uncheckedCast(map.get(Constants.Keys
+        .DELIVERY_TIMESTAMP));
+    Long expirationTimestamp = CollectionUtil.uncheckedCast(map.get(Constants.Keys
+        .EXPIRATION_TIMESTAMP));
+    Boolean isRead = CollectionUtil.uncheckedCast(map.get(Constants.Keys.IS_READ));
+    return constructMessage(messageId, deliveryTimestamp, expirationTimestamp,
+        isRead != null ? isRead : false, messageData);
+  }
+
+  static LeanplumInboxMessage constructMessage(String messageId, Long deliveryTimestamp,
+      Long expirationTimestamp, boolean isRead, Map<String, Object> actionArgs) {
+    if (!isValidMessageId(messageId)) {
+      Log.e("Malformed inbox messageId: " + messageId);
+      return null;
+    }
+
+    String[] messageIdParts = messageId.split("##");
+    ActionContext context = new ActionContext((String) actionArgs.get(Constants.Values.ACTION_ARG),
+        actionArgs, messageIdParts[0]);
+    context.preventRealtimeUpdating();
+    context.update();
+    return new LeanplumInboxMessage(messageId, deliveryTimestamp, expirationTimestamp, isRead,
+        context);
+  }
+
+
+  /**
    * Download image if prefetching is enabled.
    * Uses {@link LeanplumInbox#downloadedImageUrls} to make sure we don't call fileExist method
    * multiple times for same URLs.
    *
    * @return Boolean True if the image will be downloaded, otherwise false.
    */
-  Boolean downloadImageIfPrefetchingEnabled() {
+  boolean downloadImageIfPrefetchingEnabled() {
     if (!LeanplumInbox.isInboxImagePrefetchingEnabled) {
       return false;
     }
 
     if (TextUtils.isEmpty(imageUrl) || LeanplumInbox.downloadedImageUrls.contains(imageUrl)) {
       return false;
     }
 
     DownloadFileResult result = maybeDownloadFile(true, imageFileName,
         imageUrl, imageUrl, null);
     LeanplumInbox.downloadedImageUrls.add(imageUrl);
     return DownloadFileResult.DOWNLOADING == result;
   }
+
+  Map<String, Object> toJsonMap() {
+    Map<String, Object> map = new HashMap<>();
+    map.put(Constants.Keys.DELIVERY_TIMESTAMP, this.deliveryTimestamp);
+    map.put(Constants.Keys.EXPIRATION_TIMESTAMP, this.expirationTimestamp);
+    map.put(Constants.Keys.MESSAGE_DATA, this.actionArgs());
+    map.put(Constants.Keys.IS_READ, this.isRead());
+    return map;
+  }
+
+  boolean isActive() {
+    if (expirationTimestamp == null) {
+      return true;
+    }
+
+    Date now = new Date();
+    return now.before(new Date(expirationTimestamp));
+  }
+
+  private static boolean isValidMessageId(String messageId) {
+    return messageId.split("##").length == 2;
+  }
+
+  ActionContext getContext() {
+    return context;
+  }
+
+  private Map<String, Object> actionArgs() {
+    return context.getArgs();
+  }
+
+  private void setIsRead(boolean isRead) {
+    this.isRead = isRead;
+  }
 }
--- a/mobile/android/thirdparty/com/leanplum/LeanplumLocalPushListenerService.java
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumLocalPushListenerService.java
@@ -42,16 +42,16 @@ public class LeanplumLocalPushListenerSe
   @Override
   protected void onHandleIntent(Intent intent) {
     try {
       if (intent == null) {
         Log.e("The intent cannot be null");
         return;
       }
       Bundle extras = intent.getExtras();
-      if (!extras.isEmpty() && extras.containsKey(Constants.Keys.PUSH_MESSAGE_TEXT)) {
+      if (extras != null && extras.containsKey(Constants.Keys.PUSH_MESSAGE_TEXT)) {
         LeanplumPushService.handleNotification(this, extras);
       }
     } catch (Throwable t) {
       Util.handleException(t);
     }
   }
 }
--- a/mobile/android/thirdparty/com/leanplum/LeanplumManualProvider.java
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumManualProvider.java
@@ -28,24 +28,27 @@ import android.content.Context;
  *
  * @author Anna Orlova
  */
 public class LeanplumManualProvider extends LeanplumCloudMessagingProvider {
   LeanplumManualProvider(Context context, String registrationId) {
     onRegistrationIdReceived(context, registrationId);
   }
 
+  @Override
   public String getRegistrationId() {
     return getCurrentRegistrationId();
   }
 
+  @Override
   public boolean isInitialized() {
     return true;
   }
 
-  public boolean isManifestSetUp() {
+  @Override
+  public boolean isManifestSetup() {
     return true;
   }
 
+  @Override
   public void unregister() {
-
   }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumNotificationChannel.java
@@ -0,0 +1,623 @@
+package com.leanplum;
+
+/*
+ * Copyright 2017, 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.
+ */
+
+import android.annotation.TargetApi;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationChannelGroup;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.text.TextUtils;
+
+import com.leanplum.internal.CollectionUtil;
+import com.leanplum.internal.Constants;
+import com.leanplum.internal.JsonConverter;
+import com.leanplum.internal.Log;
+import com.leanplum.internal.Util;
+import com.leanplum.utils.BuildUtil;
+import com.leanplum.utils.SharedPreferencesUtil;
+
+import org.json.JSONArray;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Push notification channels manipulation utilities. Please use this class for Android O and upper
+ * with targetSdkVersion 26 and upper.
+ *
+ * @author Anna Orlova
+ */
+@TargetApi(26)
+class LeanplumNotificationChannel {
+
+    /**
+     * Configures notification channels.
+     *
+     * @param context  The application context.
+     * @param channels Notification channels.
+     */
+    static void configureNotificationChannels(Context context, JSONArray channels) {
+        try {
+            if (context == null || channels == null) {
+                return;
+            }
+
+            List<HashMap<String, Object>> definedChannels = retrieveNotificationChannels(context);
+            List<HashMap<String, Object>> notificationChannels = JsonConverter.listFromJson(channels);
+
+            if (definedChannels != null && notificationChannels != null) {
+                // Find difference between present and newly received channels.
+                definedChannels.removeAll(notificationChannels);
+                // Delete channels that are no longer present.
+                for (HashMap<String, Object> channel : definedChannels) {
+                    if (channel == null) {
+                        continue;
+                    }
+                    String id = (String) channel.get("id");
+                    deleteNotificationChannel(context, id);
+                }
+            }
+
+            // Store newly received channels.
+            storeNotificationChannels(context, notificationChannels);
+
+            // Configure channels.
+            if (notificationChannels != null) {
+                for (HashMap<String, Object> channel : notificationChannels) {
+                    if (channel == null) {
+                        continue;
+                    }
+                    createNotificationChannel(context, channel);
+                }
+            }
+        } catch (Throwable t) {
+            Util.handleException(t);
+        }
+    }
+
+    /**
+     * Configures default notification channel, which will be used when channel isn't specified on
+     * Android O.
+     *
+     * @param context The application context.
+     * @param channel Default channel details.
+     */
+    static void configureDefaultNotificationChannel(Context context, String channel) {
+        try {
+            if (context == null || TextUtils.isEmpty(channel)) {
+                return;
+            }
+            storeDefaultNotificationChannel(context, channel);
+        } catch (Throwable t) {
+            Util.handleException(t);
+        }
+    }
+
+    /**
+     * Configures notification groups.
+     *
+     * @param context The application context.
+     * @param groups  Notification groups.
+     */
+    static void configureNotificationGroups(Context context, JSONArray groups) {
+        try {
+            if (context == null || groups == null) {
+                return;
+            }
+            List<HashMap<String, Object>> definedGroups = retrieveNotificationGroups(context);
+            List<HashMap<String, Object>> notificationGroups = JsonConverter.listFromJson(groups);
+
+            if (definedGroups != null && notificationGroups != null) {
+                definedGroups.removeAll(notificationGroups);
+
+                // Delete groups that are no longer present.
+                for (HashMap<String, Object> group : definedGroups) {
+                    if (group == null) {
+                        continue;
+                    }
+                    String id = (String) group.get("id");
+                    deleteNotificationGroup(context, id);
+                }
+            }
+
+            // Store newly received groups.
+            storeNotificationGroups(context, notificationGroups);
+
+            // Configure groups.
+            if (notificationGroups != null) {
+                for (HashMap<String, Object> group : notificationGroups) {
+                    if (group == null) {
+                        continue;
+                    }
+                    createNotificationGroup(context, group);
+                }
+            }
+        } catch (Throwable t) {
+            Util.handleException(t);
+        }
+    }
+
+    /**
+     * Retrieves stored notification channels.
+     *
+     * @param context The application context.
+     * @return List of stored channels or null.
+     */
+    private static List<HashMap<String, Object>> retrieveNotificationChannels(Context context) {
+        if (context == null) {
+            return null;
+        }
+        try {
+            SharedPreferences preferences = context.getSharedPreferences(Constants.Defaults.LEANPLUM,
+                    Context.MODE_PRIVATE);
+            String jsonChannels = preferences.getString(Constants.Defaults.NOTIFICATION_CHANNELS_KEY,
+                    null);
+            if (jsonChannels == null) {
+                return null;
+            }
+            JSONArray json = new JSONArray(jsonChannels);
+            return JsonConverter.listFromJson(json);
+        } catch (Exception e) {
+            Log.e("Failed to convert notification channels json.");
+        }
+        return null;
+    }
+
+    /**
+     * Retrieves stored default notification channel id.
+     *
+     * @param context The Application context.
+     * @return Id of default channel.
+     */
+    private static String retrieveDefaultNotificationChannel(Context context) {
+        if (context == null) {
+            return null;
+        }
+        try {
+            SharedPreferences preferences = context.getSharedPreferences(Constants.Defaults.LEANPLUM,
+                    Context.MODE_PRIVATE);
+            return preferences.getString(Constants.Defaults.DEFAULT_NOTIFICATION_CHANNEL_KEY, null);
+        } catch (Exception e) {
+            Log.e("Failed to convert default notification channels json.");
+        }
+        return null;
+    }
+
+    /**
+     * Retrieves stored notification groups.
+     *
+     * @param context The application context.
+     * @return List of stored groups or null.
+     */
+    private static List<HashMap<String, Object>> retrieveNotificationGroups(Context context) {
+        if (context == null) {
+            return null;
+        }
+        try {
+            SharedPreferences preferences = context.getSharedPreferences(Constants.Defaults.LEANPLUM,
+                    Context.MODE_PRIVATE);
+            String jsonChannels = preferences.getString(Constants.Defaults.NOTIFICATION_GROUPS_KEY, null);
+            if (jsonChannels == null) {
+                return null;
+            }
+            JSONArray json = new JSONArray(jsonChannels);
+            return JsonConverter.listFromJson(json);
+        } catch (Exception e) {
+            Log.e("Failed to convert notification channels json.");
+        }
+        return null;
+    }
+
+    /**
+     * Stores notification channels.
+     *
+     * @param context  The application context.
+     * @param channels Channels to store.
+     */
+    private static void storeNotificationChannels(Context context,
+                                                  List<HashMap<String, Object>> channels) {
+        if (context == null || channels == null) {
+            return;
+        }
+        SharedPreferences preferences = context.getSharedPreferences(Constants.Defaults.LEANPLUM,
+                Context.MODE_PRIVATE);
+        SharedPreferences.Editor editor = preferences.edit();
+
+        String jsonChannels = new JSONArray(channels).toString();
+        editor.putString(Constants.Defaults.NOTIFICATION_CHANNELS_KEY, jsonChannels);
+
+        SharedPreferencesUtil.commitChanges(editor);
+    }
+
+    /**
+     * Stores default notification channel id.
+     *
+     * @param context   The application context.
+     * @param channelId Channel Id to store.
+     */
+    private static void storeDefaultNotificationChannel(Context context, String channelId) {
+        if (context == null || channelId == null) {
+            return;
+        }
+        SharedPreferences preferences = context.getSharedPreferences(Constants.Defaults.LEANPLUM,
+                Context.MODE_PRIVATE);
+        SharedPreferences.Editor editor = preferences.edit();
+
+        editor.putString(Constants.Defaults.DEFAULT_NOTIFICATION_CHANNEL_KEY, channelId);
+
+        SharedPreferencesUtil.commitChanges(editor);
+    }
+
+    /**
+     * Stores notification groups.
+     *
+     * @param context The application context.
+     * @param groups  Groups to store.
+     */
+    private static void storeNotificationGroups(Context context,
+                                                List<HashMap<String, Object>> groups) {
+        if (context == null || groups == null) {
+            return;
+        }
+
+        SharedPreferences preferences = context.getSharedPreferences(Constants.Defaults.LEANPLUM,
+                Context.MODE_PRIVATE);
+        SharedPreferences.Editor editor = preferences.edit();
+
+        String jsonGroups = new JSONArray(groups).toString();
+        editor.putString(Constants.Defaults.NOTIFICATION_GROUPS_KEY, jsonGroups);
+
+        SharedPreferencesUtil.commitChanges(editor);
+    }
+
+    /**
+     * Creates push notification channel.
+     *
+     * @param context The application context.
+     * @param channel Map containing channel details.
+     * @return Id of newly created channel or null if it fails.
+     */
+    static String createNotificationChannel(Context context, Map<String, Object> channel) {
+        try {
+            if (context == null || channel == null) {
+                return null;
+            }
+            NotificationChannelData data = new NotificationChannelData(channel);
+            createNotificationChannel(context,
+                    data.id,
+                    data.name,
+                    data.importance,
+                    data.description,
+                    data.groupId,
+                    data.enableLights,
+                    data.lightColor,
+                    data.enableVibration,
+                    data.vibrationPattern,
+                    data.lockscreenVisibility,
+                    data.bypassDnd,
+                    data.showBadge);
+            return data.id;
+        } catch (Exception e) {
+            Log.e("Failed to create notification channel.");
+        }
+        return null;
+    }
+
+
+    /**
+     * Create push notification channel with provided id, name and importance of the channel.
+     * You can call this method also when you need to update the name or description of a channel, at
+     * this case you should use original channel id.
+     *
+     * @param context              The application context.
+     * @param channelId            The id of the channel.
+     * @param channelName          The user-visible name of the channel.
+     * @param channelImportance    The importance of the channel. Use value from 0 to 5. 3 is default.
+     *                             Read more https://developer.android.com/reference/android/app/NotificationManager.html#IMPORTANCE_DEFAULT
+     *                             Once you create a notification channel, only the system can modify its importance.
+     * @param channelDescription   The user-visible description of the channel.
+     * @param groupId              The id of push notification channel group.
+     * @param enableLights         True if lights enable for this channel.
+     * @param lightColor           Light color for notifications posted to this channel, if the device supports
+     *                             this feature.
+     * @param enableVibration      True if vibration enable for this channel.
+     * @param vibrationPattern     Vibration pattern for notifications posted to this channel.
+     * @param lockscreenVisibility How to be shown on the lockscreen.
+     * @param bypassDnd            Whether should notification bypass DND.
+     * @param showBadge            Whether should notification show badge.
+     */
+    private static void createNotificationChannel(Context context, String channelId, String
+            channelName, int channelImportance, String channelDescription, String groupId, boolean
+                                                          enableLights, int lightColor, boolean enableVibration, long[] vibrationPattern, int
+                                                          lockscreenVisibility, boolean bypassDnd, boolean showBadge) {
+        if (context == null || TextUtils.isEmpty(channelId)) {
+            return;
+        }
+        if (BuildUtil.isNotificationChannelSupported(context)) {
+            try {
+                NotificationManager notificationManager =
+                        context.getSystemService(NotificationManager.class);
+                if (notificationManager == null) {
+                    Log.e("Notification manager is null");
+                    return;
+                }
+
+                NotificationChannel notificationChannel = new NotificationChannel(channelId,
+                        channelName, channelImportance);
+                if (!TextUtils.isEmpty(channelDescription)) {
+                    notificationChannel.setDescription(channelDescription);
+                }
+                if (enableLights) {
+                    notificationChannel.enableLights(true);
+                    notificationChannel.setLightColor(lightColor);
+                }
+                if (enableVibration) {
+                    notificationChannel.enableVibration(true);
+                    // Workaround for https://issuetracker.google.com/issues/63427588
+                    if (vibrationPattern != null && vibrationPattern.length != 0) {
+                        notificationChannel.setVibrationPattern(vibrationPattern);
+                    }
+                }
+                if (!TextUtils.isEmpty(groupId)) {
+                    notificationChannel.setGroup(groupId);
+                }
+                notificationChannel.setLockscreenVisibility(lockscreenVisibility);
+                notificationChannel.setBypassDnd(bypassDnd);
+                notificationChannel.setShowBadge(showBadge);
+
+                notificationManager.createNotificationChannel(notificationChannel);
+            } catch (Throwable t) {
+                Util.handleException(t);
+            }
+        }
+    }
+
+    /**
+     * Delete push notification channel.
+     *
+     * @param context   The application context.
+     * @param channelId The id of the channel.
+     */
+    private static void deleteNotificationChannel(Context context, String channelId) {
+        if (context == null) {
+            return;
+        }
+        if (BuildUtil.isNotificationChannelSupported(context)) {
+            try {
+                NotificationManager notificationManager =
+                        (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+                if (notificationManager == null) {
+                    Log.e("Notification manager is null");
+                    return;
+                }
+
+                notificationManager.deleteNotificationChannel(channelId);
+            } catch (Throwable t) {
+                Util.handleException(t);
+            }
+        }
+    }
+
+    /**
+     * Create push notification channel group.
+     *
+     * @param context The application context.
+     * @param group   Map containing group details.
+     * @return Id of newly created group or null if its failed.
+     */
+    private static String createNotificationGroup(Context context, Map<String, Object> group) {
+        if (context == null || group == null) {
+            return null;
+        }
+        NotificationGroupData data = new NotificationGroupData(group);
+        createNotificationGroup(context, data.id, data.name);
+        return data.id;
+    }
+
+    /**
+     * Create push notification channel group.
+     *
+     * @param context   The application context.
+     * @param groupId   The id of the group.
+     * @param groupName The user-visible name of the group.
+     */
+    private static void createNotificationGroup(Context context, String groupId, String groupName) {
+        if (context == null || TextUtils.isEmpty(groupId)) {
+            return;
+        }
+        if (BuildUtil.isNotificationChannelSupported(context)) {
+            try {
+                NotificationManager notificationManager =
+                        (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+                if (notificationManager == null) {
+                    Log.e("Notification manager is null");
+                    return;
+                }
+
+                notificationManager.createNotificationChannelGroup(new NotificationChannelGroup(groupId,
+                        groupName));
+            } catch (Throwable t) {
+                Util.handleException(t);
+            }
+        }
+    }
+
+    /**
+     * Delete push notification channel group.
+     *
+     * @param context The application context.
+     * @param groupId The id of the channel.
+     */
+    private static void deleteNotificationGroup(Context context, String groupId) {
+        if (context == null || TextUtils.isEmpty(groupId)) {
+            return;
+        }
+        if (BuildUtil.isNotificationChannelSupported(context)) {
+            try {
+                NotificationManager notificationManager =
+                        (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+                if (notificationManager == null) {
+                    Log.e("Notification manager is null");
+                    return;
+                }
+
+                notificationManager.deleteNotificationChannelGroup(groupId);
+            } catch (Throwable t) {
+                Util.handleException(t);
+            }
+        }
+    }
+
+    /**
+     * Get list of NotificationChannel.
+     *
+     * @param context The application context.
+     * @return Returns all notification channels belonging to the calling package.
+     */
+    static List<NotificationChannel> getNotificationChannels(Context context) {
+        if (BuildUtil.isNotificationChannelSupported(context)) {
+            NotificationManager notificationManager =
+                    (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+            if (notificationManager == null) {
+                Log.e("Notification manager is null");
+                return null;
+            }
+            return notificationManager.getNotificationChannels();
+        }
+        return null;
+    }
+
+    /**
+     * Get default notification channel id to be used.
+     *
+     * @param context The application context.
+     * @return Id of default notification channel.
+     */
+    static String getDefaultNotificationChannelId(Context context) {
+        if (BuildUtil.isNotificationChannelSupported(context)) {
+            return retrieveDefaultNotificationChannel(context);
+        }
+        return null;
+    }
+
+    /**
+     * Get list of Notification groups.
+     *
+     * @param context The application context.
+     * @return Returns all notification groups.
+     */
+    static List<NotificationChannelGroup> getNotificationGroups(Context context) {
+        if (BuildUtil.isNotificationChannelSupported(context)) {
+            NotificationManager notificationManager = (NotificationManager) context.getSystemService(
+                    Context.NOTIFICATION_SERVICE);
+            if (notificationManager == null) {
+                Log.e("Cannot get Notification Channel Groups, notificationManager is null.");
+                return null;
+            }
+            return notificationManager.getNotificationChannelGroups();
+        }
+        return null;
+    }
+
+    /**
+     * Helper class holding Notification Channel data parsed from JSON.
+     */
+    @TargetApi(26)
+    private static class NotificationChannelData {
+        String id;
+        String name;
+        String description;
+        String groupId;
+        int importance = NotificationManager.IMPORTANCE_DEFAULT;
+        boolean enableLights = false;
+        int lightColor = 0;
+        boolean enableVibration = false;
+        long[] vibrationPattern = null;
+        int lockscreenVisibility = Notification.VISIBILITY_PUBLIC;
+        boolean bypassDnd = false;
+        boolean showBadge = false;
+
+        NotificationChannelData(Map<String, Object> channel) {
+            id = (String) channel.get("id");
+            name = (String) channel.get("name");
+            description = (String) channel.get("description");
+            groupId = (String) channel.get("groupId");
+
+            importance = (int) CollectionUtil.getOrDefault(channel, "importance",
+                    importance);
+            enableLights = (boolean) CollectionUtil.getOrDefault(channel, "enable_lights", enableLights);
+            lightColor = (int) CollectionUtil.getOrDefault(channel, "light_color", lightColor);
+            enableVibration = (boolean) CollectionUtil.getOrDefault(channel, "enable_vibration",
+                    enableVibration);
+            lockscreenVisibility = (int) CollectionUtil.getOrDefault(channel, "lockscreen_visibility",
+                    lockscreenVisibility);
+            bypassDnd = (boolean) CollectionUtil.getOrDefault(channel, "bypass_dnd", bypassDnd);
+            showBadge = (boolean) CollectionUtil.getOrDefault(channel, "show_badge", showBadge);
+
+            try {
+                List<Number> pattern = CollectionUtil.uncheckedCast(
+                        CollectionUtil.getOrDefault(channel, "vibration_pattern", null));
+                if (pattern != null) {
+                    vibrationPattern = new long[pattern.size()];
+                    Iterator<Number> iterator = pattern.iterator();
+                    for (int i = 0; i < vibrationPattern.length; i++) {
+                        Number next = iterator.next();
+                        if (next != null) {
+                            vibrationPattern[i] = next.longValue();
+                        }
+                    }
+                }
+            } catch (Exception e) {
+                Log.w("Failed to parse vibration pattern.");
+            }
+
+            // Sanity checks.
+            if (importance < NotificationManager.IMPORTANCE_NONE &&
+                    importance > NotificationManager.IMPORTANCE_MAX) {
+                importance = NotificationManager.IMPORTANCE_DEFAULT;
+            }
+            if (lockscreenVisibility < Notification.VISIBILITY_SECRET &&
+                    lockscreenVisibility > Notification.VISIBILITY_PUBLIC) {
+                lockscreenVisibility = Notification.VISIBILITY_PUBLIC;
+            }
+        }
+    }
+
+    /**
+     * Helper class holding Notification Group data parsed from JSON.
+     */
+    @TargetApi(26)
+    private static class NotificationGroupData {
+        String id;
+        String name;
+
+        NotificationGroupData(Map<String, Object> group) {
+            id = (String) group.get("id");
+            name = (String) group.get("name");
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumNotificationHelper.java
@@ -0,0 +1,322 @@
+package com.leanplum;
+
+/*
+ * Copyright 2017, 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.
+ */
+
+import android.annotation.TargetApi;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.AdaptiveIconDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.RequiresApi;
+import android.support.v4.app.NotificationCompat;
+import android.text.TextUtils;
+import android.util.TypedValue;
+import android.widget.RemoteViews;
+
+import com.leanplum.internal.Constants;
+import com.leanplum.internal.JsonConverter;
+import com.leanplum.internal.Log;
+import com.leanplum.utils.BuildUtil;
+
+import java.util.Map;
+
+/**
+ * LeanplumNotificationHelper helper class for push notifications.
+ *
+ * @author Anna Orlova
+ */
+class LeanplumNotificationHelper {
+
+    private static final int BIGPICTURE_TEXT_TOP_PADDING = -14;
+    private static final int BIGPICTURE_TEXT_SIZE = 14;
+    private static final String LEANPLUM_DEFAULT_PUSH_ICON = "leanplum_default_push_icon";
+
+    /**
+     * If notification channels are supported this method will try to create
+     * NotificationCompat.Builder with default notification channel if default channel id is provided.
+     * If notification channels not supported this method will return NotificationCompat.Builder for
+     * context.
+     *
+     * @param context                        The application context.
+     * @param isNotificationChannelSupported True if notification channels are supported.
+     * @return NotificationCompat.Builder for provided context or null.
+     */
+    // NotificationCompat.Builder(Context context) constructor was deprecated in API level 26.
+    @SuppressWarnings("deprecation")
+    static NotificationCompat.Builder getDefaultCompatNotificationBuilder(Context context,
+                                                                          boolean isNotificationChannelSupported) {
+        if (!isNotificationChannelSupported) {
+            return new NotificationCompat.Builder(context);
+        }
+        String channelId = LeanplumNotificationChannel.getDefaultNotificationChannelId(context);
+        if (!TextUtils.isEmpty(channelId)) {
+            return new NotificationCompat.Builder(context, channelId);
+        } else {
+            Log.w("Failed to post notification, there are no notification channels configured.");
+            return null;
+        }
+    }
+
+    /**
+     * If notification channels are supported this method will try to create
+     * Notification.Builder with default notification channel if default channel id is provided.
+     * If notification channels not supported this method will return Notification.Builder for
+     * context.
+     *
+     * @param context                        The application context.
+     * @param isNotificationChannelSupported True if notification channels are supported.
+     * @return Notification.Builder for provided context or null.
+     */
+    // Notification.Builder(Context context) constructor was deprecated in API level 26.
+    @TargetApi(Build.VERSION_CODES.O)
+    @SuppressWarnings("deprecation")
+    private static Notification.Builder getDefaultNotificationBuilder(Context context,
+                                                                      boolean isNotificationChannelSupported) {
+        if (!isNotificationChannelSupported) {
+            return new Notification.Builder(context);
+        }
+        String channelId = LeanplumNotificationChannel.getDefaultNotificationChannelId(context);
+        if (!TextUtils.isEmpty(channelId)) {
+            return new Notification.Builder(context, channelId);
+        } else {
+            Log.w("Failed to post notification, there are no notification channels configured.");
+            return null;
+        }
+    }
+
+    /**
+     * If notification channels are supported this method will try to create a channel with
+     * information from the message if it doesn't exist and return NotificationCompat.Builder for this
+     * channel. In the case where no channel information inside the message, we will try to get a
+     * channel with default channel id. If notification channels not supported this method will return
+     * NotificationCompat.Builder for context.
+     *
+     * @param context The application context.
+     * @param message Push notification Bundle.
+     * @return NotificationCompat.Builder or null.
+     */
+    // NotificationCompat.Builder(Context context) constructor was deprecated in API level 26.
+    @SuppressWarnings("deprecation")
+    static NotificationCompat.Builder getNotificationCompatBuilder(Context context, Bundle message) {
+        NotificationCompat.Builder builder = null;
+        // If we are targeting API 26, try to find supplied channel to post notification.
+        if (BuildUtil.isNotificationChannelSupported(context)) {
+            try {
+                String channel = message.getString("lp_channel");
+                if (!TextUtils.isEmpty(channel)) {
+                    // Create channel if it doesn't exist and post notification to that channel.
+                    Map<String, Object> channelDetails = JsonConverter.fromJson(channel);
+                    String channelId = LeanplumNotificationChannel.createNotificationChannel(context,
+                            channelDetails);
+                    if (!TextUtils.isEmpty(channelId)) {
+                        builder = new NotificationCompat.Builder(context, channelId);
+                    } else {
+                        Log.w("Failed to post notification to specified channel.");
+                    }
+                } else {
+                    // If channel isn't supplied, try to look up for default channel.
+                    builder = LeanplumNotificationHelper.getDefaultCompatNotificationBuilder(context, true);
+                }
+            } catch (Exception e) {
+                Log.e("Failed to post notification to specified channel.");
+            }
+        } else {
+            builder = new NotificationCompat.Builder(context);
+        }
+        return builder;
+    }
+
+    /**
+     * If notification channels are supported this method will try to create a channel with
+     * information from the message if it doesn't exist and return Notification.Builder for this
+     * channel. In the case where no channel information inside the message, we will try to get a
+     * channel with default channel id. If notification channels not supported this method will return
+     * Notification.Builder for context.
+     *
+     * @param context The application context.
+     * @param message Push notification Bundle.
+     * @return Notification.Builder or null.
+     */
+    static Notification.Builder getNotificationBuilder(Context context, Bundle message) {
+        Notification.Builder builder = null;
+        // If we are targeting API 26, try to find supplied channel to post notification.
+        if (BuildUtil.isNotificationChannelSupported(context)) {
+            try {
+                String channel = message.getString("lp_channel");
+                if (!TextUtils.isEmpty(channel)) {
+                    // Create channel if it doesn't exist and post notification to that channel.
+                    Map<String, Object> channelDetails = JsonConverter.fromJson(channel);
+                    String channelId = LeanplumNotificationChannel.createNotificationChannel(context,
+                            channelDetails);
+                    if (!TextUtils.isEmpty(channelId)) {
+                        builder = new Notification.Builder(context, channelId);
+                    } else {
+                        Log.w("Failed to post notification to specified channel.");
+                    }
+                } else {
+                    // If channel isn't supplied, try to look up for default channel.
+                    builder = LeanplumNotificationHelper.getDefaultNotificationBuilder(context, true);
+                }
+            } catch (Exception e) {
+                Log.e("Failed to post notification to specified channel.");
+            }
+        } else {
+            builder = new Notification.Builder(context);
+        }
+        return builder;
+    }
+
+    /**
+     * Gets Notification.Builder with 2 lines at BigPictureStyle notification text.
+     *
+     * @param context                           The application context.
+     * @param message                           Push notification Bundle.
+     * @param contentIntent                     PendingIntent.
+     * @param title                             String with title for push notification.
+     * @param messageText                       String with text for push notification.
+     * @param bigPicture                        Bitmap for BigPictureStyle notification.
+     * @param defaultNotificationIconResourceId int Resource id for default push notification icon.
+     * @return Notification.Builder or null.
+     */
+    static Notification.Builder getNotificationBuilder(Context context, Bundle message,
+                                                       PendingIntent contentIntent, String title, final String messageText, Bitmap bigPicture,
+                                                       int defaultNotificationIconResourceId) {
+        if (Build.VERSION.SDK_INT < 16) {
+            return null;
+        }
+        Notification.Builder notificationBuilder =
+                getNotificationBuilder(context, message);
+        if (defaultNotificationIconResourceId == 0) {
+            notificationBuilder.setSmallIcon(context.getApplicationInfo().icon);
+        } else {
+            notificationBuilder.setSmallIcon(defaultNotificationIconResourceId);
+        }
+        notificationBuilder.setContentTitle(title)
+                .setContentText(messageText);
+        Notification.BigPictureStyle bigPictureStyle = new Notification.BigPictureStyle() {
+            @Override
+            protected RemoteViews getStandardView(int layoutId) {
+                RemoteViews remoteViews = super.getStandardView(layoutId);
+                // Modifications of stanxdard push RemoteView.
+                try {
+                    int id = Resources.getSystem().getIdentifier("text", "id", "android");
+                    remoteViews.setBoolean(id, "setSingleLine", false);
+                    remoteViews.setInt(id, "setLines", 2);
+                    if (Build.VERSION.SDK_INT < 23) {
+                        // Make text smaller.
+                        remoteViews.setViewPadding(id, 0, BIGPICTURE_TEXT_TOP_PADDING, 0, 0);
+                        remoteViews.setTextViewTextSize(id, TypedValue.COMPLEX_UNIT_SP, BIGPICTURE_TEXT_SIZE);
+                    }
+                } catch (Throwable throwable) {
+                    Log.e("Cannot modify push notification layout.");
+                }
+                return remoteViews;
+            }
+        };
+
+        bigPictureStyle.bigPicture(bigPicture)
+                .setBigContentTitle(title)
+                .setSummaryText(message.getString(Constants.Keys.PUSH_MESSAGE_TEXT));
+        notificationBuilder.setStyle(bigPictureStyle);
+
+        if (Build.VERSION.SDK_INT >= 24) {
+            // By default we cannot reach getStandardView method on API>=24. I we call
+            // createBigContentView, Android will call getStandardView method and we can get
+            // modified RemoteView.
+            try {
+                RemoteViews remoteView = notificationBuilder.createBigContentView();
+                if (remoteView != null) {
+                    // We need to set received RemoteView as a custom big content view.
+                    notificationBuilder.setCustomBigContentView(remoteView);
+                }
+            } catch (Throwable t) {
+                Log.e("Cannot modify push notification layout.", t);
+            }
+        }
+
+        notificationBuilder.setAutoCancel(true);
+        notificationBuilder.setContentIntent(contentIntent);
+        return notificationBuilder;
+    }
+
+    /**
+     * Checks a possibility to create icon drawable from current app icon.
+     *
+     * @param context Current application context.
+     * @return boolean True if it is possible to create a drawable from current app icon.
+     */
+    private static boolean canCreateIconDrawable(Context context) {
+        try {
+            // Try to create icon drawable.
+            Drawable drawable = AdaptiveIconDrawable.createFromStream(
+                    context.getResources().openRawResource(context.getApplicationInfo().icon),
+                    "applicationInfo.icon");
+            // If there was no crash, we still need to check for null.
+            if (drawable != null) {
+                return true;
+            }
+        } catch (Throwable ignored) {
+        }
+        return false;
+    }
+
+    /**
+     * Validation of Application icon for small icon on push notification.
+     *
+     * @param context Current application context.
+     * @return boolean True if application icon can be used for small icon on push notification.
+     */
+    static boolean isApplicationIconValid(Context context) {
+        if (context == null) {
+            return false;
+        }
+
+        // TODO: Potentially there should be checked for Build.VERSION.SDK_INT != 26, but we need to
+        // TODO: confirm that adaptive icon works well on 27, before to change it.
+        if (Build.VERSION.SDK_INT < 26) {
+            return true;
+        }
+
+        return canCreateIconDrawable(context);
+    }
+
+    /**
+     * Gets default push notification resource id for LEANPLUM_DEFAULT_PUSH_ICON in drawable.
+     *
+     * @param context Current application context.
+     * @return int Resource id.
+     */
+    static int getDefaultPushNotificationIconResourceId(Context context) {
+        try {
+            Resources resources = context.getResources();
+            return resources.getIdentifier(LEANPLUM_DEFAULT_PUSH_ICON, "drawable",
+                    context.getPackageName());
+        } catch (Throwable ignored) {
+            return 0;
+        }
+    }
+}
\ No newline at end of file
--- a/mobile/android/thirdparty/com/leanplum/LeanplumPushListenerService.java
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumPushListenerService.java
@@ -46,9 +46,9 @@ public class LeanplumPushListenerService
       if (data.containsKey(Keys.PUSH_MESSAGE_TEXT)) {
         LeanplumPushService.handleNotification(this, data);
       }
       Log.i("Received: " + data.toString());
     } catch (Throwable t) {
       Util.handleException(t);
     }
   }
-}
\ No newline at end of file
+}
--- a/mobile/android/thirdparty/com/leanplum/LeanplumPushReceiver.java
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumPushReceiver.java
@@ -20,16 +20,18 @@
  */
 
 package com.leanplum;
 
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 
+import com.leanplum.internal.Constants;
+import com.leanplum.internal.LeanplumManifestHelper;
 import com.leanplum.internal.Log;
 import com.leanplum.internal.Util;
 
 /**
  * Handles push notification intents, for example, by tracking opens and performing the open
  * action.
  *
  * @author Aleksandar Gyorev
@@ -38,14 +40,29 @@ public class LeanplumPushReceiver extend
 
   @Override
   public void onReceive(Context context, Intent intent) {
     try {
       if (intent == null) {
         Log.e("Received a null intent.");
         return;
       }
-      LeanplumPushService.openNotification(context, intent.getExtras());
+      // Parse manifest and pull metadata which contains client broadcast receiver class.
+      String receiver = LeanplumManifestHelper.parseNotificationMetadata();
+      // If receiver isn't found we will open up notification with default activity
+      if (receiver == null) {
+        Log.d("Custom broadcast receiver class not set, using default one.");
+        LeanplumPushService.openNotification(context, intent);
+      } else {
+        Log.d("Custom broadcast receiver class found, using it to handle push notifications.");
+        // Forward Intent to a client broadcast receiver.
+        Intent forwardIntent = new Intent();
+        // Add action to be able to differentiate between multiple intents.
+        forwardIntent.setAction(LeanplumPushService.LEANPLUM_NOTIFICATION);
+        forwardIntent.setClassName(context, receiver);
+        forwardIntent.putExtras(intent.getExtras());
+        context.sendBroadcast(forwardIntent);
+      }
     } catch (Throwable t) {
       Util.handleException(t);
     }
   }
 }
--- a/mobile/android/thirdparty/com/leanplum/LeanplumPushService.java
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumPushService.java
@@ -20,17 +20,16 @@
  */
 
 package com.leanplum;
 
 import android.app.Activity;
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.graphics.Bitmap;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
@@ -40,21 +39,23 @@ import android.text.TextUtils;
 import com.leanplum.callbacks.VariablesChangedCallback;
 import com.leanplum.internal.ActionManager;
 import com.leanplum.internal.Constants;
 import com.leanplum.internal.Constants.Keys;
 import com.leanplum.internal.Constants.Methods;
 import com.leanplum.internal.Constants.Params;
 import com.leanplum.internal.JsonConverter;
 import com.leanplum.internal.LeanplumInternal;
+import com.leanplum.internal.LeanplumManifestHelper;
 import com.leanplum.internal.Log;
 import com.leanplum.internal.Request;
 import com.leanplum.internal.Util;
 import com.leanplum.internal.VarCache;
 import com.leanplum.utils.BitmapUtil;
+import com.leanplum.utils.BuildUtil;
 import com.leanplum.utils.SharedPreferencesUtil;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -66,53 +67,55 @@ import java.util.Random;
  *
  * @author Andrew First, Anna Orlova
  */
 public class LeanplumPushService {
   /**
    * Leanplum's built-in Google Cloud Messaging sender ID.
    */
   public static final String LEANPLUM_SENDER_ID = "44059457771";
+  /**
+   * Intent action used when broadcast is received in custom BroadcastReceiver.
+   */
+  public static final String LEANPLUM_NOTIFICATION = "LP_NOTIFICATION";
+  /**
+   * Action param key contained when Notification Bundle is parsed with {@link
+   * LeanplumPushService#parseNotificationBundle(Bundle)}.
+   */
+  public static final String LEANPLUM_ACTION_PARAM = "lp_action_param";
+  /**
+   * Message title param key contained when Notification Bundle is parsed with {@link
+   * LeanplumPushService#parseNotificationBundle(Bundle)}.
+   */
+  public static final String LEANPLUM_MESSAGE_PARAM = "lp_message_param";
+  /**
+   * Message id param key contained when Notification Bundle is parsed with {@link
+   * LeanplumPushService#parseNotificationBundle(Bundle)}.
+   */
+  public static final String LEANPLUM_MESSAGE_ID = "lp_message_id";
+
   private static final String LEANPLUM_PUSH_FCM_LISTENER_SERVICE_CLASS =
       "com.leanplum.LeanplumPushFcmListenerService";
   private static final String PUSH_FIREBASE_MESSAGING_SERVICE_CLASS =
       "com.leanplum.LeanplumPushFirebaseMessagingService";
   private static final String LEANPLUM_PUSH_INSTANCE_ID_SERVICE_CLASS =
       "com.leanplum.LeanplumPushInstanceIDService";
   private static final String LEANPLUM_PUSH_LISTENER_SERVICE_CLASS =
       "com.leanplum.LeanplumPushListenerService";
   private static final String GCM_RECEIVER_CLASS = "com.google.android.gms.gcm.GcmReceiver";
-
-  private static Class<? extends Activity> callbackClass;
-  private static LeanplumCloudMessagingProvider provider;
-  private static boolean isFirebaseEnabled = false;
   private static final int NOTIFICATION_ID = 1;
-
   private static final String OPEN_URL = "Open URL";
   private static final String URL = "URL";
   private static final String OPEN_ACTION = "Open";
+  private static final int MAX_ONE_LINE_TEXT_LENGTH = 37;
+  private static Class<? extends Activity> callbackClass;
+  private static LeanplumCloudMessagingProvider provider;
   private static LeanplumPushNotificationCustomizer customizer;
 
   /**
-   * Use Firebase Cloud Messaging, instead of the default Google Cloud Messaging.
-   */
-  public static void enableFirebase() {
-    LeanplumPushService.isFirebaseEnabled = true;
-  }
-
-  /**
-   * Whether Firebase Cloud Messaging is enabled or not.
-   *
-   * @return Boolean - true if enabled
-   */
-  static boolean isFirebaseEnabled() {
-    return isFirebaseEnabled;
-  }
-
-  /**
    * Get Cloud Messaging provider. By default - GCM.
    *
    * @return LeanplumCloudMessagingProvider - current provider
    */
   static LeanplumCloudMessagingProvider getCloudMessagingProvider() {
     return provider;
   }
 
@@ -187,29 +190,28 @@ public class LeanplumPushService {
             Map<String, Object> params = new HashMap<>();
             params.put(Params.INCLUDE_DEFAULTS, Boolean.toString(false));
             params.put(Params.INCLUDE_MESSAGE_ID, messageId);
             Request req = Request.post(Methods.GET_VARS, params);
             req.onResponse(new Request.ResponseCallback() {
               @Override
               public void response(JSONObject response) {
                 try {
-                  JSONObject getVariablesResponse = Request.getLastResponse(response);
-                  if (getVariablesResponse == null) {
+                  if (response == null) {
                     Log.e("No response received from the server. Please contact us to " +
                         "investigate.");
                   } else {
                     Map<String, Object> values = JsonConverter.mapFromJson(
-                        getVariablesResponse.optJSONObject(Constants.Keys.VARS));
+                        response.optJSONObject(Constants.Keys.VARS));
                     Map<String, Object> messages = JsonConverter.mapFromJson(
-                        getVariablesResponse.optJSONObject(Constants.Keys.MESSAGES));
+                        response.optJSONObject(Constants.Keys.MESSAGES));
                     Map<String, Object> regions = JsonConverter.mapFromJson(
-                        getVariablesResponse.optJSONObject(Constants.Keys.REGIONS));
+                        response.optJSONObject(Constants.Keys.REGIONS));
                     List<Map<String, Object>> variants = JsonConverter.listFromJson(
-                        getVariablesResponse.optJSONArray(Constants.Keys.VARIANTS));
+                        response.optJSONArray(Constants.Keys.VARIANTS));
                     if (!Constants.canDownloadContentMidSessionInProduction ||
                         VarCache.getDiffs().equals(values)) {
                       values = null;
                     }
                     if (VarCache.getMessageDiffs().equals(messages)) {
                       messages = null;
                     }
                     if (values != null || messages != null) {
@@ -277,62 +279,110 @@ public class LeanplumPushService {
     // Leanplum.track("Displayed", 0.0, null, null, requestArgs);
 
     showNotification(context, message);
   }
 
   /**
    * Put the message into a notification and post it.
    */
-  private static void showNotification(Context context, Bundle message) {
-    NotificationManager notificationManager = (NotificationManager)
+  private static void showNotification(Context context, final Bundle message) {
+    if (context == null || message == null) {
+      return;
+    }
+
+    int defaultIconId = 0;
+    // If client will start to use adaptive icon, there can be a problem
+    // https://issuetracker.google.com/issues/68716460 that can cause a factory reset of the device
+    // on Android Version 26.
+    if (!LeanplumNotificationHelper.isApplicationIconValid(context)) {
+      defaultIconId = LeanplumNotificationHelper.getDefaultPushNotificationIconResourceId(context);
+      if (defaultIconId == 0) {
+        Log.e("You are using adaptive icons without having a fallback icon for push" +
+            " notifications on Android Oreo. \n" + "This can cause a factory reset of the device" +
+            " on Android Version 26. Please add regular icon with name " +
+            "\"leanplum_default_push_icon.png\" to your \"drawable\" folder.\n" + "Google issue: " +
+            "https://issuetracker.google.com/issues/68716460"
+        );
+        return;
+      }
+    }
+
+    final NotificationManager notificationManager = (NotificationManager)
         context.getSystemService(Context.NOTIFICATION_SERVICE);
 
     Intent intent = new Intent(context, LeanplumPushReceiver.class);
     intent.addCategory("lpAction");
     intent.putExtras(message);
     PendingIntent contentIntent = PendingIntent.getBroadcast(
         context.getApplicationContext(), new Random().nextInt(),
         intent, 0);
 
     String title = Util.getApplicationName(context.getApplicationContext());
     if (message.getString("title") != null) {
       title = message.getString("title");
     }
-    NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
-        .setSmallIcon(context.getApplicationInfo().icon)
-        .setContentTitle(title)
+    final NotificationCompat.Builder notificationCompatBuilder =
+        LeanplumNotificationHelper.getNotificationCompatBuilder(context, message);
+
+    if (notificationCompatBuilder == null) {
+      return;
+    }
+    final String messageText = message.getString(Keys.PUSH_MESSAGE_TEXT);
+
+    if (defaultIconId == 0) {
+      notificationCompatBuilder.setSmallIcon(context.getApplicationInfo().icon);
+    } else {
+      notificationCompatBuilder.setSmallIcon(defaultIconId);
+    }
+
+    notificationCompatBuilder.setContentTitle(title)
         .setStyle(new NotificationCompat.BigTextStyle()
-            .bigText(message.getString(Keys.PUSH_MESSAGE_TEXT)))
-        .setContentText(message.getString(Keys.PUSH_MESSAGE_TEXT));
+            .bigText(messageText))
+        .setContentText(messageText);
 
     String imageUrl = message.getString(Keys.PUSH_MESSAGE_IMAGE_URL);
+    Notification.Builder notificationBuilder = null;
     // BigPictureStyle support requires API 16 and higher.
     if (!TextUtils.isEmpty(imageUrl) && Build.VERSION.SDK_INT >= 16) {
       Bitmap bigPicture = BitmapUtil.getScaledBitmap(context, imageUrl);
       if (bigPicture != null) {
-        builder.setStyle(new NotificationCompat.BigPictureStyle()
-            .bigPicture(bigPicture)
-            .setBigContentTitle(title)
-            .setSummaryText(message.getString(Keys.PUSH_MESSAGE_TEXT)));
+        if ((messageText != null && messageText.length() < MAX_ONE_LINE_TEXT_LENGTH) ||
+            customizer != null) {
+          notificationCompatBuilder.setStyle(new NotificationCompat.BigPictureStyle()
+              .bigPicture(bigPicture)
+              .setBigContentTitle(title)
+              .setSummaryText(messageText));
+        } else {
+          notificationBuilder = LeanplumNotificationHelper.getNotificationBuilder(context, message,
+              contentIntent, title, messageText, bigPicture, defaultIconId);
+        }
       } else {
         Log.w(String.format("Image download failed for push notification with big picture. " +
             "No image will be included with the push notification. Image URL: %s.", imageUrl));
       }
     }
 
-    // Try to put notification on top of notification area.
-    if (Build.VERSION.SDK_INT >= 16) {
-      builder.setPriority(Notification.PRIORITY_MAX);
+    // Try to put a notification on top of the notification area. This method was deprecated in API
+    // level 26. For API level 26 and above we must use setImportance(int) for each notification
+    // channel, not for each notification message.
+    if (Build.VERSION.SDK_INT >= 16 && !BuildUtil.isNotificationChannelSupported(context)) {
+      //noinspection deprecation
+      notificationCompatBuilder.setPriority(Notification.PRIORITY_MAX);
     }
-    builder.setAutoCancel(true);
-    builder.setContentIntent(contentIntent);
+    notificationCompatBuilder.setAutoCancel(true);
+    notificationCompatBuilder.setContentIntent(contentIntent);
 
     if (LeanplumPushService.customizer != null) {
-      LeanplumPushService.customizer.customize(builder, message);
+      try {
+        LeanplumPushService.customizer.customize(notificationCompatBuilder, message);
+      } catch (Throwable t) {
+        Log.e("Unable to customize push notification: ", Log.getStackTraceString(t));
+        return;
+      }
     }
 
     int notificationId = LeanplumPushService.NOTIFICATION_ID;
     Object notificationIdObject = message.get("lp_notificationId");
     if (notificationIdObject instanceof Number) {
       notificationId = ((Number) notificationIdObject).intValue();
     } else if (notificationIdObject instanceof String) {
       try {
@@ -341,68 +391,162 @@ public class LeanplumPushService {
         notificationId = LeanplumPushService.NOTIFICATION_ID;
       }
     } else if (message.containsKey(Keys.PUSH_MESSAGE_ID)) {
       String value = message.getString(Keys.PUSH_MESSAGE_ID);
       if (value != null) {
         notificationId = value.hashCode();
       }
     }
-    notificationManager.notify(notificationId, builder.build());
+
+    try {
+      // Check if we have a chained message, and if it exists in var cache.
+      if (ActionContext.shouldForceContentUpdateForChainedMessage(
+          JsonConverter.fromJson(message.getString(Keys.PUSH_MESSAGE_ACTION)))) {
+        final int currentNotificationId = notificationId;
+        final Notification.Builder currentNotificationBuilder = notificationBuilder;
+        Leanplum.forceContentUpdate(new VariablesChangedCallback() {
+          @Override
+          public void variablesChanged() {
+            if (currentNotificationBuilder != null) {
+              notificationManager.notify(currentNotificationId, currentNotificationBuilder.build());
+            } else {
+              notificationManager.notify(currentNotificationId, notificationCompatBuilder.build());
+            }
+          }
+        });
+      } else {
+        if (notificationBuilder != null) {
+          notificationManager.notify(notificationId, notificationBuilder.build());
+        } else {
+          notificationManager.notify(notificationId, notificationCompatBuilder.build());
+        }
+      }
+    } catch (NullPointerException e) {
+      Log.e("Unable to show push notification.", e);
+    } catch (Throwable t) {
+      Log.e("Unable to show push notification.", t);
+      Util.handleException(t);
+    }
   }
 
-  static void openNotification(Context context, final Bundle notification) {
+  static void openNotification(Context context, Intent intent) {
     Log.d("Opening push notification action.");
+    // Pre handles push notification.
+    Bundle notification = preHandlePushNotification(context, intent);
     if (notification == null) {
-      Log.i("Received null Bundle.");
       return;
     }
 
     // Checks if open action is "Open URL" and there is some activity that can handle intent.
     if (isActivityWithIntentStarted(context, notification)) {
       return;
     }
 
     // Start activity.
     Class<? extends Activity> callbackClass = LeanplumPushService.getCallbackClass();
     boolean shouldStartActivity = true;
-    if (LeanplumActivityHelper.currentActivity != null &&
-        !LeanplumActivityHelper.isActivityPaused) {
+    Activity currentActivity = LeanplumActivityHelper.currentActivity;
+    if (currentActivity != null && !LeanplumActivityHelper.isActivityPaused) {
       if (callbackClass == null) {
         shouldStartActivity = false;
-      } else if (callbackClass.isInstance(LeanplumActivityHelper.currentActivity)) {
+      } else if (callbackClass.isInstance(currentActivity)) {
         shouldStartActivity = false;
       }
     }
 
     if (shouldStartActivity) {
       Intent actionIntent = getActionIntent(context);
+      if (actionIntent == null) {
+        return;
+      }
       actionIntent.putExtras(notification);
-      actionIntent.addFlags(
-          Intent.FLAG_ACTIVITY_CLEAR_TOP |
-              Intent.FLAG_ACTIVITY_NEW_TASK);
+      actionIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
       context.startActivity(actionIntent);
     }
+    // Post handles push notification.
+    postHandlePushNotification(context, intent);
+  }
 
+  /**
+   * Parse notification bundle. Use this method to get parsed bundle to decide next step. Parsed
+   * data will contain {@link LeanplumPushService#LEANPLUM_ACTION_PARAM}, {@link
+   * LeanplumPushService#LEANPLUM_MESSAGE_PARAM} and {@link LeanplumPushService#LEANPLUM_MESSAGE_ID}
+   *
+   * @param notificationBundle Bundle to be parsed.
+   * @return Map containing Actions, Message title and Message Id.
+   */
+  public static Map<String, Object> parseNotificationBundle(Bundle notificationBundle) {
+    try {
+      String notificationActions = notificationBundle.getString(Keys.PUSH_MESSAGE_ACTION);
+      String notificationMessage = notificationBundle.getString(Keys.PUSH_MESSAGE_TEXT);
+      String notificationMessageId = LeanplumPushService.getMessageId(notificationBundle);
+
+      Map<String, Object> arguments = new HashMap<>();
+      arguments.put(LEANPLUM_ACTION_PARAM, JsonConverter.fromJson(notificationActions));
+      arguments.put(LEANPLUM_MESSAGE_PARAM, notificationMessage);
+      arguments.put(LEANPLUM_MESSAGE_ID, notificationMessageId);
+
+      return arguments;
+    } catch (Throwable ignored) {
+      Log.i("Failed to parse notification bundle.");
+    }
+    return null;
+  }
+
+  /**
+   * Must be called before deciding which activity will be opened, to allow Leanplum SDK to track
+   * stats, open events etc.
+   *
+   * @param context Surrounding context.
+   * @param intent Received Intent.
+   * @return Bundle containing push notification data.
+   */
+  public static Bundle preHandlePushNotification(Context context, Intent intent) {
+    if (intent == null) {
+      Log.i("Unable to pre handle push notification, Intent is null.");
+      return null;
+    }
+    Bundle notification = intent.getExtras();
+    if (notification == null) {
+      Log.i("Unable to pre handle push notification, extras are null.");
+      return null;
+    }
+    return notification;
+  }
+
+  /**
+   * Must be called after deciding which activity will be opened, to allow Leanplum SDK to track
+   * stats, open events etc.
+   *
+   * @param context Surrounding context.
+   * @param intent Received Intent.
+   */
+  public static void postHandlePushNotification(Context context, Intent intent) {
+    final Bundle notification = intent.getExtras();
+    if (notification == null) {
+      Log.i("Could not post handle push notification, extras are null.");
+      return;
+    }
     // Perform action.
     LeanplumActivityHelper.queueActionUponActive(new VariablesChangedCallback() {
       @Override
       public void variablesChanged() {
         try {
           final String messageId = LeanplumPushService.getMessageId(notification);
           final String actionName = Constants.Values.DEFAULT_PUSH_ACTION;
 
           // Make sure content is available.
           if (messageId != null) {
             if (LeanplumPushService.areActionsEmbedded(notification)) {
               Map<String, Object> args = new HashMap<>();
               args.put(actionName, JsonConverter.fromJson(
                   notification.getString(Keys.PUSH_MESSAGE_ACTION)));
-              ActionContext context = new ActionContext(
-                  ActionManager.PUSH_NOTIFICATION_ACTION_NAME, args, messageId);
+              ActionContext context = new ActionContext(ActionManager.PUSH_NOTIFICATION_ACTION_NAME,
+                  args, messageId);
               context.preventRealtimeUpdating();
               context.update();
               context.runTrackedActionNamed(actionName);
             } else {
               Leanplum.addOnceVariablesChangedAndNoDownloadsPendingHandler(
                   new VariablesChangedCallback() {
                     @Override
                     public void variablesChanged() {
@@ -470,31 +614,23 @@ public class LeanplumPushService {
     }
     return null;
   }
 
   /**
    * Checks if there is some activity that can handle intent.
    */
   private static Boolean activityHasIntent(Context context, Intent deepLinkIntent) {
-    final int flag;
-    if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
-      flag = PackageManager.MATCH_ALL;
-    } else {
-      flag = 0;
-    }
     List<ResolveInfo> resolveInfoList =
-            context.getPackageManager().queryIntentActivities(deepLinkIntent, flag);
+        context.getPackageManager().queryIntentActivities(deepLinkIntent, 0);
     if (resolveInfoList != null && !resolveInfoList.isEmpty()) {
       for (ResolveInfo resolveInfo : resolveInfoList) {
         if (resolveInfo != null && resolveInfo.activityInfo != null &&
             resolveInfo.activityInfo.name != null) {
-          // In local build, Fennec's activityInfo.packagename is org.mozilla.fennec_<device_name>
-          // But activityInfo.name is org.mozilla.Gecko.App. Thus we should use packagename here.
-          if (resolveInfo.activityInfo.packageName.equals(context.getPackageName())) {
+          if (resolveInfo.activityInfo.name.contains(context.getPackageName())) {
             // If url can be handled by current app - set package name to intent, so url will be
             // open by current app. Skip chooser dialog.
             deepLinkIntent.setPackage(resolveInfo.activityInfo.packageName);
             return true;
           }
         }
       }
     }
@@ -559,193 +695,122 @@ public class LeanplumPushService {
       if (Util.hasPlayServices()) {
         initPushService();
       } else {
         Log.i("No valid Google Play Services APK found.");
       }
     } catch (LeanplumException e) {
       Log.e("There was an error registering for push notifications.\n" +
           Log.getStackTraceString(e));
+    } catch (Throwable ignored) {
     }
   }
 
+  /**
+   * Initialize push service.
+   */
   private static void initPushService() {
-    if (!enableServices()) {
+    if (!enableGcmServices()) {
+      Log.w("Failed to initialize GCM services.");
       return;
     }
     provider = new LeanplumGcmProvider();
-    if (!provider.isInitialized()) {
+
+    if (!provider.isInitialized() || !provider.isManifestSetup()) {
       return;
     }
     if (hasAppIDChanged(Request.appId())) {
       provider.unregister();
     }
     registerInBackground();
   }
 
-
   /**
-   * Enable Leanplum GCM or FCM services.
+   * Enables GCM services. By default, all GCM services are disabled.
    *
-   * @return True if services was enabled.
+   * @return true if services are successfully enabled, false otherwise
    */
-  private static boolean enableServices() {
+  private static boolean enableGcmServices() {
     Context context = Leanplum.getContext();
     if (context == null) {
+      Log.i("Failed to enable FCM services, context is null.");
       return false;
     }
 
     PackageManager packageManager = context.getPackageManager();
     if (packageManager == null) {
+      Log.i("Failed to enable FCM services, PackageManager is null.");
       return false;
     }
 
-    if (isFirebaseEnabled) {
-      Class fcmListenerClass = getClassForName(LEANPLUM_PUSH_FCM_LISTENER_SERVICE_CLASS);
-      if (fcmListenerClass == null) {
-        return false;
-      }
+    Class gcm = LeanplumManifestHelper.getClassForName(LEANPLUM_PUSH_INSTANCE_ID_SERVICE_CLASS);
+    if (gcm == null) {
+      Log.e("Failed to setup GCM, please compile GCM library.");
+      return false;
+    }
+    // We will only enable component once, if we are switching from FCM to GCM, we have to disable
+    // FCM services first.
+    if (!LeanplumManifestHelper.wasComponentEnabled(context, packageManager, gcm)) {
+      // Try to disable FCM first.
+      disableFcmServices();
+      LeanplumManifestHelper.enableComponent(context, packageManager, gcm);
 
-      if (!wasComponentEnabled(context, packageManager, fcmListenerClass)) {
-        if (!enableServiceAndStart(context, packageManager, PUSH_FIREBASE_MESSAGING_SERVICE_CLASS)
-            || !enableServiceAndStart(context, packageManager, fcmListenerClass)) {
-          return false;
-        }
+      // Make sure we can find the class before enabling it.
+      Class gcmReceiver = LeanplumManifestHelper.getClassForName(GCM_RECEIVER_CLASS);
+      if (gcmReceiver != null) {
+        LeanplumManifestHelper.enableComponent(context, packageManager, gcmReceiver);
       }
-    } else {
-      Class gcmPushInstanceIDClass = getClassForName(LEANPLUM_PUSH_INSTANCE_ID_SERVICE_CLASS);
-      if (gcmPushInstanceIDClass == null) {
-        return false;
+      Class pushListener = LeanplumManifestHelper.getClassForName(LEANPLUM_PUSH_LISTENER_SERVICE_CLASS);
+      if (pushListener != null) {
+        LeanplumManifestHelper.enableComponent(context, packageManager, pushListener);
       }
     }
     return true;
   }
 
   /**
-   * Gets Class for name.
-   *
-   * @param className - class name.
-   * @return Class for provided class name.
-   */
-  private static Class getClassForName(String className) {
-    try {
-      return Class.forName(className);
-    } catch (Throwable t) {
-      if (isFirebaseEnabled) {
-        Log.e("Please compile FCM library.");
-      } else {
-        Log.e("Please compile GCM library.");
-      }
-      return null;
-    }
-  }
-
-  /**
-   * Enables and starts service for provided class name.
-   *
-   * @param context Current Context.
-   * @param packageManager Current PackageManager.
-   * @param className Name of Class that needs to be enabled and started.
-   * @return True if service was enabled and started.
+   * Disables FCM services.
    */
-  private static boolean enableServiceAndStart(Context context, PackageManager packageManager,
-      String className) {
-    Class clazz;
-    try {
-      clazz = Class.forName(className);
-    } catch (Throwable t) {
-      return false;
+  private static void disableFcmServices() {
+    Context context = Leanplum.getContext();
+    if (context == null) {
+      return;
     }
-    return enableServiceAndStart(context, packageManager, clazz);
-  }
 
-  /**
-   * Enables and starts service for provided class name.
-   *
-   * @param context Current Context.
-   * @param packageManager Current PackageManager.
-   * @param clazz Class of service that needs to be enabled and started.
-   * @return True if service was enabled and started.
-   */
-  private static boolean enableServiceAndStart(Context context, PackageManager packageManager,
-      Class clazz) {
-    if (!enableComponent(context, packageManager, clazz)) {
-      return false;
+    PackageManager packageManager = context.getPackageManager();
+    if (packageManager == null) {
+      return;
     }
-    try {
-      context.startService(new Intent(context, clazz));
-    } catch (Throwable t) {
-      Log.w("Could not start service " + clazz.getName());
-      return false;
-    }
-    return true;
+
+    LeanplumManifestHelper.disableComponent(context, packageManager,
+        LEANPLUM_PUSH_FCM_LISTENER_SERVICE_CLASS);
+    LeanplumManifestHelper.disableComponent(context, packageManager,
+        PUSH_FIREBASE_MESSAGING_SERVICE_CLASS);
   }
 
   /**
-   * Enables component for provided class name.
-   *
-   * @param context Current Context.
-   * @param packageManager Current PackageManager.
-   * @param className Name of Class for enable.
-   * @return True if component was enabled.
+   * Disables GCM services
    */
-  private static boolean enableComponent(Context context, PackageManager packageManager,
-      String className) {
-    try {
-      Class clazz = Class.forName(className);
-      return enableComponent(context, packageManager, clazz);
-    } catch (Throwable t) {
-      return false;
-    }
-  }
-
-  /**
-   * Enables component for provided class.
-   *
-   * @param context Current Context.
-   * @param packageManager Current PackageManager.
-   * @param clazz Class for enable.
-   * @return True if component was enabled.
-   */
-  private static boolean enableComponent(Context context, PackageManager packageManager,
-      Class clazz) {
-    if (clazz == null || context == null || packageManager == null) {
-      return false;
+  private static void disableGcmServices() {
+    Context context = Leanplum.getContext();
+    if (context == null) {
+      return;
     }
 
-    try {
-      packageManager.setComponentEnabledSetting(new ComponentName(context, clazz),
-          PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
-    } catch (Throwable t) {
-      Log.w("Could not enable component " + clazz.getName());
-      return false;
+    PackageManager packageManager = context.getPackageManager();
+    if (packageManager == null) {
+      return;
     }
-    return true;
-  }
 
-  /**
-   * Checks if component for provided class enabled before.
-   *
-   * @param context Current Context.
-   * @param packageManager Current PackageManager.
-   * @param clazz Class for check.
-   * @return True if component was enabled before.
-   */
-  private static boolean wasComponentEnabled(Context context, PackageManager packageManager,
-      Class clazz) {
-    if (clazz == null || context == null || packageManager == null) {
-      return false;
-    }
-    int componentStatus = packageManager.getComponentEnabledSetting(new ComponentName(context,
-        clazz));
-    if (PackageManager.COMPONENT_ENABLED_STATE_DEFAULT == componentStatus ||
-        PackageManager.COMPONENT_ENABLED_STATE_DISABLED == componentStatus) {
-      return false;
-    }
-    return true;
+    LeanplumManifestHelper.disableComponent(context, packageManager,
+        LEANPLUM_PUSH_INSTANCE_ID_SERVICE_CLASS);
+    LeanplumManifestHelper.disableComponent(context, packageManager,
+        GCM_RECEIVER_CLASS);
+    LeanplumManifestHelper.disableComponent(context, packageManager,
+        LEANPLUM_PUSH_LISTENER_SERVICE_CLASS);
   }
 
   /**
    * Check if current application id is different from stored one.
    *
    * @param currentAppId - Current application id.
    * @return True if application id was stored before and doesn't equal to current.
    */
--- a/mobile/android/thirdparty/com/leanplum/LeanplumResources.java
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumResources.java
@@ -37,17 +37,19 @@ import com.leanplum.internal.VarCache;
 
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 
 // Description of resources.asrc file (we don't use this right nwo)
 // http://ekasiswanto.wordpress.com/2012/09/19/descriptions-of-androids-resources-arsc/
-
+// Suppressing deprecation warnings for Resource methods,
+// because the resource syncing feature will likely be refactored/replaced in the future.
+@SuppressWarnings("deprecation")
 public class LeanplumResources extends Resources {
   public LeanplumResources(Resources base) {
     super(base.getAssets(), base.getDisplayMetrics(), base.getConfiguration());
   }
 
   /* internal */
   <T> Var<T> getOverrideResource(int id) {
     try {
--- a/mobile/android/thirdparty/com/leanplum/LeanplumUIEditor.java
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumUIEditor.java
@@ -20,17 +20,21 @@
  */
 
 package com.leanplum;
 
 import android.app.Activity;
 
 /**
  * Describes the API of the visual editor package.
+ *
+ * @deprecated {@link LeanplumUIEditor} will be made private in future releases, since it is not
+ * intended to be public API.
  */
+@Deprecated
 public interface LeanplumUIEditor {
   /**
    * Enable interface editing via Leanplum.com Visual Editor.
    */
   void allowInterfaceEditing(Boolean isDevelopmentModeEnabled);
 
   /**
    * Enables Interface editing for the desired activity.
--- a/mobile/android/thirdparty/com/leanplum/Newsfeed.java
+++ b/mobile/android/thirdparty/com/leanplum/Newsfeed.java
@@ -25,21 +25,22 @@ import com.leanplum.callbacks.InboxChang
 import com.leanplum.callbacks.NewsfeedChangedCallback;
 
 /**
  * Newsfeed class.
  *
  * @author Aleksandar Gyorev
  */
 public class Newsfeed extends LeanplumInbox {
-
+  private static Newsfeed instance = new Newsfeed();
   /**
    * A private constructor, which prevents any other class from instantiating.
    */
-  Newsfeed() {
+  private Newsfeed() {
+      super();
   }
 
   /**
    * Static 'getInstance' method.
    */
   static Newsfeed getInstance() {
     return instance;
   }
--- a/mobile/android/thirdparty/com/leanplum/Var.java
+++ b/mobile/android/thirdparty/com/leanplum/Var.java
@@ -511,27 +511,17 @@ public class Var<T> {
     }
   }
 
   /**
    * Returns a number of elements contained in a List variable.
    *
    * @return Elements count or 0 if Variable is not a List.
    */
-  @Deprecated
   public int count() {
-    return countInternal();
-  }
-
-  /**
-   * Returns a number of elements contained in a List variable.
-   *
-   * @return Elements count or 0 if Variable is not a List.
-   */
-  private int countInternal() {
     try {
       warnIfNotStarted();
       Object result = VarCache.getMergedValueFromComponentArray(nameComponents);
       if (result instanceof List) {
         return ((List<?>) result).size();
       }
     } catch (Throwable t) {
       Util.handleException(t);
@@ -542,27 +532,17 @@ public class Var<T> {
     return 0;
   }
 
   /**
    * Gets a value from a variable initialized as Number.
    *
    * @return A Number value.
    */
-  @Deprecated
   public Number numberValue() {
-    return numberValueInternal();
-  }
-
-  /**
-   * Gets a value from a variable initialized as Number.
-   *
-   * @return A Number value.
-   */
-  private Number numberValueInternal() {
     warnIfNotStarted();
     return numberValue;
   }
 
   /**
    * Gets a value from a variable initialized as String.
    *
    * @return A String value.
--- a/mobile/android/thirdparty/com/leanplum/activities/LeanplumAccountAuthenticatorActivity.java
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumAccountAuthenticatorActivity.java
@@ -17,23 +17,27 @@
  * KIND, either express or implied.  See the License for the
  * specific language governing permissions and limitations
  * under the License.
  */
 
 package com.leanplum.activities;
 
 import android.accounts.AccountAuthenticatorActivity;
-import android.annotation.SuppressLint;
 import android.content.res.Resources;
 
 import com.leanplum.Leanplum;
 import com.leanplum.LeanplumActivityHelper;
 
-@SuppressLint("Registered")
+/**
+ *  @deprecated due to rising minimal API to 14. This class will be removed in a
+ *  future major release. Please use {@link LeanplumActivityHelper} to track your activities
+ *  automatically.
+ */
+@Deprecated
 public class LeanplumAccountAuthenticatorActivity extends AccountAuthenticatorActivity {
   private LeanplumActivityHelper helper;
 
   private LeanplumActivityHelper getHelper() {
     if (helper == null) {
       helper = new LeanplumActivityHelper(this);
     }
     return helper;
--- a/mobile/android/thirdparty/com/leanplum/activities/LeanplumActivity.java
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumActivity.java
@@ -22,16 +22,22 @@
 package com.leanplum.activities;
 
 import android.app.Activity;
 import android.content.res.Resources;
 
 import com.leanplum.Leanplum;
 import com.leanplum.LeanplumActivityHelper;
 
+/**
+ *  @deprecated due to rising minimal API to 14. This class will be removed in a
+ *  future major release. Please use {@link LeanplumActivityHelper} to track your activities
+ *  automatically.
+ */
+@Deprecated
 public abstract class LeanplumActivity extends Activity {
   private LeanplumActivityHelper helper;
 
   private LeanplumActivityHelper getHelper() {
     if (helper == null) {
       helper = new LeanplumActivityHelper(this);
     }
     return helper;
--- a/mobile/android/thirdparty/com/leanplum/activities/LeanplumActivityGroup.java
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumActivityGroup.java
@@ -16,25 +16,28 @@
  * "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.activities;
 
-import android.annotation.SuppressLint;
 import android.app.ActivityGroup;
 import android.content.res.Resources;
 
 import com.leanplum.Leanplum;
 import com.leanplum.LeanplumActivityHelper;
 
-@SuppressLint("Registered")
-@SuppressWarnings("deprecation")
+/**
+ *  @deprecated due to rising minimal API to 14. This class will be removed in a
+ *  future major release. Please use {@link LeanplumActivityHelper} to track your activities
+ *  automatically.
+ */
+@Deprecated
 public class LeanplumActivityGroup extends ActivityGroup {
   private LeanplumActivityHelper helper;
 
   private LeanplumActivityHelper getHelper() {
     if (helper == null) {
       helper = new LeanplumActivityHelper(this);
     }
     return helper;
--- a/mobile/android/thirdparty/com/leanplum/activities/LeanplumAliasActivity.java
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumAliasActivity.java
@@ -16,24 +16,28 @@
  * "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.activities;
 
-import android.annotation.SuppressLint;
 import android.app.AliasActivity;
 import android.content.res.Resources;
 
 import com.leanplum.Leanplum;
 import com.leanplum.LeanplumActivityHelper;
 
-@SuppressLint("Registered")
+/**
+ *  @deprecated due to rising minimal API to 14. This class will be removed in a
+ *  future major release. Please use {@link LeanplumActivityHelper} to track your activities
+ *  automatically.
+ */
+@Deprecated
 public class LeanplumAliasActivity extends AliasActivity {
   private LeanplumActivityHelper helper;
 
   private LeanplumActivityHelper getHelper() {
     if (helper == null) {
       helper = new LeanplumActivityHelper(this);
     }
     return helper;
--- a/mobile/android/thirdparty/com/leanplum/activities/LeanplumAppCompatActivity.java
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumAppCompatActivity.java
@@ -16,24 +16,28 @@
  * "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.activities;
 
-import android.annotation.SuppressLint;
 import android.content.res.Resources;
 import android.support.v7.app.AppCompatActivity;
 
 import com.leanplum.Leanplum;
 import com.leanplum.LeanplumActivityHelper;
 
-@SuppressLint("Registered")
+/**
+ *  @deprecated due to rising minimal API to 14. This class will be removed in a
+ *  future major release. Please use {@link LeanplumActivityHelper} to track your activities
+ *  automatically.
+ */
+@Deprecated
 public class LeanplumAppCompatActivity extends AppCompatActivity {
   private LeanplumActivityHelper helper;
 
   private LeanplumActivityHelper getHelper() {
     if (helper == null) {
       helper = new LeanplumActivityHelper(this);
     }
     return helper;
--- a/mobile/android/thirdparty/com/leanplum/activities/LeanplumExpandableListActivity.java
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumExpandableListActivity.java
@@ -16,24 +16,28 @@
  * "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.activities;
 
-import android.annotation.SuppressLint;
 import android.app.ExpandableListActivity;
 import android.content.res.Resources;
 
 import com.leanplum.Leanplum;
 import com.leanplum.LeanplumActivityHelper;
 
-@SuppressLint("Registered")
+/**
+ *  @deprecated due to rising minimal API to 14. This class will be removed in a
+ *  future major release. Please use {@link LeanplumActivityHelper} to track your activities
+ *  automatically.
+ */
+@Deprecated
 public class LeanplumExpandableListActivity extends ExpandableListActivity {
   private LeanplumActivityHelper helper;
 
   private LeanplumActivityHelper getHelper() {
     if (helper == null) {
       helper = new LeanplumActivityHelper(this);
     }
     return helper;
--- a/mobile/android/thirdparty/com/leanplum/activities/LeanplumFragmentActivity.java
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumFragmentActivity.java
@@ -22,16 +22,22 @@
 package com.leanplum.activities;
 
 import android.content.res.Resources;
 import android.support.v4.app.FragmentActivity;
 
 import com.leanplum.Leanplum;
 import com.leanplum.LeanplumActivityHelper;
 
+/**
+ *  @deprecated due to rising minimal API to 14. This class will be removed in a
+ *  future major release. Please use {@link LeanplumActivityHelper} to track your activities
+ *  automatically.
+ */
+@Deprecated
 public abstract class LeanplumFragmentActivity extends FragmentActivity {
   private LeanplumActivityHelper helper;
 
   private LeanplumActivityHelper getHelper() {
     if (helper == null) {
       helper = new LeanplumActivityHelper(this);
     }
     return helper;
--- a/mobile/android/thirdparty/com/leanplum/activities/LeanplumLauncherActivity.java
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumLauncherActivity.java
@@ -16,24 +16,28 @@
  * "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.activities;
 
-import android.annotation.SuppressLint;
 import android.app.LauncherActivity;
 import android.content.res.Resources;
 
 import com.leanplum.Leanplum;
 import com.leanplum.LeanplumActivityHelper;
 
-@SuppressLint("Registered")
+/**
+ *  @deprecated due to rising minimal API to 14. This class will be removed in a
+ *  future major release. Please use {@link LeanplumActivityHelper} to track your activities
+ *  automatically.
+ */
+@Deprecated
 public class LeanplumLauncherActivity extends LauncherActivity {
   private LeanplumActivityHelper helper;
 
   private LeanplumActivityHelper getHelper() {
     if (helper == null) {
       helper = new LeanplumActivityHelper(this);
     }
     return helper;
--- a/mobile/android/thirdparty/com/leanplum/activities/LeanplumListActivity.java
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumListActivity.java
@@ -16,24 +16,28 @@
  * "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.activities;
 
-import android.annotation.SuppressLint;
 import android.app.ListActivity;
 import android.content.res.Resources;
 
 import com.leanplum.Leanplum;
 import com.leanplum.LeanplumActivityHelper;
 
-@SuppressLint("Registered")
+/**
+ *  @deprecated due to rising minimal API to 14. This class will be removed in a
+ *  future major release. Please use {@link LeanplumActivityHelper} to track your activities
+ *  automatically.
+ */
+@Deprecated
 public class LeanplumListActivity extends ListActivity {
   private LeanplumActivityHelper helper;
 
   private LeanplumActivityHelper getHelper() {
     if (helper == null) {
       helper = new LeanplumActivityHelper(this);
     }
     return helper;
--- a/mobile/android/thirdparty/com/leanplum/activities/LeanplumNativeActivity.java
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumNativeActivity.java
@@ -16,24 +16,28 @@
  * "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.activities;
 
-import android.annotation.SuppressLint;
 import android.app.NativeActivity;
 import android.content.res.Resources;
 
 import com.leanplum.Leanplum;
 import com.leanplum.LeanplumActivityHelper;
 
-@SuppressLint("Registered")
+/**
+ *  @deprecated due to rising minimal API to 14. This class will be removed in a
+ *  future major release. Please use {@link LeanplumActivityHelper} to track your activities
+ *  automatically.
+ */
+@Deprecated
 public class LeanplumNativeActivity extends NativeActivity {
   private LeanplumActivityHelper helper;
 
   private LeanplumActivityHelper getHelper() {
     if (helper == null) {
       helper = new LeanplumActivityHelper(this);
     }
     return helper;
--- a/mobile/android/thirdparty/com/leanplum/activities/LeanplumPreferenceActivity.java
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumPreferenceActivity.java
@@ -15,24 +15,28 @@
  * 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.activities;
 
-import android.annotation.SuppressLint;
 import android.content.res.Resources;
 import android.preference.PreferenceActivity;
 
 import com.leanplum.Leanplum;
 import com.leanplum.LeanplumActivityHelper;
 
-@SuppressLint("Registered")
+/**
+ *  @deprecated due to rising minimal API to 14. This class will be removed in a
+ *  future major release. Please use {@link LeanplumActivityHelper} to track your activities
+ *  automatically.
+ */
+@Deprecated
 public class LeanplumPreferenceActivity extends PreferenceActivity {
   private LeanplumActivityHelper helper;
 
   private LeanplumActivityHelper getHelper() {
     if (helper == null) {
       helper = new LeanplumActivityHelper(this);
     }
     return helper;
--- a/mobile/android/thirdparty/com/leanplum/activities/LeanplumTabActivity.java
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumTabActivity.java
@@ -16,25 +16,28 @@
  * "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.activities;
 
-import android.annotation.SuppressLint;
 import android.app.TabActivity;
 import android.content.res.Resources;
 
 import com.leanplum.Leanplum;
 import com.leanplum.LeanplumActivityHelper;
 
-@SuppressLint("Registered")
-@SuppressWarnings("deprecation")
+/**
+ *  @deprecated due to rising minimal API to 14. This class will be removed in a
+ *  future major release. Please use {@link LeanplumActivityHelper} to track your activities
+ *  automatically.
+ */
+@Deprecated
 public class LeanplumTabActivity extends TabActivity {
   private LeanplumActivityHelper helper;
 
   private LeanplumActivityHelper getHelper() {
     if (helper == null) {
       helper = new LeanplumActivityHelper(this);
     }
     return helper;
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/callbacks/InboxSyncedCallback.java
@@ -0,0 +1,25 @@
+package com.leanplum.callbacks;
+
+/**
+ * Callback that gets run when forceContentUpdate was called.
+ *
+ * @author Anna Orlova
+ */
+public abstract class InboxSyncedCallback implements Runnable {
+    private boolean success;
+
+    public void setSuccess(boolean success) {
+        this.success = success;
+    }
+
+    public void run() {
+        this.onForceContentUpdate(success);
+    }
+
+    /**
+     * Call when forceContentUpdate was called.
+     *
+     * @param success True if syncing was successful.
+     */
+    public abstract void onForceContentUpdate(boolean success);
+}
\ No newline at end of file
--- a/mobile/android/thirdparty/com/leanplum/internal/ActionManager.java
+++ b/mobile/android/thirdparty/com/leanplum/internal/ActionManager.java
@@ -26,19 +26,19 @@ import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
 
 import com.leanplum.ActionContext;
 import com.leanplum.ActionContext.ContextualValues;
 import com.leanplum.Leanplum;
 import com.leanplum.LeanplumLocalPushListenerService;
-import com.leanplum.LeanplumPushService;
 import com.leanplum.LocationManager;
 import com.leanplum.callbacks.ActionCallback;
+import com.leanplum.utils.SharedPreferencesUtil;
 
 import java.io.Serializable;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
 /**
@@ -183,21 +183,17 @@ public class ActionManager {
           PendingIntent operation = PendingIntent.getService(
               context, messageId.hashCode(), intentAlarm,
               PendingIntent.FLAG_UPDATE_CURRENT);
           alarmManager.set(AlarmManager.RTC_WAKEUP, eta, operation);
 
           // Save notification so we can cancel it later.
           SharedPreferences.Editor editor = preferences.edit();
           editor.putLong(String.format(Constants.Defaults.LOCAL_NOTIFICATION_KEY, messageId), eta);
-          try {
-            editor.apply();
-          } catch (NoSuchMethodError e) {
-            editor.commit();
-          }
+          SharedPreferencesUtil.commitChanges(editor);
 
           Log.i("Scheduled notification");
           return true;
         } catch (Throwable t) {
           Util.handleException(t);
           return false;
         }
       }
@@ -212,24 +208,20 @@ public class ActionManager {
           // Get existing eta and clear notification from preferences.
           Context context = Leanplum.getContext();
           SharedPreferences preferences = context.getSharedPreferences(
               PREFERENCES_NAME, Context.MODE_PRIVATE);
           String preferencesKey = String.format(Constants.Defaults.LOCAL_NOTIFICATION_KEY, messageId);
           long existingEta = preferences.getLong(preferencesKey, 0L);
           SharedPreferences.Editor editor = preferences.edit();
           editor.remove(preferencesKey);
-          try {
-            editor.apply();
-          } catch (NoSuchMethodError e) {
-            editor.commit();
-          }
+          SharedPreferencesUtil.commitChanges(editor);
 
           // Cancel notification.
-          Intent intentAlarm = new Intent(context, LeanplumPushService.class);
+          Intent intentAlarm = new Intent(context, LeanplumLocalPushListenerService.class);
           AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
           PendingIntent existingIntent = PendingIntent.getService(
               context, messageId.hashCode(), intentAlarm, PendingIntent.FLAG_UPDATE_CURRENT);
           alarmManager.cancel(existingIntent);
 
           boolean didCancel = existingEta > System.currentTimeMillis();
           if (didCancel) {
             Log.i("Cancelled notification");
@@ -263,21 +255,17 @@ public class ActionManager {
     Context context = Leanplum.getContext();
     SharedPreferences preferences = context.getSharedPreferences(
         PREFERENCES_NAME, Context.MODE_PRIVATE);
     SharedPreferences.Editor editor = preferences.edit();
     editor.putString(
         String.format(Constants.Defaults.MESSAGE_IMPRESSION_OCCURRENCES_KEY, messageId),
         JsonConverter.toJson(occurrences));
     messageImpressionOccurrences.put(messageId, occurrences);
-    try {
-      editor.apply();
-    } catch (NoSuchMethodError e) {
-      editor.commit();
-    }
+   SharedPreferencesUtil.commitChanges(editor);
   }
 
   public int getMessageTriggerOccurrences(String messageId) {
     Number occurrences = messageTriggerOccurrences.get(messageId);
     if (occurrences != null) {
       return occurrences.intValue();
     }
     Context context = Leanplum.getContext();
@@ -292,21 +280,17 @@ public class ActionManager {
   public void saveMessageTriggerOccurrences(int occurrences, String messageId) {
     Context context = Leanplum.getContext();
     SharedPreferences preferences = context.getSharedPreferences(
         PREFERENCES_NAME, Context.MODE_PRIVATE);
     SharedPreferences.Editor editor = preferences.edit();
     editor.putInt(
         String.format(Constants.Defaults.MESSAGE_TRIGGER_OCCURRENCES_KEY, messageId), occurrences);
     messageTriggerOccurrences.put(messageId, occurrences);
-    try {
-      editor.apply();
-    } catch (NoSuchMethodError e) {
-      editor.commit();
-    }
+    SharedPreferencesUtil.commitChanges(editor);
   }
 
   public MessageMatchResult shouldShowMessage(String messageId, Map<String, Object> messageConfig,
       String when, String eventName, ContextualValues contextualValues) {
     MessageMatchResult result = new MessageMatchResult();
 
     // 1. Must not be muted.
     Context context = Leanplum.getContext();
@@ -586,21 +570,17 @@ public class ActionManager {
     if (messageId != null) {
       Context context = Leanplum.getContext();
       SharedPreferences preferences = context.getSharedPreferences(
           PREFERENCES_NAME, Context.MODE_PRIVATE);
       SharedPreferences.Editor editor = preferences.edit();
       editor.putBoolean(
           String.format(Constants.Defaults.MESSAGE_MUTED_KEY, messageId),
           true);
-      try {
-        editor.apply();
-      } catch (NoSuchMethodError e) {
-        editor.commit();
-      }
+      SharedPreferencesUtil.commitChanges(editor);
     }
   }
 
 
   public static void getForegroundandBackgroundRegionNames(Set<String> foregroundRegionNames,
       Set<String> backgroundRegionNames) {
     Map<String, Object> messages = VarCache.messages();
     for (String messageId : messages.keySet()) {
--- a/mobile/android/thirdparty/com/leanplum/internal/CollectionUtil.java
+++ b/mobile/android/thirdparty/com/leanplum/internal/CollectionUtil.java
@@ -167,9 +167,43 @@ public class CollectionUtil {
       return result;
     }
   }
 
   @SuppressWarnings({"unchecked"})
   public static <T> T uncheckedCast(Object obj) {
     return (T) obj;
   }
+
+  /**
+   * Gets value from map or default if key isn't found.
+   *
+   * @param map Map to get value from.
+   * @param key Key we are looking for.
+   * @param defaultValue Default value if key isn't found.
+   * @return Value or default if not found.
+   */
+  public static <K, V> V getOrDefault(Map<K, V> map, K key, V defaultValue) {
+    if (map == null) {
+      return defaultValue;
+    }
+    return map.containsKey(key) ? map.get(key) : defaultValue;
+  }
+
+  /**
+   * Converts an array of object Longs to primitives.
+   *
+   * @param array Array to convert.
+   * @return Array of long primitives.
+   */
+  public static long[] toPrimitive(final Long[] array) {
+    if (array == null) {
+      return null;
+    } else if (array.length == 0) {
+      return new long[0];
+    }
+    final long[] result = new long[array.length];
+    for (int i = 0; i < array.length; i++) {
+      result[i] = array[i].longValue();
+    }
+    return result;
+  }
 }
--- a/mobile/android/thirdparty/com/leanplum/internal/Constants.java
+++ b/mobile/android/thirdparty/com/leanplum/internal/Constants.java
@@ -16,31 +16,33 @@
  * "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.internal;
 
-//import com.leanplum.BuildConfig;
+
+import org.mozilla.gecko.thirdparty_unused.BuildConfig;
 
 /**
  * Leanplum constants.
  *
  * @author Andrew First.
  */
 public class Constants {
-  public static String API_HOST_NAME = "www.leanplum.com";
+  public static String API_HOST_NAME = "api.leanplum.com";
+  public static String API_SERVLET = "api";
+  public static boolean API_SSL = true;
   public static String SOCKET_HOST = "dev.leanplum.com";
   public static int SOCKET_PORT = 80;
-  public static boolean API_SSL = true;
   public static int NETWORK_TIMEOUT_SECONDS = 10;
   public static int NETWORK_TIMEOUT_SECONDS_FOR_DOWNLOADS = 10;
-  static final String LEANPLUM_PACKAGE_IDENTIFIER = "s";//BuildConfig.LEANPLUM_PACKAGE_IDENTIFIER;
+  static final String LEANPLUM_PACKAGE_IDENTIFIER = "s"; //TODO investigate what this should be
 
   public static String LEANPLUM_VERSION = "2.2.2-SNAPSHOT";
   public static String CLIENT = "android";
 
   static final String INVALID_MAC_ADDRESS = "02:00:00:00:00:00";
   static final String INVALID_MAC_ADDRESS_HASH = "0f607264fc6318a92b9e13c65db7cd3c";
 
   /**
@@ -61,21 +63,21 @@ public class Constants {
   public static boolean enableFileUploadingInDevelopmentMode = true;
   public static boolean canDownloadContentMidSessionInProduction = false;
   static boolean isInPermanentFailureState = false;
 
   public static boolean isNoop() {
     return isTestMode || isInPermanentFailureState;
   }
 
-  public static String API_SERVLET = "api";
-
   public static class Defaults {
+    public static final String LEANPLUM = "__leanplum__";
     public static final String COUNT_KEY = "__leanplum_unsynced";
     public static final String ITEM_KEY = "__leanplum_unsynced_%d";
+    public static final String UUID_KEY = "__leanplum_uuid";
     public static final String VARIABLES_KEY = "__leanplum_variables";
     public static final String ATTRIBUTES_KEY = "__leanplum_attributes";
     public static final String TOKEN_KEY = "__leanplum_token";
     public static final String MESSAGES_KEY = "__leanplum_messages";
     public static final String UPDATE_RULES_KEY = "__leanplum_update_rules";
     public static final String EVENT_RULES_KEY = "__leanplum_event_rules";
     public static final String REGIONS_KEY = "regions";
     public static final String MESSAGE_TRIGGER_OCCURRENCES_KEY =
@@ -84,16 +86,19 @@ public class Constants {
         "__leanplum_message_occurrences_%s";
     public static final String MESSAGE_MUTED_KEY = "__leanplum_message_muted_%s";
     public static final String LOCAL_NOTIFICATION_KEY = "__leanplum_local_message_%s";
     public static final String INBOX_KEY = "__leanplum_newsfeed";
     public static final String LEANPLUM_PUSH = "__leanplum_push__";
     public static final String APP_ID = "__app_id";
     public static final String PROPERTY_REGISTRATION_ID = "registration_id";
     public static final String PROPERTY_SENDER_IDS = "sender_ids";
+    public static final String NOTIFICATION_CHANNELS_KEY = "__leanplum_notification_channels";
+    public static final String DEFAULT_NOTIFICATION_CHANNEL_KEY = "__leanplum_default_notification_channels";
+    public static final String NOTIFICATION_GROUPS_KEY = "__leanplum_notification_groups";
   }
 
   public static class Methods {
     public static final String ADVANCE = "advance";
     public static final String DELETE_INBOX_MESSAGE = "deleteNewsfeedMessage";
     public static final String DOWNLOAD_FILE = "downloadFile";
     public static final String GET_INBOX_MESSAGES = "getNewsfeedMessages";
     public static final String GET_VARS = "getVars";
@@ -205,16 +210,19 @@ public class Constants {
     public static final String TIMEZONE_OFFSET_SECONDS = "timezoneOffsetSeconds";
     public static final String TITLE = "Title";
     public static final String INBOX_IMAGE = "Image";
     public static final String DATA = "Data";
     public static final String TOKEN = "token";
     public static final String VARIANTS = "variants";
     public static final String VARS = "vars";
     public static final String VARS_FROM_CODE = "varsFromCode";
+    public static final String NOTIFICATION_CHANNELS = "notificationChannels";
+    public static final String DEFAULT_NOTIFICATION_CHANNEL = "defaultNotificationChannel";
+    public static final String NOTIFICATION_GROUPS = "notificationChannelGroups";
   }
 
   public static class Kinds {
     public static final String INT = "integer";
     public static final String FLOAT = "float";
     public static final String STRING = "string";
     public static final String BOOLEAN = "bool";
     public static final String FILE = "file";
--- a/mobile/android/thirdparty/com/leanplum/internal/FileManager.java
+++ b/mobile/android/thirdparty/com/leanplum/internal/FileManager.java
@@ -360,17 +360,17 @@ public class FileManager {
       return;
     }
 
     try {
       final List<Pattern> compiledIncludePatterns = compilePatterns(patternsToInclude);
       final List<Pattern> compiledExcludePatterns = compilePatterns(patternsToExclude);
 
       if (isAsync) {
-        Util.executeAsyncTask(new AsyncTask<Void, Void, Void>() {
+        Util.executeAsyncTask(false, new AsyncTask<Void, Void, Void>() {
           @Override
           protected Void doInBackground(Void... params) {
             try {
               enableResourceSyncing(compiledIncludePatterns, compiledExcludePatterns);
             } catch (Throwable t) {
               Util.handleException(t);
             }
             return null;
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/internal/LeanplumEventCallbackManager.java
@@ -0,0 +1,152 @@
+package com.leanplum.internal;
+
+/*
+ * Copyright 2017, 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.
+ */
+
+import android.os.AsyncTask;
+import android.support.annotation.NonNull;
+
+import org.json.JSONObject;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * LeanplumEventCallbackManager class to handle event callbacks.
+ *
+ * @author Anna Orlova
+ */
+class LeanplumEventCallbackManager {
+    // Event callbacks map.
+    private final Map<Request, LeanplumEventCallbacks> eventCallbacks = new HashMap<>();
+
+    /**
+     * Add callbacks to the event callbacks Map.
+     *
+     * @param request          Event.
+     * @param responseCallback Response callback.
+     * @param errorCallback    Error callback.
+     */
+    void addCallbacks(Request request, Request.ResponseCallback responseCallback,
+                      Request.ErrorCallback errorCallback) {
+        if (request == null) {
+            return;
+        }
+
+        if (responseCallback == null && errorCallback == null) {
+            return;
+        }
+
+        eventCallbacks.put(request, new LeanplumEventCallbacks(responseCallback, errorCallback));
+    }
+
+    /**
+     * Invoke potential error callbacks for all events with database index less than a count of events
+     * that we got from database.
+     *
+     * @param error         Exception.
+     * @param countOfEvents Count of events that we got from database.
+     */
+    void invokeAllCallbacksWithError(@NonNull final Exception error, int countOfEvents) {
+        if (eventCallbacks.size() == 0) {
+            return;
+        }
+
+        Iterator<Map.Entry<Request, LeanplumEventCallbacks>> iterator =
+                eventCallbacks.entrySet().iterator();
+        // Loop over all callbacks.
+        for (; iterator.hasNext(); ) {
+            final Map.Entry<Request, LeanplumEventCallbacks> entry = iterator.next();
+            if (entry.getKey() == null) {
+                continue;
+            }
+            if (entry.getKey().getDataBaseIndex() >= countOfEvents) {
+                entry.getKey().setDataBaseIndex(entry.getKey().getDataBaseIndex() - countOfEvents);
+            } else {
+                if (entry.getValue() != null && entry.getValue().errorCallback != null) {
+                    // Start callback asynchronously, to avoid creation of new Request object from the same
+                    // thread.
+                    Util.executeAsyncTask(false, new AsyncTask<Void, Void, Void>() {
+                        @Override
+                        protected Void doInBackground(Void... params) {
+                            entry.getValue().errorCallback.error(error);
+                            return null;
+                        }
+                    });
+                }
+                iterator.remove();
+            }
+        }
+    }
+
+    /**
+     * Invoke potential response callbacks for all events with database index less than a count of
+     * events that we got from database.
+     *
+     * @param responseBody  JSONObject withs server response.
+     * @param countOfEvents Count of events that we got from database.
+     */
+    void invokeAllCallbacksForResponse(@NonNull final JSONObject responseBody, int countOfEvents) {
+        if (eventCallbacks.size() == 0) {
+            return;
+        }
+
+        Iterator<Map.Entry<Request, LeanplumEventCallbacks>> iterator =
+                eventCallbacks.entrySet().iterator();
+        // Loop over all callbacks.
+        for (; iterator.hasNext(); ) {
+            final Map.Entry<Request, LeanplumEventCallbacks> entry = iterator.next();
+            if (entry.getKey() == null) {
+                continue;
+            }
+
+            if (entry.getKey().getDataBaseIndex() >= countOfEvents) {
+                entry.getKey().setDataBaseIndex(entry.getKey().getDataBaseIndex() - countOfEvents);
+            } else {
+                if (entry.getValue() != null && entry.getValue().responseCallback != null) {
+                    // Start callback asynchronously, to avoid creation of new Request object from the same
+                    // thread.
+                    Util.executeAsyncTask(false, new AsyncTask<Void, Void, Void>() {
+                        @Override
+                        protected Void doInBackground(Void... params) {
+                            entry.getValue().responseCallback.response(Request.getResponseAt(responseBody,
+                                    (int) entry.getKey().getDataBaseIndex()));
+                            return null;
+                        }
+                    });
+                }
+                iterator.remove();
+            }
+        }
+    }
+
+    private static class LeanplumEventCallbacks {
+        private Request.ResponseCallback responseCallback;
+        private Request.ErrorCallback errorCallback;
+
+        LeanplumEventCallbacks(Request.ResponseCallback responseCallback, Request.ErrorCallback
+                errorCallback) {
+            this.responseCallback = responseCallback;
+            this.errorCallback = errorCallback;
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/internal/LeanplumEventDataManager.java
@@ -0,0 +1,259 @@
+package com.leanplum.internal;
+
+/*
+ * Copyright 2017, 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.
+ */
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+
+import com.leanplum.Leanplum;
+import com.leanplum.utils.SharedPreferencesUtil;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * LeanplumEventDataManager class to work with SQLite.
+ *
+ * @author Anna Orlova
+ */
+public class LeanplumEventDataManager {
+    private static final String DATABASE_NAME = "__leanplum.db";
+    private static final int DATABASE_VERSION = 1;
+    private static final String EVENT_TABLE_NAME = "event";
+    private static final String COLUMN_DATA = "data";
+    private static final String KEY_ROWID = "rowid";
+
+    private static SQLiteDatabase database;
+    private static LeanplumDataBaseManager databaseManager;
+    private static ContentValues contentValues = new ContentValues();
+
+    static boolean willSendErrorLog = false;
+
+    /**
+     * Creates connection to database, if database is not present, it will automatically create it.
+     *
+     * @param context Current context.
+     */
+    public static void init(Context context) {
+        if (database != null) {
+            Log.e("Database is already initialized.");
+            return;
+        }
+
+        // Create database if needed.
+        try {
+            if (databaseManager == null) {
+                databaseManager = new LeanplumDataBaseManager(context);
+            }
+            database = databaseManager.getWritableDatabase();
+        } catch (Throwable t) {
+            handleSQLiteError("Cannot create database.", t);
+        }
+    }
+
+    /**
+     * Inserts event to event table.
+     *
+     * @param event String with json of event.
+     */
+    static void insertEvent(String event) {
+        if (database == null) {
+            return;
+        }
+        contentValues.put(COLUMN_DATA, event);
+        try {
+            database.insert(EVENT_TABLE_NAME, null, contentValues);
+            willSendErrorLog = false;
+        } catch (Throwable t) {
+            handleSQLiteError("Unable to insert event to database.", t);
+        }
+        contentValues.clear();
+    }
+
+    /**
+     * Gets first count events from event table.
+     *
+     * @param count Number of events.
+     * @return List of events.
+     */
+    static List<Map<String, Object>> getEvents(int count) {
+        List<Map<String, Object>> events = new ArrayList<>();
+        if (database == null) {
+            return events;
+        }
+        Cursor cursor = null;
+        try {
+            cursor = database.query(EVENT_TABLE_NAME, new String[]{COLUMN_DATA}, null, null, null,
+                    null, KEY_ROWID + " ASC", "" + count);
+            willSendErrorLog = false;
+            while (cursor.moveToNext()) {
+                Map<String, Object> requestArgs = JsonConverter.mapFromJson(new JSONObject(
+                        cursor.getString(cursor.getColumnIndex(COLUMN_DATA))));
+                events.add(requestArgs);
+            }
+        } catch (Throwable t) {
+            handleSQLiteError("Unable to get events from the table.", t);
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+        return events;
+    }
+
+    /**
+     * Deletes first count elements from event table.
+     *
+     * @param count Number of event that need to be deleted.
+     */
+    static void deleteEvents(int count) {
+        if (database == null) {
+            return;
+        }
+        try {
+            database.delete(EVENT_TABLE_NAME, KEY_ROWID + " in (select " + KEY_ROWID + " from " +
+                    EVENT_TABLE_NAME + " ORDER BY " + KEY_ROWID + " ASC LIMIT " + count + ")", null);
+            willSendErrorLog = false;
+        } catch (Throwable t) {
+            handleSQLiteError("Unable to delete events from the table.", t);
+        }
+    }
+
+    /**
+     * Gets number of rows in the event table.
+     *
+     * @return Number of rows in the event table.
+     */
+    static long getEventsCount() {
+        long count = 0;
+        if (database == null) {
+            return count;
+        }
+        try {
+            count = DatabaseUtils.queryNumEntries(database, EVENT_TABLE_NAME);
+            willSendErrorLog = false;
+        } catch (Throwable t) {
+            handleSQLiteError("Unable to get a number of rows in the table.", t);
+        }
+        return count;
+    }
+
+    /**
+     * Helper function that logs and sends errors to the server.
+     */
+    private static void handleSQLiteError(String log, Throwable t) {
+        Log.e(log, t);
+        // Send error log. Using willSendErrorLog to prevent infinte loop.
+        if (!willSendErrorLog) {
+            willSendErrorLog = true;
+            Util.handleException(t);
+        }
+    }
+
+    private static class LeanplumDataBaseManager extends SQLiteOpenHelper {
+        LeanplumDataBaseManager(Context context) {
+            super(context, DATABASE_NAME, null, DATABASE_VERSION);
+        }
+
+        @Override
+        public void onCreate(SQLiteDatabase db) {
+            // Create event table.
+            db.execSQL("CREATE TABLE IF NOT EXISTS " + EVENT_TABLE_NAME + "(" + COLUMN_DATA +
+                    " TEXT)");
+
+            // Migrate old data from shared preferences.
+            try {
+                migrateFromSharedPreferences(db);
+            } catch (Throwable t) {
+                Log.e("Cannot move old data from shared preferences to SQLite table.", t);
+                Util.handleException(t);
+            }
+        }
+
+        @Override
+        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+            // No used for now.
+        }
+
+        /**
+         * Migrate data from shared preferences to SQLite.
+         */
+        private static void migrateFromSharedPreferences(SQLiteDatabase db) {
+            synchronized (Request.class) {
+                Context context = Leanplum.getContext();
+                SharedPreferences preferences = context.getSharedPreferences(
+                        Request.LEANPLUM, Context.MODE_PRIVATE);
+                SharedPreferences.Editor editor = preferences.edit();
+                int count = preferences.getInt(Constants.Defaults.COUNT_KEY, 0);
+                if (count == 0) {
+                    return;
+                }
+
+                List<Map<String, Object>> requestData = new ArrayList<>();
+                for (int i = 0; i < count; i++) {
+                    String itemKey = String.format(Locale.US, Constants.Defaults.ITEM_KEY, i);
+                    Map<String, Object> requestArgs;
+                    try {
+                        requestArgs = JsonConverter.mapFromJson(new JSONObject(
+                                preferences.getString(itemKey, "{}")));
+                        requestData.add(requestArgs);
+                    } catch (JSONException e) {
+                        e.printStackTrace();
+                    }
+                    editor.remove(itemKey);
+                }
+
+                editor.remove(Constants.Defaults.COUNT_KEY);
+
+                try {
+                    String uuid = preferences.getString(Constants.Defaults.UUID_KEY, null);
+                    if (uuid == null || count % Request.MAX_EVENTS_PER_API_CALL == 0) {
+                        uuid = UUID.randomUUID().toString();
+                        editor.putString(Constants.Defaults.UUID_KEY, uuid);
+                    }
+                    for (Map<String, Object> event : requestData) {
+                        event.put(Request.UUID_KEY, uuid);
+                        contentValues.put(COLUMN_DATA, JsonConverter.toJson(event));
+                        db.insert(EVENT_TABLE_NAME, null, contentValues);
+                        contentValues.clear();
+                    }
+                    SharedPreferencesUtil.commitChanges(editor);
+                } catch (Throwable t) {
+                    Log.e("Failed on migration data from shared preferences.", t);
+                    Util.handleException(t);
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
--- a/mobile/android/thirdparty/com/leanplum/internal/LeanplumInternal.java
+++ b/mobile/android/thirdparty/com/leanplum/internal/LeanplumInternal.java
@@ -36,16 +36,17 @@ import com.leanplum.callbacks.StartCallb
 import com.leanplum.callbacks.VariablesChangedCallback;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.ConcurrentModificationException;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Queue;
 import java.util.concurrent.ConcurrentLinkedQueue;
@@ -100,17 +101,17 @@ public class LeanplumInternal {
       }
     }
   }
 
   public static void triggerAction(ActionContext context) {
     triggerAction(context, null);
   }
 
-  private static void triggerAction(final ActionContext context,
+  public static void triggerAction(final ActionContext context,
       final VariablesChangedCallback handledCallback) {
     List<ActionCallback> callbacks;
     synchronized (actionHandlers) {
       List<ActionCallback> handlers = actionHandlers.get(context.actionName());
       if (handlers == null) {
         // Handled by default.
         if (handledCallback != null) {
           handledCallback.variablesChanged();
@@ -224,37 +225,44 @@ public class LeanplumInternal {
               priority);
           actionContext.setContextualValues(contextualValues);
           actionContexts.add(actionContext);
         }
       }
     }
 
     if (!actionContexts.isEmpty()) {
-      Collections.sort(actionContexts);
+      Collections.sort(actionContexts, new Comparator<ActionContext>() {
+        @Override
+        public int compare(ActionContext o1, ActionContext o2) {
+          return o1.getPriority() - o2.getPriority();
+        }
+      });
       int priorityThreshold = actionContexts.get(0).getPriority();
+      boolean messageActionTriggered = false;
       for (final ActionContext actionContext : actionContexts) {
-        if (actionContext.getPriority() <= priorityThreshold) {
-          if (actionContext.actionName().equals(ActionManager.HELD_BACK_ACTION_NAME)) {
-            ActionManager.getInstance().recordHeldBackImpression(
-                actionContext.getMessageId(), actionContext.getOriginalMessageId());
-          } else {
-            LeanplumInternal.triggerAction(actionContext, new VariablesChangedCallback() {
-              @Override
-              public void variablesChanged() {
-                try {
-                  ActionManager.getInstance().recordMessageImpression(actionContext.getMessageId());
-                } catch (Throwable t) {
-                  Util.handleException(t);
-                }
+        if (actionContext.getPriority() > priorityThreshold) {
+          break;
+        }
+
+        if (actionContext.actionName().equals(ActionManager.HELD_BACK_ACTION_NAME)) {
+          ActionManager.getInstance().recordHeldBackImpression(
+              actionContext.getMessageId(), actionContext.getOriginalMessageId());
+        } else if (!messageActionTriggered) {
+          messageActionTriggered = true;
+          LeanplumInternal.triggerAction(actionContext, new VariablesChangedCallback() {
+            @Override
+            public void variablesChanged() {
+              try {
+                ActionManager.getInstance().recordMessageImpression(actionContext.getMessageId());
+              } catch (Throwable t) {
+                Util.handleException(t);
               }
-            });
-          }
-        } else {
-          break;
+            }
+          });
         }
       }
     }
   }
 
   public static void track(final String event, double value, String info,
       Map<String, ?> params, Map<String, String> args) {
     if (Constants.isNoop()) {
@@ -379,17 +387,17 @@ public class LeanplumInternal {
    */
   public static void setUserLocationAttribute(final Location location,
       final LeanplumLocationAccuracyType locationAccuracyType,
       final locationAttributeRequestsCallback callback) {
     Leanplum.addStartResponseHandler(new StartCallback() {
       @Override
       public void onResponse(final boolean success) {
         // Geocoder query must be executed on background thread.
-        Util.executeAsyncTask(new AsyncTask<Void, Void, Void>() {
+        Util.executeAsyncTask(false, new AsyncTask<Void, Void, Void>() {
           @Override
           protected Void doInBackground(Void... voids) {
             if (!success) {
               return null;
             }
             if (location == null) {
               Log.e("Location can't be null in setUserLocationAttribute.");
               return null;
@@ -408,16 +416,19 @@ public class LeanplumInternal {
                 if (addresses != null && addresses.size() > 0) {
                   Address address = addresses.get(0);
                   params.put(Constants.Keys.CITY, address.getLocality());
                   params.put(Constants.Keys.REGION, address.getAdminArea());
                   params.put(Constants.Keys.COUNTRY, address.getCountryCode());
                 }
               } catch (IOException e) {
                 Log.e("Failed to connect to Geocoder: " + e);
+              } catch (IllegalArgumentException e) {
+                Log.e("Invalid latitude or longitude values: " + e);
+              } catch (Throwable ignored) {
               }
             }
             Request req = Request.post(Constants.Methods.SET_USER_ATTRIBUTES, params);
             req.onResponse(new Request.ResponseCallback() {
               @Override
               public void response(JSONObject response) {
                 callback.response(true);
               }
@@ -527,20 +538,16 @@ public class LeanplumInternal {
     if (attributes == null) {
       return null;
     }
     Map<String, T> validAttributes = new HashMap<>();
     try {
       for (Map.Entry<String, T> entry : attributes.entrySet()) {
         T value = entry.getValue();
 
-        if (value == null) {
-          continue;
-        }
-
         // Validate lists.
         if (allowLists && value instanceof Iterable<?>) {
           boolean valid = true;
           Iterable<Object> iterable = CollectionUtil.uncheckedCast(value);
           for (Object item : iterable) {
             if (!isValidScalarValue(item, argName)) {
               valid = false;
               break;
@@ -551,17 +558,17 @@ public class LeanplumInternal {
           }
 
           // Validate scalars.
         } else {
           if (value instanceof Date) {
             Date date = CollectionUtil.uncheckedCast(value);
             value = CollectionUtil.uncheckedCast(date.getTime());
           }
-          if (!isValidScalarValue(value, argName)) {
+          if (value != null && !isValidScalarValue(value, argName)) {
             continue;
           }
         }
         validAttributes.put(entry.getKey(), value);
       }
     } catch (ConcurrentModificationException e) {
       maybeThrowException(new LeanplumException("ConcurrentModificationException: You cannot " +
           "modify Map<String, ?> attributes/parameters. Will override with an empty map"));
--- a/mobile/android/thirdparty/com/leanplum/internal/LeanplumManifestHelper.java
+++ b/mobile/android/thirdparty/com/leanplum/internal/LeanplumManifestHelper.java
@@ -16,495 +16,184 @@
  * "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.internal;
 
+import android.content.ComponentName;
 import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.ComponentInfo;
 import android.content.pm.PackageManager;
-import android.text.TextUtils;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
 
 import com.leanplum.Leanplum;
-
-import org.w3c.dom.Document;
-import org.w3c.dom.NamedNodeMap;
-import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
+import com.leanplum.LeanplumPushService;
 
-import java.io.ByteArrayInputStream;
-import java.io.DataInputStream;
-import java.util.ArrayList;
-import java.util.HashSet;
 import java.util.List;
-import java.util.jar.JarFile;
-import java.util.zip.ZipEntry;
-
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
 
 /**
  * LeanplumManifestHelper class to work with AndroidManifest components.
  *
  * @author Anna Orlova
  */
 public class LeanplumManifestHelper {
-  private static final String MANIFEST = "manifest";
-  private static final String APPLICATION = "application";
-  private static final String SERVICE = "service";
-  private static final String ACTIVITY = "activity";
-  private static final String ACTIVITY_ALIAS = "activity-alias";
-  private static final String RECEIVER = "receiver";
-  private static final String PROVIDER = "provider";
-  private static final String ANDROID_NAME = "android:name";
-  private static final String ANDROID_SCHEME = "android:scheme";
-  private static final String ACTION = "action";
-  private static final String CATEGORY = "category";
-  private static final String DATA = "data";
-  private static final String ANDROID_HOST = "android:host";
-  private static final String ANDROID_PORT = "android:port";
-  private static final String ANDROID_PATH = "android:path";
-  private static final String ANDROID_PATH_PATTERN = "android:pathPattern";
-  private static final String ANDROID_PATH_PREFIX = "android:pathPrefix";
-  private static final String ANDROID_MIME_TYPE = "android:mimeType";
-  private static final String ANDROID_TYPE = "android:type";
-  private static final String INTENT_FILTER = "intent-filter";
-  private static final String ANDROID_PERMISSION = "android:permission";
-  private static final String ANDROID_MANIFEST = "AndroidManifest.xml";
-  private static final String VERSION_NAME = "versionName";
-
-  private static ManifestData manifestData;
+  // Google Cloud Messaging
+  public static final String GCM_SEND_PERMISSION = "com.google.android.c2dm.permission.SEND";
+  public static final String GCM_RECEIVE_PERMISSION = "com.google.android.c2dm.permission.RECEIVE";
+  public static final String GCM_RECEIVE_ACTION = "com.google.android.c2dm.intent.RECEIVE";
+  public static final String GCM_REGISTRATION_ACTION = "com.google.android.c2dm.intent.REGISTRATION";
+  public static final String GCM_INSTANCE_ID_ACTION = "com.google.android.gms.iid.InstanceID";
+  public static final String GCM_RECEIVER = "com.google.android.gms.gcm.GcmReceiver";
 
-  /**
-   * Gets application components from AndroidManifest.xml file.
-   */
-  private static void parseManifestNodeChildren() {
-    manifestData = new ManifestData();
-    byte[] manifestXml = getByteArrayOfManifest();
-    Document manifestDocument = getManifestDocument(manifestXml);
-    parseManifestDocument(manifestDocument);
-  }
+  // Firebase
+  public static final String FCM_INSTANCE_ID_EVENT = "com.google.firebase.INSTANCE_ID_EVENT";
+  public static final String FCM_MESSAGING_EVENT = "com.google.firebase.MESSAGING_EVENT";
 
-  /**
-   * Parse AndroidManifest.xml file to byte array.
-   *
-   * @return byte[] Byte array of AndroidManifest.xml file
-   */
-  private static byte[] getByteArrayOfManifest() {
-    Context context = Leanplum.getContext();
-    if (context == null) {
-      Log.e("Context is null. Cannot parse " + ANDROID_MANIFEST + " file.");
-      return null;
-    }
-    byte[] manifestXml = null;
-    try {
-      JarFile jarFile = new JarFile(context.getPackageResourcePath());
-      ZipEntry entry = jarFile.getEntry(ANDROID_MANIFEST);
-      manifestXml = new byte[(int) entry.getSize()];
-      DataInputStream dataInputStream = new DataInputStream(jarFile.getInputStream(entry));
-      dataInputStream.readFully(manifestXml);
-      dataInputStream.close();
-    } catch (Exception e) {
-      Log.e("Cannot parse " + ANDROID_MANIFEST + " file: " + e.getMessage());
-    }
-    return manifestXml;
-  }
+  // Leanplum
+  public static final String LP_PUSH_INSTANCE_ID_SERVICE = "com.leanplum.LeanplumPushInstanceIDService";
+  public static final String LP_PUSH_LISTENER_SERVICE = "com.leanplum.LeanplumPushListenerService";
+  public static final String LP_PUSH_FCM_LISTENER_SERVICE = "com.leanplum.LeanplumPushFcmListenerService";
+  public static final String LP_PUSH_FCM_MESSAGING_SERVICE = "com.leanplum.LeanplumPushFirebaseMessagingService";
+  public static final String LP_PUSH_REGISTRATION_SERVICE = "com.leanplum.LeanplumPushRegistrationService";
+  public static final String LP_PUSH_RECEIVER = "com.leanplum.LeanplumPushReceiver";
 
   /**
-   * Gets Document {@link Document} of AndroidManifest.xml file.
+   * Gets Class for name.
    *
-   * @param manifestXml Byte array of AndroidManifest.xml file data.
-   * @return Document Document with date of AndroidManifest.xml file.
-   */
-  private static Document getManifestDocument(byte[] manifestXml) {
-    Document document = null;
-    try {
-      DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
-      DocumentBuilder builder = builderFactory.newDocumentBuilder();
-      document = builder.parse(new ByteArrayInputStream(
-          LeanplumManifestParser.decompressXml(manifestXml).getBytes("UTF-8")));
-    } catch (Exception e) {
-      Log.e("Cannot parse " + ANDROID_MANIFEST + " file: " + e.getMessage());
-    }
-    return document;
-  }
-
-  /**
-   * Parse data from Document {@link Document} with date of AndroidManifest.xml file.
-   *
-   * @param document Document {@link Document} with date of AndroidManifest.xml file.
+   * @param className - class name.
+   * @return Class for provided class name.
    */
-  private static void parseManifestDocument(Document document) {
-    if (document == null) {
-      return;
-    }
-    parseManifestNode(document.getElementsByTagName(MANIFEST).item(0));
-  }
-
-  private static void parseManifestNode(Node manifestNode) {
-    if (manifestNode == null) {
-      return;
-    }
-    manifestData.appVersionName = getAttribute(manifestNode.getAttributes(), VERSION_NAME);
-    NodeList manifestChildren = manifestNode.getChildNodes();
-    if (manifestChildren == null) {
-      return;
-    }
-    for (int i = 0; i < manifestChildren.getLength(); i++) {
-      Node currentNode = manifestChildren.item(i);
-      if (currentNode == null) {
-        continue;
-      }
-      String currentNodeName = currentNode.getNodeName();
-      if (APPLICATION.equals(currentNodeName)) {
-        parseChildNodeList(currentNode.getChildNodes());
-      }
-    }
-  }
-
-  private static void parseChildNodeList(NodeList childrenList) {
-    if (childrenList == null) {
-      return;
-    }
-    for (int j = 0; j < childrenList.getLength(); j++) {
-      parseChildNode(childrenList.item(j));
-    }
-  }
-
-  private static void parseChildNode(Node child) {
-    if (child == null) {
-      return;
-    }
-    String childName = child.getNodeName();
-    if (childName == null) {
-      return;
-    }
-    switch (childName) {
-      case SERVICE:
-        manifestData.services.add(parseManifestComponent(child,
-            ManifestComponent.ApplicationComponent.SERVICE));
-        break;
-      case RECEIVER:
-        manifestData.receivers.add(parseManifestComponent(child,
-            ManifestComponent.ApplicationComponent.RECEIVER));
-        break;
-      case ACTIVITY:
-      case ACTIVITY_ALIAS:
-        manifestData.activities.add(parseManifestComponent(child,
-            ManifestComponent.ApplicationComponent.ACTIVITY));
-        break;
-      case PROVIDER:
-        manifestData.providers.add(parseManifestComponent(child,
-            ManifestComponent.ApplicationComponent.PROVIDER));
-        break;
-      default:
-        break;
+  public static Class getClassForName(String className) {
+    try {
+      return Class.forName(className);
+    } catch (Throwable t) {
+      return null;
     }
   }
 
   /**
-   * Parse AndroidManifest.xml components from XML.
+   * Enables and starts service for provided class name.
    *
-   * @param node XML node to parse.
-   * @param type Type of application component {@link ManifestComponent.ApplicationComponent}.
-   * @return Return ManifestComponent {@link ManifestComponent} with information from manifest.
+   * @param context Current Context.
+   * @param packageManager Current PackageManager.
+   * @param clazz Class of service that needs to be enabled and started.
+   * @return True if service was enabled and started.
    */
-  private static ManifestComponent parseManifestComponent(Node node,
-      ManifestComponent.ApplicationComponent type) {
-    ManifestComponent manifestComponent = new ManifestComponent(type);
-    NamedNodeMap attributes = node.getAttributes();
-    manifestComponent.name = getAttribute(attributes, ANDROID_NAME);
-    manifestComponent.permission = getAttribute(attributes, ANDROID_PERMISSION);
-    List<ManifestIntentFilter> intentFilters = new ArrayList<>();
-    NodeList childrenList = node.getChildNodes();
-    for (int i = 0; i < childrenList.getLength(); i++) {
-      Node child = childrenList.item(i);
-      String childName = child.getNodeName();
-      if (INTENT_FILTER.equals(childName)) {
-        ManifestIntentFilter intentFilter = parseManifestIntentFilter(child);
-        if (intentFilter != null) {
-          intentFilters.add(intentFilter);
-        }
-      }
+  public static boolean enableServiceAndStart(Context context, PackageManager packageManager,
+      Class clazz) {
+    if (!enableComponent(context, packageManager, clazz)) {
+      return false;
     }
-    manifestComponent.intentFilters = intentFilters;
-    return manifestComponent;
+    try {
+      context.startService(new Intent(context, clazz));
+    } catch (Throwable t) {
+      Log.w("Could not start service " + clazz.getName());
+      return false;
+    }
+    return true;
   }
 
   /**
-   * Parse intent filter from XML node.
+   * Enables component for provided class.
    *
-   * @param intentNode XML node to parse.
-   * @return Return ManifestIntentFilter {@link ManifestIntentFilter} with information from
-   * manifest.
+   * @param context Current Context.
+   * @param packageManager Current PackageManager.
+   * @param clazz Class for enable.
+   * @return True if component was enabled.
    */
-  private static ManifestIntentFilter parseManifestIntentFilter(Node intentNode) {
-    if (intentNode == null) {
-      return null;
-    }
-
-    NodeList intentChildren = intentNode.getChildNodes();
-    if (intentChildren == null) {
-      return null;
+  public static boolean enableComponent(Context context, PackageManager packageManager,
+      Class clazz) {
+    if (clazz == null || context == null || packageManager == null) {
+      return false;
     }
 
-    ManifestIntentFilter intentFilter = new ManifestIntentFilter();
-    intentFilter.attributes = intentNode.getAttributes();
-    for (int j = 0; j < intentChildren.getLength(); j++) {
-      Node intentChild = intentChildren.item(j);
-      String intentChildName = intentChild.getNodeName();
-      NamedNodeMap intentChildAttributes = intentChild.getAttributes();
-      switch (intentChildName) {
-        case ACTION:
-          intentFilter.actions.add(getAttribute(intentChildAttributes, ANDROID_NAME));
-          break;
-        case CATEGORY:
-          intentFilter.categories.add(getAttribute(intentChildAttributes, ANDROID_NAME));
-          break;
-        case DATA:
-          String scheme = getAttribute(intentChildAttributes, ANDROID_SCHEME);
-          String host = getAttribute(intentChildAttributes, ANDROID_HOST);
-          String port = getAttribute(intentChildAttributes, ANDROID_PORT);
-          String path = getAttribute(intentChildAttributes, ANDROID_PATH);
-          String pathPattern = getAttribute(intentChildAttributes, ANDROID_PATH_PATTERN);
-          String pathPrefix = getAttribute(intentChildAttributes, ANDROID_PATH_PREFIX);
-          String mimeType = getAttribute(intentChildAttributes, ANDROID_MIME_TYPE);
-          String type = getAttribute(intentChildAttributes, ANDROID_TYPE);
-          intentFilter.dataList.add(new ManifestIntentFilter.IntentData(scheme, host, port, path,
-              pathPattern, pathPrefix, mimeType, type));
-          break;
-        default:
-          break;
-      }
-    }
-    return intentFilter;
-  }
-
-  /**
-   * @return Return String with attribute value or null if attribute with this name not found.
-   */
-  private static String getAttribute(NamedNodeMap namedNodeMap, String name) {
-    Node node = namedNodeMap.getNamedItem(name);
-    if (node == null) {
-      if (name.startsWith("android:")) {
-        name = name.substring("android:".length());
-      }
-      node = namedNodeMap.getNamedItem(name);
-      if (node == null) {
-        return null;
-      }
+    try {
+      packageManager.setComponentEnabledSetting(new ComponentName(context, clazz),
+          PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
+    } catch (Throwable t) {
+      Log.w("Could not enable component " + clazz.getName());
+      return false;
     }
-    return node.getNodeValue();
-  }
-
-  /**
-   * @return Return List of services from manifest.
-   */
-  public static List<ManifestComponent> getServices() {
-    if (manifestData == null) {
-      parseManifestNodeChildren();
-    }
-    return manifestData.services;
-  }
-
-  /**
-   * @return Return List of activities from manifest.
-   */
-  static List<ManifestComponent> getActivities() {
-    if (manifestData == null) {
-      parseManifestNodeChildren();
-    }
-    return manifestData.activities;
-  }
-
-  /**
-   * @return Return List of providers from manifest.
-   */
-  static List<ManifestComponent> getProviders() {
-    if (manifestData == null) {
-      parseManifestNodeChildren();
-    }
-    return manifestData.providers;
-  }
-
-  /**
-   * @return Return List of receivers from manifest.
-   */
-  public static List<ManifestComponent> getReceivers() {
-    if (manifestData == null) {
-      parseManifestNodeChildren();
-    }
-    return manifestData.receivers;
-  }
-
-  /**
-   * @return String String of application version name.
-   */
-  public static String getAppVersionName() {
-    if (manifestData == null) {
-      parseManifestNodeChildren();
-    }
-    return manifestData.appVersionName;
+    return true;
   }
 
   /**
-   * Verifies that a certain component (receiver or service) is implemented in the
-   * AndroidManifest.xml file or the application, in order to make sure that push notifications
-   * work.
+   * Disables component for provided class name.
    *
-   * @param componentsList List of application components(services or receivers).
-   * @param name The name of the class.
-   * @param exported What the exported option should be.
-   * @param permission Whether we need any permission.
-   * @param actions What actions we need to check for in the intent-filter.
-   * @param packageName The package name for the category tag, if we require one.
-   * @return true if the respective component is in the manifest file, and false otherwise.
+   * @param context The application context.
+   * @param packageManager Application Package manager.
+   * @param className Class name to disable.
+   * @return True if component was disabled successfully, false otherwise.
    */
-  public static boolean checkComponent(List<ManifestComponent> componentsList,
-      String name, boolean exported, String permission, List<String> actions, String packageName) {
-    boolean hasComponent = hasComponent(componentsList, name, permission, actions);
-    if (!hasComponent && !componentsList.isEmpty()) {
-      Log.e(getComponentError(componentsList.get(0).type, name, exported, permission, actions,
-          packageName));
+  public static boolean disableComponent(Context context, PackageManager packageManager, String className) {
+    if (context == null || packageManager == null || className == null) {
+      return false;
     }
-    return hasComponent;
-  }
-
-  /**
-   * Check if list of application components contains class instance of class with name className.
-   *
-   * @param componentsList List of application components(services or receivers).
-   * @param className The name of the class.
-   * @param permission Whether we need any permission..
-   * @param actions What actions we need to check for in the intent-filter.
-   * @return boolean True if componentList contains class instance of class with name className.
-   */
-  private static boolean hasComponent(List<ManifestComponent> componentsList, String className,
-      String permission, List<String> actions) {
-    for (ManifestComponent component : componentsList) {
-      if (isInstance(component, className)) {
-        if (hasPermission(component, permission, actions)) {
-          return true;
-        }
-      }
+    try {
+      packageManager.setComponentEnabledSetting(new ComponentName(context, className),
+          PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP);
+    } catch (Throwable t) {
+      return false;
     }
-    return false;
+    return true;
   }
 
   /**
-   * Check if component instance of class with name className.
+   * Checks if component for provided class enabled before.
    *
-   * @param component Application component(service or receiver).
-   * @param className The name of the class.
-   * @return boolean True if component instance of class with name className.
+   * @param context Current Context.
+   * @param packageManager Current PackageManager.
+   * @param clazz Class for check.
+   * @return True if component was enabled before.
    */
-  private static boolean isInstance(ManifestComponent component, String className) {
-    try {
-      if (component.name.equals(className)) {
-        return true;
-      } else {
-        Class clazz = null;
-        try {
-          clazz = Class.forName(component.name);
-        } catch (Throwable ignored) {
-        }
-        if (clazz == null) {
-          Log.w("Cannot find class with name: " + component.name);
-          return false;
-        }
-        while (clazz != Object.class) {
-          clazz = clazz.getSuperclass();
-          if (clazz.getName().equals(className)) {
-            return true;
-          }
-        }
-      }
-      return false;
-    } catch (Exception e) {
-      Util.handleException(e);
+  public static boolean wasComponentEnabled(Context context, PackageManager packageManager,
+      Class clazz) {
+    if (clazz == null || context == null || packageManager == null) {
       return false;
     }
+    int componentStatus = packageManager.getComponentEnabledSetting(new ComponentName(context,
+        clazz));
+    if (PackageManager.COMPONENT_ENABLED_STATE_DEFAULT == componentStatus ||
+        PackageManager.COMPONENT_ENABLED_STATE_DISABLED == componentStatus) {
+      return false;
+    }
+    return true;
   }
 
   /**
-   * Check if application component has permission with provided actions.
+   * Parses and returns client broadcast receiver class name.
    *
-   * @param component application component(service or receiver).
-   * @param permission Whether we need any permission.
-   * @param actions What actions we need to check for in the intent-filter.
-   * @return boolean True if component has permission with actions.
+   * @return Client broadcast receiver class name.
    */
-  private static boolean hasPermission(ManifestComponent component, String permission,
-      List<String> actions) {
-    Boolean hasPermissions = TextUtils.equals(component.permission, permission);
-    if (hasPermissions && actions != null) {
-      HashSet<String> actionsToCheck = new HashSet<>(actions);
-      for (ManifestIntentFilter intentFilter : component.intentFilters) {
-        actionsToCheck.removeAll(intentFilter.actions);
-      }
-      if (actionsToCheck.isEmpty()) {
-        return true;
-      }
-    } else if (hasPermissions) {
-      return true;
+  public static String parseNotificationMetadata() {
+    try {
+      Context context = Leanplum.getContext();
+      ApplicationInfo app = context.getPackageManager().getApplicationInfo(context.getPackageName(),
+          PackageManager.GET_META_DATA);
+      Bundle bundle = app.metaData;
+      return bundle.getString(LeanplumPushService.LEANPLUM_NOTIFICATION);
+    } catch (Throwable ignored) {
     }
-    return false;
+    return null;
   }
 
-  /**
-   * Gets string of error message with instruction how to set up AndroidManifest.xml for push
-   * notifications.
-   *
-   * @return String String of error message with instruction how to set up AndroidManifest.xml for
-   * push notifications.
-   */
-  private static String getComponentError(ManifestComponent.ApplicationComponent componentType,
-      String name, boolean exported, String permission, List<String> actions, String packageName) {
-    StringBuilder errorMessage = new StringBuilder("Push notifications requires you to add the " +
-        componentType.name().toLowerCase() + " " + name + " to your AndroidManifest.xml file." +
-        "Add this code within the <application> section:\n");
-    errorMessage.append("<").append(componentType.name().toLowerCase()).append("\n");
-    errorMessage.append("    android:name=\"").append(name).append("\"\n");
-    errorMessage.append("    android:exported=\"").append(Boolean.toString(exported)).append("\"");
-    if (permission != null) {
-      errorMessage.append("\n    android:permission=\"").append(permission).append("\"");
-    }
-    errorMessage.append(">\n");
-    if (actions != null) {
-      errorMessage.append("    <intent-filter>\n");
-      for (String action : actions) {
-        errorMessage.append("        <action android:name=\"").append(action).append("\" />\n");
-      }
-      if (packageName != null) {
-        errorMessage.append("        <category android:name=\"").append(packageName)
-            .append("\" />\n");
-      }
-      errorMessage.append("    </intent-filter>\n");
-    }
-    errorMessage.append("</").append(componentType.name().toLowerCase()).append(">");
-    return errorMessage.toString();
-  }
-
-  /**
-   * Check if the application has registered for a certain permission.
-   *
-   * @param permission Requested permission.
-   * @param definesPermission Permission need definition or not.
-   * @param logError Need print log or not.
-   * @return boolean True if application has permission.
-   */
   public static boolean checkPermission(String permission, boolean definesPermission,
       boolean logError) {
     Context context = Leanplum.getContext();
     if (context == null) {
       return false;
     }
-    if (context.checkCallingOrSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
+    int result = context.checkCallingOrSelfPermission(permission);
+    if (result != PackageManager.PERMISSION_GRANTED) {
       String definition;
       if (definesPermission) {
         definition = "<permission android:name=\"" + permission +
             "\" android:protectionLevel=\"signature\" />\n";
       } else {
         definition = "";
       }
       if (logError) {
@@ -514,70 +203,111 @@ public class LeanplumManifestHelper {
             definition + "<uses-permission android:name=\"" + permission + "\" />");
       }
       return false;
     }
     return true;
   }
 
   /**
-   * Class with Android manifest data.
+   * Verifies that a certain component (receiver or sender) is implemented in the
+   * AndroidManifest.xml file or the application, in order to make sure that push notifications
+   * work.
+   *
+   * @param componentType A receiver or a service.
+   * @param name The name of the class.
+   * @param exported What the exported option should be.
+   * @param permission Whether we need any permission.
+   * @param actions What actions we need to check for in the intent-filter.
+   * @param packageName The package name for the category tag, if we require one.
+   * @return true if the respective component is in the manifest file, and false otherwise.
    */
-  private static class ManifestData {
-    private List<ManifestComponent> services = new ArrayList<>();
-    private List<ManifestComponent> activities = new ArrayList<>();
-    private List<ManifestComponent> receivers = new ArrayList<>();
-    private List<ManifestComponent> providers = new ArrayList<>();
-    private String appVersionName;
-  }
-
-  /**
-   * Class with application component from AndroidManifest.xml.
-   */
-  private static class ManifestComponent {
-    enum ApplicationComponent {SERVICE, RECEIVER, ACTIVITY, PROVIDER}
-
-    ApplicationComponent type;
-    String name;
-    String permission;
-    List<ManifestIntentFilter> intentFilters = new ArrayList<>();
-
-    ManifestComponent(ApplicationComponent type) {
-      this.type = type;
+  public static boolean checkComponent(ApplicationComponent componentType, String name,
+      boolean exported, String permission, List<String> actions, String packageName) {
+    Context context = Leanplum.getContext();
+    if (context == null) {
+      return false;
     }
+    if (actions != null) {
+      for (String action : actions) {
+        List<ResolveInfo> components = (componentType == ApplicationComponent.RECEIVER)
+            ? context.getPackageManager().queryBroadcastReceivers(new Intent(action), 0)
+            : context.getPackageManager().queryIntentServices(new Intent(action), 0);
+        if (components == null) {
+          return false;
+        }
+        boolean foundComponent = false;
+        for (ResolveInfo component : components) {
+          if (component == null) {
+            continue;
+          }
+          ComponentInfo componentInfo = (componentType == ApplicationComponent.RECEIVER)
+              ? component.activityInfo : component.serviceInfo;
+          if (componentInfo != null && componentInfo.name.equals(name)) {
+            // Only check components from our package.
+            if (componentInfo.packageName != null && componentInfo.packageName.equals(packageName)) {
+              foundComponent = true;
+            }
+          }
+        }
+        if (!foundComponent) {
+          Log.e(getComponentError(componentType, name, exported,
+              permission, actions, packageName));
+          return false;
+        }
+      }
+    } else {
+      try {
+        if (componentType == ApplicationComponent.RECEIVER) {
+          context.getPackageManager().getReceiverInfo(
+              new ComponentName(context.getPackageName(), name), 0);
+        } else {
+          context.getPackageManager().getServiceInfo(
+              new ComponentName(context.getPackageName(), name), 0);
+        }
+      } catch (PackageManager.NameNotFoundException e) {
+        Log.e(getComponentError(componentType, name, exported,
+            permission, actions, packageName));
+        return false;
+      }
+    }
+    return true;
   }
 
   /**
-   * Class for declaration of intent filter from AndroidManifest.
+   * Formats error if component isn't found in app's manifest.
+   *
+   * @param componentType The component type to format.
+   * @param name Name of the component to format.
+   * @param exported Whether component is exported.
+   * @param permission Permission to format.
+   * @param actions Actions to format.
+   * @param packageName Package name to format
+   * @return Formatted error message to be printed to console.
    */
-  private static class ManifestIntentFilter {
-    final List<String> actions = new ArrayList<>();
-    final List<String> categories = new ArrayList<>();
-    final List<IntentData> dataList = new ArrayList<>();
-    public NamedNodeMap attributes;
+  private static String getComponentError(ApplicationComponent componentType, String name,
+      boolean exported, String permission, List<String> actions, String packageName) {
+    StringBuilder errorMessage = new StringBuilder("Push notifications requires you to add the " +
+        componentType.name().toLowerCase() + " " + name + " to your AndroidManifest.xml file." +
+        "Add this code within the <application> section:\n");
+    errorMessage.append("<").append(componentType.name().toLowerCase()).append("\n");
+    errorMessage.append("    ").append("android:name=\"").append(name).append("\"\n");
+    errorMessage.append("    android:exported=\"").append(Boolean.toString(exported)).append("\"");
+    if (permission != null) {
+      errorMessage.append("\n    android:permission=\"").append(permission).append("\"");
+    }
+    errorMessage.append(">\n");
+    if (actions != null) {
+      errorMessage.append("    <intent-filter>\n");
+      for (String action : actions) {
+        errorMessage.append("        <action android:name=\"").append(action).append("\" />\n");
+      }
+      if (packageName != null) {
+        errorMessage.append("        <category android:name=\"").append(packageName).append("\" />\n");
+      }
+      errorMessage.append("    </intent-filter>\n");
+    }
+    errorMessage.append("</").append(componentType.name().toLowerCase()).append(">");
+    return errorMessage.toString();
+  }
 
-    /**
-     * Class for data of intent filter from AndroidManifest.
-     */
-    static class IntentData {
-      final String scheme;
-      final String host;
-      final String port;
-      final String path;
-      final String pathPattern;
-      final String pathPrefix;
-      final String mimeType;
-      final String type;
-
-      IntentData(String scheme, String host, String port, String path, String pathPattern,
-          String pathPrefix, String mimeType, String type) {
-        this.scheme = scheme;
-        this.host = host;
-        this.port = port;
-        this.path = path;
-        this.pathPattern = pathPattern;
-        this.pathPrefix = pathPrefix;
-        this.mimeType = mimeType;
-        this.type = type;
-      }
-    }
-  }
-}
+  public enum ApplicationComponent {SERVICE, RECEIVER}
+}
\ No newline at end of file
--- a/mobile/android/thirdparty/com/leanplum/internal/Log.java
+++ b/mobile/android/thirdparty/com/leanplum/internal/Log.java
@@ -16,17 +16,16 @@
  * "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.internal;
 
-//import com.leanplum.BuildConfig;
 
 import java.util.HashMap;
 
 /**
  * Handles logging within the Leanplum SDK.
  *
  * @author Ben Marten
  */
--- a/mobile/android/thirdparty/com/leanplum/internal/Registration.java
+++ b/mobile/android/thirdparty/com/leanplum/internal/Registration.java
@@ -35,24 +35,23 @@ public class Registration {
     Request request = Request.post(Constants.Methods.REGISTER_FOR_DEVELOPMENT, params);
     request.onResponse(new Request.ResponseCallback() {
       @Override
       public void response(final JSONObject response) {
         OsHandler.getInstance().post(new Runnable() {
           @Override
           public void run() {
             try {
-              JSONObject registerResponse = Request.getLastResponse(response);
-              boolean isSuccess = Request.isResponseSuccess(registerResponse);
+              boolean isSuccess = Request.isResponseSuccess(response);
               if (isSuccess) {
                 if (callback != null) {
                   callback.onResponse(true);
                 }
               } else {
-                Log.e(Request.getResponseError(registerResponse));
+                Log.e(Request.getResponseError(response));
                 if (callback != null) {
                   callback.onResponse(false);
                 }
               }
             } catch (Throwable t) {
               Util.handleException(t);
             }
           }
--- a/mobile/android/thirdparty/com/leanplum/internal/Request.java
+++ b/mobile/android/thirdparty/com/leanplum/internal/Request.java
@@ -19,79 +19,102 @@
  * under the License.
  */
 
 package com.leanplum.internal;
 
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.os.AsyncTask;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
 
 import com.leanplum.Leanplum;
+import com.leanplum.utils.SharedPreferencesUtil;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 
 import java.io.EOFException;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.HttpURLConnection;
 import java.net.SocketTimeoutException;
 import java.net.URL;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import java.util.Stack;
+import java.util.UUID;
 
 /**
  * Leanplum request class.
  *
  * @author Andrew First
  */
 public class Request {
   private static final long DEVELOPMENT_MIN_DELAY_MS = 100;
   private static final long DEVELOPMENT_MAX_DELAY_MS = 5000;
   private static final long PRODUCTION_DELAY = 60000;
-  private static final String LEANPLUM = "__leanplum__";
+  static final int MAX_EVENTS_PER_API_CALL;
+  static final String LEANPLUM = "__leanplum__";
+  static final String UUID_KEY = "uuid";
 
   private static String appId;
   private static String accessKey;
   private static String deviceId;
   private static String userId;
+
+  private static final LeanplumEventCallbackManager eventCallbackManager =
+      new LeanplumEventCallbackManager();
   private static final Map<String, Boolean> fileTransferStatus = new HashMap<>();
   private static int pendingDownloads;
   private static NoPendingDownloadsCallback noPendingDownloadsBlock;
 
   // The token is saved primarily for legacy SharedPreferences decryption. This could
   // likely be removed in the future.
   private static String token = null;
-  private static final Object lock = Request.class;
   private static final Map<File, Long> fileUploadSize = new HashMap<>();
   private static final Map<File, Double> fileUploadProgress = new HashMap<>();
   private static String fileUploadProgressString = "";
   private static long lastSendTimeMs;
   private static final Object uploadFileLock = new Object();
 
   private final String httpMethod;
   private final String apiMethod;
   private final Map<String, Object> params;
   private ResponseCallback response;
   private ErrorCallback error;
   private boolean sent;
+  private long dataBaseIndex;
 
   private static ApiResponseCallback apiResponse;
 
+  private static List<Map<String, Object>> localErrors = new ArrayList<>();
+
+  static {
+    if (Build.VERSION.SDK_INT <= 17) {
+      MAX_EVENTS_PER_API_CALL = 5000;
+    } else {
+      MAX_EVENTS_PER_API_CALL = 10000;
+    }
+  }
+
   public static void setAppId(String appId, String accessKey) {
-    Request.appId = appId;
-    Request.accessKey = accessKey;
+    if (!TextUtils.isEmpty(appId)) {
+      Request.appId = appId.trim();
+    }
+    if (!TextUtils.isEmpty(accessKey)) {
+      Request.accessKey = accessKey.trim();
+    }
   }
 
   public static void setDeviceId(String deviceId) {
     Request.deviceId = deviceId;
   }
 
   public static void setUserId(String userId) {
     Request.userId = userId;
@@ -100,16 +123,32 @@ public class Request {
   public static void setToken(String token) {
     Request.token = token;
   }
 
   public static String token() {
     return token;
   }
 
+  /**
+   * Since requests are batched there can be a case where other Request can take future Request
+   * events. We need to have for each Request database index for handle response, error or start
+   * callbacks.
+   *
+   * @return Index of event at database.
+   */
+  public long getDataBaseIndex() {
+    return dataBaseIndex;
+  }
+
+  // Update index of event at database.
+  public void setDataBaseIndex(long dataBaseIndex) {
+    this.dataBaseIndex = dataBaseIndex;
+  }
+
   public static void loadToken() {
     Context context = Leanplum.getContext();
     SharedPreferences defaults = context.getSharedPreferences(
         LEANPLUM, Context.MODE_PRIVATE);
     String token = defaults.getString(Constants.Defaults.TOKEN_KEY, null);
     if (token == null) {
       return;
     }
@@ -117,21 +156,17 @@ public class Request {
   }
 
   public static void saveToken() {
     Context context = Leanplum.getContext();
     SharedPreferences defaults = context.getSharedPreferences(
         LEANPLUM, Context.MODE_PRIVATE);
     SharedPreferences.Editor editor = defaults.edit();
     editor.putString(Constants.Defaults.TOKEN_KEY, Request.token());
-    try {
-      editor.apply();
-    } catch (NoSuchMethodError e) {
-      editor.commit();
-    }
+    SharedPreferencesUtil.commitChanges(editor);
   }
 
   public static String appId() {
     return appId;
   }
 
   public static String deviceId() {
     return deviceId;
@@ -140,19 +175,23 @@ public class Request {
   public static String userId() {
     return Request.userId;
   }
 
   public Request(String httpMethod, String apiMethod, Map<String, Object> params) {
     this.httpMethod = httpMethod;
     this.apiMethod = apiMethod;
     this.params = params != null ? params : new HashMap<String, Object>();
-
+    // Check if it is error and here was SQLite exception.
+    if (Constants.Methods.LOG.equals(apiMethod) && LeanplumEventDataManager.willSendErrorLog) {
+      localErrors.add(createArgsDictionary());
+    }
     // Make sure the Handler is initialized on the main thread.
     OsHandler.getInstance();
+    dataBaseIndex = -1;
   }
 
   public static Request get(String apiMethod, Map<String, Object> params) {
     Log.LeanplumLogType level = Constants.Methods.LOG.equals(apiMethod) ?
         Log.LeanplumLogType.DEBUG : Log.LeanplumLogType.VERBOSE;
     Log.log(level, "Will call API method " + apiMethod + " with arguments " + params);
     return RequestFactory.getInstance().createRequest("GET", apiMethod, params);
   }
@@ -186,31 +225,38 @@ public class Request {
     args.put(Constants.Params.TIME, Double.toString(new Date().getTime() / 1000.0));
     if (token != null) {
       args.put(Constants.Params.TOKEN, token);
     }
     args.putAll(params);
     return args;
   }
 
-  private static void saveRequestForLater(Map<String, Object> args) {
-    synchronized (lock) {
+  private void saveRequestForLater(Map<String, Object> args) {
+    synchronized (Request.class) {
       Context context = Leanplum.getContext();
       SharedPreferences preferences = context.getSharedPreferences(
           LEANPLUM, Context.MODE_PRIVATE);
       SharedPreferences.Editor editor = preferences.edit();
-      int count = preferences.getInt(Constants.Defaults.COUNT_KEY, 0);
-      String itemKey = String.format(Locale.US, Constants.Defaults.ITEM_KEY, count);
-      editor.putString(itemKey, JsonConverter.toJson(args));
-      count++;
-      editor.putInt(Constants.Defaults.COUNT_KEY, count);
-      try {
-        editor.apply();
-      } catch (NoSuchMethodError e) {
-        editor.commit();
+      long count = LeanplumEventDataManager.getEventsCount();
+      String uuid = preferences.getString(Constants.Defaults.UUID_KEY, null);
+      if (uuid == null || count % MAX_EVENTS_PER_API_CALL == 0) {
+        uuid = UUID.randomUUID().toString();
+        editor.putString(Constants.Defaults.UUID_KEY, uuid);
+        SharedPreferencesUtil.commitChanges(editor);
+      }
+      args.put(UUID_KEY, uuid);
+      LeanplumEventDataManager.insertEvent(JsonConverter.toJson(args));
+
+      dataBaseIndex = count;
+      // Checks if here response and/or error callback for this request. We need to add callbacks to
+      // eventCallbackManager only if here was internet connection, otherwise triggerErrorCallback
+      // will handle error callback for this event.
+      if (response != null || error != null && !Util.isConnected()) {
+        eventCallbackManager.addCallbacks(this, response, error);
       }
     }
   }
 
   public void send() {
     this.sendEventually();
     if (Constants.isDevelopmentModeEnabled) {
       long currentTimeMs = System.currentTimeMillis();
@@ -276,96 +322,128 @@ public class Request {
   }
 
   private void triggerErrorCallback(Exception e) {
     if (error != null) {
       error.error(e);
     }
     if (apiResponse != null) {
       List<Map<String, Object>> requests = getUnsentRequests();
-      apiResponse.response(requests, null);
+      List<Map<String, Object>> requestsToSend = removeIrrelevantBackgroundStartRequests(requests);
+      apiResponse.response(requestsToSend, null, requests.size());
     }
   }
 
   @SuppressWarnings("BooleanMethodIsAlwaysInverted")
-  private boolean attachApiKeys(Map<String, Object> dict) {
+  private static boolean attachApiKeys(Map<String, Object> dict) {
     if (appId == null || accessKey == null) {
       Log.e("API keys are not set. Please use Leanplum.setAppIdForDevelopmentMode or "
           + "Leanplum.setAppIdForProductionMode.");
       return false;
     }
     dict.put(Constants.Params.APP_ID, appId);
     dict.put(Constants.Params.CLIENT_KEY, accessKey);
     dict.put(Constants.Params.CLIENT, Constants.CLIENT);
     return true;
   }
 
   public interface ResponseCallback {
     void response(JSONObject response);
   }
 
   public interface ApiResponseCallback {
-    void response(List<Map<String, Object>> requests, JSONObject response);
+    void response(List<Map<String, Object>> requests, JSONObject response, int countOfEvents);
   }
 
   public interface ErrorCallback {
     void error(Exception e);
   }
 
   public interface NoPendingDownloadsCallback {
     void noPendingDownloads();
   }
 
-  private void parseResponseJson(JSONObject responseJson, List<Map<String, Object>> requestsToSend,
-      Exception error) {
-    if (apiResponse != null) {
-      apiResponse.response(requestsToSend, responseJson);
-    }
+  /**
+   * Parse response body from server.  Invoke potential error or response callbacks for all events
+   * of this request.
+   *
+   * @param responseBody JSONObject with response body from server.
+   * @param requestsToSend List of requests that were sent to the server/
+   * @param error Exception.
+   * @param unsentRequestsSize Size of unsent request, that we will delete.
+   */
+  private void parseResponseBody(JSONObject responseBody, List<Map<String, Object>>
+      requestsToSend, Exception error, int unsentRequestsSize) {
+    synchronized (Request.class) {
+      if (responseBody == null && error != null) {
+        // Invoke potential error callbacks for all events of this request.
+        eventCallbackManager.invokeAllCallbacksWithError(error, unsentRequestsSize);
+        return;
+      } else if (responseBody == null) {
+        return;
+      }
 
-    if (responseJson != null) {
-      Exception lastResponseError = null;
-      int numResponses = Request.numResponses(responseJson);
+      // Response for last start call.
+      if (apiResponse != null) {
+        apiResponse.response(requestsToSend, responseBody, unsentRequestsSize);
+      }
+
+      // We will replace it with error from response body, if we found it.
+      Exception lastResponseError = error;
+      // Valid response, parse and handle response body.
+      int numResponses = Request.numResponses(responseBody);
       for (int i = 0; i < numResponses; i++) {
-        JSONObject response = Request.getResponseAt(responseJson, i);
-        if (!Request.isResponseSuccess(response)) {
-          String errorMessage = Request.getResponseError(response);
-          if (errorMessage == null || errorMessage.length() == 0) {
-            errorMessage = "API error";
-          } else if (errorMessage.startsWith("App not found")) {
-            errorMessage = "No app matching the provided app ID was found.";
-            Constants.isInPermanentFailureState = true;
-          } else if (errorMessage.startsWith("Invalid access key")) {
-            errorMessage = "The access key you provided is not valid for this app.";
-            Constants.isInPermanentFailureState = true;
-          } else if (errorMessage.startsWith("Development mode requested but not permitted")) {
-            errorMessage = "A call to Leanplum.setAppIdForDevelopmentMode "
-                + "with your production key was made, which is not permitted.";
-            Constants.isInPermanentFailureState = true;
-          } else {
-            errorMessage = "API error: " + errorMessage;
-          }
-          Log.e(errorMessage);
-          if (i == numResponses - 1) {
-            lastResponseError = new Exception(errorMessage);
-          }
+        JSONObject response = Request.getResponseAt(responseBody, i);
+        if (Request.isResponseSuccess(response)) {
+          continue; // If event response is successful, proceed with next one.
+        }
+
+        // If event response was not successful, handle error.
+        String errorMessage = getReadableErrorMessage(Request.getResponseError(response));
+        Log.e(errorMessage);
+        // Throw an exception if last event response is negative.
+        if (i == numResponses - 1) {
+          lastResponseError = new Exception(errorMessage);
         }
       }
 
-      if (lastResponseError == null) {
-        lastResponseError = error;
+      if (lastResponseError != null) {
+        // Invoke potential error callbacks for all events of this request.
+        eventCallbackManager.invokeAllCallbacksWithError(lastResponseError, unsentRequestsSize);
+      } else {
+        // Invoke potential response callbacks for all events of this request.
+        eventCallbackManager.invokeAllCallbacksForResponse(responseBody, unsentRequestsSize);
       }
+    }
+  }
 
-      if (lastResponseError != null && this.error != null) {
-        this.error.error(lastResponseError);
-      } else if (this.response != null) {
-        this.response.response(responseJson);
-      }
-    } else if (error != null && this.error != null) {
-      this.error.error(error);
+  /**
+   * Parse error message from server response and return readable error message.
+   *
+   * @param errorMessage String of error from server response.
+   * @return String of readable error message.
+   */
+  @NonNull
+  private String getReadableErrorMessage(String errorMessage) {
+    if (errorMessage == null || errorMessage.length() == 0) {
+      errorMessage = "API error";
+    } else if (errorMessage.startsWith("App not found")) {
+      errorMessage = "No app matching the provided app ID was found.";
+      Constants.isInPermanentFailureState = true;
+    } else if (errorMessage.startsWith("Invalid access key")) {
+      errorMessage = "The access key you provided is not valid for this app.";
+      Constants.isInPermanentFailureState = true;
+    } else if (errorMessage.startsWith("Development mode requested but not permitted")) {
+      errorMessage = "A call to Leanplum.setAppIdForDevelopmentMode "
+          + "with your production key was made, which is not permitted.";
+      Constants.isInPermanentFailureState = true;
+    } else {
+      errorMessage = "API error: " + errorMessage;
     }
+    return errorMessage;
   }
 
   private void sendNow() {
     if (Constants.isTestMode) {
       return;
     }
     if (appId == null) {
       Log.e("Cannot send request. appId is not set.");
@@ -373,150 +451,157 @@ public class Request {
     }
     if (accessKey == null) {
       Log.e("Cannot send request. accessKey is not set.");
       return;
     }
 
     this.sendEventually();
 
-    final List<Map<String, Object>> requestsToSend = popUnsentRequests();
+    Util.executeAsyncTask(true, new AsyncTask<Void, Void, Void>() {
+      @Override
+      protected Void doInBackground(Void... params) {
+        sendRequests();
+        return null;
+      }
+    });
+  }
+
+  private void sendRequests() {
+    List<Map<String, Object>> unsentRequests = new ArrayList<>();
+    List<Map<String, Object>> requestsToSend;
+    // Check if we have localErrors, if yes then we will send only errors to the server.
+    if (localErrors.size() != 0) {
+      String uuid = UUID.randomUUID().toString();
+      for (Map<String, Object> error : localErrors) {
+        error.put(UUID_KEY, uuid);
+        unsentRequests.add(error);
+      }
+      requestsToSend = unsentRequests;
+    } else {
+      unsentRequests = getUnsentRequests();
+      requestsToSend = removeIrrelevantBackgroundStartRequests(unsentRequests);
+    }
+
     if (requestsToSend.isEmpty()) {
       return;
     }
 
     final Map<String, Object> multiRequestArgs = new HashMap<>();
+    if (!Request.attachApiKeys(multiRequestArgs)) {
+      return;
+    }
     multiRequestArgs.put(Constants.Params.DATA, jsonEncodeUnsentRequests(requestsToSend));
     multiRequestArgs.put(Constants.Params.SDK_VERSION, Constants.LEANPLUM_VERSION);
     multiRequestArgs.put(Constants.Params.ACTION, Constants.Methods.MULTI);
     multiRequestArgs.put(Constants.Params.TIME, Double.toString(new Date().getTime() / 1000.0));
-    if (!this.attachApiKeys(multiRequestArgs)) {
-      return;
-    }
 
-    Util.executeAsyncTask(new AsyncTask<Void, Void, Void>() {
-      @Override
-      protected Void doInBackground(Void... params) {
-        JSONObject result = null;
-        HttpURLConnection op = null;
-        try {
-          try {
-            op = Util.operation(
-                Constants.API_HOST_NAME,
-                Constants.API_SERVLET,
-                multiRequestArgs,
-                httpMethod,
-                Constants.API_SSL,
-                Constants.NETWORK_TIMEOUT_SECONDS);
+    JSONObject responseBody;
+    HttpURLConnection op = null;
+    try {
+      try {
+        op = Util.operation(
+            Constants.API_HOST_NAME,
+            Constants.API_SERVLET,
+            multiRequestArgs,
+            httpMethod,
+            Constants.API_SSL,
+            Constants.NETWORK_TIMEOUT_SECONDS);
 
-            result = Util.getJsonResponse(op);
-            int statusCode = op.getResponseCode();
+        responseBody = Util.getJsonResponse(op);
+        int statusCode = op.getResponseCode();
+
+        Exception errorException;
+        if (statusCode >= 200 && statusCode <= 299) {
+          if (responseBody == null) {
+            errorException = new Exception("Response JSON is null.");
+            deleteSentRequests(unsentRequests.size());
+            parseResponseBody(null, requestsToSend, errorException, unsentRequests.size());
+            return;
+          }
 
-            Exception errorException = null;
-            if (statusCode >= 400) {
-              errorException = new Exception("HTTP error " + statusCode);
-              if (statusCode == 408 || (statusCode >= 500 && statusCode <= 599)) {
-                pushUnsentRequests(requestsToSend);
-              }
-            } else {
-              if (result != null) {
-                int numResponses = Request.numResponses(result);
-                if (numResponses != requestsToSend.size()) {
-                  Log.w("Sent " + requestsToSend.size() +
-                      " requests but only" + " received " + numResponses);
-                }
-              } else {
-                errorException = new Exception("Response JSON is null.");
-              }
-            }
-            parseResponseJson(result, requestsToSend, errorException);
-          } catch (JSONException e) {
-            Log.e("Error parsing JSON response: " + e.toString() + "\n" +
-                Log.getStackTraceString(e));
-            parseResponseJson(null, requestsToSend, e);
-          } catch (Exception e) {
-            pushUnsentRequests(requestsToSend);
-            Log.e("Unable to send request: " + e.toString() + "\n" +
-                Log.getStackTraceString(e));
-            parseResponseJson(result, requestsToSend, e);
-          } finally {
-            if (op != null) {
-              op.disconnect();
-            }
+          Exception exception = null;
+          // Checks if we received the same number of responses as a number of sent request.
+          int numResponses = Request.numResponses(responseBody);
+          if (numResponses != requestsToSend.size()) {
+            Log.w("Sent " + requestsToSend.size() + " requests but only" +
+                " received " + numResponses);
+          }
+          parseResponseBody(responseBody, requestsToSend, null, unsentRequests.size());
+          // Clear localErrors list.
+          localErrors.clear();
+          deleteSentRequests(unsentRequests.size());
+
+          // Send another request if the last request had maximum events per api call.
+          if (unsentRequests.size() == MAX_EVENTS_PER_API_CALL) {
+            sendRequests();
           }
-        } catch (Throwable t) {
-          Util.handleException(t);
+        } else {
+          errorException = new Exception("HTTP error " + statusCode);
+          if (statusCode != -1 && statusCode != 408 && !(statusCode >= 500 && statusCode <= 599)) {
+            deleteSentRequests(unsentRequests.size());
+            parseResponseBody(responseBody, requestsToSend, errorException, unsentRequests.size());
+          }
         }
-        return null;
+      } catch (JSONException e) {
+        Log.e("Error parsing JSON response: " + e.toString() + "\n" + Log.getStackTraceString(e));
+        deleteSentRequests(unsentRequests.size());
+        parseResponseBody(null, requestsToSend, e, unsentRequests.size());
+      } catch (Exception e) {
+        Log.e("Unable to send request: " + e.toString() + "\n" + Log.getStackTraceString(e));
+      } finally {
+        if (op != null) {
+          op.disconnect();
+        }
       }
-    });
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
   }
 
   public void sendEventually() {
     if (Constants.isTestMode) {
       return;
     }
+
+    if (LeanplumEventDataManager.willSendErrorLog) {
+      return;
+    }
+
     if (!sent) {
       sent = true;
       Map<String, Object> args = createArgsDictionary();
       saveRequestForLater(args);
     }
   }
 
-  static List<Map<String, Object>> popUnsentRequests() {
-    return getUnsentRequests(true);
+  static void deleteSentRequests(int requestsCount) {
+    if (requestsCount == 0) {
+      return;
+    }
+    synchronized (Request.class) {
+      LeanplumEventDataManager.deleteEvents(requestsCount);
+    }
   }
 
-  static List<Map<String, Object>> getUnsentRequests() {
-    return getUnsentRequests(false);
-  }
+  private static List<Map<String, Object>> getUnsentRequests() {
+    List<Map<String, Object>> requestData;
 
-  private static List<Map<String, Object>> getUnsentRequests(boolean remove) {
-    List<Map<String, Object>> requestData = new ArrayList<>();
-
-    synchronized (lock) {
+    synchronized (Request.class) {
       lastSendTimeMs = System.currentTimeMillis();
-
       Context context = Leanplum.getContext();
       SharedPreferences preferences = context.getSharedPreferences(
           LEANPLUM, Context.MODE_PRIVATE);
       SharedPreferences.Editor editor = preferences.edit();
 
-      int count = preferences.getInt(Constants.Defaults.COUNT_KEY, 0);
-      if (count == 0) {
-        return new ArrayList<>();
-      }
-      if (remove) {
-        editor.remove(Constants.Defaults.COUNT_KEY);
-      }
-
-      for (int i = 0; i < count; i++) {
-        String itemKey = String.format(Locale.US, Constants.Defaults.ITEM_KEY, i);
-        Map<String, Object> requestArgs;
-        try {
-          requestArgs = JsonConverter.mapFromJson(new JSONObject(
-              preferences.getString(itemKey, "{}")));
-          requestData.add(requestArgs);
-        } catch (JSONException e) {
-          e.printStackTrace();
-        }
-        if (remove) {
-          editor.remove(itemKey);
-        }
-      }
-      if (remove) {
-        try {
-          editor.apply();
-        } catch (NoSuchMethodError e) {
-          editor.commit();
-        }
-      }
+      requestData = LeanplumEventDataManager.getEvents(MAX_EVENTS_PER_API_CALL);
+      editor.remove(Constants.Defaults.UUID_KEY);
+      SharedPreferencesUtil.commitChanges(editor);
     }
 
-    requestData = removeIrrelevantBackgroundStartRequests(requestData);
     return requestData;
   }
 
   /**
    * In various scenarios we can end up batching a big number of requests (e.g. device is offline,
    * background sessions), which could make the stored API calls batch look something like:
    * <p>
    * <code>start(B), start(B), start(F), track, start(B), track, start(F), resumeSession</code>
@@ -555,32 +640,16 @@ public class Request {
   }
 
   private static String jsonEncodeUnsentRequests(List<Map<String, Object>> requestData) {
     Map<String, Object> data = new HashMap<>();
     data.put(Constants.Params.DATA, requestData);
     return JsonConverter.toJson(data);
   }
 
-  private static void pushUnsentRequests(List<Map<String, Object>> requestData) {
-    if (requestData == null) {
-      return;
-    }
-    for (Map<String, Object> args : requestData) {
-      Object retryCountString = args.get("retryCount");
-      int retryCount;
-      if (retryCountString != null) {
-        retryCount = Integer.parseInt(retryCountString.toString()) + 1;
-      } else {
-        retryCount = 1;
-      }
-      args.put("retryCount", Integer.toString(retryCount));
-      saveRequestForLater(args);
-    }
-  }
 
   private static String getSizeAsString(int bytes) {
     if (bytes < (1 << 10)) {
       return bytes + " B";
     } else if (bytes < (1 << 20)) {
       return (bytes >> 10) + " KB";
     } else {
       return (bytes >> 20) + " MB";
@@ -645,17 +714,17 @@ public class Request {
     }
     if (filesToUpload.size() == 0) {
       return;
     }
 
     printUploadProgress();
 
     // Now upload the files
-    Util.executeAsyncTask(new AsyncTask<Void, Void, Void>() {
+    Util.executeAsyncTask(false, new AsyncTask<Void, Void, Void>() {
       @Override
       protected Void doInBackground(Void... params) {
         synchronized (uploadFileLock) {  // Don't overload app and server with many upload tasks
           JSONObject result;
           HttpURLConnection op = null;
 
           try {
             op = Util.uploadFilesOperation(
@@ -729,17 +798,17 @@ public class Request {
     Log.i("Downloading resource " + path);
     fileTransferStatus.put(path, true);
     final Map<String, Object> dict = createArgsDictionary();
     dict.put(Constants.Keys.FILENAME, path);
     if (!attachApiKeys(dict)) {
       return;
     }
 
-    Util.executeAsyncTask(new AsyncTask<Void, Void, Void>() {
+    Util.executeAsyncTask(false, new AsyncTask<Void, Void, Void>() {
       @Override
       protected Void doInBackground(Void... params) {
         try {
           downloadHelper(Constants.API_HOST_NAME, Constants.API_SERVLET, path, url, dict);
         } catch (Throwable t) {
           Util.handleException(t);
         }
         return null;
--- a/mobile/android/thirdparty/com/leanplum/internal/ResourceQualifiers.java
+++ b/mobile/android/thirdparty/com/leanplum/internal/ResourceQualifiers.java
@@ -75,30 +75,34 @@ public class ResourceQualifiers {
         if (str.length() == 2) {
           return str;
         }
         return null;
       }
 
       @Override
       public boolean isMatch(Object value, Configuration config, DisplayMetrics display) {
+        // Suppressing deprecated locale.
+        //noinspection deprecation
         return config.locale.getLanguage().equals(value);
       }
     }),
     REGION(new QualifierFilter() {
       @Override
       public Object getMatch(String str) {
         if (str.startsWith("r") && str.length() == 3) {
           return str.substring(1);
         }
         return null;
       }
 
       @Override
       public boolean isMatch(Object value, Configuration config, DisplayMetrics display) {
+        // Suppressing deprecated locale.
+        //noinspection deprecation
         return config.locale.getCountry().toLowerCase().equals(value);
       }
     }),
     LAYOUT_DIRECTION(new QualifierFilter() {
       // From http://developer.android.com/reference/android/content/res/Configuration.html#SMALLEST_SCREEN_WIDTH_DP_UNDEFINED
       public static final int SCREENLAYOUT_LAYOUTDIR_LTR = 0x00000040;
       public static final int SCREENLAYOUT_LAYOUTDIR_RTL = 0x00000080;
       public static final int SCREENLAYOUT_LAYOUTDIR_MASK = 0x000000c0;
--- a/mobile/android/thirdparty/com/leanplum/internal/Socket.java
+++ b/mobile/android/thirdparty/com/leanplum/internal/Socket.java
@@ -43,26 +43,29 @@ import java.util.Map;
 import java.util.Timer;
 import java.util.TimerTask;
 
 /**
  * Leanplum socket class, that handles connections to the Leanplum remote socket.
  *
  * @author Andrew First, Ben Marten
  */
+// Suppressing deprecated apache dependency.
+@SuppressWarnings("deprecation")
 public class Socket {
   private static final String TAG = "Leanplum";
   private static final String EVENT_CONTENT_RESPONSE = "getContentResponse";
   private static final String EVENT_UPDATE_VARS = "updateVars";
   private static final String EVENT_GET_VIEW_HIERARCHY = "getViewHierarchy";
   private static final String EVENT_PREVIEW_UPDATE_RULES = "previewUpdateRules";
   private static final String EVENT_TRIGGER = "trigger";
   private static final String EVENT_GET_VARIABLES = "getVariables";
   private static final String EVENT_GET_ACTIONS = "getActions";
   private static final String EVENT_REGISTER_DEVICE = "registerDevice";
+  private static final String EVENT_APPLY_VARS = "applyVars";
 
   private static Socket instance = new Socket();
   private SocketIOClient sio;
   private boolean authSent;
   private boolean connected = false;
   private boolean connecting = false;
 
   public Socket() {
@@ -131,16 +134,19 @@ public class Socket {
               handleGetVariablesEvent();
               break;
             case EVENT_GET_ACTIONS:
               handleGetActionsEvent();
               break;
             case EVENT_REGISTER_DEVICE:
               handleRegisterDeviceEvent(arguments);
               break;
+            case EVENT_APPLY_VARS:
+              handleApplyVarsEvent(arguments);
+              break;
             default:
               break;
           }
         } catch (Throwable t) {
           Util.handleException(t);
         }
       }
     };
@@ -282,16 +288,38 @@ public class Socket {
             });
             alert.show();
           }
         });
       }
     });
   }
 
+  /**
+   * Apply variables passed in from applyVars endpoint.
+   */
+  static void handleApplyVarsEvent(JSONArray args) {
+    if (args == null) {
+      return;
+    }
+
+    try {
+      JSONObject object = args.getJSONObject(0);
+      if (object == null) {
+        return;
+      }
+      VarCache.applyVariableDiffs(
+          JsonConverter.mapFromJson(object), null, null, null, null, null);
+    } catch (JSONException e) {
+      Log.e("Couldn't applyVars for preview.", e);
+    } catch (Throwable e) {
+      Util.handleException(e);
+    }
+  }
+
   void previewUpdateRules(JSONArray arguments) {
     JSONObject packetData;
     try {
       packetData = arguments.getJSONObject(0);
     } catch (Exception e) {
       Log.e("Error parsing data");
       return;
     }
--- a/mobile/android/thirdparty/com/leanplum/internal/SocketIOClient.java
+++ b/mobile/android/thirdparty/com/leanplum/internal/SocketIOClient.java
@@ -25,16 +25,17 @@ package com.leanplum.internal;
 
 import android.os.Looper;
 
 import com.leanplum.Leanplum;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
+import org.mozilla.gecko.thirdparty_unused.BuildConfig;
 
 import java.io.BufferedReader;
 import java.io.ByteArrayOutputStream;
 import java.io.DataInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.net.HttpURLConnection;
@@ -64,23 +65,16 @@ class SocketIOClient {
   private Looper mSendLooper;
 
   public SocketIOClient(URI uri, Handler handler) {
     // remove trailing "/" from URI, in case user provided e.g. http://test.com/
     mURL = uri.toString().replaceAll("/$", "") + "/socket.io/1/";
     mHandler = handler;
   }
 
-  private static String userAgentString() {
-    String appName = (Leanplum.getContext() != null) ?
-        Util.getApplicationName(Leanplum.getContext()) + "/" + Util.getVersionName() : "websocket";
-    return appName + "(" + Request.appId() + "; " + Constants.CLIENT + "; "
-        + Constants.LEANPLUM_VERSION + "/" + Constants.LEANPLUM_PACKAGE_IDENTIFIER + ")";
-  }
-
   private String downloadUriAsString()
           throws IOException {
     URL url = new URL(this.mURL);
     HttpURLConnection connection = (HttpURLConnection) url.openConnection();
 
     try {
       InputStream inputStream = connection.getInputStream();
       BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
--- a/mobile/android/thirdparty/com/leanplum/internal/Util.java
+++ b/mobile/android/thirdparty/com/leanplum/internal/Util.java
@@ -43,16 +43,17 @@ import android.util.TypedValue;
 
 import com.google.android.gms.ads.identifier.AdvertisingIdClient;
 import com.leanplum.Leanplum;
 import com.leanplum.LeanplumActivityHelper;
 import com.leanplum.LeanplumDeviceIdMode;
 import com.leanplum.LeanplumException;
 import com.leanplum.internal.Constants.Methods;
 import com.leanplum.internal.Constants.Params;
+import com.leanplum.utils.SharedPreferencesUtil;
 
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.json.JSONTokener;
 
 import java.io.BufferedReader;
 import java.io.BufferedWriter;
 import java.io.DataOutputStream;
@@ -86,16 +87,17 @@ import javax.net.ssl.SSLSocketFactory;
 
 /**
  * Leanplum utilities.
  *
  * @author Andrew First
  */
 public class Util {
   private static final Executor asyncExecutor = Executors.newCachedThreadPool();
+  private static final Executor singleThreadExecutor = Executors.newSingleThreadExecutor();
 
   private static final String ACCESS_WIFI_STATE_PERMISSION = "android.permission.ACCESS_WIFI_STATE";
 
   private static String appName = null;
   private static String versionName = null;
 
   private static boolean hasPlayServicesCalled = false;
   private static boolean hasPlayServices = false;
@@ -345,19 +347,16 @@ public class Util {
   }
 
   public static String getVersionName() {
     if (versionName != null) {
       return versionName;
     }
     Context context = Leanplum.getContext();
     try {
-      versionName = LeanplumManifestHelper.getAppVersionName();
-      // If we didn't get application version name from AndroidManifest.xml - will try to get it
-      // from PackageInfo.
       if (TextUtils.isEmpty(versionName)) {
         PackageInfo pInfo = context.getPackageManager().getPackageInfo(
             context.getPackageName(), 0);
         versionName = pInfo.versionName;
       }
     } catch (Exception e) {
       Log.w("Could not extract versionName from Manifest or PackageInfo.");
     }
@@ -718,21 +717,30 @@ public class Util {
       if (!((Map<?, ?>) current).containsKey(index)) {
         return null;
       }
       current = ((Map<?, ?>) current).get(index);
     }
     return CollectionUtil.uncheckedCast(current);
   }
 
-  public static <T> void executeAsyncTask(AsyncTask<T, ?, ?> task, T... params) {
-    if (Build.VERSION.SDK_INT >= 11) {
+  /**
+   * Execute async task on single thread Executer or cached thread pool Executer.
+   *
+   * @param singleThread True if needs to be executed on single thread Executer, otherwise it will
+   * use cached thread pool Executer.
+   * @param task Async task to execute.
+   * @param params Params.
+   */
+  public static <T> void executeAsyncTask(boolean singleThread, AsyncTask<T, ?, ?> task,
+      T... params) {
+    if (singleThread) {
+      task.executeOnExecutor(singleThreadExecutor, params);
+    } else {
       task.executeOnExecutor(asyncExecutor, params);
-    } else {
-      task.execute(params);
     }
   }
 
   /**
    * Check the device to make sure it has the Google Play Services APK. If it doesn't, display a
    * dialog that allows users to download the APK from the Google Play Store or enable it in the
    * device's system settings.
    */
@@ -789,21 +797,17 @@ public class Util {
 
     PackageManager packageManager = context.getPackageManager();
     String packageName = context.getPackageName();
     setInstallTime(params, packageManager, packageName);
     setUpdateTime(params, packageManager, packageName);
 
     SharedPreferences.Editor editor = preferences.edit();
     editor.putBoolean(Constants.Keys.INSTALL_TIME_INITIALIZED, true);
-    try {
-      editor.apply();
-    } catch (NoSuchMethodError e) {
-      editor.commit();
-    }
+    SharedPreferencesUtil.commitChanges(editor);
   }
 
   /**
    * Set install time from package manager and update time from apk file modification time.
    */
   private static void setInstallTime(Map<String, Object> params, PackageManager packageManager,
       String packageName) {
     try {
--- a/mobile/android/thirdparty/com/leanplum/internal/VarCache.java
+++ b/mobile/android/thirdparty/com/leanplum/internal/VarCache.java
@@ -25,16 +25,17 @@ import android.content.Context;
 import android.content.SharedPreferences;
 
 import com.leanplum.ActionContext;
 import com.leanplum.CacheUpdateBlock;
 import com.leanplum.Leanplum;
 import com.leanplum.LocationManager;
 import com.leanplum.Var;
 import com.leanplum.internal.FileManager.HashResults;
+import com.leanplum.utils.SharedPreferencesUtil;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
 import java.io.InputStream;
 import java.lang.reflect.Array;
 import java.math.BigDecimal;
@@ -427,21 +428,17 @@ public class VarCache {
       editor.putString(Constants.Keys.VARIANTS, aesContext.encrypt(variantsJson));
     } catch (JSONException e1) {
       Log.e("Error converting " + variants + " to JSON.\n" + Log.getStackTraceString(e1));
     }
     editor.putString(Constants.Params.DEVICE_ID, aesContext.encrypt(Request.deviceId()));
     editor.putString(Constants.Params.USER_ID, aesContext.encrypt(Request.userId()));
     editor.putString(Constants.Keys.LOGGING_ENABLED,
         aesContext.encrypt(String.valueOf(Constants.loggingEnabled)));
-    try {
-      editor.apply();
-    } catch (NoSuchMethodError e) {
-      editor.commit();
-    }
+    SharedPreferencesUtil.commitChanges(editor);
   }
 
   /**
    * Convert a resId to a resPath.
    */
   static int getResIdFromPath(String resPath) {
     int resId = 0;
     try {
@@ -460,21 +457,24 @@ public class VarCache {
 
   /**
    * Update file variables stream info with override info, so that override files don't require
    * downloads if they're already available.
    */
   private static void fileVariableFinish() {
     for (String name : new HashMap<>(vars).keySet()) {
       Var<?> var = vars.get(name);
+      if (var == null) {
+        continue;
+      }
       String overrideFile = var.stringValue;
-      if (var.isResource && (var.kind().equals(Constants.Kinds.FILE)) && overrideFile != null &&
-          !var.defaultValue().equals(overrideFile)) {
+      if (var.isResource && Constants.Kinds.FILE.equals(var.kind()) && overrideFile != null &&
+              !overrideFile.equals(var.defaultValue())) {
         Map<String, Object> variationAttributes = CollectionUtil.uncheckedCast(fileAttributes.get
-            (overrideFile));
+                (overrideFile));
         InputStream stream = fileStreams.get(overrideFile);
         if (variationAttributes != null && stream != null) {
           var.setOverrideResId(getResIdFromPath(var.stringValue()));
         }
       }
     }
   }
 
@@ -515,17 +515,17 @@ public class VarCache {
         newConfig.put(Constants.Keys.VARS, vars);
       }
 
       VarCache.messages = newMessages;
       for (Map.Entry<String, Object> entry : VarCache.messages.entrySet()) {
         String name = entry.getKey();
         Map<String, Object> messageConfig = CollectionUtil.uncheckedCast(VarCache.messages.get
             (name));
-        if (messageConfig.get("action") != null) {
+        if (messageConfig != null && messageConfig.get("action") != null) {
           Map<String, Object> actionArgs =
               CollectionUtil.uncheckedCast(messageConfig.get(Constants.Keys.VARS));
           new ActionContext(
               messageConfig.get("action").toString(), actionArgs, name).update();
         }
       }
     }
 
@@ -859,21 +859,17 @@ public class VarCache {
     }
     Context context = Leanplum.getContext();
     SharedPreferences defaults = context.getSharedPreferences(LEANPLUM, Context.MODE_PRIVATE);
     SharedPreferences.Editor editor = defaults.edit();
     // Crypt functions return input text if there was a problem.
     String plaintext = JsonConverter.toJson(userAttributes);
     AESCrypt aesContext = new AESCrypt(Request.appId(), Request.token());
     editor.putString(Constants.Defaults.ATTRIBUTES_KEY, aesContext.encrypt(plaintext));
-    try {
-      editor.apply();
-    } catch (NoSuchMethodError e) {
-      editor.commit();
-    }
+    SharedPreferencesUtil.commitChanges(editor);
   }
 
   /**
    * Resets the VarCache to stock state.
    */
   public static void reset() {
     vars.clear();
     fileAttributes.clear();
--- a/mobile/android/thirdparty/com/leanplum/internal/WebSocketClient.java
+++ b/mobile/android/thirdparty/com/leanplum/internal/WebSocketClient.java
@@ -49,16 +49,18 @@ import ch.boye.httpclientandroidlib.Head
 import ch.boye.httpclientandroidlib.HttpException;
 import ch.boye.httpclientandroidlib.HttpStatus;
 import ch.boye.httpclientandroidlib.NameValuePair;
 import ch.boye.httpclientandroidlib.StatusLine;
 import ch.boye.httpclientandroidlib.client.HttpResponseException;
 import ch.boye.httpclientandroidlib.message.BasicLineParser;
 import ch.boye.httpclientandroidlib.message.BasicNameValuePair;
 
+// Suppressing deprecated apache dependency.
+@SuppressWarnings("deprecation")
 class WebSocketClient {
   private static final String TAG = "WebSocketClient";
 
   private URI mURI;
   private Listener mListener;
   private java.net.Socket mSocket;
   private Thread mThread;
   private HandlerThread mHandlerThread;
--- a/mobile/android/thirdparty/com/leanplum/messagetemplates/BaseMessageDialog.java
+++ b/mobile/android/thirdparty/com/leanplum/messagetemplates/BaseMessageDialog.java
@@ -27,21 +27,69 @@ import android.app.Dialog;
 import android.content.Context;
 import android.graphics.Color;
 import android.graphics.Point;
 import android.graphics.Typeface;
 import android.graphics.drawable.ShapeDrawable;
 import android.graphics.drawable.shapes.RoundRectShape;
 import android.graphics.drawable.shapes.Shape;
 import android.os.Build;
-import android.os.Bundle;
 import android.os.Handler;
 import android.text.TextUtils;
 import android.util.TypedValue;
-import android.view.Display;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.DecelerateInterpolator;
+import android.webkit.WebChromeClient;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import com.leanplum.ActionContext;
+import com.leanplum.Leanplum;
+import com.leanplum.utils.BitmapUtil;
+import com.leanplum.utils.SizeUtil;
+import com.leanplum.views.BackgroundImageView;
+import com.leanplum.views.CloseButton;
+
+import org.json.JSONObject;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.util.Map;
+
+/**
+ * Base dialog used to display the Center Popup, Interstitial, Web Interstitial, HTML template.
+ *
+ * @author Martin Yanakiev, Anna Orlova
+ */
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Point;
+import android.graphics.Typeface;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.shapes.RoundRectShape;
+import android.graphics.drawable.shapes.Shape;
+import android.os.Build;
+import android.os.Handler;
+import android.text.TextUtils;
+import android.util.TypedValue;
 import android.view.Gravity;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup.LayoutParams;
 import android.view.Window;
 import android.view.WindowManager;
 import android.view.animation.AccelerateInterpolator;
 import android.view.animation.AlphaAnimation;
@@ -80,48 +128,34 @@ public class BaseMessageDialog extends D
   protected HTMLOptions htmlOptions;
   protected Activity activity;
   protected WebView webView;
 
   private boolean isWeb = false;
   private boolean isHtml = false;
   private boolean isClosing = false;
 
-  @Override
-  protected void onCreate(Bundle savedInstanceState) {
-    super.onCreate(savedInstanceState);
-    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
-      final Window window = getWindow();
-      window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
-      window.setStatusBarColor(Color.TRANSPARENT);
-
-      int flags = window.getDecorView().getSystemUiVisibility();
-      flags |= View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
-      window.getDecorView().setSystemUiVisibility(flags);
-    }
-  }
-
   protected BaseMessageDialog(Activity activity, boolean fullscreen, BaseMessageOptions options,
-      WebInterstitialOptions webOptions, HTMLOptions htmlOptions) {
+                              WebInterstitialOptions webOptions, HTMLOptions htmlOptions) {
     super(activity, getTheme(activity));
 
     SizeUtil.init(activity);
     this.activity = activity;
     this.options = options;
     this.webOptions = webOptions;
     this.htmlOptions = htmlOptions;
     if (webOptions != null) {
       isWeb = true;
     }
     if (htmlOptions != null) {
       isHtml = true;
     }
     dialogView = new RelativeLayout(activity);
     RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
-        LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+            LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
     dialogView.setBackgroundColor(Color.TRANSPARENT);
     dialogView.setLayoutParams(layoutParams);
 
     RelativeLayout view = createContainerView(activity, fullscreen);
     view.setId(108);
     dialogView.addView(view, view.getLayoutParams());
 
     if ((!isWeb || (webOptions != null && webOptions.hasDismissButton())) && !isHtml) {
@@ -141,29 +175,29 @@ public class BaseMessageDialog extends D
       if (!isHtml) {
         window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
         if (Build.VERSION.SDK_INT >= 14) {
           window.setDimAmount(0.7f);
         }
       } else {
         window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
         window.setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
-            WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
+                WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
         if (htmlOptions != null &&
-            MessageTemplates.Args.HTML_ALIGN_BOTTOM.equals(htmlOptions.getHtmlAlign())) {
+                MessageTemplates.Args.HTML_ALIGN_BOTTOM.equals(htmlOptions.getHtmlAlign())) {
           dialogView.setGravity(Gravity.BOTTOM);
         }
       }
     }
   }
 
   @Override
   public void onWindowFocusChanged(boolean hasFocus) {
     try {
-      if (webView != null && Build.VERSION.SDK_INT >= 11) {
+      if (webView != null) {
         if (hasFocus) {
           webView.onResume();
         } else {
           webView.onPause();
         }
       }
     } catch (Throwable ignore) {
     }
@@ -222,17 +256,17 @@ public class BaseMessageDialog extends D
     });
     dialogView.startAnimation(animation);
   }
 
   private CloseButton createCloseButton(Activity context, boolean fullscreen, View parent) {
     CloseButton closeButton = new CloseButton(context);
     closeButton.setId(103);
     RelativeLayout.LayoutParams closeLayout = new RelativeLayout.LayoutParams(
-        LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+            LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
     if (fullscreen) {
       closeLayout.addRule(RelativeLayout.ALIGN_PARENT_TOP, dialogView.getId());
       closeLayout.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, dialogView.getId());
       closeLayout.setMargins(0, SizeUtil.dp5, SizeUtil.dp5, 0);
     } else {
       closeLayout.addRule(RelativeLayout.ALIGN_TOP, parent.getId());
       closeLayout.addRule(RelativeLayout.ALIGN_RIGHT, parent.getId());
       closeLayout.setMargins(0, -SizeUtil.dp7, -SizeUtil.dp7, 0);
@@ -250,32 +284,44 @@ public class BaseMessageDialog extends D
   @SuppressWarnings("deprecation")
   private RelativeLayout createContainerView(Activity context, boolean fullscreen) {
     RelativeLayout view = new RelativeLayout(context);
 
     // Positions the dialog.
     RelativeLayout.LayoutParams layoutParams;
     if (fullscreen) {
       layoutParams = new RelativeLayout.LayoutParams(
-          LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+              LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
     } else if (isHtml) {
       int height = SizeUtil.dpToPx(context, htmlOptions.getHtmlHeight());
-      layoutParams = new RelativeLayout.LayoutParams(
-          LayoutParams.MATCH_PARENT, height);
-    } else {
-
-      // Make sure the dialog fits on screen.
-      Display display = context.getWindowManager().getDefaultDisplay();
-      Point size = new Point();
-      if (Build.VERSION.SDK_INT >= 13) {
-        display.getSize(size);
+      HTMLOptions.Size htmlWidth = htmlOptions.getHtmlWidth();
+      if (htmlWidth == null || TextUtils.isEmpty(htmlWidth.type)) {
+        layoutParams = new RelativeLayout.LayoutParams(
+                LayoutParams.MATCH_PARENT, height);
       } else {
-        size = new Point(display.getHeight(), display.getHeight());
+        int width = htmlWidth.value;
+        if ("%".equals(htmlWidth.type)) {
+          Point size = SizeUtil.getDisplaySize(context);
+          width = size.x * width / 100;
+        } else {
+          width = SizeUtil.dpToPx(context, width);
+        }
+        layoutParams = new RelativeLayout.LayoutParams(width, height);
       }
 
+      layoutParams.addRule(RelativeLayout.CENTER_HORIZONTAL, RelativeLayout.TRUE);
+      int htmlYOffset = htmlOptions.getHtmlYOffset(context);
+      if (MessageTemplates.Args.HTML_ALIGN_BOTTOM.equals(htmlOptions.getHtmlAlign())) {
+        layoutParams.bottomMargin = htmlYOffset;
+      } else {
+        layoutParams.topMargin = htmlYOffset;
+      }
+    } else {
+      // Make sure the dialog fits on screen.
+      Point size = SizeUtil.getDisplaySize(context);
       int width = SizeUtil.dpToPx(context, ((CenterPopupOptions) options).getWidth());
       int height = SizeUtil.dpToPx(context, ((CenterPopupOptions) options).getHeight());
 
       int maxWidth = size.x - SizeUtil.dp20;
       int maxHeight = size.y - SizeUtil.dp20;
       double aspectRatio = width / (double) height;
       if (width > maxWidth && (int) (width / aspectRatio) < maxHeight) {
         width = maxWidth;
@@ -310,19 +356,19 @@ public class BaseMessageDialog extends D
       view.addView(title, title.getLayoutParams());
 
       View button = createAcceptButton(context);
       button.setId(105);
       view.addView(button, button.getLayoutParams());
 
       View message = createMessageView(context);
       ((RelativeLayout.LayoutParams) message.getLayoutParams())
-          .addRule(RelativeLayout.BELOW, title.getId());
+              .addRule(RelativeLayout.BELOW, title.getId());
       ((RelativeLayout.LayoutParams) message.getLayoutParams())
-          .addRule(RelativeLayout.ABOVE, button.getId());
+              .addRule(RelativeLayout.ABOVE, button.getId());
       view.addView(message, message.getLayoutParams());
     } else if (isWeb) {
       WebView webView = createWebView(context);
       view.addView(webView, webView.getLayoutParams());
     } else {
       webView = createHtml(context);
       view.addView(webView, webView.getLayoutParams());
     }
@@ -331,16 +377,17 @@ public class BaseMessageDialog extends D
   }
 
   private Shape createRoundRect(int cornerRadius) {
     int c = cornerRadius;
     float[] outerRadii = new float[] {c, c, c, c, c, c, c, c};
     return new RoundRectShape(outerRadii, null, null);
   }
 
+  // setBackgroundDrawable was deprecated at API 16.
   @SuppressWarnings("deprecation")
   private ImageView createBackgroundImageView(Context context, boolean fullscreen) {
     BackgroundImageView view = new BackgroundImageView(context, fullscreen);
     view.setScaleType(ImageView.ScaleType.CENTER_CROP);
     int cornerRadius;
     if (!fullscreen) {
       cornerRadius = SizeUtil.dp20;
     } else {
@@ -351,59 +398,59 @@ public class BaseMessageDialog extends D
     footerBackground.setShape(createRoundRect(cornerRadius));
     footerBackground.getPaint().setColor(options.getBackgroundColor());
     if (Build.VERSION.SDK_INT >= 16) {
       view.setBackground(footerBackground);
     } else {
       view.setBackgroundDrawable(footerBackground);
     }
     RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
-        LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+            LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
     view.setLayoutParams(layoutParams);
     return view;
   }
 
   private RelativeLayout createTitleView(Context context) {
     RelativeLayout view = new RelativeLayout(context);
     view.setLayoutParams(new LayoutParams(
-        LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
+            LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
 
     TextView title = new TextView(context);
     title.setPadding(0, SizeUtil.dp5, 0, SizeUtil.dp5);
     title.setGravity(Gravity.CENTER);
     title.setText(options.getTitle());
     title.setTextColor(options.getTitleColor());
     title.setTextSize(TypedValue.COMPLEX_UNIT_SP, SizeUtil.textSize0);
     title.setTypeface(null, Typeface.BOLD);
     RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
-        LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+            LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
     layoutParams.addRule(RelativeLayout.CENTER_HORIZONTAL, RelativeLayout.TRUE);
     layoutParams.addRule(RelativeLayout.CENTER_VERTICAL, RelativeLayout.TRUE);
     title.setLayoutParams(layoutParams);
 
     view.addView(title, title.getLayoutParams());
     return view;
   }
 
   private TextView createMessageView(Context context) {
     TextView view = new TextView(context);
     RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
-        LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+            LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
     view.setLayoutParams(layoutParams);
     view.setGravity(Gravity.CENTER);
     view.setText(options.getMessageText());
     view.setTextColor(options.getMessageColor());
     view.setTextSize(TypedValue.COMPLEX_UNIT_SP, SizeUtil.textSize0_1);
     return view;
   }
 
   private WebView createWebView(Context context) {
     WebView view = new WebView(context);
     RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
-        LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+            LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
     view.setLayoutParams(layoutParams);
     view.setWebViewClient(new WebViewClient() {
       @SuppressWarnings("deprecation")
       @Override
       public boolean shouldOverrideUrlLoading(WebView wView, String url) {
         if (url.contains(webOptions.getCloseUrl())) {
           cancel();
           String[] urlComponents = url.split("\\?");
@@ -467,28 +514,28 @@ public class BaseMessageDialog extends D
     webViewSettings.setJavaScriptCanOpenWindowsAutomatically(true);
     webViewSettings.setLoadWithOverviewMode(true);
     webViewSettings.setLoadsImagesAutomatically(true);
 
     if (Build.VERSION.SDK_INT >= 16) {
       webViewSettings.setAllowFileAccessFromFileURLs(true);
       webViewSettings.setAllowUniversalAccessFromFileURLs(true);
     }
-    if (Build.VERSION.SDK_INT >= 11) {
-      webViewSettings.setBuiltInZoomControls(false);
-      webViewSettings.setDisplayZoomControls(false);
-    }
+
+    webViewSettings.setBuiltInZoomControls(false);
+    webViewSettings.setDisplayZoomControls(false);
     webViewSettings.setSupportZoom(false);
 
     RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
-        LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+            LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
     webView.setLayoutParams(layoutParams);
     final Dialog currentDialog = this;
     webView.setWebChromeClient(new WebChromeClient());
     webView.setWebViewClient(new WebViewClient() {
+      // shouldOverrideUrlLoading(WebView wView, String url) was deprecated at API 24.
       @SuppressWarnings("deprecation")
       @Override
       public boolean shouldOverrideUrlLoading(WebView wView, String url) {
         // Open URL event.
         if (url.contains(htmlOptions.getOpenUrl())) {
           dialogView.setVisibility(View.VISIBLE);
           if (activity != null && !activity.isFinishing()) {
             currentDialog.show();
@@ -511,33 +558,33 @@ public class BaseMessageDialog extends D
           String eventName = queryComponentsFromUrl(url, "event");
           if (!TextUtils.isEmpty(eventName)) {
             Double value = Double.parseDouble(queryComponentsFromUrl(url, "value"));
             String info = queryComponentsFromUrl(url, "info");
             Map<String, Object> paramsMap = null;
 
             try {
               paramsMap = ActionContext.mapFromJson(new JSONObject(queryComponentsFromUrl(url,
-                  "parameters")));
+                      "parameters")));
             } catch (Exception ignored) {
             }
 
             if (queryComponentsFromUrl(url, "isMessageEvent").equals("true")) {
               ActionContext actionContext = htmlOptions.getActionContext();
               actionContext.trackMessageEvent(eventName, value, info, paramsMap);
             } else {
               Leanplum.track(eventName, value, info, paramsMap);
             }
           }
           return true;
         }
 
         // Action URL or track action URL event.
         if (url.contains(htmlOptions.getActionUrl()) ||
-            url.contains(htmlOptions.getTrackActionUrl())) {
+                url.contains(htmlOptions.getTrackActionUrl())) {
           cancel();
           String queryComponentsFromUrl = queryComponentsFromUrl(url, "action");
           try {
             queryComponentsFromUrl = URLDecoder.decode(queryComponentsFromUrl, "UTF-8");
           } catch (UnsupportedEncodingException ignored) {
           }
 
           ActionContext actionContext = htmlOptions.getActionContext();
@@ -586,45 +633,45 @@ public class BaseMessageDialog extends D
     } catch (Exception ignored) {
     }
     return componentsFromUrl;
   }
 
   private TextView createAcceptButton(Context context) {
     TextView view = new TextView(context);
     RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
-        LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+            LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
     layoutParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM, RelativeLayout.TRUE);
     layoutParams.addRule(RelativeLayout.CENTER_HORIZONTAL, RelativeLayout.TRUE);
     layoutParams.setMargins(0, 0, 0, SizeUtil.dp5);
 
     view.setPadding(SizeUtil.dp20, SizeUtil.dp5, SizeUtil.dp20, SizeUtil.dp5);
     view.setLayoutParams(layoutParams);
     view.setText(options.getAcceptButtonText());
     view.setTextColor(options.getAcceptButtonTextColor());
     view.setTypeface(null, Typeface.BOLD);
 
     BitmapUtil.stateBackgroundDarkerByPercentage(view,
-        options.getAcceptButtonBackgroundColor(), 30);
+            options.getAcceptButtonBackgroundColor(), 30);
 
     view.setTextSize(TypedValue.COMPLEX_UNIT_SP, SizeUtil.textSize0_1);
     view.setOnClickListener(new View.OnClickListener() {
       @Override
       public void onClick(View arg0) {
         if (!isClosing) {
           options.accept();
           cancel();
         }
       }
     });
     return view;
   }
 
   private static int getTheme(Activity activity) {
     boolean full = (activity.getWindow().getAttributes().flags &
-        WindowManager.LayoutParams.FLAG_FULLSCREEN) == WindowManager.LayoutParams.FLAG_FULLSCREEN;
+            WindowManager.LayoutParams.FLAG_FULLSCREEN) == WindowManager.LayoutParams.FLAG_FULLSCREEN;
     if (full) {
       return android.R.style.Theme_Translucent_NoTitleBar_Fullscreen;
     } else {
       return android.R.style.Theme_Translucent_NoTitleBar;
     }
   }
 }
--- a/mobile/android/thirdparty/com/leanplum/messagetemplates/BaseMessageOptions.java
+++ b/mobile/android/thirdparty/com/leanplum/messagetemplates/BaseMessageOptions.java
@@ -56,18 +56,18 @@ abstract class BaseMessageOptions {
     setTitle(context.stringNamed(Args.TITLE_TEXT));
     setTitleColor(context.numberNamed(Args.TITLE_COLOR).intValue());
     setMessageText(context.stringNamed(Args.MESSAGE_TEXT));
     setMessageColor(context.numberNamed(Args.MESSAGE_COLOR).intValue());
     InputStream imageStream = context.streamNamed(Args.BACKGROUND_IMAGE);
     if (imageStream != null) {
       try {
         setBackgroundImage(BitmapFactory.decodeStream(imageStream));
-      } catch (Exception e) {
-        Log.e("Leanplum", "Error loading background image", e);
+      } catch (Throwable t) {
+        Log.e("Leanplum", "Error loading background image", t);
       }
     }
     setBackgroundColor(context.numberNamed(Args.BACKGROUND_COLOR).intValue());
     setAcceptButtonText(context.stringNamed(Args.ACCEPT_BUTTON_TEXT));
     setAcceptButtonBackgroundColor(context.numberNamed(
         Args.ACCEPT_BUTTON_BACKGROUND_COLOR).intValue());
     setAcceptButtonTextColor(context.numberNamed(
         Args.ACCEPT_BUTTON_TEXT_COLOR).intValue());
--- a/mobile/android/thirdparty/com/leanplum/messagetemplates/HTMLOptions.java
+++ b/mobile/android/thirdparty/com/leanplum/messagetemplates/HTMLOptions.java
@@ -20,21 +20,24 @@
  */
 
 package com.leanplum.messagetemplates;
 
 import android.app.Activity;
 import android.graphics.Point;
 import android.text.TextUtils;
 import android.util.Log;
+
 import com.leanplum.ActionArgs;
 import com.leanplum.ActionContext;
 import com.leanplum.Leanplum;
 import com.leanplum.utils.SizeUtil;
+
 import org.json.JSONException;
+
 import java.io.BufferedReader;
 import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.util.Map;
 
 /**
@@ -47,32 +50,34 @@ class HTMLOptions {
   private String openUrl;
   private String trackUrl;
   private String actionUrl;
   private String trackActionUrl;
   private String htmlTemplate;
   private ActionContext actionContext;
   private String htmlAlign;
   private int htmlHeight;
+  private Size htmlWidth;
   private Size htmlYOffset;
   private boolean htmlTabOutsideToClose;
 
   HTMLOptions(ActionContext context) {
     this.setActionContext(context);
     this.setHtmlTemplate(getTemplate(context));
     this.setCloseUrl(context.stringNamed(MessageTemplates.Args.CLOSE_URL));
     this.setOpenUrl(context.stringNamed(MessageTemplates.Args.OPEN_URL));
     this.setTrackUrl(context.stringNamed(MessageTemplates.Args.TRACK_URL));
     this.setActionUrl(context.stringNamed(MessageTemplates.Args.ACTION_URL));
     this.setTrackActionUrl(context.stringNamed(MessageTemplates.Args.TRACK_ACTION_URL));
     this.setHtmlAlign(context.stringNamed(MessageTemplates.Args.HTML_ALIGN));
     this.setHtmlHeight(context.numberNamed(MessageTemplates.Args.HTML_HEIGHT).intValue());
+    this.setHtmlWidth(context.stringNamed(MessageTemplates.Args.HTML_WIDTH));
     this.setHtmlYOffset(context.stringNamed(MessageTemplates.Args.HTML_Y_OFFSET));
     this.setHtmlTabOutsideToClose(context.booleanNamed(
-            MessageTemplates.Args.HTML_TAP_OUTSIDE_TO_CLOSE));
+        MessageTemplates.Args.HTML_TAP_OUTSIDE_TO_CLOSE));
   }
 
   /**
    * Read data from file as String.
    *
    * @param context ActionContext.
    * @param name Name of file.
    * @return String String with data of file.
@@ -143,21 +148,16 @@ class HTMLOptions {
               localPath.replace(" ", "%20"));
         }
         map.remove(key);
       }
     }
     return map;
   }
 
-  static class Size {
-    int value;
-    String type;
-  }
-
   /**
    * Get HTML template file.
    *
    * @param context ActionContext.
    * @return String String with data of HTML template file.
    */
   private static String getTemplate(ActionContext context) {
     if (context == null) {
@@ -175,43 +175,46 @@ class HTMLOptions {
     if (context.getContextualValues() != null && context.getContextualValues().arguments != null) {
       htmlArgs.put("displayEvent", context.getContextualValues().arguments);
     }
 
     String htmlString = "";
     try {
       htmlString = (htmlTemplate.replace("##Vars##",
           ActionContext.mapToJsonObject(htmlArgs).toString()));
+      try {
+        htmlString = context.fillTemplate(htmlString);
+      } catch (Throwable ignored) {
+      }
     } catch (JSONException e) {
       Log.e("Leanplum", "Cannot convert map of arguments to JSON object.");
+    } catch (Throwable t) {
+      Log.e("Leanplum", "Cannot get html template.", t);
     }
     return htmlString.replace("\\/", "/");
   }
 
   /**
    * @return boolean True if it's full screen template.
    */
   boolean isFullScreen() {
     return htmlHeight == 0;
   }
 
   int getHtmlHeight() {
     return htmlHeight;
   }
 
-  private void setHtmlHeight(int htmlHeight) {
-    this.htmlHeight = htmlHeight;
+  // Gets html width.
+  Size getHtmlWidth() {
+    return htmlWidth;
   }
 
-  String getHtmlAlign() {
-    return htmlAlign;
-  }
-
-  private void setHtmlAlign(String htmlAlign) {
-    this.htmlAlign = htmlAlign;
+  private void setHtmlWidth(String htmlWidth) {
+    this.htmlWidth = getSizeValueAndType(htmlWidth);
   }
 
   //Gets html y offset in pixels.
   int getHtmlYOffset(Activity context) {
     int yOffset = 0;
     if (context == null) {
       return yOffset;
     }
@@ -257,16 +260,28 @@ class HTMLOptions {
   boolean isHtmlTabOutsideToClose() {
     return htmlTabOutsideToClose;
   }
 
   private void setHtmlTabOutsideToClose(boolean htmlTabOutsideToClose) {
     this.htmlTabOutsideToClose = htmlTabOutsideToClose;
   }
 
+  private void setHtmlHeight(int htmlHeight) {
+    this.htmlHeight = htmlHeight;
+  }
+
+  String getHtmlAlign() {
+    return htmlAlign;
+  }
+
+  private void setHtmlAlign(String htmlAlign) {
+    this.htmlAlign = htmlAlign;
+  }
+
   ActionContext getActionContext() {
     return actionContext;
   }
 
   private void setActionContext(ActionContext actionContext) {
     //noinspection AccessStaticViaInstance
     this.actionContext = actionContext;
   }
@@ -325,9 +340,14 @@ class HTMLOptions {
         .with(MessageTemplates.Args.OPEN_URL, MessageTemplates.Values.DEFAULT_OPEN_URL)
         .with(MessageTemplates.Args.ACTION_URL, MessageTemplates.Values.DEFAULT_ACTION_URL)
         .with(MessageTemplates.Args.TRACK_ACTION_URL,
             MessageTemplates.Values.DEFAULT_TRACK_ACTION_URL)
         .with(MessageTemplates.Args.TRACK_URL, MessageTemplates.Values.DEFAULT_TRACK_URL)
         .with(MessageTemplates.Args.HTML_ALIGN, MessageTemplates.Values.DEFAULT_HTML_ALING)
         .with(MessageTemplates.Args.HTML_HEIGHT, MessageTemplates.Values.DEFAULT_HTML_HEIGHT);
   }
+
+  static class Size {
+    int value;
+    String type;
+  }
 }
--- a/mobile/android/thirdparty/com/leanplum/messagetemplates/HTMLTemplate.java
+++ b/mobile/android/thirdparty/com/leanplum/messagetemplates/HTMLTemplate.java
@@ -106,9 +106,9 @@ public class HTMLTemplate extends BaseMe
                           }
                         });
                   }
                 });
             return true;
           }
         });
   }
-}
+}
\ No newline at end of file
--- a/mobile/android/thirdparty/com/leanplum/messagetemplates/MessageTemplates.java
+++ b/mobile/android/thirdparty/com/leanplum/messagetemplates/MessageTemplates.java
@@ -50,19 +50,20 @@ public class MessageTemplates {
     static final String MESSAGE_COLOR = "Message.Color";
     static final String ACCEPT_BUTTON_TEXT = "Accept button.Text";
     static final String ACCEPT_BUTTON_BACKGROUND_COLOR = "Accept button.Background color";
     static final String ACCEPT_BUTTON_TEXT_COLOR = "Accept button.Text color";
     static final String BACKGROUND_IMAGE = "Background image";
     static final String BACKGROUND_COLOR = "Background color";
     static final String LAYOUT_WIDTH = "Layout.Width";
     static final String LAYOUT_HEIGHT = "Layout.Height";
+    static final String HTML_WIDTH = "HTML Width";
+    static final String HTML_HEIGHT = "HTML Height";
     static final String HTML_Y_OFFSET = "HTML Y Offset";
     static final String HTML_TAP_OUTSIDE_TO_CLOSE = "Tap Outside to Close";
-    static final String HTML_HEIGHT = "HTML Height";
     static final String HTML_ALIGN = "HTML Align";
     static final String HTML_ALIGN_TOP = "Top";
     static final String HTML_ALIGN_BOTTOM = "Bottom";
 
     // Web interstitial arguments.
     static final String CLOSE_URL = "Close URL";
     static final String HAS_DISMISS_BUTTON = "Has dismiss button";
 
--- a/mobile/android/thirdparty/com/leanplum/messagetemplates/WebInterstitial.java
+++ b/mobile/android/thirdparty/com/leanplum/messagetemplates/WebInterstitial.java
@@ -38,25 +38,16 @@ import com.leanplum.callbacks.Postponabl
 public class WebInterstitial extends BaseMessageDialog {
   private static final String NAME = "Web Interstitial";
 
   public WebInterstitial(Activity activity, WebInterstitialOptions options) {
     super(activity, true, null, options, null);
     this.webOptions = options;
   }
 
-  /**
-   * Deprecated: Use {@link WebInterstitial#register()}.
-   */
-  @Deprecated
-  @SuppressWarnings("unused")
-  public static void register(Context currentContext) {
-    register();
-  }
-
   public static void register() {
     Leanplum.defineAction(NAME, Leanplum.ACTION_KIND_MESSAGE | Leanplum.ACTION_KIND_ACTION,
         WebInterstitialOptions.toArgs(), new ActionCallback() {
           @Override
           public boolean onResponse(final ActionContext context) {
             LeanplumActivityHelper.queueActionUponActive(new PostponableAction() {
               @Override
               public void run() {
--- a/mobile/android/thirdparty/com/leanplum/messagetemplates/WebInterstitialOptions.java
+++ b/mobile/android/thirdparty/com/leanplum/messagetemplates/WebInterstitialOptions.java
@@ -64,24 +64,15 @@ public class WebInterstitialOptions {
   public String getCloseUrl() {
     return closeUrl;
   }
 
   private void setCloseUrl(String closeUrl) {
     this.closeUrl = closeUrl;
   }
 
-  /**
-   * Deprecated: Use {@link WebInterstitialOptions#toArgs()}.
-   */
-  @Deprecated
-  @SuppressWarnings("unused")
-  public static ActionArgs toArgs(Context currentContext) {
-    return toArgs();
-  }
-
   public static ActionArgs toArgs() {
     return new ActionArgs()
         .with(Args.URL, Values.DEFAULT_URL)
         .with(Args.CLOSE_URL, Values.DEFAULT_CLOSE_URL)
         .with(Args.HAS_DISMISS_BUTTON, Values.DEFAULT_HAS_DISMISS_BUTTON);
   }
 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/utils/BuildUtil.java
@@ -0,0 +1,57 @@
+package com.leanplum.utils;
+
+/*
+ * Copyright 2017, 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.
+ */
+
+import android.content.Context;
+import android.os.Build;
+
+/**
+ * Utilities related to Build Version and target SDK.
+ *
+ * @author Anna Orlova
+ */
+public class BuildUtil {
+    private static int targetSdk = -1;
+
+    /**
+     * Whether notification channels are supported.
+     *
+     * @param context The application context.
+     * @return True if notification channels are supported, false otherwise.
+     */
+    public static boolean isNotificationChannelSupported(Context context) {
+        return Build.VERSION.SDK_INT >= 26 && getTargetSdkVersion(context) >= 26;
+    }
+
+    /**
+     * Returns target SDK version parsed from manifest.
+     *
+     * @param context The application context.
+     * @return Target SDK version.
+     */
+    private static int getTargetSdkVersion(Context context) {
+        if (targetSdk == -1 && context != null) {
+            targetSdk = context.getApplicationInfo().targetSdkVersion;
+        }
+        return targetSdk;
+    }
+}
\ No newline at end of file
--- a/mobile/android/thirdparty/com/leanplum/utils/SharedPreferencesUtil.java
+++ b/mobile/android/thirdparty/com/leanplum/utils/SharedPreferencesUtil.java
@@ -64,15 +64,19 @@ public class SharedPreferencesUtil {
    * @param key key of preference.
    * @param value value of preference.
    */
   public static void setString(Context context, String sharedPreferenceName, String key,
       String value) {
     final SharedPreferences sharedPreferences = getPreferences(context, sharedPreferenceName);
     SharedPreferences.Editor editor = sharedPreferences.edit();
     editor.putString(key, value);
+    commitChanges(editor);
+  }
+
+  public static void commitChanges(SharedPreferences.Editor editor){
     try {
       editor.apply();
     } catch (NoSuchMethodError e) {
       editor.commit();
     }
   }
 }