--- 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 >= 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 >= 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
+ * <application> tag:
+ * <p>
+ * <pre><meta-data android:name="com.google.android.gms.version"
+ * android:value="@integer/google_play_services_version" /></pre>
+ */
+ ADVERTISING_ID,
+}
+
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumEditorMode.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2016, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+/**
+ * Enum for describing the Editor Mode.
+ *
+ * @author Ben Marten
+ */
+public enum LeanplumEditorMode {
+ LP_EDITOR_MODE_INTERFACE(0),
+ LP_EDITOR_MODE_EVENT(1);
+
+ private final int value;
+
+ /**
+ * Creates a new EditorMode enum with given value.
+ */
+ LeanplumEditorMode(final int newValue) {
+ value = newValue;
+ }
+
+ /**
+ * Returns the value of the enum entry.
+ *
+ * @return The value of the entry.
+ */
+ public int getValue() {
+ return value;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumException.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+/**
+ * Leanplum exception.
+ *
+ * @author Andrew First
+ */
+public class LeanplumException extends RuntimeException {
+ private static final long serialVersionUID = 1L;
+
+ public LeanplumException(String message) {
+ super(message);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumGcmProvider.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2016, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import android.content.Context;
+
+import com.google.android.gms.gcm.GoogleCloudMessaging;
+import com.google.android.gms.iid.InstanceID;
+import com.leanplum.internal.Constants;
+import com.leanplum.internal.LeanplumManifestHelper;
+import com.leanplum.internal.Log;
+import com.leanplum.internal.Util;
+import com.leanplum.utils.SharedPreferencesUtil;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+
+/**
+ * Leanplum provider for work with GCM.
+ *
+ * @author Anna Orlova
+ */
+class LeanplumGcmProvider extends LeanplumCloudMessagingProvider {
+ private static final String ERROR_TIMEOUT = "TIMEOUT";
+ private static final String ERROR_INVALID_SENDER = "INVALID_SENDER";
+ private static final String ERROR_AUTHENTICATION_FAILED = "AUTHENTICATION_FAILED";
+ private static final String ERROR_PHONE_REGISTRATION_ERROR = "PHONE_REGISTRATION_ERROR";
+ private static final String ERROR_TOO_MANY_REGISTRATIONS = "TOO_MANY_REGISTRATIONS";
+
+ private static final String SEND_PERMISSION = "com.google.android.c2dm.permission.SEND";
+ private static final String RECEIVE_PERMISSION = "com.google.android.c2dm.permission.RECEIVE";
+ private static final String RECEIVE_ACTION = "com.google.android.c2dm.intent.RECEIVE";
+ private static final String REGISTRATION_ACTION = "com.google.android.c2dm.intent.REGISTRATION";
+ private static final String INSTANCE_ID_ACTION = "com.google.android.gms.iid.InstanceID";
+ private static final String PUSH_LISTENER_SERVICE = "com.leanplum.LeanplumPushListenerService";
+ private static final String GCM_RECEIVER = "com.google.android.gms.gcm.GcmReceiver";
+ private static final String PUSH_INSTANCE_ID_SERVICE =
+ "com.leanplum.LeanplumPushInstanceIDService";
+
+ private static String senderIds;
+
+ static void setSenderId(String senderId) {
+ senderIds = senderId;
+ }
+
+ /**
+ * Stores the GCM sender ID in the application's {@code SharedPreferences}.
+ *
+ * @param context application's context.
+ */
+ @Override
+ public void storePreferences(Context context) {
+ super.storePreferences(context);
+ Log.v("Saving GCM sender ID");
+ SharedPreferencesUtil.setString(context, Constants.Defaults.LEANPLUM_PUSH,
+ Constants.Defaults.PROPERTY_SENDER_IDS, senderIds);
+ }
+
+ public String getRegistrationId() {
+ String registrationId = null;
+ try {
+ InstanceID instanceID = InstanceID.getInstance(Leanplum.getContext());
+ if (senderIds == null || instanceID == null) {
+ Log.w("There was a problem setting up GCM, please make sure you follow instructions " +
+ "on how to set it up.");
+ return null;
+ }
+ registrationId = instanceID.getToken(senderIds,
+ GoogleCloudMessaging.INSTANCE_ID_SCOPE, null);
+ } catch (IOException e) {
+ if (GoogleCloudMessaging.ERROR_SERVICE_NOT_AVAILABLE.equals(e.getMessage())) {
+ Log.w("GCM service is not available. Will try to " +
+ "register again next time the app starts.");
+ } else if (ERROR_TIMEOUT.equals(e.getMessage())) {
+ Log.w("Retrieval of GCM registration token timed out. " +
+ "Will try to register again next time the app starts.");
+ } else if (ERROR_INVALID_SENDER.equals(e.getMessage())) {
+ Log.e("The GCM sender account is not recognized. Please be " +
+ "sure to call LeanplumPushService.setGsmSenderId() with a valid GCM sender id.");
+ } else if (ERROR_AUTHENTICATION_FAILED.equals(e.getMessage())) {
+ Log.w("Bad Google Account password.");
+ } else if (ERROR_PHONE_REGISTRATION_ERROR.equals(e.getMessage())) {
+ Log.w("This phone doesn't currently support GCM.");
+ } else if (ERROR_TOO_MANY_REGISTRATIONS.equals(e.getMessage())) {
+ Log.w("This phone has more than the allowed number of " +
+ "apps that are registered with GCM.");
+ } else {
+ Log.e("Failed to complete registration token refresh.");
+ Util.handleException(e);
+ }
+ } catch (Throwable t) {
+ Log.w("There was a problem setting up GCM, please make sure you follow instructions " +
+ "on how to set it up. Please verify that you are using correct version of " +
+ "Google Play Services and Android Support Library v4.");
+ Util.handleException(t);
+ }
+ return registrationId;
+ }
+
+ public boolean isInitialized() {
+ return senderIds != null || getCurrentRegistrationId() != null;
+ }
+
+ public boolean isManifestSetUp() {
+ Context context = Leanplum.getContext();
+ if (context == null) {
+ return false;
+ }
+
+ boolean hasPermissions = LeanplumManifestHelper.checkPermission(RECEIVE_PERMISSION, false, true)
+ && (LeanplumManifestHelper.checkPermission(context.getPackageName() +
+ ".gcm.permission.C2D_MESSAGE", true, false) || LeanplumManifestHelper.checkPermission(
+ context.getPackageName() + ".permission.C2D_MESSAGE", true, true));
+
+ boolean hasGcmReceiver = LeanplumManifestHelper.checkComponent(
+ LeanplumManifestHelper.getReceivers(), GCM_RECEIVER, true, SEND_PERMISSION,
+ Arrays.asList(RECEIVE_ACTION, REGISTRATION_ACTION), context.getPackageName());
+ boolean hasPushReceiver = LeanplumManifestHelper.checkComponent(
+ LeanplumManifestHelper.getReceivers(), PUSH_RECEIVER, false, null,
+ Collections.singletonList(PUSH_LISTENER_SERVICE), null);
+
+ boolean hasReceivers = hasGcmReceiver && hasPushReceiver;
+
+ boolean hasPushListenerService = LeanplumManifestHelper.checkComponent(
+ LeanplumManifestHelper.getServices(), PUSH_LISTENER_SERVICE, false, null,
+ Collections.singletonList(RECEIVE_ACTION), null);
+ boolean hasPushInstanceIDService = LeanplumManifestHelper.checkComponent(
+ LeanplumManifestHelper.getServices(), PUSH_INSTANCE_ID_SERVICE, false, null,
+ Collections.singletonList(INSTANCE_ID_ACTION), null);
+ boolean hasPushRegistrationService = LeanplumManifestHelper.checkComponent(
+ LeanplumManifestHelper.getServices(), PUSH_REGISTRATION_SERVICE, false, null, null, null);
+
+ boolean hasServices = hasPushListenerService && hasPushInstanceIDService
+ && hasPushRegistrationService;
+
+ return hasPermissions && hasReceivers && hasServices;
+ }
+
+ /**
+ * Unregister from GCM.
+ */
+ public void unregister() {
+ try {
+ InstanceID.getInstance(Leanplum.getContext()).deleteInstanceID();
+ Log.i("Application was unregistred from GCM.");
+ } catch (Exception e) {
+ Log.e("Failed to unregister from GCM.");
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumInbox.java
@@ -0,0 +1,405 @@
+/*
+ * Copyright 2017, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import com.leanplum.callbacks.InboxChangedCallback;
+import com.leanplum.callbacks.VariablesChangedCallback;
+import com.leanplum.internal.AESCrypt;
+import com.leanplum.internal.CollectionUtil;
+import com.leanplum.internal.Constants;
+import com.leanplum.internal.JsonConverter;
+import com.leanplum.internal.Log;
+import com.leanplum.internal.OsHandler;
+import com.leanplum.internal.Request;
+import com.leanplum.internal.Util;
+
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Inbox class.
+ *
+ * @author Aleksandar Gyorev, Anna Orlova
+ */
+public class LeanplumInbox {
+ static boolean isInboxImagePrefetchingEnabled = true;
+ /**
+ * Should be like this until Newsfeed is removed for backward compatibility.
+ */
+ static Newsfeed instance = new Newsfeed();
+ static Set<String> downloadedImageUrls;
+
+ // Inbox properties.
+ private int unreadCount;
+ private Map<String, LeanplumInboxMessage> messages;
+ private boolean didLoad = false;
+ private List<InboxChangedCallback> changedCallbacks;
+ private Object updatingLock = new Object();
+
+ LeanplumInbox() {
+ this.unreadCount = 0;
+ this.messages = new HashMap<>();
+ this.didLoad = false;
+ this.changedCallbacks = new ArrayList<>();
+ downloadedImageUrls = new HashSet<>();
+ }
+
+ /**
+ * Static 'getInstance' method.
+ */
+ static LeanplumInbox getInstance() {
+ return instance;
+ }
+
+ /**
+ * Disable prefetching images.
+ */
+ public static void disableImagePrefetching() {
+ isInboxImagePrefetchingEnabled = false;
+ }
+
+ boolean isInboxImagePrefetchingEnabled() {
+ return isInboxImagePrefetchingEnabled;
+ }
+
+ void updateUnreadCount(int unreadCount) {
+ this.unreadCount = unreadCount;
+ save();
+ triggerChanged();
+ }
+
+ void update(Map<String, LeanplumInboxMessage> messages, int unreadCount, boolean shouldSave) {
+ try {
+ synchronized (updatingLock) {
+ this.unreadCount = unreadCount;
+ if (messages != null) {
+ this.messages = messages;
+ }
+ }
+ this.didLoad = true;
+ if (shouldSave) {
+ save();
+ }
+ triggerChanged();
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ }
+
+ void removeMessage(String messageId) {
+ int unreadCount = this.unreadCount;
+ LeanplumInboxMessage message = messageForId(messageId);
+ if (message != null && !message.isRead()) {
+ unreadCount--;
+ }
+
+ messages.remove(messageId);
+ update(messages, unreadCount, true);
+
+ if (Constants.isNoop()) {
+ return;
+ }
+
+ Map<String, Object> params = new HashMap<>();
+ params.put(Constants.Params.INBOX_MESSAGE_ID, messageId);
+ Request req = Request.post(Constants.Methods.DELETE_INBOX_MESSAGE, params);
+ req.send();
+ }
+
+ void triggerChanged() {
+ synchronized (changedCallbacks) {
+ for (InboxChangedCallback callback : changedCallbacks) {
+ OsHandler.getInstance().post(callback);
+ }
+ }
+ }
+
+ void load() {
+ if (Constants.isNoop()) {
+ return;
+ }
+ Context context = Leanplum.getContext();
+ SharedPreferences defaults = context.getSharedPreferences(
+ "__leanplum__", Context.MODE_PRIVATE);
+ if (Request.token() == null) {
+ update(new HashMap<String, LeanplumInboxMessage>(), 0, false);
+ return;
+ }
+ int unreadCount = 0;
+ AESCrypt aesContext = new AESCrypt(Request.appId(), Request.token());
+ String newsfeedString = aesContext.decodePreference(
+ defaults, Constants.Defaults.INBOX_KEY, "{}");
+ Map<String, Object> newsfeed = JsonConverter.fromJson(newsfeedString);
+
+ Map<String, LeanplumInboxMessage> messages = new HashMap<>();
+ if (newsfeed == null) {
+ Log.e("Could not parse newsfeed string: " + newsfeedString);
+ } else {
+ for (Map.Entry<String, Object> entry : newsfeed.entrySet()) {
+ String messageId = entry.getKey();
+ Map<String, Object> data = CollectionUtil.uncheckedCast(entry.getValue());
+ LeanplumInboxMessage message = LeanplumInboxMessage.createFromJsonMap(messageId, data);
+
+ if (message != null && message.isActive()) {
+ messages.put(messageId, message);
+ if (!message.isRead()) {
+ unreadCount++;
+ }
+ }
+ }
+ }
+
+ update(messages, unreadCount, false);
+ }
+
+ void save() {
+ if (Constants.isNoop()) {
+ return;
+ }
+ if (Request.token() == null) {
+ return;
+ }
+ Context context = Leanplum.getContext();
+ SharedPreferences defaults = context.getSharedPreferences(
+ "__leanplum__", Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = defaults.edit();
+ Map<String, Object> messages = new HashMap<>();
+ for (Map.Entry<String, LeanplumInboxMessage> entry : this.messages.entrySet()) {
+ String messageId = entry.getKey();
+ NewsfeedMessage newsfeedMessage = entry.getValue();
+ Map<String, Object> data = newsfeedMessage.toJsonMap();
+ messages.put(messageId, data);
+ }
+ String messagesJson = JsonConverter.toJson(messages);
+ AESCrypt aesContext = new AESCrypt(Request.appId(), Request.token());
+ editor.putString(Constants.Defaults.INBOX_KEY, aesContext.encrypt(messagesJson));
+ try {
+ editor.apply();
+ } catch (NoSuchMethodError e) {
+ editor.commit();
+ }
+ }
+
+ void downloadMessages() {
+ if (Constants.isNoop()) {
+ return;
+ }
+
+ Request req = Request.post(Constants.Methods.GET_INBOX_MESSAGES, null);
+ req.onResponse(new Request.ResponseCallback() {
+ @Override
+ public void response(JSONObject responses) {
+ try {
+ JSONObject response = Request.getLastResponse(responses);
+ if (response == null) {
+ Log.e("No inbox response received from the server.");
+ return;
+ }
+
+ JSONObject messagesDict = response.optJSONObject(Constants.Keys.INBOX_MESSAGES);
+ if (messagesDict == null) {
+ Log.e("No inbox messages found in the response from the server.", response);
+ return;
+ }
+ int unreadCount = 0;
+ final Map<String, LeanplumInboxMessage> messages = new HashMap<>();
+ Boolean willDownladImages = false;
+
+ for (Iterator iterator = messagesDict.keys(); iterator.hasNext(); ) {
+ String messageId = (String) iterator.next();
+ JSONObject messageDict = messagesDict.getJSONObject(messageId);
+
+ Map<String, Object> actionArgs = JsonConverter.mapFromJson(
+ messageDict.getJSONObject(Constants.Keys.MESSAGE_DATA).getJSONObject(Constants.Keys.VARS)
+ );
+ Long deliveryTimestamp = messageDict.getLong(Constants.Keys.DELIVERY_TIMESTAMP);
+ Long expirationTimestamp = null;
+ if (messageDict.opt(Constants.Keys.EXPIRATION_TIMESTAMP) != null) {
+ expirationTimestamp = messageDict.getLong(Constants.Keys.EXPIRATION_TIMESTAMP);
+ }
+ boolean isRead = messageDict.getBoolean(Constants.Keys.IS_READ);
+ LeanplumInboxMessage message = LeanplumInboxMessage.constructMessage(messageId,
+ deliveryTimestamp, expirationTimestamp, isRead, actionArgs);
+ if (message != null) {
+ willDownladImages |= message.downloadImageIfPrefetchingEnabled();
+ if (!isRead) {
+ unreadCount++;
+ }
+ messages.put(messageId, message);
+ }
+ }
+
+ if (!willDownladImages) {
+ update(messages, unreadCount, true);
+ return;
+ }
+
+ final int totalUnreadCount = unreadCount;
+ Leanplum.addOnceVariablesChangedAndNoDownloadsPendingHandler(
+ new VariablesChangedCallback() {
+ @Override
+ public void variablesChanged() {
+ update(messages, totalUnreadCount, true);
+ }
+ });
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ }
+ });
+ req.sendIfConnected();
+ }
+
+ /**
+ * Returns the number of all inbox messages on the device.
+ */
+ public int count() {
+ return messages.size();
+ }
+
+ /**
+ * Returns the number of the unread inbox messages on the device.
+ */
+ public int unreadCount() {
+ return unreadCount;
+ }
+
+ /**
+ * Returns the identifiers of all inbox messages on the device sorted in ascending
+ * chronological order, i.e. the id of the oldest message is the first one, and the most recent
+ * one is the last one in the array.
+ */
+ public List<String> messagesIds() {
+ List<String> messageIds = new ArrayList<>(messages.keySet());
+ try {
+ Collections.sort(messageIds, new Comparator<String>() {
+ @Override
+ public int compare(String firstMessage, String secondMessage) {
+ Date firstDate = messageForId(firstMessage).getDeliveryTimestamp();
+ Date secondDate = messageForId(secondMessage).getDeliveryTimestamp();
+ return firstDate.compareTo(secondDate);
+ }
+ });
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ return messageIds;
+ }
+
+ /**
+ * Have to stay as is because of backward compatibility + generics super-sub incompatibility
+ * (http://www.angelikalanger.com/GenericsFAQ/FAQSections/ParameterizedTypes.html#Topic2).
+ * <p>
+ * Returns a List containing all of the newsfeed messages sorted chronologically ascending (i.e.
+ * the oldest first and the newest last).
+ */
+ public List<NewsfeedMessage> allMessages() {
+ return allMessages(new ArrayList<NewsfeedMessage>());
+ }
+
+ /**
+ * Have to stay as is because of backward compatibility + generics super-sub incompatibility
+ * (http://www.angelikalanger.com/GenericsFAQ/FAQSections/ParameterizedTypes.html#Topic2).
+ * <p>
+ * Returns a List containing all of the unread newsfeed messages sorted chronologically ascending
+ * (i.e. the oldest first and the newest last).
+ */
+ public List<NewsfeedMessage> unreadMessages() {
+ return unreadMessages(new ArrayList<NewsfeedMessage>());
+ }
+
+ /**
+ * Suggested workaround for generics to be used with {@link LeanplumInbox#getInstance()} although
+ * only LeanplumInboxMessage could be an instance of NewsfeedMessage.
+ */
+ private <T extends NewsfeedMessage> List<T> allMessages(List<T> messages) {
+ if (messages == null) {
+ messages = new ArrayList<>();
+ }
+ try {
+ for (String messageId : messagesIds()) {
+ messages.add((T) messageForId(messageId));
+ }
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ return messages;
+ }
+
+ /**
+ * Suggested workaround for generics to be used with {@link LeanplumInbox#getInstance()} although
+ * only LeanplumInboxMessage could be an instance of NewsfeedMessage.
+ */
+ private <T extends NewsfeedMessage> List<T> unreadMessages(List<T> unreadMessages) {
+ if (unreadMessages == null) {
+ unreadMessages = new ArrayList<>();
+ }
+ List<LeanplumInboxMessage> messages = allMessages(null);
+ for (LeanplumInboxMessage message : messages) {
+ if (!message.isRead()) {
+ unreadMessages.add((T) message);
+ }
+ }
+ return unreadMessages;
+ }
+
+ /**
+ * Returns the inbox messages associated with the given getMessageId identifier.
+ */
+ public LeanplumInboxMessage messageForId(String messageId) {
+ return messages.get(messageId);
+ }
+
+ /**
+ * Add a callback for when the inbox receives new values from the server.
+ */
+ public void addChangedHandler(InboxChangedCallback handler) {
+ synchronized (changedCallbacks) {
+ changedCallbacks.add(handler);
+ }
+ if (this.didLoad) {
+ handler.inboxChanged();
+ }
+ }
+
+ /**
+ * Removes a inbox changed callback.
+ */
+ public void removeChangedHandler(InboxChangedCallback handler) {
+ synchronized (changedCallbacks) {
+ changedCallbacks.remove(handler);
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumInboxMessage.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2017, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import android.net.Uri;
+import android.text.TextUtils;
+
+import com.leanplum.internal.CollectionUtil;
+import com.leanplum.internal.Constants;
+import com.leanplum.internal.Log;
+import com.leanplum.internal.Util;
+
+import org.json.JSONObject;
+
+import java.io.File;
+import java.util.Map;
+
+import static com.leanplum.internal.FileManager.DownloadFileResult;
+import static com.leanplum.internal.FileManager.fileExistsAtPath;
+import static com.leanplum.internal.FileManager.fileValue;
+import static com.leanplum.internal.FileManager.maybeDownloadFile;
+
+/**
+ * LeanplumInboxMessage class.
+ *
+ * @author Anna Orlova
+ */
+public class LeanplumInboxMessage extends NewsfeedMessage {
+ private String imageUrl;
+ private String imageFileName;
+
+ private LeanplumInboxMessage(String messageId, Long deliveryTimestamp, Long expirationTimestamp,
+ boolean isRead, ActionContext context) {
+ super(messageId, deliveryTimestamp, expirationTimestamp, isRead, context);
+ imageUrl = context.stringNamed(Constants.Keys.INBOX_IMAGE);
+ if (imageUrl != null) {
+ try {
+ imageFileName = Util.sha256(imageUrl);
+ } catch (Exception ignored) {
+ }
+ }
+ }
+
+ static LeanplumInboxMessage createFromJsonMap(String messageId, Map<String, Object> map) {
+ Map<String, Object> messageData = CollectionUtil.uncheckedCast(map.get(Constants.Keys
+ .MESSAGE_DATA));
+ Long deliveryTimestamp = CollectionUtil.uncheckedCast(map.get(Constants.Keys
+ .DELIVERY_TIMESTAMP));
+ Long expirationTimestamp = CollectionUtil.uncheckedCast(map.get(Constants.Keys
+ .EXPIRATION_TIMESTAMP));
+ Boolean isRead = CollectionUtil.uncheckedCast(map.get(Constants.Keys.IS_READ));
+ return constructMessage(messageId, deliveryTimestamp, expirationTimestamp,
+ isRead != null ? isRead : false, messageData);
+ }
+
+ static LeanplumInboxMessage constructMessage(String messageId, Long deliveryTimestamp,
+ Long expirationTimestamp, boolean isRead, Map<String, Object> actionArgs) {
+ if (!isValidMessageId(messageId)) {
+ Log.e("Malformed inbox messageId: " + messageId);
+ return null;
+ }
+
+ String[] messageIdParts = messageId.split("##");
+ ActionContext context = new ActionContext((String) actionArgs.get(Constants.Values.ACTION_ARG),
+ actionArgs, messageIdParts[0]);
+ context.preventRealtimeUpdating();
+ context.update();
+ return new LeanplumInboxMessage(messageId, deliveryTimestamp, expirationTimestamp, isRead,
+ context);
+ }
+
+ /**
+ * Returns the image file path of the inbox message. Can be null.
+ */
+ public String getImageFilePath() {
+ String path = fileValue(imageFileName);
+ if (fileExistsAtPath(path)) {
+ return new File(path).getAbsolutePath();
+ }
+ if (!LeanplumInbox.getInstance().isInboxImagePrefetchingEnabled()) {
+ Log.w("Inbox Message image path is null because you're calling disableImagePrefetching. " +
+ "Consider using imageURL method or remove disableImagePrefetching.");
+ }
+ return null;
+ }
+
+ /**
+ * Returns the image Uri of the inbox message.
+ * You can safely use this with prefetching enabled.
+ * It will return the file Uri path instead if the image is in cache.
+ */
+ public Uri getImageUrl() {
+ String path = fileValue(imageFileName);
+ if (fileExistsAtPath(path)) {
+ return Uri.fromFile(new File(path));
+ }
+ if (TextUtils.isEmpty(imageUrl)) {
+ return null;
+ }
+
+ return Uri.parse(imageUrl);
+ }
+
+ /**
+ * Returns the data of the inbox message. Advanced use only.
+ */
+ public JSONObject getData() {
+ JSONObject object = null;
+ try {
+ String dataString = getContext().stringNamed(Constants.Keys.DATA);
+ if (!TextUtils.isEmpty(dataString)) {
+ object = new JSONObject(dataString);
+ }
+ } catch (Exception e) {
+ Log.w("Unable to parse JSONObject for Data field of inbox message.");
+ }
+ return object;
+ }
+
+ /**
+ * Download image if prefetching is enabled.
+ * Uses {@link LeanplumInbox#downloadedImageUrls} to make sure we don't call fileExist method
+ * multiple times for same URLs.
+ *
+ * @return Boolean True if the image will be downloaded, otherwise false.
+ */
+ Boolean downloadImageIfPrefetchingEnabled() {
+ if (!LeanplumInbox.isInboxImagePrefetchingEnabled) {
+ return false;
+ }
+
+ if (TextUtils.isEmpty(imageUrl) || LeanplumInbox.downloadedImageUrls.contains(imageUrl)) {
+ return false;
+ }
+
+ DownloadFileResult result = maybeDownloadFile(true, imageFileName,
+ imageUrl, imageUrl, null);
+ LeanplumInbox.downloadedImageUrls.add(imageUrl);
+ return DownloadFileResult.DOWNLOADING == result;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumInflater.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.XmlResourceParser;
+import android.view.InflateException;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.leanplum.internal.Log;
+import com.leanplum.internal.Util;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Inflates layout files that may be overridden by other files.
+ *
+ * @author Andrew First
+ */
+public class LeanplumInflater {
+ private Context context;
+ private LeanplumResources res;
+
+ public static LeanplumInflater from(Context context) {
+ return new LeanplumInflater(context);
+ }
+
+ private LeanplumInflater(Context context) {
+ this.context = context;
+ }
+
+ public LeanplumResources getLeanplumResources() {
+ return getLeanplumResources(null);
+ }
+
+ public LeanplumResources getLeanplumResources(Resources baseResources) {
+ if (res != null) {
+ return res;
+ }
+ if (baseResources == null) {
+ baseResources = context.getResources();
+ }
+ if (baseResources instanceof LeanplumResources) {
+ return (LeanplumResources) baseResources;
+ }
+ res = new LeanplumResources(baseResources);
+ return res;
+ }
+
+ /**
+ * Creates a view from the corresponding resource ID.
+ */
+ public View inflate(int layoutResID) {
+ return inflate(layoutResID, null, false);
+ }
+
+ /**
+ * Creates a view from the corresponding resource ID.
+ */
+ public View inflate(int layoutResID, ViewGroup root) {
+ return inflate(layoutResID, root, root != null);
+ }
+
+ /**
+ * Creates a view from the corresponding resource ID.
+ */
+ public View inflate(int layoutResID, ViewGroup root, boolean attachToRoot) {
+ Var<String> var;
+ try {
+ LeanplumResources res = getLeanplumResources(context.getResources());
+ var = res.getOverrideResource(layoutResID);
+ if (var == null || var.stringValue.equals(var.defaultValue())) {
+ return LayoutInflater.from(context).inflate(layoutResID, root, attachToRoot);
+ }
+ int overrideResId = var.overrideResId();
+ if (overrideResId != 0) {
+ return LayoutInflater.from(context).inflate(overrideResId, root, attachToRoot);
+ }
+ } catch (Throwable t) {
+ if (!(t instanceof InflateException)) {
+ Util.handleException(t);
+ }
+ return LayoutInflater.from(context).inflate(layoutResID, root, attachToRoot);
+ }
+
+ InputStream stream = null;
+
+ try {
+ ByteArrayOutputStream fileData = new ByteArrayOutputStream();
+ stream = var.stream();
+ byte[] buffer = new byte[8192];
+ int bytesRead;
+ while ((bytesRead = stream.read(buffer)) > -1) {
+ fileData.write(buffer, 0, bytesRead);
+ }
+ Object xmlBlock = Class.forName("android.content.res.XmlBlock").getConstructor(
+ byte[].class).newInstance((Object) fileData.toByteArray());
+ XmlResourceParser parser = null;
+ try {
+ parser = (XmlResourceParser) xmlBlock.getClass().getMethod(
+ "newParser").invoke(xmlBlock);
+ return LayoutInflater.from(context).inflate(parser, root, attachToRoot);
+ } catch (Throwable t) {
+ throw new RuntimeException(t);
+ } finally {
+ if (parser != null) {
+ parser.close();
+ }
+ }
+ } catch (Throwable t) {
+ Log.e("Could not inflate resource " + layoutResID + ":" + var.stringValue(), t);
+ } finally {
+ if (stream != null) {
+ try {
+ stream.close();
+ } catch (IOException e) {
+ Log.e("Failed to close input stream.");
+ }
+ }
+ }
+ return LayoutInflater.from(context).inflate(layoutResID, root, attachToRoot);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumLocalPushListenerService.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2016, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import android.app.IntentService;
+import android.content.Intent;
+import android.os.Bundle;
+
+import com.leanplum.internal.Constants;
+import com.leanplum.internal.Log;
+import com.leanplum.internal.Util;
+
+/**
+ * Listener Service for local push notifications.
+ *
+ * @author Aleksandar Gyorev
+ */
+public class LeanplumLocalPushListenerService extends IntentService {
+ public LeanplumLocalPushListenerService() {
+ super("LeanplumLocalPushListenerService");
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ try {
+ if (intent == null) {
+ Log.e("The intent cannot be null");
+ return;
+ }
+ Bundle extras = intent.getExtras();
+ if (!extras.isEmpty() && extras.containsKey(Constants.Keys.PUSH_MESSAGE_TEXT)) {
+ LeanplumPushService.handleNotification(this, extras);
+ }
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumLocationAccuracyType.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2017, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+/**
+ * LeanplumLocationAccuracyType enum used for Leanplum.setUserLocationAttribute.
+ *
+ * @author Alexis Oyama
+ */
+public enum LeanplumLocationAccuracyType {
+ /**
+ * Lowest accuracy. Reserved for internal use.
+ */
+ IP(0),
+
+ /**
+ * Default accuracy.
+ */
+ CELL(1),
+
+ /**
+ * Highest accuracy.
+ */
+ GPS(2);
+
+ private int value;
+
+ LeanplumLocationAccuracyType(int value) {
+ this.value = value;
+ }
+
+ public int value() {
+ return value;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumManualProvider.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2016, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import android.content.Context;
+
+/**
+ * Leanplum provider for manually registering for Cloud Messaging services.
+ *
+ * @author Anna Orlova
+ */
+public class LeanplumManualProvider extends LeanplumCloudMessagingProvider {
+ LeanplumManualProvider(Context context, String registrationId) {
+ onRegistrationIdReceived(context, registrationId);
+ }
+
+ public String getRegistrationId() {
+ return getCurrentRegistrationId();
+ }
+
+ public boolean isInitialized() {
+ return true;
+ }
+
+ public boolean isManifestSetUp() {
+ return true;
+ }
+
+ public void unregister() {
+
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumPushInstanceIDService.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2016, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import android.content.Intent;
+
+import com.google.android.gms.iid.InstanceIDListenerService;
+import com.leanplum.internal.Log;
+
+/**
+ * GCM InstanceID listener service to handle creation, rotation, and updating of registration
+ * tokens.
+ *
+ * @author Aleksandar Gyorev
+ */
+public class LeanplumPushInstanceIDService extends InstanceIDListenerService {
+ /**
+ * Called if InstanceID token is updated. This may occur if the security of the previous token had
+ * been compromised. This call is initiated by the InstanceID provider.
+ */
+ @Override
+ public void onTokenRefresh() {
+ Log.i("GCM InstanceID token needs an update");
+ // Fetch updated Instance ID token and notify our app's server of any changes (if applicable).
+ Intent intent = new Intent(this, LeanplumPushRegistrationService.class);
+ startService(intent);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumPushListenerService.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2016, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import android.os.Bundle;
+
+import com.google.android.gms.gcm.GcmListenerService;
+import com.leanplum.internal.Constants.Keys;
+import com.leanplum.internal.Log;
+import com.leanplum.internal.Util;
+
+/**
+ * GCM listener service, which enables handling messages on the app's behalf.
+ *
+ * @author Aleksandar Gyorev
+ */
+public class LeanplumPushListenerService extends GcmListenerService {
+ /**
+ * Called when a message is received.
+ *
+ * @param senderId Sender ID of the sender.
+ * @param data Data bundle containing the message data as key-value pairs.
+ */
+ @Override
+ public void onMessageReceived(String senderId, Bundle data) {
+ try {
+ if (data.containsKey(Keys.PUSH_MESSAGE_TEXT)) {
+ LeanplumPushService.handleNotification(this, data);
+ }
+ Log.i("Received: " + data.toString());
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumPushNotificationCustomizer.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2015, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import android.os.Bundle;
+import android.support.v4.app.NotificationCompat;
+
+/**
+ * Implement LeanplumPushNotificationCustomizer to customize the appearance of notifications.
+ */
+public interface LeanplumPushNotificationCustomizer {
+ void customize(NotificationCompat.Builder builder, Bundle notificationPayload);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumPushReceiver.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2016, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+
+import com.leanplum.internal.Log;
+import com.leanplum.internal.Util;
+
+/**
+ * Handles push notification intents, for example, by tracking opens and performing the open
+ * action.
+ *
+ * @author Aleksandar Gyorev
+ */
+public class LeanplumPushReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ try {
+ if (intent == null) {
+ Log.e("Received a null intent.");
+ return;
+ }
+ LeanplumPushService.openNotification(context, intent.getExtras());
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumPushRegistrationService.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2016, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import android.app.IntentService;
+import android.content.Intent;
+
+import com.leanplum.internal.Log;
+
+/**
+ * Registration service that handles registration with the GCM and FCM, using
+ * InstanceID.
+ *
+ * @author Aleksandar Gyorev
+ */
+public class LeanplumPushRegistrationService extends IntentService {
+ private static String existingRegistrationId;
+
+ public LeanplumPushRegistrationService() {
+ super("LeanplumPushRegistrationService");
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ LeanplumCloudMessagingProvider provider = LeanplumPushService.getCloudMessagingProvider();
+ if (provider == null) {
+ Log.e("Failed to complete registration token refresh.");
+ return;
+ }
+ String registrationId = provider.getRegistrationId();
+ if (registrationId != null) {
+ if (existingRegistrationId != null && !registrationId.equals(existingRegistrationId)) {
+ Log.e("WARNING: It appears your app is registering " +
+ "with GCM/FCM using multiple GCM/FCM sender ids. Please be sure to call " +
+ "LeanplumPushService.setGcmSenderIds() with " +
+ "all of the GCM sender ids that you use, not just the one that you use with " +
+ "Leanplum. Otherwise, GCM/FCM push notifications may not work consistently.");
+ }
+ existingRegistrationId = registrationId;
+ provider.onRegistrationIdReceived(getApplicationContext(), registrationId);
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumPushService.java
@@ -0,0 +1,776 @@
+/*
+ * Copyright 2014, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import android.app.Activity;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.v4.app.NotificationCompat;
+import android.text.TextUtils;
+
+import com.leanplum.callbacks.VariablesChangedCallback;
+import com.leanplum.internal.ActionManager;
+import com.leanplum.internal.Constants;
+import com.leanplum.internal.Constants.Keys;
+import com.leanplum.internal.Constants.Methods;
+import com.leanplum.internal.Constants.Params;
+import com.leanplum.internal.JsonConverter;
+import com.leanplum.internal.LeanplumInternal;
+import com.leanplum.internal.Log;
+import com.leanplum.internal.Request;
+import com.leanplum.internal.Util;
+import com.leanplum.internal.VarCache;
+import com.leanplum.utils.BitmapUtil;
+import com.leanplum.utils.SharedPreferencesUtil;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
+
+/**
+ * Leanplum push notification service class, handling initialization, opening, showing, integration
+ * verification and registration for push notifications.
+ *
+ * @author Andrew First, Anna Orlova
+ */
+public class LeanplumPushService {
+ /**
+ * Leanplum's built-in Google Cloud Messaging sender ID.
+ */
+ public static final String LEANPLUM_SENDER_ID = "44059457771";
+ private static final String LEANPLUM_PUSH_FCM_LISTENER_SERVICE_CLASS =
+ "com.leanplum.LeanplumPushFcmListenerService";
+ private static final String PUSH_FIREBASE_MESSAGING_SERVICE_CLASS =
+ "com.leanplum.LeanplumPushFirebaseMessagingService";
+ private static final String LEANPLUM_PUSH_INSTANCE_ID_SERVICE_CLASS =
+ "com.leanplum.LeanplumPushInstanceIDService";
+ private static final String LEANPLUM_PUSH_LISTENER_SERVICE_CLASS =
+ "com.leanplum.LeanplumPushListenerService";
+ private static final String GCM_RECEIVER_CLASS = "com.google.android.gms.gcm.GcmReceiver";
+
+ private static Class<? extends Activity> callbackClass;
+ private static LeanplumCloudMessagingProvider provider;
+ private static boolean isFirebaseEnabled = false;
+ private static final int NOTIFICATION_ID = 1;
+
+ private static final String OPEN_URL = "Open URL";
+ private static final String URL = "URL";
+ private static final String OPEN_ACTION = "Open";
+ private static LeanplumPushNotificationCustomizer customizer;
+
+ /**
+ * Use Firebase Cloud Messaging, instead of the default Google Cloud Messaging.
+ */
+ public static void enableFirebase() {
+ LeanplumPushService.isFirebaseEnabled = true;
+ }
+
+ /**
+ * Whether Firebase Cloud Messaging is enabled or not.
+ *
+ * @return Boolean - true if enabled
+ */
+ static boolean isFirebaseEnabled() {
+ return isFirebaseEnabled;
+ }
+
+ /**
+ * Get Cloud Messaging provider. By default - GCM.
+ *
+ * @return LeanplumCloudMessagingProvider - current provider
+ */
+ static LeanplumCloudMessagingProvider getCloudMessagingProvider() {
+ return provider;
+ }
+
+ /**
+ * Changes the default activity to launch if the user opens a push notification.
+ *
+ * @param callbackClass The activity class.
+ */
+ public static void setDefaultCallbackClass(Class<? extends Activity> callbackClass) {
+ LeanplumPushService.callbackClass = callbackClass;
+ }
+
+ /**
+ * Sets an object used to customize the appearance of notifications. <p>Call this from your
+ * Application class's onCreate method so that the customizer is set when your application starts
+ * in the background.
+ */
+ public static void setCustomizer(LeanplumPushNotificationCustomizer customizer) {
+ LeanplumPushService.customizer = customizer;
+ }
+
+ /**
+ * Sets the Google Cloud Messaging/Firebase Cloud Messaging sender ID. Required for push
+ * notifications to work.
+ *
+ * @param senderId The GCM/FCM sender ID to permit notifications from. Use {@link
+ * LeanplumPushService#LEANPLUM_SENDER_ID} to use the built-in sender ID for GCM. If you have
+ * multiple sender IDs, use {@link LeanplumPushService#setGcmSenderIds}.
+ */
+ public static void setGcmSenderId(String senderId) {
+ LeanplumGcmProvider.setSenderId(senderId);
+ }
+
+ /**
+ * Sets the Google Cloud Messaging/Firebase Cloud Messaging sender ID. Required for push
+ * notifications to work.
+ *
+ * @param senderIds The GCM/FCM sender IDs to permit notifications from. Use {@link
+ * LeanplumPushService#LEANPLUM_SENDER_ID} to use the built-in sender ID.
+ */
+ public static void setGcmSenderIds(String... senderIds) {
+ StringBuilder joinedSenderIds = new StringBuilder();
+ for (String senderId : senderIds) {
+ if (joinedSenderIds.length() > 0) {
+ joinedSenderIds.append(',');
+ }
+ joinedSenderIds.append(senderId);
+ }
+ LeanplumGcmProvider.setSenderId(joinedSenderIds.toString());
+ }
+
+ private static Class<? extends Activity> getCallbackClass() {
+ return callbackClass;
+ }
+
+ private static boolean areActionsEmbedded(final Bundle message) {
+ return message.containsKey(Keys.PUSH_MESSAGE_ACTION);
+ }
+
+ private static void requireMessageContent(
+ final String messageId, final VariablesChangedCallback onComplete) {
+ Leanplum.addOnceVariablesChangedAndNoDownloadsPendingHandler(new VariablesChangedCallback() {
+ @Override
+ public void variablesChanged() {
+ try {
+ Map<String, Object> messages = VarCache.messages();
+ if (messageId == null || (messages != null && messages.containsKey(messageId))) {
+ onComplete.variablesChanged();
+ } else {
+ // Try downloading the messages again if it doesn't exist.
+ // Maybe the message was created while the app was running.
+ Map<String, Object> params = new HashMap<>();
+ params.put(Params.INCLUDE_DEFAULTS, Boolean.toString(false));
+ params.put(Params.INCLUDE_MESSAGE_ID, messageId);
+ Request req = Request.post(Methods.GET_VARS, params);
+ req.onResponse(new Request.ResponseCallback() {
+ @Override
+ public void response(JSONObject response) {
+ try {
+ JSONObject getVariablesResponse = Request.getLastResponse(response);
+ if (getVariablesResponse == null) {
+ Log.e("No response received from the server. Please contact us to " +
+ "investigate.");
+ } else {
+ Map<String, Object> values = JsonConverter.mapFromJson(
+ getVariablesResponse.optJSONObject(Constants.Keys.VARS));
+ Map<String, Object> messages = JsonConverter.mapFromJson(
+ getVariablesResponse.optJSONObject(Constants.Keys.MESSAGES));
+ Map<String, Object> regions = JsonConverter.mapFromJson(
+ getVariablesResponse.optJSONObject(Constants.Keys.REGIONS));
+ List<Map<String, Object>> variants = JsonConverter.listFromJson(
+ getVariablesResponse.optJSONArray(Constants.Keys.VARIANTS));
+ if (!Constants.canDownloadContentMidSessionInProduction ||
+ VarCache.getDiffs().equals(values)) {
+ values = null;
+ }
+ if (VarCache.getMessageDiffs().equals(messages)) {
+ messages = null;
+ }
+ if (values != null || messages != null) {
+ VarCache.applyVariableDiffs(values, messages, null, null, regions, variants);
+ }
+ }
+ onComplete.variablesChanged();
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ }
+ });
+ req.onError(new Request.ErrorCallback() {
+ @Override
+ public void error(Exception e) {
+ onComplete.variablesChanged();
+ }
+ });
+ req.sendIfConnected();
+ }
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ }
+ });
+ }
+
+ private static String getMessageId(Bundle message) {
+ String messageId = message.getString(Keys.PUSH_MESSAGE_ID_NO_MUTE_WITH_ACTION);
+ if (messageId == null) {
+ messageId = message.getString(Keys.PUSH_MESSAGE_ID_MUTE_WITH_ACTION);
+ if (messageId == null) {
+ messageId = message.getString(Keys.PUSH_MESSAGE_ID_NO_MUTE);
+ if (messageId == null) {
+ messageId = message.getString(Keys.PUSH_MESSAGE_ID_MUTE);
+ }
+ }
+ }
+ if (messageId != null) {
+ message.putString(Keys.PUSH_MESSAGE_ID, messageId);
+ }
+ return messageId;
+ }
+
+ static void handleNotification(final Context context, final Bundle message) {
+ if (LeanplumActivityHelper.currentActivity != null
+ && !LeanplumActivityHelper.isActivityPaused
+ && (message.containsKey(Keys.PUSH_MESSAGE_ID_MUTE_WITH_ACTION)
+ || message.containsKey(Keys.PUSH_MESSAGE_ID_MUTE))) {
+ // Mute notifications that have "Mute inside app" set if the app is open.
+ return;
+ }
+
+ final String messageId = LeanplumPushService.getMessageId(message);
+ if (messageId == null || !LeanplumInternal.hasCalledStart()) {
+ showNotification(context, message);
+ return;
+ }
+
+ // Can only track displays if we call Leanplum.start explicitly above where it says
+ // if (!Leanplum.calledStart). However, this is probably not worth it.
+ //
+ // Map<String, String> requestArgs = new HashMap<String, String>();
+ // requestArgs.put(Constants.Params.MESSAGE_ID, getMessageId);
+ // Leanplum.track("Displayed", 0.0, null, null, requestArgs);
+
+ showNotification(context, message);
+ }
+
+ /**
+ * Put the message into a notification and post it.
+ */
+ private static void showNotification(Context context, Bundle message) {
+ NotificationManager notificationManager = (NotificationManager)
+ context.getSystemService(Context.NOTIFICATION_SERVICE);
+
+ Intent intent = new Intent(context, LeanplumPushReceiver.class);
+ intent.addCategory("lpAction");
+ intent.putExtras(message);
+ PendingIntent contentIntent = PendingIntent.getBroadcast(
+ context.getApplicationContext(), new Random().nextInt(),
+ intent, 0);
+
+ String title = Util.getApplicationName(context.getApplicationContext());
+ if (message.getString("title") != null) {
+ title = message.getString("title");
+ }
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
+ .setSmallIcon(context.getApplicationInfo().icon)
+ .setContentTitle(title)
+ .setStyle(new NotificationCompat.BigTextStyle()
+ .bigText(message.getString(Keys.PUSH_MESSAGE_TEXT)))
+ .setContentText(message.getString(Keys.PUSH_MESSAGE_TEXT));
+
+ String imageUrl = message.getString(Keys.PUSH_MESSAGE_IMAGE_URL);
+ // BigPictureStyle support requires API 16 and higher.
+ if (!TextUtils.isEmpty(imageUrl) && Build.VERSION.SDK_INT >= 16) {
+ Bitmap bigPicture = BitmapUtil.getScaledBitmap(context, imageUrl);
+ if (bigPicture != null) {
+ builder.setStyle(new NotificationCompat.BigPictureStyle()
+ .bigPicture(bigPicture)
+ .setBigContentTitle(title)
+ .setSummaryText(message.getString(Keys.PUSH_MESSAGE_TEXT)));
+ } else {
+ Log.w(String.format("Image download failed for push notification with big picture. " +
+ "No image will be included with the push notification. Image URL: %s.", imageUrl));
+ }
+ }
+
+ // Try to put notification on top of notification area.
+ if (Build.VERSION.SDK_INT >= 16) {
+ builder.setPriority(Notification.PRIORITY_MAX);
+ }
+ builder.setAutoCancel(true);
+ builder.setContentIntent(contentIntent);
+
+ if (LeanplumPushService.customizer != null) {
+ LeanplumPushService.customizer.customize(builder, message);
+ }
+
+ int notificationId = LeanplumPushService.NOTIFICATION_ID;
+ Object notificationIdObject = message.get("lp_notificationId");
+ if (notificationIdObject instanceof Number) {
+ notificationId = ((Number) notificationIdObject).intValue();
+ } else if (notificationIdObject instanceof String) {
+ try {
+ notificationId = Integer.parseInt((String) notificationIdObject);
+ } catch (NumberFormatException e) {
+ notificationId = LeanplumPushService.NOTIFICATION_ID;
+ }
+ } else if (message.containsKey(Keys.PUSH_MESSAGE_ID)) {
+ String value = message.getString(Keys.PUSH_MESSAGE_ID);
+ if (value != null) {
+ notificationId = value.hashCode();
+ }
+ }
+ notificationManager.notify(notificationId, builder.build());
+ }
+
+ static void openNotification(Context context, final Bundle notification) {
+ Log.d("Opening push notification action.");
+ if (notification == null) {
+ Log.i("Received null Bundle.");
+ return;
+ }
+
+ // Checks if open action is "Open URL" and there is some activity that can handle intent.
+ if (isActivityWithIntentStarted(context, notification)) {
+ return;
+ }
+
+ // Start activity.
+ Class<? extends Activity> callbackClass = LeanplumPushService.getCallbackClass();
+ boolean shouldStartActivity = true;
+ if (LeanplumActivityHelper.currentActivity != null &&
+ !LeanplumActivityHelper.isActivityPaused) {
+ if (callbackClass == null) {
+ shouldStartActivity = false;
+ } else if (callbackClass.isInstance(LeanplumActivityHelper.currentActivity)) {
+ shouldStartActivity = false;
+ }
+ }
+
+ if (shouldStartActivity) {
+ Intent actionIntent = getActionIntent(context);
+ actionIntent.putExtras(notification);
+ actionIntent.addFlags(
+ Intent.FLAG_ACTIVITY_CLEAR_TOP |
+ Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(actionIntent);
+ }
+
+ // Perform action.
+ LeanplumActivityHelper.queueActionUponActive(new VariablesChangedCallback() {
+ @Override
+ public void variablesChanged() {
+ try {
+ final String messageId = LeanplumPushService.getMessageId(notification);
+ final String actionName = Constants.Values.DEFAULT_PUSH_ACTION;
+
+ // Make sure content is available.
+ if (messageId != null) {
+ if (LeanplumPushService.areActionsEmbedded(notification)) {
+ Map<String, Object> args = new HashMap<>();
+ args.put(actionName, JsonConverter.fromJson(
+ notification.getString(Keys.PUSH_MESSAGE_ACTION)));
+ ActionContext context = new ActionContext(
+ ActionManager.PUSH_NOTIFICATION_ACTION_NAME, args, messageId);
+ context.preventRealtimeUpdating();
+ context.update();
+ context.runTrackedActionNamed(actionName);
+ } else {
+ Leanplum.addOnceVariablesChangedAndNoDownloadsPendingHandler(
+ new VariablesChangedCallback() {
+ @Override
+ public void variablesChanged() {
+ try {
+ LeanplumPushService.requireMessageContent(messageId,
+ new VariablesChangedCallback() {
+ @Override
+ public void variablesChanged() {
+ try {
+ LeanplumInternal.performTrackedAction(actionName, messageId);
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ }
+ });
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ }
+ });
+ }
+ }
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ }
+ });
+ }
+
+ /**
+ * Return true if we found an activity to handle Intent and started it.
+ */
+ private static boolean isActivityWithIntentStarted(Context context, Bundle notification) {
+ String action = notification.getString(Keys.PUSH_MESSAGE_ACTION);
+ if (action != null && action.contains(OPEN_URL)) {
+ Intent deepLinkIntent = getDeepLinkIntent(notification);
+ if (deepLinkIntent != null && activityHasIntent(context, deepLinkIntent)) {
+ String messageId = LeanplumPushService.getMessageId(notification);
+ if (messageId != null) {
+ ActionContext actionContext = new ActionContext(
+ ActionManager.PUSH_NOTIFICATION_ACTION_NAME, null, messageId);
+ actionContext.track(OPEN_ACTION, 0.0, null);
+ context.startActivity(deepLinkIntent);
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Gets Intent from Push Notification Bundle.
+ */
+ private static Intent getDeepLinkIntent(Bundle notification) {
+ try {
+ String actionString = notification.getString(Keys.PUSH_MESSAGE_ACTION);
+ if (actionString != null) {
+ JSONObject openAction = new JSONObject(actionString);
+ Intent deepLinkIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(
+ openAction.getString(URL)));
+ deepLinkIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ return deepLinkIntent;
+ }
+ } catch (JSONException ignored) {
+ }
+ return null;
+ }
+
+ /**
+ * Checks if there is some activity that can handle intent.
+ */
+ private static Boolean activityHasIntent(Context context, Intent deepLinkIntent) {
+ List<ResolveInfo> resolveInfoList =
+ context.getPackageManager().queryIntentActivities(deepLinkIntent, 0);
+ if (resolveInfoList != null && !resolveInfoList.isEmpty()) {
+ for (ResolveInfo resolveInfo : resolveInfoList) {
+ if (resolveInfo != null && resolveInfo.activityInfo != null &&
+ resolveInfo.activityInfo.name != null) {
+ if (resolveInfo.activityInfo.name.contains(context.getPackageName())) {
+ // If url can be handled by current app - set package name to intent, so url will be
+ // open by current app. Skip chooser dialog.
+ deepLinkIntent.setPackage(resolveInfo.activityInfo.packageName);
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ private static Intent getActionIntent(Context context) {
+ Class<? extends Activity> callbackClass = LeanplumPushService.getCallbackClass();
+ if (callbackClass != null) {
+ return new Intent(context, callbackClass);
+ } else {
+ PackageManager pm = context.getPackageManager();
+ return pm.getLaunchIntentForPackage(context.getPackageName());
+ }
+ }
+
+ /**
+ * Unregisters the device from all GCM push notifications. You shouldn't need to call this method
+ * in production.
+ */
+ public static void unregister() {
+ try {
+ Intent unregisterIntent = new Intent("com.google.android.c2dm.intent.UNREGISTER");
+ Context context = Leanplum.getContext();
+ unregisterIntent.putExtra("app", PendingIntent.getBroadcast(context, 0, new Intent(), 0));
+ unregisterIntent.setPackage("com.google.android.gms");
+ context.startService(unregisterIntent);
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ }
+
+ /**
+ * Registers the application with GCM servers asynchronously.
+ * <p>
+ * Stores the registration ID and app versionCode in the application's shared preferences.
+ */
+ private static void registerInBackground() {
+ Context context = Leanplum.getContext();
+ if (context == null) {
+ Log.e("Failed to register application with GCM/FCM. Your application context is not set.");
+ return;
+ }
+ Intent registerIntent = new Intent(context, LeanplumPushRegistrationService.class);
+ context.startService(registerIntent);
+ }
+
+ /**
+ * Register manually for Google Cloud Messaging services.
+ *
+ * @param token The registration ID token or the instance ID security token.
+ */
+ public static void setGcmRegistrationId(String token) {
+ new LeanplumManualProvider(Leanplum.getContext().getApplicationContext(), token);
+ }
+
+ /**
+ * Call this when Leanplum starts.
+ */
+ static void onStart() {
+ try {
+ if (Util.hasPlayServices()) {
+ initPushService();
+ } else {
+ Log.i("No valid Google Play Services APK found.");
+ }
+ } catch (LeanplumException e) {
+ Log.e("There was an error registering for push notifications.\n" +
+ Log.getStackTraceString(e));
+ }
+ }
+
+ private static void initPushService() {
+ if (!enableServices()) {
+ return;
+ }
+ provider = new LeanplumGcmProvider();
+ if (!provider.isInitialized() || !provider.isManifestSetUp()) {
+ return;
+ }
+ if (hasAppIDChanged(Request.appId())) {
+ provider.unregister();
+ }
+ registerInBackground();
+ }
+
+
+ /**
+ * Enable Leanplum GCM or FCM services.
+ *
+ * @return True if services was enabled.
+ */
+ private static boolean enableServices() {
+ Context context = Leanplum.getContext();
+ if (context == null) {
+ return false;
+ }
+
+ PackageManager packageManager = context.getPackageManager();
+ if (packageManager == null) {
+ return false;
+ }
+
+ if (isFirebaseEnabled) {
+ Class fcmListenerClass = getClassForName(LEANPLUM_PUSH_FCM_LISTENER_SERVICE_CLASS);
+ if (fcmListenerClass == null) {
+ return false;
+ }
+
+ if (!wasComponentEnabled(context, packageManager, fcmListenerClass)) {
+ if (!enableServiceAndStart(context, packageManager, PUSH_FIREBASE_MESSAGING_SERVICE_CLASS)
+ || !enableServiceAndStart(context, packageManager, fcmListenerClass)) {
+ return false;
+ }
+ }
+ } else {
+ Class gcmPushInstanceIDClass = getClassForName(LEANPLUM_PUSH_INSTANCE_ID_SERVICE_CLASS);
+ if (gcmPushInstanceIDClass == null) {
+ return false;
+ }
+
+ if (!wasComponentEnabled(context, packageManager, gcmPushInstanceIDClass)) {
+ if (!enableComponent(context, packageManager, LEANPLUM_PUSH_LISTENER_SERVICE_CLASS) ||
+ !enableComponent(context, packageManager, gcmPushInstanceIDClass) ||
+ !enableComponent(context, packageManager, GCM_RECEIVER_CLASS)) {
+ return false;
+ }
+
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Gets Class for name.
+ *
+ * @param className - class name.
+ * @return Class for provided class name.
+ */
+ private static Class getClassForName(String className) {
+ try {
+ return Class.forName(className);
+ } catch (Throwable t) {
+ if (isFirebaseEnabled) {
+ Log.e("Please compile FCM library.");
+ } else {
+ Log.e("Please compile GCM library.");
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Enables and starts service for provided class name.
+ *
+ * @param context Current Context.
+ * @param packageManager Current PackageManager.
+ * @param className Name of Class that needs to be enabled and started.
+ * @return True if service was enabled and started.
+ */
+ private static boolean enableServiceAndStart(Context context, PackageManager packageManager,
+ String className) {
+ Class clazz;
+ try {
+ clazz = Class.forName(className);
+ } catch (Throwable t) {
+ return false;
+ }
+ return enableServiceAndStart(context, packageManager, clazz);
+ }
+
+ /**
+ * Enables and starts service for provided class name.
+ *
+ * @param context Current Context.
+ * @param packageManager Current PackageManager.
+ * @param clazz Class of service that needs to be enabled and started.
+ * @return True if service was enabled and started.
+ */
+ private static boolean enableServiceAndStart(Context context, PackageManager packageManager,
+ Class clazz) {
+ if (!enableComponent(context, packageManager, clazz)) {
+ return false;
+ }
+ try {
+ context.startService(new Intent(context, clazz));
+ } catch (Throwable t) {
+ Log.w("Could not start service " + clazz.getName());
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Enables component for provided class name.
+ *
+ * @param context Current Context.
+ * @param packageManager Current PackageManager.
+ * @param className Name of Class for enable.
+ * @return True if component was enabled.
+ */
+ private static boolean enableComponent(Context context, PackageManager packageManager,
+ String className) {
+ try {
+ Class clazz = Class.forName(className);
+ return enableComponent(context, packageManager, clazz);
+ } catch (Throwable t) {
+ return false;
+ }
+ }
+
+ /**
+ * Enables component for provided class.
+ *
+ * @param context Current Context.
+ * @param packageManager Current PackageManager.
+ * @param clazz Class for enable.
+ * @return True if component was enabled.
+ */
+ private static boolean enableComponent(Context context, PackageManager packageManager,
+ Class clazz) {
+ if (clazz == null || context == null || packageManager == null) {
+ return false;
+ }
+
+ try {
+ packageManager.setComponentEnabledSetting(new ComponentName(context, clazz),
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP);
+ } catch (Throwable t) {
+ Log.w("Could not enable component " + clazz.getName());
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Checks if component for provided class enabled before.
+ *
+ * @param context Current Context.
+ * @param packageManager Current PackageManager.
+ * @param clazz Class for check.
+ * @return True if component was enabled before.
+ */
+ private static boolean wasComponentEnabled(Context context, PackageManager packageManager,
+ Class clazz) {
+ if (clazz == null || context == null || packageManager == null) {
+ return false;
+ }
+ int componentStatus = packageManager.getComponentEnabledSetting(new ComponentName(context,
+ clazz));
+ if (PackageManager.COMPONENT_ENABLED_STATE_DEFAULT == componentStatus ||
+ PackageManager.COMPONENT_ENABLED_STATE_DISABLED == componentStatus) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Check if current application id is different from stored one.
+ *
+ * @param currentAppId - Current application id.
+ * @return True if application id was stored before and doesn't equal to current.
+ */
+ private static boolean hasAppIDChanged(String currentAppId) {
+ if (currentAppId == null) {
+ return false;
+ }
+
+ Context context = Leanplum.getContext();
+ if (context == null) {
+ return false;
+ }
+
+ String storedAppId = SharedPreferencesUtil.getString(context, Constants.Defaults.LEANPLUM_PUSH,
+ Constants.Defaults.APP_ID);
+ if (!currentAppId.equals(storedAppId)) {
+ Log.v("Saving the application id in the shared preferences.");
+ SharedPreferencesUtil.setString(context, Constants.Defaults.LEANPLUM_PUSH,
+ Constants.Defaults.APP_ID, currentAppId);
+ // Check application id was stored before.
+ if (!SharedPreferencesUtil.DEFAULT_STRING_VALUE.equals(storedAppId)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumResources.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.util.DisplayMetrics;
+
+import com.leanplum.internal.CollectionUtil;
+import com.leanplum.internal.Constants;
+import com.leanplum.internal.FileManager;
+import com.leanplum.internal.Log;
+import com.leanplum.internal.ResourceQualifiers;
+import com.leanplum.internal.ResourceQualifiers.Qualifier;
+import com.leanplum.internal.Util;
+import com.leanplum.internal.VarCache;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+// Description of resources.asrc file (we don't use this right nwo)
+// http://ekasiswanto.wordpress.com/2012/09/19/descriptions-of-androids-resources-arsc/
+
+public class LeanplumResources extends Resources {
+ public LeanplumResources(Resources base) {
+ super(base.getAssets(), base.getDisplayMetrics(), base.getConfiguration());
+ }
+
+ /* internal */
+ <T> Var<T> getOverrideResource(int id) {
+ try {
+ String name = getResourceEntryName(id);
+ String type = getResourceTypeName(id);
+ if (FileManager.resources == null) {
+ return null;
+ }
+ HashMap<String, Object> resourceValues = CollectionUtil.uncheckedCast(FileManager.resources
+ .objectForKeyPath());
+ Map<String, String> eligibleFolders = new HashMap<>();
+ synchronized (VarCache.valuesFromClient) {
+ for (String folder : resourceValues.keySet()) {
+ if (!folder.toLowerCase().startsWith(type)) {
+ continue;
+ }
+ HashMap<String, Object> files = CollectionUtil.uncheckedCast(resourceValues.get(folder));
+ String eligibleFile = null;
+ for (String filename : files.keySet()) {
+ String currentName = filename.replace("\\.", ".");
+ // Get filename without extension.
+ int dotPos = currentName.lastIndexOf('.');
+ if (dotPos >= 0) {
+ currentName = currentName.substring(0, dotPos);
+ }
+
+ if (currentName.equals(name)) {
+ eligibleFile = filename;
+ }
+ }
+ if (eligibleFile == null) {
+ continue;
+ }
+ eligibleFolders.put(folder, eligibleFile);
+ }
+ }
+
+ Map<String, ResourceQualifiers> folderQualifiers = new HashMap<>();
+ for (String folder : eligibleFolders.keySet()) {
+ folderQualifiers.put(folder, ResourceQualifiers.fromFolder(folder));
+ }
+
+ // 1. Eliminate qualifiers that contradict the device configuration.
+ // See http://developer.android.com/guide/topics/resources/providing-resources.html
+ Configuration config = getConfiguration();
+ DisplayMetrics display = getDisplayMetrics();
+ Set<String> matchedFolders = new HashSet<>();
+ for (String folder : eligibleFolders.keySet()) {
+ ResourceQualifiers qualifiers = folderQualifiers.get(folder);
+ for (Qualifier qualifier : qualifiers.qualifiers.keySet()) {
+ if (qualifier.getFilter().isMatch(
+ qualifiers.qualifiers.get(qualifier), config, display)) {
+ matchedFolders.add(folder);
+ }
+ }
+ }
+
+ // 2. Identify the next qualifier in the table (MCC first, then MNC,
+ // then language, and so on.
+ for (Qualifier qualifier : ResourceQualifiers.Qualifier.values()) {
+ Map<String, Object> betterMatchedFolders = new HashMap<>();
+ for (String folder : matchedFolders) {
+ ResourceQualifiers folderQualifier = folderQualifiers.get(folder);
+ Object qualifierValue = folderQualifier.qualifiers.get(qualifier);
+ if (qualifierValue != null) {
+ betterMatchedFolders.put(folder, qualifierValue);
+ }
+ }
+ betterMatchedFolders = qualifier.getFilter().bestMatch(
+ betterMatchedFolders, config, display);
+
+ // 3. Do any resource directories use this qualifier?
+ if (!betterMatchedFolders.isEmpty()) {
+ // Yes.
+ // 4. Eliminate directories that do not include this qualifier.
+ matchedFolders = betterMatchedFolders.keySet();
+ }
+ }
+
+ // Return result.
+ if (!eligibleFolders.isEmpty()) {
+ String folder = eligibleFolders.entrySet().iterator().next().getValue();
+ String varName = Constants.Values.RESOURCES_VARIABLE + "." + folder
+ + "." + eligibleFolders.get(folder);
+ return VarCache.getVariable(varName);
+ }
+ } catch (Exception e) {
+ Log.e("Error getting resource", e);
+ }
+ return null;
+ }
+
+ @Override
+ public Drawable getDrawable(int id) throws NotFoundException {
+ try {
+ Var<String> override = getOverrideResource(id);
+ if (override != null) {
+ int overrideResId = override.overrideResId();
+ if (overrideResId != 0) {
+ return super.getDrawable(overrideResId);
+ }
+ if (!override.stringValue.equals(override.defaultValue())) {
+ Drawable result = Drawable.createFromStream(override.stream(), override.fileValue());
+ if (result != null) {
+ return result;
+ }
+ }
+ }
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ return super.getDrawable(id);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LeanplumUIEditor.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2017, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import android.app.Activity;
+
+/**
+ * Describes the API of the visual editor package.
+ */
+public interface LeanplumUIEditor {
+ /**
+ * Enable interface editing via Leanplum.com Visual Editor.
+ */
+ void allowInterfaceEditing(Boolean isDevelopmentModeEnabled);
+
+ /**
+ * Enables Interface editing for the desired activity.
+ *
+ * @param activity The activity to enable interface editing for.
+ */
+ void applyInterfaceEdits(Activity activity);
+
+ /**
+ * Sets the update flag to true.
+ */
+ void startUpdating();
+
+ /**
+ * Sets the update flag to false.
+ */
+ void stopUpdating();
+
+ /**
+ * Send an immediate update of the UI to the LP server.
+ */
+ void sendUpdate();
+
+ /**
+ * Send an update with given delay of the UI to the LP server.
+ */
+ void sendUpdateDelayed(int delay);
+
+ /**
+ * Send an update of the UI to the LP server, delayed by the default time.
+ */
+ void sendUpdateDelayedDefault();
+
+ /**
+ * Returns the current editor mode.
+ *
+ * @return The current editor mode.
+ */
+ LeanplumEditorMode getMode();
+
+ /**
+ * Sets the current editor mode.
+ *
+ * @param mode The editor mode to set.
+ */
+ void setMode(LeanplumEditorMode mode);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/LocationManager.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2014, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Public interface to LocationManager. This is abstracted away so that the Google Play Services
+ * dependencies are constrained to {@link LocationManagerImplementation}.
+ *
+ * @author Andrew First
+ */
+public interface LocationManager {
+ void updateGeofencing();
+
+ void updateUserLocation();
+
+ void setRegionsData(Map<String, Object> regionData,
+ Set<String> foregroundRegionNames, Set<String> backgroundRegionNames);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/Newsfeed.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2015, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import com.leanplum.callbacks.InboxChangedCallback;
+import com.leanplum.callbacks.NewsfeedChangedCallback;
+
+/**
+ * Newsfeed class.
+ *
+ * @author Aleksandar Gyorev
+ */
+public class Newsfeed extends LeanplumInbox {
+
+ /**
+ * A private constructor, which prevents any other class from instantiating.
+ */
+ Newsfeed() {
+ }
+
+ /**
+ * Static 'getInstance' method.
+ */
+ static Newsfeed getInstance() {
+ return instance;
+ }
+
+ /**
+ * Add a callback for when the newsfeed receives new values from the server.
+ *
+ * @deprecated use {@link #addChangedHandler(InboxChangedCallback)} instead
+ */
+ @Deprecated
+ public void addNewsfeedChangedHandler(NewsfeedChangedCallback handler) {
+ super.addChangedHandler(handler);
+ }
+
+ /**
+ * Removes a newsfeed changed callback.
+ *
+ * @deprecated use {@link #removeChangedHandler(InboxChangedCallback)} instead
+ */
+ @Deprecated
+ public void removeNewsfeedChangedHandler(NewsfeedChangedCallback handler) {
+ super.removeChangedHandler(handler);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/NewsfeedMessage.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright 2015, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import com.leanplum.internal.Constants;
+import com.leanplum.internal.Request;
+import com.leanplum.internal.Util;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * NewsfeedMessage class.
+ *
+ * @author Aleksandar Gyorev
+ */
+public abstract class NewsfeedMessage {
+ private String messageId;
+ private Long deliveryTimestamp;
+ private Long expirationTimestamp;
+ private boolean isRead;
+ private ActionContext context;
+
+ NewsfeedMessage(String messageId, Long deliveryTimestamp, Long expirationTimestamp,
+ boolean isRead, ActionContext context) {
+ this.messageId = messageId;
+ this.deliveryTimestamp = deliveryTimestamp;
+ this.expirationTimestamp = expirationTimestamp;
+ this.isRead = isRead;
+ this.context = context;
+ }
+
+ Map<String, Object> toJsonMap() {
+ Map<String, Object> map = new HashMap<>();
+ map.put(Constants.Keys.DELIVERY_TIMESTAMP, this.deliveryTimestamp);
+ map.put(Constants.Keys.EXPIRATION_TIMESTAMP, this.expirationTimestamp);
+ map.put(Constants.Keys.MESSAGE_DATA, this.actionArgs());
+ map.put(Constants.Keys.IS_READ, this.isRead());
+ return map;
+ }
+
+ Map<String, Object> actionArgs() {
+ return context.getArgs();
+ }
+
+ void setIsRead(boolean isRead) {
+ this.isRead = isRead;
+ }
+
+ boolean isActive() {
+ if (expirationTimestamp == null) {
+ return true;
+ }
+
+ Date now = new Date();
+ return now.before(new Date(expirationTimestamp));
+ }
+
+ static boolean isValidMessageId(String messageId) {
+ return messageId.split("##").length == 2;
+ }
+
+ ActionContext getContext() {
+ return context;
+ }
+
+ /**
+ * Returns the message identifier of the newsfeed message.
+ *
+ * @deprecated As of release 1.3.0, replaced by {@link #getMessageId()}
+ */
+ @Deprecated
+ public String messageId() {
+ return getMessageId();
+ }
+
+ /**
+ * Returns the message identifier of the newsfeed message.
+ */
+ public String getMessageId() {
+ return messageId;
+ }
+
+ /**
+ * Returns the title of the newsfeed message.
+ *
+ * @deprecated As of release 1.3.0, replaced by {@link #getTitle()}
+ */
+ @Deprecated
+ public String title() {
+ return getTitle();
+ }
+
+ /**
+ * Returns the title of the newsfeed message.
+ */
+ public String getTitle() {
+ return context.stringNamed(Constants.Keys.TITLE);
+ }
+
+ /**
+ * Returns the subtitle of the newsfeed message.
+ *
+ * @deprecated As of release 1.3.0, replaced by {@link #getSubtitle()}
+ */
+ @Deprecated
+ public String subtitle() {
+ return getSubtitle();
+ }
+
+ /**
+ * Returns the subtitle of the newsfeed message.
+ */
+ public String getSubtitle() {
+ return context.stringNamed(Constants.Keys.SUBTITLE);
+ }
+
+ /**
+ * Returns the delivery timestamp of the newsfeed message.
+ *
+ * @deprecated As of release 1.3.0, replaced by {@link #getDeliveryTimestamp()}
+ */
+ @Deprecated
+ public Date deliveryTimestamp() {
+ return getDeliveryTimestamp();
+ }
+
+ /**
+ * Returns the delivery timestamp of the newsfeed message.
+ */
+ public Date getDeliveryTimestamp() {
+ return new Date(deliveryTimestamp);
+ }
+
+ /**
+ * Return the expiration timestamp of the newsfeed message.
+ *
+ * @deprecated As of release 1.3.0, replaced by {@link #getExpirationTimestamp()}
+ */
+ @Deprecated
+ public Date expirationTimestamp() {
+ return getExpirationTimestamp();
+ }
+
+ /**
+ * Return the expiration timestamp of the newsfeed message.
+ */
+ public Date getExpirationTimestamp() {
+ if (expirationTimestamp == null) {
+ return null;
+ }
+ return new Date(expirationTimestamp);
+ }
+
+ /**
+ * Returns 'true' if the newsfeed message is read.
+ */
+ public boolean isRead() {
+ return isRead;
+ }
+
+ /**
+ * Read the newsfeed message, marking it as read and invoking its open action.
+ */
+ public void read() {
+ try {
+ if (Constants.isNoop()) {
+ return;
+ }
+
+ if (!this.isRead) {
+ setIsRead(true);
+
+ int unreadCount = Newsfeed.getInstance().unreadCount() - 1;
+ Newsfeed.getInstance().updateUnreadCount(unreadCount);
+
+ Map<String, Object> params = new HashMap<>();
+ params.put(Constants.Params.INBOX_MESSAGE_ID, messageId);
+ Request req = Request.post(Constants.Methods.MARK_INBOX_MESSAGE_AS_READ,
+ params);
+ req.send();
+ }
+ this.context.runTrackedActionNamed(Constants.Values.DEFAULT_PUSH_ACTION);
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ }
+
+ /**
+ * Remove the newsfeed message from the newsfeed.
+ */
+ public void remove() {
+ try {
+ Newsfeed.getInstance().removeMessage(messageId);
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/UIEditorBridge.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2017, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import com.leanplum.internal.FileManager;
+import com.leanplum.internal.Socket;
+import com.leanplum.internal.Util;
+import com.leanplum.internal.VarCache;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Bridge class for the UI editor package to access LP internal methods.
+ *
+ * @author Ben Marten
+ */
+public class UIEditorBridge {
+ public static void setInterfaceUpdateBlock(CacheUpdateBlock block) {
+ VarCache.onInterfaceUpdate(block);
+ }
+
+ public static void setEventsUpdateBlock(CacheUpdateBlock block) {
+ VarCache.onEventsUpdate(block);
+ }
+
+ public static List<Map<String, Object>> getUpdateRuleDiffs() {
+ return VarCache.getUpdateRuleDiffs();
+ }
+
+ public static List<Map<String, Object>> getEventRuleDiffs() {
+ return VarCache.getEventRuleDiffs();
+ }
+
+ public static boolean isSocketConnected() {
+ return Socket.getInstance() != null && Socket.getInstance().isConnected();
+ }
+
+ public static <T> void socketSendEvent(String eventName, Map<String, T> data) {
+ if (Socket.getInstance() != null && eventName != null) {
+ Socket.getInstance().sendEvent(eventName, data);
+ }
+ }
+
+ public static String fileRelativeToDocuments(String path) {
+ return FileManager.fileRelativeToDocuments(path);
+ }
+
+ public static void handleException(Throwable t) {
+ Util.handleException(t);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/Var.java
@@ -0,0 +1,622 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum;
+
+import android.text.TextUtils;
+
+import com.leanplum.callbacks.VariableCallback;
+import com.leanplum.internal.Constants;
+import com.leanplum.internal.FileManager;
+import com.leanplum.internal.FileManager.DownloadFileResult;
+import com.leanplum.internal.LeanplumInternal;
+import com.leanplum.internal.Log;
+import com.leanplum.internal.OsHandler;
+import com.leanplum.internal.Util;
+import com.leanplum.internal.VarCache;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Leanplum variable.
+ *
+ * @param <T> Type of the variable. Can be Boolean, Byte, Short, Integer, Long, Float, Double,
+ * Character, String, List, or Map. You may nest lists and maps arbitrarily.
+ * @author Andrew First
+ */
+public class Var<T> {
+ private String name;
+ private String[] nameComponents;
+ public String stringValue;
+ private Double numberValue;
+ private T defaultValue;
+ private T value;
+ private String kind;
+ private final List<VariableCallback<T>> fileReadyHandlers = new ArrayList<>();
+ private final List<VariableCallback<T>> valueChangedHandlers = new ArrayList<>();
+ private boolean fileIsPending;
+ private boolean hadStarted;
+ private boolean isAsset;
+ public boolean isResource;
+ private int size;
+ private String hash;
+ private byte[] data;
+ private boolean valueIsInAssets = false;
+ private boolean isInternal;
+ private int overrideResId;
+ private static boolean printedCallbackWarning;
+
+ private void warnIfNotStarted() {
+ if (!isInternal && !Leanplum.hasStarted() && !printedCallbackWarning) {
+ Log.w("Leanplum hasn't finished retrieving values from the server. "
+ + "You should use a callback to make sure the value for '" + name +
+ "' is ready. Otherwise, your app may not use the most up-to-date value.");
+ printedCallbackWarning = true;
+ }
+ }
+
+ private interface VarInitializer<T> {
+ void init(Var<T> var);
+ }
+
+ private static <T> Var<T> define(
+ String name, T defaultValue, String kind, VarInitializer<T> initializer) {
+ if (TextUtils.isEmpty(name)) {
+ Log.e("Empty name parameter provided.");
+ return null;
+ }
+ Var<T> existing = VarCache.getVariable(name);
+ if (existing != null) {
+ return existing;
+ }
+ if (LeanplumInternal.hasCalledStart() &&
+ !name.startsWith(Constants.Values.RESOURCES_VARIABLE)) {
+ Log.w("You should not create new variables after calling start (name=" + name + ")");
+ }
+ Var<T> var = new Var<>();
+ try {
+ var.name = name;
+ var.nameComponents = VarCache.getNameComponents(name);
+ var.defaultValue = defaultValue;
+ var.value = defaultValue;
+ var.kind = kind;
+ if (name.startsWith(Constants.Values.RESOURCES_VARIABLE)) {
+ var.isInternal = true;
+ }
+ if (initializer != null) {
+ initializer.init(var);
+ }
+ var.cacheComputedValues();
+ VarCache.registerVariable(var);
+ if (Constants.Kinds.FILE.equals(var.kind)) {
+ VarCache.registerFile(var.stringValue,
+ var.defaultValue() == null ? null : var.defaultValue().toString(),
+ var.defaultStream(), var.isResource, var.hash, var.size);
+ }
+ var.update();
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ return var;
+ }
+
+ /**
+ * Defines a new variable with a default value.
+ *
+ * @param name Name of the variable.
+ * @param defaultValue Default value of the variable. Can't be null.
+ */
+ public static <T> Var<T> define(String name, T defaultValue) {
+ return define(name, defaultValue, VarCache.kindFromValue(defaultValue), null);
+ }
+
+ /**
+ * Defines a variable with kind. Can be Boolean, Byte, Short, Integer, Long, Float, Double,
+ * Character, String, List, or Map. You may nest lists and maps arbitrarily.
+ *
+ * @param name Name of the variable.
+ * @param defaultValue Default value.
+ * @param kind Kind of the variable.
+ * @param <T> Boolean, Byte, Short, Integer, Long, Float, Double, Character, String, List, or
+ * Map.
+ * @return Initialized variable.
+ */
+ public static <T> Var<T> define(String name, T defaultValue, String kind) {
+ return define(name, defaultValue, kind, null);
+ }
+
+ /**
+ * Defines a color.
+ *
+ * @param name Name of the variable
+ * @param defaultValue Default value.
+ * @return Initialized variable.
+ */
+ @SuppressWarnings("WeakerAccess")
+ public static Var<Integer> defineColor(String name, int defaultValue) {
+ return define(name, defaultValue, Constants.Kinds.COLOR, null);
+ }
+
+ /**
+ * Defines a variable for a file.
+ *
+ * @param name Name of the variable.
+ * @param defaultFilename Default filename.
+ * @return Initialized variable.
+ */
+ public static Var<String> defineFile(String name, String defaultFilename) {
+ return define(name, defaultFilename, Constants.Kinds.FILE, null);
+ }
+
+ /**
+ * Defines a variable for a file located in assets directory.
+ *
+ * @param name Name of the variable.
+ * @param defaultFilename Default filename.
+ * @return Initialized variable.
+ */
+ public static Var<String> defineAsset(String name, String defaultFilename) {
+ return define(name, defaultFilename, Constants.Kinds.FILE, new VarInitializer<String>() {
+ @Override
+ public void init(Var<String> var) {
+ var.isAsset = true;
+ }
+ });
+ }
+
+ /**
+ * Define a resource variable with default value referencing id of the file located in
+ * res/ directory.
+ *
+ * @param name Name of the variable.
+ * @param resId Resource id of any file located in res/ directory.
+ * @return Initalized variable.
+ */
+ public static Var<String> defineResource(String name, int resId) {
+ String resourceName = Util.generateResourceNameFromId(resId);
+ return define(name, resourceName, Constants.Kinds.FILE, new VarInitializer<String>() {
+ @Override
+ public void init(Var<String> var) {
+ var.isResource = true;
+ }
+ });
+ }
+
+ /**
+ * Defines a resource.
+ *
+ * @param name Name of the variable.
+ * @param defaultFilename Default filename.
+ * @param size Size of the data.
+ * @param hash Hash of the data.
+ * @param data Data.
+ * @return Initalized variable.
+ */
+ public static Var<String> defineResource(String name, String defaultFilename,
+ final int size, final String hash, final byte[] data) {
+ return define(name, defaultFilename, Constants.Kinds.FILE, new VarInitializer<String>() {
+ @Override
+ public void init(Var<String> var) {
+ var.isResource = true;
+ var.size = size;
+ var.hash = hash;
+ var.data = data;
+ }
+ });
+ }
+
+ protected Var() {
+ }
+
+ /**
+ * Gets name of the variable.
+ *
+ * @return Varaible name.
+ */
+ public String name() {
+ return name;
+ }
+
+ /**
+ * Gets name components of a variable.
+ *
+ * @return Name components.
+ */
+ public String[] nameComponents() {
+ return nameComponents;
+ }
+
+ /**
+ * Gets the kind of a variable.
+ *
+ * @return Kind of a variable.
+ */
+ public String kind() {
+ return kind;
+ }
+
+ /**
+ * Gets variable default value.
+ *
+ * @return Default value.
+ */
+ public T defaultValue() {
+ return defaultValue;
+ }
+
+ /**
+ * Get variable value.
+ *
+ * @return Value.
+ */
+ public T value() {
+ warnIfNotStarted();
+ return value;
+ }
+
+ /**
+ * Gets overridden resource id for variable.
+ *
+ * @return Id of the overridden resource.
+ */
+ public int overrideResId() {
+ return overrideResId;
+ }
+
+ /**
+ * Sets overridden resource id for a variable.
+ *
+ * @param resId Resource id.
+ */
+ public void setOverrideResId(int resId) {
+ overrideResId = resId;
+ }
+
+ @SuppressWarnings("unchecked")
+ private void cacheComputedValues() {
+ if (value instanceof String) {
+ stringValue = (String) value;
+ try {
+ numberValue = Double.valueOf(stringValue);
+ } catch (NumberFormatException e) {
+ numberValue = null;
+ }
+ } else if (value instanceof Number) {
+ stringValue = "" + value;
+ numberValue = ((Number) value).doubleValue();
+ if (defaultValue instanceof Byte) {
+ value = (T) (Byte) ((Number) value).byteValue();
+ } else if (defaultValue instanceof Short) {
+ value = (T) (Short) ((Number) value).shortValue();
+ } else if (defaultValue instanceof Integer) {
+ value = (T) (Integer) ((Number) value).intValue();
+ } else if (defaultValue instanceof Long) {
+ value = (T) (Long) ((Number) value).longValue();
+ } else if (defaultValue instanceof Float) {
+ value = (T) (Float) ((Number) value).floatValue();
+ } else if (defaultValue instanceof Double) {
+ value = (T) (Double) ((Number) value).doubleValue();
+ } else if (defaultValue instanceof Character) {
+ value = (T) (Character) (char) ((Number) value).intValue();
+ }
+ } else if (value != null &&
+ !(value instanceof Iterable<?>) && !(value instanceof Map<?, ?>)) {
+ stringValue = value.toString();
+ numberValue = null;
+ } else {
+ stringValue = null;
+ numberValue = null;
+ }
+ }
+
+ /**
+ * Updates variable with values from server.
+ */
+ public void update() {
+ // TODO: Clean up memory for resource variables.
+ //data = null;
+
+ T oldValue = value;
+ value = VarCache.getMergedValueFromComponentArray(nameComponents);
+ if (value == null && oldValue == null) {
+ return;
+ }
+ if (value != null && oldValue != null && value.equals(oldValue) && hadStarted) {
+ return;
+ }
+ cacheComputedValues();
+
+ if (VarCache.silent() && name.startsWith(Constants.Values.RESOURCES_VARIABLE)
+ && Constants.Kinds.FILE.equals(kind) && !fileIsPending) {
+ triggerFileIsReady();
+ }
+
+ if (VarCache.silent()) {
+ return;
+ }
+
+ if (Leanplum.hasStarted()) {
+ triggerValueChanged();
+ }
+
+ // Check if file exists, otherwise we need to download it.
+ if (Constants.Kinds.FILE.equals(kind)) {
+ if (!Constants.isNoop()) {
+ DownloadFileResult result = FileManager.maybeDownloadFile(
+ isResource, stringValue, (String) defaultValue, null,
+ new Runnable() {
+ @Override
+ public void run() {
+ triggerFileIsReady();
+ }
+ });
+ valueIsInAssets = false;
+ if (result == DownloadFileResult.DOWNLOADING) {
+ fileIsPending = true;
+ } else if (result == DownloadFileResult.EXISTS_IN_ASSETS) {
+ valueIsInAssets = true;
+ }
+ }
+ if (Leanplum.hasStarted() && !fileIsPending) {
+ triggerFileIsReady();
+ }
+ }
+
+ if (Leanplum.hasStarted()) {
+ hadStarted = true;
+ }
+ }
+
+ private void triggerValueChanged() {
+ synchronized (valueChangedHandlers) {
+ for (VariableCallback<T> callback : valueChangedHandlers) {
+ callback.setVariable(this);
+ OsHandler.getInstance().post(callback);
+ }
+ }
+ }
+
+ /**
+ * Adds value changed handler for a given variable.
+ *
+ * @param handler Handler to add.
+ */
+ public void addValueChangedHandler(VariableCallback<T> handler) {
+ if (handler == null) {
+ Log.e("Invalid handler parameter provided.");
+ return;
+ }
+
+ synchronized (valueChangedHandlers) {
+ valueChangedHandlers.add(handler);
+ }
+ if (Leanplum.hasStarted()) {
+ handler.handle(this);
+ }
+ }
+
+ /**
+ * Removes value changed handler for a given variable.
+ *
+ * @param handler Handler to be removed.
+ */
+ public void removeValueChangedHandler(VariableCallback<T> handler) {
+ synchronized (valueChangedHandlers) {
+ valueChangedHandlers.remove(handler);
+ }
+ }
+
+ private void triggerFileIsReady() {
+ synchronized (fileReadyHandlers) {
+ fileIsPending = false;
+ for (VariableCallback<T> callback : fileReadyHandlers) {
+ callback.setVariable(this);
+ OsHandler.getInstance().post(callback);
+ }
+ }
+ }
+
+ /**
+ * Adds file ready handler for a given variable.
+ *
+ * @param handler Handler to add.
+ */
+ public void addFileReadyHandler(VariableCallback<T> handler) {
+ if (handler == null) {
+ Log.e("Invalid handler parameter provided.");
+ return;
+ }
+ synchronized (fileReadyHandlers) {
+ fileReadyHandlers.add(handler);
+ }
+ if (Leanplum.hasStarted() && !fileIsPending) {
+ handler.handle(this);
+ }
+ }
+
+ /**
+ * Removes file ready handler for a given variable.
+ *
+ * @param handler Handler to be removed.
+ */
+ public void removeFileReadyHandler(VariableCallback<T> handler) {
+ if (handler == null) {
+ Log.e("Invalid handler parameter provided.");
+ return;
+ }
+ synchronized (fileReadyHandlers) {
+ fileReadyHandlers.remove(handler);
+ }
+ }
+
+ /**
+ * Returns file value for variable initialized as file/asset/resource.
+ *
+ * @return String representing file value.
+ */
+ public String fileValue() {
+ try {
+ warnIfNotStarted();
+ if (Constants.Kinds.FILE.equals(kind)) {
+ return FileManager.fileValue(stringValue, (String) defaultValue, valueIsInAssets);
+ }
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ return null;
+ }
+
+ /**
+ * Returns object for specified key path.
+ *
+ * @param keys Keys to look for.
+ * @return Object if found, null otherwise.
+ */
+ @SuppressWarnings("WeakerAccess") // Used by Air SDK.
+ public Object objectForKeyPath(Object... keys) {
+ try {
+ warnIfNotStarted();
+ List<Object> components = new ArrayList<>();
+ Collections.addAll(components, nameComponents);
+ if (keys != null && keys.length > 0) {
+ Collections.addAll(components, keys);
+ }
+ return VarCache.getMergedValueFromComponentArray(
+ components.toArray(new Object[components.size()]));
+ } catch (Throwable t) {
+ Util.handleException(t);
+ return null;
+ }
+ }
+
+ /**
+ * Returns a number of elements contained in a List variable.
+ *
+ * @return Elements count or 0 if Variable is not a List.
+ */
+ @Deprecated
+ public int count() {
+ return countInternal();
+ }
+
+ /**
+ * Returns a number of elements contained in a List variable.
+ *
+ * @return Elements count or 0 if Variable is not a List.
+ */
+ private int countInternal() {
+ try {
+ warnIfNotStarted();
+ Object result = VarCache.getMergedValueFromComponentArray(nameComponents);
+ if (result instanceof List) {
+ return ((List<?>) result).size();
+ }
+ } catch (Throwable t) {
+ Util.handleException(t);
+ return 0;
+ }
+ LeanplumInternal.maybeThrowException(new UnsupportedOperationException(
+ "This variable is not a list."));
+ return 0;
+ }
+
+ /**
+ * Gets a value from a variable initialized as Number.
+ *
+ * @return A Number value.
+ */
+ @Deprecated
+ public Number numberValue() {
+ return numberValueInternal();
+ }
+
+ /**
+ * Gets a value from a variable initialized as Number.
+ *
+ * @return A Number value.
+ */
+ private Number numberValueInternal() {
+ warnIfNotStarted();
+ return numberValue;
+ }
+
+ /**
+ * Gets a value from a variable initialized as String.
+ *
+ * @return A String value.
+ */
+ public String stringValue() {
+ warnIfNotStarted();
+ return stringValue;
+ }
+
+ /**
+ * Creates and returns InputStream for overridden file/asset/resource variable.
+ * Caller is responsible for closing it properly to avoid leaking resources.
+ *
+ * @return InputStream for a file.
+ */
+ public InputStream stream() {
+ try {
+ if (!Constants.Kinds.FILE.equals(kind)) {
+ return null;
+ }
+ warnIfNotStarted();
+ InputStream stream = FileManager.stream(isResource, isAsset, valueIsInAssets,
+ fileValue(), (String) defaultValue, data);
+ if (stream == null) {
+ return defaultStream();
+ }
+ return stream;
+ } catch (Throwable t) {
+ Util.handleException(t);
+ return null;
+ }
+ }
+
+ /**
+ * Creates and returns InputStream for default file/asset/resource variable.
+ * Caller is responsible for closing it properly to avoid leaking resources.
+ *
+ * @return InputStream for a file.
+ */
+ private InputStream defaultStream() {
+ try {
+ if (!Constants.Kinds.FILE.equals(kind)) {
+ return null;
+ }
+ return FileManager.stream(isResource, isAsset, valueIsInAssets,
+ (String) defaultValue, (String) defaultValue, data);
+ } catch (Throwable t) {
+ Util.handleException(t);
+ return null;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "Var(" + name + ")=" + value;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumAccountAuthenticatorActivity.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.activities;
+
+import android.accounts.AccountAuthenticatorActivity;
+import android.annotation.SuppressLint;
+import android.content.res.Resources;
+
+import com.leanplum.Leanplum;
+import com.leanplum.LeanplumActivityHelper;
+
+@SuppressLint("Registered")
+public class LeanplumAccountAuthenticatorActivity extends AccountAuthenticatorActivity {
+ private LeanplumActivityHelper helper;
+
+ private LeanplumActivityHelper getHelper() {
+ if (helper == null) {
+ helper = new LeanplumActivityHelper(this);
+ }
+ return helper;
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ getHelper().onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ getHelper().onStop();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ getHelper().onResume();
+ }
+
+ @Override
+ public Resources getResources() {
+ if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+ return super.getResources();
+ }
+ return getHelper().getLeanplumResources(super.getResources());
+ }
+
+ @Override
+ public void setContentView(final int layoutResID) {
+ if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+ super.setContentView(layoutResID);
+ return;
+ }
+ getHelper().setContentView(layoutResID);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumActionBarActivity.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.activities;
+
+import android.annotation.SuppressLint;
+import android.content.res.Resources;
+import android.support.v7.app.ActionBarActivity;
+
+import com.leanplum.Leanplum;
+import com.leanplum.LeanplumActivityHelper;
+
+@SuppressLint("Registered")
+@SuppressWarnings("deprecation")
+public class LeanplumActionBarActivity extends ActionBarActivity {
+ private LeanplumActivityHelper helper;
+
+ private LeanplumActivityHelper getHelper() {
+ if (helper == null) {
+ helper = new LeanplumActivityHelper(this);
+ }
+ return helper;
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ getHelper().onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ getHelper().onStop();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ getHelper().onResume();
+ }
+
+ @Override
+ public Resources getResources() {
+ if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+ return super.getResources();
+ }
+ return getHelper().getLeanplumResources(super.getResources());
+ }
+
+ @Override
+ public void setContentView(final int layoutResID) {
+ if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+ super.setContentView(layoutResID);
+ return;
+ }
+ getHelper().setContentView(layoutResID);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumActivity.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.activities;
+
+import android.app.Activity;
+import android.content.res.Resources;
+
+import com.leanplum.Leanplum;
+import com.leanplum.LeanplumActivityHelper;
+
+public abstract class LeanplumActivity extends Activity {
+ private LeanplumActivityHelper helper;
+
+ private LeanplumActivityHelper getHelper() {
+ if (helper == null) {
+ helper = new LeanplumActivityHelper(this);
+ }
+ return helper;
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ getHelper().onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ getHelper().onStop();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ getHelper().onResume();
+ }
+
+ @Override
+ public Resources getResources() {
+ if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+ return super.getResources();
+ }
+ return getHelper().getLeanplumResources(super.getResources());
+ }
+
+ @Override
+ public void setContentView(final int layoutResID) {
+ if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+ super.setContentView(layoutResID);
+ return;
+ }
+ getHelper().setContentView(layoutResID);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumActivityGroup.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.activities;
+
+import android.annotation.SuppressLint;
+import android.app.ActivityGroup;
+import android.content.res.Resources;
+
+import com.leanplum.Leanplum;
+import com.leanplum.LeanplumActivityHelper;
+
+@SuppressLint("Registered")
+@SuppressWarnings("deprecation")
+public class LeanplumActivityGroup extends ActivityGroup {
+ private LeanplumActivityHelper helper;
+
+ private LeanplumActivityHelper getHelper() {
+ if (helper == null) {
+ helper = new LeanplumActivityHelper(this);
+ }
+ return helper;
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ getHelper().onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ getHelper().onStop();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ getHelper().onResume();
+ }
+
+ @Override
+ public Resources getResources() {
+ if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+ return super.getResources();
+ }
+ return getHelper().getLeanplumResources(super.getResources());
+ }
+
+ @Override
+ public void setContentView(final int layoutResID) {
+ if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+ super.setContentView(layoutResID);
+ return;
+ }
+ getHelper().setContentView(layoutResID);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumAliasActivity.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.activities;
+
+import android.annotation.SuppressLint;
+import android.app.AliasActivity;
+import android.content.res.Resources;
+
+import com.leanplum.Leanplum;
+import com.leanplum.LeanplumActivityHelper;
+
+@SuppressLint("Registered")
+public class LeanplumAliasActivity extends AliasActivity {
+ private LeanplumActivityHelper helper;
+
+ private LeanplumActivityHelper getHelper() {
+ if (helper == null) {
+ helper = new LeanplumActivityHelper(this);
+ }
+ return helper;
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ getHelper().onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ getHelper().onStop();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ getHelper().onResume();
+ }
+
+ @Override
+ public Resources getResources() {
+ if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+ return super.getResources();
+ }
+ return getHelper().getLeanplumResources(super.getResources());
+ }
+
+ @Override
+ public void setContentView(final int layoutResID) {
+ if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+ super.setContentView(layoutResID);
+ return;
+ }
+ getHelper().setContentView(layoutResID);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumAppCompatActivity.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2015, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.activities;
+
+import android.annotation.SuppressLint;
+import android.content.res.Resources;
+import android.support.v7.app.AppCompatActivity;
+
+import com.leanplum.Leanplum;
+import com.leanplum.LeanplumActivityHelper;
+
+@SuppressLint("Registered")
+public class LeanplumAppCompatActivity extends AppCompatActivity {
+ private LeanplumActivityHelper helper;
+
+ private LeanplumActivityHelper getHelper() {
+ if (helper == null) {
+ helper = new LeanplumActivityHelper(this);
+ }
+ return helper;
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ getHelper().onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ getHelper().onStop();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ getHelper().onResume();
+ }
+
+ @Override
+ public Resources getResources() {
+ if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+ return super.getResources();
+ }
+ return getHelper().getLeanplumResources(super.getResources());
+ }
+
+ @Override
+ public void setContentView(final int layoutResID) {
+ if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+ super.setContentView(layoutResID);
+ return;
+ }
+ getHelper().setContentView(layoutResID);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumExpandableListActivity.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.activities;
+
+import android.annotation.SuppressLint;
+import android.app.ExpandableListActivity;
+import android.content.res.Resources;
+
+import com.leanplum.Leanplum;
+import com.leanplum.LeanplumActivityHelper;
+
+@SuppressLint("Registered")
+public class LeanplumExpandableListActivity extends ExpandableListActivity {
+ private LeanplumActivityHelper helper;
+
+ private LeanplumActivityHelper getHelper() {
+ if (helper == null) {
+ helper = new LeanplumActivityHelper(this);
+ }
+ return helper;
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ getHelper().onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ getHelper().onStop();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ getHelper().onResume();
+ }
+
+ @Override
+ public Resources getResources() {
+ if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+ return super.getResources();
+ }
+ return getHelper().getLeanplumResources(super.getResources());
+ }
+
+ @Override
+ public void setContentView(final int layoutResID) {
+ if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+ super.setContentView(layoutResID);
+ return;
+ }
+ getHelper().setContentView(layoutResID);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumFragmentActivity.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.activities;
+
+import android.content.res.Resources;
+import android.support.v4.app.FragmentActivity;
+
+import com.leanplum.Leanplum;
+import com.leanplum.LeanplumActivityHelper;
+
+public abstract class LeanplumFragmentActivity extends FragmentActivity {
+ private LeanplumActivityHelper helper;
+
+ private LeanplumActivityHelper getHelper() {
+ if (helper == null) {
+ helper = new LeanplumActivityHelper(this);
+ }
+ return helper;
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ getHelper().onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ getHelper().onStop();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ getHelper().onResume();
+ }
+
+ @Override
+ public Resources getResources() {
+ if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+ return super.getResources();
+ }
+ return getHelper().getLeanplumResources(super.getResources());
+ }
+
+ @Override
+ public void setContentView(final int layoutResID) {
+ if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+ super.setContentView(layoutResID);
+ return;
+ }
+ getHelper().setContentView(layoutResID);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumLauncherActivity.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.activities;
+
+import android.annotation.SuppressLint;
+import android.app.LauncherActivity;
+import android.content.res.Resources;
+
+import com.leanplum.Leanplum;
+import com.leanplum.LeanplumActivityHelper;
+
+@SuppressLint("Registered")
+public class LeanplumLauncherActivity extends LauncherActivity {
+ private LeanplumActivityHelper helper;
+
+ private LeanplumActivityHelper getHelper() {
+ if (helper == null) {
+ helper = new LeanplumActivityHelper(this);
+ }
+ return helper;
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ getHelper().onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ getHelper().onStop();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ getHelper().onResume();
+ }
+
+ @Override
+ public Resources getResources() {
+ if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+ return super.getResources();
+ }
+ return getHelper().getLeanplumResources(super.getResources());
+ }
+
+ @Override
+ public void setContentView(final int layoutResID) {
+ if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+ super.setContentView(layoutResID);
+ return;
+ }
+ getHelper().setContentView(layoutResID);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumListActivity.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.activities;
+
+import android.annotation.SuppressLint;
+import android.app.ListActivity;
+import android.content.res.Resources;
+
+import com.leanplum.Leanplum;
+import com.leanplum.LeanplumActivityHelper;
+
+@SuppressLint("Registered")
+public class LeanplumListActivity extends ListActivity {
+ private LeanplumActivityHelper helper;
+
+ private LeanplumActivityHelper getHelper() {
+ if (helper == null) {
+ helper = new LeanplumActivityHelper(this);
+ }
+ return helper;
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ getHelper().onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ getHelper().onStop();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ getHelper().onResume();
+ }
+
+ @Override
+ public Resources getResources() {
+ if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+ return super.getResources();
+ }
+ return getHelper().getLeanplumResources(super.getResources());
+ }
+
+ @Override
+ public void setContentView(final int layoutResID) {
+ if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+ super.setContentView(layoutResID);
+ return;
+ }
+ getHelper().setContentView(layoutResID);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumNativeActivity.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.activities;
+
+import android.annotation.SuppressLint;
+import android.app.NativeActivity;
+import android.content.res.Resources;
+
+import com.leanplum.Leanplum;
+import com.leanplum.LeanplumActivityHelper;
+
+@SuppressLint("Registered")
+public class LeanplumNativeActivity extends NativeActivity {
+ private LeanplumActivityHelper helper;
+
+ private LeanplumActivityHelper getHelper() {
+ if (helper == null) {
+ helper = new LeanplumActivityHelper(this);
+ }
+ return helper;
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ getHelper().onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ getHelper().onStop();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ getHelper().onResume();
+ }
+
+ @Override
+ public Resources getResources() {
+ if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+ return super.getResources();
+ }
+ return getHelper().getLeanplumResources(super.getResources());
+ }
+
+ @Override
+ public void setContentView(final int layoutResID) {
+ if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+ super.setContentView(layoutResID);
+ return;
+ }
+ getHelper().setContentView(layoutResID);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumPreferenceActivity.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package com.leanplum.activities;
+
+import android.annotation.SuppressLint;
+import android.content.res.Resources;
+import android.preference.PreferenceActivity;
+
+import com.leanplum.Leanplum;
+import com.leanplum.LeanplumActivityHelper;
+
+@SuppressLint("Registered")
+public class LeanplumPreferenceActivity extends PreferenceActivity {
+ private LeanplumActivityHelper helper;
+
+ private LeanplumActivityHelper getHelper() {
+ if (helper == null) {
+ helper = new LeanplumActivityHelper(this);
+ }
+ return helper;
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ getHelper().onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ getHelper().onStop();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ getHelper().onResume();
+ }
+
+ @Override
+ public Resources getResources() {
+ if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+ return super.getResources();
+ }
+ return getHelper().getLeanplumResources(super.getResources());
+ }
+
+ @Override
+ public void setContentView(final int layoutResID) {
+ if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+ super.setContentView(layoutResID);
+ return;
+ }
+ getHelper().setContentView(layoutResID);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/activities/LeanplumTabActivity.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.activities;
+
+import android.annotation.SuppressLint;
+import android.app.TabActivity;
+import android.content.res.Resources;
+
+import com.leanplum.Leanplum;
+import com.leanplum.LeanplumActivityHelper;
+
+@SuppressLint("Registered")
+@SuppressWarnings("deprecation")
+public class LeanplumTabActivity extends TabActivity {
+ private LeanplumActivityHelper helper;
+
+ private LeanplumActivityHelper getHelper() {
+ if (helper == null) {
+ helper = new LeanplumActivityHelper(this);
+ }
+ return helper;
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ getHelper().onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ getHelper().onStop();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ getHelper().onResume();
+ }
+
+ @Override
+ public Resources getResources() {
+ if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+ return super.getResources();
+ }
+ return getHelper().getLeanplumResources(super.getResources());
+ }
+
+ @Override
+ public void setContentView(final int layoutResID) {
+ if (Leanplum.isTestModeEnabled() || !Leanplum.isResourceSyncingEnabled()) {
+ super.setContentView(layoutResID);
+ return;
+ }
+ getHelper().setContentView(layoutResID);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/annotations/File.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Leanplum variable annotation. Use this to make this variable changeable from the Leanplum
+ * dashboard.
+ *
+ * @author Andrew First
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface File {
+ /**
+ * (Optional). The group to put the variable in. Use "." to nest groups.
+ */
+ String group() default "";
+
+ /**
+ * (Optional). The name of the variable. If not set, then uses the actual name of the field.
+ */
+ String name() default "";
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/annotations/Parser.java
@@ -0,0 +1,203 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.annotations;
+
+import android.text.TextUtils;
+
+import com.leanplum.Var;
+import com.leanplum.callbacks.VariableCallback;
+import com.leanplum.internal.Log;
+
+import java.lang.ref.WeakReference;
+import java.lang.reflect.Field;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Parses Leanplum annotations.
+ *
+ * @author Andrew First
+ */
+public class Parser {
+ private static <T> void defineVariable(
+ final Object instance,
+ String name,
+ T value,
+ String kind,
+ final Field field) {
+ final Var<T> var = Var.define(name, value, kind);
+ final boolean hasInstance = instance != null;
+ final WeakReference<Object> weakInstance = new WeakReference<>(instance);
+ var.addValueChangedHandler(new VariableCallback<T>() {
+ @Override
+ public void handle(Var<T> variable) {
+ Object instance = weakInstance.get();
+ if ((hasInstance && instance == null) || field == null) {
+ var.removeValueChangedHandler(this);
+ return;
+ }
+ try {
+ boolean accessible = field.isAccessible();
+ if (!accessible) {
+ field.setAccessible(true);
+ }
+ field.set(instance, var.value());
+ if (!accessible) {
+ field.setAccessible(false);
+ }
+ } catch (IllegalArgumentException e) {
+ Log.e("Leanplum", "Invalid value " + var.value() +
+ " for field " + var.name(), e);
+ } catch (IllegalAccessException e) {
+ Log.e("Leanplum", "Error setting value for field " + var.name(), e);
+ }
+ }
+ });
+ }
+
+ private static void defineFileVariable(
+ final Object instance,
+ String name,
+ String value,
+ final Field field) {
+ final Var<String> var = Var.defineFile(name, value);
+ final boolean hasInstance = instance != null;
+ final WeakReference<Object> weakInstance = new WeakReference<>(instance);
+ var.addFileReadyHandler(new VariableCallback<String>() {
+ @Override
+ public void handle(Var<String> variable) {
+ Object instance = weakInstance.get();
+ if ((hasInstance && instance == null) || field == null) {
+ var.removeFileReadyHandler(this);
+ return;
+ }
+ try {
+ boolean accessible = field.isAccessible();
+ if (!accessible) {
+ field.setAccessible(true);
+ }
+ field.set(instance, var.fileValue());
+ if (!accessible) {
+ field.setAccessible(false);
+ }
+ } catch (IllegalArgumentException e) {
+ Log.e("Leanplum", "Invalid value " + var.value() +
+ " for field " + var.name(), e);
+ } catch (IllegalAccessException e) {
+ Log.e("Leanplum", "Error setting value for field " + var.name(), e);
+ }
+ }
+ });
+ }
+
+ /**
+ * Parses Leanplum annotations for all given object instances.
+ */
+ public static void parseVariables(Object... instances) {
+ try {
+ for (Object instance : instances) {
+ parseVariablesHelper(instance, instance.getClass());
+ }
+ } catch (Throwable t) {
+ Log.e("Leanplum", "Error parsing variables", t);
+ }
+ }
+
+ /**
+ * Parses Leanplum annotations for all given classes.
+ */
+ public static void parseVariablesForClasses(Class<?>... classes) {
+ try {
+ for (Class<?> clazz : classes) {
+ parseVariablesHelper(null, clazz);
+ }
+ } catch (Throwable t) {
+ Log.e("Leanplum", "Error parsing variables", t);
+ }
+ }
+
+ private static void parseVariablesHelper(Object instance, Class<?> clazz)
+ throws IllegalArgumentException, IllegalAccessException {
+ Field[] fields = clazz.getFields();
+
+ for (final Field field : fields) {
+ String group, name;
+ boolean isFile = false;
+ if (field.isAnnotationPresent(Variable.class)) {
+ Variable annotation = field.getAnnotation(Variable.class);
+ group = annotation.group();
+ name = annotation.name();
+ } else if (field.isAnnotationPresent(File.class)) {
+ File annotation = field.getAnnotation(File.class);
+ group = annotation.group();
+ name = annotation.name();
+ isFile = true;
+ } else {
+ continue;
+ }
+
+ String variableName = name;
+ if (TextUtils.isEmpty(variableName)) {
+ variableName = field.getName();
+ }
+ if (!TextUtils.isEmpty(group)) {
+ variableName = group + "." + variableName;
+ }
+
+ Class<?> fieldType = field.getType();
+ String fieldTypeString = fieldType.toString();
+ if (fieldTypeString.equals("int")) {
+ defineVariable(instance, variableName, field.getInt(instance), "integer", field);
+ } else if (fieldTypeString.equals("byte")) {
+ defineVariable(instance, variableName, field.getByte(instance), "integer", field);
+ } else if (fieldTypeString.equals("short")) {
+ defineVariable(instance, variableName, field.getShort(instance), "integer", field);
+ } else if (fieldTypeString.equals("long")) {
+ defineVariable(instance, variableName, field.getLong(instance), "integer", field);
+ } else if (fieldTypeString.equals("char")) {
+ defineVariable(instance, variableName, field.getChar(instance), "integer", field);
+ } else if (fieldTypeString.equals("float")) {
+ defineVariable(instance, variableName, field.getFloat(instance), "float", field);
+ } else if (fieldTypeString.equals("double")) {
+ defineVariable(instance, variableName, field.getDouble(instance), "float", field);
+ } else if (fieldTypeString.equals("boolean")) {
+ defineVariable(instance, variableName, field.getBoolean(instance), "bool", field);
+ } else if (fieldType.isPrimitive()) {
+ Log.e("Variable " + variableName + " is an unsupported primitive type.");
+ } else if (fieldType.isArray()) {
+ Log.e("Variable " + variableName + " should be a List instead of an Array.");
+ } else if (fieldType.isAssignableFrom(List.class)) {
+ defineVariable(instance, variableName, field.get(instance), "list", field);
+ } else if (fieldType.isAssignableFrom(Map.class)) {
+ defineVariable(instance, variableName, field.get(instance), "group", field);
+ } else {
+ Object value = field.get(instance);
+ String stringValue = value == null ? null : value.toString();
+ if (isFile) {
+ defineFileVariable(instance, variableName, stringValue, field);
+ } else {
+ defineVariable(instance, variableName, stringValue, "string", field);
+ }
+ }
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/annotations/Variable.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Leanplum variable annotation.
+ * <p>
+ * <p>Use this to make this variable changeable from the Leanplum dashboard. Variables must be of
+ * type boolean, byte, short, int, long, float, double, char, String, List, or Map. Lists and maps
+ * may contain other lists and maps.
+ * <p>
+ * <p>Variables with this annotation update when the API call for Leanplum.start completes
+ * successfully or fails (in which case values are loaded from a cache stored on the device).
+ *
+ * @author Andrew First
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Variable {
+ /**
+ * (Optional). The group to put the variable in. Use "." to nest groups.
+ */
+ String group() default "";
+
+ /**
+ * (Optional). The name of the variable. If not set, then uses the actual name of the field.
+ */
+ String name() default "";
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/callbacks/ActionCallback.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2014, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.callbacks;
+
+import com.leanplum.ActionContext;
+
+/**
+ * Callback that gets run when an action is triggered.
+ *
+ * @author Andrew First
+ */
+public abstract class ActionCallback {
+ public abstract boolean onResponse(ActionContext context);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/callbacks/InboxChangedCallback.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.callbacks;
+
+/**
+ * Inbox changes callback.
+ *
+ * @author Anna Orlova
+ */
+public abstract class InboxChangedCallback implements Runnable {
+ public void run() {
+ this.inboxChanged();
+ }
+
+ public abstract void inboxChanged();
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/callbacks/NewsfeedChangedCallback.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2015, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.callbacks;
+
+/**
+ * Newsfeed changed callback.
+ *
+ * @author Aleksandar Gyorev
+ */
+public abstract class NewsfeedChangedCallback extends InboxChangedCallback {
+ @Override
+ public void inboxChanged() {
+ newsfeedChanged();
+ }
+
+ public abstract void newsfeedChanged();
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/callbacks/PostponableAction.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2016, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.callbacks;
+
+import com.leanplum.LeanplumActivityHelper;
+
+/**
+ * Action callback that will not be executed for activity classes that are ignored via
+ * {@link LeanplumActivityHelper#deferMessagesForActivities(Class[])}
+ *
+ * @author Ben Marten
+ */
+public abstract class PostponableAction implements Runnable {
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/callbacks/RegisterDeviceCallback.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.callbacks;
+
+/**
+ * Callback that gets run when the device needs to be registered.
+ *
+ * @author Andrew First
+ */
+public abstract class RegisterDeviceCallback implements Runnable {
+ public static abstract class EmailCallback implements Runnable {
+ private String email;
+
+ public void setResponseHandler(String email) {
+ this.email = email;
+ }
+
+ public void run() {
+ this.onResponse(email);
+ }
+
+ public abstract void onResponse(String email);
+ }
+
+ private EmailCallback callback;
+
+ public void setResponseHandler(EmailCallback callback) {
+ this.callback = callback;
+ }
+
+ public void run() {
+ this.onResponse(callback);
+ }
+
+ public abstract void onResponse(EmailCallback callback);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/callbacks/RegisterDeviceFinishedCallback.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.callbacks;
+
+/**
+ * Callback that gets run when the device has been registered.
+ *
+ * @author Andrew First
+ */
+public abstract class RegisterDeviceFinishedCallback implements Runnable {
+ private boolean success;
+
+ public void setSuccess(boolean success) {
+ this.success = success;
+ }
+
+ public void run() {
+ this.onResponse(success);
+ }
+
+ public abstract void onResponse(boolean success);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/callbacks/StartCallback.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.callbacks;
+
+/**
+ * Callback that gets run when Leanplum is started.
+ *
+ * @author Andrew First
+ */
+public abstract class StartCallback implements Runnable {
+ private boolean success;
+
+ public void setSuccess(boolean success) {
+ this.success = success;
+ }
+
+ public void run() {
+ this.onResponse(success);
+ }
+
+ public abstract void onResponse(boolean success);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/callbacks/VariableCallback.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.callbacks;
+
+import com.leanplum.Var;
+
+/**
+ * Leanplum variable callback.
+ *
+ * @author Andrew First
+ */
+public abstract class VariableCallback<T> implements Runnable {
+ private Var<T> variable;
+
+ public void setVariable(Var<T> variable) {
+ this.variable = variable;
+ }
+
+ public void run() {
+ this.handle(variable);
+ }
+
+ public abstract void handle(Var<T> variable);
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/callbacks/VariablesChangedCallback.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.callbacks;
+
+/**
+ * Variables changed callback.
+ *
+ * @author Andrew First
+ */
+public abstract class VariablesChangedCallback implements Runnable {
+ public void run() {
+ this.variablesChanged();
+ }
+
+ public abstract void variablesChanged();
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/internal/AESCrypt.java
@@ -0,0 +1,311 @@
+/*
+ * Copyright 2013, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.internal;
+
+import android.content.SharedPreferences;
+import android.util.Pair;
+
+import java.io.UnsupportedEncodingException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.KeySpec;
+import java.util.Arrays;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+
+/**
+ * AES Encryption as detailed at
+ * http://nelenkov.blogspot.com/2012/04/using-password-based-encryption-on.html
+ *
+ * @author Aakash Patel
+ */
+public class AESCrypt {
+
+ private static enum EncryptionType {
+ /**
+ * Encryption based on a token received from the server. Used in SDK versions prior to 1.2.20.
+ * <p>
+ * Corresponds to ciphertexts of the format "[12, -33, 52]", corresponding to Arrays.toString()
+ * of an encrypted byte[].
+ * <p>
+ * Legacy values may decrypt to ciphertexts. We ignore what appear to be double-encrypted
+ * ciphertexts that are stored in a legacy format.
+ */
+ LEGACY_TOKEN(0),
+
+ /**
+ * Encryption based on the app ID. Used in SDK versions since 1.2.20.
+ * <p>
+ * Corresponds to ciphertexts of the format "01[12, -33, 52]". The format adds a version
+ * identifier ("01") prefix to ciphertexts, allowing us to change the format in the future.
+ * <p>
+ * With the exception of LEGACY_TOKEN ciphertexts, which must continue to be supported, we will
+ * use the first two characters to determine the encryption protocol.
+ */
+ APP_ID_KEY(1);
+
+ public final int id;
+ public final String prefix;
+ public final String prefixWithBracket;
+
+ EncryptionType(int id) {
+ this.id = id;
+ prefix = String.format("%02d", id);
+ prefixWithBracket = prefix + "[";
+ }
+
+ private static EncryptionType forId(int id) {
+ if (id == 1) {
+ return APP_ID_KEY;
+ }
+ return null;
+ }
+
+ public static Pair<EncryptionType, String> parseCipherText(String cipherText) {
+ if (cipherText == null || cipherText.isEmpty()) {
+ return null;
+ }
+ if (cipherText.startsWith("[")) {
+ return Pair.create(LEGACY_TOKEN, cipherText);
+ }
+ if (cipherText.startsWith(APP_ID_KEY.prefixWithBracket)) {
+ return Pair.create(
+ APP_ID_KEY, cipherText.substring(APP_ID_KEY.prefixWithBracket.length() - 1));
+ }
+ return null;
+ }
+ }
+
+ // Build prefix and suffix strings longhand, to obfuscate them slightly.
+ // Probably doesn't matter.
+ private static final String APP_ID_KEY_PREFIX = new StringBuilder()
+ .append("L").append("q").append(3).append("f").append("z").toString();
+ private static final String APP_ID_KEY_SUFFIX = new StringBuilder()
+ .append("b").append("L").append("t").append("i").append(2).toString();
+
+ private final String appId;
+ private final String token;
+
+ /**
+ * Creates an AESCrypt encryption context.
+ * <p>
+ * Intended for short-term use, since the encryption token can change.
+ */
+ public AESCrypt(String appId, String token) {
+ this.appId = appId;
+ this.token = token;
+ }
+
+ private String appIdKeyPassword() {
+ return APP_ID_KEY_PREFIX + appId + APP_ID_KEY_SUFFIX;
+ }
+
+ /**
+ * Creates a ciphertext using a password based on current context parameters.
+ *
+ * @param plaintext
+ * @return A cipher text string, or null if encryption fails (unexpected).
+ */
+ public String encrypt(String plaintext) {
+ if (plaintext == null) {
+ return null;
+ }
+ // Always encrypt using the APP_ID_KEY method.
+ if (appId == null || appId.isEmpty()) {
+ Log.e("Encrypt called with null appId.");
+ return null;
+ }
+ String cipherText = encryptInternal(appIdKeyPassword(), plaintext);
+ if (cipherText == null) {
+ Log.w("Failed to encrypt.");
+ return null;
+ }
+ if (cipherText.isEmpty() || cipherText.equals(plaintext) || !cipherText.startsWith("[")) {
+ Log.w("Invalid ciphertext: " + cipherText);
+ return null;
+ }
+ return EncryptionType.APP_ID_KEY.prefix + cipherText;
+ }
+
+ public String decodePreference(SharedPreferences preferences, String key, String defaultValue) {
+ String cipherText = preferences.getString(key, null);
+ if (cipherText == null) {
+ return defaultValue;
+ }
+ String decoded = decrypt(cipherText);
+ if (decoded == null) {
+ return defaultValue;
+ }
+ return decoded;
+ }
+
+ /**
+ * Decrypts a ciphertext in either legacy or current format, using a password based on context
+ * parameters.
+ *
+ * @param cipherText The value to encrypt; tolerates null.
+ * @return A cipher text string, or null if the value can't be decrypted.
+ */
+ public String decrypt(String cipherText) {
+ Pair<EncryptionType, String> encryptionSpec = EncryptionType.parseCipherText(cipherText);
+ String result = null;
+ if (encryptionSpec == null) {
+ Log.v("Got null encryptionSpec for encrypted: " + cipherText);
+ } else {
+ switch (encryptionSpec.first) {
+ case LEGACY_TOKEN:
+ if (token == null || token.isEmpty()) {
+ Log.e("Decrypt called with null token.");
+ } else {
+ result = decryptInternal(token, encryptionSpec.second);
+ // For legacy keys only -- detect if the value we decode is a valid legacy ciphertext.
+ // If so, it was almost certainly produced by legacy decryption, which would return
+ // ciphertext on decryption failure. Discard the value and return null.
+ if (result != null && parseCiphertextInternal(result) != null) {
+ Log.e("Discarding legacy value that appears to be an encrypted value: " +
+ result);
+ return null;
+ }
+ }
+ break;
+ case APP_ID_KEY:
+ if (appId == null || appId.isEmpty()) {
+ Log.e("Decrypt called with null appId.");
+ } else {
+ result = decryptInternal(appIdKeyPassword(), encryptionSpec.second);
+ }
+ break;
+ }
+ }
+ if (result == null) {
+ Log.w("Unable to decrypt " + cipherText);
+ }
+ return result;
+ }
+
+ /**
+ * Encrypts the plaintext using password. In case of exception, returns null.
+ */
+ // VisibleForTesting
+ public static String encryptInternal(String password, String plaintext) {
+ try {
+ return Arrays.toString(performCryptOperation(Cipher.ENCRYPT_MODE, password,
+ plaintext.getBytes("UTF-8")));
+ } catch (UnsupportedEncodingException e) {
+ Log.w("Unable to encrypt " + plaintext, e);
+ return null;
+ }
+ }
+
+ private static byte[] parseCiphertextInternal(String ciphertext) {
+ if (ciphertext == null) {
+ return null;
+ }
+ ciphertext = ciphertext.trim();
+ if (ciphertext.length() < 2) {
+ return null;
+ }
+ try {
+ String[] byteStrings =
+ ciphertext.substring(1, ciphertext.length() - 1).trim().split("\\s*,\\s*");
+ byte[] bytes = new byte[byteStrings.length];
+ for (int i = 0; i < byteStrings.length; i++) {
+ bytes[i] = Byte.parseByte(byteStrings[i]);
+ }
+ return bytes;
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Decrypts the ciphertext using password. In case of exception, returns null.
+ *
+ * @param ciphertext Must be a valid byte array represented as a string as returned by
+ * Arrays.toString().
+ */
+ private static String decryptInternal(String password, String ciphertext) {
+ try {
+ byte[] bytes = parseCiphertextInternal(ciphertext);
+ if (bytes == null) {
+ Log.w("Invalid ciphertext: " + ciphertext);
+ return null;
+ }
+ byte[] byteResult = performCryptOperation(Cipher.DECRYPT_MODE, password, bytes);
+ if (byteResult != null) {
+ return new String(byteResult, "UTF-8");
+ }
+ } catch (UnsupportedEncodingException e) {
+ // Unreachable on android, which guarantees UTF-8 support.
+ Log.w("Could not encode UTF8 string.\n" + Log.getStackTraceString(e));
+ }
+ return null;
+ }
+
+ /**
+ * Performs either an encryption or a decryption based on the mode. In case of exception, returns
+ * null.
+ *
+ * @param mode Should be either Cipher.ENCRYPT_MODE or Cipher.DECRYPT_MODE
+ * @param password The password to crypt.
+ * @param text The text to crypt.
+ * @return The result of the crypt.
+ */
+ private static byte[] performCryptOperation(int mode, String password, byte[] text) {
+ byte[] result = null;
+ try {
+ byte[] SALT = Constants.Crypt.SALT.getBytes("UTF-8");
+ byte[] IV = Constants.Crypt.IV.getBytes("UTF-8");
+ KeySpec keySpec = new PBEKeySpec(password.toCharArray(), SALT, Constants.Crypt.ITER_COUNT,
+ Constants.Crypt.KEY_LENGTH);
+ SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBEWithMD5And128BitAES-CBC-OpenSSL");
+ byte[] keyBytes = keyFactory.generateSecret(keySpec).getEncoded();
+ SecretKey key = new SecretKeySpec(keyBytes, "AES");
+
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ IvParameterSpec ivParams = new IvParameterSpec(IV);
+ cipher.init(mode, key, ivParams);
+
+ result = cipher.doFinal(text);
+ } catch (InvalidKeyException e) {
+ // Don't log exceptions; we have more useful warning logs when this returns null.
+ } catch (NoSuchAlgorithmException e) {
+ } catch (NoSuchPaddingException e) {
+ } catch (InvalidAlgorithmParameterException e) {
+ } catch (IllegalBlockSizeException e) {
+ } catch (BadPaddingException e) {
+ } catch (UnsupportedEncodingException e) {
+ } catch (InvalidKeySpecException e) {
+ }
+ return result;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/internal/ActionArg.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2014, Leanplum, Inc. All rights reserved.
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.leanplum.internal;
+
+import android.text.TextUtils;
+
+import java.io.InputStream;
+
+/**
+ * Represents an argument for a message or action.
+ *
+ * @param <T> Type of the argument. Can be Boolean, Byte, Short, Integer, Long, Float, Double,
+ * Character, String, List, or Map.
+ * @author Andrew First
+ */
+public class ActionArg<T> {
+ private String name;
+ private String kind;
+ private T defaultValue;
+ private boolean isAsset;
+
+ private ActionArg() {
+ }
+
+ private static <T> ActionArg<T> argNamed(String name, T defaultValue, String kind) {
+ ActionArg<T> arg = new ActionArg<>();
+ arg.name = name;
+ arg.kind = kind;
+ arg.defaultValue = defaultValue;
+ return arg;
+ }
+
+ /**
+ * Creates an instance of a new arg with a default value.
+ *
+ * @param name Name of the arg.
+ * @param defaultValue Default value of the arg. Can't be null.
+ */
+ public static <T> ActionArg<T> argNamed(String name, T defaultValue) {
+ return argNamed(name, defaultValue, VarCache.kindFromValue(defaultValue));
+ }
+
+ public static ActionArg<Integer> colorArgNamed(String name, int defaultValue) {
+ return argNamed(name, defaultValue, Constants.Kinds.COLOR);
+ }
+
+ public static ActionArg<String> fileArgNamed(String name, String defaultFilename) {
+ if (TextUtils.isEmpty(defaultFilename)) {
+ defaultFilename = "";
+ }
+ ActionArg<String> arg = argNamed(name, defaultFilename, Constants.Kinds.FILE);
+ VarCache.registerFile(arg.defaultValue, arg.defaultValue,
+ arg.defaultStream(), false, null, 0);
+ return arg;
+ }
+
+ public static ActionArg<String> assetArgNamed(String name, String defaultFilename) {
+ ActionArg<String> arg = argNamed(name, defaultFilename, Constants.Kinds.FILE);
+ arg.isAsset = true;
+ VarCache.registerFile(arg.defaultValue, arg.defaultValue,
+ arg.defaultStream(), false, null, 0);
+ return arg;
+ }
+
+ public static ActionArg<String> actionArgNamed(String name, String defaultValue) {
+ if (TextUtils.isEmpty(defaultValue)) {
+ defaultValue = "";
+ }
+ return argNamed(name, defaultValue, Constants.Kinds.ACTION);
+ }
+
+ public String name() {
+ return name;
+ }
+
+ public String kind() {
+ return kind;
+ }
+
+ public T defaultValue() {
+ return defaultValue;
+ }
+
+ public InputStream defaultStream() {
+ if (!kind.equals(Constants.Kinds.FILE)) {
+ return null;
+ }
+ return FileManager.stream(false, isAsset, isAsset,
+ (String) defaultValue, (String) defaultValue, null);