Bug 1351585 - Part 1. Add Leanplum SDK source code to thirdparty module r=maliu
authorcnevinc <cnevinc@livemail.tw>
Sat, 13 May 2017 14:05:09 -0700
changeset 409651 01942414ab897069110bbb1a4799c934094abd94
parent 409650 e3f094340972b618c30e7db5446907bfc2ae9b12
child 409652 a1e49355451750f01d92da281939f4daf36a0374
push id1490
push usermtabara@mozilla.com
push dateMon, 31 Jul 2017 14:08:16 +0000
treeherdermozilla-release@70e32e6bf15e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmaliu
bugs1351585
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1351585 - Part 1. Add Leanplum SDK source code to thirdparty module r=maliu 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
@@ -25,34 +25,47 @@ android {
             manifest.srcFile 'AndroidManifest.xml'
             java {
                 srcDir '.'
 
                 if (!mozconfig.substs.MOZ_INSTALL_TRACKING) {
                     exclude 'com/adjust/**'
                 }
 
+                if (!mozconfig.substs.MOZ_ANDROID_MMA) {
+                    exclude 'com/leanplum/**'
+                }
+
                 // Exclude LeakCanary: It will be added again via a gradle dependency. This version
                 // here is only the no-op library for mach-based builds.
                 exclude 'com/squareup/leakcanary/**'
             }
         }
     }
 }
 
 dependencies {
     compile "com.android.support:support-v4:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
+    if (mozconfig.substs.MOZ_ANDROID_MMA) {
+        compile "com.android.support:appcompat-v7:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
+        compile "com.android.support:support-annotations:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
+        compile "com.google.android.gms:play-services-ads:${mozconfig.substs.ANDROID_GOOGLE_PLAY_SERVICES_VERSION}"
+        compile "com.google.android.gms:play-services-gcm:${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) {
             excludeDirs += file('com/adjust/sdk')
         }
+        if (!mozconfig.substs.MOZ_ANDROID_MMA) {
+            excludeDirs += file('com/leanplum')
+        }
     }
 }
 
 // Bug 1353055 - Strip 'vars' debugging information to agree with moz.build.
 apply from: "${topsrcdir}/mobile/android/gradle/debug_level.gradle"
 android.libraryVariants.all configureVariantDebugLevel
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