Bug 1351585 - Part 1. Add Leanplum SDK source code to thirdparty module r?nalexander,maliu,sebastian draft
authorcnevinc <cnevinc@livemail.tw>
Sat, 13 May 2017 14:05:09 -0700
changeset 578518 ddce5b179da8fd49c87e6a992d689c5eba3b04c4
parent 578129 4cffaa3864f156c5b0d0b67c08fbc4dc7837f757
child 578519 1b3f2cf0e35771e8f3b3d5a2a022dae98dd94e5f
push id58945
push usernalexander@mozilla.com
push dateTue, 16 May 2017 04:41:39 +0000
reviewersnalexander, maliu, sebastian
bugs1351585
milestone55.0a1
Bug 1351585 - Part 1. Add Leanplum SDK source code to thirdparty module r?nalexander,maliu,sebastian MozReview-Commit-ID: 6r7ZGpAww2n
mobile/android/thirdparty/build.gradle
mobile/android/thirdparty/com/leanplum/ActionArgs.java
mobile/android/thirdparty/com/leanplum/ActionContext.java
mobile/android/thirdparty/com/leanplum/CacheUpdateBlock.java
mobile/android/thirdparty/com/leanplum/Leanplum.java
mobile/android/thirdparty/com/leanplum/LeanplumActivityHelper.java
mobile/android/thirdparty/com/leanplum/LeanplumApplication.java
mobile/android/thirdparty/com/leanplum/LeanplumCloudMessagingProvider.java
mobile/android/thirdparty/com/leanplum/LeanplumDeviceIdMode.java
mobile/android/thirdparty/com/leanplum/LeanplumEditorMode.java
mobile/android/thirdparty/com/leanplum/LeanplumException.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/LeanplumInflater.java
mobile/android/thirdparty/com/leanplum/LeanplumLocalPushListenerService.java
mobile/android/thirdparty/com/leanplum/LeanplumLocationAccuracyType.java
mobile/android/thirdparty/com/leanplum/LeanplumManualProvider.java
mobile/android/thirdparty/com/leanplum/LeanplumPushInstanceIDService.java
mobile/android/thirdparty/com/leanplum/LeanplumPushListenerService.java
mobile/android/thirdparty/com/leanplum/LeanplumPushNotificationCustomizer.java
mobile/android/thirdparty/com/leanplum/LeanplumPushReceiver.java
mobile/android/thirdparty/com/leanplum/LeanplumPushRegistrationService.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/LocationManager.java
mobile/android/thirdparty/com/leanplum/Newsfeed.java
mobile/android/thirdparty/com/leanplum/NewsfeedMessage.java
mobile/android/thirdparty/com/leanplum/UIEditorBridge.java
mobile/android/thirdparty/com/leanplum/Var.java
mobile/android/thirdparty/com/leanplum/activities/LeanplumAccountAuthenticatorActivity.java
mobile/android/thirdparty/com/leanplum/activities/LeanplumActionBarActivity.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/annotations/File.java
mobile/android/thirdparty/com/leanplum/annotations/Parser.java
mobile/android/thirdparty/com/leanplum/annotations/Variable.java
mobile/android/thirdparty/com/leanplum/callbacks/ActionCallback.java
mobile/android/thirdparty/com/leanplum/callbacks/InboxChangedCallback.java
mobile/android/thirdparty/com/leanplum/callbacks/NewsfeedChangedCallback.java
mobile/android/thirdparty/com/leanplum/callbacks/PostponableAction.java
mobile/android/thirdparty/com/leanplum/callbacks/RegisterDeviceCallback.java
mobile/android/thirdparty/com/leanplum/callbacks/RegisterDeviceFinishedCallback.java
mobile/android/thirdparty/com/leanplum/callbacks/StartCallback.java
mobile/android/thirdparty/com/leanplum/callbacks/VariableCallback.java
mobile/android/thirdparty/com/leanplum/callbacks/VariablesChangedCallback.java
mobile/android/thirdparty/com/leanplum/internal/AESCrypt.java
mobile/android/thirdparty/com/leanplum/internal/ActionArg.java
mobile/android/thirdparty/com/leanplum/internal/ActionManager.java
mobile/android/thirdparty/com/leanplum/internal/BaseActionContext.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/HybiParser.java
mobile/android/thirdparty/com/leanplum/internal/JsonConverter.java
mobile/android/thirdparty/com/leanplum/internal/LeanplumInternal.java
mobile/android/thirdparty/com/leanplum/internal/LeanplumManifestHelper.java
mobile/android/thirdparty/com/leanplum/internal/LeanplumManifestParser.java
mobile/android/thirdparty/com/leanplum/internal/LeanplumMessageMatchFilter.java
mobile/android/thirdparty/com/leanplum/internal/LeanplumUIEditorWrapper.java
mobile/android/thirdparty/com/leanplum/internal/Log.java
mobile/android/thirdparty/com/leanplum/internal/OsHandler.java
mobile/android/thirdparty/com/leanplum/internal/Registration.java
mobile/android/thirdparty/com/leanplum/internal/Request.java
mobile/android/thirdparty/com/leanplum/internal/RequestFactory.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/Alert.java
mobile/android/thirdparty/com/leanplum/messagetemplates/BaseMessageDialog.java
mobile/android/thirdparty/com/leanplum/messagetemplates/BaseMessageOptions.java
mobile/android/thirdparty/com/leanplum/messagetemplates/CenterPopup.java
mobile/android/thirdparty/com/leanplum/messagetemplates/CenterPopupOptions.java
mobile/android/thirdparty/com/leanplum/messagetemplates/Confirm.java
mobile/android/thirdparty/com/leanplum/messagetemplates/HTMLOptions.java
mobile/android/thirdparty/com/leanplum/messagetemplates/HTMLTemplate.java
mobile/android/thirdparty/com/leanplum/messagetemplates/Interstitial.java
mobile/android/thirdparty/com/leanplum/messagetemplates/InterstitialOptions.java
mobile/android/thirdparty/com/leanplum/messagetemplates/MessageTemplates.java
mobile/android/thirdparty/com/leanplum/messagetemplates/OpenURL.java
mobile/android/thirdparty/com/leanplum/messagetemplates/WebInterstitial.java
mobile/android/thirdparty/com/leanplum/messagetemplates/WebInterstitialOptions.java
mobile/android/thirdparty/com/leanplum/utils/BitmapUtil.java
mobile/android/thirdparty/com/leanplum/utils/SharedPreferencesUtil.java
mobile/android/thirdparty/com/leanplum/utils/SizeUtil.java
mobile/android/thirdparty/com/leanplum/views/BackgroundImageView.java
mobile/android/thirdparty/com/leanplum/views/CloseButton.java
--- a/mobile/android/thirdparty/build.gradle
+++ b/mobile/android/thirdparty/build.gradle
@@ -1,16 +1,16 @@
 buildDir "${topobjdir}/gradle/build/mobile/android/thirdparty"
 
 apply plugin: 'com.android.library'
 
 android {
     compileSdkVersion 23
     buildToolsVersion mozconfig.substs.ANDROID_BUILD_TOOLS_VERSION
-
+    useLibrary 'org.apache.http.legacy'
     defaultConfig {
         targetSdkVersion 23
         minSdkVersion 15
     }
 
     compileOptions {
         sourceCompatibility JavaVersion.VERSION_1_7
         targetCompatibility JavaVersion.VERSION_1_7
@@ -35,16 +35,21 @@ android {
                 exclude 'com/squareup/leakcanary/**'
             }
         }
     }
 }
 
 dependencies {
     compile "com.android.support:support-v4:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
+    compile "com.android.support:appcompat-v7:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
+    compile "com.google.android.gms:play-services-gcm:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
+    compile "com.google.android.gms:play-services-location:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
+    compile "com.google.android.gms:play-services-ads:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
+    compile "com.android.support:support-annotations:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
 }
 
 apply plugin: 'idea'
 
 idea {
     module {
         // This is cosmetic.  See the excludes in the root project.
         if (!mozconfig.substs.MOZ_INSTALL_TRACKING) {
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/ActionArgs.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2014, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import com.leanplum.internal.ActionArg;
+import com.leanplum.internal.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Represents arguments for a message or action.
+ *
+ * @author Andrew First
+ */
+public class ActionArgs {
+  private List<ActionArg<?>> args = new ArrayList<>();
+
+  /**
+   * Adds a basic argument of type T.
+   *
+   * @param <T> Type of the argument. Can be Boolean, Byte, Short, Integer, Long, Float, Double,
+   * Character, String, List, or Map.
+   * @param name The name of the argument.
+   * @param defaultValue The default value of the argument.
+   */
+  public <T> ActionArgs with(String name, T defaultValue) {
+    if (name == null) {
+      Log.e("with - Invalid name parameter provided.");
+      return this;
+    }
+    args.add(ActionArg.argNamed(name, defaultValue));
+    return this;
+  }
+
+  /**
+   * Adds a color argument with an integer value.
+   *
+   * @param name The name of the argument.
+   * @param defaultValue The integer value representing the color.
+   */
+  public ActionArgs withColor(String name, int defaultValue) {
+    if (name == null) {
+      Log.e("withColor - Invalid name parameter provided.");
+      return this;
+    }
+    args.add(ActionArg.colorArgNamed(name, defaultValue));
+    return this;
+  }
+
+  /**
+   * Adds a file argument.
+   *
+   * @param name The name of the argument.
+   * @param defaultFilename The path of the default file value. Use null to indicate no default
+   * value.
+   */
+  public ActionArgs withFile(String name, String defaultFilename) {
+    if (name == null) {
+      Log.e("withFile - Invalid name parameter provided.");
+      return this;
+    }
+    args.add(ActionArg.fileArgNamed(name, defaultFilename));
+    return this;
+  }
+
+  /**
+   * Adds an asset argument. Same as {@link ActionArgs#withFile} except that the filename is
+   * relative to the assets directory.
+   *
+   * @param name The name of the argument.
+   * @param defaultFilename The path of the default file value relative to the assets directory. Use
+   * null to indicate no default value.
+   */
+  public ActionArgs withAsset(String name, String defaultFilename) {
+    if (name == null) {
+      Log.e("withAsset - Invalid name parameter provided.");
+      return this;
+    }
+    args.add(ActionArg.assetArgNamed(name, defaultFilename));
+    return this;
+  }
+
+  /**
+   * Adds an action argument.
+   *
+   * @param name The name of the argument.
+   * @param defaultValue The default action name. Use null to indicate no action.
+   */
+  public ActionArgs withAction(String name, String defaultValue) {
+    if (name == null) {
+      Log.e("withAction - Invalid name parameter provided.");
+      return this;
+    }
+    args.add(ActionArg.actionArgNamed(name, defaultValue));
+    return this;
+  }
+
+  List<ActionArg<?>> getValue() {
+    return args;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/ActionContext.java
@@ -0,0 +1,551 @@
+/*
+ * Copyright 2014, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+
+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;
+import com.leanplum.internal.Util;
+import com.leanplum.internal.VarCache;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * The context in which an action or message is executed.
+ *
+ * @author Andrew First
+ */
+public class ActionContext extends BaseActionContext implements Comparable<ActionContext> {
+  private final String name;
+  private ActionContext parentContext;
+  private final int contentVersion;
+  private String key;
+  private boolean preventRealtimeUpdating = false;
+  private ContextualValues contextualValues;
+
+  public static class ContextualValues {
+    /**
+     * Parameters from the triggering event or state.
+     */
+    public Map<String, ?> parameters;
+
+    /**
+     * Arguments from the triggering event or state.
+     */
+    public Map<String, Object> arguments;
+
+    /**
+     * The previous user attribute value.
+     */
+    public Object previousAttributeValue;
+
+    /**
+     * The current user attribute value.
+     */
+    public Object attributeValue;
+  }
+
+  public ActionContext(String name, Map<String, Object> args, String messageId) {
+    this(name, args, messageId, null, Constants.Messaging.DEFAULT_PRIORITY);
+  }
+
+  public ActionContext(String name, Map<String, Object> args, final String messageId,
+      final String originalMessageId, int priority) {
+    super(messageId, originalMessageId);
+    this.name = name;
+    this.args = args;
+    this.contentVersion = VarCache.contentVersion();
+    this.priority = priority;
+  }
+
+  public void preventRealtimeUpdating() {
+    preventRealtimeUpdating = true;
+  }
+
+  public void setContextualValues(ContextualValues values) {
+    contextualValues = values;
+  }
+
+  public ContextualValues getContextualValues() {
+    return contextualValues;
+  }
+
+  private static Map<String, Object> getDefinition(String name) {
+    Map<String, Object> definition = CollectionUtil.uncheckedCast(
+        VarCache.actionDefinitions().get(name));
+    if (definition == null) {
+      return new HashMap<>();
+    }
+    return definition;
+  }
+
+  private Map<String, Object> getDefinition() {
+    return getDefinition(name);
+  }
+
+  private Map<String, Object> defaultValues() {
+    Map<String, Object> values = CollectionUtil.uncheckedCast(getDefinition().get("values"));
+    if (values == null) {
+      return new HashMap<>();
+    }
+    return values;
+  }
+
+  private Map<String, String> kinds() {
+    Map<String, String> kinds = CollectionUtil.uncheckedCast(getDefinition().get("kinds"));
+    if (kinds == null) {
+      return new HashMap<>();
+    }
+    return kinds;
+  }
+
+  public void update() {
+    this.updateArgs(args, "", defaultValues());
+  }
+
+  @SuppressWarnings("unchecked")
+  private void updateArgs(Map<String, Object> args,
+      String prefix, Map<String, Object> defaultValues) {
+    Map<String, String> kinds = kinds();
+    for (Map.Entry<String, Object> entry : args.entrySet()) {
+      String arg = entry.getKey();
+      Object value = entry.getValue();
+      Object defaultValue = defaultValues != null ? defaultValues.get(arg) : null;
+      String kind = kinds.get(prefix + arg);
+      if ((kind == null || !kind.equals(Constants.Kinds.ACTION)) && value instanceof Map &&
+          !((Map<String, ?>) value).containsKey(Constants.Values.ACTION_ARG)) {
+        Map<String, Object> defaultValueMap = (defaultValue instanceof Map) ?
+            (Map<String, Object>) defaultValue : null;
+        this.updateArgs((Map<String, Object>) value, prefix + arg + ".", defaultValueMap);
+      } else {
+        if (kind != null && kind.equals(Constants.Kinds.FILE) ||
+            arg.contains(Constants.Values.FILE_PREFIX)) {
+          FileManager.maybeDownloadFile(false, value.toString(),
+              defaultValue != null ? defaultValue.toString() : null, null, null);
+
+          // Need to check for null because server actions like push notifications aren't
+          // defined in the SDK, and so there's no associated metadata.
+        } else if (kind == null || kind.equals(Constants.Kinds.ACTION)) {
+          Object actionArgsObj = objectNamed(prefix + arg);
+          if (!(actionArgsObj instanceof Map)) {
+            continue;
+          }
+          Map<String, Object> actionArgs = (Map<String, Object>) actionArgsObj;
+          ActionContext context = new ActionContext(
+              (String) actionArgs.get(Constants.Values.ACTION_ARG),
+              actionArgs, messageId);
+          context.update();
+        }
+      }
+    }
+  }
+
+  public String actionName() {
+    return name;
+  }
+
+  public <T> T objectNamed(String name) {
+    if (TextUtils.isEmpty(name)) {
+      Log.e("objectNamed - Invalid name parameter provided.");
+      return null;
+    }
+    try {
+      if (!preventRealtimeUpdating && VarCache.contentVersion() > contentVersion) {
+        ActionContext parent = parentContext;
+        if (parent != null) {
+          args = parent.getChildArgs(key);
+        } else if (messageId != null) {
+          // This is sort of a best effort to display the most recent version of the message, if
+          // this happens to be null, it probably means that it got changed somehow in between the
+          // time when it was activated and displayed (e.g. by forceContentUpdate), in which case
+          // we just ignore it and display the latest stable version.
+          Map<String, Object> message = CollectionUtil.uncheckedCast(VarCache.messages().get
+              (messageId));
+          if (message != null) {
+            args = CollectionUtil.uncheckedCast(message.get(Constants.Keys.VARS));
+          }
+        }
+      }
+      return VarCache.getMergedValueFromComponentArray(
+          VarCache.getNameComponents(name), args);
+    } catch (Throwable t) {
+      Util.handleException(t);
+      return null;
+    }
+  }
+
+  public String stringNamed(String name) {
+    if (TextUtils.isEmpty(name)) {
+      Log.e("stringNamed - Invalid name parameter provided.");
+      return null;
+    }
+    Object object = objectNamed(name);
+    if (object == null) {
+      return null;
+    }
+    try {
+      return fillTemplate(object.toString());
+    } catch (Throwable t) {
+      Util.handleException(t);
+      return object.toString();
+    }
+  }
+
+  private 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());
+      }
+    }
+    if (contextualValues.previousAttributeValue != null) {
+      value = value.replace("##Previous Value##",
+          contextualValues.previousAttributeValue.toString());
+    }
+    if (contextualValues.attributeValue != null) {
+      value = value.replace("##Value##", contextualValues.attributeValue.toString());
+    }
+    return value;
+  }
+
+  private String getDefaultValue(String name) {
+    String[] components = name.split("\\.");
+    Map<String, Object> defaultValues = defaultValues();
+    for (int i = 0; i < components.length; i++) {
+      if (defaultValues == null) {
+        return null;
+      }
+      if (i == components.length - 1) {
+        Object value = defaultValues.get(components[i]);
+        return value == null ? null : value.toString();
+      }
+      defaultValues = CollectionUtil.uncheckedCast(defaultValues.get(components[i]));
+    }
+    return null;
+  }
+
+  public InputStream streamNamed(String name) {
+    try {
+      if (TextUtils.isEmpty(name)) {
+        Log.e("streamNamed - Invalid name parameter provided.");
+        return null;
+      }
+      String stringValue = stringNamed(name);
+      String defaultValue = getDefaultValue(name);
+      if ((stringValue == null || stringValue.length() == 0) &&
+          (defaultValue == null || defaultValue.length() == 0)) {
+        return null;
+      }
+      InputStream stream = FileManager.stream(false, null, null,
+          FileManager.fileValue(stringValue, defaultValue, null), defaultValue, null);
+      if (stream == null) {
+        Log.e("Could not open stream named " + name);
+      }
+      return stream;
+    } catch (Throwable t) {
+      Util.handleException(t);
+      return null;
+    }
+  }
+
+  public boolean booleanNamed(String name) {
+    if (TextUtils.isEmpty(name)) {
+      Log.e("booleanNamed - Invalid name parameter provided.");
+      return false;
+    }
+    Object object = objectNamed(name);
+    try {
+      if (object == null) {
+        return false;
+      } else if (object instanceof Boolean) {
+        return (Boolean) object;
+      }
+      return convertToBoolean(object.toString());
+    } catch (Throwable t) {
+      Util.handleException(t);
+      return false;
+    }
+  }
+
+  /**
+   * In contrast to Boolean.valueOf this function also converts 1, yes or similar string values
+   * correctly to Boolean, e.g.: "1", "yes", "true", "on" --> true; "0", "no", "false", "off" -->
+   * false; else null.
+   *
+   * @param value the text to convert to Boolean.
+   * @return Boolean
+   */
+  private static boolean convertToBoolean(String value) {
+    return "1".equalsIgnoreCase(value) || "yes".equalsIgnoreCase(value) ||
+        "true".equalsIgnoreCase(value) || "on".equalsIgnoreCase(value);
+  }
+
+  public Number numberNamed(String name) {
+    if (TextUtils.isEmpty(name)) {
+      Log.e("numberNamed - Invalid name parameter provided.");
+      return null;
+    }
+    Object object = objectNamed(name);
+    try {
+      if (object == null || TextUtils.isEmpty(object.toString())) {
+        return 0.0;
+      }
+      if (object instanceof Number) {
+        return (Number) object;
+      }
+      return Double.valueOf(object.toString());
+    } catch (Throwable t) {
+      Util.handleException(t);
+      return 0.0;
+    }
+  }
+
+  private Map<String, Object> getChildArgs(String name) {
+    Object actionArgsObj = objectNamed(name);
+    if (!(actionArgsObj instanceof Map)) {
+      return null;
+    }
+    Map<String, Object> actionArgs = CollectionUtil.uncheckedCast(actionArgsObj);
+    Map<String, Object> defaultArgs = CollectionUtil.uncheckedCast(getDefinition(
+        (String) actionArgs.get(Constants.Values.ACTION_ARG)).get("values"));
+    actionArgs = CollectionUtil.uncheckedCast(VarCache.mergeHelper(defaultArgs, actionArgs));
+    return actionArgs;
+  }
+
+  public void runActionNamed(String name) {
+    if (TextUtils.isEmpty(name)) {
+      Log.e("runActionNamed - Invalid name parameter provided.");
+      return;
+    }
+    Map<String, Object> args = getChildArgs(name);
+    if (args == null) {
+      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);
+      }
+    }
+  }
+
+  /**
+   * 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) {
+    try {
+      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);
+      return true;
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+    return false;
+  }
+
+  /**
+   * Return true if here was action "Chain to Existing Message" and we started it.
+   */
+  private boolean isChainToExistingMessageStarted(Map<String, Object> args, 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(
+              message.get(Constants.Keys.VARS));
+          Object messageAction = message.get("action");
+          return messageAction != null && createActionContextForMessageId(messageAction.toString(),
+              messageArgs, messageId, name);
+        }
+      }
+    }
+    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).
+   *
+   * @param eventName Current event.
+   * @return Prefixed event name with all parent actions.
+   */
+  private String eventWithParentEventNames(String eventName) {
+    StringBuilder fullEventName = new StringBuilder();
+    ActionContext context = this;
+    List<ActionContext> parents = new ArrayList<>();
+    while (context.parentContext != null) {
+      parents.add(context);
+      context = context.parentContext;
+    }
+    for (int i = parents.size() - 1; i >= -1; i--) {
+      if (fullEventName.length() > 0) {
+        fullEventName.append(' ');
+      }
+      String actionName;
+      if (i > -1) {
+        actionName = parents.get(i).key;
+      } else {
+        actionName = eventName;
+      }
+      if (actionName == null) {
+        fullEventName = new StringBuilder("");
+        break;
+      }
+      actionName = actionName.replace(" action", "");
+      fullEventName.append(actionName);
+    }
+
+    return fullEventName.toString();
+  }
+
+  /**
+   * Run the action with the given variable name, and track a message event with the name.
+   *
+   * @param name Action variable name to run.
+   */
+  public void runTrackedActionNamed(String name) {
+    try {
+      if (!Constants.isNoop() && messageId != null && isRooted) {
+        if (TextUtils.isEmpty(name)) {
+          Log.e("runTrackedActionNamed - Invalid name parameter provided.");
+          return;
+        }
+        trackMessageEvent(name, 0.0, null, null);
+      }
+      runActionNamed(name);
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+  }
+
+  /**
+   * Track a message event with the given parameters. Any parent event names will be prepended to
+   * given event name.
+   *
+   * @param event Name of event.
+   * @param value Value for event.
+   * @param info Info for event.
+   * @param params Dictionary of params for event.
+   */
+  public void trackMessageEvent(String event, double value, String info,
+      Map<String, Object> params) {
+    try {
+      if (!Constants.isNoop() && this.messageId != null) {
+        if (TextUtils.isEmpty(event)) {
+          Log.e("trackMessageEvent - Invalid event parameter provided.");
+          return;
+        }
+
+        event = eventWithParentEventNames(event);
+        if (TextUtils.isEmpty(event)) {
+          Log.e("trackMessageEvent - Failed to generate parent action names.");
+          return;
+        }
+
+        Map<String, String> requestArgs = new HashMap<>();
+        requestArgs.put(Constants.Params.MESSAGE_ID, messageId);
+        LeanplumInternal.track(event, value, info, params, requestArgs);
+      }
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+  }
+
+  public void track(String event, double value, Map<String, Object> params) {
+    try {
+      if (!Constants.isNoop() && this.messageId != null) {
+        if (TextUtils.isEmpty(event)) {
+          Log.e("track - Invalid event parameter provided.");
+          return;
+        }
+        Map<String, String> requestArgs = new HashMap<>();
+        requestArgs.put(Constants.Params.MESSAGE_ID, messageId);
+        LeanplumInternal.track(event, value, null, params, requestArgs);
+      }
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+  }
+
+  public void muteFutureMessagesOfSameKind() {
+    try {
+      ActionManager.getInstance().muteFutureMessagesOfKind(messageId);
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+  }
+
+  public int compareTo(@NonNull ActionContext other) {
+    return priority - other.getPriority();
+  }
+
+  /**
+   * Returns path to requested file.
+   */
+  public static String filePath(String stringValue) {
+    return FileManager.fileValue(stringValue);
+  }
+
+  public static JSONObject mapToJsonObject(Map<String, ?> map) throws JSONException {
+    return JsonConverter.mapToJsonObject(map);
+  }
+
+  public static <T> Map<String, T> mapFromJson(JSONObject jsonObject) throws JSONException {
+    return JsonConverter.mapFromJson(jsonObject);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/CacheUpdateBlock.java
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+
+package com.leanplum;
+
+/**
+ * Update block that will be triggered on new content.
+ *
+ * @author Ben Marten
+ */
+public interface CacheUpdateBlock {
+  void updateCache();
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/Leanplum.java
@@ -0,0 +1,2049 @@
+/*
+ * Copyright 2016, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import android.app.Activity;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.location.Location;
+import android.os.AsyncTask;
+import android.support.v4.app.NotificationCompat;
+import android.text.TextUtils;
+
+import com.leanplum.ActionContext.ContextualValues;
+import com.leanplum.callbacks.ActionCallback;
+import com.leanplum.callbacks.RegisterDeviceCallback;
+import com.leanplum.callbacks.RegisterDeviceFinishedCallback;
+import com.leanplum.callbacks.StartCallback;
+import com.leanplum.callbacks.VariablesChangedCallback;
+import com.leanplum.internal.Constants;
+import com.leanplum.internal.FileManager;
+import com.leanplum.internal.JsonConverter;
+import com.leanplum.internal.LeanplumInternal;
+import com.leanplum.internal.LeanplumMessageMatchFilter;
+import com.leanplum.internal.LeanplumUIEditorWrapper;
+import com.leanplum.internal.Log;
+import com.leanplum.internal.OsHandler;
+import com.leanplum.internal.Registration;
+import com.leanplum.internal.Request;
+import com.leanplum.internal.Util;
+import com.leanplum.internal.Util.DeviceIdInfo;
+import com.leanplum.internal.VarCache;
+import com.leanplum.messagetemplates.MessageTemplates;
+
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TimeZone;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Leanplum Android SDK.
+ *
+ * @author Andrew First, Ben Marten
+ */
+public class Leanplum {
+  public static final int ACTION_KIND_MESSAGE = 1;
+  public static final int ACTION_KIND_ACTION = 1 << 1;
+
+  /**
+   * Default event name to use for Purchase events.
+   */
+  public static final String PURCHASE_EVENT_NAME = "Purchase";
+
+  private static final ArrayList<StartCallback> startHandlers = new ArrayList<>();
+  private static final ArrayList<VariablesChangedCallback> variablesChangedHandlers =
+      new ArrayList<>();
+  private static final ArrayList<VariablesChangedCallback> noDownloadsHandlers =
+      new ArrayList<>();
+  private static final ArrayList<VariablesChangedCallback> onceNoDownloadsHandlers =
+      new ArrayList<>();
+  private static RegisterDeviceCallback registerDeviceHandler;
+  private static RegisterDeviceFinishedCallback registerDeviceFinishedHandler;
+
+  private static LeanplumDeviceIdMode deviceIdMode = LeanplumDeviceIdMode.MD5_MAC_ADDRESS;
+  private static String customDeviceId;
+  private static boolean userSpecifiedDeviceId;
+  private static boolean initializedMessageTemplates = false;
+  private static boolean locationCollectionEnabled = true;
+
+  private static ScheduledExecutorService heartbeatExecutor;
+  private static final Object heartbeatLock = new Object();
+
+  private static Context context;
+
+  private static Runnable pushStartCallback;
+
+  private Leanplum() {
+  }
+
+  /**
+   * Optional. Sets the API server. The API path is of the form http[s]://hostname/servletName
+   *
+   * @param hostName The name of the API host, such as www.leanplum.com
+   * @param servletName The name of the API servlet, such as api
+   * @param ssl Whether to use SSL
+   */
+  public static void setApiConnectionSettings(String hostName, String servletName, boolean ssl) {
+    if (TextUtils.isEmpty(hostName)) {
+      Log.e("setApiConnectionSettings - Empty hostname parameter provided.");
+      return;
+    }
+    if (TextUtils.isEmpty(servletName)) {
+      Log.e("setApiConnectionSettings - Empty servletName parameter provided.");
+      return;
+    }
+
+    Constants.API_HOST_NAME = hostName;
+    Constants.API_SERVLET = servletName;
+    Constants.API_SSL = ssl;
+  }
+
+  /**
+   * Optional. Sets the socket server path for Development mode. Path is of the form hostName:port
+   *
+   * @param hostName The host name of the socket server.
+   * @param port The port to connect to.
+   */
+  public static void setSocketConnectionSettings(String hostName, int port) {
+    if (TextUtils.isEmpty(hostName)) {
+      Log.e("setSocketConnectionSettings - Empty hostName parameter provided.");
+      return;
+    }
+    if (port < 1 || port > 65535) {
+      Log.e("setSocketConnectionSettings - Invalid port parameter provided.");
+      return;
+    }
+
+    Constants.SOCKET_HOST = hostName;
+    Constants.SOCKET_PORT = port;
+  }
+
+  /**
+   * Optional. By default, Leanplum will hash file variables to determine if they're modified and
+   * need to be uploaded to the server. Use this method to override this setting.
+   *
+   * @param enabled Setting this to false will reduce startup latency in development mode, but it's
+   * possible that Leanplum will always have the most up-to-date versions of your resources.
+   * (Default: true)
+   */
+  public static void setFileHashingEnabledInDevelopmentMode(boolean enabled) {
+    Constants.hashFilesToDetermineModifications = enabled;
+  }
+
+  /**
+   * Optional. Whether to enable file uploading in development mode.
+   *
+   * @param enabled Whether or not files should be uploaded. (Default: true)
+   */
+  public static void setFileUploadingEnabledInDevelopmentMode(boolean enabled) {
+    Constants.enableFileUploadingInDevelopmentMode = enabled;
+  }
+
+  /**
+   * Optional. Enables verbose logging in development mode.
+   */
+  public static void enableVerboseLoggingInDevelopmentMode() {
+    Constants.enableVerboseLoggingInDevelopmentMode = true;
+  }
+
+  /**
+   * Optional. Adjusts the network timeouts. The default timeout is 10 seconds for requests, and 15
+   * seconds for file downloads.
+   */
+  public static void setNetworkTimeout(int seconds, int downloadSeconds) {
+    if (seconds < 0) {
+      Log.e("setNetworkTimeout - Invalid seconds parameter provided.");
+      return;
+    }
+    if (downloadSeconds < 0) {
+      Log.e("setNetworkTimeout - Invalid downloadSeconds parameter provided.");
+      return;
+    }
+
+    Constants.NETWORK_TIMEOUT_SECONDS = seconds;
+    Constants.NETWORK_TIMEOUT_SECONDS_FOR_DOWNLOADS = downloadSeconds;
+  }
+
+  /**
+   * Advanced: Whether new variables can be downloaded mid-session. By default, this is disabled.
+   * Currently, if this is enabled, new variables can only be downloaded if a push notification is
+   * sent while the app is running, and the notification's metadata hasn't be downloaded yet.
+   */
+  public static void setCanDownloadContentMidSessionInProductionMode(boolean value) {
+    Constants.canDownloadContentMidSessionInProduction = value;
+  }
+
+  /**
+   * Must call either this or {@link Leanplum#setAppIdForProductionMode} before issuing any calls to
+   * the API, including start.
+   *
+   * @param appId Your app ID.
+   * @param accessKey Your development key.
+   */
+  public static void setAppIdForDevelopmentMode(String appId, String accessKey) {
+    if (TextUtils.isEmpty(appId)) {
+      Log.e("setAppIdForDevelopmentMode - Empty appId parameter provided.");
+      return;
+    }
+    if (TextUtils.isEmpty(accessKey)) {
+      Log.e("setAppIdForDevelopmentMode - Empty accessKey parameter provided.");
+      return;
+    }
+
+    Constants.isDevelopmentModeEnabled = true;
+    Request.setAppId(appId, accessKey);
+  }
+
+  /**
+   * Must call either this or {@link Leanplum#setAppIdForDevelopmentMode} before issuing any calls
+   * to the API, including start.
+   *
+   * @param appId Your app ID.
+   * @param accessKey Your production key.
+   */
+  public static void setAppIdForProductionMode(String appId, String accessKey) {
+    if (TextUtils.isEmpty(appId)) {
+      Log.e("setAppIdForProductionMode - Empty appId parameter provided.");
+      return;
+    }
+    if (TextUtils.isEmpty(accessKey)) {
+      Log.e("setAppIdForProductionMode - Empty accessKey parameter provided.");
+      return;
+    }
+
+    Constants.isDevelopmentModeEnabled = false;
+    Request.setAppId(appId, accessKey);
+  }
+
+  /**
+   * Enable interface editing via Leanplum.com Visual Editor.
+   */
+  @Deprecated
+  public static void allowInterfaceEditing() {
+    if (Constants.isDevelopmentModeEnabled) {
+      throw new LeanplumException("Leanplum UI Editor has moved to a separate package. " +
+          "Please remove this method call and include this line in your build.gradle: " +
+          "compile 'com.leanplum:UIEditor:+'");
+    }
+  }
+
+  /**
+   * Enable screen tracking.
+   */
+  public static void trackAllAppScreens() {
+    LeanplumInternal.enableAutomaticScreenTracking();
+  }
+
+  /**
+   * Whether screen tracking is enabled or not.
+   *
+   * @return Boolean - true if enabled
+   */
+  public static boolean isScreenTrackingEnabled() {
+    return LeanplumInternal.getIsScreenTrackingEnabled();
+  }
+
+  /**
+   * Whether interface editing is enabled or not.
+   *
+   * @return Boolean - true if enabled
+   */
+  public static boolean isInterfaceEditingEnabled() {
+    return LeanplumUIEditorWrapper.isUIEditorAvailable();
+  }
+
+  /**
+   * Sets the type of device ID to use. Default: {@link LeanplumDeviceIdMode#MD5_MAC_ADDRESS}
+   */
+  public static void setDeviceIdMode(LeanplumDeviceIdMode mode) {
+    if (mode == null) {
+      Log.e("setDeviceIdMode - Invalid mode parameter provided.");
+      return;
+    }
+
+    deviceIdMode = mode;
+    userSpecifiedDeviceId = true;
+  }
+
+  /**
+   * (Advanced) Sets a custom device ID. Normally, you should use setDeviceIdMode to change the type
+   * of device ID provided.
+   */
+  public static void setDeviceId(String deviceId) {
+    if (TextUtils.isEmpty(deviceId)) {
+      Log.w("setDeviceId - Empty deviceId parameter provided.");
+    }
+
+    customDeviceId = deviceId;
+    userSpecifiedDeviceId = true;
+  }
+
+  /**
+   * Sets the application context. This should be the first call to Leanplum.
+   */
+  public static void setApplicationContext(Context context) {
+    if (context == null) {
+      Log.w("setApplicationContext - Null context parameter provided.");
+    }
+
+    Leanplum.context = context;
+  }
+
+  /**
+   * Gets the application context.
+   */
+  public static Context getContext() {
+    if (context == null) {
+      Log.e("Your application context is not set. "
+          + "You should call Leanplum.setApplicationContext(this) or "
+          + "LeanplumActivityHelper.enableLifecycleCallbacks(this) in your application's "
+          + "onCreate method, or have your application extend LeanplumApplication.");
+    }
+    return context;
+  }
+
+  /**
+   * Called when the device needs to be registered in development mode.
+   */
+  @Deprecated
+  public static void setRegisterDeviceHandler(RegisterDeviceCallback handler,
+      RegisterDeviceFinishedCallback finishHandler) {
+    if (handler == null && finishHandler == null) {
+      Log.w("setRegisterDeviceHandler - Invalid handler parameter provided.");
+    }
+
+    registerDeviceHandler = handler;
+    registerDeviceFinishedHandler = finishHandler;
+  }
+
+  /**
+   * Syncs resources between Leanplum and the current app. You should only call this once, and
+   * before {@link Leanplum#start}. syncResourcesAsync should be used instead unless file variables
+   * need to be defined early
+   */
+  public static void syncResources() {
+    if (Constants.isNoop()) {
+      return;
+    }
+    try {
+      FileManager.enableResourceSyncing(null, null, false);
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+  }
+
+  /**
+   * Syncs resources between Leanplum and the current app. You should only call this once, and
+   * before {@link Leanplum#start}.
+   */
+  public static void syncResourcesAsync() {
+    if (Constants.isNoop()) {
+      return;
+    }
+    try {
+      FileManager.enableResourceSyncing(null, null, true);
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+  }
+
+  /**
+   * Syncs resources between Leanplum and the current app. You should only call this once, and
+   * before {@link Leanplum#start}. syncResourcesAsync should be used instead unless file variables
+   * need to be defined early
+   *
+   * @param patternsToInclude Limit paths to only those matching at least one pattern in this list.
+   * Supply null to indicate no inclusion patterns. Paths start with the folder name within the res
+   * folder, e.g. "layout/main.xml".
+   * @param patternsToExclude Exclude paths matching at least one of these patterns. Supply null to
+   * indicate no exclusion patterns.
+   */
+  public static void syncResources(
+      List<String> patternsToInclude,
+      List<String> patternsToExclude) {
+    try {
+      FileManager.enableResourceSyncing(patternsToInclude, patternsToExclude, false);
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+  }
+
+  /**
+   * Syncs resources between Leanplum and the current app. You should only call this once, and
+   * before {@link Leanplum#start}. syncResourcesAsync should be used instead unless file variables
+   * need to be defined early
+   *
+   * @param patternsToInclude Limit paths to only those matching at least one pattern in this list.
+   * Supply null to indicate no inclusion patterns. Paths start with the folder name within the res
+   * folder, e.g. "layout/main.xml".
+   * @param patternsToExclude Exclude paths matching at least one of these patterns. Supply null to
+   * indicate no exclusion patterns.
+   */
+  public static void syncResourcesAsync(
+      List<String> patternsToInclude,
+      List<String> patternsToExclude) {
+    try {
+      FileManager.enableResourceSyncing(patternsToInclude, patternsToExclude, true);
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+  }
+
+  /**
+   * Returns true if resource syncing is enabled. Resource syncing may not be fully initialized.
+   */
+  public static boolean isResourceSyncingEnabled() {
+    return FileManager.isResourceSyncingEnabled();
+  }
+
+  /**
+   * Call this when your application starts. This will initiate a call to Leanplum's servers to get
+   * the values of the variables used in your app.
+   */
+  public static void start(Context context) {
+    start(context, null, null, null, null);
+  }
+
+  /**
+   * Call this when your application starts. This will initiate a call to Leanplum's servers to get
+   * the values of the variables used in your app.
+   */
+  public static void start(Context context, StartCallback callback) {
+    start(context, null, null, callback, null);
+  }
+
+  /**
+   * Call this when your application starts. This will initiate a call to Leanplum's servers to get
+   * the values of the variables used in your app.
+   */
+  public static void start(Context context, Map<String, ?> userAttributes) {
+    start(context, null, userAttributes, null, null);
+  }
+
+  /**
+   * Call this when your application starts. This will initiate a call to Leanplum's servers to get
+   * the values of the variables used in your app.
+   */
+  public static void start(Context context, String userId) {
+    start(context, userId, null, null, null);
+  }
+
+  /**
+   * Call this when your application starts. This will initiate a call to Leanplum's servers to get
+   * the values of the variables used in your app.
+   */
+  public static void start(Context context, String userId, StartCallback callback) {
+    start(context, userId, null, callback, null);
+  }
+
+  /**
+   * Call this when your application starts. This will initiate a call to Leanplum's servers to get
+   * the values of the variables used in your app.
+   */
+  public static void start(Context context, String userId, Map<String, ?> userAttributes) {
+    start(context, userId, userAttributes, null, null);
+  }
+
+  /**
+   * Call this when your application starts. This will initiate a call to Leanplum's servers to get
+   * the values of the variables used in your app.
+   */
+  public static synchronized void start(final Context context, String userId,
+      Map<String, ?> attributes, StartCallback response) {
+    start(context, userId, attributes, response, null);
+  }
+
+  static synchronized void start(final Context context, final String userId,
+      final Map<String, ?> attributes, StartCallback response, final Boolean isBackground) {
+    try {
+      OsHandler.getInstance();
+
+      if (context instanceof Activity) {
+        LeanplumActivityHelper.currentActivity = (Activity) context;
+      }
+
+      // Detect if app is in background automatically if isBackground is not set.
+      final boolean actuallyInBackground;
+      if (isBackground == null) {
+        actuallyInBackground = LeanplumActivityHelper.currentActivity == null ||
+            LeanplumActivityHelper.isActivityPaused();
+      } else {
+        actuallyInBackground = isBackground;
+      }
+
+      if (Constants.isNoop()) {
+        LeanplumInternal.setHasStarted(true);
+        LeanplumInternal.setStartSuccessful(true);
+        triggerStartResponse(true);
+        triggerVariablesChanged();
+        triggerVariablesChangedAndNoDownloadsPending();
+        VarCache.applyVariableDiffs(
+            new HashMap<String, Object>(),
+            new HashMap<String, Object>(),
+            VarCache.getUpdateRuleDiffs(),
+            VarCache.getEventRuleDiffs(),
+            new HashMap<String, Object>(),
+            new ArrayList<Map<String, Object>>());
+        LeanplumInbox.getInstance().update(new HashMap<String, LeanplumInboxMessage>(), 0, false);
+        return;
+      }
+
+      if (response != null) {
+        addStartResponseHandler(response);
+      }
+
+      if (context != null) {
+        Leanplum.setApplicationContext(context.getApplicationContext());
+      }
+
+      if (LeanplumInternal.hasCalledStart()) {
+        if (!actuallyInBackground && LeanplumInternal.hasStartedInBackground()) {
+          // Move to foreground.
+          LeanplumInternal.setStartedInBackground(false);
+          LeanplumInternal.moveToForeground();
+        } else {
+          Log.i("Already called start");
+        }
+        return;
+      }
+
+      initializedMessageTemplates = true;
+      MessageTemplates.register(Leanplum.getContext());
+
+      LeanplumInternal.setStartedInBackground(actuallyInBackground);
+
+      final Map<String, ?> validAttributes = LeanplumInternal.validateAttributes(attributes,
+          "userAttributes", true);
+      LeanplumInternal.setCalledStart(true);
+
+      if (validAttributes != null) {
+        LeanplumInternal.getUserAttributeChanges().add(validAttributes);
+      }
+
+      Request.loadToken();
+      VarCache.setSilent(true);
+      VarCache.loadDiffs();
+      VarCache.setSilent(false);
+      LeanplumInbox.getInstance().load();
+
+      // Setup class members.
+      VarCache.onUpdate(new CacheUpdateBlock() {
+        @Override
+        public void updateCache() {
+          triggerVariablesChanged();
+          if (Request.numPendingDownloads() == 0) {
+            triggerVariablesChangedAndNoDownloadsPending();
+          }
+        }
+      });
+      Request.onNoPendingDownloads(new Request.NoPendingDownloadsCallback() {
+        @Override
+        public void noPendingDownloads() {
+          triggerVariablesChangedAndNoDownloadsPending();
+        }
+      });
+
+      // Reduce latency by running the rest of the start call in a background thread.
+      Util.executeAsyncTask(new AsyncTask<Void, Void, Void>() {
+        @Override
+        protected Void doInBackground(Void... params) {
+          try {
+            startHelper(userId, validAttributes, actuallyInBackground);
+          } catch (Throwable t) {
+            Util.handleException(t);
+          }
+          return null;
+        }
+      });
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+  }
+
+  private static void startHelper(
+      String userId, final Map<String, ?> attributes, final boolean isBackground) {
+    LeanplumPushService.onStart();
+
+    Boolean limitAdTracking = null;
+    String deviceId = Request.deviceId();
+    if (deviceId == null) {
+      if (!userSpecifiedDeviceId && Constants.defaultDeviceId != null) {
+        deviceId = Constants.defaultDeviceId;
+      } else if (customDeviceId != null) {
+        deviceId = customDeviceId;
+      } else {
+        DeviceIdInfo deviceIdInfo = Util.getDeviceId(deviceIdMode);
+        deviceId = deviceIdInfo.id;
+        limitAdTracking = deviceIdInfo.limitAdTracking;
+      }
+      Request.setDeviceId(deviceId);
+    }
+
+    if (userId == null) {
+      userId = Request.userId();
+      if (userId == null) {
+        userId = Request.deviceId();
+      }
+    }
+    Request.setUserId(userId);
+
+    // Setup parameters.
+    String versionName = Util.getVersionName();
+    if (versionName == null) {
+      versionName = "";
+    }
+
+    TimeZone localTimeZone = TimeZone.getDefault();
+    Date now = new Date();
+    int timezoneOffsetSeconds = localTimeZone.getOffset(now.getTime()) / 1000;
+
+    HashMap<String, Object> params = new HashMap<>();
+    params.put(Constants.Params.INCLUDE_DEFAULTS, Boolean.toString(false));
+    if (isBackground) {
+      params.put(Constants.Params.BACKGROUND, Boolean.toString(true));
+    }
+    params.put(Constants.Params.VERSION_NAME, versionName);
+    params.put(Constants.Params.DEVICE_NAME, Util.getDeviceName());
+    params.put(Constants.Params.DEVICE_MODEL, Util.getDeviceModel());
+    params.put(Constants.Params.DEVICE_SYSTEM_NAME, Util.getSystemName());
+    params.put(Constants.Params.DEVICE_SYSTEM_VERSION, Util.getSystemVersion());
+    params.put(Constants.Keys.TIMEZONE, localTimeZone.getID());
+    params.put(Constants.Keys.TIMEZONE_OFFSET_SECONDS, Integer.toString(timezoneOffsetSeconds));
+    params.put(Constants.Keys.LOCALE, Util.getLocale());
+    params.put(Constants.Keys.COUNTRY, Constants.Values.DETECT);
+    params.put(Constants.Keys.REGION, Constants.Values.DETECT);
+    params.put(Constants.Keys.CITY, Constants.Values.DETECT);
+    params.put(Constants.Keys.LOCATION, Constants.Values.DETECT);
+    if (Boolean.TRUE.equals(limitAdTracking)) {
+      params.put(Constants.Params.LIMIT_TRACKING, limitAdTracking.toString());
+    }
+    if (attributes != null) {
+      params.put(Constants.Params.USER_ATTRIBUTES, JsonConverter.toJson(attributes));
+    }
+    if (Constants.isDevelopmentModeEnabled) {
+      params.put(Constants.Params.DEV_MODE, Boolean.TRUE.toString());
+    }
+
+    // Get the current inbox messages on the device.
+    params.put(Constants.Params.INBOX_MESSAGES, LeanplumInbox.getInstance().messagesIds());
+
+    Util.initializePreLeanplumInstall(params);
+
+    // Issue start API call.
+    Request req = Request.post(Constants.Methods.START, params);
+    req.onApiResponse(new Request.ApiResponseCallback() {
+      @Override
+      public void response(List<Map<String, Object>> requests, JSONObject response) {
+        Leanplum.handleApiResponse(response, requests);
+      }
+    });
+
+    if (isBackground) {
+      req.sendEventually();
+    } else {
+      req.sendIfConnected();
+    }
+
+    LeanplumInternal.triggerStartIssued();
+  }
+
+  private static void handleApiResponse(JSONObject response, List<Map<String, Object>> requests) {
+    boolean hasStartResponse = false;
+    JSONObject lastStartResponse = null;
+
+    // Find and handle the last start response.
+    try {
+      int numResponses = Request.numResponses(response);
+      for (int i = requests.size() - 1; i >= 0; i--) {
+        Map<String, Object> request = requests.get(i);
+        if (Constants.Methods.START.equals(request.get(Constants.Params.ACTION))) {
+          if (i < numResponses) {
+            lastStartResponse = Request.getResponseAt(response, i);
+          }
+          hasStartResponse = true;
+          break;
+        }
+      }
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+
+    if (hasStartResponse) {
+      if (!LeanplumInternal.hasStarted()) {
+        Leanplum.handleStartResponse(lastStartResponse);
+      }
+    }
+  }
+
+  private static void handleStartResponse(JSONObject response) {
+    boolean success = Request.isResponseSuccess(response);
+    if (!success) {
+      try {
+        LeanplumInternal.setHasStarted(true);
+        LeanplumInternal.setStartSuccessful(false);
+
+        // Load the variables that were stored on the device from the last session.
+        VarCache.loadDiffs();
+
+        triggerStartResponse(false);
+      } catch (Throwable t) {
+        Util.handleException(t);
+      }
+    } else {
+      try {
+        LeanplumInternal.setHasStarted(true);
+        LeanplumInternal.setStartSuccessful(true);
+
+        JSONObject values = response.optJSONObject(Constants.Keys.VARS);
+        if (values == null) {
+          Log.e("No variable values were received from the server. " +
+              "Please contact us to investigate.");
+        }
+
+        JSONObject messages = response.optJSONObject(Constants.Keys.MESSAGES);
+        if (messages == null) {
+          Log.d("No messages received from the server.");
+        }
+
+        JSONObject regions = response.optJSONObject(Constants.Keys.REGIONS);
+        if (regions == null) {
+          Log.d("No regions received from the server.");
+        }
+
+        JSONArray variants = response.optJSONArray(Constants.Keys.VARIANTS);
+        if (variants == null) {
+          Log.d("No variants received from the server.");
+        }
+
+        String token = response.optString(Constants.Keys.TOKEN, null);
+        Request.setToken(token);
+        Request.saveToken();
+
+        applyContentInResponse(response, true);
+
+        VarCache.saveUserAttributes();
+        triggerStartResponse(true);
+
+        if (response.optBoolean(Constants.Keys.SYNC_INBOX, false)) {
+          LeanplumInbox.getInstance().downloadMessages();
+        }
+
+        if (response.optBoolean(Constants.Keys.LOGGING_ENABLED, false)) {
+          Constants.loggingEnabled = true;
+        }
+
+        // Allow bidirectional realtime variable updates.
+        if (Constants.isDevelopmentModeEnabled) {
+
+          final Context currentContext = (
+              LeanplumActivityHelper.currentActivity != context &&
+                  LeanplumActivityHelper.currentActivity != null) ?
+              LeanplumActivityHelper.currentActivity
+              : context;
+
+          // Register device.
+          if (!response.optBoolean(
+              Constants.Keys.IS_REGISTERED) && registerDeviceHandler != null) {
+            registerDeviceHandler.setResponseHandler(new RegisterDeviceCallback.EmailCallback() {
+              @Override
+              public void onResponse(String email) {
+                try {
+                  if (email != null) {
+                    Registration.registerDevice(email, new StartCallback() {
+                      @Override
+                      public void onResponse(boolean success) {
+                        if (registerDeviceFinishedHandler != null) {
+                          registerDeviceFinishedHandler.setSuccess(success);
+                          OsHandler.getInstance().post(registerDeviceFinishedHandler);
+                        }
+                        if (success) {
+                          try {
+                            LeanplumInternal.onHasStartedAndRegisteredAsDeveloper();
+                          } catch (Throwable t) {
+                            Util.handleException(t);
+                          }
+                        }
+                      }
+                    });
+                  }
+                } catch (Throwable t) {
+                  Util.handleException(t);
+                }
+              }
+            });
+            OsHandler.getInstance().post(registerDeviceHandler);
+          }
+
+          // Show device is already registered.
+          if (response.optBoolean(Constants.Keys.IS_REGISTERED_FROM_OTHER_APP)) {
+            OsHandler.getInstance().post(new Runnable() {
+              @Override
+              public void run() {
+                try {
+                  NotificationCompat.Builder mBuilder =
+                      new NotificationCompat.Builder(currentContext)
+                          .setSmallIcon(android.R.drawable.star_on)
+                          .setContentTitle("Leanplum")
+                          .setContentText("Your device is registered.");
+                  mBuilder.setContentIntent(PendingIntent.getActivity(
+                      currentContext.getApplicationContext(), 0, new Intent(), 0));
+                  NotificationManager mNotificationManager =
+                      (NotificationManager) currentContext.getSystemService(
+                          Context.NOTIFICATION_SERVICE);
+                  // mId allows you to update the notification later on.
+                  mNotificationManager.notify(0, mBuilder.build());
+                } catch (Throwable t) {
+                  Log.i("Device is registered.");
+                }
+              }
+            });
+          }
+
+          boolean isRegistered = response.optBoolean(Constants.Keys.IS_REGISTERED);
+
+          // Check for updates.
+          final String latestVersion = response.optString(Constants.Keys.LATEST_VERSION, null);
+          if (isRegistered && latestVersion != null) {
+            Log.i("An update to Leanplum Android SDK, " + latestVersion +
+                ", is available. Go to leanplum.com to download it.");
+          }
+
+          JSONObject valuesFromCode = response.optJSONObject(Constants.Keys.VARS_FROM_CODE);
+          if (valuesFromCode == null) {
+            valuesFromCode = new JSONObject();
+          }
+
+          JSONObject actionDefinitions =
+              response.optJSONObject(Constants.Params.ACTION_DEFINITIONS);
+          if (actionDefinitions == null) {
+            actionDefinitions = new JSONObject();
+          }
+
+          JSONObject fileAttributes = response.optJSONObject(Constants.Params.FILE_ATTRIBUTES);
+          if (fileAttributes == null) {
+            fileAttributes = new JSONObject();
+          }
+
+          VarCache.setDevModeValuesFromServer(
+              JsonConverter.mapFromJson(valuesFromCode),
+              JsonConverter.mapFromJson(fileAttributes),
+              JsonConverter.mapFromJson(actionDefinitions));
+
+          if (isRegistered) {
+            LeanplumInternal.onHasStartedAndRegisteredAsDeveloper();
+          }
+        }
+
+        LeanplumInternal.moveToForeground();
+        startHeartbeat();
+      } catch (Throwable t) {
+        Util.handleException(t);
+      }
+    }
+  }
+
+  /**
+   * Applies the variables, messages, or update rules in a start or getVars response.
+   *
+   * @param response The response containing content.
+   * @param alwaysApply Always apply the content regardless of whether the content changed.
+   */
+  private static void applyContentInResponse(JSONObject response, boolean alwaysApply) {
+    Map<String, Object> values = JsonConverter.mapFromJsonOrDefault(
+        response.optJSONObject(Constants.Keys.VARS));
+    Map<String, Object> messages = JsonConverter.mapFromJsonOrDefault(
+        response.optJSONObject(Constants.Keys.MESSAGES));
+    List<Map<String, Object>> updateRules = JsonConverter.listFromJsonOrDefault(
+        response.optJSONArray(Constants.Keys.UPDATE_RULES));
+    List<Map<String, Object>> eventRules = JsonConverter.listFromJsonOrDefault(
+        response.optJSONArray(Constants.Keys.EVENT_RULES));
+    Map<String, Object> regions = JsonConverter.mapFromJsonOrDefault(
+        response.optJSONObject(Constants.Keys.REGIONS));
+    List<Map<String, Object>> variants = JsonConverter.listFromJsonOrDefault(
+        response.optJSONArray(Constants.Keys.VARIANTS));
+
+    if (alwaysApply
+        || !values.equals(VarCache.getDiffs())
+        || !messages.equals(VarCache.getMessageDiffs())
+        || !updateRules.equals(VarCache.getUpdateRuleDiffs())
+        || !eventRules.equals(VarCache.getEventRuleDiffs())
+        || !regions.equals(VarCache.regions())) {
+      VarCache.applyVariableDiffs(values, messages, updateRules,
+          eventRules, regions, variants);
+    }
+  }
+
+  /**
+   * Used by wrapper SDKs like Unity to override the SDK client name and version.
+   */
+  static void setClient(String client, String sdkVersion, String defaultDeviceId) {
+    Constants.CLIENT = client;
+    Constants.LEANPLUM_VERSION = sdkVersion;
+    Constants.defaultDeviceId = defaultDeviceId;
+  }
+
+  /**
+   * Call this when your activity pauses. This is called from LeanplumActivityHelper.
+   */
+  static void pause() {
+    if (Constants.isNoop()) {
+      return;
+    }
+    if (!LeanplumInternal.hasCalledStart()) {
+      Log.e("You cannot call pause before calling start");
+      return;
+    }
+    LeanplumInternal.setIsPaused(true);
+
+    if (LeanplumInternal.isPaused()) {
+      pauseInternal();
+    } else {
+      LeanplumInternal.addStartIssuedHandler(new Runnable() {
+        @Override
+        public void run() {
+          try {
+            pauseInternal();
+          } catch (Throwable t) {
+            Util.handleException(t);
+          }
+        }
+      });
+    }
+  }
+
+  private static void pauseInternal() {
+    Request.post(Constants.Methods.PAUSE_SESSION, null).sendIfConnected();
+    pauseHeartbeat();
+  }
+
+  /**
+   * Call this when your activity resumes. This is called from LeanplumActivityHelper.
+   */
+  static void resume() {
+    if (Constants.isNoop()) {
+      return;
+    }
+    if (!LeanplumInternal.hasCalledStart()) {
+      Log.e("You cannot call resume before calling start");
+      return;
+    }
+    LeanplumInternal.setIsPaused(false);
+
+    if (LeanplumInternal.issuedStart()) {
+      resumeInternal();
+    } else {
+      LeanplumInternal.addStartIssuedHandler(new Runnable() {
+        @Override
+        public void run() {
+          try {
+            resumeInternal();
+          } catch (Throwable t) {
+            Util.handleException(t);
+          }
+        }
+      });
+    }
+  }
+
+  private static void resumeInternal() {
+    Request request = Request.post(Constants.Methods.RESUME_SESSION, null);
+    if (LeanplumInternal.hasStartedInBackground()) {
+      LeanplumInternal.setStartedInBackground(false);
+      request.sendIfConnected();
+    } else {
+      request.sendIfDelayed();
+      LeanplumInternal.maybePerformActions("resume", null,
+          LeanplumMessageMatchFilter.LEANPLUM_ACTION_FILTER_ALL, null, null);
+    }
+    resumeHeartbeat();
+  }
+
+  /**
+   * Send a heartbeat every 15 minutes while the app is running.
+   */
+  private static void startHeartbeat() {
+    synchronized (heartbeatLock) {
+      heartbeatExecutor = Executors.newSingleThreadScheduledExecutor();
+      heartbeatExecutor.scheduleAtFixedRate(new Runnable() {
+        public void run() {
+          try {
+            Request.post(Constants.Methods.HEARTBEAT, null).sendIfDelayed();
+          } catch (Throwable t) {
+            Util.handleException(t);
+          }
+        }
+      }, 15, 15, TimeUnit.MINUTES);
+    }
+  }
+
+  private static void pauseHeartbeat() {
+    synchronized (heartbeatLock) {
+      if (heartbeatExecutor != null) {
+        heartbeatExecutor.shutdown();
+      }
+    }
+  }
+
+  private static void resumeHeartbeat() {
+    startHeartbeat();
+  }
+
+  /**
+   * Call this to explicitly end the session. This should not be used in most cases, so we won't
+   * make it public for now.
+   */
+  static void stop() {
+    if (Constants.isNoop()) {
+      return;
+    }
+    if (!LeanplumInternal.hasCalledStart()) {
+      Log.e("You cannot call stop before calling start");
+      return;
+    }
+
+    if (LeanplumInternal.issuedStart()) {
+      stopInternal();
+    } else {
+      LeanplumInternal.addStartIssuedHandler(new Runnable() {
+        @Override
+        public void run() {
+          try {
+            stopInternal();
+          } catch (Throwable t) {
+            Util.handleException(t);
+          }
+        }
+      });
+    }
+  }
+
+  private static void stopInternal() {
+    Request.post(Constants.Methods.STOP, null).sendIfConnected();
+  }
+
+  /**
+   * Whether or not Leanplum has finished starting.
+   */
+  public static boolean hasStarted() {
+    return LeanplumInternal.hasStarted();
+  }
+
+  /**
+   * Returns an instance to the singleton Newsfeed object.
+   *
+   * @deprecated use {@link #getInbox} instead
+   */
+  public static Newsfeed newsfeed() {
+    return Newsfeed.getInstance();
+  }
+
+  /**
+   * Returns an instance to the singleton LeanplumInbox object.
+   */
+  public static LeanplumInbox getInbox() {
+    return LeanplumInbox.getInstance();
+  }
+
+  /**
+   * Whether or not Leanplum has finished starting and the device is registered as a developer.
+   */
+  public static boolean hasStartedAndRegisteredAsDeveloper() {
+    return LeanplumInternal.hasStartedAndRegisteredAsDeveloper();
+  }
+
+  /**
+   * Add a callback for when the start call finishes, and variables are returned back from the
+   * server.
+   */
+  public static void addStartResponseHandler(StartCallback handler) {
+    if (handler == null) {
+      Log.e("addStartResponseHandler - Invalid handler parameter provided.");
+      return;
+    }
+
+    if (LeanplumInternal.hasStarted()) {
+      if (LeanplumInternal.isStartSuccessful()) {
+        handler.setSuccess(true);
+      }
+      handler.run();
+    } else {
+      synchronized (startHandlers) {
+        if (startHandlers.indexOf(handler) == -1) {
+          startHandlers.add(handler);
+        }
+      }
+    }
+  }
+
+  /**
+   * Removes a start response callback.
+   */
+  public static void removeStartResponseHandler(StartCallback handler) {
+    if (handler == null) {
+      Log.e("removeStartResponseHandler - Invalid handler parameter provided.");
+      return;
+    }
+
+    synchronized (startHandlers) {
+      startHandlers.remove(handler);
+    }
+  }
+
+  private static void triggerStartResponse(boolean success) {
+    synchronized (startHandlers) {
+      for (StartCallback callback : startHandlers) {
+        callback.setSuccess(success);
+        OsHandler.getInstance().post(callback);
+      }
+      startHandlers.clear();
+    }
+  }
+
+  /**
+   * Add a callback for when the variables receive new values from the server. This will be called
+   * on start, and also later on if the user is in an experiment that can updated in realtime.
+   */
+  public static void addVariablesChangedHandler(VariablesChangedCallback handler) {
+    if (handler == null) {
+      Log.e("addVariablesChangedHandler - Invalid handler parameter provided.");
+      return;
+    }
+
+    synchronized (variablesChangedHandlers) {
+      variablesChangedHandlers.add(handler);
+    }
+    if (VarCache.hasReceivedDiffs()) {
+      handler.variablesChanged();
+    }
+  }
+
+  /**
+   * Removes a variables changed callback.
+   */
+  public static void removeVariablesChangedHandler(VariablesChangedCallback handler) {
+    if (handler == null) {
+      Log.e("removeVariablesChangedHandler - Invalid handler parameter provided.");
+      return;
+    }
+
+    synchronized (variablesChangedHandlers) {
+      variablesChangedHandlers.remove(handler);
+    }
+  }
+
+  private static void triggerVariablesChanged() {
+    synchronized (variablesChangedHandlers) {
+      for (VariablesChangedCallback callback : variablesChangedHandlers) {
+        OsHandler.getInstance().post(callback);
+      }
+    }
+  }
+
+  /**
+   * Add a callback for when no more file downloads are pending (either when no files needed to be
+   * downloaded or all downloads have been completed).
+   */
+  public static void addVariablesChangedAndNoDownloadsPendingHandler(
+      VariablesChangedCallback handler) {
+    if (handler == null) {
+      Log.e("addVariablesChangedAndNoDownloadsPendingHandler - Invalid handler parameter " +
+          "provided.");
+      return;
+    }
+
+    synchronized (noDownloadsHandlers) {
+      noDownloadsHandlers.add(handler);
+    }
+    if (VarCache.hasReceivedDiffs()
+        && Request.numPendingDownloads() == 0) {
+      handler.variablesChanged();
+    }
+  }
+
+  /**
+   * Removes a variables changed and no downloads pending callback.
+   */
+  public static void removeVariablesChangedAndNoDownloadsPendingHandler(
+      VariablesChangedCallback handler) {
+    if (handler == null) {
+      Log.e("removeVariablesChangedAndNoDownloadsPendingHandler - Invalid handler parameter " +
+          "provided.");
+      return;
+    }
+
+    synchronized (noDownloadsHandlers) {
+      noDownloadsHandlers.remove(handler);
+    }
+  }
+
+  /**
+   * Add a callback to call ONCE when no more file downloads are pending (either when no files
+   * needed to be downloaded or all downloads have been completed).
+   */
+  public static void addOnceVariablesChangedAndNoDownloadsPendingHandler(
+      VariablesChangedCallback handler) {
+    if (handler == null) {
+      Log.e("addOnceVariablesChangedAndNoDownloadsPendingHandler - Invalid handler parameter" +
+          " provided.");
+      return;
+    }
+
+    if (VarCache.hasReceivedDiffs()
+        && Request.numPendingDownloads() == 0) {
+      handler.variablesChanged();
+    } else {
+      synchronized (onceNoDownloadsHandlers) {
+        onceNoDownloadsHandlers.add(handler);
+      }
+    }
+  }
+
+  /**
+   * Removes a once variables changed and no downloads pending callback.
+   */
+  public static void removeOnceVariablesChangedAndNoDownloadsPendingHandler(
+      VariablesChangedCallback handler) {
+    if (handler == null) {
+      Log.e("removeOnceVariablesChangedAndNoDownloadsPendingHandler - Invalid handler" +
+          " parameter provided.");
+      return;
+    }
+
+    synchronized (onceNoDownloadsHandlers) {
+      onceNoDownloadsHandlers.remove(handler);
+    }
+  }
+
+  static void triggerVariablesChangedAndNoDownloadsPending() {
+    synchronized (noDownloadsHandlers) {
+      for (VariablesChangedCallback callback : noDownloadsHandlers) {
+        OsHandler.getInstance().post(callback);
+      }
+    }
+    synchronized (onceNoDownloadsHandlers) {
+      for (VariablesChangedCallback callback : onceNoDownloadsHandlers) {
+        OsHandler.getInstance().post(callback);
+      }
+      onceNoDownloadsHandlers.clear();
+    }
+  }
+
+  /**
+   * Defines an action that is used within Leanplum Marketing Automation. Actions can be set up to
+   * get triggered based on app opens, events, and states. Call {@link Leanplum#onAction} to handle
+   * the action.
+   *
+   * @param name The name of the action to register.
+   * @param kind Whether to display the action as a message and/or a regular action.
+   * @param args User-customizable options for the action.
+   */
+  public static void defineAction(String name, int kind, ActionArgs args) {
+    defineAction(name, kind, args, null, null);
+  }
+
+  @Deprecated
+  static void defineAction(String name, int kind, ActionArgs args,
+      Map<String, Object> options) {
+    defineAction(name, kind, args, options, null);
+  }
+
+  /**
+   * Defines an action that is used within Leanplum Marketing Automation. Actions can be set up to
+   * get triggered based on app opens, events, and states.
+   *
+   * @param name The name of the action to register.
+   * @param kind Whether to display the action as a message and/or a regular action.
+   * @param args User-customizable options for the action.
+   * @param responder Called when the action is triggered with a context object containing the
+   * user-specified options.
+   */
+  public static void defineAction(String name, int kind, ActionArgs args,
+      ActionCallback responder) {
+    defineAction(name, kind, args, null, responder);
+  }
+
+  private static void defineAction(String name, int kind, ActionArgs args,
+      Map<String, Object> options, ActionCallback responder) {
+    if (TextUtils.isEmpty(name)) {
+      Log.e("defineAction - Empty name parameter provided.");
+      return;
+    }
+    if (args == null) {
+      Log.e("defineAction - Invalid args parameter provided.");
+      return;
+    }
+
+    try {
+      Context context = Leanplum.getContext();
+      if (!initializedMessageTemplates) {
+        initializedMessageTemplates = true;
+        MessageTemplates.register(context);
+      }
+
+      if (options == null) {
+        options = new HashMap<>();
+      }
+      LeanplumInternal.getActionHandlers().remove(name);
+      VarCache.registerActionDefinition(name, kind, args.getValue(), options);
+      if (responder != null) {
+        onAction(name, responder);
+      }
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+  }
+
+  /**
+   * Adds a callback that handles an action with the given name.
+   *
+   * @param actionName The name of the type of action to handle.
+   * @param handler The callback that runs when the action is triggered.
+   */
+  public static void onAction(String actionName, ActionCallback handler) {
+    if (actionName == null) {
+      Log.e("onAction - Invalid actionName parameter provided.");
+      return;
+    }
+    if (handler == null) {
+      Log.e("onAction - Invalid handler parameter provided.");
+      return;
+    }
+
+    List<ActionCallback> handlers = LeanplumInternal.getActionHandlers().get(actionName);
+    if (handlers == null) {
+      handlers = new ArrayList<>();
+      LeanplumInternal.getActionHandlers().put(actionName, handlers);
+    }
+    handlers.add(handler);
+  }
+
+  /**
+   * Updates the user ID and adds or modifies user attributes.
+   */
+  public static void setUserAttributes(final String userId, Map<String, ?> userAttributes) {
+    if (Constants.isNoop()) {
+      return;
+    }
+    if (!LeanplumInternal.hasCalledStart()) {
+      Log.e("You cannot call setUserAttributes before calling start");
+      return;
+    }
+    try {
+      final HashMap<String, Object> params = new HashMap<>();
+      if (userId != null) {
+        params.put(Constants.Params.NEW_USER_ID, userId);
+      }
+      if (userAttributes != null) {
+        userAttributes = LeanplumInternal.validateAttributes(userAttributes, "userAttributes",
+            true);
+        params.put(Constants.Params.USER_ATTRIBUTES, JsonConverter.toJson(userAttributes));
+        LeanplumInternal.getUserAttributeChanges().add(userAttributes);
+      }
+
+      if (LeanplumInternal.issuedStart()) {
+        setUserAttributesInternal(userId, params);
+      } else {
+        LeanplumInternal.addStartIssuedHandler(new Runnable() {
+          @Override
+          public void run() {
+            try {
+              setUserAttributesInternal(userId, params);
+            } catch (Throwable t) {
+              Util.handleException(t);
+            }
+          }
+        });
+      }
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+  }
+
+  private static void setUserAttributesInternal(String userId,
+      HashMap<String, Object> requestArgs) {
+    Request.post(Constants.Methods.SET_USER_ATTRIBUTES, requestArgs).send();
+    if (userId != null && userId.length() > 0) {
+      Request.setUserId(userId);
+      if (LeanplumInternal.hasStarted()) {
+        VarCache.saveDiffs();
+      }
+    }
+    LeanplumInternal.recordAttributeChanges();
+  }
+
+  /**
+   * Updates the user ID.
+   */
+  public static void setUserId(String userId) {
+    if (userId == null) {
+      Log.e("setUserId - Invalid userId parameter provided.");
+      return;
+    }
+
+    setUserAttributes(userId, null);
+  }
+
+  /**
+   * Adds or modifies user attributes.
+   */
+  public static void setUserAttributes(Map<String, Object> userAttributes) {
+    if (userAttributes == null || userAttributes.isEmpty()) {
+      Log.e("setUserAttributes - Invalid userAttributes parameter provided (null or empty).");
+      return;
+    }
+
+    setUserAttributes(null, userAttributes);
+  }
+
+  /**
+   * Sets the registration ID used for Cloud Messaging.
+   */
+  static void setRegistrationId(final String registrationId) {
+    if (Constants.isNoop()) {
+      return;
+    }
+    pushStartCallback = new Runnable() {
+      @Override
+      public void run() {
+        if (Constants.isNoop()) {
+          return;
+        }
+        try {
+          HashMap<String, Object> params = new HashMap<>();
+          params.put(Constants.Params.DEVICE_PUSH_TOKEN, registrationId);
+          Request.post(Constants.Methods.SET_DEVICE_ATTRIBUTES, params).send();
+        } catch (Throwable t) {
+          Util.handleException(t);
+        }
+      }
+    };
+    LeanplumInternal.addStartIssuedHandler(pushStartCallback);
+  }
+
+  /**
+   * Sets the traffic source info for the current user. Keys in info must be one of: publisherId,
+   * publisherName, publisherSubPublisher, publisherSubSite, publisherSubCampaign,
+   * publisherSubAdGroup, publisherSubAd.
+   */
+  public static void setTrafficSourceInfo(Map<String, String> info) {
+    if (Constants.isNoop()) {
+      return;
+    }
+    if (!LeanplumInternal.hasCalledStart()) {
+      Log.e("You cannot call setTrafficSourceInfo before calling start");
+      return;
+    }
+    if (info == null || info.isEmpty()) {
+      Log.e("setTrafficSourceInfo - Invalid info parameter provided (null or empty).");
+      return;
+    }
+
+    try {
+      final HashMap<String, Object> params = new HashMap<>();
+      info = LeanplumInternal.validateAttributes(info, "info", false);
+      params.put(Constants.Params.TRAFFIC_SOURCE, JsonConverter.toJson(info));
+      if (LeanplumInternal.issuedStart()) {
+        setTrafficSourceInfoInternal(params);
+      } else {
+        LeanplumInternal.addStartIssuedHandler(new Runnable() {
+          @Override
+          public void run() {
+            try {
+              setTrafficSourceInfoInternal(params);
+            } catch (Throwable t) {
+              Util.handleException(t);
+            }
+          }
+        });
+      }
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+  }
+
+  private static void setTrafficSourceInfoInternal(HashMap<String, Object> params) {
+    Request.post(Constants.Methods.SET_TRAFFIC_SOURCE_INFO, params).send();
+  }
+
+  /**
+   * Logs a particular event in your application. The string can be any value of your choosing, and
+   * will show up in the dashboard.
+   * <p>
+   * <p>To track Purchase events, call {@link Leanplum#trackGooglePlayPurchase} instead for in-app
+   * purchases, or use {@link Leanplum#PURCHASE_EVENT_NAME} as the event name for other types of
+   * purchases.
+   *
+   * @param event Name of the event. Event may be empty for message impression events.
+   * @param value The value of the event. The value is special in that you can use it for targeting
+   * content and messages to users who have a particular lifetime value. For purchase events, the
+   * value is the revenue associated with the purchase.
+   * @param info Basic context associated with the event, such as the item purchased. info is
+   * treated like a default parameter.
+   * @param params Key-value pairs with metrics or data associated with the event. Parameters can be
+   * strings or numbers. You can use up to 200 different parameter names in your app.
+   */
+  public static void track(final String event, double value, String info,
+      Map<String, ?> params) {
+    LeanplumInternal.track(event, value, info, params, null);
+  }
+
+  /**
+   * Tracks an in-app purchase as a Purchase event.
+   *
+   * @param item The name of the item that was purchased.
+   * @param priceMicros The price in micros in the user's local currency.
+   * @param currencyCode The currency code corresponding to the price.
+   * @param purchaseData Purchase data from purchase.getOriginalJson().
+   * @param dataSignature Signature from purchase.getSignature().
+   */
+  public static void trackGooglePlayPurchase(String item, long priceMicros, String currencyCode,
+      String purchaseData, String dataSignature) {
+    trackGooglePlayPurchase(PURCHASE_EVENT_NAME, item, priceMicros, currencyCode, purchaseData,
+        dataSignature, null);
+  }
+
+  /**
+   * Tracks an in-app purchase as a Purchase event.
+   *
+   * @param item The name of the item that was purchased.
+   * @param priceMicros The price in micros in the user's local currency.
+   * @param currencyCode The currency code corresponding to the price.
+   * @param purchaseData Purchase data from purchase.getOriginalJson().
+   * @param dataSignature Signature from purchase.getSignature().
+   * @param params Any additional parameters to track with the event.
+   */
+  public static void trackGooglePlayPurchase(String item, long priceMicros, String currencyCode,
+      String purchaseData, String dataSignature, Map<String, ?> params) {
+    trackGooglePlayPurchase(PURCHASE_EVENT_NAME, item, priceMicros, currencyCode,
+        purchaseData, dataSignature, params);
+  }
+
+  /**
+   * Tracks an in-app purchase.
+   *
+   * @param eventName The name of the event to record the purchase under. Normally, this would be
+   * {@link Leanplum#PURCHASE_EVENT_NAME}.
+   * @param item The name of the item that was purchased.
+   * @param priceMicros The price in micros in the user's local currency.
+   * @param currencyCode The currency code corresponding to the price.
+   * @param purchaseData Purchase data from purchase.getOriginalJson().
+   * @param dataSignature Signature from purchase.getSignature().
+   * @param params Any additional parameters to track with the event.
+   */
+  @SuppressWarnings("SameParameterValue")
+  public static void trackGooglePlayPurchase(String eventName, String item, long priceMicros,
+      String currencyCode, String purchaseData, String dataSignature, Map<String, ?> params) {
+    if (TextUtils.isEmpty(eventName)) {
+      Log.w("trackGooglePlayPurchase - Empty eventName parameter provided.");
+    }
+
+    final Map<String, String> requestArgs = new HashMap<>();
+    requestArgs.put(Constants.Params.GOOGLE_PLAY_PURCHASE_DATA, purchaseData);
+    requestArgs.put(Constants.Params.GOOGLE_PLAY_PURCHASE_DATA_SIGNATURE, dataSignature);
+    requestArgs.put(Constants.Params.IAP_CURRENCY_CODE, currencyCode);
+
+    Map<String, Object> modifiedParams;
+    if (params == null) {
+      modifiedParams = new HashMap<>();
+    } else {
+      modifiedParams = new HashMap<>(params);
+    }
+    modifiedParams.put(Constants.Params.IAP_ITEM, item);
+
+    LeanplumInternal.track(eventName, priceMicros / 1000000.0, null, modifiedParams, requestArgs);
+  }
+
+  /**
+   * Logs a particular event in your application. The string can be any value of your choosing, and
+   * will show up in the dashboard.
+   * <p>
+   * <p>To track Purchase events, use {@link Leanplum#PURCHASE_EVENT_NAME}.
+   *
+   * @param event Name of the event.
+   */
+  public static void track(String event) {
+    track(event, 0.0, "", null);
+  }
+
+  /**
+   * Logs a particular event in your application. The string can be any value of your choosing, and
+   * will show up in the dashboard.
+   * <p>
+   * <p>To track Purchase events, use {@link Leanplum#PURCHASE_EVENT_NAME}.
+   *
+   * @param event Name of the event.
+   * @param value The value of the event. The value is special in that you can use it for targeting
+   * content and messages to users who have a particular lifetime value. For purchase events, the
+   * value is the revenue associated with the purchase.
+   */
+  public static void track(String event, double value) {
+    track(event, value, "", null);
+  }
+
+  /**
+   * Logs a particular event in your application. The string can be any value of your choosing, and
+   * will show up in the dashboard.
+   * <p>
+   * <p>To track Purchase events, use {@link Leanplum#PURCHASE_EVENT_NAME}.
+   *
+   * @param event Name of the event.
+   * @param info Basic context associated with the event, such as the item purchased. info is
+   * treated like a default parameter.
+   */
+  public static void track(String event, String info) {
+    track(event, 0.0, info, null);
+  }
+
+  /**
+   * Logs a particular event in your application. The string can be any value of your choosing, and
+   * will show up in the dashboard.
+   * <p>
+   * <p>To track Purchase events, use {@link Leanplum#PURCHASE_EVENT_NAME}.
+   *
+   * @param event Name of the event.
+   * @param params Key-value pairs with metrics or data associated with the event. Parameters can be
+   * strings or numbers. You can use up to 200 different parameter names in your app.
+   */
+  public static void track(String event, Map<String, ?> params) {
+    track(event, 0.0, "", params);
+  }
+
+  /**
+   * Logs a particular event in your application. The string can be any value of your choosing, and
+   * will show up in the dashboard.
+   * <p>
+   * <p>To track Purchase events, use {@link Leanplum#PURCHASE_EVENT_NAME}.
+   *
+   * @param event Name of the event.
+   * @param value The value of the event. The value is special in that you can use it for targeting
+   * content and messages to users who have a particular lifetime value. For purchase events, the
+   * value is the revenue associated with the purchase.
+   * @param params Key-value pairs with metrics or data associated with the event. Parameters can be
+   * strings or numbers. You can use up to 200 different parameter names in your app.
+   */
+  public static void track(String event, double value, Map<String, ?> params) {
+    track(event, value, "", params);
+  }
+
+  /**
+   * Logs a particular event in your application. The string can be any value of your choosing, and
+   * will show up in the dashboard.
+   * <p>
+   * <p>To track Purchase events, use {@link Leanplum#PURCHASE_EVENT_NAME}.
+   *
+   * @param event Name of the event.
+   * @param value The value of the event. The value is special in that you can use it for targeting
+   * content and messages to users who have a particular lifetime value. For purchase events, the
+   * value is the revenue associated with the purchase.
+   * @param info Basic context associated with the event, such as the item purchased. info is
+   * treated like a default parameter.
+   */
+  public static void track(String event, double value, String info) {
+    track(event, value, info, null);
+  }
+
+  /**
+   * Advances to a particular state in your application. The string can be any value of your
+   * choosing, and will show up in the dashboard. A state is a section of your app that the user is
+   * currently in.
+   *
+   * @param state Name of the state. State may be empty for message impression events.
+   * @param info Basic context associated with the state, such as the item purchased. info is
+   * treated like a default parameter.
+   * @param params Key-value pairs with metrics or data associated with the state. Parameters can be
+   * strings or numbers. You can use up to 200 different parameter names in your app.
+   */
+  public static void advanceTo(final String state, String info, final Map<String, ?> params) {
+    if (Constants.isNoop()) {
+      return;
+    }
+    if (!LeanplumInternal.hasCalledStart()) {
+      Log.e("You cannot call advanceTo before calling start");
+      return;
+    }
+
+    try {
+      final Map<String, Object> requestParams = new HashMap<>();
+      requestParams.put(Constants.Params.INFO, info);
+      requestParams.put(Constants.Params.STATE, state);
+      final Map<String, ?> validatedParams;
+      if (params != null) {
+        validatedParams = LeanplumInternal.validateAttributes(params, "params", false);
+        requestParams.put(Constants.Params.PARAMS, JsonConverter.toJson(validatedParams));
+      } else {
+        validatedParams = null;
+      }
+
+      if (LeanplumInternal.issuedStart()) {
+        advanceToInternal(state, validatedParams, requestParams);
+      } else {
+        LeanplumInternal.addStartIssuedHandler(new Runnable() {
+          @Override
+          public void run() {
+            try {
+              advanceToInternal(state, validatedParams, requestParams);
+            } catch (Throwable t) {
+              Util.handleException(t);
+            }
+          }
+        });
+      }
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+  }
+
+  /**
+   * Performs the advance API and any actions that are associated with the state.
+   *
+   * @param state The state name. State may be empty for message impression events.
+   * @param params The state parameters.
+   * @param requestParams The arguments to send with the API request.
+   */
+  private static void advanceToInternal(String state, Map<String, ?> params,
+      Map<String, Object> requestParams) {
+    Request.post(Constants.Methods.ADVANCE, requestParams).send();
+
+    ContextualValues contextualValues = new ContextualValues();
+    contextualValues.parameters = params;
+
+    LeanplumInternal.maybePerformActions("state", state,
+        LeanplumMessageMatchFilter.LEANPLUM_ACTION_FILTER_ALL, null, contextualValues);
+  }
+
+  /**
+   * Advances to a particular state in your application. The string can be any value of your
+   * choosing, and will show up in the dashboard. A state is a section of your app that the user is
+   * currently in.
+   *
+   * @param state Name of the state. State may be empty for message impression events.
+   */
+  public static void advanceTo(String state) {
+    advanceTo(state, "", null);
+  }
+
+  /**
+   * Advances to a particular state in your application. The string can be any value of your
+   * choosing, and will show up in the dashboard. A state is a section of your app that the user is
+   * currently in.
+   *
+   * @param state Name of the state. State may be empty for message impression events.
+   * @param info Basic context associated with the state, such as the item purchased. info is
+   * treated like a default parameter.
+   */
+  public static void advanceTo(String state, String info) {
+    advanceTo(state, info, null);
+  }
+
+  /**
+   * Advances to a particular state in your application. The string can be any value of your
+   * choosing, and will show up in the dashboard. A state is a section of your app that the user is
+   * currently in.
+   *
+   * @param state Name of the state. State may be empty for message impression events.
+   * @param params Key-value pairs with metrics or data associated with the state. Parameters can be
+   * strings or numbers. You can use up to 200 different parameter names in your app.
+   */
+  public static void advanceTo(String state, Map<String, ?> params) {
+    advanceTo(state, "", params);
+  }
+
+  /**
+   * Pauses the current state. You can use this if your game has a "pause" mode. You shouldn't call
+   * it when someone switches out of your app because that's done automatically.
+   */
+  public static void pauseState() {
+    if (Constants.isNoop()) {
+      return;
+    }
+    if (!LeanplumInternal.hasCalledStart()) {
+      Log.e("You cannot call pauseState before calling start");
+      return;
+    }
+
+    try {
+      if (LeanplumInternal.issuedStart()) {
+        pauseStateInternal();
+      } else {
+        LeanplumInternal.addStartIssuedHandler(new Runnable() {
+          @Override
+          public void run() {
+            try {
+              pauseStateInternal();
+            } catch (Throwable t) {
+              Util.handleException(t);
+            }
+          }
+        });
+      }
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+  }
+
+  private static void pauseStateInternal() {
+    Request.post(Constants.Methods.PAUSE_STATE, new HashMap<String, Object>()).send();
+  }
+
+  /**
+   * Resumes the current state.
+   */
+  public static void resumeState() {
+    if (Constants.isNoop()) {
+      return;
+    }
+    if (!LeanplumInternal.hasCalledStart()) {
+      Log.e("You cannot call resumeState before calling start");
+      return;
+    }
+
+    try {
+      if (LeanplumInternal.issuedStart()) {
+        resumeStateInternal();
+      } else {
+        LeanplumInternal.addStartIssuedHandler(new Runnable() {
+          @Override
+          public void run() {
+            try {
+              resumeStateInternal();
+            } catch (Throwable t) {
+              Util.handleException(t);
+            }
+          }
+        });
+      }
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+  }
+
+  private static void resumeStateInternal() {
+    Request.post(Constants.Methods.RESUME_STATE, new HashMap<String, Object>()).send();
+  }
+
+  /**
+   * Forces content to update from the server. If variables have changed, the appropriate callbacks
+   * will fire. Use sparingly as if the app is updated, you'll have to deal with potentially
+   * inconsistent state or user experience.
+   */
+  public static void forceContentUpdate() {
+    forceContentUpdate(null);
+  }
+
+  /**
+   * Forces content to update from the server. If variables have changed, the appropriate callbacks
+   * will fire. Use sparingly as if the app is updated, you'll have to deal with potentially
+   * inconsistent state or user experience.
+   *
+   * @param callback The callback to invoke when the call completes from the server. The callback
+   * will fire regardless of whether the variables have changed.
+   */
+  @SuppressWarnings("SameParameterValue")
+  public static void forceContentUpdate(final VariablesChangedCallback callback) {
+    if (Constants.isNoop()) {
+      if (callback != null) {
+        OsHandler.getInstance().post(callback);
+      }
+      return;
+    }
+    try {
+      Map<String, Object> params = new HashMap<>();
+      params.put(Constants.Params.INCLUDE_DEFAULTS, Boolean.toString(false));
+      params.put(Constants.Params.INBOX_MESSAGES, LeanplumInbox.getInstance().messagesIds());
+      Request req = Request.post(Constants.Methods.GET_VARS, params);
+      req.onResponse(new Request.ResponseCallback() {
+        @Override
+        public void response(JSONObject response) {
+          try {
+            JSONObject lastResponse = Request.getLastResponse(response);
+            if (lastResponse == null) {
+              Log.e("No response received from the server. Please contact us to investigate.");
+            } else {
+              applyContentInResponse(lastResponse, false);
+              if (lastResponse.optBoolean(Constants.Keys.SYNC_INBOX, false)) {
+                LeanplumInbox.getInstance().downloadMessages();
+              }
+              if (lastResponse.optBoolean(Constants.Keys.LOGGING_ENABLED, false)) {
+                Constants.loggingEnabled = true;
+              }
+            }
+            if (callback != null) {
+              OsHandler.getInstance().post(callback);
+            }
+          } catch (Throwable t) {
+            Util.handleException(t);
+          }
+        }
+      });
+      req.onError(
+          new Request.ErrorCallback() {
+            @Override
+            public void error(Exception e) {
+              if (callback != null) {
+                OsHandler.getInstance().post(callback);
+              }
+            }
+          });
+      req.sendIfConnected();
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+  }
+
+  /**
+   * This should be your first statement in a unit test. This prevents Leanplum from communicating
+   * with the server.
+   */
+  public static void enableTestMode() {
+    Constants.isTestMode = true;
+  }
+
+  public static boolean isTestModeEnabled() {
+    return Constants.isTestMode;
+  }
+
+  /**
+   * This should be your first statement in a unit test. This prevents Leanplum from communicating
+   * with the server.
+   */
+  public static void setIsTestModeEnabled(boolean isTestModeEnabled) {
+    Constants.isTestMode = isTestModeEnabled;
+  }
+
+  /**
+   * Gets the path for a particular resource. The resource can be overridden by the server.
+   */
+  public static String pathForResource(String filename) {
+    if (TextUtils.isEmpty(filename)) {
+      Log.e("pathForResource - Empty filename parameter provided.");
+      return null;
+    }
+
+    Var fileVar = Var.defineFile(filename, filename);
+    return (fileVar != null) ? fileVar.fileValue() : null;
+  }
+
+  /**
+   * Traverses the variable structure with the specified path. Path components can be either strings
+   * representing keys in a dictionary, or integers representing indices in a list.
+   */
+  public static Object objectForKeyPath(Object... components) {
+    return objectForKeyPathComponents(components);
+  }
+
+  /**
+   * Traverses the variable structure with the specified path. Path components can be either strings
+   * representing keys in a dictionary, or integers representing indices in a list.
+   */
+  public static Object objectForKeyPathComponents(Object[] pathComponents) {
+    try {
+      return VarCache.getMergedValueFromComponentArray(pathComponents);
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+    return null;
+  }
+
+  /**
+   * Returns information about the active variants for the current user. Each variant will contain
+   * an "id" key mapping to the numeric ID of the variant.
+   */
+  public static List<Map<String, Object>> variants() {
+    List<Map<String, Object>> variants = VarCache.variants();
+    if (variants == null) {
+      return new ArrayList<>();
+    }
+    return variants;
+  }
+
+  /**
+   * Returns metadata for all active in-app messages. Recommended only for debugging purposes and
+   * advanced use cases.
+   */
+  public static Map<String, Object> messageMetadata() {
+    Map<String, Object> messages = VarCache.messages();
+    if (messages == null) {
+      return new HashMap<>();
+    }
+    return messages;
+  }
+
+  /**
+   * Set location manually. Calls setDeviceLocation with cell type. Best if used in after calling
+   * disableLocationCollection.
+   *
+   * @param location Device location.
+   */
+  public static void setDeviceLocation(Location location) {
+    setDeviceLocation(location, LeanplumLocationAccuracyType.CELL);
+  }
+
+  /**
+   * Set location manually. Best if used in after calling disableLocationCollection. Useful if you
+   * want to apply additional logic before sending in the location.
+   *
+   * @param location Device location.
+   * @param type LeanplumLocationAccuracyType of the location.
+   */
+  public static void setDeviceLocation(Location location, LeanplumLocationAccuracyType type) {
+    if (locationCollectionEnabled) {
+      Log.w("Leanplum is automatically collecting device location, so there is no need to " +
+          "call setDeviceLocation. If you prefer to always set location manually, " +
+          "then call disableLocationCollection.");
+    }
+    LeanplumInternal.setUserLocationAttribute(location, type,
+        new LeanplumInternal.locationAttributeRequestsCallback() {
+          @Override
+          public void response(boolean success) {
+            if (success) {
+              Log.d("setUserAttributes with location is successfully called");
+            }
+          }
+        });
+  }
+
+  /**
+   * Disable location collection by setting |locationCollectionEnabled| to false.
+   */
+  public static void disableLocationCollection() {
+    locationCollectionEnabled = false;
+  }
+
+  /**
+   * Returns whether a customer enabled location collection.
+   *
+   * @return The value of |locationCollectionEnabled|.
+   */
+  public static boolean isLocationCollectionEnabled() {
+    return locationCollectionEnabled;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumActivityHelper.java
@@ -0,0 +1,344 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import android.app.Activity;
+import android.app.Application;
+import android.app.Application.ActivityLifecycleCallbacks;
+import android.content.res.Resources;
+import android.os.Build;
+import android.os.Bundle;
+
+import com.leanplum.annotations.Parser;
+import com.leanplum.callbacks.PostponableAction;
+import com.leanplum.internal.ActionManager;
+import com.leanplum.internal.LeanplumInternal;
+import com.leanplum.internal.LeanplumUIEditorWrapper;
+import com.leanplum.internal.OsHandler;
+import com.leanplum.internal.Util;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Queue;
+import java.util.Set;
+
+/**
+ * Utility class for handling activity lifecycle events. Call these methods from your activity if
+ * you don't extend one of the Leanplum*Activity classes.
+ *
+ * @author Andrew First
+ */
+public class LeanplumActivityHelper {
+  /**
+   * Whether any of the activities are paused.
+   */
+  static boolean isActivityPaused;
+  private static Set<Class> ignoredActivityClasses;
+
+  /**
+   * Whether lifecycle callbacks were registered. This is only supported on Android OS &gt;= 4.0.
+   */
+  private static boolean registeredCallbacks;
+
+  static Activity currentActivity;
+
+  private final Activity activity;
+  private LeanplumResources res;
+  private LeanplumInflater inflater;
+
+  private static final Queue<Runnable> pendingActions = new LinkedList<>();
+  private static final Runnable runPendingActionsRunnable = new Runnable() {
+    @Override
+    public void run() {
+      runPendingActions();
+    }
+  };
+
+  public LeanplumActivityHelper(Activity activity) {
+    this.activity = activity;
+    Leanplum.setApplicationContext(activity.getApplicationContext());
+    Parser.parseVariables(activity);
+  }
+
+  /**
+   * Retrieves the currently active activity.
+   */
+  public static Activity getCurrentActivity() {
+    return currentActivity;
+  }
+
+  /**
+   * Retrieves if the activity is paused.
+   */
+  public static boolean isActivityPaused() {
+    return isActivityPaused;
+  }
+
+  /**
+   * Enables lifecycle callbacks for Android devices with Android OS &gt;= 4.0
+   */
+  public static void enableLifecycleCallbacks(final Application app) {
+    Leanplum.setApplicationContext(app.getApplicationContext());
+    if (Build.VERSION.SDK_INT < 14) {
+      return;
+    }
+    app.registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
+      @Override
+      public void onActivityStopped(Activity activity) {
+        try {
+          onStop(activity);
+        } catch (Throwable t) {
+          Util.handleException(t);
+        }
+      }
+
+      @Override
+      public void onActivityResumed(final Activity activity) {
+        try {
+          if (Leanplum.isInterfaceEditingEnabled()) {
+            // Execute runnable in next frame to ensure that all system stuff is setup, before
+            // applying UI edits.
+            OsHandler.getInstance().post(new Runnable() {
+              @Override
+              public void run() {
+                LeanplumUIEditorWrapper.getInstance().applyInterfaceEdits(activity);
+              }
+            });
+          }
+          onResume(activity);
+          if (Leanplum.isScreenTrackingEnabled()) {
+            Leanplum.advanceTo(activity.getLocalClassName());
+          }
+        } catch (Throwable t) {
+          Util.handleException(t);
+        }
+      }
+
+      @Override
+      public void onActivityPaused(Activity activity) {
+        try {
+          onPause(activity);
+        } catch (Throwable t) {
+          Util.handleException(t);
+        }
+      }
+
+      @Override
+      public void onActivityStarted(Activity activity) {
+      }
+
+      @Override
+      public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
+      }
+
+      @Override
+      public void onActivityDestroyed(Activity activity) {
+      }
+
+      @Override
+      public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
+      }
+
+    });
+    registeredCallbacks = true;
+  }
+
+  public LeanplumResources getLeanplumResources() {
+    return getLeanplumResources(null);
+  }
+
+  public LeanplumResources getLeanplumResources(Resources baseResources) {
+    if (res != null) {
+      return res;
+    }
+    if (baseResources == null) {
+      baseResources = activity.getResources();
+    }
+    if (baseResources instanceof LeanplumResources) {
+      return (LeanplumResources) baseResources;
+    }
+    res = new LeanplumResources(baseResources);
+    return res;
+  }
+
+  /**
+   * Sets the view from a layout file.
+   */
+  public void setContentView(final int layoutResID) {
+    if (inflater == null) {
+      inflater = LeanplumInflater.from(activity);
+    }
+    activity.setContentView(inflater.inflate(layoutResID));
+  }
+
+  @SuppressWarnings("unused")
+  private static void onPause(Activity activity) {
+    isActivityPaused = true;
+  }
+
+  /**
+   * Call this when your activity gets paused.
+   */
+  public void onPause() {
+    try {
+      if (!registeredCallbacks) {
+        onPause(activity);
+      }
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+  }
+
+  private static void onResume(Activity activity) {
+    isActivityPaused = false;
+    currentActivity = activity;
+    if (LeanplumInternal.isPaused() || LeanplumInternal.hasStartedInBackground()) {
+      Leanplum.resume();
+      LocationManager locationManager = ActionManager.getLocationManager();
+      if (locationManager != null) {
+        locationManager.updateGeofencing();
+        locationManager.updateUserLocation();
+      }
+    }
+
+    // Pending actions execution triggered, but Leanplum.start() may not be done yet.
+    LeanplumInternal.addStartIssuedHandler(runPendingActionsRunnable);
+  }
+
+  /**
+   * Call this when your activity gets resumed.
+   */
+  public void onResume() {
+    try {
+      if (!registeredCallbacks) {
+        onResume(activity);
+      }
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+  }
+
+  private static void onStop(Activity activity) {
+    // onStop is called when the activity gets hidden, and is
+    // called after onPause.
+    //
+    // However, if we're switching to another activity, that activity
+    // will call onResume, so we shouldn't pause if that's the case.
+    //
+    // Thus, we can call pause from here, only if all activities are paused.
+    if (isActivityPaused) {
+      Leanplum.pause();
+      LocationManager locationManager = ActionManager.getLocationManager();
+      if (locationManager != null) {
+        locationManager.updateGeofencing();
+      }
+    }
+    if (currentActivity != null && currentActivity.equals(activity)) {
+      // Don't leak activities.
+      currentActivity = null;
+    }
+  }
+
+  /**
+   * Call this when your activity gets stopped.
+   */
+  public void onStop() {
+    try {
+      if (!registeredCallbacks) {
+        onStop(activity);
+      }
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+  }
+
+  /**
+   * Enqueues a callback to invoke when an activity reaches in the foreground.
+   */
+  public static void queueActionUponActive(Runnable action) {
+    try {
+      if (currentActivity != null && !currentActivity.isFinishing() && !isActivityPaused &&
+          (!(action instanceof PostponableAction) || !isActivityClassIgnored(currentActivity))) {
+        action.run();
+      } else {
+        synchronized (pendingActions) {
+          pendingActions.add(action);
+        }
+      }
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+  }
+
+  /**
+   * Runs any pending actions that have been queued.
+   */
+  private static void runPendingActions() {
+    if (isActivityPaused || currentActivity == null) {
+      // Trying to run pending actions, but no activity is resumed. Skip.
+      return;
+    }
+
+    Queue<Runnable> runningActions;
+    synchronized (pendingActions) {
+      runningActions = new LinkedList<>(pendingActions);
+      pendingActions.clear();
+    }
+    for (Runnable action : runningActions) {
+      // If postponable callback and current activity should be skipped, then postpone.
+      if (action instanceof PostponableAction && isActivityClassIgnored(currentActivity)) {
+        pendingActions.add(action);
+      } else {
+        action.run();
+      }
+    }
+  }
+
+  /**
+   * Whether or not an activity is configured to not show messages.
+   *
+   * @param activity The activity to check.
+   * @return Whether or not the activity is ignored.
+   */
+  private static boolean isActivityClassIgnored(Activity activity) {
+    return ignoredActivityClasses != null && ignoredActivityClasses.contains(activity.getClass());
+  }
+
+  /**
+   * Does not show messages for the provided activity classes.
+   *
+   * @param activityClasses The activity classes to not show messages on.
+   */
+  public static void deferMessagesForActivities(Class... activityClasses) {
+    // Check if valid arguments are provided.
+    if (activityClasses == null || activityClasses.length == 0) {
+      return;
+    }
+    // Lazy instantiate activityClasses set.
+    if (ignoredActivityClasses == null) {
+      ignoredActivityClasses = new HashSet<>(activityClasses.length);
+    }
+    // Add all class names to set.
+    Collections.addAll(ignoredActivityClasses, activityClasses);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumApplication.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import android.annotation.SuppressLint;
+import android.app.Application;
+import android.content.Context;
+import android.content.res.Resources;
+
+import com.leanplum.annotations.Parser;
+import com.leanplum.internal.Constants;
+
+/**
+ * Base class for your Application that handles lifecycle events.
+ *
+ * @author Andrew First
+ */
+@SuppressLint("Registered")
+public class LeanplumApplication extends Application {
+  private static LeanplumApplication instance;
+
+  public static LeanplumApplication getInstance() {
+    return instance;
+  }
+
+  public static Context getContext() {
+    return instance;
+  }
+
+  @Override
+  public void onCreate() {
+    super.onCreate();
+    instance = this;
+    LeanplumActivityHelper.enableLifecycleCallbacks(this);
+    Parser.parseVariables(this);
+  }
+
+  @Override
+  public Resources getResources() {
+    if (Constants.isNoop() || !Leanplum.isResourceSyncingEnabled()) {
+      return super.getResources();
+    }
+    return new LeanplumResources(super.getResources());
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumCloudMessagingProvider.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2016, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import android.content.Context;
+
+import com.leanplum.internal.Constants;
+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;
+
+  /**
+   * Registration app for Cloud Messaging.
+   *
+   * @return String - registration id for app.
+   */
+  public abstract String getRegistrationId();
+
+  /**
+   * Verifies that Android Manifest is set up correctly.
+   *
+   * @return true If Android Manifest is set up correctly.
+   */
+  public abstract boolean isManifestSetUp();
+
+  public abstract boolean isInitialized();
+
+  /**
+   * Unregister from cloud messaging.
+   */
+  public abstract void unregister();
+
+  static String getCurrentRegistrationId() {
+    return registrationId;
+  }
+
+  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());
+    }
+    // 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.
+   */
+  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);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumDeviceIdMode.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2015, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+/**
+ * LeanplumDeviceIdMode enum used for Leanplum.setDeviceMode.
+ *
+ * @author Paul Beusterien
+ */
+public enum LeanplumDeviceIdMode {
+  /**
+   * Takes the md5 hash of the MAC address, or the ANDROID_ID on Marshmallow or later, or if the
+   * permission to access the MAC address is not set (Default).
+   */
+  MD5_MAC_ADDRESS,
+
+  /**
+   * Uses the ANDROID_ID.
+   */
+  ANDROID_ID,
+
+  /**
+   * Uses the Android Advertising ID. Requires Google Play Services v4.0 or higher. If there is an
+   * error retrieving the Advertising ID, MD5_MAC_ADDRESS will be used instead.
+   * <p>
+   * <p>You also need the following line of code in your Android manifest within your
+   * &lt;application&gt; tag:
+   * <p>
+   * <pre>&lt;meta-data android:name="com.google.android.gms.version"
+   * android:value="@integer/google_play_services_version" /&gt;</pre>
+   */
+  ADVERTISING_ID,
+}
+
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumEditorMode.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2016, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+/**
+ * Enum for describing the Editor Mode.
+ *
+ * @author Ben Marten
+ */
+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.
+   */
+  LeanplumEditorMode(final int newValue) {
+    value = newValue;
+  }
+
+  /**
+   * Returns the value of the enum entry.
+   *
+   * @return The value of the entry.
+   */
+  public int getValue() {
+    return value;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumException.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+/**
+ * Leanplum exception.
+ *
+ * @author Andrew First
+ */
+public class LeanplumException extends RuntimeException {
+  private static final long serialVersionUID = 1L;
+
+  public LeanplumException(String message) {
+    super(message);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumGcmProvider.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2016, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import android.content.Context;
+
+import com.google.android.gms.gcm.GoogleCloudMessaging;
+import com.google.android.gms.iid.InstanceID;
+import com.leanplum.internal.Constants;
+import com.leanplum.internal.LeanplumManifestHelper;
+import com.leanplum.internal.Log;
+import com.leanplum.internal.Util;
+import com.leanplum.utils.SharedPreferencesUtil;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+
+/**
+ * Leanplum provider for work with GCM.
+ *
+ * @author Anna Orlova
+ */
+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;
+
+  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);
+  }
+
+  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;
+      }
+      registrationId = instanceID.getToken(senderIds,
+          GoogleCloudMessaging.INSTANCE_ID_SCOPE, null);
+    } catch (IOException e) {
+      if (GoogleCloudMessaging.ERROR_SERVICE_NOT_AVAILABLE.equals(e.getMessage())) {
+        Log.w("GCM service is not available. Will try to " +
+            "register again next time the app starts.");
+      } else if (ERROR_TIMEOUT.equals(e.getMessage())) {
+        Log.w("Retrieval of GCM registration token timed out. " +
+            "Will try to register again next time the app starts.");
+      } else if (ERROR_INVALID_SENDER.equals(e.getMessage())) {
+        Log.e("The GCM sender account is not recognized. Please be " +
+            "sure to call LeanplumPushService.setGsmSenderId() with a valid GCM sender id.");
+      } else if (ERROR_AUTHENTICATION_FAILED.equals(e.getMessage())) {
+        Log.w("Bad Google Account password.");
+      } else if (ERROR_PHONE_REGISTRATION_ERROR.equals(e.getMessage())) {
+        Log.w("This phone doesn't currently support GCM.");
+      } else if (ERROR_TOO_MANY_REGISTRATIONS.equals(e.getMessage())) {
+        Log.w("This phone has more than the allowed number of " +
+            "apps that are registered with GCM.");
+      } else {
+        Log.e("Failed to complete registration token refresh.");
+        Util.handleException(e);
+      }
+    } catch (Throwable t) {
+      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;
+  }
+
+  public boolean isInitialized() {
+    return senderIds != null || getCurrentRegistrationId() != null;
+  }
+
+  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));
+
+    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 hasReceivers = hasGcmReceiver && hasPushReceiver;
+
+    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 && hasPushInstanceIDService
+        && hasPushRegistrationService;
+
+    return hasPermissions && hasReceivers && hasServices;
+  }
+
+  /**
+   * Unregister from GCM.
+   */
+  public void unregister() {
+    try {
+      InstanceID.getInstance(Leanplum.getContext()).deleteInstanceID();
+      Log.i("Application was unregistred from GCM.");
+    } catch (Exception e) {
+      Log.e("Failed to unregister from GCM.");
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumInbox.java
@@ -0,0 +1,405 @@
+/*
+ * 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.
+ */
+
+package com.leanplum;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import com.leanplum.callbacks.InboxChangedCallback;
+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 org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Inbox class.
+ *
+ * @author Aleksandar Gyorev, Anna Orlova
+ */
+public class LeanplumInbox {
+  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() {
+    this.unreadCount = 0;
+    this.messages = new HashMap<>();
+    this.didLoad = false;
+    this.changedCallbacks = new ArrayList<>();
+    downloadedImageUrls = new HashSet<>();
+  }
+
+  /**
+   * 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();
+  }
+
+  void update(Map<String, LeanplumInboxMessage> messages, int unreadCount, boolean shouldSave) {
+    try {
+      synchronized (updatingLock) {
+        this.unreadCount = unreadCount;
+        if (messages != null) {
+          this.messages = messages;
+        }
+      }
+      this.didLoad = true;
+      if (shouldSave) {
+        save();
+      }
+      triggerChanged();
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+  }
+
+  void removeMessage(String messageId) {
+    int unreadCount = this.unreadCount;
+    LeanplumInboxMessage message = messageForId(messageId);
+    if (message != null && !message.isRead()) {
+      unreadCount--;
+    }
+
+    messages.remove(messageId);
+    update(messages, unreadCount, true);
+
+    if (Constants.isNoop()) {
+      return;
+    }
+
+    Map<String, Object> params = new HashMap<>();
+    params.put(Constants.Params.INBOX_MESSAGE_ID, messageId);
+    Request req = Request.post(Constants.Methods.DELETE_INBOX_MESSAGE, params);
+    req.send();
+  }
+
+  void triggerChanged() {
+    synchronized (changedCallbacks) {
+      for (InboxChangedCallback callback : changedCallbacks) {
+        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) {
+      update(new HashMap<String, LeanplumInboxMessage>(), 0, false);
+      return;
+    }
+    int unreadCount = 0;
+    AESCrypt aesContext = new AESCrypt(Request.appId(), Request.token());
+    String newsfeedString = aesContext.decodePreference(
+        defaults, Constants.Defaults.INBOX_KEY, "{}");
+    Map<String, Object> newsfeed = JsonConverter.fromJson(newsfeedString);
+
+    Map<String, LeanplumInboxMessage> messages = new HashMap<>();
+    if (newsfeed == null) {
+      Log.e("Could not parse newsfeed string: " + newsfeedString);
+    } else {
+      for (Map.Entry<String, Object> entry : newsfeed.entrySet()) {
+        String messageId = entry.getKey();
+        Map<String, Object> data = CollectionUtil.uncheckedCast(entry.getValue());
+        LeanplumInboxMessage message = LeanplumInboxMessage.createFromJsonMap(messageId, data);
+
+        if (message != null && message.isActive()) {
+          messages.put(messageId, message);
+          if (!message.isRead()) {
+            unreadCount++;
+          }
+        }
+      }
+    }
+
+    update(messages, unreadCount, false);
+  }
+
+  void save() {
+    if (Constants.isNoop()) {
+      return;
+    }
+    if (Request.token() == null) {
+      return;
+    }
+    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();
+      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();
+    }
+  }
+
+  void downloadMessages() {
+    if (Constants.isNoop()) {
+      return;
+    }
+
+    Request req = Request.post(Constants.Methods.GET_INBOX_MESSAGES, null);
+    req.onResponse(new Request.ResponseCallback() {
+      @Override
+      public void response(JSONObject responses) {
+        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);
+            return;
+          }
+          int unreadCount = 0;
+          final Map<String, LeanplumInboxMessage> messages = new HashMap<>();
+          Boolean willDownladImages = false;
+
+          for (Iterator iterator = messagesDict.keys(); iterator.hasNext(); ) {
+            String messageId = (String) iterator.next();
+            JSONObject messageDict = messagesDict.getJSONObject(messageId);
+
+            Map<String, Object> actionArgs = JsonConverter.mapFromJson(
+                messageDict.getJSONObject(Constants.Keys.MESSAGE_DATA).getJSONObject(Constants.Keys.VARS)
+            );
+            Long deliveryTimestamp = messageDict.getLong(Constants.Keys.DELIVERY_TIMESTAMP);
+            Long expirationTimestamp = null;
+            if (messageDict.opt(Constants.Keys.EXPIRATION_TIMESTAMP) != null) {
+              expirationTimestamp = messageDict.getLong(Constants.Keys.EXPIRATION_TIMESTAMP);
+            }
+            boolean isRead = messageDict.getBoolean(Constants.Keys.IS_READ);
+            LeanplumInboxMessage message = LeanplumInboxMessage.constructMessage(messageId,
+                deliveryTimestamp, expirationTimestamp, isRead, actionArgs);
+            if (message != null) {
+              willDownladImages |= message.downloadImageIfPrefetchingEnabled();
+              if (!isRead) {
+                unreadCount++;
+              }
+              messages.put(messageId, message);
+            }
+          }
+
+          if (!willDownladImages) {
+            update(messages, unreadCount, true);
+            return;
+          }
+
+          final int totalUnreadCount = unreadCount;
+          Leanplum.addOnceVariablesChangedAndNoDownloadsPendingHandler(
+              new VariablesChangedCallback() {
+                @Override
+                public void variablesChanged() {
+                  update(messages, totalUnreadCount, true);
+                }
+              });
+        } catch (Throwable t) {
+          Util.handleException(t);
+        }
+      }
+    });
+    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.
+   * 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) {
+    if (messages == null) {
+      messages = new ArrayList<>();
+    }
+    try {
+      for (String messageId : messagesIds()) {
+        messages.add((T) 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.
+   */
+  private <T extends NewsfeedMessage> List<T> unreadMessages(List<T> unreadMessages) {
+    if (unreadMessages == null) {
+      unreadMessages = new ArrayList<>();
+    }
+    List<LeanplumInboxMessage> messages = allMessages(null);
+    for (LeanplumInboxMessage message : messages) {
+      if (!message.isRead()) {
+        unreadMessages.add((T) 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);
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumInboxMessage.java
@@ -0,0 +1,160 @@
+/*
+ * 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.
+ */
+
+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.Log;
+import com.leanplum.internal.Util;
+
+import org.json.JSONObject;
+
+import java.io.File;
+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 {
+  private String imageUrl;
+  private String imageFileName;
+
+  private LeanplumInboxMessage(String messageId, Long deliveryTimestamp, Long expirationTimestamp,
+      boolean isRead, ActionContext context) {
+    super(messageId, deliveryTimestamp, expirationTimestamp, isRead, 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();
+    }
+    if (!LeanplumInbox.getInstance().isInboxImagePrefetchingEnabled()) {
+      Log.w("Inbox Message image path is null because you're calling disableImagePrefetching. " +
+          "Consider using imageURL method or remove disableImagePrefetching.");
+    }
+    return null;
+  }
+
+  /**
+   * Returns the image Uri of the inbox message.
+   * You can safely use this with prefetching enabled.
+   * It will return the file Uri path instead if the image is in cache.
+   */
+  public Uri getImageUrl() {
+    String path = fileValue(imageFileName);
+    if (fileExistsAtPath(path)) {
+      return Uri.fromFile(new File(path));
+    }
+    if (TextUtils.isEmpty(imageUrl)) {
+      return null;
+    }
+
+    return Uri.parse(imageUrl);
+  }
+
+  /**
+   * 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) {
+      Log.w("Unable to parse JSONObject for Data field of inbox message.");
+    }
+    return object;
+  }
+
+  /**
+   * 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() {
+    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;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumInflater.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import android.view.InflateException;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.leanplum.internal.Log;
+import com.leanplum.internal.Util;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Inflates layout files that may be overridden by other files.
+ *
+ * @author Andrew First
+ */
+public class LeanplumInflater {
+  private Context context;
+  private LeanplumResources res;
+
+  public static LeanplumInflater from(Context context) {
+    return new LeanplumInflater(context);
+  }
+
+  private LeanplumInflater(Context context) {
+    this.context = context;
+  }
+
+  public LeanplumResources getLeanplumResources() {
+    return getLeanplumResources(null);
+  }
+
+  public LeanplumResources getLeanplumResources(Resources baseResources) {
+    if (res != null) {
+      return res;
+    }
+    if (baseResources == null) {
+      baseResources = context.getResources();
+    }
+    if (baseResources instanceof LeanplumResources) {
+      return (LeanplumResources) baseResources;
+    }
+    res = new LeanplumResources(baseResources);
+    return res;
+  }
+
+  /**
+   * Creates a view from the corresponding resource ID.
+   */
+  public View inflate(int layoutResID) {
+    return inflate(layoutResID, null, false);
+  }
+
+  /**
+   * Creates a view from the corresponding resource ID.
+   */
+  public View inflate(int layoutResID, ViewGroup root) {
+    return inflate(layoutResID, root, root != null);
+  }
+
+  /**
+   * Creates a view from the corresponding resource ID.
+   */
+  public View inflate(int layoutResID, ViewGroup root, boolean attachToRoot) {
+    Var<String> var;
+    try {
+      LeanplumResources res = getLeanplumResources(context.getResources());
+      var = res.getOverrideResource(layoutResID);
+      if (var == null || var.stringValue.equals(var.defaultValue())) {
+        return LayoutInflater.from(context).inflate(layoutResID, root, attachToRoot);
+      }
+      int overrideResId = var.overrideResId();
+      if (overrideResId != 0) {
+        return LayoutInflater.from(context).inflate(overrideResId, root, attachToRoot);
+      }
+    } catch (Throwable t) {
+      if (!(t instanceof InflateException)) {
+        Util.handleException(t);
+      }
+      return LayoutInflater.from(context).inflate(layoutResID, root, attachToRoot);
+    }
+
+    InputStream stream = null;
+
+    try {
+      ByteArrayOutputStream fileData = new ByteArrayOutputStream();
+      stream = var.stream();
+      byte[] buffer = new byte[8192];
+      int bytesRead;
+      while ((bytesRead = stream.read(buffer)) > -1) {
+        fileData.write(buffer, 0, bytesRead);
+      }
+      Object xmlBlock = Class.forName("android.content.res.XmlBlock").getConstructor(
+          byte[].class).newInstance((Object) fileData.toByteArray());
+      XmlResourceParser parser = null;
+      try {
+        parser = (XmlResourceParser) xmlBlock.getClass().getMethod(
+            "newParser").invoke(xmlBlock);
+        return LayoutInflater.from(context).inflate(parser, root, attachToRoot);
+      } catch (Throwable t) {
+        throw new RuntimeException(t);
+      } finally {
+        if (parser != null) {
+          parser.close();
+        }
+      }
+    } catch (Throwable t) {
+      Log.e("Could not inflate resource " + layoutResID + ":" + var.stringValue(), t);
+    } finally {
+      if (stream != null) {
+        try {
+          stream.close();
+        } catch (IOException e) {
+          Log.e("Failed to close input stream.");
+        }
+      }
+    }
+    return LayoutInflater.from(context).inflate(layoutResID, root, attachToRoot);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumLocalPushListenerService.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2016, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import android.app.IntentService;
+import android.content.Intent;
+import android.os.Bundle;
+
+import com.leanplum.internal.Constants;
+import com.leanplum.internal.Log;
+import com.leanplum.internal.Util;
+
+/**
+ * Listener Service for local push notifications.
+ *
+ * @author Aleksandar Gyorev
+ */
+public class LeanplumLocalPushListenerService extends IntentService {
+  public LeanplumLocalPushListenerService() {
+    super("LeanplumLocalPushListenerService");
+  }
+
+  @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)) {
+        LeanplumPushService.handleNotification(this, extras);
+      }
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumLocationAccuracyType.java
@@ -0,0 +1,54 @@
+/*
+ * 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.
+ */
+
+package com.leanplum;
+
+/**
+ * LeanplumLocationAccuracyType enum used for Leanplum.setUserLocationAttribute.
+ *
+ * @author Alexis Oyama
+ */
+public enum LeanplumLocationAccuracyType {
+  /**
+   * Lowest accuracy. Reserved for internal use.
+   */
+  IP(0),
+
+  /**
+   * Default accuracy.
+   */
+  CELL(1),
+
+  /**
+   * Highest accuracy.
+   */
+  GPS(2);
+
+  private int value;
+
+  LeanplumLocationAccuracyType(int value) {
+    this.value = value;
+  }
+
+  public int value() {
+    return value;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumManualProvider.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2016, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import android.content.Context;
+
+/**
+ * Leanplum provider for manually registering for Cloud Messaging services.
+ *
+ * @author Anna Orlova
+ */
+public class LeanplumManualProvider extends LeanplumCloudMessagingProvider {
+  LeanplumManualProvider(Context context, String registrationId) {
+    onRegistrationIdReceived(context, registrationId);
+  }
+
+  public String getRegistrationId() {
+    return getCurrentRegistrationId();
+  }
+
+  public boolean isInitialized() {
+    return true;
+  }
+
+  public boolean isManifestSetUp() {
+    return true;
+  }
+
+  public void unregister() {
+
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumPushInstanceIDService.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2016, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import android.content.Intent;
+
+import com.google.android.gms.iid.InstanceIDListenerService;
+import com.leanplum.internal.Log;
+
+/**
+ * GCM InstanceID listener service to handle creation, rotation, and updating of registration
+ * tokens.
+ *
+ * @author Aleksandar Gyorev
+ */
+public class LeanplumPushInstanceIDService extends InstanceIDListenerService {
+  /**
+   * Called if InstanceID token is updated. This may occur if the security of the previous token had
+   * been compromised. This call is initiated by the InstanceID provider.
+   */
+  @Override
+  public void onTokenRefresh() {
+    Log.i("GCM InstanceID token needs an update");
+    // Fetch updated Instance ID token and notify our app's server of any changes (if applicable).
+    Intent intent = new Intent(this, LeanplumPushRegistrationService.class);
+    startService(intent);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumPushListenerService.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2016, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import android.os.Bundle;
+
+import com.google.android.gms.gcm.GcmListenerService;
+import com.leanplum.internal.Constants.Keys;
+import com.leanplum.internal.Log;
+import com.leanplum.internal.Util;
+
+/**
+ * GCM listener service, which enables handling messages on the app's behalf.
+ *
+ * @author Aleksandar Gyorev
+ */
+public class LeanplumPushListenerService extends GcmListenerService {
+  /**
+   * Called when a message is received.
+   *
+   * @param senderId Sender ID of the sender.
+   * @param data Data bundle containing the message data as key-value pairs.
+   */
+  @Override
+  public void onMessageReceived(String senderId, Bundle data) {
+    try {
+      if (data.containsKey(Keys.PUSH_MESSAGE_TEXT)) {
+        LeanplumPushService.handleNotification(this, data);
+      }
+      Log.i("Received: " + data.toString());
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumPushNotificationCustomizer.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2015, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import android.os.Bundle;
+import android.support.v4.app.NotificationCompat;
+
+/**
+ * Implement LeanplumPushNotificationCustomizer to customize the appearance of notifications.
+ */
+public interface LeanplumPushNotificationCustomizer {
+  void customize(NotificationCompat.Builder builder, Bundle notificationPayload);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumPushReceiver.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2016, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+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
+ */
+public class LeanplumPushReceiver extends BroadcastReceiver {
+
+  @Override
+  public void onReceive(Context context, Intent intent) {
+    try {
+      if (intent == null) {
+        Log.e("Received a null intent.");
+        return;
+      }
+      LeanplumPushService.openNotification(context, intent.getExtras());
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumPushRegistrationService.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2016, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import android.app.IntentService;
+import android.content.Intent;
+
+import com.leanplum.internal.Log;
+
+/**
+ * Registration service that handles registration with the GCM and FCM, using
+ * InstanceID.
+ *
+ * @author Aleksandar Gyorev
+ */
+public class LeanplumPushRegistrationService extends IntentService {
+  private static String existingRegistrationId;
+
+  public LeanplumPushRegistrationService() {
+    super("LeanplumPushRegistrationService");
+  }
+
+  @Override
+  protected void onHandleIntent(Intent intent) {
+    LeanplumCloudMessagingProvider provider = LeanplumPushService.getCloudMessagingProvider();
+    if (provider == null) {
+      Log.e("Failed to complete registration token refresh.");
+      return;
+    }
+    String registrationId = provider.getRegistrationId();
+    if (registrationId != null) {
+      if (existingRegistrationId != null && !registrationId.equals(existingRegistrationId)) {
+        Log.e("WARNING: It appears your app is registering " +
+            "with GCM/FCM using multiple GCM/FCM sender ids. Please be sure to call " +
+            "LeanplumPushService.setGcmSenderIds() with " +
+            "all of the GCM sender ids that you use, not just the one that you use with " +
+            "Leanplum. Otherwise, GCM/FCM push notifications may not work consistently.");
+      }
+      existingRegistrationId = registrationId;
+      provider.onRegistrationIdReceived(getApplicationContext(), registrationId);
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumPushService.java
@@ -0,0 +1,776 @@
+/*
+ * Copyright 2014, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import android.app.Activity;
+import android.app.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;
+import android.support.v4.app.NotificationCompat;
+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.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.SharedPreferencesUtil;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+
+/**
+ * Leanplum push notification service class, handling initialization, opening, showing, integration
+ * verification and registration for push notifications.
+ *
+ * @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";
+  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 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;
+  }
+
+  /**
+   * Changes the default activity to launch if the user opens a push notification.
+   *
+   * @param callbackClass The activity class.
+   */
+  public static void setDefaultCallbackClass(Class<? extends Activity> callbackClass) {
+    LeanplumPushService.callbackClass = callbackClass;
+  }
+
+  /**
+   * Sets an object used to customize the appearance of notifications. <p>Call this from your
+   * Application class's onCreate method so that the customizer is set when your application starts
+   * in the background.
+   */
+  public static void setCustomizer(LeanplumPushNotificationCustomizer customizer) {
+    LeanplumPushService.customizer = customizer;
+  }
+
+  /**
+   * Sets the Google Cloud Messaging/Firebase Cloud Messaging sender ID. Required for push
+   * notifications to work.
+   *
+   * @param senderId The GCM/FCM sender ID to permit notifications from. Use {@link
+   * LeanplumPushService#LEANPLUM_SENDER_ID} to use the built-in sender ID for GCM. If you have
+   * multiple sender IDs, use {@link LeanplumPushService#setGcmSenderIds}.
+   */
+  public static void setGcmSenderId(String senderId) {
+    LeanplumGcmProvider.setSenderId(senderId);
+  }
+
+  /**
+   * Sets the Google Cloud Messaging/Firebase Cloud Messaging sender ID. Required for push
+   * notifications to work.
+   *
+   * @param senderIds The GCM/FCM sender IDs to permit notifications from. Use {@link
+   * LeanplumPushService#LEANPLUM_SENDER_ID} to use the built-in sender ID.
+   */
+  public static void setGcmSenderIds(String... senderIds) {
+    StringBuilder joinedSenderIds = new StringBuilder();
+    for (String senderId : senderIds) {
+      if (joinedSenderIds.length() > 0) {
+        joinedSenderIds.append(',');
+      }
+      joinedSenderIds.append(senderId);
+    }
+    LeanplumGcmProvider.setSenderId(joinedSenderIds.toString());
+  }
+
+  private static Class<? extends Activity> getCallbackClass() {
+    return callbackClass;
+  }
+
+  private static boolean areActionsEmbedded(final Bundle message) {
+    return message.containsKey(Keys.PUSH_MESSAGE_ACTION);
+  }
+
+  private static void requireMessageContent(
+      final String messageId, final VariablesChangedCallback onComplete) {
+    Leanplum.addOnceVariablesChangedAndNoDownloadsPendingHandler(new VariablesChangedCallback() {
+      @Override
+      public void variablesChanged() {
+        try {
+          Map<String, Object> messages = VarCache.messages();
+          if (messageId == null || (messages != null && messages.containsKey(messageId))) {
+            onComplete.variablesChanged();
+          } else {
+            // Try downloading the messages again if it doesn't exist.
+            // Maybe the message was created while the app was running.
+            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) {
+                    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));
+                    Map<String, Object> messages = JsonConverter.mapFromJson(
+                        getVariablesResponse.optJSONObject(Constants.Keys.MESSAGES));
+                    Map<String, Object> regions = JsonConverter.mapFromJson(
+                        getVariablesResponse.optJSONObject(Constants.Keys.REGIONS));
+                    List<Map<String, Object>> variants = JsonConverter.listFromJson(
+                        getVariablesResponse.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) {
+                      VarCache.applyVariableDiffs(values, messages, null, null, regions, variants);
+                    }
+                  }
+                  onComplete.variablesChanged();
+                } catch (Throwable t) {
+                  Util.handleException(t);
+                }
+              }
+            });
+            req.onError(new Request.ErrorCallback() {
+              @Override
+              public void error(Exception e) {
+                onComplete.variablesChanged();
+              }
+            });
+            req.sendIfConnected();
+          }
+        } catch (Throwable t) {
+          Util.handleException(t);
+        }
+      }
+    });
+  }
+
+  private static String getMessageId(Bundle message) {
+    String messageId = message.getString(Keys.PUSH_MESSAGE_ID_NO_MUTE_WITH_ACTION);
+    if (messageId == null) {
+      messageId = message.getString(Keys.PUSH_MESSAGE_ID_MUTE_WITH_ACTION);
+      if (messageId == null) {
+        messageId = message.getString(Keys.PUSH_MESSAGE_ID_NO_MUTE);
+        if (messageId == null) {
+          messageId = message.getString(Keys.PUSH_MESSAGE_ID_MUTE);
+        }
+      }
+    }
+    if (messageId != null) {
+      message.putString(Keys.PUSH_MESSAGE_ID, messageId);
+    }
+    return messageId;
+  }
+
+  static void handleNotification(final Context context, final Bundle message) {
+    if (LeanplumActivityHelper.currentActivity != null
+        && !LeanplumActivityHelper.isActivityPaused
+        && (message.containsKey(Keys.PUSH_MESSAGE_ID_MUTE_WITH_ACTION)
+        || message.containsKey(Keys.PUSH_MESSAGE_ID_MUTE))) {
+      // Mute notifications that have "Mute inside app" set if the app is open.
+      return;
+    }
+
+    final String messageId = LeanplumPushService.getMessageId(message);
+    if (messageId == null || !LeanplumInternal.hasCalledStart()) {
+      showNotification(context, message);
+      return;
+    }
+
+    // Can only track displays if we call Leanplum.start explicitly above where it says
+    // if (!Leanplum.calledStart). However, this is probably not worth it.
+    //
+    // Map<String, String> requestArgs = new HashMap<String, String>();
+    // requestArgs.put(Constants.Params.MESSAGE_ID, getMessageId);
+    // 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)
+        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)
+        .setStyle(new NotificationCompat.BigTextStyle()
+            .bigText(message.getString(Keys.PUSH_MESSAGE_TEXT)))
+        .setContentText(message.getString(Keys.PUSH_MESSAGE_TEXT));
+
+    String imageUrl = message.getString(Keys.PUSH_MESSAGE_IMAGE_URL);
+    // 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)));
+      } 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);
+    }
+    builder.setAutoCancel(true);
+    builder.setContentIntent(contentIntent);
+
+    if (LeanplumPushService.customizer != null) {
+      LeanplumPushService.customizer.customize(builder, message);
+    }
+
+    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 {
+        notificationId = Integer.parseInt((String) notificationIdObject);
+      } catch (NumberFormatException e) {
+        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());
+  }
+
+  static void openNotification(Context context, final Bundle notification) {
+    Log.d("Opening push notification action.");
+    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) {
+      if (callbackClass == null) {
+        shouldStartActivity = false;
+      } else if (callbackClass.isInstance(LeanplumActivityHelper.currentActivity)) {
+        shouldStartActivity = false;
+      }
+    }
+
+    if (shouldStartActivity) {
+      Intent actionIntent = getActionIntent(context);
+      actionIntent.putExtras(notification);
+      actionIntent.addFlags(
+          Intent.FLAG_ACTIVITY_CLEAR_TOP |
+              Intent.FLAG_ACTIVITY_NEW_TASK);
+      context.startActivity(actionIntent);
+    }
+
+    // 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);
+              context.preventRealtimeUpdating();
+              context.update();
+              context.runTrackedActionNamed(actionName);
+            } else {
+              Leanplum.addOnceVariablesChangedAndNoDownloadsPendingHandler(
+                  new VariablesChangedCallback() {
+                    @Override
+                    public void variablesChanged() {
+                      try {
+                        LeanplumPushService.requireMessageContent(messageId,
+                            new VariablesChangedCallback() {
+                              @Override
+                              public void variablesChanged() {
+                                try {
+                                  LeanplumInternal.performTrackedAction(actionName, messageId);
+                                } catch (Throwable t) {
+                                  Util.handleException(t);
+                                }
+                              }
+                            });
+                      } catch (Throwable t) {
+                        Util.handleException(t);
+                      }
+                    }
+                  });
+            }
+          }
+        } catch (Throwable t) {
+          Util.handleException(t);
+        }
+      }
+    });
+  }
+
+  /**
+   * Return true if we found an activity to handle Intent and started it.
+   */
+  private static boolean isActivityWithIntentStarted(Context context, Bundle notification) {
+    String action = notification.getString(Keys.PUSH_MESSAGE_ACTION);
+    if (action != null && action.contains(OPEN_URL)) {
+      Intent deepLinkIntent = getDeepLinkIntent(notification);
+      if (deepLinkIntent != null && activityHasIntent(context, deepLinkIntent)) {
+        String messageId = LeanplumPushService.getMessageId(notification);
+        if (messageId != null) {
+          ActionContext actionContext = new ActionContext(
+              ActionManager.PUSH_NOTIFICATION_ACTION_NAME, null, messageId);
+          actionContext.track(OPEN_ACTION, 0.0, null);
+          context.startActivity(deepLinkIntent);
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Gets Intent from Push Notification Bundle.
+   */
+  private static Intent getDeepLinkIntent(Bundle notification) {
+    try {
+      String actionString = notification.getString(Keys.PUSH_MESSAGE_ACTION);
+      if (actionString != null) {
+        JSONObject openAction = new JSONObject(actionString);
+        Intent deepLinkIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(
+            openAction.getString(URL)));
+        deepLinkIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        return deepLinkIntent;
+      }
+    } catch (JSONException ignored) {
+    }
+    return null;
+  }
+
+  /**
+   * Checks if there is some activity that can handle intent.
+   */
+  private static Boolean activityHasIntent(Context context, Intent deepLinkIntent) {
+    List<ResolveInfo> resolveInfoList =
+        context.getPackageManager().queryIntentActivities(deepLinkIntent, 0);
+    if (resolveInfoList != null && !resolveInfoList.isEmpty()) {
+      for (ResolveInfo resolveInfo : resolveInfoList) {
+        if (resolveInfo != null && resolveInfo.activityInfo != null &&
+            resolveInfo.activityInfo.name != null) {
+          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;
+          }
+        }
+      }
+    }
+    return false;
+  }
+
+  private static Intent getActionIntent(Context context) {
+    Class<? extends Activity> callbackClass = LeanplumPushService.getCallbackClass();
+    if (callbackClass != null) {
+      return new Intent(context, callbackClass);
+    } else {
+      PackageManager pm = context.getPackageManager();
+      return pm.getLaunchIntentForPackage(context.getPackageName());
+    }
+  }
+
+  /**
+   * Unregisters the device from all GCM push notifications. You shouldn't need to call this method
+   * in production.
+   */
+  public static void unregister() {
+    try {
+      Intent unregisterIntent = new Intent("com.google.android.c2dm.intent.UNREGISTER");
+      Context context = Leanplum.getContext();
+      unregisterIntent.putExtra("app", PendingIntent.getBroadcast(context, 0, new Intent(), 0));
+      unregisterIntent.setPackage("com.google.android.gms");
+      context.startService(unregisterIntent);
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+  }
+
+  /**
+   * Registers the application with GCM servers asynchronously.
+   * <p>
+   * Stores the registration ID and app versionCode in the application's shared preferences.
+   */
+  private static void registerInBackground() {
+    Context context = Leanplum.getContext();
+    if (context == null) {
+      Log.e("Failed to register application with GCM/FCM. Your application context is not set.");
+      return;
+    }
+    Intent registerIntent = new Intent(context, LeanplumPushRegistrationService.class);
+    context.startService(registerIntent);
+  }
+
+  /**
+   * Register manually for Google Cloud Messaging services.
+   *
+   * @param token The registration ID token or the instance ID security token.
+   */
+  public static void setGcmRegistrationId(String token) {
+    new LeanplumManualProvider(Leanplum.getContext().getApplicationContext(), token);
+  }
+
+  /**
+   * Call this when Leanplum starts.
+   */
+  static void onStart() {
+    try {
+      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));
+    }
+  }
+
+  private static void initPushService() {
+    if (!enableServices()) {
+      return;
+    }
+    provider = new LeanplumGcmProvider();
+    if (!provider.isInitialized() || !provider.isManifestSetUp()) {
+      return;
+    }
+    if (hasAppIDChanged(Request.appId())) {
+      provider.unregister();
+    }
+    registerInBackground();
+  }
+
+
+  /**
+   * Enable Leanplum GCM or FCM services.
+   *
+   * @return True if services was enabled.
+   */
+  private static boolean enableServices() {
+    Context context = Leanplum.getContext();
+    if (context == null) {
+      return false;
+    }
+
+    PackageManager packageManager = context.getPackageManager();
+    if (packageManager == null) {
+      return false;
+    }
+
+    if (isFirebaseEnabled) {
+      Class fcmListenerClass = getClassForName(LEANPLUM_PUSH_FCM_LISTENER_SERVICE_CLASS);
+      if (fcmListenerClass == null) {
+        return false;
+      }
+
+      if (!wasComponentEnabled(context, packageManager, fcmListenerClass)) {
+        if (!enableServiceAndStart(context, packageManager, PUSH_FIREBASE_MESSAGING_SERVICE_CLASS)
+            || !enableServiceAndStart(context, packageManager, fcmListenerClass)) {
+          return false;
+        }
+      }
+    } else {
+      Class gcmPushInstanceIDClass = getClassForName(LEANPLUM_PUSH_INSTANCE_ID_SERVICE_CLASS);
+      if (gcmPushInstanceIDClass == null) {
+        return false;
+      }
+
+      if (!wasComponentEnabled(context, packageManager, gcmPushInstanceIDClass)) {
+        if (!enableComponent(context, packageManager, LEANPLUM_PUSH_LISTENER_SERVICE_CLASS) ||
+            !enableComponent(context, packageManager, gcmPushInstanceIDClass) ||
+            !enableComponent(context, packageManager, GCM_RECEIVER_CLASS)) {
+          return false;
+        }
+
+      }
+    }
+    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.
+   */
+  private static boolean enableServiceAndStart(Context context, PackageManager packageManager,
+      String className) {
+    Class clazz;
+    try {
+      clazz = Class.forName(className);
+    } catch (Throwable t) {
+      return false;
+    }
+    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;
+    }
+    try {
+      context.startService(new Intent(context, clazz));
+    } catch (Throwable t) {
+      Log.w("Could not start service " + clazz.getName());
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * 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.
+   */
+  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;
+    }
+
+    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 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;
+  }
+
+  /**
+   * 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.
+   */
+  private static boolean hasAppIDChanged(String currentAppId) {
+    if (currentAppId == null) {
+      return false;
+    }
+
+    Context context = Leanplum.getContext();
+    if (context == null) {
+      return false;
+    }
+
+    String storedAppId = SharedPreferencesUtil.getString(context, Constants.Defaults.LEANPLUM_PUSH,
+        Constants.Defaults.APP_ID);
+    if (!currentAppId.equals(storedAppId)) {
+      Log.v("Saving the application id in the shared preferences.");
+      SharedPreferencesUtil.setString(context, Constants.Defaults.LEANPLUM_PUSH,
+          Constants.Defaults.APP_ID, currentAppId);
+      // Check application id was stored before.
+      if (!SharedPreferencesUtil.DEFAULT_STRING_VALUE.equals(storedAppId)) {
+        return true;
+      }
+    }
+    return false;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumResources.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.util.DisplayMetrics;
+
+import com.leanplum.internal.CollectionUtil;
+import com.leanplum.internal.Constants;
+import com.leanplum.internal.FileManager;
+import com.leanplum.internal.Log;
+import com.leanplum.internal.ResourceQualifiers;
+import com.leanplum.internal.ResourceQualifiers.Qualifier;
+import com.leanplum.internal.Util;
+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/
+
+public class LeanplumResources extends Resources {
+  public LeanplumResources(Resources base) {
+    super(base.getAssets(), base.getDisplayMetrics(), base.getConfiguration());
+  }
+
+  /* internal */
+  <T> Var<T> getOverrideResource(int id) {
+    try {
+      String name = getResourceEntryName(id);
+      String type = getResourceTypeName(id);
+      if (FileManager.resources == null) {
+        return null;
+      }
+      HashMap<String, Object> resourceValues = CollectionUtil.uncheckedCast(FileManager.resources
+          .objectForKeyPath());
+      Map<String, String> eligibleFolders = new HashMap<>();
+      synchronized (VarCache.valuesFromClient) {
+        for (String folder : resourceValues.keySet()) {
+          if (!folder.toLowerCase().startsWith(type)) {
+            continue;
+          }
+          HashMap<String, Object> files = CollectionUtil.uncheckedCast(resourceValues.get(folder));
+          String eligibleFile = null;
+          for (String filename : files.keySet()) {
+            String currentName = filename.replace("\\.", ".");
+            // Get filename without extension.
+            int dotPos = currentName.lastIndexOf('.');
+            if (dotPos >= 0) {
+              currentName = currentName.substring(0, dotPos);
+            }
+
+            if (currentName.equals(name)) {
+              eligibleFile = filename;
+            }
+          }
+          if (eligibleFile == null) {
+            continue;
+          }
+          eligibleFolders.put(folder, eligibleFile);
+        }
+      }
+
+      Map<String, ResourceQualifiers> folderQualifiers = new HashMap<>();
+      for (String folder : eligibleFolders.keySet()) {
+        folderQualifiers.put(folder, ResourceQualifiers.fromFolder(folder));
+      }
+
+      // 1. Eliminate qualifiers that contradict the device configuration.
+      // See http://developer.android.com/guide/topics/resources/providing-resources.html
+      Configuration config = getConfiguration();
+      DisplayMetrics display = getDisplayMetrics();
+      Set<String> matchedFolders = new HashSet<>();
+      for (String folder : eligibleFolders.keySet()) {
+        ResourceQualifiers qualifiers = folderQualifiers.get(folder);
+        for (Qualifier qualifier : qualifiers.qualifiers.keySet()) {
+          if (qualifier.getFilter().isMatch(
+              qualifiers.qualifiers.get(qualifier), config, display)) {
+            matchedFolders.add(folder);
+          }
+        }
+      }
+
+      // 2. Identify the next qualifier in the table (MCC first, then MNC,
+      // then language, and so on.
+      for (Qualifier qualifier : ResourceQualifiers.Qualifier.values()) {
+        Map<String, Object> betterMatchedFolders = new HashMap<>();
+        for (String folder : matchedFolders) {
+          ResourceQualifiers folderQualifier = folderQualifiers.get(folder);
+          Object qualifierValue = folderQualifier.qualifiers.get(qualifier);
+          if (qualifierValue != null) {
+            betterMatchedFolders.put(folder, qualifierValue);
+          }
+        }
+        betterMatchedFolders = qualifier.getFilter().bestMatch(
+            betterMatchedFolders, config, display);
+
+        // 3. Do any resource directories use this qualifier?
+        if (!betterMatchedFolders.isEmpty()) {
+          // Yes.
+          // 4. Eliminate directories that do not include this qualifier.
+          matchedFolders = betterMatchedFolders.keySet();
+        }
+      }
+
+      // Return result.
+      if (!eligibleFolders.isEmpty()) {
+        String folder = eligibleFolders.entrySet().iterator().next().getValue();
+        String varName = Constants.Values.RESOURCES_VARIABLE + "." + folder
+            + "." + eligibleFolders.get(folder);
+        return VarCache.getVariable(varName);
+      }
+    } catch (Exception e) {
+      Log.e("Error getting resource", e);
+    }
+    return null;
+  }
+
+  @Override
+  public Drawable getDrawable(int id) throws NotFoundException {
+    try {
+      Var<String> override = getOverrideResource(id);
+      if (override != null) {
+        int overrideResId = override.overrideResId();
+        if (overrideResId != 0) {
+          return super.getDrawable(overrideResId);
+        }
+        if (!override.stringValue.equals(override.defaultValue())) {
+          Drawable result = Drawable.createFromStream(override.stream(), override.fileValue());
+          if (result != null) {
+            return result;
+          }
+        }
+      }
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+    return super.getDrawable(id);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumUIEditor.java
@@ -0,0 +1,80 @@
+/*
+ * 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.
+ */
+
+package com.leanplum;
+
+import android.app.Activity;
+
+/**
+ * Describes the API of the visual editor package.
+ */
+public interface LeanplumUIEditor {
+  /**
+   * Enable interface editing via Leanplum.com Visual Editor.
+   */
+  void allowInterfaceEditing(Boolean isDevelopmentModeEnabled);
+
+  /**
+   * Enables Interface editing for the desired activity.
+   *
+   * @param activity The activity to enable interface editing for.
+   */
+  void applyInterfaceEdits(Activity activity);
+
+  /**
+   * Sets the update flag to true.
+   */
+  void startUpdating();
+
+  /**
+   * Sets the update flag to false.
+   */
+  void stopUpdating();
+
+  /**
+   * Send an immediate update of the UI to the LP server.
+   */
+  void sendUpdate();
+
+  /**
+   * Send an update with given delay of the UI to the LP server.
+   */
+  void sendUpdateDelayed(int delay);
+
+  /**
+   * Send an update of the UI to the LP server, delayed by the default time.
+   */
+  void sendUpdateDelayedDefault();
+
+  /**
+   * Returns the current editor mode.
+   *
+   * @return The current editor mode.
+   */
+  LeanplumEditorMode getMode();
+
+  /**
+   * Sets the current editor mode.
+   *
+   * @param mode The editor mode to set.
+   */
+  void setMode(LeanplumEditorMode mode);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LocationManager.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2014, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Public interface to LocationManager. This is abstracted away so that the Google Play Services
+ * dependencies are constrained to {@link LocationManagerImplementation}.
+ *
+ * @author Andrew First
+ */
+public interface LocationManager {
+  void updateGeofencing();
+
+  void updateUserLocation();
+
+  void setRegionsData(Map<String, Object> regionData,
+      Set<String> foregroundRegionNames, Set<String> backgroundRegionNames);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/Newsfeed.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2015, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import com.leanplum.callbacks.InboxChangedCallback;
+import com.leanplum.callbacks.NewsfeedChangedCallback;
+
+/**
+ * Newsfeed class.
+ *
+ * @author Aleksandar Gyorev
+ */
+public class Newsfeed extends LeanplumInbox {
+
+  /**
+   * A private constructor, which prevents any other class from instantiating.
+   */
+  Newsfeed() {
+  }
+
+  /**
+   * Static 'getInstance' method.
+   */
+  static Newsfeed getInstance() {
+    return instance;
+  }
+
+  /**
+   * Add a callback for when the newsfeed receives new values from the server.
+   *
+   * @deprecated use {@link #addChangedHandler(InboxChangedCallback)} instead
+   */
+  @Deprecated
+  public void addNewsfeedChangedHandler(NewsfeedChangedCallback handler) {
+    super.addChangedHandler(handler);
+  }
+
+  /**
+   * Removes a newsfeed changed callback.
+   *
+   * @deprecated use {@link #removeChangedHandler(InboxChangedCallback)} instead
+   */
+  @Deprecated
+  public void removeNewsfeedChangedHandler(NewsfeedChangedCallback handler) {
+    super.removeChangedHandler(handler);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/NewsfeedMessage.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright 2015, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import com.leanplum.internal.Constants;
+import com.leanplum.internal.Request;
+import com.leanplum.internal.Util;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * NewsfeedMessage class.
+ *
+ * @author Aleksandar Gyorev
+ */
+public abstract class NewsfeedMessage {
+  private String messageId;
+  private Long deliveryTimestamp;
+  private Long expirationTimestamp;
+  private boolean isRead;
+  private ActionContext context;
+
+  NewsfeedMessage(String messageId, Long deliveryTimestamp, Long expirationTimestamp,
+      boolean isRead, ActionContext context) {
+    this.messageId = messageId;
+    this.deliveryTimestamp = deliveryTimestamp;
+    this.expirationTimestamp = expirationTimestamp;
+    this.isRead = isRead;
+    this.context = context;
+  }
+
+  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;
+  }
+
+  Map<String, Object> actionArgs() {
+    return context.getArgs();
+  }
+
+  void setIsRead(boolean isRead) {
+    this.isRead = isRead;
+  }
+
+  boolean isActive() {
+    if (expirationTimestamp == null) {
+      return true;
+    }
+
+    Date now = new Date();
+    return now.before(new Date(expirationTimestamp));
+  }
+
+  static boolean isValidMessageId(String messageId) {
+    return messageId.split("##").length == 2;
+  }
+
+  ActionContext getContext() {
+    return context;
+  }
+
+  /**
+   * Returns the message identifier of the newsfeed message.
+   *
+   * @deprecated As of release 1.3.0, replaced by {@link #getMessageId()}
+   */
+  @Deprecated
+  public String messageId() {
+    return getMessageId();
+  }
+
+  /**
+   * Returns the message identifier of the newsfeed message.
+   */
+  public String getMessageId() {
+    return messageId;
+  }
+
+  /**
+   * Returns the title of the newsfeed message.
+   *
+   * @deprecated As of release 1.3.0, replaced by {@link #getTitle()}
+   */
+  @Deprecated
+  public String title() {
+    return getTitle();
+  }
+
+  /**
+   * Returns the title of the newsfeed message.
+   */
+  public String getTitle() {
+    return context.stringNamed(Constants.Keys.TITLE);
+  }
+
+  /**
+   * Returns the subtitle of the newsfeed message.
+   *
+   * @deprecated As of release 1.3.0, replaced by {@link #getSubtitle()}
+   */
+  @Deprecated
+  public String subtitle() {
+    return getSubtitle();
+  }
+
+  /**
+   * Returns the subtitle of the newsfeed message.
+   */
+  public String getSubtitle() {
+    return context.stringNamed(Constants.Keys.SUBTITLE);
+  }
+
+  /**
+   * Returns the delivery timestamp of the newsfeed message.
+   *
+   * @deprecated As of release 1.3.0, replaced by {@link #getDeliveryTimestamp()}
+   */
+  @Deprecated
+  public Date deliveryTimestamp() {
+    return getDeliveryTimestamp();
+  }
+
+  /**
+   * Returns the delivery timestamp of the newsfeed message.
+   */
+  public Date getDeliveryTimestamp() {
+    return new Date(deliveryTimestamp);
+  }
+
+  /**
+   * Return the expiration timestamp of the newsfeed message.
+   *
+   * @deprecated As of release 1.3.0, replaced by {@link #getExpirationTimestamp()}
+   */
+  @Deprecated
+  public Date expirationTimestamp() {
+    return getExpirationTimestamp();
+  }
+
+  /**
+   * Return the expiration timestamp of the newsfeed message.
+   */
+  public Date getExpirationTimestamp() {
+    if (expirationTimestamp == null) {
+      return null;
+    }
+    return new Date(expirationTimestamp);
+  }
+
+  /**
+   * Returns 'true' if the newsfeed message is read.
+   */
+  public boolean isRead() {
+    return isRead;
+  }
+
+  /**
+   * Read the newsfeed 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 = Newsfeed.getInstance().unreadCount() - 1;
+        Newsfeed.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 newsfeed message from the newsfeed.
+   */
+  public void remove() {
+    try {
+      Newsfeed.getInstance().removeMessage(messageId);
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/UIEditorBridge.java
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+
+package com.leanplum;
+
+import com.leanplum.internal.FileManager;
+import com.leanplum.internal.Socket;
+import com.leanplum.internal.Util;
+import com.leanplum.internal.VarCache;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Bridge class for the UI editor package to access LP internal methods.
+ *
+ * @author Ben Marten
+ */
+public class UIEditorBridge {
+  public static void setInterfaceUpdateBlock(CacheUpdateBlock block) {
+    VarCache.onInterfaceUpdate(block);
+  }
+
+  public static void setEventsUpdateBlock(CacheUpdateBlock block) {
+    VarCache.onEventsUpdate(block);
+  }
+
+  public static List<Map<String, Object>> getUpdateRuleDiffs() {
+    return VarCache.getUpdateRuleDiffs();
+  }
+
+  public static List<Map<String, Object>> getEventRuleDiffs() {
+    return VarCache.getEventRuleDiffs();
+  }
+
+  public static boolean isSocketConnected() {
+    return Socket.getInstance() != null && Socket.getInstance().isConnected();
+  }
+
+  public static <T> void socketSendEvent(String eventName, Map<String, T> data) {
+    if (Socket.getInstance() != null && eventName != null) {
+      Socket.getInstance().sendEvent(eventName, data);
+    }
+  }
+
+  public static String fileRelativeToDocuments(String path) {
+    return FileManager.fileRelativeToDocuments(path);
+  }
+
+  public static void handleException(Throwable t) {
+    Util.handleException(t);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/Var.java
@@ -0,0 +1,622 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import android.text.TextUtils;
+
+import com.leanplum.callbacks.VariableCallback;
+import com.leanplum.internal.Constants;
+import com.leanplum.internal.FileManager;
+import com.leanplum.internal.FileManager.DownloadFileResult;
+import com.leanplum.internal.LeanplumInternal;
+import com.leanplum.internal.Log;
+import com.leanplum.internal.OsHandler;
+import com.leanplum.internal.Util;
+import com.leanplum.internal.VarCache;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Leanplum variable.
+ *
+ * @param <T> Type of the variable. Can be Boolean, Byte, Short, Integer, Long, Float, Double,
+ * Character, String, List, or Map. You may nest lists and maps arbitrarily.
+ * @author Andrew First
+ */
+public class Var<T> {
+  private String name;
+  private String[] nameComponents;
+  public String stringValue;
+  private Double numberValue;
+  private T defaultValue;
+  private T value;
+  private String kind;
+  private final List<VariableCallback<T>> fileReadyHandlers = new ArrayList<>();
+  private final List<VariableCallback<T>> valueChangedHandlers = new ArrayList<>();
+  private boolean fileIsPending;
+  private boolean hadStarted;
+  private boolean isAsset;
+  public boolean isResource;
+  private int size;
+  private String hash;
+  private byte[] data;
+  private boolean valueIsInAssets = false;
+  private boolean isInternal;
+  private int overrideResId;
+  private static boolean printedCallbackWarning;
+
+  private void warnIfNotStarted() {
+    if (!isInternal && !Leanplum.hasStarted() && !printedCallbackWarning) {
+      Log.w("Leanplum hasn't finished retrieving values from the server. "
+          + "You should use a callback to make sure the value for '" + name +
+          "' is ready. Otherwise, your app may not use the most up-to-date value.");
+      printedCallbackWarning = true;
+    }
+  }
+
+  private interface VarInitializer<T> {
+    void init(Var<T> var);
+  }
+
+  private static <T> Var<T> define(
+      String name, T defaultValue, String kind, VarInitializer<T> initializer) {
+    if (TextUtils.isEmpty(name)) {
+      Log.e("Empty name parameter provided.");
+      return null;
+    }
+    Var<T> existing = VarCache.getVariable(name);
+    if (existing != null) {
+      return existing;
+    }
+    if (LeanplumInternal.hasCalledStart() &&
+        !name.startsWith(Constants.Values.RESOURCES_VARIABLE)) {
+      Log.w("You should not create new variables after calling start (name=" + name + ")");
+    }
+    Var<T> var = new Var<>();
+    try {
+      var.name = name;
+      var.nameComponents = VarCache.getNameComponents(name);
+      var.defaultValue = defaultValue;
+      var.value = defaultValue;
+      var.kind = kind;
+      if (name.startsWith(Constants.Values.RESOURCES_VARIABLE)) {
+        var.isInternal = true;
+      }
+      if (initializer != null) {
+        initializer.init(var);
+      }
+      var.cacheComputedValues();
+      VarCache.registerVariable(var);
+      if (Constants.Kinds.FILE.equals(var.kind)) {
+        VarCache.registerFile(var.stringValue,
+            var.defaultValue() == null ? null : var.defaultValue().toString(),
+            var.defaultStream(), var.isResource, var.hash, var.size);
+      }
+      var.update();
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+    return var;
+  }
+
+  /**
+   * Defines a new variable with a default value.
+   *
+   * @param name Name of the variable.
+   * @param defaultValue Default value of the variable. Can't be null.
+   */
+  public static <T> Var<T> define(String name, T defaultValue) {
+    return define(name, defaultValue, VarCache.kindFromValue(defaultValue), null);
+  }
+
+  /**
+   * Defines a variable with kind. Can be Boolean, Byte, Short, Integer, Long, Float, Double,
+   * Character, String, List, or Map. You may nest lists and maps arbitrarily.
+   *
+   * @param name Name of the variable.
+   * @param defaultValue Default value.
+   * @param kind Kind of the variable.
+   * @param <T> Boolean, Byte, Short, Integer, Long, Float, Double, Character, String, List, or
+   * Map.
+   * @return Initialized variable.
+   */
+  public static <T> Var<T> define(String name, T defaultValue, String kind) {
+    return define(name, defaultValue, kind, null);
+  }
+
+  /**
+   * Defines a color.
+   *
+   * @param name Name of the variable
+   * @param defaultValue Default value.
+   * @return Initialized variable.
+   */
+  @SuppressWarnings("WeakerAccess")
+  public static Var<Integer> defineColor(String name, int defaultValue) {
+    return define(name, defaultValue, Constants.Kinds.COLOR, null);
+  }
+
+  /**
+   * Defines a variable for a file.
+   *
+   * @param name Name of the variable.
+   * @param defaultFilename Default filename.
+   * @return Initialized variable.
+   */
+  public static Var<String> defineFile(String name, String defaultFilename) {
+    return define(name, defaultFilename, Constants.Kinds.FILE, null);
+  }
+
+  /**
+   * Defines a variable for a file located in assets directory.
+   *
+   * @param name Name of the variable.
+   * @param defaultFilename Default filename.
+   * @return Initialized variable.
+   */
+  public static Var<String> defineAsset(String name, String defaultFilename) {
+    return define(name, defaultFilename, Constants.Kinds.FILE, new VarInitializer<String>() {
+      @Override
+      public void init(Var<String> var) {
+        var.isAsset = true;
+      }
+    });
+  }
+
+  /**
+   * Define a resource variable with default value referencing id of the file located in
+   * res/ directory.
+   *
+   * @param name Name of the variable.
+   * @param resId Resource id of any file located in res/ directory.
+   * @return Initalized variable.
+   */
+  public static Var<String> defineResource(String name, int resId) {
+    String resourceName = Util.generateResourceNameFromId(resId);
+    return define(name, resourceName, Constants.Kinds.FILE, new VarInitializer<String>() {
+      @Override
+      public void init(Var<String> var) {
+        var.isResource = true;
+      }
+    });
+  }
+
+  /**
+   * Defines a resource.
+   *
+   * @param name Name of the variable.
+   * @param defaultFilename Default filename.
+   * @param size Size of the data.
+   * @param hash Hash of the data.
+   * @param data Data.
+   * @return Initalized variable.
+   */
+  public static Var<String> defineResource(String name, String defaultFilename,
+      final int size, final String hash, final byte[] data) {
+    return define(name, defaultFilename, Constants.Kinds.FILE, new VarInitializer<String>() {
+      @Override
+      public void init(Var<String> var) {
+        var.isResource = true;
+        var.size = size;
+        var.hash = hash;
+        var.data = data;
+      }
+    });
+  }
+
+  protected Var() {
+  }
+
+  /**
+   * Gets name of the variable.
+   *
+   * @return Varaible name.
+   */
+  public String name() {
+    return name;
+  }
+
+  /**
+   * Gets name components of a variable.
+   *
+   * @return Name components.
+   */
+  public String[] nameComponents() {
+    return nameComponents;
+  }
+
+  /**
+   * Gets the kind of a variable.
+   *
+   * @return Kind of a variable.
+   */
+  public String kind() {
+    return kind;
+  }
+
+  /**
+   * Gets variable default value.
+   *
+   * @return Default value.
+   */
+  public T defaultValue() {
+    return defaultValue;
+  }
+
+  /**
+   * Get variable value.
+   *
+   * @return Value.
+   */
+  public T value() {
+    warnIfNotStarted();
+    return value;
+  }
+
+  /**
+   * Gets overridden resource id for variable.
+   *
+   * @return Id of the overridden resource.
+   */
+  public int overrideResId() {
+    return overrideResId;
+  }
+
+  /**
+   * Sets overridden resource id for a variable.
+   *
+   * @param resId Resource id.
+   */
+  public void setOverrideResId(int resId) {
+    overrideResId = resId;
+  }
+
+  @SuppressWarnings("unchecked")
+  private void cacheComputedValues() {
+    if (value instanceof String) {
+      stringValue = (String) value;
+      try {
+        numberValue = Double.valueOf(stringValue);
+      } catch (NumberFormatException e) {
+        numberValue = null;
+      }
+    } else if (value instanceof Number) {
+      stringValue = "" + value;
+      numberValue = ((Number) value).doubleValue();
+      if (defaultValue instanceof Byte) {
+        value = (T) (Byte) ((Number) value).byteValue();
+      } else if (defaultValue instanceof Short) {
+        value = (T) (Short) ((Number) value).shortValue();
+      } else if (defaultValue instanceof Integer) {
+        value = (T) (Integer) ((Number) value).intValue();
+      } else if (defaultValue instanceof Long) {
+        value = (T) (Long) ((Number) value).longValue();
+      } else if (defaultValue instanceof Float) {
+        value = (T) (Float) ((Number) value).floatValue();
+      } else if (defaultValue instanceof Double) {
+        value = (T) (Double) ((Number) value).doubleValue();
+      } else if (defaultValue instanceof Character) {
+        value = (T) (Character) (char) ((Number) value).intValue();
+      }
+    } else if (value != null &&
+        !(value instanceof Iterable<?>) && !(value instanceof Map<?, ?>)) {
+      stringValue = value.toString();
+      numberValue = null;
+    } else {
+      stringValue = null;
+      numberValue = null;
+    }
+  }
+
+  /**
+   * Updates variable with values from server.
+   */
+  public void update() {
+    // TODO: Clean up memory for resource variables.
+    //data = null;
+
+    T oldValue = value;
+    value = VarCache.getMergedValueFromComponentArray(nameComponents);
+    if (value == null && oldValue == null) {
+      return;
+    }
+    if (value != null && oldValue != null && value.equals(oldValue) && hadStarted) {
+      return;
+    }
+    cacheComputedValues();
+
+    if (VarCache.silent() && name.startsWith(Constants.Values.RESOURCES_VARIABLE)
+        && Constants.Kinds.FILE.equals(kind) && !fileIsPending) {
+      triggerFileIsReady();
+    }
+
+    if (VarCache.silent()) {
+      return;
+    }
+
+    if (Leanplum.hasStarted()) {
+      triggerValueChanged();
+    }
+
+    // Check if file exists, otherwise we need to download it.
+    if (Constants.Kinds.FILE.equals(kind)) {
+      if (!Constants.isNoop()) {
+        DownloadFileResult result = FileManager.maybeDownloadFile(
+            isResource, stringValue, (String) defaultValue, null,
+            new Runnable() {
+              @Override
+              public void run() {
+                triggerFileIsReady();
+              }
+            });
+        valueIsInAssets = false;
+        if (result == DownloadFileResult.DOWNLOADING) {
+          fileIsPending = true;
+        } else if (result == DownloadFileResult.EXISTS_IN_ASSETS) {
+          valueIsInAssets = true;
+        }
+      }
+      if (Leanplum.hasStarted() && !fileIsPending) {
+        triggerFileIsReady();
+      }
+    }
+
+    if (Leanplum.hasStarted()) {
+      hadStarted = true;
+    }
+  }
+
+  private void triggerValueChanged() {
+    synchronized (valueChangedHandlers) {
+      for (VariableCallback<T> callback : valueChangedHandlers) {
+        callback.setVariable(this);
+        OsHandler.getInstance().post(callback);
+      }
+    }
+  }
+
+  /**
+   * Adds value changed handler for a given variable.
+   *
+   * @param handler Handler to add.
+   */
+  public void addValueChangedHandler(VariableCallback<T> handler) {
+    if (handler == null) {
+      Log.e("Invalid handler parameter provided.");
+      return;
+    }
+
+    synchronized (valueChangedHandlers) {
+      valueChangedHandlers.add(handler);
+    }
+    if (Leanplum.hasStarted()) {
+      handler.handle(this);
+    }
+  }
+
+  /**
+   * Removes value changed handler for a given variable.
+   *
+   * @param handler Handler to be removed.
+   */
+  public void removeValueChangedHandler(VariableCallback<T> handler) {
+    synchronized (valueChangedHandlers) {
+      valueChangedHandlers.remove(handler);
+    }
+  }
+
+  private void triggerFileIsReady() {
+    synchronized (fileReadyHandlers) {
+      fileIsPending = false;
+      for (VariableCallback<T> callback : fileReadyHandlers) {
+        callback.setVariable(this);
+        OsHandler.getInstance().post(callback);
+      }
+    }
+  }
+
+  /**
+   * Adds file ready handler for a given variable.
+   *
+   * @param handler Handler to add.
+   */
+  public void addFileReadyHandler(VariableCallback<T> handler) {
+    if (handler == null) {
+      Log.e("Invalid handler parameter provided.");
+      return;
+    }
+    synchronized (fileReadyHandlers) {
+      fileReadyHandlers.add(handler);
+    }
+    if (Leanplum.hasStarted() && !fileIsPending) {
+      handler.handle(this);
+    }
+  }
+
+  /**
+   * Removes file ready handler for a given variable.
+   *
+   * @param handler Handler to be removed.
+   */
+  public void removeFileReadyHandler(VariableCallback<T> handler) {
+    if (handler == null) {
+      Log.e("Invalid handler parameter provided.");
+      return;
+    }
+    synchronized (fileReadyHandlers) {
+      fileReadyHandlers.remove(handler);
+    }
+  }
+
+  /**
+   * Returns file value for variable initialized as file/asset/resource.
+   *
+   * @return String representing file value.
+   */
+  public String fileValue() {
+    try {
+      warnIfNotStarted();
+      if (Constants.Kinds.FILE.equals(kind)) {
+        return FileManager.fileValue(stringValue, (String) defaultValue, valueIsInAssets);
+      }
+    } catch (Throwable t) {
+      Util.handleException(t);
+    }
+    return null;
+  }
+
+  /**
+   * Returns object for specified key path.
+   *
+   * @param keys Keys to look for.
+   * @return Object if found, null otherwise.
+   */
+  @SuppressWarnings("WeakerAccess") // Used by Air SDK.
+  public Object objectForKeyPath(Object... keys) {
+    try {
+      warnIfNotStarted();
+      List<Object> components = new ArrayList<>();
+      Collections.addAll(components, nameComponents);
+      if (keys != null && keys.length > 0) {
+        Collections.addAll(components, keys);
+      }
+      return VarCache.getMergedValueFromComponentArray(
+          components.toArray(new Object[components.size()]));
+    } catch (Throwable t) {
+      Util.handleException(t);
+      return null;
+    }
+  }
+
+  /**
+   * 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);
+      return 0;
+    }
+    LeanplumInternal.maybeThrowException(new UnsupportedOperationException(
+        "This variable is not a list."));
+    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.
+   */
+  public String stringValue() {
+    warnIfNotStarted();
+    return stringValue;
+  }
+
+  /**
+   * Creates and returns InputStream for overridden file/asset/resource variable.
+   * Caller is responsible for closing it properly to avoid leaking resources.
+   *
+   * @return InputStream for a file.
+   */
+  public InputStream stream() {
+    try {
+      if (!Constants.Kinds.FILE.equals(kind)) {
+        return null;
+      }
+      warnIfNotStarted();
+      InputStream stream = FileManager.stream(isResource, isAsset, valueIsInAssets,
+          fileValue(), (String) defaultValue, data);
+      if (stream == null) {
+        return defaultStream();
+      }
+      return stream;
+    } catch (Throwable t) {
+      Util.handleException(t);
+      return null;
+    }
+  }
+
+  /**
+   * Creates and returns InputStream for default file/asset/resource variable.
+   * Caller is responsible for closing it properly to avoid leaking resources.
+   *
+   * @return InputStream for a file.
+   */
+  private InputStream defaultStream() {
+    try {
+      if (!Constants.Kinds.FILE.equals(kind)) {
+        return null;
+      }
+      return FileManager.stream(isResource, isAsset, valueIsInAssets,
+          (String) defaultValue, (String) defaultValue, data);
+    } catch (Throwable t) {
+      Util.handleException(t);
+      return null;
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "Var(" + name + ")=" + value;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumAccountAuthenticatorActivity.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.activities;
+
+import android.accounts.AccountAuthenticatorActivity;
+import android.annotation.SuppressLint;
+import android.content.res.Resources;
+
+import com.leanplum.Leanplum;
+import com.leanplum.LeanplumActivityHelper;
+
+@SuppressLint("Registered")
+public class LeanplumAccountAuthenticatorActivity extends AccountAuthenticatorActivity {
+  private LeanplumActivityHelper helper;
+
+  private LeanplumActivityHelper getHelper() {
+    if (helper == null) {
+      helper = new LeanplumActivityHelper(this);
+    }
+    return helper;
+  }
+
+  @Override
+  protected void onPause() {
+    super.onPause();
+    getHelper().onPause();
+  }
+
+  @Override
+  protected void onStop() {
+    super.onStop();
+    getHelper().onStop();
+  }
+
+  @Override
+  protected void onResume() {
+    super.onResume();
+    getHelper().onResume();
+  }
+
+  @Override
+  public Resources getResources() {
+    if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+      return super.getResources();
+    }
+    return getHelper().getLeanplumResources(super.getResources());
+  }
+
+  @Override
+  public void setContentView(final int layoutResID) {
+    if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+      super.setContentView(layoutResID);
+      return;
+    }
+    getHelper().setContentView(layoutResID);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumActionBarActivity.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.activities;
+
+import android.annotation.SuppressLint;
+import android.content.res.Resources;
+import android.support.v7.app.ActionBarActivity;
+
+import com.leanplum.Leanplum;
+import com.leanplum.LeanplumActivityHelper;
+
+@SuppressLint("Registered")
+@SuppressWarnings("deprecation")
+public class LeanplumActionBarActivity extends ActionBarActivity {
+  private LeanplumActivityHelper helper;
+
+  private LeanplumActivityHelper getHelper() {
+    if (helper == null) {
+      helper = new LeanplumActivityHelper(this);
+    }
+    return helper;
+  }
+
+  @Override
+  protected void onPause() {
+    super.onPause();
+    getHelper().onPause();
+  }
+
+  @Override
+  protected void onStop() {
+    super.onStop();
+    getHelper().onStop();
+  }
+
+  @Override
+  protected void onResume() {
+    super.onResume();
+    getHelper().onResume();
+  }
+
+  @Override
+  public Resources getResources() {
+    if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+      return super.getResources();
+    }
+    return getHelper().getLeanplumResources(super.getResources());
+  }
+
+  @Override
+  public void setContentView(final int layoutResID) {
+    if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+      super.setContentView(layoutResID);
+      return;
+    }
+    getHelper().setContentView(layoutResID);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumActivity.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.activities;
+
+import android.app.Activity;
+import android.content.res.Resources;
+
+import com.leanplum.Leanplum;
+import com.leanplum.LeanplumActivityHelper;
+
+public abstract class LeanplumActivity extends Activity {
+  private LeanplumActivityHelper helper;
+
+  private LeanplumActivityHelper getHelper() {
+    if (helper == null) {
+      helper = new LeanplumActivityHelper(this);
+    }
+    return helper;
+  }
+
+  @Override
+  protected void onPause() {
+    super.onPause();
+    getHelper().onPause();
+  }
+
+  @Override
+  protected void onStop() {
+    super.onStop();
+    getHelper().onStop();
+  }
+
+  @Override
+  protected void onResume() {
+    super.onResume();
+    getHelper().onResume();
+  }
+
+  @Override
+  public Resources getResources() {
+    if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+      return super.getResources();
+    }
+    return getHelper().getLeanplumResources(super.getResources());
+  }
+
+  @Override
+  public void setContentView(final int layoutResID) {
+    if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+      super.setContentView(layoutResID);
+      return;
+    }
+    getHelper().setContentView(layoutResID);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumActivityGroup.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.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")
+public class LeanplumActivityGroup extends ActivityGroup {
+  private LeanplumActivityHelper helper;
+
+  private LeanplumActivityHelper getHelper() {
+    if (helper == null) {
+      helper = new LeanplumActivityHelper(this);
+    }
+    return helper;
+  }
+
+  @Override
+  protected void onPause() {
+    super.onPause();
+    getHelper().onPause();
+  }
+
+  @Override
+  protected void onStop() {
+    super.onStop();
+    getHelper().onStop();
+  }
+
+  @Override
+  protected void onResume() {
+    super.onResume();
+    getHelper().onResume();
+  }
+
+  @Override
+  public Resources getResources() {
+    if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+      return super.getResources();
+    }
+    return getHelper().getLeanplumResources(super.getResources());
+  }
+
+  @Override
+  public void setContentView(final int layoutResID) {
+    if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+      super.setContentView(layoutResID);
+      return;
+    }
+    getHelper().setContentView(layoutResID);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumAliasActivity.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.activities;
+
+import android.annotation.SuppressLint;
+import android.app.AliasActivity;
+import android.content.res.Resources;
+
+import com.leanplum.Leanplum;
+import com.leanplum.LeanplumActivityHelper;
+
+@SuppressLint("Registered")
+public class LeanplumAliasActivity extends AliasActivity {
+  private LeanplumActivityHelper helper;
+
+  private LeanplumActivityHelper getHelper() {
+    if (helper == null) {
+      helper = new LeanplumActivityHelper(this);
+    }
+    return helper;
+  }
+
+  @Override
+  protected void onPause() {
+    super.onPause();
+    getHelper().onPause();
+  }
+
+  @Override
+  protected void onStop() {
+    super.onStop();
+    getHelper().onStop();
+  }
+
+  @Override
+  protected void onResume() {
+    super.onResume();
+    getHelper().onResume();
+  }
+
+  @Override
+  public Resources getResources() {
+    if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+      return super.getResources();
+    }
+    return getHelper().getLeanplumResources(super.getResources());
+  }
+
+  @Override
+  public void setContentView(final int layoutResID) {
+    if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+      super.setContentView(layoutResID);
+      return;
+    }
+    getHelper().setContentView(layoutResID);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumAppCompatActivity.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2015, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.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")
+public class LeanplumAppCompatActivity extends AppCompatActivity {
+  private LeanplumActivityHelper helper;
+
+  private LeanplumActivityHelper getHelper() {
+    if (helper == null) {
+      helper = new LeanplumActivityHelper(this);
+    }
+    return helper;
+  }
+
+  @Override
+  protected void onPause() {
+    super.onPause();
+    getHelper().onPause();
+  }
+
+  @Override
+  protected void onStop() {
+    super.onStop();
+    getHelper().onStop();
+  }
+
+  @Override
+  protected void onResume() {
+    super.onResume();
+    getHelper().onResume();
+  }
+
+  @Override
+  public Resources getResources() {
+    if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+      return super.getResources();
+    }
+    return getHelper().getLeanplumResources(super.getResources());
+  }
+
+  @Override
+  public void setContentView(final int layoutResID) {
+    if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+      super.setContentView(layoutResID);
+      return;
+    }
+    getHelper().setContentView(layoutResID);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumExpandableListActivity.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.activities;
+
+import android.annotation.SuppressLint;
+import android.app.ExpandableListActivity;
+import android.content.res.Resources;
+
+import com.leanplum.Leanplum;
+import com.leanplum.LeanplumActivityHelper;
+
+@SuppressLint("Registered")
+public class LeanplumExpandableListActivity extends ExpandableListActivity {
+  private LeanplumActivityHelper helper;
+
+  private LeanplumActivityHelper getHelper() {
+    if (helper == null) {
+      helper = new LeanplumActivityHelper(this);
+    }
+    return helper;
+  }
+
+  @Override
+  protected void onPause() {
+    super.onPause();
+    getHelper().onPause();
+  }
+
+  @Override
+  protected void onStop() {
+    super.onStop();
+    getHelper().onStop();
+  }
+
+  @Override
+  protected void onResume() {
+    super.onResume();
+    getHelper().onResume();
+  }
+
+  @Override
+  public Resources getResources() {
+    if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+      return super.getResources();
+    }
+    return getHelper().getLeanplumResources(super.getResources());
+  }
+
+  @Override
+  public void setContentView(final int layoutResID) {
+    if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+      super.setContentView(layoutResID);
+      return;
+    }
+    getHelper().setContentView(layoutResID);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumFragmentActivity.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.activities;
+
+import android.content.res.Resources;
+import android.support.v4.app.FragmentActivity;
+
+import com.leanplum.Leanplum;
+import com.leanplum.LeanplumActivityHelper;
+
+public abstract class LeanplumFragmentActivity extends FragmentActivity {
+  private LeanplumActivityHelper helper;
+
+  private LeanplumActivityHelper getHelper() {
+    if (helper == null) {
+      helper = new LeanplumActivityHelper(this);
+    }
+    return helper;
+  }
+
+  @Override
+  protected void onPause() {
+    super.onPause();
+    getHelper().onPause();
+  }
+
+  @Override
+  protected void onStop() {
+    super.onStop();
+    getHelper().onStop();
+  }
+
+  @Override
+  protected void onResume() {
+    super.onResume();
+    getHelper().onResume();
+  }
+
+  @Override
+  public Resources getResources() {
+    if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+      return super.getResources();
+    }
+    return getHelper().getLeanplumResources(super.getResources());
+  }
+
+  @Override
+  public void setContentView(final int layoutResID) {
+    if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+      super.setContentView(layoutResID);
+      return;
+    }
+    getHelper().setContentView(layoutResID);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumLauncherActivity.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.activities;
+
+import android.annotation.SuppressLint;
+import android.app.LauncherActivity;
+import android.content.res.Resources;
+
+import com.leanplum.Leanplum;
+import com.leanplum.LeanplumActivityHelper;
+
+@SuppressLint("Registered")
+public class LeanplumLauncherActivity extends LauncherActivity {
+  private LeanplumActivityHelper helper;
+
+  private LeanplumActivityHelper getHelper() {
+    if (helper == null) {
+      helper = new LeanplumActivityHelper(this);
+    }
+    return helper;
+  }
+
+  @Override
+  protected void onPause() {
+    super.onPause();
+    getHelper().onPause();
+  }
+
+  @Override
+  protected void onStop() {
+    super.onStop();
+    getHelper().onStop();
+  }
+
+  @Override
+  protected void onResume() {
+    super.onResume();
+    getHelper().onResume();
+  }
+
+  @Override
+  public Resources getResources() {
+    if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+      return super.getResources();
+    }
+    return getHelper().getLeanplumResources(super.getResources());
+  }
+
+  @Override
+  public void setContentView(final int layoutResID) {
+    if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+      super.setContentView(layoutResID);
+      return;
+    }
+    getHelper().setContentView(layoutResID);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumListActivity.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.activities;
+
+import android.annotation.SuppressLint;
+import android.app.ListActivity;
+import android.content.res.Resources;
+
+import com.leanplum.Leanplum;
+import com.leanplum.LeanplumActivityHelper;
+
+@SuppressLint("Registered")
+public class LeanplumListActivity extends ListActivity {
+  private LeanplumActivityHelper helper;
+
+  private LeanplumActivityHelper getHelper() {
+    if (helper == null) {
+      helper = new LeanplumActivityHelper(this);
+    }
+    return helper;
+  }
+
+  @Override
+  protected void onPause() {
+    super.onPause();
+    getHelper().onPause();
+  }
+
+  @Override
+  protected void onStop() {
+    super.onStop();
+    getHelper().onStop();
+  }
+
+  @Override
+  protected void onResume() {
+    super.onResume();
+    getHelper().onResume();
+  }
+
+  @Override
+  public Resources getResources() {
+    if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+      return super.getResources();
+    }
+    return getHelper().getLeanplumResources(super.getResources());
+  }
+
+  @Override
+  public void setContentView(final int layoutResID) {
+    if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+      super.setContentView(layoutResID);
+      return;
+    }
+    getHelper().setContentView(layoutResID);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumNativeActivity.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.activities;
+
+import android.annotation.SuppressLint;
+import android.app.NativeActivity;
+import android.content.res.Resources;
+
+import com.leanplum.Leanplum;
+import com.leanplum.LeanplumActivityHelper;
+
+@SuppressLint("Registered")
+public class LeanplumNativeActivity extends NativeActivity {
+  private LeanplumActivityHelper helper;
+
+  private LeanplumActivityHelper getHelper() {
+    if (helper == null) {
+      helper = new LeanplumActivityHelper(this);
+    }
+    return helper;
+  }
+
+  @Override
+  protected void onPause() {
+    super.onPause();
+    getHelper().onPause();
+  }
+
+  @Override
+  protected void onStop() {
+    super.onStop();
+    getHelper().onStop();
+  }
+
+  @Override
+  protected void onResume() {
+    super.onResume();
+    getHelper().onResume();
+  }
+
+  @Override
+  public Resources getResources() {
+    if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+      return super.getResources();
+    }
+    return getHelper().getLeanplumResources(super.getResources());
+  }
+
+  @Override
+  public void setContentView(final int layoutResID) {
+    if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+      super.setContentView(layoutResID);
+      return;
+    }
+    getHelper().setContentView(layoutResID);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumPreferenceActivity.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package com.leanplum.activities;
+
+import android.annotation.SuppressLint;
+import android.content.res.Resources;
+import android.preference.PreferenceActivity;
+
+import com.leanplum.Leanplum;
+import com.leanplum.LeanplumActivityHelper;
+
+@SuppressLint("Registered")
+public class LeanplumPreferenceActivity extends PreferenceActivity {
+  private LeanplumActivityHelper helper;
+
+  private LeanplumActivityHelper getHelper() {
+    if (helper == null) {
+      helper = new LeanplumActivityHelper(this);
+    }
+    return helper;
+  }
+
+  @Override
+  protected void onPause() {
+    super.onPause();
+    getHelper().onPause();
+  }
+
+  @Override
+  protected void onStop() {
+    super.onStop();
+    getHelper().onStop();
+  }
+
+  @Override
+  protected void onResume() {
+    super.onResume();
+    getHelper().onResume();
+  }
+
+  @Override
+  public Resources getResources() {
+    if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+      return super.getResources();
+    }
+    return getHelper().getLeanplumResources(super.getResources());
+  }
+
+  @Override
+  public void setContentView(final int layoutResID) {
+    if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+      super.setContentView(layoutResID);
+      return;
+    }
+    getHelper().setContentView(layoutResID);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumTabActivity.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.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")
+public class LeanplumTabActivity extends TabActivity {
+  private LeanplumActivityHelper helper;
+
+  private LeanplumActivityHelper getHelper() {
+    if (helper == null) {
+      helper = new LeanplumActivityHelper(this);
+    }
+    return helper;
+  }
+
+  @Override
+  protected void onPause() {
+    super.onPause();
+    getHelper().onPause();
+  }
+
+  @Override
+  protected void onStop() {
+    super.onStop();
+    getHelper().onStop();
+  }
+
+  @Override
+  protected void onResume() {
+    super.onResume();
+    getHelper().onResume();
+  }
+
+  @Override
+  public Resources getResources() {
+    if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+      return super.getResources();
+    }
+    return getHelper().getLeanplumResources(super.getResources());
+  }
+
+  @Override
+  public void setContentView(final int layoutResID) {
+    if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+      super.setContentView(layoutResID);
+      return;
+    }
+    getHelper().setContentView(layoutResID);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/annotations/File.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Leanplum variable annotation. Use this to make this variable changeable from the Leanplum
+ * dashboard.
+ *
+ * @author Andrew First
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface File {
+  /**
+   * (Optional). The group to put the variable in. Use "." to nest groups.
+   */
+  String group() default "";
+
+  /**
+   * (Optional). The name of the variable. If not set, then uses the actual name of the field.
+   */
+  String name() default "";
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/annotations/Parser.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.annotations;
+
+import android.text.TextUtils;
+
+import com.leanplum.Var;
+import com.leanplum.callbacks.VariableCallback;
+import com.leanplum.internal.Log;
+
+import java.lang.ref.WeakReference;
+import java.lang.reflect.Field;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Parses Leanplum annotations.
+ *
+ * @author Andrew First
+ */
+public class Parser {
+  private static <T> void defineVariable(
+      final Object instance,
+      String name,
+      T value,
+      String kind,
+      final Field field) {
+    final Var<T> var = Var.define(name, value, kind);
+    final boolean hasInstance = instance != null;
+    final WeakReference<Object> weakInstance = new WeakReference<>(instance);
+    var.addValueChangedHandler(new VariableCallback<T>() {
+      @Override
+      public void handle(Var<T> variable) {
+        Object instance = weakInstance.get();
+        if ((hasInstance && instance == null) || field == null) {
+          var.removeValueChangedHandler(this);
+          return;
+        }
+        try {
+          boolean accessible = field.isAccessible();
+          if (!accessible) {
+            field.setAccessible(true);
+          }
+          field.set(instance, var.value());
+          if (!accessible) {
+            field.setAccessible(false);
+          }
+        } catch (IllegalArgumentException e) {
+          Log.e("Leanplum", "Invalid value " + var.value() +
+              " for field " + var.name(), e);
+        } catch (IllegalAccessException e) {
+          Log.e("Leanplum", "Error setting value for field " + var.name(), e);
+        }
+      }
+    });
+  }
+
+  private static void defineFileVariable(
+      final Object instance,
+      String name,
+      String value,
+      final Field field) {
+    final Var<String> var = Var.defineFile(name, value);
+    final boolean hasInstance = instance != null;
+    final WeakReference<Object> weakInstance = new WeakReference<>(instance);
+    var.addFileReadyHandler(new VariableCallback<String>() {
+      @Override
+      public void handle(Var<String> variable) {
+        Object instance = weakInstance.get();
+        if ((hasInstance && instance == null) || field == null) {
+          var.removeFileReadyHandler(this);
+          return;
+        }
+        try {
+          boolean accessible = field.isAccessible();
+          if (!accessible) {
+            field.setAccessible(true);
+          }
+          field.set(instance, var.fileValue());
+          if (!accessible) {
+            field.setAccessible(false);
+          }
+        } catch (IllegalArgumentException e) {
+          Log.e("Leanplum", "Invalid value " + var.value() +
+              " for field " + var.name(), e);
+        } catch (IllegalAccessException e) {
+          Log.e("Leanplum", "Error setting value for field " + var.name(), e);
+        }
+      }
+    });
+  }
+
+  /**
+   * Parses Leanplum annotations for all given object instances.
+   */
+  public static void parseVariables(Object... instances) {
+    try {
+      for (Object instance : instances) {
+        parseVariablesHelper(instance, instance.getClass());
+      }
+    } catch (Throwable t) {
+      Log.e("Leanplum", "Error parsing variables", t);
+    }
+  }
+
+  /**
+   * Parses Leanplum annotations for all given classes.
+   */
+  public static void parseVariablesForClasses(Class<?>... classes) {
+    try {
+      for (Class<?> clazz : classes) {
+        parseVariablesHelper(null, clazz);
+      }
+    } catch (Throwable t) {
+      Log.e("Leanplum", "Error parsing variables", t);
+    }
+  }
+
+  private static void parseVariablesHelper(Object instance, Class<?> clazz)
+      throws IllegalArgumentException, IllegalAccessException {
+    Field[] fields = clazz.getFields();
+
+    for (final Field field : fields) {
+      String group, name;
+      boolean isFile = false;
+      if (field.isAnnotationPresent(Variable.class)) {
+        Variable annotation = field.getAnnotation(Variable.class);
+        group = annotation.group();
+        name = annotation.name();
+      } else if (field.isAnnotationPresent(File.class)) {
+        File annotation = field.getAnnotation(File.class);
+        group = annotation.group();
+        name = annotation.name();
+        isFile = true;
+      } else {
+        continue;
+      }
+
+      String variableName = name;
+      if (TextUtils.isEmpty(variableName)) {
+        variableName = field.getName();
+      }
+      if (!TextUtils.isEmpty(group)) {
+        variableName = group + "." + variableName;
+      }
+
+      Class<?> fieldType = field.getType();
+      String fieldTypeString = fieldType.toString();
+      if (fieldTypeString.equals("int")) {
+        defineVariable(instance, variableName, field.getInt(instance), "integer", field);
+      } else if (fieldTypeString.equals("byte")) {
+        defineVariable(instance, variableName, field.getByte(instance), "integer", field);
+      } else if (fieldTypeString.equals("short")) {
+        defineVariable(instance, variableName, field.getShort(instance), "integer", field);
+      } else if (fieldTypeString.equals("long")) {
+        defineVariable(instance, variableName, field.getLong(instance), "integer", field);
+      } else if (fieldTypeString.equals("char")) {
+        defineVariable(instance, variableName, field.getChar(instance), "integer", field);
+      } else if (fieldTypeString.equals("float")) {
+        defineVariable(instance, variableName, field.getFloat(instance), "float", field);
+      } else if (fieldTypeString.equals("double")) {
+        defineVariable(instance, variableName, field.getDouble(instance), "float", field);
+      } else if (fieldTypeString.equals("boolean")) {
+        defineVariable(instance, variableName, field.getBoolean(instance), "bool", field);
+      } else if (fieldType.isPrimitive()) {
+        Log.e("Variable " + variableName + " is an unsupported primitive type.");
+      } else if (fieldType.isArray()) {
+        Log.e("Variable " + variableName + " should be a List instead of an Array.");
+      } else if (fieldType.isAssignableFrom(List.class)) {
+        defineVariable(instance, variableName, field.get(instance), "list", field);
+      } else if (fieldType.isAssignableFrom(Map.class)) {
+        defineVariable(instance, variableName, field.get(instance), "group", field);
+      } else {
+        Object value = field.get(instance);
+        String stringValue = value == null ? null : value.toString();
+        if (isFile) {
+          defineFileVariable(instance, variableName, stringValue, field);
+        } else {
+          defineVariable(instance, variableName, stringValue, "string", field);
+        }
+      }
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/annotations/Variable.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Leanplum variable annotation.
+ * <p>
+ * <p>Use this to make this variable changeable from the Leanplum dashboard. Variables must be of
+ * type boolean, byte, short, int, long, float, double, char, String, List, or Map. Lists and maps
+ * may contain other lists and maps.
+ * <p>
+ * <p>Variables with this annotation update when the API call for Leanplum.start completes
+ * successfully or fails (in which case values are loaded from a cache stored on the device).
+ *
+ * @author Andrew First
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Variable {
+  /**
+   * (Optional). The group to put the variable in. Use "." to nest groups.
+   */
+  String group() default "";
+
+  /**
+   * (Optional). The name of the variable. If not set, then uses the actual name of the field.
+   */
+  String name() default "";
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/callbacks/ActionCallback.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2014, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.callbacks;
+
+import com.leanplum.ActionContext;
+
+/**
+ * Callback that gets run when an action is triggered.
+ *
+ * @author Andrew First
+ */
+public abstract class ActionCallback {
+  public abstract boolean onResponse(ActionContext context);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/callbacks/InboxChangedCallback.java
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+
+package com.leanplum.callbacks;
+
+/**
+ * Inbox changes callback.
+ *
+ * @author Anna Orlova
+ */
+public abstract class InboxChangedCallback implements Runnable {
+  public void run() {
+    this.inboxChanged();
+  }
+
+  public abstract void inboxChanged();
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/callbacks/NewsfeedChangedCallback.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2015, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.callbacks;
+
+/**
+ * Newsfeed changed callback.
+ *
+ * @author Aleksandar Gyorev
+ */
+public abstract class NewsfeedChangedCallback extends InboxChangedCallback {
+  @Override
+  public void inboxChanged() {
+    newsfeedChanged();
+  }
+
+  public abstract void newsfeedChanged();
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/callbacks/PostponableAction.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2016, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.callbacks;
+
+import com.leanplum.LeanplumActivityHelper;
+
+/**
+ * Action callback that will not be executed for activity classes that are ignored via
+ * {@link LeanplumActivityHelper#deferMessagesForActivities(Class[])}
+ *
+ * @author Ben Marten
+ */
+public abstract class PostponableAction implements Runnable {
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/callbacks/RegisterDeviceCallback.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.callbacks;
+
+/**
+ * Callback that gets run when the device needs to be registered.
+ *
+ * @author Andrew First
+ */
+public abstract class RegisterDeviceCallback implements Runnable {
+  public static abstract class EmailCallback implements Runnable {
+    private String email;
+
+    public void setResponseHandler(String email) {
+      this.email = email;
+    }
+
+    public void run() {
+      this.onResponse(email);
+    }
+
+    public abstract void onResponse(String email);
+  }
+
+  private EmailCallback callback;
+
+  public void setResponseHandler(EmailCallback callback) {
+    this.callback = callback;
+  }
+
+  public void run() {
+    this.onResponse(callback);
+  }
+
+  public abstract void onResponse(EmailCallback callback);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/callbacks/RegisterDeviceFinishedCallback.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.callbacks;
+
+/**
+ * Callback that gets run when the device has been registered.
+ *
+ * @author Andrew First
+ */
+public abstract class RegisterDeviceFinishedCallback implements Runnable {
+  private boolean success;
+
+  public void setSuccess(boolean success) {
+    this.success = success;
+  }
+
+  public void run() {
+    this.onResponse(success);
+  }
+
+  public abstract void onResponse(boolean success);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/callbacks/StartCallback.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.callbacks;
+
+/**
+ * Callback that gets run when Leanplum is started.
+ *
+ * @author Andrew First
+ */
+public abstract class StartCallback implements Runnable {
+  private boolean success;
+
+  public void setSuccess(boolean success) {
+    this.success = success;
+  }
+
+  public void run() {
+    this.onResponse(success);
+  }
+
+  public abstract void onResponse(boolean success);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/callbacks/VariableCallback.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.callbacks;
+
+import com.leanplum.Var;
+
+/**
+ * Leanplum variable callback.
+ *
+ * @author Andrew First
+ */
+public abstract class VariableCallback<T> implements Runnable {
+  private Var<T> variable;
+
+  public void setVariable(Var<T> variable) {
+    this.variable = variable;
+  }
+
+  public void run() {
+    this.handle(variable);
+  }
+
+  public abstract void handle(Var<T> variable);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/callbacks/VariablesChangedCallback.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.callbacks;
+
+/**
+ * Variables changed callback.
+ *
+ * @author Andrew First
+ */
+public abstract class VariablesChangedCallback implements Runnable {
+  public void run() {
+    this.variablesChanged();
+  }
+
+  public abstract void variablesChanged();
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/internal/AESCrypt.java
@@ -0,0 +1,311 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.internal;
+
+import android.content.SharedPreferences;
+import android.util.Pair;
+
+import java.io.UnsupportedEncodingException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.KeySpec;
+import java.util.Arrays;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * AES Encryption as detailed at
+ * http://nelenkov.blogspot.com/2012/04/using-password-based-encryption-on.html
+ *
+ * @author Aakash Patel
+ */
+public class AESCrypt {
+
+  private static enum EncryptionType {
+    /**
+     * Encryption based on a token received from the server. Used in SDK versions prior to 1.2.20.
+     * <p>
+     * Corresponds to ciphertexts of the format "[12, -33, 52]", corresponding to Arrays.toString()
+     * of an encrypted byte[].
+     * <p>
+     * Legacy values may decrypt to ciphertexts. We ignore what appear to be double-encrypted
+     * ciphertexts that are stored in a legacy format.
+     */
+    LEGACY_TOKEN(0),
+
+    /**
+     * Encryption based on the app ID. Used in SDK versions since 1.2.20.
+     * <p>
+     * Corresponds to ciphertexts of the format "01[12, -33, 52]". The format adds a version
+     * identifier ("01") prefix to ciphertexts, allowing us to change the format in the future.
+     * <p>
+     * With the exception of LEGACY_TOKEN ciphertexts, which must continue to be supported, we will
+     * use the first two characters to determine the encryption protocol.
+     */
+    APP_ID_KEY(1);
+
+    public final int id;
+    public final String prefix;
+    public final String prefixWithBracket;
+
+    EncryptionType(int id) {
+      this.id = id;
+      prefix = String.format("%02d", id);
+      prefixWithBracket = prefix + "[";
+    }
+
+    private static EncryptionType forId(int id) {
+      if (id == 1) {
+        return APP_ID_KEY;
+      }
+      return null;
+    }
+
+    public static Pair<EncryptionType, String> parseCipherText(String cipherText) {
+      if (cipherText == null || cipherText.isEmpty()) {
+        return null;
+      }
+      if (cipherText.startsWith("[")) {
+        return Pair.create(LEGACY_TOKEN, cipherText);
+      }
+      if (cipherText.startsWith(APP_ID_KEY.prefixWithBracket)) {
+        return Pair.create(
+            APP_ID_KEY, cipherText.substring(APP_ID_KEY.prefixWithBracket.length() - 1));
+      }
+      return null;
+    }
+  }
+
+  // Build prefix and suffix strings longhand, to obfuscate them slightly.
+  // Probably doesn't matter.
+  private static final String APP_ID_KEY_PREFIX = new StringBuilder()
+      .append("L").append("q").append(3).append("f").append("z").toString();
+  private static final String APP_ID_KEY_SUFFIX = new StringBuilder()
+      .append("b").append("L").append("t").append("i").append(2).toString();
+
+  private final String appId;
+  private final String token;
+
+  /**
+   * Creates an AESCrypt encryption context.
+   * <p>
+   * Intended for short-term use, since the encryption token can change.
+   */
+  public AESCrypt(String appId, String token) {
+    this.appId = appId;
+    this.token = token;
+  }
+
+  private String appIdKeyPassword() {
+    return APP_ID_KEY_PREFIX + appId + APP_ID_KEY_SUFFIX;
+  }
+
+  /**
+   * Creates a ciphertext using a password based on current context parameters.
+   *
+   * @param plaintext
+   * @return A cipher text string, or null if encryption fails (unexpected).
+   */
+  public String encrypt(String plaintext) {
+    if (plaintext == null) {
+      return null;
+    }
+    // Always encrypt using the APP_ID_KEY method.
+    if (appId == null || appId.isEmpty()) {
+      Log.e("Encrypt called with null appId.");
+      return null;
+    }
+    String cipherText = encryptInternal(appIdKeyPassword(), plaintext);
+    if (cipherText == null) {
+      Log.w("Failed to encrypt.");
+      return null;
+    }
+    if (cipherText.isEmpty() || cipherText.equals(plaintext) || !cipherText.startsWith("[")) {
+      Log.w("Invalid ciphertext: " + cipherText);
+      return null;
+    }
+    return EncryptionType.APP_ID_KEY.prefix + cipherText;
+  }
+
+  public String decodePreference(SharedPreferences preferences, String key, String defaultValue) {
+    String cipherText = preferences.getString(key, null);
+    if (cipherText == null) {
+      return defaultValue;
+    }
+    String decoded = decrypt(cipherText);
+    if (decoded == null) {
+      return defaultValue;
+    }
+    return decoded;
+  }
+
+  /**
+   * Decrypts a ciphertext in either legacy or current format, using a password based on context
+   * parameters.
+   *
+   * @param cipherText The value to encrypt; tolerates null.
+   * @return A cipher text string, or null if the value can't be decrypted.
+   */
+  public String decrypt(String cipherText) {
+    Pair<EncryptionType, String> encryptionSpec = EncryptionType.parseCipherText(cipherText);
+    String result = null;
+    if (encryptionSpec == null) {
+      Log.v("Got null encryptionSpec for encrypted: " + cipherText);
+    } else {
+      switch (encryptionSpec.first) {
+        case LEGACY_TOKEN:
+          if (token == null || token.isEmpty()) {
+            Log.e("Decrypt called with null token.");
+          } else {
+            result = decryptInternal(token, encryptionSpec.second);
+            // For legacy keys only -- detect if the value we decode is a valid legacy ciphertext.
+            // If so, it was almost certainly produced by legacy decryption, which would return
+            // ciphertext on decryption failure. Discard the value and return null.
+            if (result != null && parseCiphertextInternal(result) != null) {
+              Log.e("Discarding legacy value that appears to be an encrypted value: " +
+                  result);
+              return null;
+            }
+          }
+          break;
+        case APP_ID_KEY:
+          if (appId == null || appId.isEmpty()) {
+            Log.e("Decrypt called with null appId.");
+          } else {
+            result = decryptInternal(appIdKeyPassword(), encryptionSpec.second);
+          }
+          break;
+      }
+    }
+    if (result == null) {
+      Log.w("Unable to decrypt " + cipherText);
+    }
+    return result;
+  }
+
+  /**
+   * Encrypts the plaintext using password. In case of exception, returns null.
+   */
+  // VisibleForTesting
+  public static String encryptInternal(String password, String plaintext) {
+    try {
+      return Arrays.toString(performCryptOperation(Cipher.ENCRYPT_MODE, password,
+          plaintext.getBytes("UTF-8")));
+    } catch (UnsupportedEncodingException e) {
+      Log.w("Unable to encrypt " + plaintext, e);
+      return null;
+    }
+  }
+
+  private static byte[] parseCiphertextInternal(String ciphertext) {
+    if (ciphertext == null) {
+      return null;
+    }
+    ciphertext = ciphertext.trim();
+    if (ciphertext.length() < 2) {
+      return null;
+    }
+    try {
+      String[] byteStrings =
+          ciphertext.substring(1, ciphertext.length() - 1).trim().split("\\s*,\\s*");
+      byte[] bytes = new byte[byteStrings.length];
+      for (int i = 0; i < byteStrings.length; i++) {
+        bytes[i] = Byte.parseByte(byteStrings[i]);
+      }
+      return bytes;
+    } catch (NumberFormatException e) {
+      return null;
+    }
+  }
+
+  /**
+   * Decrypts the ciphertext using password. In case of exception, returns null.
+   *
+   * @param ciphertext Must be a valid byte array represented as a string as returned by
+   * Arrays.toString().
+   */
+  private static String decryptInternal(String password, String ciphertext) {
+    try {
+      byte[] bytes = parseCiphertextInternal(ciphertext);
+      if (bytes == null) {
+        Log.w("Invalid ciphertext: " + ciphertext);
+        return null;
+      }
+      byte[] byteResult = performCryptOperation(Cipher.DECRYPT_MODE, password, bytes);
+      if (byteResult != null) {
+        return new String(byteResult, "UTF-8");
+      }
+    } catch (UnsupportedEncodingException e) {
+      // Unreachable on android, which guarantees UTF-8 support.
+      Log.w("Could not encode UTF8 string.\n" + Log.getStackTraceString(e));
+    }
+    return null;
+  }
+
+  /**
+   * Performs either an encryption or a decryption based on the mode. In case of exception, returns
+   * null.
+   *
+   * @param mode Should be either Cipher.ENCRYPT_MODE or Cipher.DECRYPT_MODE
+   * @param password The password to crypt.
+   * @param text The text to crypt.
+   * @return The result of the crypt.
+   */
+  private static byte[] performCryptOperation(int mode, String password, byte[] text) {
+    byte[] result = null;
+    try {
+      byte[] SALT = Constants.Crypt.SALT.getBytes("UTF-8");
+      byte[] IV = Constants.Crypt.IV.getBytes("UTF-8");
+      KeySpec keySpec = new PBEKeySpec(password.toCharArray(), SALT, Constants.Crypt.ITER_COUNT,
+          Constants.Crypt.KEY_LENGTH);
+      SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBEWithMD5And128BitAES-CBC-OpenSSL");
+      byte[] keyBytes = keyFactory.generateSecret(keySpec).getEncoded();
+      SecretKey key = new SecretKeySpec(keyBytes, "AES");
+
+      Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+      IvParameterSpec ivParams = new IvParameterSpec(IV);
+      cipher.init(mode, key, ivParams);
+
+      result = cipher.doFinal(text);
+    } catch (InvalidKeyException e) {
+      // Don't log exceptions; we have more useful warning logs when this returns null.
+    } catch (NoSuchAlgorithmException e) {
+    } catch (NoSuchPaddingException e) {
+    } catch (InvalidAlgorithmParameterException e) {
+    } catch (IllegalBlockSizeException e) {
+    } catch (BadPaddingException e) {
+    } catch (UnsupportedEncodingException e) {
+    } catch (InvalidKeySpecException e) {
+    }
+    return result;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/internal/ActionArg.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2014, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.internal;
+
+import android.text.TextUtils;
+
+import java.io.InputStream;
+
+/**
+ * Represents an argument for a message or action.
+ *
+ * @param <T> Type of the argument. Can be Boolean, Byte, Short, Integer, Long, Float, Double,
+ * Character, String, List, or Map.
+ * @author Andrew First
+ */
+public class ActionArg<T> {
+  private String name;
+  private String kind;
+  private T defaultValue;
+  private boolean isAsset;
+
+  private ActionArg() {
+  }
+
+  private static <T> ActionArg<T> argNamed(String name, T defaultValue, String kind) {
+    ActionArg<T> arg = new ActionArg<>();
+    arg.name = name;
+    arg.kind = kind;
+    arg.defaultValue = defaultValue;
+    return arg;
+  }
+
+  /**
+   * Creates an instance of a new arg with a default value.
+   *
+   * @param name Name of the arg.
+   * @param defaultValue Default value of the arg. Can't be null.
+   */
+  public static <T> ActionArg<T> argNamed(String name, T defaultValue) {
+    return argNamed(name, defaultValue, VarCache.kindFromValue(defaultValue));
+  }
+
+  public static ActionArg<Integer> colorArgNamed(String name, int defaultValue) {
+    return argNamed(name, defaultValue, Constants.Kinds.COLOR);
+  }
+
+  public static ActionArg<String> fileArgNamed(String name, String defaultFilename) {
+    if (TextUtils.isEmpty(defaultFilename)) {
+      defaultFilename = "";
+    }
+    ActionArg<String> arg = argNamed(name, defaultFilename, Constants.Kinds.FILE);
+    VarCache.registerFile(arg.defaultValue, arg.defaultValue,
+        arg.defaultStream(), false, null, 0);
+    return arg;
+  }
+
+  public static ActionArg<String> assetArgNamed(String name, String defaultFilename) {
+    ActionArg<String> arg = argNamed(name, defaultFilename, Constants.Kinds.FILE);
+    arg.isAsset = true;
+    VarCache.registerFile(arg.defaultValue, arg.defaultValue,
+        arg.defaultStream(), false, null, 0);
+    return arg;
+  }
+
+  public static ActionArg<String> actionArgNamed(String name, String defaultValue) {
+    if (TextUtils.isEmpty(defaultValue)) {
+      defaultValue = "";
+    }
+    return argNamed(name, defaultValue, Constants.Kinds.ACTION);
+  }
+
+  public String name() {
+    return name;
+  }
+
+  public String kind() {
+    return kind;
+  }
+
+  public T defaultValue() {
+    return defaultValue;
+  }
+
+  public InputStream defaultStream() {
+    if (!kind.equals(Constants.Kinds.FILE)) {
+      return null;
+    }
+    return FileManager.stream(false, isAsset, isAsset,
+        (String) defaultValue, (String) defaultValue, null);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/internal/ActionManager.java
@@ -0,0 +1,642 @@
+/*
+ * Copyright 2014, Leanplum, Inc. All rights reserved.
+ *
+ * License