--- 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);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/internal/ActionManager.java
@@ -0,0 +1,642 @@
+/*
+ * Copyright 2014, Leanplum, Inc. All rights reserved.
+ *
+ * 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.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+
+import com.leanplum.ActionContext;
+import com.leanplum.ActionContext.ContextualValues;
+import com.leanplum.Leanplum;
+import com.leanplum.LeanplumLocalPushListenerService;
+import com.leanplum.LeanplumPushService;
+import com.leanplum.LocationManager;
+import com.leanplum.callbacks.ActionCallback;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Handles in-app and push messaging.
+ *
+ * @author Andrew First
+ */
+public class ActionManager {
+ private Map<String, Map<String, Number>> messageImpressionOccurrences;
+ private Map<String, Number> messageTriggerOccurrences;
+ private Map<String, Number> sessionOccurrences;
+
+ private static ActionManager instance;
+
+ public static final String PUSH_NOTIFICATION_ACTION_NAME = "__Push Notification";
+ public static final String HELD_BACK_ACTION_NAME = "__held_back";
+
+ private static final String PREFERENCES_NAME = "__leanplum_messaging__";
+
+ public static class MessageMatchResult {
+ public boolean matchedTrigger;
+ public boolean matchedUnlessTrigger;
+ public boolean matchedLimit;
+ }
+
+ public static synchronized ActionManager getInstance() {
+ if (instance == null) {
+ instance = new ActionManager();
+ }
+ return instance;
+ }
+
+ private static boolean loggedLocationManagerFailure = false;
+
+ public static LocationManager getLocationManager() {
+ if (Util.hasPlayServices()) {
+ loggedLocationManagerFailure = true;
+ }
+ return null;
+ }
+
+ private ActionManager() {
+ listenForLocalNotifications();
+ sessionOccurrences = new HashMap<>();
+ messageImpressionOccurrences = new HashMap<>();
+ messageTriggerOccurrences = new HashMap<>();
+ }
+
+ private void listenForLocalNotifications() {
+ Leanplum.onAction(PUSH_NOTIFICATION_ACTION_NAME, new ActionCallback() {
+ @Override
+ public boolean onResponse(ActionContext actionContext) {
+ try {
+ String messageId = actionContext.getMessageId();
+
+ // Get eta.
+ Object countdownObj;
+ if (((BaseActionContext) actionContext).isPreview()) {
+ countdownObj = 5.0;
+ } else {
+ Map<String, Object> messageConfig = CollectionUtil.uncheckedCast(
+ VarCache.getMessageDiffs().get(messageId));
+ if (messageConfig == null) {
+ Log.e("Could not find message options for ID " + messageId);
+ return false;
+ }
+ countdownObj = messageConfig.get("countdown");
+ }
+ if (!(countdownObj instanceof Number)) {
+ Log.e("Invalid notification countdown: " + countdownObj);
+ return false;
+ }
+ long eta = System.currentTimeMillis() + ((Number) countdownObj).longValue() * 1000L;
+
+ Context context = Leanplum.getContext();
+ Intent intentAlarm = new Intent(context, LeanplumLocalPushListenerService.class);
+ AlarmManager alarmManager = (AlarmManager) context.getSystemService(
+ Context.ALARM_SERVICE);
+
+ // If there's already one scheduled before the eta, discard this.
+ // Otherwise, discard the scheduled one.
+ SharedPreferences preferences = context.getSharedPreferences(
+ PREFERENCES_NAME, Context.MODE_PRIVATE);
+ long existingEta = preferences.getLong(String.format(
+ Constants.Defaults.LOCAL_NOTIFICATION_KEY, messageId), 0L);
+ if (existingEta > 0L && existingEta > System.currentTimeMillis()) {
+ if (existingEta < eta) {
+ return false;
+ } else if (existingEta >= eta) {
+ PendingIntent existingIntent = PendingIntent.getService(
+ context, messageId.hashCode(), intentAlarm,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ alarmManager.cancel(existingIntent);
+ }
+ }
+
+ // Specify custom data for the notification
+ Map<String, Serializable> data = actionContext.objectNamed("Advanced options.Data");
+ if (data != null) {
+ for (String key : data.keySet()) {
+ intentAlarm.putExtra(key, data.get(key));
+ }
+ }
+
+ // Specify open action
+ String openAction = actionContext.stringNamed(Constants.Values.DEFAULT_PUSH_ACTION);
+ boolean muteInsideApp = Boolean.TRUE.equals(actionContext.objectNamed(
+ "Advanced options.Mute inside app"));
+ if (openAction != null) {
+ if (muteInsideApp) {
+ intentAlarm.putExtra(Constants.Keys.PUSH_MESSAGE_ID_MUTE_WITH_ACTION, messageId);
+ } else {
+ intentAlarm.putExtra(Constants.Keys.PUSH_MESSAGE_ID_NO_MUTE_WITH_ACTION, messageId);
+ }
+ } else {
+ if (muteInsideApp) {
+ intentAlarm.putExtra(Constants.Keys.PUSH_MESSAGE_ID_MUTE, messageId);
+ } else {
+ intentAlarm.putExtra(Constants.Keys.PUSH_MESSAGE_ID_NO_MUTE, messageId);
+ }
+ }
+
+ // Message.
+ String message = actionContext.stringNamed("Message");
+ intentAlarm.putExtra(Constants.Keys.PUSH_MESSAGE_TEXT,
+ message != null ? message : Constants.Values.DEFAULT_PUSH_MESSAGE);
+
+ // Collapse key.
+ String collapseKey = actionContext.stringNamed("Android options.Collapse key");
+ if (collapseKey != null) {
+ intentAlarm.putExtra("collapseKey", collapseKey);
+ }
+
+ // Delay while idle.
+ boolean delayWhileIdle = Boolean.TRUE.equals(actionContext.objectNamed(
+ "Android options.Delay while idle"));
+ if (delayWhileIdle) {
+ intentAlarm.putExtra("delayWhileIdle", true);
+ }
+
+ // Schedule notification.
+ PendingIntent operation = PendingIntent.getService(
+ context, messageId.hashCode(), intentAlarm,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ alarmManager.set(AlarmManager.RTC_WAKEUP, eta, operation);
+
+ // Save notification so we can cancel it later.
+ SharedPreferences.Editor editor = preferences.edit();
+ editor.putLong(String.format(Constants.Defaults.LOCAL_NOTIFICATION_KEY, messageId), eta);
+ try {
+ editor.apply();
+ } catch (NoSuchMethodError e) {
+ editor.commit();
+ }
+
+ Log.i("Scheduled notification");
+ return true;
+ } catch (Throwable t) {
+ Util.handleException(t);
+ return false;
+ }
+ }
+ });
+
+ Leanplum.onAction("__Cancel" + PUSH_NOTIFICATION_ACTION_NAME, new ActionCallback() {
+ @Override
+ public boolean onResponse(ActionContext actionContext) {
+ try {
+ String messageId = actionContext.getMessageId();
+
+ // Get existing eta and clear notification from preferences.
+ Context context = Leanplum.getContext();
+ SharedPreferences preferences = context.getSharedPreferences(
+ PREFERENCES_NAME, Context.MODE_PRIVATE);
+ String preferencesKey = String.format(Constants.Defaults.LOCAL_NOTIFICATION_KEY, messageId);
+ long existingEta = preferences.getLong(preferencesKey, 0L);
+ SharedPreferences.Editor editor = preferences.edit();
+ editor.remove(preferencesKey);
+ try {
+ editor.apply();
+ } catch (NoSuchMethodError e) {
+ editor.commit();
+ }
+
+ // Cancel notification.
+ Intent intentAlarm = new Intent(context, LeanplumPushService.class);
+ AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ PendingIntent existingIntent = PendingIntent.getService(
+ context, messageId.hashCode(), intentAlarm, PendingIntent.FLAG_UPDATE_CURRENT);
+ alarmManager.cancel(existingIntent);
+
+ boolean didCancel = existingEta > System.currentTimeMillis();
+ if (didCancel) {
+ Log.i("Cancelled notification");
+ }
+ return didCancel;
+ } catch (Throwable t) {
+ Util.handleException(t);
+ return false;
+ }
+ }
+ });
+ }
+
+ public Map<String, Number> getMessageImpressionOccurrences(String messageId) {
+ Map<String, Number> occurrences = messageImpressionOccurrences.get(messageId);
+ if (occurrences != null) {
+ return occurrences;
+ }
+ Context context = Leanplum.getContext();
+ SharedPreferences preferences = context.getSharedPreferences(
+ PREFERENCES_NAME, Context.MODE_PRIVATE);
+ String savedValue = preferences.getString(
+ String.format(Constants.Defaults.MESSAGE_IMPRESSION_OCCURRENCES_KEY, messageId),
+ "{}");
+ occurrences = CollectionUtil.uncheckedCast(JsonConverter.fromJson(savedValue));
+ messageImpressionOccurrences.put(messageId, occurrences);
+ return occurrences;
+ }
+
+ public void saveMessageImpressionOccurrences(Map<String, Number> occurrences, String messageId) {
+ Context context = Leanplum.getContext();
+ SharedPreferences preferences = context.getSharedPreferences(
+ PREFERENCES_NAME, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = preferences.edit();
+ editor.putString(
+ String.format(Constants.Defaults.MESSAGE_IMPRESSION_OCCURRENCES_KEY, messageId),
+ JsonConverter.toJson(occurrences));
+ messageImpressionOccurrences.put(messageId, occurrences);
+ try {
+ editor.apply();
+ } catch (NoSuchMethodError e) {
+ editor.commit();
+ }
+ }
+
+ public int getMessageTriggerOccurrences(String messageId) {
+ Number occurrences = messageTriggerOccurrences.get(messageId);
+ if (occurrences != null) {
+ return occurrences.intValue();
+ }
+ Context context = Leanplum.getContext();
+ SharedPreferences preferences = context.getSharedPreferences(
+ PREFERENCES_NAME, Context.MODE_PRIVATE);
+ int savedValue = preferences.getInt(
+ String.format(Constants.Defaults.MESSAGE_TRIGGER_OCCURRENCES_KEY, messageId), 0);
+ messageTriggerOccurrences.put(messageId, savedValue);
+ return savedValue;
+ }
+
+ public void saveMessageTriggerOccurrences(int occurrences, String messageId) {
+ Context context = Leanplum.getContext();
+ SharedPreferences preferences = context.getSharedPreferences(
+ PREFERENCES_NAME, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = preferences.edit();
+ editor.putInt(
+ String.format(Constants.Defaults.MESSAGE_TRIGGER_OCCURRENCES_KEY, messageId), occurrences);
+ messageTriggerOccurrences.put(messageId, occurrences);
+ try {
+ editor.apply();
+ } catch (NoSuchMethodError e) {
+ editor.commit();
+ }
+ }
+
+ public MessageMatchResult shouldShowMessage(String messageId, Map<String, Object> messageConfig,
+ String when, String eventName, ContextualValues contextualValues) {
+ MessageMatchResult result = new MessageMatchResult();
+
+ // 1. Must not be muted.
+ Context context = Leanplum.getContext();
+ SharedPreferences preferences = context.getSharedPreferences(
+ PREFERENCES_NAME, Context.MODE_PRIVATE);
+ if (preferences.getBoolean(
+ String.format(Constants.Defaults.MESSAGE_MUTED_KEY, messageId), false)) {
+ return result;
+ }
+
+ // 2. Must match at least one trigger.
+ result.matchedTrigger = matchedTriggers(messageConfig.get("whenTriggers"), when, eventName,
+ contextualValues);
+ result.matchedUnlessTrigger = matchedTriggers(messageConfig.get("unlessTriggers"), when, eventName,
+ contextualValues);
+ if (!result.matchedTrigger && !result.matchedUnlessTrigger) {
+ return result;
+ }
+
+ // 3. Must match all limit conditions.
+ Object limitConfigObj = messageConfig.get("whenLimits");
+ Map<String, Object> limitConfig = null;
+ if (limitConfigObj instanceof Map<?, ?>) {
+ limitConfig = CollectionUtil.uncheckedCast(limitConfigObj);
+ }
+ result.matchedLimit = matchesLimits(messageId, limitConfig);
+ return result;
+ }
+
+ private boolean matchesLimits(String messageId, Map<String, Object> limitConfig) {
+ if (limitConfig == null) {
+ return true;
+ }
+ List<Object> limits = CollectionUtil.uncheckedCast(limitConfig.get("children"));
+ if (limits.isEmpty()) {
+ return true;
+ }
+ Map<String, Number> impressionOccurrences = getMessageImpressionOccurrences(messageId);
+ int triggerOccurrences = getMessageTriggerOccurrences(messageId) + 1;
+ for (Object limitObj : limits) {
+ Map<String, Object> limit = CollectionUtil.uncheckedCast(limitObj);
+ String subject = limit.get("subject").toString();
+ String noun = limit.get("noun").toString();
+ String verb = limit.get("verb").toString();
+
+ // E.g. 5 times per session; 2 times per 7 minutes.
+ if (subject.equals("times")) {
+ List<Object> objects = CollectionUtil.uncheckedCast(limit.get("objects"));
+ int perTimeUnit = objects.size() > 0 ?
+ Integer.parseInt(objects.get(0).toString()) : 0;
+ if (!matchesLimitTimes(Integer.parseInt(noun),
+ perTimeUnit, verb, impressionOccurrences, messageId)) {
+ return false;
+ }
+
+ // E.g. On the 5th occurrence.
+ } else if (subject.equals("onNthOccurrence")) {
+ int amount = Integer.parseInt(noun);
+ if (triggerOccurrences != amount) {
+ return false;
+ }
+
+ // E.g. Every 5th occurrence.
+ } else if (subject.equals("everyNthOccurrence")) {
+ int multiple = Integer.parseInt(noun);
+ if (multiple == 0 || triggerOccurrences % multiple != 0) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ private boolean matchesLimitTimes(int amount, int time, String units,
+ Map<String, Number> occurrences, String messageId) {
+ Number existing = 0L;
+ if (units.equals("limitSession")) {
+ existing = sessionOccurrences.get(messageId);
+ if (existing == null) {
+ existing = 0L;
+ }
+ } else {
+ if (occurrences == null || occurrences.isEmpty()) {
+ return true;
+ }
+ Number min = occurrences.get("min");
+ Number max = occurrences.get("max");
+ if (min == null) {
+ min = 0L;
+ }
+ if (max == null) {
+ max = 0L;
+ }
+ if (units.equals("limitUser")) {
+ existing = max.longValue() - min.longValue() + 1;
+ } else {
+ if (units.equals("limitMinute")) {
+ time *= 60;
+ } else if (units.equals("limitHour")) {
+ time *= 3600;
+ } else if (units.equals("limitDay")) {
+ time *= 86400;
+ } else if (units.equals("limitWeek")) {
+ time *= 604800;
+ } else if (units.equals("limitMonth")) {
+ time *= 2592000;
+ }
+ long now = System.currentTimeMillis();
+ int matchedOccurrences = 0;
+ for (long i = max.longValue(); i >= min.longValue(); i--) {
+ if (occurrences.containsKey("" + i)) {
+ long timeAgo = (now - occurrences.get("" + i).longValue()) / 1000;
+ if (timeAgo > time) {
+ break;
+ }
+ matchedOccurrences++;
+ if (matchedOccurrences >= amount) {
+ return false;
+ }
+ }
+ }
+ }
+ }
+ return existing.longValue() < amount;
+ }
+
+ private boolean matchedTriggers(Object triggerConfigObj, String when, String eventName,
+ ContextualValues contextualValues) {
+ if (triggerConfigObj instanceof Map<?, ?>) {
+ Map<String, Object> triggerConfig = CollectionUtil.uncheckedCast(triggerConfigObj);
+ List<Object> triggers = CollectionUtil.uncheckedCast(triggerConfig.get("children"));
+ for (Object triggerObj : triggers) {
+ Map<String, Object> trigger = CollectionUtil.uncheckedCast(triggerObj);
+ if (matchedTrigger(trigger, when, eventName, contextualValues)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private boolean matchedTrigger(Map<String, Object> trigger, String when, String eventName,
+ ContextualValues contextualValues) {
+ String subject = (String) trigger.get("subject");
+ if (subject.equals(when)) {
+ String noun = (String) trigger.get("noun");
+ if ((noun == null && eventName == null) || (noun != null && noun.equals(eventName))) {
+ String verb = (String) trigger.get("verb");
+ List<Object> objects = CollectionUtil.uncheckedCast(trigger.get("objects"));
+
+ // Evaluate user attribute changed to value.
+ if ("changesTo".equals(verb)) {
+ if (contextualValues != null && objects != null) {
+ for (Object object : objects) {
+ if ((object == null && contextualValues.attributeValue == null) ||
+ (object != null && object.toString().equalsIgnoreCase(
+ contextualValues.attributeValue.toString()))) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ // Evaluate user attribute changed from value to value.
+ if ("changesFromTo".equals(verb)) {
+ return contextualValues != null &&
+ objects.size() == 2 && objects.get(0) != null && objects.get(1) != null &&
+ contextualValues.previousAttributeValue != null &&
+ contextualValues.attributeValue != null &&
+ objects.get(0).toString().equalsIgnoreCase(
+ contextualValues.previousAttributeValue.toString()) &&
+ objects.get(1).toString().equalsIgnoreCase(
+ contextualValues.attributeValue.toString());
+ }
+
+ // Evaluate event parameter is value.
+ if ("triggersWithParameter".equals(verb)) {
+ if (contextualValues != null &&
+ objects.size() == 2 && objects.get(0) != null && objects.get(1) != null &&
+ contextualValues.parameters != null) {
+ Object parameterValue = contextualValues.parameters.get(objects.get(0));
+ return parameterValue != null && parameterValue.toString().equalsIgnoreCase(
+ objects.get(1).toString());
+ }
+ return false;
+ }
+
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public void recordMessageTrigger(String messageId) {
+ int occurrences = getMessageTriggerOccurrences(messageId);
+ occurrences++;
+ saveMessageTriggerOccurrences(occurrences, messageId);
+ }
+
+ /**
+ * Tracks the "Held Back" event for a message and records the held back occurrences.
+ *
+ * @param messageId The spoofed ID of the message.
+ * @param originalMessageId The original ID of the held back message.
+ */
+ public void recordHeldBackImpression(String messageId, String originalMessageId) {
+ recordImpression(messageId, originalMessageId);
+ }
+
+ /**
+ * Tracks the "Open" event for a message and records it's occurrence.
+ *
+ * @param messageId The ID of the message
+ */
+ public void recordMessageImpression(String messageId) {
+ recordImpression(messageId, null);
+ }
+
+ /**
+ * Records the occurrence of a message and tracks the correct impression event.
+ *
+ * @param messageId The ID of the message.
+ * @param originalMessageId The original message ID of the held back message. Supply this only if
+ * the message is held back. Otherwise, use null.
+ */
+ private void recordImpression(String messageId, String originalMessageId) {
+ Map<String, String> requestArgs = new HashMap<>();
+ if (originalMessageId != null) {
+ // This is a held back message - track the event with the original message ID.
+ requestArgs.put(Constants.Params.MESSAGE_ID, originalMessageId);
+ LeanplumInternal.track(Constants.HELD_BACK_EVENT_NAME, 0.0, null, null, requestArgs);
+ } else {
+ // Track the message impression and occurrence.
+ requestArgs.put(Constants.Params.MESSAGE_ID, messageId);
+ LeanplumInternal.track(null, 0.0, null, null, requestArgs);
+ }
+
+ // Record session occurrences.
+ Number existing = sessionOccurrences.get(messageId);
+ if (existing == null) {
+ existing = 0L;
+ }
+ existing = existing.longValue() + 1L;
+ sessionOccurrences.put(messageId, existing);
+
+ // Record cross-session occurrences.
+ Map<String, Number> occurrences = getMessageImpressionOccurrences(messageId);
+ if (occurrences == null || occurrences.isEmpty()) {
+ occurrences = new HashMap<>();
+ occurrences.put("min", 0L);
+ occurrences.put("max", 0L);
+ occurrences.put("0", System.currentTimeMillis());
+ } else {
+ Number min = occurrences.get("min");
+ Number max = occurrences.get("max");
+ if (min == null) {
+ min = 0L;
+ }
+ if (max == null) {
+ max = 0L;
+ }
+ max = max.longValue() + 1L;
+ occurrences.put("" + max, System.currentTimeMillis());
+ if (max.longValue() - min.longValue() + 1 >
+ Constants.Messaging.MAX_STORED_OCCURRENCES_PER_MESSAGE) {
+ occurrences.remove("" + min);
+ min = min.longValue() + 1L;
+ occurrences.put("min", min);
+ }
+ occurrences.put("max", max);
+ }
+ saveMessageImpressionOccurrences(occurrences, messageId);
+ }
+
+ public void muteFutureMessagesOfKind(String messageId) {
+ if (messageId != null) {
+ Context context = Leanplum.getContext();
+ SharedPreferences preferences = context.getSharedPreferences(
+ PREFERENCES_NAME, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = preferences.edit();
+ editor.putBoolean(
+ String.format(Constants.Defaults.MESSAGE_MUTED_KEY, messageId),
+ true);
+ try {
+ editor.apply();
+ } catch (NoSuchMethodError e) {
+ editor.commit();
+ }
+ }
+ }
+
+
+ public static void getForegroundandBackgroundRegionNames(Set<String> foregroundRegionNames,
+ Set<String> backgroundRegionNames) {
+ Map<String, Object> messages = VarCache.messages();
+ for (String messageId : messages.keySet()) {
+ Map<String, Object> messageConfig = CollectionUtil.uncheckedCast(messages.get(messageId));
+ Set<String> regionNames;
+ Object action = messageConfig.get("action");
+ if (action instanceof String) {
+ if (action.equals(PUSH_NOTIFICATION_ACTION_NAME)) {
+ regionNames = backgroundRegionNames;
+ } else {
+ regionNames = foregroundRegionNames;
+ }
+
+ Map<String, Object> whenTriggers = CollectionUtil.uncheckedCast(messageConfig.get
+ ("whenTriggers"));
+ Map<String, Object> unlessTriggers = CollectionUtil.uncheckedCast(messageConfig.get
+ ("unlessTriggers"));
+
+ addRegionNamesFromTriggersToSet(whenTriggers, regionNames);
+ addRegionNamesFromTriggersToSet(unlessTriggers, regionNames);
+ }
+ }
+ }
+
+ public static void addRegionNamesFromTriggersToSet(
+ Map<String, Object> triggerConfig, Set<String> set) {
+ if (triggerConfig == null) {
+ return;
+ }
+ List<Map<String, Object>> triggers = CollectionUtil.uncheckedCast(triggerConfig.get
+ ("children"));
+ for (Map<String, Object> trigger : triggers) {
+ String subject = (String) trigger.get("subject");
+ if (subject.equals("enterRegion") || subject.equals("exitRegion")) {
+ set.add((String) trigger.get("noun"));
+ }
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/internal/BaseActionContext.java
@@ -0,0 +1,71 @@
+/*
+ * 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.internal;
+
+import java.util.Map;
+
+/**
+ * Base class for ActionContext that contains internal methods.
+ *
+ * @author Andrew First
+ */
+public abstract class BaseActionContext {
+ protected String messageId = null;
+ protected String originalMessageId = null;
+ protected int priority;
+ protected Map<String, Object> args;
+ protected boolean isRooted = true;
+ private boolean isPreview = false;
+
+ public BaseActionContext(String messageId, String originalMessageId) {
+ this.messageId = messageId;
+ this.originalMessageId = originalMessageId;
+ }
+
+ void setIsRooted(boolean value) {
+ isRooted = value;
+ }
+
+ void setIsPreview(boolean isPreview) {
+ this.isPreview = isPreview;
+ }
+
+ boolean isPreview() {
+ return isPreview;
+ }
+
+ public String getMessageId() {
+ return messageId;
+ }
+
+ public String getOriginalMessageId() {
+ return originalMessageId;
+ }
+
+ public int getPriority() {
+ return priority;
+ }
+
+ public Map<String, Object> getArgs() {
+ return args;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/internal/CollectionUtil.java
@@ -0,0 +1,175 @@
+/*
+ * 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.internal;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Helper class to easily create new list, map or set objects containing provided parameters.
+ *
+ * @author Ben Marten
+ */
+public class CollectionUtil {
+ /**
+ * Creates a new ArrayList and adds the passed arguments to it.
+ *
+ * @param items The items to add to the list.
+ * @param <T> The type of the list to be created.
+ * @return A typed list that contains the passed arguments.
+ */
+ @SafeVarargs
+ public static <T> ArrayList<T> newArrayList(T... items) {
+ ArrayList<T> result = new ArrayList<>((items != null) ? items.length : 0);
+ if (items != null) {
+ Collections.addAll(result, items);
+ }
+ return result;
+ }
+
+ /**
+ * Creates a new HashSet and adds the passed arguments to it.
+ *
+ * @param items The items to add to the set.
+ * @param <T> The type of the set to be created.
+ * @return A typed set that contains the passed arguments.
+ */
+ @SafeVarargs
+ static <T> HashSet<T> newHashSet(T... items) {
+ HashSet<T> result = new HashSet<>((items != null) ? items.length : 0);
+ if (items != null) {
+ Collections.addAll(result, items);
+ }
+ return result;
+ }
+
+ /**
+ * Creates a new HashMap and adds the passed arguments to it in pairs.
+ *
+ * @param items The keys and values, to add to the map in pairs.
+ * @param <T> The type of the map to be created.
+ * @return A typed map that contains the passed arguments.
+ * @throws IllegalArgumentException Throws an exception when an uneven number of arguments are
+ * passed.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T, U> HashMap<T, U> newHashMap(Object... items) {
+ return (HashMap<T, U>) newMap(
+ new HashMap((items != null) ? items.length : 0),
+ (items != null) ? items : null);
+ }
+
+ /**
+ * Creates a new HashMap and adds the passed arguments to it in pairs.
+ *
+ * @param items The keys and values, to add to the map in pairs.
+ * @param <T> The type of the map to be created.
+ * @return A typed map that contains the passed arguments.
+ * @throws IllegalArgumentException Throws an exception when an uneven number of arguments are
+ * passed.
+ */
+ @SuppressWarnings("unchecked")
+ static <T, U> LinkedHashMap<T, U> newLinkedHashMap(Object... items) {
+ return (LinkedHashMap<T, U>) newMap(
+ new LinkedHashMap((items != null) ? items.length : 0),
+ (items != null) ? items : null);
+ }
+
+ /**
+ * Creates a new Map and adds the passed arguments to it in pairs.
+ *
+ * @param items The keys and values, to add to the map in pairs.
+ * @param <T> The type of the map to be created.
+ * @return A typed map that contains the passed arguments.
+ * @throws IllegalArgumentException Throws an exception when an even number of arguments are
+ * passed, or the type parameter is not a subclass of map.
+ */
+ @SuppressWarnings("unchecked")
+ private static <T, U> Map<T, U> newMap(Map<T, U> map, T[] items) {
+ if (items == null || items.length == 0) {
+ return map;
+ }
+ if (items.length % 2 != 0) {
+ throw new IllegalArgumentException("newMap requires an even number of items.");
+ }
+
+ for (int i = 0; i < items.length; i += 2) {
+ map.put(items[i], (U) items[i + 1]);
+ }
+ return map;
+ }
+
+ /**
+ * Returns the components of an array as concatenated String by calling toString() on each item.
+ *
+ * @param array The array to be concatenated.
+ * @param separator The separator between elements.
+ * @return A concatenated string of the items in list.
+ */
+ static <T> String concatenateArray(T[] array, String separator) {
+ if (array == null) {
+ return null;
+ }
+ return concatenateList(Arrays.asList(array), separator);
+ }
+
+ /**
+ * Returns the components of a list as concatenated String by calling toString() on each item.
+ *
+ * @param list The list to be concatenated.
+ * @param separator The separator between elements.
+ * @return A concatenated string of the items in list.
+ */
+ static String concatenateList(List<?> list, String separator) {
+ if (list == null) {
+ return null;
+ }
+ if (separator == null) {
+ separator = "";
+ }
+ StringBuilder stringBuilder = new StringBuilder();
+ for (Object item : list) {
+ if (item != null) {
+ stringBuilder.append(item.toString());
+ stringBuilder.append(separator);
+ }
+ }
+ String result = stringBuilder.toString();
+
+ if (result.length() > 0) {
+ return result.substring(0, result.length() - separator.length());
+ } else {
+ return result;
+ }
+ }
+
+ @SuppressWarnings({"unchecked"})
+ public static <T> T uncheckedCast(Object obj) {
+ return (T) obj;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/internal/Constants.java
@@ -0,0 +1,263 @@
+/*
+ * 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 com.leanplum.BuildConfig;
+
+/**
+ * Leanplum constants.
+ *
+ * @author Andrew First.
+ */
+public class Constants {
+ public static String API_HOST_NAME = "www.leanplum.com";
+ public static String SOCKET_HOST = "dev.leanplum.com";
+ public static int SOCKET_PORT = 80;
+ public static boolean API_SSL = true;
+ public static int NETWORK_TIMEOUT_SECONDS = 10;
+ public static int NETWORK_TIMEOUT_SECONDS_FOR_DOWNLOADS = 10;
+ static final String LEANPLUM_PACKAGE_IDENTIFIER = "s";//BuildConfig.LEANPLUM_PACKAGE_IDENTIFIER;
+
+ public static String LEANPLUM_VERSION = "2.2.2-SNAPSHOT";
+ public static String CLIENT = "android";
+
+ static final String INVALID_MAC_ADDRESS = "02:00:00:00:00:00";
+ static final String INVALID_MAC_ADDRESS_HASH = "0f607264fc6318a92b9e13c65db7cd3c";
+
+ /**
+ * From very old versions of the SDK, leading zeros were stripped from the mac address.
+ */
+ static final String OLD_INVALID_MAC_ADDRESS_HASH = "f607264fc6318a92b9e13c65db7cd3c";
+
+ static final String INVALID_ANDROID_ID = "9774d56d682e549c";
+ static final int MAX_DEVICE_ID_LENGTH = 400;
+ static final int MAX_USER_ID_LENGTH = 400;
+
+ public static String defaultDeviceId = null;
+ public static boolean hashFilesToDetermineModifications = true;
+ public static boolean isDevelopmentModeEnabled = false;
+ public static boolean loggingEnabled = false;
+ public static boolean isTestMode = false;
+ public static boolean enableVerboseLoggingInDevelopmentMode = false;
+ public static boolean enableFileUploadingInDevelopmentMode = true;
+ public static boolean canDownloadContentMidSessionInProduction = false;
+ static boolean isInPermanentFailureState = false;
+
+ public static boolean isNoop() {
+ return isTestMode || isInPermanentFailureState;
+ }
+
+ public static String API_SERVLET = "api";
+
+ public static class Defaults {
+ public static final String COUNT_KEY = "__leanplum_unsynced";
+ public static final String ITEM_KEY = "__leanplum_unsynced_%d";
+ public static final String VARIABLES_KEY = "__leanplum_variables";
+ public static final String ATTRIBUTES_KEY = "__leanplum_attributes";
+ public static final String TOKEN_KEY = "__leanplum_token";
+ public static final String MESSAGES_KEY = "__leanplum_messages";
+ public static final String UPDATE_RULES_KEY = "__leanplum_update_rules";
+ public static final String EVENT_RULES_KEY = "__leanplum_event_rules";
+ public static final String REGIONS_KEY = "regions";
+ public static final String MESSAGE_TRIGGER_OCCURRENCES_KEY =
+ "__leanplum_message_trigger_occurrences_%s";
+ public static final String MESSAGE_IMPRESSION_OCCURRENCES_KEY =
+ "__leanplum_message_occurrences_%s";
+ public static final String MESSAGE_MUTED_KEY = "__leanplum_message_muted_%s";
+ public static final String LOCAL_NOTIFICATION_KEY = "__leanplum_local_message_%s";
+ public static final String INBOX_KEY = "__leanplum_newsfeed";
+ public static final String LEANPLUM_PUSH = "__leanplum_push__";
+ public static final String APP_ID = "__app_id";
+ public static final String PROPERTY_REGISTRATION_ID = "registration_id";
+ public static final String PROPERTY_SENDER_IDS = "sender_ids";
+ }
+
+ public static class Methods {
+ public static final String ADVANCE = "advance";
+ public static final String DELETE_INBOX_MESSAGE = "deleteNewsfeedMessage";
+ public static final String DOWNLOAD_FILE = "downloadFile";
+ public static final String GET_INBOX_MESSAGES = "getNewsfeedMessages";
+ public static final String GET_VARS = "getVars";
+ public static final String HEARTBEAT = "heartbeat";
+ public static final String LOG = "log";
+ public static final String MARK_INBOX_MESSAGE_AS_READ = "markNewsfeedMessageAsRead";
+ public static final String MULTI = "multi";
+ public static final String PAUSE_SESSION = "pauseSession";
+ public static final String PAUSE_STATE = "pauseState";
+ public static final String REGISTER_FOR_DEVELOPMENT = "registerDevice";
+ public static final String RESUME_SESSION = "resumeSession";
+ public static final String RESUME_STATE = "resumeState";
+ public static final String SET_DEVICE_ATTRIBUTES = "setDeviceAttributes";
+ public static final String SET_TRAFFIC_SOURCE_INFO = "setTrafficSourceInfo";
+ public static final String SET_USER_ATTRIBUTES = "setUserAttributes";
+ public static final String SET_VARS = "setVars";
+ public static final String START = "start";
+ public static final String STOP = "stop";
+ public static final String TRACK = "track";
+ public static final String UPLOAD_FILE = "uploadFile";
+ }
+
+ public static class Params {
+ public static final String ACTION = "action";
+ public static final String ACTION_DEFINITIONS = "actionDefinitions";
+ public static final String APP_ID = "appId";
+ public static final String BACKGROUND = "background";
+ public static final String CLIENT = "client";
+ public static final String CLIENT_KEY = "clientKey";
+ public static final String DATA = "data";
+ public static final String DEV_MODE = "devMode";
+ public static final String DEVICE_ID = "deviceId";
+ public static final String DEVICE_MODEL = "deviceModel";
+ public static final String DEVICE_NAME = "deviceName";
+ public static final String DEVICE_PUSH_TOKEN = "gcmRegistrationId";
+ public static final String DEVICE_SYSTEM_NAME = "systemName";
+ public static final String DEVICE_SYSTEM_VERSION = "systemVersion";
+ public static final String EMAIL = "email";
+ public static final String EVENT = "event";
+ public static final String FILE = "file";
+ public static final String FILE_ATTRIBUTES = "fileAttributes";
+ public static final String GOOGLE_PLAY_PURCHASE_DATA = "googlePlayPurchaseData";
+ public static final String GOOGLE_PLAY_PURCHASE_DATA_SIGNATURE =
+ "googlePlayPurchaseDataSignature";
+ public static final String IAP_CURRENCY_CODE = "currencyCode";
+ public static final String IAP_ITEM = "item";
+ public static final String INCLUDE_DEFAULTS = "includeDefaults";
+ public static final String INCLUDE_MESSAGE_ID = "includeMessageId";
+ public static final String INFO = "info";
+ public static final String INSTALL_DATE = "installDate";
+ public static final String KINDS = "kinds";
+ public static final String LIMIT_TRACKING = "limitTracking";
+ public static final String MESSAGE = "message";
+ public static final String MESSAGE_ID = "messageId";
+ public static final String NEW_USER_ID = "newUserId";
+ public static final String INBOX_MESSAGE_ID = "newsfeedMessageId";
+ public static final String INBOX_MESSAGES = "newsfeedMessages";
+ public static final String PARAMS = "params";
+ public static final String SDK_VERSION = "sdkVersion";
+ public static final String STATE = "state";
+ public static final String TIME = "time";
+ public static final String TYPE = "type";
+ public static final String TOKEN = "token";
+ public static final String TRAFFIC_SOURCE = "trafficSource";
+ public static final String UPDATE_DATE = "updateDate";
+ public static final String USER_ID = "userId";
+ public static final String USER_ATTRIBUTES = "userAttributes";
+ public static final String VALUE = "value";
+ public static final String VARS = "vars";
+ public static final String VERSION_NAME = "versionName";
+ }
+
+ public static class Keys {
+ public static final String CITY = "city";
+ public static final String COUNTRY = "country";
+ public static final String DELIVERY_TIMESTAMP = "deliveryTimestamp";
+ public static final String EXPIRATION_TIMESTAMP = "expirationTimestamp";
+ public static final String FILENAME = "filename";
+ public static final String HASH = "hash";
+ public static final String INSTALL_TIME_INITIALIZED = "installTimeInitialized";
+ public static final String IS_READ = "isRead";
+ public static final String IS_REGISTERED = "isRegistered";
+ public static final String IS_REGISTERED_FROM_OTHER_APP = "isRegisteredFromOtherApp";
+ public static final String LATEST_VERSION = "latestVersion";
+ public static final String LOCALE = "locale";
+ public static final String LOCATION = "location";
+ public static final String LOCATION_ACCURACY_TYPE = "locationAccuracyType";
+ public static final String MESSAGE_DATA = "messageData";
+ public static final String MESSAGES = "messages";
+ public static final String UPDATE_RULES = "interfaceRules";
+ public static final String EVENT_RULES = "interfaceEvents";
+ public static final String INBOX_MESSAGES = "newsfeedMessages";
+ public static final String PUSH_MESSAGE_ACTION = "_lpx";
+ public static final String PUSH_MESSAGE_ID_NO_MUTE_WITH_ACTION = "_lpm";
+ public static final String PUSH_MESSAGE_ID_MUTE_WITH_ACTION = "_lpu";
+ public static final String PUSH_MESSAGE_ID_NO_MUTE = "_lpn";
+ public static final String PUSH_MESSAGE_ID_MUTE = "_lpv";
+ public static final String PUSH_MESSAGE_ID = "lp_messageId";
+ public static final String PUSH_MESSAGE_TEXT = "lp_message";
+ public static final String PUSH_MESSAGE_IMAGE_URL = "lp_imageUrl";
+ public static final String REGION = "region";
+ public static final String REGION_STATE = "regionState";
+ public static final String REGIONS = "regions";
+ public static final String SIZE = "size";
+ public static final String SUBTITLE = "Subtitle";
+ public static final String SYNC_INBOX = "syncNewsfeed";
+ public static final String LOGGING_ENABLED = "loggingEnabled";
+ public static final String TIMEZONE = "timezone";
+ public static final String TIMEZONE_OFFSET_SECONDS = "timezoneOffsetSeconds";
+ public static final String TITLE = "Title";
+ public static final String INBOX_IMAGE = "Image";
+ public static final String DATA = "Data";
+ public static final String TOKEN = "token";
+ public static final String VARIANTS = "variants";
+ public static final String VARS = "vars";
+ public static final String VARS_FROM_CODE = "varsFromCode";
+ }
+
+ public static class Kinds {
+ public static final String INT = "integer";
+ public static final String FLOAT = "float";
+ public static final String STRING = "string";
+ public static final String BOOLEAN = "bool";
+ public static final String FILE = "file";
+ public static final String DICTIONARY = "group";
+ public static final String ARRAY = "list";
+ public static final String ACTION = "action";
+ public static final String COLOR = "color";
+ }
+
+ static class Files {
+ public static final int MAX_UPLOAD_BATCH_SIZES = (25 * (1 << 20));
+ public static final int MAX_UPLOAD_BATCH_FILES = 16;
+ }
+
+ public static final String HELD_BACK_EVENT_NAME = "Held Back";
+ public static final String HELD_BACK_MESSAGE_PREFIX = "__held_back__";
+
+ public static class Values {
+ public static final String DETECT = "(detect)";
+ public static final String RESOURCES_VARIABLE = "__Android Resources";
+ public static final String ACTION_ARG = "__name__";
+ public static final String CHAIN_MESSAGE_ARG = "Chained message";
+ public static final String DEFAULT_PUSH_ACTION = "Open action";
+ public static final String CHAIN_MESSAGE_ACTION_NAME = "Chain to Existing Message";
+ public static final String DEFAULT_PUSH_MESSAGE = "Push message goes here.";
+ public static final String SDK_LOG = "sdkLog";
+ public static final String SDK_ERROR = "sdkError";
+ public static final String FILE_PREFIX = "__file__";
+ }
+
+ public static class Crypt {
+ public static final int ITER_COUNT = 1000;
+ public static final int KEY_LENGTH = 256;
+ public static final String SALT = "L3@nP1Vm"; // Must have 8 bytes
+ public static final String IV = "__l3anplum__iv__"; // Must have 16 bytes
+ }
+
+ public static class Messaging {
+ public static final int MAX_STORED_OCCURRENCES_PER_MESSAGE = 100;
+ public static final int DEFAULT_PRIORITY = 1000;
+ }
+
+ public static class ClassUtil {
+ public static final String UI_INTERFACE_EDITOR = "com.leanplum.uieditor.LeanplumUIEditor";
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/internal/FileManager.java
@@ -0,0 +1,501 @@
+/*
+ * 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.ContentResolver;
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.text.TextUtils;
+
+import com.leanplum.Leanplum;
+import com.leanplum.Var;
+
+import org.json.JSONObject;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.DigestInputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+/**
+ * Leanplum file manager.
+ *
+ * @author Andrew First
+ */
+public class FileManager {
+ interface ResourceUpdateCallback {
+ void onResourceSyncFinish();
+ }
+
+ private static ResourceUpdateCallback updateCallback;
+ private static boolean isInitialized = false;
+ private static boolean initializing = false;
+ static final Object initializingLock = new Object();
+ public static Var<HashMap<String, Object>> resources = null;
+
+ public enum DownloadFileResult {
+ NONE,
+ EXISTS_IN_ASSETS,
+ DOWNLOADING
+ }
+
+ static class HashResults {
+ final String hash;
+ final int size;
+
+ public HashResults(String hash, int size) {
+ this.hash = hash;
+ this.size = size;
+ }
+ }
+
+ public static DownloadFileResult maybeDownloadFile(boolean isResource, String stringValue,
+ String defaultValue, String urlValue, final Runnable onComplete) {
+ if (stringValue != null && !stringValue.equals(defaultValue) &&
+ (!isResource || VarCache.getResIdFromPath(stringValue) == 0)) {
+ InputStream inputStream = null;
+ try {
+ Context context = Leanplum.getContext();
+ inputStream = context.getResources().getAssets().open(stringValue);
+ if (inputStream != null) {
+ return DownloadFileResult.EXISTS_IN_ASSETS;
+ }
+ } catch (IOException ignored) {
+ } finally {
+ if (inputStream != null) {
+ try {
+ inputStream.close();
+ } catch (IOException e) {
+ Log.w("Failed to close InputStream.", e.getMessage());
+ }
+ }
+ }
+ String realPath = FileManager.fileRelativeToAppBundle(stringValue);
+ if (!FileManager.fileExistsAtPath(realPath)) {
+ realPath = FileManager.fileRelativeToDocuments(stringValue);
+ if (!FileManager.fileExistsAtPath(realPath)) {
+ Request downloadRequest = Request.get(Constants.Methods.DOWNLOAD_FILE, null);
+ downloadRequest.onResponse(new Request.ResponseCallback() {
+ @Override
+ public void response(JSONObject response) {
+ if (onComplete != null) {
+ onComplete.run();
+ }
+ }
+ });
+ downloadRequest.onError(new Request.ErrorCallback() {
+ @Override
+ public void error(Exception e) {
+ if (onComplete != null) {
+ onComplete.run();
+ }
+ }
+ });
+ downloadRequest.downloadFile(stringValue, urlValue);
+ return DownloadFileResult.DOWNLOADING;
+ }
+ }
+ }
+ return DownloadFileResult.NONE;
+ }
+
+ static HashResults fileMD5HashCreateWithPath(InputStream is) {
+ try {
+ MessageDigest md = MessageDigest.getInstance("MD5");
+ int size = 0;
+ try {
+ is = new DigestInputStream(is, md);
+ byte[] buffer = new byte[8192];
+ int bytesRead;
+ while ((bytesRead = is.read(buffer)) != -1) {
+ size += bytesRead;
+ }
+ } finally {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (IOException e) {
+ Log.w("Failed to close InputStream.", e.getMessage());
+ }
+ }
+ }
+ byte[] digest = md.digest();
+
+ StringBuilder hexString = new StringBuilder();
+ for (byte dig : digest) {
+ String hex = Integer.toHexString(0xFF & dig);
+ if (hex.length() == 1) {
+ hexString.append('0');
+ }
+ hexString.append(hex);
+ }
+ return new HashResults(hexString.toString(), size);
+ } catch (NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ return null;
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ static int getFileSize(String path) {
+ if (path == null) {
+ return -1;
+ }
+ return (int) new File(path).length();
+ }
+
+ public static boolean fileExistsAtPath(String path) {
+ return path != null && new File(path).exists();
+ }
+
+ @SuppressWarnings("SameReturnValue")
+ private static String appBundlePath() {
+ return "";
+ }
+
+ private static String documentsPath() {
+ Context context = Leanplum.getContext();
+ if (context != null) {
+ return context.getDir("Leanplum_Documents", Context.MODE_PRIVATE).getAbsolutePath();
+ }
+ return null;
+ }
+
+ private static String bundlePath() {
+ Context context = Leanplum.getContext();
+ return context.getDir("Leanplum_Bundle", Context.MODE_PRIVATE).getAbsolutePath();
+ }
+
+ private static String fileRelativeTo(String root, String path) {
+ return root + "/" + path;
+ }
+
+ static String fileRelativeToAppBundle(String path) {
+ if (path == null) {
+ return null;
+ }
+ return fileRelativeTo(appBundlePath(), path);
+ }
+
+ static String fileRelativeToLPBundle(String path) {
+ if (path == null) {
+ return null;
+ }
+ return fileRelativeTo(bundlePath(), path);
+ }
+
+ public static String fileRelativeToDocuments(String path) {
+ if (path == null) {
+ return null;
+ }
+ return fileRelativeTo(documentsPath(), path);
+ }
+
+ static boolean isNewerLocally(
+ Map<String, Object> localAttributes,
+ Map<String, Object> serverAttributes) {
+ if (serverAttributes == null) {
+ return true;
+ }
+ String localHash = (String) localAttributes.get(Constants.Keys.HASH);
+ String serverHash = (String) serverAttributes.get(Constants.Keys.HASH);
+ Integer localSize = (Integer) localAttributes.get(Constants.Keys.SIZE);
+ Integer serverSize = (Integer) serverAttributes.get(Constants.Keys.SIZE);
+ return (serverSize == null || (localSize != null && !localSize.equals(serverSize))) ||
+ (localHash != null && (serverHash == null || !localHash.equals(serverHash)));
+ }
+
+ static void setResourceSyncFinishBlock(ResourceUpdateCallback callback) {
+ updateCallback = callback;
+ }
+
+ static boolean initializing() {
+ return initializing;
+ }
+
+ public static boolean isResourceSyncingEnabled() {
+ return initializing || isInitialized;
+ }
+
+ private static void enableResourceSyncing(List<Pattern> patternsToInclude,
+ List<Pattern> patternsToExclude) {
+ resources = Var.define(Constants.Values.RESOURCES_VARIABLE, new HashMap<String, Object>());
+
+ // This is from http://stackoverflow.com/questions/3052964/is-there-any-way-to-tell-what-folder-a-drawable-resource-was-inflated-from.
+ String drawableDirPrefix = "res/drawable";
+ String layoutDirPrefix = "res/layout";
+ ZipInputStream apk = null;
+ Context context = Leanplum.getContext();
+ try {
+ apk = new ZipInputStream(new FileInputStream(context.getPackageResourcePath()));
+ ZipEntry entry;
+ while ((entry = apk.getNextEntry()) != null) {
+ String resourcePath = entry.getName();
+ if (resourcePath.startsWith(drawableDirPrefix) ||
+ resourcePath.startsWith(layoutDirPrefix)) {
+ String unprefixedResourcePath = resourcePath.substring(4);
+
+ if (patternsToInclude != null &&
+ patternsToInclude.size() > 0) {
+ boolean included = false;
+ for (Pattern pattern : patternsToInclude) {
+ if (pattern.matcher(unprefixedResourcePath).matches()) {
+ included = true;
+ break;
+ }
+ }
+ if (!included) {
+ continue;
+ }
+ }
+ if (patternsToExclude != null) {
+ boolean excluded = false;
+ for (Pattern pattern : patternsToExclude) {
+ if (pattern.matcher(unprefixedResourcePath).matches()) {
+ excluded = true;
+ break;
+ }
+ }
+ if (excluded) {
+ continue;
+ }
+ }
+
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+
+ int bytesRead;
+ int size = 0;
+ byte[] buffer = new byte[8192];
+ while ((bytesRead = apk.read(buffer)) != -1) {
+ outputStream.write(buffer, 0, bytesRead);
+ size += bytesRead;
+ }
+ apk.closeEntry();
+
+ String hash = ("" + entry.getTime()) + ("" + size);
+
+ Var.defineResource(
+ Constants.Values.RESOURCES_VARIABLE
+ + "." + unprefixedResourcePath.replace(".", "\\.").replace('/', '.'),
+ resourcePath, size, hash, outputStream.toByteArray());
+ }
+ }
+ } catch (IOException e) {
+ Log.w("Error occurred when trying " +
+ "to enable resource syncing." + e.getMessage());
+ } finally {
+ if (apk != null) {
+ try {
+ apk.close();
+ } catch (IOException e) {
+ Log.w("Failed to close ZipInputStream.", e.getMessage());
+ }
+ }
+ }
+ isInitialized = true;
+ synchronized (initializingLock) {
+ initializing = false;
+ if (updateCallback != null) {
+ updateCallback.onResourceSyncFinish();
+ }
+ }
+ }
+
+ private static List<Pattern> compilePatterns(List<String> patterns) {
+ if (patterns == null) {
+ return new ArrayList<>(0);
+ }
+ List<Pattern> compiledPatterns = new ArrayList<>(patterns.size());
+ for (String pattern : patterns) {
+ try {
+ compiledPatterns.add(Pattern.compile(pattern));
+ } catch (PatternSyntaxException e) {
+ Log.e("Ignoring malformed resource syncing pattern: \"" + pattern +
+ "\". Patterns must be regular expressions.");
+ }
+ }
+ return compiledPatterns;
+ }
+
+ public static void enableResourceSyncing(final List<String> patternsToInclude,
+ final List<String> patternsToExclude, boolean isAsync) {
+ initializing = true;
+ if (isInitialized) {
+ return;
+ }
+
+ try {
+ final List<Pattern> compiledIncludePatterns = compilePatterns(patternsToInclude);
+ final List<Pattern> compiledExcludePatterns = compilePatterns(patternsToExclude);
+
+ if (isAsync) {
+ Util.executeAsyncTask(new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ try {
+ enableResourceSyncing(compiledIncludePatterns, compiledExcludePatterns);
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ return null;
+ }
+ });
+ } else {
+ enableResourceSyncing(compiledIncludePatterns, compiledExcludePatterns);
+ }
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ }
+
+ /**
+ * Returns an String with path for a specified file/resource/asset variable.
+ *
+ * @param stringValue Name of the file.
+ * @return String Path for a given variable.
+ */
+ public static String fileValue(String stringValue) {
+ return fileValue(stringValue, stringValue, null);
+ }
+
+ public static String fileValue(String stringValue, String defaultValue,
+ Boolean valueIsInAssets) {
+ String result;
+ if (stringValue == null) {
+ return null;
+ }
+ if (stringValue.equals(defaultValue)) {
+ result = FileManager.fileRelativeToAppBundle(defaultValue);
+ if (FileManager.fileExistsAtPath(result)) {
+ return result;
+ }
+ }
+
+ if (valueIsInAssets == null) {
+ InputStream inputStream = null;
+ try {
+ Context context = Leanplum.getContext();
+ inputStream = context.getAssets().open(stringValue);
+ return stringValue;
+ } catch (Exception ignored) {
+ } finally {
+ if (inputStream != null) {
+ try {
+ inputStream.close();
+ } catch (Exception e) {
+ Log.w("Failed to close InputStream: " + e);
+ }
+ }
+ }
+ } else if (valueIsInAssets) {
+ return stringValue;
+ }
+
+ result = FileManager.fileRelativeToLPBundle(stringValue);
+ if (!FileManager.fileExistsAtPath(result)) {
+ result = FileManager.fileRelativeToDocuments(stringValue);
+ if (!FileManager.fileExistsAtPath(result)) {
+ result = FileManager.fileRelativeToAppBundle(stringValue);
+ if (!FileManager.fileExistsAtPath(result)) {
+ result = FileManager.fileRelativeToLPBundle(defaultValue);
+ if (!FileManager.fileExistsAtPath(result)) {
+ result = FileManager.fileRelativeToAppBundle(defaultValue);
+ if (!FileManager.fileExistsAtPath(result)) {
+ return defaultValue;
+ }
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Returns an InputStream for a specified file/resource/asset variable. It will automatically find
+ * a proper file based on provided data. File can be located in either assets, res or in-memory.
+ * Caller is responsible for closing the returned InputStream.
+ *
+ * @param isResource Whether variable is resource.
+ * @param isAsset Whether variable is asset.
+ * @param valueIsInAssets Whether variable is located in assets directory.
+ * @param value Name of the file.
+ * @param defaultValue Default name of the file.
+ * @param resourceData Data if file is not located on internal storage.
+ * @return InputStream for a given variable.
+ */
+ public static InputStream stream(boolean isResource, Boolean isAsset, Boolean valueIsInAssets,
+ String value, String defaultValue, byte[] resourceData) {
+ if (TextUtils.isEmpty(value)) {
+ return null;
+ }
+ try {
+ Context context = Leanplum.getContext();
+ if (isResource && value.equals(defaultValue) && resourceData != null) {
+ return new ByteArrayInputStream(resourceData);
+ } else if (isResource && value.equals(defaultValue) && resourceData == null) {
+ // Convert resource name into resource id.
+ int resourceId = Util.generateIdFromResourceName(value);
+ // Android only generates id's greater then 0.
+ if (resourceId != 0) {
+ Resources res = context.getResources();
+ // Based on resource Id, we can extract package it belongs, directory where it is stored
+ // and name of the file.
+ Uri resourceUri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE +
+ "://" + res.getResourcePackageName(resourceId)
+ + '/' + res.getResourceTypeName(resourceId)
+ + '/' + res.getResourceEntryName(resourceId));
+ return context.getContentResolver().openInputStream(resourceUri);
+ }
+ return null;
+ }
+ if (valueIsInAssets == null) {
+ try {
+ return context.getAssets().open(value);
+ } catch (IOException ignored) {
+ }
+ } else if (valueIsInAssets || (isAsset && value.equals(defaultValue))) {
+ return context.getAssets().open(value);
+ }
+ return new FileInputStream(new File(value));
+ } catch (IOException e) {
+ Log.w("Failed to load a stream." + e.getMessage());
+ return null;
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/internal/HybiParser.java
@@ -0,0 +1,422 @@
+//
+// HybiParser.java: draft-ietf-hybi-thewebsocketprotocol-13 parser
+//
+// Based on code from the faye project.
+// https://github.com/faye/faye-websocket-node
+// Copyright (c) 2009-2012 James Coglan
+//
+// Ported from Javascript to Java by Eric Butler <eric@codebutler.com>
+//
+// (The MIT License)
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+package com.leanplum.internal;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.Arrays;
+import java.util.List;
+
+class HybiParser {
+ private WebSocketClient mClient;
+
+ private boolean mMasking = true;
+
+ private int mStage;
+
+ private boolean mFinal;
+ private boolean mMasked;
+ private int mOpcode;
+ private int mLengthSize;
+ private int mLength;
+ private int mMode;
+
+ private byte[] mMask = new byte[0];
+ private byte[] mPayload = new byte[0];
+
+ private boolean mClosed = false;
+
+ private ByteArrayOutputStream mBuffer = new ByteArrayOutputStream();
+
+ private static final int BYTE = 255;
+ private static final int FIN = 128;
+ private static final int MASK = 128;
+ private static final int RSV1 = 64;
+ private static final int RSV2 = 32;
+ private static final int RSV3 = 16;
+ private static final int OPCODE = 15;
+ private static final int LENGTH = 127;
+
+ private static final int MODE_TEXT = 1;
+ private static final int MODE_BINARY = 2;
+
+ private static final int OP_CONTINUATION = 0;
+ private static final int OP_TEXT = 1;
+ private static final int OP_BINARY = 2;
+ private static final int OP_CLOSE = 8;
+ private static final int OP_PING = 9;
+ private static final int OP_PONG = 10;
+
+ private static final List<Integer> OPCODES = Arrays.asList(OP_CONTINUATION,
+ OP_TEXT, OP_BINARY, OP_CLOSE, OP_PING, OP_PONG);
+
+ private static final List<Integer> FRAGMENTED_OPCODES = Arrays.asList(
+ OP_CONTINUATION, OP_TEXT, OP_BINARY);
+
+ public HybiParser(WebSocketClient client) {
+ mClient = client;
+ }
+
+ private static byte[] mask(byte[] payload, byte[] mask, int offset) {
+ if (mask.length == 0)
+ return payload;
+
+ for (int i = 0; i < payload.length - offset; i++) {
+ payload[offset + i] = (byte) (payload[offset + i] ^ mask[i % 4]);
+ }
+ return payload;
+ }
+
+ public void start(HappyDataInputStream stream) throws IOException {
+ while (true) {
+ if (stream.available() == -1)
+ break;
+ switch (mStage) {
+ case 0:
+ parseOpcode(stream.readByte());
+ break;
+ case 1:
+ parseLength(stream.readByte());
+ break;
+ case 2:
+ parseExtendedLength(stream.readBytes(mLengthSize));
+ break;
+ case 3:
+ mMask = stream.readBytes(4);
+ mStage = 4;
+ break;
+ case 4:
+ mPayload = stream.readBytes(mLength);
+ emitFrame();
+ mStage = 0;
+ break;
+ default:
+ break;
+ }
+ }
+ mClient.getListener().onDisconnect(0, "EOF");
+ }
+
+ private void parseOpcode(byte data) throws ProtocolError {
+ boolean rsv1 = (data & RSV1) == RSV1;
+ boolean rsv2 = (data & RSV2) == RSV2;
+ boolean rsv3 = (data & RSV3) == RSV3;
+
+ if (rsv1 || rsv2 || rsv3) {
+ throw new ProtocolError("RSV not zero");
+ }
+
+ mFinal = (data & FIN) == FIN;
+ mOpcode = (data & OPCODE);
+ mMask = new byte[0];
+ mPayload = new byte[0];
+
+ if (!OPCODES.contains(mOpcode)) {
+ throw new ProtocolError("Bad opcode");
+ }
+
+ if (!FRAGMENTED_OPCODES.contains(mOpcode) && !mFinal) {
+ throw new ProtocolError("Expected non-final packet");
+ }
+
+ mStage = 1;
+ }
+
+ private void parseLength(byte data) {
+ mMasked = (data & MASK) == MASK;
+ mLength = (data & LENGTH);
+
+ if (mLength >= 0 && mLength <= 125) {
+ mStage = mMasked ? 3 : 4;
+ } else {
+ mLengthSize = (mLength == 126) ? 2 : 8;
+ mStage = 2;
+ }
+ }
+
+ private void parseExtendedLength(byte[] buffer) throws ProtocolError {
+ mLength = getInteger(buffer);
+ mStage = mMasked ? 3 : 4;
+ }
+
+ public byte[] frame(String data) {
+ return frame(data, OP_TEXT, -1);
+ }
+
+ public byte[] frame(byte[] data) {
+ return frame(data, OP_BINARY, -1);
+ }
+
+ private byte[] frame(byte[] data, int opcode, int errorCode) {
+ return frame((Object) data, opcode, errorCode);
+ }
+
+ private byte[] frame(String data, int opcode, int errorCode) {
+ return frame((Object) data, opcode, errorCode);
+ }
+
+ private byte[] frame(Object data, int opcode, int errorCode) {
+ if (mClosed)
+ return null;
+
+ // Log.d("Creating frame for: " + data + " op: " + opcode + " err: " +
+ // errorCode);
+
+ byte[] buffer = (data instanceof String) ? decode((String) data)
+ : (byte[]) data;
+ int insert = (errorCode > 0) ? 2 : 0;
+ int length = buffer.length + insert;
+ int header = (length <= 125) ? 2 : (length <= 65535 ? 4 : 10);
+ int offset = header + (mMasking ? 4 : 0);
+ int masked = mMasking ? MASK : 0;
+ byte[] frame = new byte[length + offset];
+
+ frame[0] = (byte) ((byte) FIN | (byte) opcode);
+
+ if (length <= 125) {
+ frame[1] = (byte) (masked | length);
+ } else if (length <= 65535) {
+ frame[1] = (byte) (masked | 126);
+ frame[2] = (byte) Math.floor(length / 256f);
+ frame[3] = (byte) (length & BYTE);
+ } else {
+ frame[1] = (byte) (masked | 127);
+ frame[2] = (byte) (((int) Math.floor(length / Math.pow(2, 56))) & BYTE);
+ frame[3] = (byte) (((int) Math.floor(length / Math.pow(2, 48))) & BYTE);
+ frame[4] = (byte) (((int) Math.floor(length / Math.pow(2, 40))) & BYTE);
+ frame[5] = (byte) (((int) Math.floor(length / Math.pow(2, 32))) & BYTE);
+ frame[6] = (byte) (((int) Math.floor(length / Math.pow(2, 24))) & BYTE);
+ frame[7] = (byte) (((int) Math.floor(length / Math.pow(2, 16))) & BYTE);
+ frame[8] = (byte) (((int) Math.floor(length / Math.pow(2, 8))) & BYTE);
+ frame[9] = (byte) (length & BYTE);
+ }
+
+ if (errorCode > 0) {
+ frame[offset] = (byte) (((int) Math.floor(errorCode / 256f)) & BYTE);
+ frame[offset + 1] = (byte) (errorCode & BYTE);
+ }
+ System.arraycopy(buffer, 0, frame, offset + insert, buffer.length);
+
+ if (mMasking) {
+ byte[] mask = {(byte) Math.floor(Math.random() * 256),
+ (byte) Math.floor(Math.random() * 256),
+ (byte) Math.floor(Math.random() * 256),
+ (byte) Math.floor(Math.random() * 256)};
+ System.arraycopy(mask, 0, frame, header, mask.length);
+ mask(frame, mask, offset);
+ }
+
+ return frame;
+ }
+
+ public void ping(String message) {
+ mClient.send(frame(message, OP_PING, -1));
+ }
+
+ public void close(int code, String reason) {
+ if (mClosed)
+ return;
+ mClient.send(frame(reason, OP_CLOSE, code));
+ mClosed = true;
+ }
+
+ private void emitFrame() throws IOException {
+ byte[] payload = mask(mPayload, mMask, 0);
+ int opcode = mOpcode;
+
+ if (opcode == OP_CONTINUATION) {
+ if (mMode == 0) {
+ throw new ProtocolError("Mode was not set.");
+ }
+ mBuffer.write(payload);
+ if (mFinal) {
+ byte[] message = mBuffer.toByteArray();
+ if (mMode == MODE_TEXT) {
+ mClient.getListener().onMessage(encode(message));
+ } else {
+ mClient.getListener().onMessage(message);
+ }
+ reset();
+ }
+
+ } else if (opcode == OP_TEXT) {
+ if (mFinal) {
+ String messageText = encode(payload);
+ mClient.getListener().onMessage(messageText);
+ } else {
+ mMode = MODE_TEXT;
+ mBuffer.write(payload);
+ }
+
+ } else if (opcode == OP_BINARY) {
+ if (mFinal) {
+ mClient.getListener().onMessage(payload);
+ } else {
+ mMode = MODE_BINARY;
+ mBuffer.write(payload);
+ }
+
+ } else if (opcode == OP_CLOSE) {
+ int code = (payload.length >= 2) ? 256 * payload[0] + payload[1] : 0;
+ String reason = (payload.length > 2) ? encode(slice(payload, 2)) : null;
+ // Log.d("Got close op! " + code + " " + reason);
+ mClient.getListener().onDisconnect(code, reason);
+
+ } else if (opcode == OP_PING) {
+ if (payload.length > 125) {
+ throw new ProtocolError("Ping payload too large");
+ }
+ // Log.d("Sending pong!!");
+ mClient.sendFrame(frame(payload, OP_PONG, -1));
+
+ } else if (opcode == OP_PONG) {
+ // String message = encode(payload);
+ // FIXME: Fire callback...
+ // Log.d("Got pong! " + message);
+ }
+ }
+
+ private void reset() {
+ mMode = 0;
+ mBuffer.reset();
+ }
+
+ private String encode(byte[] buffer) {
+ try {
+ return new String(buffer, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private byte[] decode(String string) {
+ try {
+ return (string).getBytes("UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private int getInteger(byte[] bytes) throws ProtocolError {
+ long i = byteArrayToLong(bytes, 0, bytes.length);
+ if (i < 0 || i > Integer.MAX_VALUE) {
+ throw new ProtocolError("Bad integer: " + i);
+ }
+ return (int) i;
+ }
+
+ /**
+ * Copied from AOSP Arrays.java.
+ */
+ /**
+ * Copies elements from {@code original} into a new array, from indexes start (inclusive) to end
+ * (exclusive). The original order of elements is preserved. If {@code end} is greater than {@code
+ * original.length}, the result is padded with the value {@code (byte) 0}.
+ *
+ * @param original the original array
+ * @param start the start index, inclusive
+ * @param end the end index, exclusive
+ * @return the new array
+ * @throws ArrayIndexOutOfBoundsException if {@code start < 0 || start > original.length}
+ * @throws IllegalArgumentException if {@code start > end}
+ * @throws NullPointerException if {@code original == null}
+ * @since 1.6
+ */
+ private static byte[] copyOfRange(byte[] original, int start, int end) {
+ if (start > end) {
+ throw new IllegalArgumentException();
+ }
+ int originalLength = original.length;
+ if (start < 0 || start > originalLength) {
+ throw new ArrayIndexOutOfBoundsException();
+ }
+ int resultLength = end - start;
+ int copyLength = Math.min(resultLength, originalLength - start);
+ byte[] result = new byte[resultLength];
+ System.arraycopy(original, start, result, 0, copyLength);
+ return result;
+ }
+
+ private byte[] slice(byte[] array, int start) {
+ return copyOfRange(array, start, array.length);
+ }
+
+ @SuppressWarnings("serial")
+ static class ProtocolError extends IOException {
+ public ProtocolError(String detailMessage) {
+ super(detailMessage);
+ }
+ }
+
+ private static long byteArrayToLong(byte[] b, int offset, int length) {
+ if (b.length < length)
+ throw new IllegalArgumentException(
+ "length must be less than or equal to b.length");
+
+ long value = 0;
+ for (int i = 0; i < length; i++) {
+ int shift = (length - 1 - i) * 8;
+ value += (b[i + offset] & 0x000000FF) << shift;
+ }
+ return value;
+ }
+
+ static class HappyDataInputStream extends DataInputStream {
+ public HappyDataInputStream(InputStream in) {
+ super(in);
+ }
+
+ public byte[] readBytes(int length) throws IOException {
+ byte[] buffer = new byte[length];
+
+ int total = 0;
+
+ while (total < length) {
+ int count = read(buffer, total, length - total);
+ if (count == -1) {
+ break;
+ }
+ total += count;
+ }
+
+ if (total != length) {
+ throw new IOException(
+ String.format("Read wrong number of bytes. Got: %s, Expected: %s.",
+ total, length));
+ }
+
+ return buffer;
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/internal/JsonConverter.java
@@ -0,0 +1,167 @@
+/*
+ * 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.text.Editable;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Converts objects to/from JSON.
+ *
+ * @author Andrew First
+ */
+public class JsonConverter {
+ public static String toJson(Map<String, ?> map) {
+ if (map == null) {
+ return null;
+ }
+ try {
+ return mapToJsonObject(map).toString();
+ } catch (JSONException e) {
+ Log.e("Error converting " + map + " to JSON", e);
+ return null;
+ }
+ }
+
+ public static Map<String, Object> fromJson(String json) {
+ if (json == null) {
+ return null;
+ }
+ try {
+ return mapFromJson(new JSONObject(json));
+ } catch (JSONException e) {
+ Log.e("Error converting " + json + " from JSON", e);
+ return null;
+ }
+ }
+
+ public static JSONObject mapToJsonObject(Map<String, ?> map) throws JSONException {
+ if (map == null) {
+ return null;
+ }
+ JSONObject obj = new JSONObject();
+ for (Map.Entry<String, ?> entry : map.entrySet()) {
+ String key = entry.getKey();
+ Object value = entry.getValue();
+ if (value instanceof Map) {
+ Map<String, ?> mappedValue = CollectionUtil.uncheckedCast(value);
+ value = mapToJsonObject(mappedValue);
+ } else if (value instanceof Iterable) {
+ value = listToJsonArray((Iterable<?>) value);
+ } else if (value instanceof Editable) {
+ value = value.toString();
+ } else if (value == null) {
+ value = JSONObject.NULL;
+ }
+ obj.put(key, value);
+ }
+ return obj;
+ }
+
+
+ public static JSONArray listToJsonArray(Iterable<?> list) throws JSONException {
+ if (list == null) {
+ return null;
+ }
+ JSONArray obj = new JSONArray();
+ for (Object value : list) {
+ if (value instanceof Map) {
+ Map<String, ?> mappedValue = CollectionUtil.uncheckedCast(value);
+ value = mapToJsonObject(mappedValue);
+ } else if (value instanceof Iterable) {
+ value = listToJsonArray((Iterable<?>) value);
+ } else if (value == null) {
+ value = JSONObject.NULL;
+ }
+ obj.put(value);
+ }
+ return obj;
+ }
+
+ public static <T> Map<String, T> mapFromJson(JSONObject object) {
+ if (object == null) {
+ return null;
+ }
+ Map<String, T> result = new HashMap<>();
+ Iterator<?> keysIterator = object.keys();
+ while (keysIterator.hasNext()) {
+ String key = (String) keysIterator.next();
+ Object value = object.opt(key);
+ if (value == null || value == JSONObject.NULL) {
+ value = null;
+ } else if (value instanceof JSONObject) {
+ value = mapFromJson((JSONObject) value);
+ } else if (value instanceof JSONArray) {
+ value = listFromJson((JSONArray) value);
+ } else if (JSONObject.NULL.equals(value)) {
+ value = null;
+ }
+ T castedValue = CollectionUtil.uncheckedCast(value);
+ result.put(key, castedValue);
+ }
+ return result;
+ }
+
+ public static <T> Map<String, T> mapFromJsonOrDefault(JSONObject object) {
+ if (object == null) {
+ return new HashMap<>();
+ }
+ return mapFromJson(object);
+ }
+
+ public static <T> List<T> listFromJson(JSONArray json) {
+ if (json == null) {
+ return null;
+ }
+ List<Object> result = new ArrayList<>(json.length());
+ for (int i = 0; i < json.length(); i++) {
+ Object value = json.opt(i);
+ if (value == null || value == JSONObject.NULL) {
+ value = null;
+ } else if (value instanceof JSONObject) {
+ value = mapFromJson((JSONObject) value);
+ } else if (value instanceof JSONArray) {
+ value = listFromJson((JSONArray) value);
+ } else if (JSONObject.NULL.equals(value)) {
+ value = null;
+ }
+ result.add(value);
+ }
+ return CollectionUtil.uncheckedCast(result);
+ }
+
+ public static <T> List<T> listFromJsonOrDefault(JSONArray json) {
+ if (json == null) {
+ return new ArrayList<>();
+ }
+ return listFromJson(json);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/internal/LeanplumInternal.java
@@ -0,0 +1,666 @@
+/*
+ * 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.internal;
+
+import android.location.Address;
+import android.location.Geocoder;
+import android.location.Location;
+import android.os.AsyncTask;
+
+import com.leanplum.ActionContext;
+import com.leanplum.Leanplum;
+import com.leanplum.LeanplumActivityHelper;
+import com.leanplum.LeanplumException;
+import com.leanplum.LeanplumLocationAccuracyType;
+import com.leanplum.callbacks.ActionCallback;
+import com.leanplum.callbacks.StartCallback;
+import com.leanplum.callbacks.VariablesChangedCallback;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.ConcurrentModificationException;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Internal leanplum methods that are very generic but should not be exposed to the public.
+ *
+ * @author Ben Marten
+ */
+public class LeanplumInternal {
+ private static final String ACTION = "action";
+ private static boolean hasStartedAndRegisteredAsDeveloper;
+ private static final Map<String, List<ActionCallback>> actionHandlers = new HashMap<>();
+ private static boolean issuedStart;
+ private static boolean hasStarted;
+ private static boolean startSuccessful;
+ private static boolean calledStart;
+ private static boolean isPaused;
+
+ private static boolean startedInBackground;
+ private static boolean inForeground;
+ private static final Object inForegroundLock = new Object();
+ private static final Queue<Map<String, ?>> userAttributeChanges = new ConcurrentLinkedQueue<>();
+ private static final ArrayList<Runnable> startIssuedHandlers = new ArrayList<>();
+ private static boolean isScreenTrackingEnabled = false;
+
+ private static void onHasStartedAndRegisteredAsDeveloperAndFinishedSyncing() {
+ if (!hasStartedAndRegisteredAsDeveloper) {
+ hasStartedAndRegisteredAsDeveloper = true;
+ }
+ }
+
+ /**
+ * Called when the development mode has started and the device is registered successfully.
+ */
+ public static void onHasStartedAndRegisteredAsDeveloper() {
+ synchronized (FileManager.initializingLock) {
+ if (FileManager.initializing()) {
+ FileManager.setResourceSyncFinishBlock(new FileManager.ResourceUpdateCallback() {
+ @Override
+ public void onResourceSyncFinish() {
+ try {
+ onHasStartedAndRegisteredAsDeveloperAndFinishedSyncing();
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ }
+ });
+ } else {
+ onHasStartedAndRegisteredAsDeveloperAndFinishedSyncing();
+ }
+ }
+ }
+
+ public static void triggerAction(ActionContext context) {
+ triggerAction(context, null);
+ }
+
+ private static void triggerAction(final ActionContext context,
+ final VariablesChangedCallback handledCallback) {
+ List<ActionCallback> callbacks;
+ synchronized (actionHandlers) {
+ List<ActionCallback> handlers = actionHandlers.get(context.actionName());
+ if (handlers == null) {
+ // Handled by default.
+ if (handledCallback != null) {
+ handledCallback.variablesChanged();
+ }
+ return;
+ }
+ callbacks = new ArrayList<>(handlers);
+ }
+ final AtomicBoolean handled = new AtomicBoolean(false);
+ for (final ActionCallback callback : callbacks) {
+ OsHandler.getInstance().post(new Runnable() {
+ @Override
+ public void run() {
+ if (callback.onResponse(context) && handledCallback != null) {
+ if (!handled.getAndSet(true)) {
+ handledCallback.variablesChanged();
+ }
+ }
+ }
+ });
+ }
+ }
+
+ public static void maybePerformActions(String when, String eventName, int filter,
+ String fromMessageId, ActionContext.ContextualValues contextualValues) {
+ maybePerformActions(new String[] {when}, eventName, filter, fromMessageId, contextualValues);
+ }
+
+ static void maybePerformActions(String[] whenConditions, String eventName, int filter,
+ String fromMessageId, ActionContext.ContextualValues contextualValues) {
+ Map<String, Object> messages = VarCache.messages();
+ if (messages == null) {
+ return;
+ }
+
+ ArrayList<ActionContext> actionContexts = new ArrayList<>();
+ for (final String messageId : messages.keySet()) {
+ if (messageId != null && messageId.equals(fromMessageId)) {
+ continue;
+ }
+
+ Map<String, Object> messageConfig = CollectionUtil.uncheckedCast(messages.get(messageId));
+
+ String actionType = (String) messageConfig.get(ACTION);
+ if (actionType == null) {
+ continue;
+ }
+
+ final String internalMessageId;
+ if (actionType.equals(ActionManager.HELD_BACK_ACTION_NAME)) {
+ // Spoof the message ID for held back messages,
+ // and store the original ID for the track event.
+ internalMessageId = Constants.HELD_BACK_MESSAGE_PREFIX + messageId;
+ } else {
+ // If this is not a holdback, the internal message ID is the same as the original.
+ internalMessageId = messageId;
+ }
+
+ boolean isForeground = !actionType.equals(ActionManager.PUSH_NOTIFICATION_ACTION_NAME);
+ if (isForeground &&
+ ((filter & LeanplumMessageMatchFilter.LEANPLUM_ACTION_FILTER_FOREGROUND) == 0)) {
+ continue;
+ }
+
+ ActionManager.MessageMatchResult result = new ActionManager.MessageMatchResult();
+ for (String when : whenConditions) {
+ ActionManager.MessageMatchResult conditionResult = ActionManager.getInstance()
+ .shouldShowMessage(internalMessageId, messageConfig, when, eventName, contextualValues);
+ result.matchedTrigger |= conditionResult.matchedTrigger;
+ result.matchedUnlessTrigger |= conditionResult.matchedUnlessTrigger;
+ result.matchedLimit |= conditionResult.matchedLimit;
+ }
+
+ // Make sure we cancel before matching in case the criteria overlap.
+ if (result.matchedUnlessTrigger) {
+ String cancelActionName = "__Cancel" + messageConfig.get(ACTION);
+ LeanplumInternal.triggerAction(new ActionContext(
+ cancelActionName, new HashMap<String, Object>(), messageId),
+ new VariablesChangedCallback() {
+ @Override
+ public void variablesChanged() {
+ // Track cancel.
+ try {
+ Map<String, String> requestArgs = new HashMap<>();
+ requestArgs.put(Constants.Params.MESSAGE_ID, messageId);
+ track("Cancel", 0.0, null, null, requestArgs);
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ }
+ });
+ }
+
+ if (result.matchedTrigger) {
+ ActionManager.getInstance().recordMessageTrigger(internalMessageId);
+
+ if (result.matchedLimit) {
+ int priority;
+ if (messageConfig.containsKey("priority")) {
+ priority = (int) messageConfig.get("priority");
+ } else {
+ priority = Constants.Messaging.DEFAULT_PRIORITY;
+ }
+ Map<String, Object> vars = CollectionUtil.uncheckedCast(messageConfig.get(Constants
+ .Keys.VARS));
+ ActionContext actionContext = new ActionContext(
+ messageConfig.get(ACTION).toString(),
+ vars,
+ internalMessageId,
+ messageId,
+ priority);
+ actionContext.setContextualValues(contextualValues);
+ actionContexts.add(actionContext);
+ }
+ }
+ }
+
+ if (!actionContexts.isEmpty()) {
+ Collections.sort(actionContexts);
+ int priorityThreshold = actionContexts.get(0).getPriority();
+ for (final ActionContext actionContext : actionContexts) {
+ if (actionContext.getPriority() <= priorityThreshold) {
+ if (actionContext.actionName().equals(ActionManager.HELD_BACK_ACTION_NAME)) {
+ ActionManager.getInstance().recordHeldBackImpression(
+ actionContext.getMessageId(), actionContext.getOriginalMessageId());
+ } else {
+ LeanplumInternal.triggerAction(actionContext, new VariablesChangedCallback() {
+ @Override
+ public void variablesChanged() {
+ try {
+ ActionManager.getInstance().recordMessageImpression(actionContext.getMessageId());
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ }
+ });
+ }
+ } else {
+ break;
+ }
+ }
+ }
+ }
+
+ public static void track(final String event, double value, String info,
+ Map<String, ?> params, Map<String, String> args) {
+ if (Constants.isNoop()) {
+ return;
+ }
+
+ try {
+ final Map<String, Object> requestParams = new HashMap<>();
+ if (args != null) {
+ requestParams.putAll(args);
+ }
+ requestParams.put(Constants.Params.VALUE, Double.toString(value));
+ requestParams.put(Constants.Params.INFO, info);
+ if (event != null) {
+ requestParams.put(Constants.Params.EVENT, event);
+ }
+ if (params != null) {
+ params = validateAttributes(params, "params", false);
+ requestParams.put(Constants.Params.PARAMS, JsonConverter.toJson(params));
+ }
+ if (!inForeground || LeanplumActivityHelper.isActivityPaused()) {
+ requestParams.put("allowOffline", Boolean.TRUE.toString());
+ }
+
+ trackInternalWhenStarted(event, params, requestParams);
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ }
+
+ /**
+ * Performs the track operation once Leanplum has started. If Leanplum is already started, perform
+ * the track immediately.
+ *
+ * @param event The event name. Event may be empty for message impression events.
+ * @param params The event parameters.
+ * @param requestArgs The arguments to send with the API request.
+ */
+ private static void trackInternalWhenStarted(final String event,
+ final Map<String, ?> params, final Map<String, Object> requestArgs) {
+ if (issuedStart) {
+ trackInternal(event, params, requestArgs);
+ } else {
+ addStartIssuedHandler(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ trackInternal(event, params, requestArgs);
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ }
+ });
+ }
+ }
+
+ /**
+ * Performs the track API and any actions that are associated with the event.
+ *
+ * @param event The event name. Event may be empty for message impression events.
+ * @param params The event parameters.
+ * @param requestArgs The arguments to send with the API request.
+ */
+ private static void trackInternal(String event, Map<String, ?> params,
+ Map<String, Object> requestArgs) {
+ Request.post(Constants.Methods.TRACK, requestArgs).send();
+
+ String eventTriggerName = event;
+ String messageId = null;
+ if (requestArgs.get(Constants.Params.MESSAGE_ID) != null) {
+ messageId = requestArgs.get(Constants.Params.MESSAGE_ID).toString();
+ eventTriggerName = ".m" + messageId;
+ if (event != null && event.length() > 0) {
+ eventTriggerName += " " + event;
+ }
+ }
+
+ ActionContext.ContextualValues contextualValues = new ActionContext.ContextualValues();
+ contextualValues.parameters = params;
+ contextualValues.arguments = requestArgs;
+ if (contextualValues.arguments.containsKey(Constants.Params.PARAMS)) {
+ try {
+ contextualValues.arguments.put(Constants.Params.PARAMS,
+ new JSONObject(contextualValues.arguments.get(Constants.Params.PARAMS).toString()));
+ } catch (JSONException ignored) {
+ }
+ }
+ LeanplumInternal.maybePerformActions("event", eventTriggerName,
+ LeanplumMessageMatchFilter.LEANPLUM_ACTION_FILTER_ALL, messageId, contextualValues);
+ }
+
+ public static void performTrackedAction(String actionName, String messageId) {
+ Map<String, Object> messages = VarCache.messages();
+ if (messages == null) {
+ return;
+ }
+ Map<String, Object> messageConfig = CollectionUtil.uncheckedCast(messages.get(messageId));
+ if (messageConfig == null) {
+ return;
+ }
+ Map<String, Object> vars = CollectionUtil.uncheckedCast(messageConfig.get(Constants.Keys.VARS));
+ ActionContext context = new ActionContext(
+ messageConfig.get(ACTION).toString(),
+ vars,
+ messageId);
+ context.runTrackedActionNamed(actionName);
+ }
+
+ /**
+ * Callback for setUserLocationAttribute.
+ */
+ public interface locationAttributeRequestsCallback {
+ void response(boolean success);
+ }
+
+ /**
+ * Callback for setUserLocationAttribute.
+ *
+ * @param location Location of the device.
+ * @param locationAccuracyType LocationAccuracyType of the location.
+ * @param callback Callback for the requests was made.
+ */
+ public static void setUserLocationAttribute(final Location location,
+ final LeanplumLocationAccuracyType locationAccuracyType,
+ final locationAttributeRequestsCallback callback) {
+ Leanplum.addStartResponseHandler(new StartCallback() {
+ @Override
+ public void onResponse(final boolean success) {
+ // Geocoder query must be executed on background thread.
+ Util.executeAsyncTask(new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... voids) {
+ if (!success) {
+ return null;
+ }
+ if (location == null) {
+ Log.e("Location can't be null in setUserLocationAttribute.");
+ return null;
+ }
+ String latLongLocation = String.format(Locale.US, "%.6f,%.6f", location.getLatitude(),
+ location.getLongitude());
+ HashMap<String, Object> params = new HashMap<>();
+ params.put(Constants.Keys.LOCATION, latLongLocation);
+ params.put(Constants.Keys.LOCATION_ACCURACY_TYPE,
+ locationAccuracyType.name().toLowerCase());
+ if (Leanplum.getContext() != null && Locale.US != null) {
+ Geocoder geocoder = new Geocoder(Leanplum.getContext(), Locale.US);
+ try {
+ List<Address> addresses = geocoder.getFromLocation(location.getLatitude(),
+ location.getLongitude(), 1);
+ if (addresses != null && addresses.size() > 0) {
+ Address address = addresses.get(0);
+ params.put(Constants.Keys.CITY, address.getLocality());
+ params.put(Constants.Keys.REGION, address.getAdminArea());
+ params.put(Constants.Keys.COUNTRY, address.getCountryCode());
+ }
+ } catch (IOException e) {
+ Log.e("Failed to connect to Geocoder: " + e);
+ }
+ }
+ Request req = Request.post(Constants.Methods.SET_USER_ATTRIBUTES, params);
+ req.onResponse(new Request.ResponseCallback() {
+ @Override
+ public void response(JSONObject response) {
+ callback.response(true);
+ }
+ });
+ req.onError(new Request.ErrorCallback() {
+ @Override
+ public void error(Exception e) {
+ callback.response(false);
+ Log.e("setUserAttributes failed when specifying location with error: " +
+ e.getMessage());
+ }
+ });
+ req.send();
+ return null;
+ }
+ });
+ }
+ });
+ }
+
+ public static void recordAttributeChanges() {
+ boolean madeChanges = false;
+ for (Map<String, ?> attributes : userAttributeChanges) {
+ Map<String, Object> existingAttributes = VarCache.userAttributes();
+ if (existingAttributes == null) {
+ existingAttributes = new HashMap<>();
+ }
+ for (String attributeName : attributes.keySet()) {
+ Object existingValue = existingAttributes.get(attributeName);
+ Object value = attributes.get(attributeName);
+ if ((existingValue == null && value != null) ||
+ (existingValue != null && !existingValue.equals(value))) {
+ ActionContext.ContextualValues contextualValues = new ActionContext.ContextualValues();
+ contextualValues.previousAttributeValue = existingValue;
+ contextualValues.attributeValue = value;
+ existingAttributes.put(attributeName, value);
+ LeanplumInternal.maybePerformActions("userAttribute", attributeName,
+ LeanplumMessageMatchFilter.LEANPLUM_ACTION_FILTER_ALL, null, contextualValues);
+ madeChanges = true;
+ }
+ }
+ }
+ userAttributeChanges.clear();
+ if (madeChanges) {
+ VarCache.saveUserAttributes();
+ }
+ }
+
+ public static void moveToForeground() {
+ synchronized (inForegroundLock) {
+ if (inForeground) {
+ return;
+ }
+ inForeground = true;
+ }
+
+ Leanplum.addStartResponseHandler(new StartCallback() {
+ @Override
+ public void onResponse(boolean success) {
+ if (!success) {
+ return;
+ }
+ if (Constants.isDevelopmentModeEnabled && !Constants.isNoop()) {
+ Socket.getInstance();
+ }
+ maybePerformActions(new String[] {"start", "resume"}, null,
+ LeanplumMessageMatchFilter.LEANPLUM_ACTION_FILTER_ALL, null, null);
+ recordAttributeChanges();
+ }
+ });
+ }
+
+ public static void addStartIssuedHandler(Runnable handler) {
+ if (handler == null) {
+ Log.e("addStartIssuedHandler - Invalid handler parameter provided.");
+ return;
+ }
+
+ synchronized (startIssuedHandlers) {
+ if (!issuedStart) {
+ startIssuedHandlers.add(handler);
+ return;
+ }
+ }
+ handler.run();
+ }
+
+ public static void triggerStartIssued() {
+ synchronized (startIssuedHandlers) {
+ LeanplumInternal.setIssuedStart(true);
+ for (Runnable callback : startIssuedHandlers) {
+ OsHandler.getInstance().post(callback);
+ }
+ startIssuedHandlers.clear();
+ }
+ }
+
+ /**
+ * Validates that the attributes are of the correct format. User attributes must be Strings,
+ * Booleans, or Numbers.
+ *
+ * @param argName Argument name used for error messages.
+ * @param allowLists Whether attribute values can be lists.
+ */
+ public static <T> Map<String, T> validateAttributes(Map<String, T> attributes, String argName,
+ boolean allowLists) {
+ if (attributes == null) {
+ return null;
+ }
+ Map<String, T> validAttributes = new HashMap<>();
+ try {
+ for (Map.Entry<String, T> entry : attributes.entrySet()) {
+ T value = entry.getValue();
+
+ if (value == null) {
+ continue;
+ }
+
+ // Validate lists.
+ if (allowLists && value instanceof Iterable<?>) {
+ boolean valid = true;
+ Iterable<Object> iterable = CollectionUtil.uncheckedCast(value);
+ for (Object item : iterable) {
+ if (!isValidScalarValue(item, argName)) {
+ valid = false;
+ break;
+ }
+ }
+ if (!valid) {
+ continue;
+ }
+
+ // Validate scalars.
+ } else {
+ if (value instanceof Date) {
+ Date date = CollectionUtil.uncheckedCast(value);
+ value = CollectionUtil.uncheckedCast(date.getTime());
+ }
+ if (!isValidScalarValue(value, argName)) {
+ continue;
+ }
+ }
+ validAttributes.put(entry.getKey(), value);
+ }
+ } catch (ConcurrentModificationException e) {
+ maybeThrowException(new LeanplumException("ConcurrentModificationException: You cannot " +
+ "modify Map<String, ?> attributes/parameters. Will override with an empty map"));
+ validAttributes = new HashMap<>();
+ }
+ return validAttributes;
+ }
+
+ @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+ private static boolean isValidScalarValue(Object value, String argName) {
+ if (!(value instanceof Number) && !(value instanceof String) && !(value instanceof Boolean)) {
+ maybeThrowException(new LeanplumException(
+ argName + " values must be of type String, Number, or Boolean."));
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Throws a LeanplumException when in development mode. Otherwise, logs the exception to prevent
+ * the app from crashing.
+ */
+ public static void maybeThrowException(RuntimeException e) {
+ if (Constants.isDevelopmentModeEnabled) {
+ throw e;
+ } else {
+ Log.e(e.getMessage() + " This error is only thrown in development mode.", e);
+ }
+ }
+
+ /***
+ * Getters & Setters
+ ***/
+ public static boolean hasStartedAndRegisteredAsDeveloper() {
+ return hasStartedAndRegisteredAsDeveloper;
+ }
+
+ public static Map<String, List<ActionCallback>> getActionHandlers() {
+ return actionHandlers;
+ }
+
+ public static boolean issuedStart() {
+ return issuedStart;
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private static void setIssuedStart(boolean issuedStart) {
+ LeanplumInternal.issuedStart = issuedStart;
+ }
+
+ public static boolean hasStarted() {
+ return hasStarted;
+ }
+
+ public static void setHasStarted(boolean hasStarted) {
+ LeanplumInternal.hasStarted = hasStarted;
+ }
+
+ public static boolean isStartSuccessful() {
+ return startSuccessful;
+ }
+
+ public static void setStartSuccessful(boolean startSuccessful) {
+ LeanplumInternal.startSuccessful = startSuccessful;
+ }
+
+ public static boolean hasCalledStart() {
+ return calledStart;
+ }
+
+ public static void setCalledStart(boolean calledStart) {
+ LeanplumInternal.calledStart = calledStart;
+ }
+
+ public static boolean isPaused() {
+ return isPaused;
+ }
+
+ public static void setIsPaused(boolean isPaused) {
+ LeanplumInternal.isPaused = isPaused;
+ }
+
+ public static boolean hasStartedInBackground() {
+ return startedInBackground;
+ }
+
+ public static void setStartedInBackground(boolean startedInBackground) {
+ LeanplumInternal.startedInBackground = startedInBackground;
+ }
+
+ public static Queue<Map<String, ?>> getUserAttributeChanges() {
+ return userAttributeChanges;
+ }
+
+ public static boolean getIsScreenTrackingEnabled() {
+ return isScreenTrackingEnabled;
+ }
+
+ public static void enableAutomaticScreenTracking() {
+ isScreenTrackingEnabled = true;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/internal/LeanplumManifestHelper.java
@@ -0,0 +1,583 @@
+/*
+ * 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.internal;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.text.TextUtils;
+
+import com.leanplum.Leanplum;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.jar.JarFile;
+import java.util.zip.ZipEntry;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+/**
+ * LeanplumManifestHelper class to work with AndroidManifest components.
+ *
+ * @author Anna Orlova
+ */
+public class LeanplumManifestHelper {
+ private static final String MANIFEST = "manifest";
+ private static final String APPLICATION = "application";
+ private static final String SERVICE = "service";
+ private static final String ACTIVITY = "activity";
+ private static final String ACTIVITY_ALIAS = "activity-alias";
+ private static final String RECEIVER = "receiver";
+ private static final String PROVIDER = "provider";
+ private static final String ANDROID_NAME = "android:name";
+ private static final String ANDROID_SCHEME = "android:scheme";
+ private static final String ACTION = "action";
+ private static final String CATEGORY = "category";
+ private static final String DATA = "data";
+ private static final String ANDROID_HOST = "android:host";
+ private static final String ANDROID_PORT = "android:port";
+ private static final String ANDROID_PATH = "android:path";
+ private static final String ANDROID_PATH_PATTERN = "android:pathPattern";
+ private static final String ANDROID_PATH_PREFIX = "android:pathPrefix";
+ private static final String ANDROID_MIME_TYPE = "android:mimeType";
+ private static final String ANDROID_TYPE = "android:type";
+ private static final String INTENT_FILTER = "intent-filter";
+ private static final String ANDROID_PERMISSION = "android:permission";
+ private static final String ANDROID_MANIFEST = "AndroidManifest.xml";
+ private static final String VERSION_NAME = "versionName";
+
+ private static ManifestData manifestData;
+
+ /**
+ * Gets application components from AndroidManifest.xml file.
+ */
+ private static void parseManifestNodeChildren() {
+ manifestData = new ManifestData();
+ byte[] manifestXml = getByteArrayOfManifest();
+ Document manifestDocument = getManifestDocument(manifestXml);
+ parseManifestDocument(manifestDocument);
+ }
+
+ /**
+ * Parse AndroidManifest.xml file to byte array.
+ *
+ * @return byte[] Byte array of AndroidManifest.xml file
+ */
+ private static byte[] getByteArrayOfManifest() {
+ Context context = Leanplum.getContext();
+ if (context == null) {
+ Log.e("Context is null. Cannot parse " + ANDROID_MANIFEST + " file.");
+ return null;
+ }
+ byte[] manifestXml = null;
+ try {
+ JarFile jarFile = new JarFile(context.getPackageResourcePath());
+ ZipEntry entry = jarFile.getEntry(ANDROID_MANIFEST);
+ manifestXml = new byte[(int) entry.getSize()];
+ DataInputStream dataInputStream = new DataInputStream(jarFile.getInputStream(entry));
+ dataInputStream.readFully(manifestXml);
+ dataInputStream.close();
+ } catch (Exception e) {
+ Log.e("Cannot parse " + ANDROID_MANIFEST + " file: " + e.getMessage());
+ }
+ return manifestXml;
+ }
+
+ /**
+ * Gets Document {@link Document} of AndroidManifest.xml file.
+ *
+ * @param manifestXml Byte array of AndroidManifest.xml file data.
+ * @return Document Document with date of AndroidManifest.xml file.
+ */
+ private static Document getManifestDocument(byte[] manifestXml) {
+ Document document = null;
+ try {
+ DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
+ DocumentBuilder builder = builderFactory.newDocumentBuilder();
+ document = builder.parse(new ByteArrayInputStream(
+ LeanplumManifestParser.decompressXml(manifestXml).getBytes("UTF-8")));
+ } catch (Exception e) {
+ Log.e("Cannot parse " + ANDROID_MANIFEST + " file: " + e.getMessage());
+ }
+ return document;
+ }
+
+ /**
+ * Parse data from Document {@link Document} with date of AndroidManifest.xml file.
+ *
+ * @param document Document {@link Document} with date of AndroidManifest.xml file.
+ */
+ private static void parseManifestDocument(Document document) {
+ if (document == null) {
+ return;
+ }
+ parseManifestNode(document.getElementsByTagName(MANIFEST).item(0));
+ }
+
+ private static void parseManifestNode(Node manifestNode) {
+ if (manifestNode == null) {
+ return;
+ }
+ manifestData.appVersionName = getAttribute(manifestNode.getAttributes(), VERSION_NAME);
+ NodeList manifestChildren = manifestNode.getChildNodes();
+ if (manifestChildren == null) {
+ return;
+ }
+ for (int i = 0; i < manifestChildren.getLength(); i++) {
+ Node currentNode = manifestChildren.item(i);
+ if (currentNode == null) {
+ continue;
+ }
+ String currentNodeName = currentNode.getNodeName();
+ if (APPLICATION.equals(currentNodeName)) {
+ parseChildNodeList(currentNode.getChildNodes());
+ }
+ }
+ }
+
+ private static void parseChildNodeList(NodeList childrenList) {
+ if (childrenList == null) {
+ return;
+ }
+ for (int j = 0; j < childrenList.getLength(); j++) {
+ parseChildNode(childrenList.item(j));
+ }
+ }
+
+ private static void parseChildNode(Node child) {
+ if (child == null) {
+ return;
+ }
+ String childName = child.getNodeName();
+ if (childName == null) {
+ return;
+ }
+ switch (childName) {
+ case SERVICE:
+ manifestData.services.add(parseManifestComponent(child,
+ ManifestComponent.ApplicationComponent.SERVICE));
+ break;
+ case RECEIVER:
+ manifestData.receivers.add(parseManifestComponent(child,
+ ManifestComponent.ApplicationComponent.RECEIVER));
+ break;
+ case ACTIVITY:
+ case ACTIVITY_ALIAS:
+ manifestData.activities.add(parseManifestComponent(child,
+ ManifestComponent.ApplicationComponent.ACTIVITY));
+ break;
+ case PROVIDER:
+ manifestData.providers.add(parseManifestComponent(child,
+ ManifestComponent.ApplicationComponent.PROVIDER));
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * Parse AndroidManifest.xml components from XML.
+ *
+ * @param node XML node to parse.
+ * @param type Type of application component {@link ManifestComponent.ApplicationComponent}.
+ * @return Return ManifestComponent {@link ManifestComponent} with information from manifest.
+ */
+ private static ManifestComponent parseManifestComponent(Node node,
+ ManifestComponent.ApplicationComponent type) {
+ ManifestComponent manifestComponent = new ManifestComponent(type);
+ NamedNodeMap attributes = node.getAttributes();
+ manifestComponent.name = getAttribute(attributes, ANDROID_NAME);
+ manifestComponent.permission = getAttribute(attributes, ANDROID_PERMISSION);
+ List<ManifestIntentFilter> intentFilters = new ArrayList<>();
+ NodeList childrenList = node.getChildNodes();
+ for (int i = 0; i < childrenList.getLength(); i++) {
+ Node child = childrenList.item(i);
+ String childName = child.getNodeName();
+ if (INTENT_FILTER.equals(childName)) {
+ ManifestIntentFilter intentFilter = parseManifestIntentFilter(child);
+ if (intentFilter != null) {
+ intentFilters.add(intentFilter);
+ }
+ }
+ }
+ manifestComponent.intentFilters = intentFilters;
+ return manifestComponent;
+ }
+
+ /**
+ * Parse intent filter from XML node.
+ *
+ * @param intentNode XML node to parse.
+ * @return Return ManifestIntentFilter {@link ManifestIntentFilter} with information from
+ * manifest.
+ */
+ private static ManifestIntentFilter parseManifestIntentFilter(Node intentNode) {
+ if (intentNode == null) {
+ return null;
+ }
+
+ NodeList intentChildren = intentNode.getChildNodes();
+ if (intentChildren == null) {
+ return null;
+ }
+
+ ManifestIntentFilter intentFilter = new ManifestIntentFilter();
+ intentFilter.attributes = intentNode.getAttributes();
+ for (int j = 0; j < intentChildren.getLength(); j++) {
+ Node intentChild = intentChildren.item(j);
+ String intentChildName = intentChild.getNodeName();
+ NamedNodeMap intentChildAttributes = intentChild.getAttributes();
+ switch (intentChildName) {
+ case ACTION:
+ intentFilter.actions.add(getAttribute(intentChildAttributes, ANDROID_NAME));
+ break;
+ case CATEGORY:
+ intentFilter.categories.add(getAttribute(intentChildAttributes, ANDROID_NAME));
+ break;
+ case DATA:
+ String scheme = getAttribute(intentChildAttributes, ANDROID_SCHEME);
+ String host = getAttribute(intentChildAttributes, ANDROID_HOST);
+ String port = getAttribute(intentChildAttributes, ANDROID_PORT);
+ String path = getAttribute(intentChildAttributes, ANDROID_PATH);
+ String pathPattern = getAttribute(intentChildAttributes, ANDROID_PATH_PATTERN);
+ String pathPrefix = getAttribute(intentChildAttributes, ANDROID_PATH_PREFIX);
+ String mimeType = getAttribute(intentChildAttributes, ANDROID_MIME_TYPE);
+ String type = getAttribute(intentChildAttributes, ANDROID_TYPE);
+ intentFilter.dataList.add(new ManifestIntentFilter.IntentData(scheme, host, port, path,
+ pathPattern, pathPrefix, mimeType, type));
+ break;
+ default:
+ break;
+ }
+ }
+ return intentFilter;
+ }
+
+ /**
+ * @return Return String with attribute value or null if attribute with this name not found.
+ */
+ private static String getAttribute(NamedNodeMap namedNodeMap, String name) {
+ Node node = namedNodeMap.getNamedItem(name);
+ if (node == null) {
+ if (name.startsWith("android:")) {
+ name = name.substring("android:".length());
+ }
+ node = namedNodeMap.getNamedItem(name);
+ if (node == null) {
+ return null;
+ }
+ }
+ return node.getNodeValue();
+ }
+
+ /**
+ * @return Return List of services from manifest.
+ */
+ public static List<ManifestComponent> getServices() {
+ if (manifestData == null) {
+ parseManifestNodeChildren();
+ }
+ return manifestData.services;
+ }
+
+ /**
+ * @return Return List of activities from manifest.
+ */
+ static List<ManifestComponent> getActivities() {
+ if (manifestData == null) {
+ parseManifestNodeChildren();
+ }
+ return manifestData.activities;
+ }
+
+ /**
+ * @return Return List of providers from manifest.
+ */
+ static List<ManifestComponent> getProviders() {
+ if (manifestData == null) {
+ parseManifestNodeChildren();
+ }
+ return manifestData.providers;
+ }
+
+ /**
+ * @return Return List of receivers from manifest.
+ */
+ public static List<ManifestComponent> getReceivers() {
+ if (manifestData == null) {
+ parseManifestNodeChildren();
+ }
+ return manifestData.receivers;
+ }
+
+ /**
+ * @return String String of application version name.
+ */
+ public static String getAppVersionName() {
+ if (manifestData == null) {
+ parseManifestNodeChildren();
+ }
+ return manifestData.appVersionName;
+ }
+
+ /**
+ * Verifies that a certain component (receiver or service) is implemented in the
+ * AndroidManifest.xml file or the application, in order to make sure that push notifications
+ * work.
+ *
+ * @param componentsList List of application components(services or receivers).
+ * @param name The name of the class.
+ * @param exported What the exported option should be.
+ * @param permission Whether we need any permission.
+ * @param actions What actions we need to check for in the intent-filter.
+ * @param packageName The package name for the category tag, if we require one.
+ * @return true if the respective component is in the manifest file, and false otherwise.
+ */
+ public static boolean checkComponent(List<ManifestComponent> componentsList,
+ String name, boolean exported, String permission, List<String> actions, String packageName) {
+ boolean hasComponent = hasComponent(componentsList, name, permission, actions);
+ if (!hasComponent && !componentsList.isEmpty()) {
+ Log.e(getComponentError(componentsList.get(0).type, name, exported, permission, actions,
+ packageName));
+ }
+ return hasComponent;
+ }
+
+ /**
+ * Check if list of application components contains class instance of class with name className.
+ *
+ * @param componentsList List of application components(services or receivers).
+ * @param className The name of the class.
+ * @param permission Whether we need any permission..
+ * @param actions What actions we need to check for in the intent-filter.
+ * @return boolean True if componentList contains class instance of class with name className.
+ */
+ private static boolean hasComponent(List<ManifestComponent> componentsList, String className,
+ String permission, List<String> actions) {
+ for (ManifestComponent component : componentsList) {
+ if (isInstance(component, className)) {
+ if (hasPermission(component, permission, actions)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Check if component instance of class with name className.
+ *
+ * @param component Application component(service or receiver).
+ * @param className The name of the class.
+ * @return boolean True if component instance of class with name className.
+ */
+ private static boolean isInstance(ManifestComponent component, String className) {
+ try {
+ if (component.name.equals(className)) {
+ return true;
+ } else {
+ Class clazz = null;
+ try {
+ clazz = Class.forName(component.name);
+ } catch (Throwable ignored) {
+ }
+ if (clazz == null) {
+ Log.w("Cannot find class with name: " + component.name);
+ return false;
+ }
+ while (clazz != Object.class) {
+ clazz = clazz.getSuperclass();
+ if (clazz.getName().equals(className)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ } catch (Exception e) {
+ Util.handleException(e);
+ return false;
+ }
+ }
+
+ /**
+ * Check if application component has permission with provided actions.
+ *
+ * @param component application component(service or receiver).
+ * @param permission Whether we need any permission.
+ * @param actions What actions we need to check for in the intent-filter.
+ * @return boolean True if component has permission with actions.
+ */
+ private static boolean hasPermission(ManifestComponent component, String permission,
+ List<String> actions) {
+ Boolean hasPermissions = TextUtils.equals(component.permission, permission);
+ if (hasPermissions && actions != null) {
+ HashSet<String> actionsToCheck = new HashSet<>(actions);
+ for (ManifestIntentFilter intentFilter : component.intentFilters) {
+ actionsToCheck.removeAll(intentFilter.actions);
+ }
+ if (actionsToCheck.isEmpty()) {
+ return true;
+ }
+ } else if (hasPermissions) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Gets string of error message with instruction how to set up AndroidManifest.xml for push
+ * notifications.
+ *
+ * @return String String of error message with instruction how to set up AndroidManifest.xml for
+ * push notifications.
+ */
+ private static String getComponentError(ManifestComponent.ApplicationComponent componentType,
+ String name, boolean exported, String permission, List<String> actions, String packageName) {
+ StringBuilder errorMessage = new StringBuilder("Push notifications requires you to add the " +
+ componentType.name().toLowerCase() + " " + name + " to your AndroidManifest.xml file." +
+ "Add this code within the <application> section:\n");
+ errorMessage.append("<").append(componentType.name().toLowerCase()).append("\n");
+ errorMessage.append(" android:name=\"").append(name).append("\"\n");
+ errorMessage.append(" android:exported=\"").append(Boolean.toString(exported)).append("\"");
+ if (permission != null) {
+ errorMessage.append("\n android:permission=\"").append(permission).append("\"");
+ }
+ errorMessage.append(">\n");
+ if (actions != null) {
+ errorMessage.append(" <intent-filter>\n");
+ for (String action : actions) {
+ errorMessage.append(" <action android:name=\"").append(action).append("\" />\n");
+ }
+ if (packageName != null) {
+ errorMessage.append(" <category android:name=\"").append(packageName)
+ .append("\" />\n");
+ }
+ errorMessage.append(" </intent-filter>\n");
+ }
+ errorMessage.append("</").append(componentType.name().toLowerCase()).append(">");
+ return errorMessage.toString();
+ }
+
+ /**
+ * Check if the application has registered for a certain permission.
+ *
+ * @param permission Requested permission.
+ * @param definesPermission Permission need definition or not.
+ * @param logError Need print log or not.
+ * @return boolean True if application has permission.
+ */
+ public static boolean checkPermission(String permission, boolean definesPermission,
+ boolean logError) {
+ Context context = Leanplum.getContext();
+ if (context == null) {
+ return false;
+ }
+ if (context.checkCallingOrSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
+ String definition;
+ if (definesPermission) {
+ definition = "<permission android:name=\"" + permission +
+ "\" android:protectionLevel=\"signature\" />\n";
+ } else {
+ definition = "";
+ }
+ if (logError) {
+ Log.e("In order to use push notifications, you need to enable " +
+ "the " + permission + " permission in your AndroidManifest.xml file. " +
+ "Add this within the <manifest> section:\n" +
+ definition + "<uses-permission android:name=\"" + permission + "\" />");
+ }
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Class with Android manifest data.
+ */
+ private static class ManifestData {
+ private List<ManifestComponent> services = new ArrayList<>();
+ private List<ManifestComponent> activities = new ArrayList<>();
+ private List<ManifestComponent> receivers = new ArrayList<>();
+ private List<ManifestComponent> providers = new ArrayList<>();
+ private String appVersionName;
+ }
+
+ /**
+ * Class with application component from AndroidManifest.xml.
+ */
+ private static class ManifestComponent {
+ enum ApplicationComponent {SERVICE, RECEIVER, ACTIVITY, PROVIDER}
+
+ ApplicationComponent type;
+ String name;
+ String permission;
+ List<ManifestIntentFilter> intentFilters = new ArrayList<>();
+
+ ManifestComponent(ApplicationComponent type) {
+ this.type = type;
+ }
+ }
+
+ /**
+ * Class for declaration of intent filter from AndroidManifest.
+ */
+ private static class ManifestIntentFilter {
+ final List<String> actions = new ArrayList<>();
+ final List<String> categories = new ArrayList<>();
+ final List<IntentData> dataList = new ArrayList<>();
+ public NamedNodeMap attributes;
+
+ /**
+ * Class for data of intent filter from AndroidManifest.
+ */
+ static class IntentData {
+ final String scheme;
+ final String host;
+ final String port;
+ final String path;
+ final String pathPattern;
+ final String pathPrefix;
+ final String mimeType;
+ final String type;
+
+ IntentData(String scheme, String host, String port, String path, String pathPattern,
+ String pathPrefix, String mimeType, String type) {
+ this.scheme = scheme;
+ this.host = host;
+ this.port = port;
+ this.path = path;
+ this.pathPattern = pathPattern;
+ this.pathPrefix = pathPrefix;
+ this.mimeType = mimeType;
+ this.type = type;
+ }
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/internal/LeanplumManifestParser.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.internal;
+
+/**
+ * LeanplumManifestParser class for get AndroidManifest.xml. http://stackoverflow.com/questions/2097813/how-to-parse-the-androidmanifest-xml-file-inside-an-apk-package
+ *
+ * @author Anna Orlova
+ */
+class LeanplumManifestParser {
+ // XML tags and attributes:
+ // Every XML start and end tag consists of 6 32 bit words:
+ // 0th word: 02011000 for START_TAG and 03011000 for END_TAG
+ // 1st word: a flag?, like 38000000
+ // 2nd word: Line of where this tag appeared in the original source file
+ // 3rd word: FFFFFFFF ??
+ // 4th word: StringIndex of NameSpace name, or FFFFFFFF for default NS
+ // 5th word: StringIndex of Element Name
+ // (Note: 01011000 in 0th word means end of XML document, END_DOC_TAG).
+
+ // Start tags (not end tags) contain 3 more words:
+ // 6th word: 14001400 meaning??
+ // 7th word: Number of Attributes that follow this tag(follow word 8th)
+ // 8th word: 00000000 meaning??
+
+ // Attributes consist of 5 words:
+ // 0th word: StringIndex of Attribute Name's Namespace, or FFFFFFFF
+ // 1st word: StringIndex of Attribute Name
+ // 2nd word: StringIndex of Attribute Value, or FFFFFFF if ResourceId used
+ // 3rd word: Flags?
+ // 4th word: str ind of attr value again, or ResourceId of value.
+ // END_DOC_TAG = 0x00100101;
+ private static final int START_TAG = 0x00100102;
+ private static final int END_TAG = 0x00100103;
+ private static final String SPACES = " ";
+
+ /**
+ * Parse the 'compressed' binary form of Android XML docs such as for AndroidManifest.xml in .apk
+ * files.
+ *
+ * @param xml byte array of AndroidManifest.xml.
+ * @return String with data of AndroidManifest.xml.
+ */
+ static String decompressXml(byte[] xml) {
+ String out = "";
+ // Compressed XML file/bytes starts with 24x bytes of data,
+ // 9 32 bit words in little endian order (LSB first):
+ // 0th word is 03 00 08 00
+ // 3rd word SEEMS TO BE: Offset at then of StringTable
+ // 4th word is: Number of strings in string table
+ // WARNING: Sometime I indiscriminately display or refer to word in
+ // little endian storage format, or in integer format (ie MSB first).
+ int numbStrings = littleEndianValue(xml, 4 * 4);
+ // StringIndexTable starts at offset 24x, an array of 32 bit LE offsets
+ // of the length/string data in the StringTable.
+ int sitOff = 0x24; // Offset of start of StringIndexTable.
+ // StringTable, each string is represented with a 16 bit little endian
+ // character count, followed by that number of 16 bit (LE) (Unicode) chars.
+ int stOff = sitOff + numbStrings * 4; // StringTable follows StrIndexTable.
+ // Step through the XML tree element tags and attributes.
+ int off = scanForFirstStartTag(xml);
+ int indent = 0;
+
+ while (off < xml.length) {
+ int tag0 = littleEndianValue(xml, off);
+ int nameSi = littleEndianValue(xml, off + 5 * 4);
+ if (tag0 == START_TAG) {
+ int numbAttrs = littleEndianValue(xml, off + 7 * 4); // Number of Attributes to follow.
+ off += 9 * 4; // Skip over 6+3 words of START_TAG data
+ String name = compXmlString(xml, sitOff, stOff, nameSi);
+ // Look for the Attributes
+ StringBuilder sb = new StringBuilder();
+ for (int ii = 0; ii < numbAttrs; ii++) {
+ int attrNameSi = littleEndianValue(xml, off + 4); // AttrName String Index.
+ int attrValueSi = littleEndianValue(xml, off + 2 * 4); // AttrValue Str Ind, or FFFFFFFF.
+ int attrResId = littleEndianValue(xml, off + 4 * 4); // AttrValue ResourceId or dup.
+ // AttrValue StrInd.
+ off += 5 * 4; // Skip over the 5 words of an attribute.
+ String attrName = compXmlString(xml, sitOff, stOff, attrNameSi);
+ String attrValue = attrValueSi != -1
+ ? compXmlString(xml, sitOff, stOff, attrValueSi)
+ : "resourceID 0x" + Integer.toHexString(attrResId);
+ sb.append(" ").append(attrName).append("=\"").append(attrValue).append("\"");
+ }
+ out += SPACES.substring(0, Math.min(indent * 2, SPACES.length())) + "<" + name + sb + ">";
+ indent++;
+ } else if (tag0 == END_TAG) {
+ indent--;
+ off += 6 * 4; // Skip over 6 words of END_TAG data
+ String name = compXmlString(xml, sitOff, stOff, nameSi);
+ out += SPACES.substring(0, Math.min(indent * 2, SPACES.length())) + "</" + name + ">";
+
+ } else {
+ break;
+ }
+ }
+ return out;
+ }
+
+ private static String compXmlString(byte[] xml, int sitOff, int stOff, int strInd) {
+ if (strInd < 0) return null;
+ int strOff = stOff + littleEndianValue(xml, sitOff + strInd * 4);
+ return compXmlStringAt(xml, strOff);
+ }
+
+ /**
+ * @return Return the string stored in StringTable format at offset strOff. This offset points to
+ * the 16 bit string length, which is followed by that number of 16 bit (Unicode) chars.
+ */
+ private static String compXmlStringAt(byte[] arr, int strOff) {
+ int strLen = arr[strOff + 1] << 8 & 0xff00 | arr[strOff] & 0xff;
+ byte[] chars = new byte[strLen];
+ for (int ii = 0; ii < strLen; ii++) {
+ chars[ii] = arr[strOff + 2 + ii * 2];
+ }
+ return new String(chars); // Hack, just use 8 byte chars.
+ }
+
+ /**
+ * @return Return value of a Little Endian 32 bit word from the byte array at offset off.
+ */
+ private static int littleEndianValue(byte[] arr, int off) {
+ return arr[off + 3] << 24 & 0xff000000 | arr[off + 2] << 16 & 0xff0000
+ | arr[off + 1] << 8 & 0xff00 | arr[off] & 0xFF;
+ }
+
+ private static int scanForFirstStartTag(byte[] xml) {
+ // XMLTags, The XML tag tree starts after some unknown content after the
+ // StringTable. There is some unknown data after the StringTable, scan forward
+ // from this point to the flag for the start of an XML start tag.
+ int xmlTagOff = littleEndianValue(xml, 3 * 4); // Start from the offset in the 3rd word.
+ // Scan forward until we find the bytes: 0x02011000(x00100102 in normal int).
+ for (int ii = xmlTagOff; ii < xml.length - 4; ii += 4) {
+ if (littleEndianValue(xml, ii) == START_TAG) {
+ xmlTagOff = ii;
+ break;
+ }
+ }
+ return xmlTagOff;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/internal/LeanplumMessageMatchFilter.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.internal;
+
+/**
+ * Messaging filter.
+ *
+ * @author Atanas Dobrev
+ */
+public class LeanplumMessageMatchFilter {
+ public static final int LEANPLUM_ACTION_FILTER_FOREGROUND = 1;
+ public static final int LEANPLUM_ACTION_FILTER_BACKGROUND = 2;
+ public static final int LEANPLUM_ACTION_FILTER_ALL = 3;
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/internal/LeanplumUIEditorWrapper.java
@@ -0,0 +1,154 @@
+/*
+ * 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.internal;
+
+import android.app.Activity;
+
+import com.leanplum.LeanplumEditorMode;
+import com.leanplum.LeanplumUIEditor;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import static com.leanplum.internal.Constants.ClassUtil.UI_INTERFACE_EDITOR;
+
+/**
+ * Wrapper class for the UI Editor. Method calls will be forwarded to UI Editor package if its
+ * available.
+ */
+public class LeanplumUIEditorWrapper implements LeanplumUIEditor {
+ private static LeanplumUIEditor interfaceEditorSingleton;
+ private static LeanplumUIEditorWrapper instance = null;
+
+ protected LeanplumUIEditorWrapper() {
+ // Exists only to defeat instantiation.
+ }
+
+ static {
+ Class<?> clazz = null;
+ try {
+ clazz = Class.forName(UI_INTERFACE_EDITOR);
+ } catch (ClassNotFoundException ignored) {
+ }
+ if (clazz != null) {
+ Method method = null;
+ try {
+ method = clazz.getMethod("getInstance");
+ } catch (NoSuchMethodException e) {
+ Util.handleException(e);
+ }
+ if (method != null) {
+ try {
+ interfaceEditorSingleton = (LeanplumUIEditor) method.invoke(null);
+ if (interfaceEditorSingleton != null) {
+ interfaceEditorSingleton.allowInterfaceEditing(Constants.isDevelopmentModeEnabled);
+ }
+ } catch (IllegalAccessException e) {
+ Util.handleException(e);
+ } catch (InvocationTargetException e) {
+ Util.handleException(e);
+ }
+ }
+ }
+ }
+
+ public static LeanplumUIEditorWrapper getInstance() {
+ if (instance == null) {
+ instance = new LeanplumUIEditorWrapper();
+ }
+ return instance;
+ }
+
+ public static boolean isUIEditorAvailable() {
+ return interfaceEditorSingleton != null;
+ }
+
+ @Override
+ public void allowInterfaceEditing(Boolean isDevelopmentModeEnabled) {
+ if (interfaceEditorSingleton != null) {
+ interfaceEditorSingleton.allowInterfaceEditing(isDevelopmentModeEnabled);
+ }
+ }
+
+ @Override
+ public void applyInterfaceEdits(Activity activity) {
+ if (interfaceEditorSingleton != null && activity != null) {
+ interfaceEditorSingleton.applyInterfaceEdits(activity);
+ }
+ }
+
+ /**
+ * Sets the update flag to true.
+ */
+ @Override
+ public void startUpdating() {
+ if (interfaceEditorSingleton != null) {
+ interfaceEditorSingleton.startUpdating();
+ }
+ }
+
+ /**
+ * Sets the update flag to false.
+ */
+ @Override
+ public void stopUpdating() {
+ if (interfaceEditorSingleton != null) {
+ interfaceEditorSingleton.stopUpdating();
+ }
+ }
+
+ @Override
+ public void sendUpdate() {
+ if (interfaceEditorSingleton != null) {
+ interfaceEditorSingleton.sendUpdate();
+ }
+ }
+
+ @Override
+ public void sendUpdateDelayed(int delay) {
+ if (interfaceEditorSingleton != null) {
+ interfaceEditorSingleton.sendUpdateDelayed(delay);
+ }
+ }
+
+ @Override
+ public void sendUpdateDelayedDefault() {
+ if (interfaceEditorSingleton != null) {
+ interfaceEditorSingleton.sendUpdateDelayedDefault();
+ }
+ }
+
+ @Override
+ public LeanplumEditorMode getMode() {
+ if (interfaceEditorSingleton != null) {
+ return interfaceEditorSingleton.getMode();
+ }
+ return null;
+ }
+
+ @Override
+ public void setMode(LeanplumEditorMode mode) {
+ if (interfaceEditorSingleton != null) {
+ interfaceEditorSingleton.setMode(mode);
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/internal/Log.java
@@ -0,0 +1,203 @@
+/*
+ * 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.internal;
+
+//import com.leanplum.BuildConfig;
+
+import java.util.HashMap;
+
+/**
+ * Handles logging within the Leanplum SDK.
+ *
+ * @author Ben Marten
+ */
+public class Log {
+ public enum LeanplumLogType {
+ /**
+ * Always visible to customers. Sent to us when remote logging is enabled.
+ */
+ ERROR,
+
+ /**
+ * Always visible to customers. Sent to us when remote logging is enabled.
+ */
+ WARNING,
+
+ /**
+ * Always visible to customers. Sent to us when remote logging is enabled.
+ */
+ INFO,
+
+ /**
+ * Visible to customers only when verbose logging is enabled. Sent to us when remote logging is
+ * enabled.
+ */
+ VERBOSE,
+
+ /**
+ * Not visible to customers. Sent to us when remote logging is enabled.
+ */
+ PRIVATE,
+
+ /**
+ * Used only for Leanplum SDK debugging. Not visible to customers. Not sent to us when remote
+ * logging is enabled.
+ */
+ DEBUG
+ }
+
+ private static final ThreadLocal<Boolean> isLogging = new ThreadLocal<Boolean>() {
+ @Override
+ protected Boolean initialValue() {
+ return false;
+ }
+ };
+
+ public static void e(Object... objects) {
+ log(LeanplumLogType.ERROR, CollectionUtil.concatenateArray(objects, ", "));
+ }
+
+ public static void w(Object... objects) {
+ log(LeanplumLogType.WARNING, CollectionUtil.concatenateArray(objects, ", "));
+ }
+
+ public static void i(Object... objects) {
+ log(LeanplumLogType.INFO, CollectionUtil.concatenateArray(objects, ", "));
+ }
+
+ public static void v(Object... objects) {
+ log(LeanplumLogType.VERBOSE, CollectionUtil.concatenateArray(objects, ", "));
+ }
+
+ public static void p(Object... objects) {
+ log(LeanplumLogType.PRIVATE, CollectionUtil.concatenateArray(objects, ", "));
+ }
+
+ public static void d(Object... objects) {
+ log(LeanplumLogType.DEBUG, CollectionUtil.concatenateArray(objects, ", "));
+ }
+
+ /**
+ * Handle Leanplum log messages, which may be sent to the server for remote logging if
+ * Constants.loggingEnabled is set.
+ * <p/>
+ * This will format the string in all cases, and is therefore less efficient than checking the
+ * conditionals inline. Avoid this in performance-critical code.
+ *
+ * @param type The log type level of the message.
+ * @param message The message to be logged.
+ */
+ public static void log(LeanplumLogType type, String message) {
+ String tag = generateTag(type);
+ String prefix = generateMessagePrefix();
+
+ switch (type) {
+ case ERROR:
+ android.util.Log.e(tag, prefix + message);
+ maybeSendLog(tag + prefix + message);
+ return;
+ case WARNING:
+ android.util.Log.w(tag, prefix + message);
+ maybeSendLog(tag + prefix + message);
+ return;
+ case INFO:
+ android.util.Log.i(tag, prefix + message);
+ maybeSendLog(tag + prefix + message);
+ return;
+ case VERBOSE:
+ if (Constants.isDevelopmentModeEnabled
+ && Constants.enableVerboseLoggingInDevelopmentMode) {
+ android.util.Log.v(tag, prefix + message);
+ maybeSendLog(tag + prefix + message);
+ }
+ return;
+ case PRIVATE:
+ maybeSendLog(tag + prefix + message);
+ return;
+ default:
+ }
+ }
+
+ /**
+ * Generates tag for logging purpose in format [LogType][Leanplum]
+ *
+ * @param type log type
+ * @return generated tag
+ */
+ private static String generateTag(LeanplumLogType type) {
+ return "[" + type.name() + "][Leanplum]";
+ }
+
+ /**
+ * Generates a log message prefix based on current class and method name in format
+ * [ClassName::MethodName::LineNumber].
+ * This shouldn't be called directly, since getting className and methodName is hardcoded and
+ * extracted based on StackTrace.
+ *
+ * @return a message prefix for logging purpose
+ */
+ private static String generateMessagePrefix() {
+ // Since this is called from log method, caller method should be on index 5.
+ int callerIndex = 5;
+ int minimumStackTraceIndex = 5;
+
+ StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
+ if (stackTraceElements.length >= minimumStackTraceIndex) {
+ String tag = "[";
+ tag += stackTraceElements[callerIndex].getClassName();
+ tag += "::";
+ tag += stackTraceElements[callerIndex].getMethodName();
+ tag += "::";
+ tag += stackTraceElements[callerIndex].getLineNumber();
+ tag += "]: ";
+ return tag;
+ }
+ return "";
+ }
+
+ private static void maybeSendLog(String message) {
+ if (!Constants.loggingEnabled || isLogging.get()) {
+ return;
+ }
+
+ isLogging.set(true);
+ try {
+ HashMap<String, Object> params = new HashMap<>();
+ params.put(Constants.Params.TYPE, Constants.Values.SDK_LOG);
+ params.put(Constants.Params.MESSAGE, message);
+ Request.post(Constants.Methods.LOG, params).sendEventually();
+ } catch (Throwable t) {
+ android.util.Log.e("Leanplum", "Unable to send log.", t);
+ } finally {
+ isLogging.remove();
+ }
+ }
+
+ /**
+ * Handy function to get a loggable stack trace from a Throwable.
+ *
+ * @param throwable An exception to log.
+ */
+ public static String getStackTraceString(Throwable throwable) {
+ return android.util.Log.getStackTraceString(throwable);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/internal/OsHandler.java
@@ -0,0 +1,52 @@
+/*
+ * 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.internal;
+
+import android.os.Handler;
+import android.os.Looper;
+
+/**
+ * Wraps Handler while allowing overriding of methods that are needed for unit testing
+ *
+ * @author kkafadarov
+ */
+public class OsHandler {
+ // Posts to UI thread. Visible for testing.
+ public static OsHandler instance;
+
+ final Handler handler = new Handler(Looper.getMainLooper());
+
+ public Boolean post(Runnable runnable) {
+ return handler.post(runnable);
+ }
+
+ public Boolean postDelayed(Runnable runnable, long lng) {
+ return handler.postDelayed(runnable, lng);
+ }
+
+ public static OsHandler getInstance() {
+ if (instance == null) {
+ instance = new OsHandler();
+ }
+ return instance;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/internal/Registration.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.internal;
+
+import com.leanplum.callbacks.StartCallback;
+
+import org.json.JSONObject;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class Registration {
+ public static void registerDevice(String email, final StartCallback callback) {
+ Map<String, Object> params = new HashMap<>();
+ params.put(Constants.Params.EMAIL, email);
+ Request request = Request.post(Constants.Methods.REGISTER_FOR_DEVELOPMENT, params);
+ request.onResponse(new Request.ResponseCallback() {
+ @Override
+ public void response(final JSONObject response) {
+ OsHandler.getInstance().post(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ JSONObject registerResponse = Request.getLastResponse(response);
+ boolean isSuccess = Request.isResponseSuccess(registerResponse);
+ if (isSuccess) {
+ if (callback != null) {
+ callback.onResponse(true);
+ }
+ } else {
+ Log.e(Request.getResponseError(registerResponse));
+ if (callback != null) {
+ callback.onResponse(false);
+ }
+ }
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ }
+ });
+ }
+ });
+ request.onError(new Request.ErrorCallback() {
+ @Override
+ public void error(final Exception e) {
+ OsHandler.getInstance().post(new Runnable() {
+ @Override
+ public void run() {
+ if (callback != null) {
+ callback.onResponse(false);
+ }
+ }
+ });
+ }
+ });
+ request.sendIfConnected();
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/internal/Request.java
@@ -0,0 +1,886 @@
+/*
+ * 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.Context;
+import android.content.SharedPreferences;
+import android.os.AsyncTask;
+
+import com.leanplum.Leanplum;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.SocketTimeoutException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Stack;
+
+/**
+ * Leanplum request class.
+ *
+ * @author Andrew First
+ */
+public class Request {
+ private static final long DEVELOPMENT_MIN_DELAY_MS = 100;
+ private static final long DEVELOPMENT_MAX_DELAY_MS = 5000;
+ private static final long PRODUCTION_DELAY = 60000;
+ private static final String LEANPLUM = "__leanplum__";
+
+ private static String appId;
+ private static String accessKey;
+ private static String deviceId;
+ private static String userId;
+ private static final Map<String, Boolean> fileTransferStatus = new HashMap<>();
+ private static int pendingDownloads;
+ private static NoPendingDownloadsCallback noPendingDownloadsBlock;
+
+ // The token is saved primarily for legacy SharedPreferences decryption. This could
+ // likely be removed in the future.
+ private static String token = null;
+ private static final Object lock = Request.class;
+ private static final Map<File, Long> fileUploadSize = new HashMap<>();
+ private static final Map<File, Double> fileUploadProgress = new HashMap<>();
+ private static String fileUploadProgressString = "";
+ private static long lastSendTimeMs;
+ private static final Object uploadFileLock = new Object();
+
+ private final String httpMethod;
+ private final String apiMethod;
+ private final Map<String, Object> params;
+ private ResponseCallback response;
+ private ErrorCallback error;
+ private boolean sent;
+
+ private static ApiResponseCallback apiResponse;
+
+ public static void setAppId(String appId, String accessKey) {
+ Request.appId = appId;
+ Request.accessKey = accessKey;
+ }
+
+ public static void setDeviceId(String deviceId) {
+ Request.deviceId = deviceId;
+ }
+
+ public static void setUserId(String userId) {
+ Request.userId = userId;
+ }
+
+ public static void setToken(String token) {
+ Request.token = token;
+ }
+
+ public static String token() {
+ return token;
+ }
+
+ public static void loadToken() {
+ Context context = Leanplum.getContext();
+ SharedPreferences defaults = context.getSharedPreferences(
+ LEANPLUM, Context.MODE_PRIVATE);
+ String token = defaults.getString(Constants.Defaults.TOKEN_KEY, null);
+ if (token == null) {
+ return;
+ }
+ setToken(token);
+ }
+
+ public static void saveToken() {
+ Context context = Leanplum.getContext();
+ SharedPreferences defaults = context.getSharedPreferences(
+ LEANPLUM, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = defaults.edit();
+ editor.putString(Constants.Defaults.TOKEN_KEY, Request.token());
+ try {
+ editor.apply();
+ } catch (NoSuchMethodError e) {
+ editor.commit();
+ }
+ }
+
+ public static String appId() {
+ return appId;
+ }
+
+ public static String deviceId() {
+ return deviceId;
+ }
+
+ public static String userId() {
+ return Request.userId;
+ }
+
+ public Request(String httpMethod, String apiMethod, Map<String, Object> params) {
+ this.httpMethod = httpMethod;
+ this.apiMethod = apiMethod;
+ this.params = params != null ? params : new HashMap<String, Object>();
+
+ // Make sure the Handler is initialized on the main thread.
+ OsHandler.getInstance();
+ }
+
+ public static Request get(String apiMethod, Map<String, Object> params) {
+ Log.LeanplumLogType level = Constants.Methods.LOG.equals(apiMethod) ?
+ Log.LeanplumLogType.DEBUG : Log.LeanplumLogType.VERBOSE;
+ Log.log(level, "Will call API method " + apiMethod + " with arguments " + params);
+ return RequestFactory.getInstance().createRequest("GET", apiMethod, params);
+ }
+
+ public static Request post(String apiMethod, Map<String, Object> params) {
+ Log.LeanplumLogType level = Constants.Methods.LOG.equals(apiMethod) ?
+ Log.LeanplumLogType.DEBUG : Log.LeanplumLogType.VERBOSE;
+ Log.log(level, "Will call API method " + apiMethod + " with arguments " + params);
+ return RequestFactory.getInstance().createRequest("POST", apiMethod, params);
+ }
+
+ public void onResponse(ResponseCallback response) {
+ this.response = response;
+ }
+
+ public void onError(ErrorCallback error) {
+ this.error = error;
+ }
+
+ public void onApiResponse(ApiResponseCallback apiResponse) {
+ Request.apiResponse = apiResponse;
+ }
+
+ private Map<String, Object> createArgsDictionary() {
+ Map<String, Object> args = new HashMap<>();
+ args.put(Constants.Params.DEVICE_ID, deviceId);
+ args.put(Constants.Params.USER_ID, userId);
+ args.put(Constants.Params.ACTION, apiMethod);
+ args.put(Constants.Params.SDK_VERSION, Constants.LEANPLUM_VERSION);
+ args.put(Constants.Params.DEV_MODE, Boolean.toString(Constants.isDevelopmentModeEnabled));
+ args.put(Constants.Params.TIME, Double.toString(new Date().getTime() / 1000.0));
+ if (token != null) {
+ args.put(Constants.Params.TOKEN, token);
+ }
+ args.putAll(params);
+ return args;
+ }
+
+ private static void saveRequestForLater(Map<String, Object> args) {
+ synchronized (lock) {
+ Context context = Leanplum.getContext();
+ SharedPreferences preferences = context.getSharedPreferences(
+ LEANPLUM, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = preferences.edit();
+ int count = preferences.getInt(Constants.Defaults.COUNT_KEY, 0);
+ String itemKey = String.format(Locale.US, Constants.Defaults.ITEM_KEY, count);
+ editor.putString(itemKey, JsonConverter.toJson(args));
+ count++;
+ editor.putInt(Constants.Defaults.COUNT_KEY, count);
+ try {
+ editor.apply();
+ } catch (NoSuchMethodError e) {
+ editor.commit();
+ }
+ }
+ }
+
+ public void send() {
+ this.sendEventually();
+ if (Constants.isDevelopmentModeEnabled) {
+ long currentTimeMs = System.currentTimeMillis();
+ long delayMs;
+ if (lastSendTimeMs == 0 || currentTimeMs - lastSendTimeMs > DEVELOPMENT_MAX_DELAY_MS) {
+ delayMs = DEVELOPMENT_MIN_DELAY_MS;
+ } else {
+ delayMs = (lastSendTimeMs + DEVELOPMENT_MAX_DELAY_MS) - currentTimeMs;
+ }
+ OsHandler.getInstance().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ sendIfConnected();
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ }
+ }, delayMs);
+ }
+ }
+
+ /**
+ * Wait 1 second for potential other API calls, and then sends the call synchronously if no other
+ * call has been sent within 1 minute.
+ */
+ public void sendIfDelayed() {
+ sendEventually();
+ OsHandler.getInstance().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ sendIfDelayedHelper();
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ }
+ }, 1000);
+ }
+
+ /**
+ * Sends the call synchronously if no other call has been sent within 1 minute.
+ */
+ private void sendIfDelayedHelper() {
+ if (Constants.isDevelopmentModeEnabled) {
+ send();
+ } else {
+ long currentTimeMs = System.currentTimeMillis();
+ if (lastSendTimeMs == 0 || currentTimeMs - lastSendTimeMs > PRODUCTION_DELAY) {
+ sendIfConnected();
+ }
+ }
+ }
+
+ public void sendIfConnected() {
+ if (Util.isConnected()) {
+ this.sendNow();
+ } else {
+ this.sendEventually();
+ Log.i("Device is offline, will send later");
+ triggerErrorCallback(new Exception("Not connected to the Internet"));
+ }
+ }
+
+ private void triggerErrorCallback(Exception e) {
+ if (error != null) {
+ error.error(e);
+ }
+ if (apiResponse != null) {
+ List<Map<String, Object>> requests = getUnsentRequests();
+ apiResponse.response(requests, null);
+ }
+ }
+
+ @SuppressWarnings("BooleanMethodIsAlwaysInverted")
+ private boolean attachApiKeys(Map<String, Object> dict) {
+ if (appId == null || accessKey == null) {
+ Log.e("API keys are not set. Please use Leanplum.setAppIdForDevelopmentMode or "
+ + "Leanplum.setAppIdForProductionMode.");
+ return false;
+ }
+ dict.put(Constants.Params.APP_ID, appId);
+ dict.put(Constants.Params.CLIENT_KEY, accessKey);
+ dict.put(Constants.Params.CLIENT, Constants.CLIENT);
+ return true;
+ }
+
+ public interface ResponseCallback {
+ void response(JSONObject response);
+ }
+
+ public interface ApiResponseCallback {
+ void response(List<Map<String, Object>> requests, JSONObject response);
+ }
+
+ public interface ErrorCallback {
+ void error(Exception e);
+ }
+
+ public interface NoPendingDownloadsCallback {
+ void noPendingDownloads();
+ }
+
+ private void parseResponseJson(JSONObject responseJson, List<Map<String, Object>> requestsToSend,
+ Exception error) {
+ if (apiResponse != null) {
+ apiResponse.response(requestsToSend, responseJson);
+ }
+
+ if (responseJson != null) {
+ Exception lastResponseError = null;
+ int numResponses = Request.numResponses(responseJson);
+ for (int i = 0; i < numResponses; i++) {
+ JSONObject response = Request.getResponseAt(responseJson, i);
+ if (!Request.isResponseSuccess(response)) {
+ String errorMessage = Request.getResponseError(response);
+ if (errorMessage == null || errorMessage.length() == 0) {
+ errorMessage = "API error";
+ } else if (errorMessage.startsWith("App not found")) {
+ errorMessage = "No app matching the provided app ID was found.";
+ Constants.isInPermanentFailureState = true;
+ } else if (errorMessage.startsWith("Invalid access key")) {
+ errorMessage = "The access key you provided is not valid for this app.";
+ Constants.isInPermanentFailureState = true;
+ } else if (errorMessage.startsWith("Development mode requested but not permitted")) {
+ errorMessage = "A call to Leanplum.setAppIdForDevelopmentMode "
+ + "with your production key was made, which is not permitted.";
+ Constants.isInPermanentFailureState = true;
+ } else {
+ errorMessage = "API error: " + errorMessage;
+ }
+ Log.e(errorMessage);
+ if (i == numResponses - 1) {
+ lastResponseError = new Exception(errorMessage);
+ }
+ }
+ }
+
+ if (lastResponseError == null) {
+ lastResponseError = error;
+ }
+
+ if (lastResponseError != null && this.error != null) {
+ this.error.error(lastResponseError);
+ } else if (this.response != null) {
+ this.response.response(responseJson);
+ }
+ } else if (error != null && this.error != null) {
+ this.error.error(error);
+ }
+ }
+
+ private void sendNow() {
+ if (Constants.isTestMode) {
+ return;
+ }
+ if (appId == null) {
+ Log.e("Cannot send request. appId is not set.");
+ return;
+ }
+ if (accessKey == null) {
+ Log.e("Cannot send request. accessKey is not set.");
+ return;
+ }
+
+ this.sendEventually();
+
+ final List<Map<String, Object>> requestsToSend = popUnsentRequests();
+ if (requestsToSend.isEmpty()) {
+ return;
+ }
+
+ final Map<String, Object> multiRequestArgs = new HashMap<>();
+ multiRequestArgs.put(Constants.Params.DATA, jsonEncodeUnsentRequests(requestsToSend));
+ multiRequestArgs.put(Constants.Params.SDK_VERSION, Constants.LEANPLUM_VERSION);
+ multiRequestArgs.put(Constants.Params.ACTION, Constants.Methods.MULTI);
+ multiRequestArgs.put(Constants.Params.TIME, Double.toString(new Date().getTime() / 1000.0));
+ if (!this.attachApiKeys(multiRequestArgs)) {
+ return;
+ }
+
+ Util.executeAsyncTask(new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ JSONObject result = null;
+ HttpURLConnection op = null;
+ try {
+ try {
+ op = Util.operation(
+ Constants.API_HOST_NAME,
+ Constants.API_SERVLET,
+ multiRequestArgs,
+ httpMethod,
+ Constants.API_SSL,
+ Constants.NETWORK_TIMEOUT_SECONDS);
+
+ result = Util.getJsonResponse(op);
+ int statusCode = op.getResponseCode();
+
+ Exception errorException = null;
+ if (statusCode >= 400) {
+ errorException = new Exception("HTTP error " + statusCode);
+ if (statusCode == 408 || (statusCode >= 500 && statusCode <= 599)) {
+ pushUnsentRequests(requestsToSend);
+ }
+ } else {
+ if (result != null) {
+ int numResponses = Request.numResponses(result);
+ if (numResponses != requestsToSend.size()) {
+ Log.w("Sent " + requestsToSend.size() +
+ " requests but only" + " received " + numResponses);
+ }
+ } else {
+ errorException = new Exception("Response JSON is null.");
+ }
+ }
+ parseResponseJson(result, requestsToSend, errorException);
+ } catch (JSONException e) {
+ Log.e("Error parsing JSON response: " + e.toString() + "\n" +
+ Log.getStackTraceString(e));
+ parseResponseJson(null, requestsToSend, e);
+ } catch (Exception e) {
+ pushUnsentRequests(requestsToSend);
+ Log.e("Unable to send request: " + e.toString() + "\n" +
+ Log.getStackTraceString(e));
+ parseResponseJson(result, requestsToSend, e);
+ } finally {
+ if (op != null) {
+ op.disconnect();
+ }
+ }
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ return null;
+ }
+ });
+ }
+
+ public void sendEventually() {
+ if (Constants.isTestMode) {
+ return;
+ }
+ if (!sent) {
+ sent = true;
+ Map<String, Object> args = createArgsDictionary();
+ saveRequestForLater(args);
+ }
+ }
+
+ static List<Map<String, Object>> popUnsentRequests() {
+ return getUnsentRequests(true);
+ }
+
+ static List<Map<String, Object>> getUnsentRequests() {
+ return getUnsentRequests(false);
+ }
+
+ private static List<Map<String, Object>> getUnsentRequests(boolean remove) {
+ List<Map<String, Object>> requestData = new ArrayList<>();
+
+ synchronized (lock) {
+ lastSendTimeMs = System.currentTimeMillis();
+
+ Context context = Leanplum.getContext();
+ SharedPreferences preferences = context.getSharedPreferences(
+ LEANPLUM, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = preferences.edit();
+
+ int count = preferences.getInt(Constants.Defaults.COUNT_KEY, 0);
+ if (count == 0) {
+ return new ArrayList<>();
+ }
+ if (remove) {
+ editor.remove(Constants.Defaults.COUNT_KEY);
+ }
+
+ for (int i = 0; i < count; i++) {
+ String itemKey = String.format(Locale.US, Constants.Defaults.ITEM_KEY, i);
+ Map<String, Object> requestArgs;
+ try {
+ requestArgs = JsonConverter.mapFromJson(new JSONObject(
+ preferences.getString(itemKey, "{}")));
+ requestData.add(requestArgs);
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ if (remove) {
+ editor.remove(itemKey);
+ }
+ }
+ if (remove) {
+ try {
+ editor.apply();
+ } catch (NoSuchMethodError e) {
+ editor.commit();
+ }
+ }
+ }
+
+ requestData = removeIrrelevantBackgroundStartRequests(requestData);
+ return requestData;
+ }
+
+ /**
+ * In various scenarios we can end up batching a big number of requests (e.g. device is offline,
+ * background sessions), which could make the stored API calls batch look something like:
+ * <p>
+ * <code>start(B), start(B), start(F), track, start(B), track, start(F), resumeSession</code>
+ * <p>
+ * where <code>start(B)</code> indicates a start in the background, and <code>start(F)</code>
+ * one in the foreground.
+ * <p>
+ * In this case the first two <code>start(B)</code> can be dropped because they don't contribute
+ * any relevant information for the batch call.
+ * <p>
+ * Essentially we drop every <code>start(B)</code> call, that is directly followed by any kind of
+ * a <code>start</code> call.
+ *
+ * @param requestData A list of the requests, stored on the device.
+ * @return A list of only these requests, which contain relevant information for the API call.
+ */
+ private static List<Map<String, Object>> removeIrrelevantBackgroundStartRequests(
+ List<Map<String, Object>> requestData) {
+ List<Map<String, Object>> relevantRequests = new ArrayList<>();
+
+ int requestCount = requestData.size();
+ if (requestCount > 0) {
+ for (int i = 0; i < requestCount; i++) {
+ Map<String, Object> currentRequest = requestData.get(i);
+ if (i < requestCount - 1
+ && Constants.Methods.START.equals(requestData.get(i + 1).get(Constants.Params.ACTION))
+ && Constants.Methods.START.equals(currentRequest.get(Constants.Params.ACTION))
+ && Boolean.TRUE.toString().equals(currentRequest.get(Constants.Params.BACKGROUND))) {
+ continue;
+ }
+ relevantRequests.add(currentRequest);
+ }
+ }
+
+ return relevantRequests;
+ }
+
+ private static String jsonEncodeUnsentRequests(List<Map<String, Object>> requestData) {
+ Map<String, Object> data = new HashMap<>();
+ data.put(Constants.Params.DATA, requestData);
+ return JsonConverter.toJson(data);
+ }
+
+ private static void pushUnsentRequests(List<Map<String, Object>> requestData) {
+ if (requestData == null) {
+ return;
+ }
+ for (Map<String, Object> args : requestData) {
+ Object retryCountString = args.get("retryCount");
+ int retryCount;
+ if (retryCountString != null) {
+ retryCount = Integer.parseInt(retryCountString.toString()) + 1;
+ } else {
+ retryCount = 1;
+ }
+ args.put("retryCount", Integer.toString(retryCount));
+ saveRequestForLater(args);
+ }
+ }
+
+ private static String getSizeAsString(int bytes) {
+ if (bytes < (1 << 10)) {
+ return bytes + " B";
+ } else if (bytes < (1 << 20)) {
+ return (bytes >> 10) + " KB";
+ } else {
+ return (bytes >> 20) + " MB";
+ }
+ }
+
+ private static void printUploadProgress() {
+ int totalFiles = fileUploadSize.size();
+ int sentFiles = 0;
+ int totalBytes = 0;
+ int sentBytes = 0;
+ for (Map.Entry<File, Long> entry : fileUploadSize.entrySet()) {
+ File file = entry.getKey();
+ long fileSize = entry.getValue();
+ double fileProgress = fileUploadProgress.get(file);
+ if (fileProgress == 1) {
+ sentFiles++;
+ }
+ sentBytes += (int) (fileSize * fileProgress);
+ totalBytes += fileSize;
+ }
+ String progressString = "Uploading resources. " +
+ sentFiles + '/' + totalFiles + " files completed; " +
+ getSizeAsString(sentBytes) + '/' + getSizeAsString(totalBytes) + " transferred.";
+ if (!fileUploadProgressString.equals(progressString)) {
+ fileUploadProgressString = progressString;
+ Log.i(progressString);
+ }
+ }
+
+ public void sendFilesNow(final List<String> filenames, final List<InputStream> streams) {
+ if (Constants.isTestMode) {
+ return;
+ }
+ final Map<String, Object> dict = createArgsDictionary();
+ if (!attachApiKeys(dict)) {
+ return;
+ }
+ final List<File> filesToUpload = new ArrayList<>();
+
+ // First set up the files for upload
+ for (int i = 0; i < filenames.size(); i++) {
+ String filename = filenames.get(i);
+ if (filename == null || Boolean.TRUE.equals(fileTransferStatus.get(filename))) {
+ continue;
+ }
+ File file = new File(filename);
+ long size;
+ try {
+ size = streams.get(i).available();
+ } catch (IOException e) {
+ size = file.length();
+ } catch (NullPointerException e) {
+ // Not good. Can't read asset.
+ Log.e("Unable to read file " + filename);
+ continue;
+ }
+ fileTransferStatus.put(filename, true);
+ filesToUpload.add(file);
+ fileUploadSize.put(file, size);
+ fileUploadProgress.put(file, 0.0);
+ }
+ if (filesToUpload.size() == 0) {
+ return;
+ }
+
+ printUploadProgress();
+
+ // Now upload the files
+ Util.executeAsyncTask(new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ synchronized (uploadFileLock) { // Don't overload app and server with many upload tasks
+ JSONObject result;
+ HttpURLConnection op = null;
+
+ try {
+ op = Util.uploadFilesOperation(
+ Constants.Params.FILE,
+ filesToUpload,
+ streams,
+ Constants.API_HOST_NAME,
+ Constants.API_SERVLET,
+ dict,
+ httpMethod,
+ Constants.API_SSL,
+ 60);
+
+ if (op != null) {
+ result = Util.getJsonResponse(op);
+ int statusCode = op.getResponseCode();
+ if (statusCode != 200) {
+ throw new Exception("Leanplum: Error sending request: " + statusCode);
+ }
+ if (Request.this.response != null) {
+ Request.this.response.response(result);
+ }
+ } else {
+ if (error != null) {
+ error.error(new Exception("Leanplum: Unable to read file."));
+ }
+ }
+ } catch (JSONException e) {
+ Log.e("Unable to convert to JSON.", e);
+ if (error != null) {
+ error.error(e);
+ }
+ } catch (SocketTimeoutException e) {
+ Log.e("Timeout uploading files. Try again or limit the number of files " +
+ "to upload with parameters to syncResourcesAsync.");
+ if (error != null) {
+ error.error(e);
+ }
+ } catch (Exception e) {
+ Log.e("Unable to send file.", e);
+ if (error != null) {
+ error.error(e);
+ }
+ } finally {
+ if (op != null) {
+ op.disconnect();
+ }
+ }
+
+ for (File file : filesToUpload) {
+ fileUploadProgress.put(file, 1.0);
+ }
+ printUploadProgress();
+
+ return null;
+ }
+ }
+ });
+
+ // TODO: Upload progress
+ }
+
+ void downloadFile(final String path, final String url) {
+ if (Constants.isTestMode) {
+ return;
+ }
+ if (Boolean.TRUE.equals(fileTransferStatus.get(path))) {
+ return;
+ }
+ pendingDownloads++;
+ Log.i("Downloading resource " + path);
+ fileTransferStatus.put(path, true);
+ final Map<String, Object> dict = createArgsDictionary();
+ dict.put(Constants.Keys.FILENAME, path);
+ if (!attachApiKeys(dict)) {
+ return;
+ }
+
+ Util.executeAsyncTask(new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected Void doInBackground(Void... params) {
+ try {
+ downloadHelper(Constants.API_HOST_NAME, Constants.API_SERVLET, path, url, dict);
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ return null;
+ }
+ });
+ // TODO: Download progress
+ }
+
+ private void downloadHelper(String hostName, String servlet, final String path, final String url,
+ final Map<String, Object> dict) {
+ HttpURLConnection op = null;
+ URL originalURL = null;
+ try {
+ if (url == null) {
+ op = Util.operation(
+ hostName,
+ servlet,
+ dict,
+ httpMethod,
+ Constants.API_SSL,
+ Constants.NETWORK_TIMEOUT_SECONDS_FOR_DOWNLOADS);
+ } else {
+ op = Util.createHttpUrlConnection(url, httpMethod, url.startsWith("https://"),
+ Constants.NETWORK_TIMEOUT_SECONDS_FOR_DOWNLOADS);
+ }
+ originalURL = op.getURL();
+ op.connect();
+ int statusCode = op.getResponseCode();
+ if (statusCode != 200) {
+ throw new Exception("Leanplum: Error sending request to: " + hostName +
+ ", HTTP status code: " + statusCode);
+ }
+ Stack<String> dirs = new Stack<>();
+ String currentDir = path;
+ while ((currentDir = new File(currentDir).getParent()) != null) {
+ dirs.push(currentDir);
+ }
+ while (!dirs.isEmpty()) {
+ String directory = FileManager.fileRelativeToDocuments(dirs.pop());
+ boolean isCreated = new File(directory).mkdir();
+ if (!isCreated) {
+ Log.w("Failed to create directory: ", directory);
+ }
+ }
+
+ FileOutputStream out = new FileOutputStream(
+ new File(FileManager.fileRelativeToDocuments(path)));
+ Util.saveResponse(op, out);
+ pendingDownloads--;
+ if (Request.this.response != null) {
+ Request.this.response.response(null);
+ }
+ if (pendingDownloads == 0 && noPendingDownloadsBlock != null) {
+ noPendingDownloadsBlock.noPendingDownloads();
+ }
+ } catch (Exception e) {
+ if (e instanceof EOFException) {
+ if (op != null && !op.getURL().equals(originalURL)) {
+ downloadHelper(null, op.getURL().toString(), path, url, new HashMap<String, Object>());
+ return;
+ }
+ }
+ Log.e("Error downloading resource:" + path, e);
+ pendingDownloads--;
+ if (error != null) {
+ error.error(e);
+ }
+ if (pendingDownloads == 0 && noPendingDownloadsBlock != null) {
+ noPendingDownloadsBlock.noPendingDownloads();
+ }
+ } finally {
+ if (op != null) {
+ op.disconnect();
+ }
+ }
+ }
+
+ public static int numPendingDownloads() {
+ return pendingDownloads;
+ }
+
+ public static void onNoPendingDownloads(NoPendingDownloadsCallback block) {
+ noPendingDownloadsBlock = block;
+ }
+
+
+ public static int numResponses(JSONObject response) {
+ if (response == null) {
+ return 0;
+ }
+ try {
+ return response.getJSONArray("response").length();
+ } catch (JSONException e) {
+ Log.e("Could not parse JSON response.", e);
+ return 0;
+ }
+ }
+
+ public static JSONObject getResponseAt(JSONObject response, int index) {
+ try {
+ return response.getJSONArray("response").getJSONObject(index);
+ } catch (JSONException e) {
+ Log.e("Could not parse JSON response.", e);
+ return null;
+ }
+ }
+
+ public static JSONObject getLastResponse(JSONObject response) {
+ int numResponses = numResponses(response);
+ if (numResponses > 0) {
+ return getResponseAt(response, numResponses - 1);
+ } else {
+ return null;
+ }
+ }
+
+ public static boolean isResponseSuccess(JSONObject response) {
+ if (response == null) {
+ return false;
+ }
+ try {
+ return response.getBoolean("success");
+ } catch (JSONException e) {
+ Log.e("Could not parse JSON response.", e);
+ return false;
+ }
+ }
+
+ public static String getResponseError(JSONObject response) {
+ if (response == null) {
+ return null;
+ }
+ try {
+ JSONObject error = response.optJSONObject("error");
+ if (error == null) {
+ return null;
+ }
+ return error.getString("message");
+ } catch (JSONException e) {
+ Log.e("Could not parse JSON response.", e);
+ return null;
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/internal/RequestFactory.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.internal;
+
+import java.util.Map;
+
+public class RequestFactory {
+ public static RequestFactory defaultFactory;
+
+ public synchronized static RequestFactory getInstance() {
+ if (defaultFactory == null) {
+ defaultFactory = new RequestFactory();
+ }
+ return defaultFactory;
+ }
+
+ public Request createRequest(
+ String httpMethod, String apiMethod, Map<String, Object> params) {
+ return new Request(httpMethod, apiMethod, params);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/internal/ResourceQualifiers.java
@@ -0,0 +1,538 @@
+/*
+ * 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.res.Configuration;
+import android.os.Build;
+import android.util.DisplayMetrics;
+
+import java.lang.reflect.Field;
+import java.util.HashMap;
+import java.util.Map;
+
+public class ResourceQualifiers {
+ public static abstract class QualifierFilter {
+ abstract Object getMatch(String str);
+
+ public abstract boolean isMatch(Object value, Configuration config, DisplayMetrics display);
+
+ public Map<String, Object> bestMatch(Map<String, Object> values, Configuration config, DisplayMetrics display) {
+ return values;
+ }
+ }
+
+ public enum Qualifier {
+ MCC(new QualifierFilter() {
+ @Override
+ public Object getMatch(String str) {
+ if (str.startsWith("mcc")) {
+ return Integer.getInteger(str.substring(3));
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isMatch(Object value, Configuration config, DisplayMetrics display) {
+ return config.mcc == ((Integer) value);
+ }
+ }),
+ MNC(new QualifierFilter() {
+ @Override
+ public Object getMatch(String str) {
+ if (str.startsWith("mnc")) {
+ return Integer.getInteger(str.substring(3));
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isMatch(Object value, Configuration config, DisplayMetrics display) {
+ return config.mnc == ((Integer) value);
+ }
+ }),
+ LANGUAGE(new QualifierFilter() {
+ @Override
+ public Object getMatch(String str) {
+ if (str.length() == 2) {
+ return str;
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isMatch(Object value, Configuration config, DisplayMetrics display) {
+ return config.locale.getLanguage().equals(value);
+ }
+ }),
+ REGION(new QualifierFilter() {
+ @Override
+ public Object getMatch(String str) {
+ if (str.startsWith("r") && str.length() == 3) {
+ return str.substring(1);
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isMatch(Object value, Configuration config, DisplayMetrics display) {
+ return config.locale.getCountry().toLowerCase().equals(value);
+ }
+ }),
+ LAYOUT_DIRECTION(new QualifierFilter() {
+ // From http://developer.android.com/reference/android/content/res/Configuration.html#SMALLEST_SCREEN_WIDTH_DP_UNDEFINED
+ public static final int SCREENLAYOUT_LAYOUTDIR_LTR = 0x00000040;
+ public static final int SCREENLAYOUT_LAYOUTDIR_RTL = 0x00000080;
+ public static final int SCREENLAYOUT_LAYOUTDIR_MASK = 0x000000c0;
+
+ @Override
+ public Object getMatch(String str) {
+ if ("ldrtl".equals(str)) {
+ return SCREENLAYOUT_LAYOUTDIR_RTL;
+ } else if ("ldltr".equals(str)) {
+ return SCREENLAYOUT_LAYOUTDIR_LTR;
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isMatch(Object value, Configuration config, DisplayMetrics display) {
+ return (config.screenLayout & SCREENLAYOUT_LAYOUTDIR_MASK) == (Integer) value;
+ }
+ }),
+ SMALLEST_WIDTH(new QualifierFilter() {
+ @Override
+ public Object getMatch(String str) {
+ if (str.startsWith("sw") && str.endsWith("dp")) {
+ return Integer.getInteger(str.substring(2, str.length() - 2));
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isMatch(Object value, Configuration config, DisplayMetrics display) {
+ try {
+ Field field = config.getClass().getField("smallestScreenWidthDp");
+ int smallestWidthDp = (int) (Integer) field.get(config);
+ return smallestWidthDp >= (Integer) value;
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ return false;
+ }
+
+ @Override
+ public Map<String, Object> bestMatch(Map<String, Object> values, Configuration config, DisplayMetrics display) {
+ Map<String, Object> result = new HashMap<>();
+ int max = Integer.MIN_VALUE;
+ for (Map.Entry<String, Object> entry : values.entrySet()) {
+ Integer intObj = (Integer) entry.getValue();
+ if (intObj > max) {
+ max = intObj;
+ result.clear();
+ }
+ if (intObj == max) {
+ result.put(entry.getKey(), intObj);
+ }
+ }
+ return result;
+ }
+ }),
+ AVAILABLE_WIDTH(new QualifierFilter() {
+ @Override
+ public Object getMatch(String str) {
+ if (str.startsWith("w") && str.endsWith("dp")) {
+ return Integer.getInteger(str.substring(1, str.length() - 2));
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isMatch(Object value, Configuration config, DisplayMetrics display) {
+ try {
+ Field field = config.getClass().getField("screenWidthDp");
+ int screenWidthDp = (int) (Integer) field.get(config);
+ return screenWidthDp >= (Integer) value;
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ return false;
+ }
+
+ @Override
+ public Map<String, Object> bestMatch(Map<String, Object> values, Configuration config, DisplayMetrics display) {
+ Map<String, Object> result = new HashMap<>();
+ int max = Integer.MIN_VALUE;
+ for (Map.Entry<String, Object> entry : values.entrySet()) {
+ Integer intObj = (Integer) entry.getValue();
+ if (intObj > max) {
+ max = intObj;
+ result.clear();
+ }
+ if (intObj == max) {
+ result.put(entry.getKey(), intObj);
+ }
+ }
+ return result;
+ }
+ }),
+ AVAILABLE_HEIGHT(new QualifierFilter() {
+ @Override
+ public Object getMatch(String str) {
+ if (str.startsWith("h") && str.endsWith("dp")) {
+ return Integer.getInteger(str.substring(1, str.length() - 2));
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isMatch(Object value, Configuration config, DisplayMetrics display) {
+ try {
+ Field field = config.getClass().getField("screenHeightDp");
+ int screenHeightDp = (int) (Integer) field.get(config);
+ return screenHeightDp >= (Integer) value;
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ return false;
+ }
+
+ @Override
+ public Map<String, Object> bestMatch(Map<String, Object> values, Configuration config, DisplayMetrics display) {
+ Map<String, Object> result = new HashMap<>();
+ int max = Integer.MIN_VALUE;
+ for (Map.Entry<String, Object> entry : values.entrySet()) {
+ Integer intObj = (Integer) entry.getValue();
+ if (intObj > max) {
+ max = intObj;
+ result.clear();
+ }
+ if (intObj == max) {
+ result.put(entry.getKey(), intObj);
+ }
+ }
+ return result;
+ }
+ }),
+ SCREEN_SIZE(new QualifierFilter() {
+ @Override
+ public Object getMatch(String str) {
+ if ("small".equals(str)) {
+ return Configuration.SCREENLAYOUT_SIZE_SMALL;
+ } else if ("normal".equals(str)) {
+ return Configuration.SCREENLAYOUT_SIZE_NORMAL;
+ } else if ("large".equals(str)) {
+ return Configuration.SCREENLAYOUT_SIZE_LARGE;
+ } else if ("xlarge".equals(str)) {
+ return Configuration.SCREENLAYOUT_SIZE_XLARGE;
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isMatch(Object value, Configuration config, DisplayMetrics display) {
+ return (config.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK) <= (Integer) value;
+ }
+
+ @Override
+ public Map<String, Object> bestMatch(Map<String, Object> values, Configuration config, DisplayMetrics display) {
+ Map<String, Object> result = new HashMap<>();
+ int max = Integer.MIN_VALUE;
+ for (Map.Entry<String, Object> entry : values.entrySet()) {
+ Integer intObj = (Integer) entry.getValue();
+ if (intObj > max) {
+ max = intObj;
+ result.clear();
+ }
+ if (intObj == max) {
+ result.put(entry.getKey(), intObj);
+ }
+ }
+ return result;
+ }
+ }),
+ SCREEN_ASPECT(new QualifierFilter() {
+ @Override
+ public Object getMatch(String str) {
+ if ("long".equals(str)) {
+ return Configuration.SCREENLAYOUT_LONG_YES;
+ } else if ("notlong".equals(str)) {
+ return Configuration.SCREENLAYOUT_LONG_NO;
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isMatch(Object value, Configuration config, DisplayMetrics display) {
+ return (config.screenLayout & Configuration.SCREENLAYOUT_LONG_MASK) == (Integer) value;
+ }
+ }),
+ SCREEN_ORIENTATION(new QualifierFilter() {
+ @Override
+ public Object getMatch(String str) {
+ if ("port".equals(str)) {
+ return Configuration.ORIENTATION_PORTRAIT;
+ } else if ("land".equals(str)) {
+ return Configuration.ORIENTATION_LANDSCAPE;
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isMatch(Object value, Configuration config, DisplayMetrics display) {
+ return config.orientation == (Integer) value;
+ }
+ }),
+ UI_MODE(new QualifierFilter() {
+ public static final int UI_MODE_TYPE_TELEVISION = 0x00000004;
+ public static final int UI_MODE_TYPE_APPLIANCE = 0x00000005;
+
+ @Override
+ public Object getMatch(String str) {
+ if ("car".equals(str)) {
+ return Configuration.UI_MODE_TYPE_CAR;
+ } else if ("desk".equals(str)) {
+ return Configuration.UI_MODE_TYPE_DESK;
+ } else if ("television".equals(str)) {
+ return UI_MODE_TYPE_TELEVISION;
+ } else if ("appliance".equals(str)) {
+ return UI_MODE_TYPE_APPLIANCE;
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isMatch(Object value, Configuration config, DisplayMetrics display) {
+ return (config.uiMode & Configuration.UI_MODE_TYPE_MASK) == (Integer) value;
+ }
+ }),
+ NIGHT_MODE(new QualifierFilter() {
+ @Override
+ public Object getMatch(String str) {
+ if ("night".equals(str)) {
+ return Configuration.UI_MODE_NIGHT_YES;
+ } else if ("notnight".equals(str)) {
+ return Configuration.UI_MODE_NIGHT_NO;
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isMatch(Object value, Configuration config, DisplayMetrics display) {
+ return (config.uiMode & Configuration.UI_MODE_NIGHT_MASK) == (Integer) value;
+ }
+ }),
+ SCREEN_PIXEL_DENSITY(new QualifierFilter() {
+ public static final int DENSITY_TV = 0x000000d5;
+ public static final int DENSITY_XXHIGH = 0x000001e0;
+ public static final int DENSITY_NONE = 0;
+
+ @Override
+ public Object getMatch(String str) {
+ if ("ldpi".equals(str)) {
+ return DisplayMetrics.DENSITY_LOW;
+ } else if ("mdpi".equals(str)) {
+ return DisplayMetrics.DENSITY_MEDIUM;
+ } else if ("hdpi".equals(str)) {
+ return DisplayMetrics.DENSITY_HIGH;
+ } else if ("xhdpi".equals(str)) {
+ return DisplayMetrics.DENSITY_XHIGH;
+ } else if ("nodpi".equals(str)) {
+ return DENSITY_NONE;
+ } else if ("tvdpi".equals(str)) {
+ return DENSITY_TV;
+ } else if ("xxhigh".equals(str)) {
+ return DENSITY_XXHIGH;
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isMatch(Object value, Configuration config, DisplayMetrics display) {
+ return true;
+ }
+
+ @Override
+ public Map<String, Object> bestMatch(Map<String, Object> values, Configuration config, DisplayMetrics display) {
+ Map<String, Object> result = new HashMap<>();
+ int min = Integer.MAX_VALUE;
+
+ for (Map.Entry<String, Object> entry : values.entrySet()) {
+ Integer intObj = (Integer) entry.getValue();
+ if (intObj < min && intObj >= display.densityDpi) {
+ min = intObj;
+ result.clear();
+ }
+ if (intObj == min) {
+ result.put(entry.getKey(), intObj);
+ }
+ }
+ if (result.size() == 0) {
+ int max = Integer.MIN_VALUE;
+ for (String key : values.keySet()) {
+ Integer intObj = (Integer) values.get(key);
+ if (intObj > max) {
+ max = intObj;
+ result.clear();
+ }
+ if (intObj == max) {
+ result.put(key, intObj);
+ }
+ }
+ }
+ return result;
+ }
+ }),
+ TOUCHSCREEN_TYPE(new QualifierFilter() {
+ @Override
+ public Object getMatch(String str) {
+ if ("notouch".equals(str)) {
+ return Configuration.TOUCHSCREEN_NOTOUCH;
+ } else if ("finger".equals(str)) {
+ return Configuration.TOUCHSCREEN_FINGER;
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isMatch(Object value, Configuration config, DisplayMetrics display) {
+ return config.touchscreen == (Integer) value;
+ }
+ }),
+ KEYBOARD_AVAILABILITY(new QualifierFilter() {
+ @Override
+ public Object getMatch(String str) {
+ if ("keysexposed".equals(str)) {
+ return Configuration.KEYBOARDHIDDEN_NO;
+ } else if ("keyshidden".equals(str)) {
+ return Configuration.KEYBOARDHIDDEN_YES;
+ } else if ("keyssoft".equals(str)) {
+ return 0;
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isMatch(Object value, Configuration config, DisplayMetrics display) {
+ return (Integer) value == 0 || config.keyboardHidden == (Integer) value;
+ }
+ }),
+ PRIMARY_TEXT_INPUTMETHOD(new QualifierFilter() {
+ @Override
+ public Object getMatch(String str) {
+ if ("nokeys".equals(str)) {
+ return Configuration.KEYBOARD_NOKEYS;
+ } else if ("qwerty".equals(str)) {
+ return Configuration.KEYBOARD_QWERTY;
+ } else if ("12key".equals(str)) {
+ return Configuration.KEYBOARD_12KEY;
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isMatch(Object value, Configuration config, DisplayMetrics display) {
+ return config.keyboard == (Integer) value;
+ }
+ }),
+ NAVIGATION_KEY_AVAILABILITY(new QualifierFilter() {
+ @Override
+ public Object getMatch(String str) {
+ if ("navexposed".equals(str)) {
+ return Configuration.NAVIGATIONHIDDEN_NO;
+ } else if ("navhidden".equals(str)) {
+ return Configuration.NAVIGATIONHIDDEN_YES;
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isMatch(Object value, Configuration config, DisplayMetrics display) {
+ return config.navigationHidden == (Integer) value;
+ }
+ }),
+ PRIMARY_NON_TOUCH_NAVIGATION_METHOD(new QualifierFilter() {
+ @Override
+ public Object getMatch(String str) {
+ if ("nonav".equals(str)) {
+ return Configuration.NAVIGATION_NONAV;
+ } else if ("dpad".equals(str)) {
+ return Configuration.NAVIGATION_DPAD;
+ } else if ("trackball".equals(str)) {
+ return Configuration.NAVIGATION_TRACKBALL;
+ } else if ("wheel".equals(str)) {
+ return Configuration.NAVIGATION_WHEEL;
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isMatch(Object value, Configuration config, DisplayMetrics display) {
+ return config.navigation == (Integer) value;
+ }
+ }),
+ PLATFORM_VERSION(new QualifierFilter() {
+ @Override
+ public Object getMatch(String str) {
+ if (str.startsWith("v")) {
+ return Integer.getInteger(str.substring(1));
+ }
+ return null;
+ }
+
+ @Override
+ public boolean isMatch(Object value, Configuration config, DisplayMetrics display) {
+ return Build.VERSION.SDK_INT >= (Integer) value;
+ }
+ });
+
+ private QualifierFilter filter;
+
+ Qualifier(QualifierFilter filter) {
+ this.filter = filter;
+ }
+
+ public QualifierFilter getFilter() {
+ return filter;
+ }
+ }
+
+ public Map<Qualifier, Object> qualifiers = new HashMap<>();
+
+ public static ResourceQualifiers fromFolder(String folderName) {
+ ResourceQualifiers result = new ResourceQualifiers();
+ String[] nameParts = folderName.toLowerCase().split("-");
+ int qualifierIndex = 0;
+ for (String part : nameParts) {
+ boolean isMatch = false;
+ while (!isMatch && qualifierIndex < Qualifier.values().length) {
+ Qualifier qualifier = Qualifier.values()[qualifierIndex];
+ Object match = qualifier.getFilter().getMatch(part);
+ if (match != null) {
+ result.qualifiers.put(qualifier, match);
+ isMatch = true;
+ }
+ qualifierIndex++;
+ }
+ }
+ return result;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/internal/Socket.java
@@ -0,0 +1,332 @@
+/*
+ * 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.internal;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+
+import com.leanplum.ActionContext;
+import com.leanplum.Leanplum;
+import com.leanplum.LeanplumActivityHelper;
+import com.leanplum.LeanplumEditorMode;
+import com.leanplum.callbacks.VariablesChangedCallback;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Timer;
+import java.util.TimerTask;
+
+/**
+ * Leanplum socket class, that handles connections to the Leanplum remote socket.
+ *
+ * @author Andrew First, Ben Marten
+ */
+public class Socket {
+ private static final String TAG = "Leanplum";
+ private static final String EVENT_CONTENT_RESPONSE = "getContentResponse";
+ private static final String EVENT_UPDATE_VARS = "updateVars";
+ private static final String EVENT_GET_VIEW_HIERARCHY = "getViewHierarchy";
+ private static final String EVENT_PREVIEW_UPDATE_RULES = "previewUpdateRules";
+ private static final String EVENT_TRIGGER = "trigger";
+ private static final String EVENT_GET_VARIABLES = "getVariables";
+ private static final String EVENT_GET_ACTIONS = "getActions";
+ private static final String EVENT_REGISTER_DEVICE = "registerDevice";
+
+ private static Socket instance = new Socket();
+ private SocketIOClient sio;
+ private boolean authSent;
+ private boolean connected = false;
+ private boolean connecting = false;
+
+ public Socket() {
+ createSocketClient();
+ }
+
+ public static Socket getInstance() {
+ return instance;
+ }
+
+ private void createSocketClient() {
+ SocketIOClient.Handler socketIOClientHandler = new SocketIOClient.Handler() {
+ @Override
+ public void onError(Exception error) {
+ Log.e("Development socket error", error);
+ }
+
+ @Override
+ public void onDisconnect(int code, String reason) {
+ Log.i("Disconnected from development server");
+ connected = false;
+ connecting = false;
+ authSent = false;
+ }
+
+ @Override
+ public void onConnect() {
+ if (!authSent) {
+ Log.i("Connected to development server");
+ try {
+ Map<String, String> args = Util.newMap(
+ Constants.Params.APP_ID, Request.appId(),
+ Constants.Params.DEVICE_ID, Request.deviceId());
+ try {
+ sio.emit("auth", new JSONArray(Collections.singletonList(new JSONObject(args))));
+ } catch (JSONException e) {
+ e.printStackTrace();
+ }
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ authSent = true;
+ connected = true;
+ connecting = false;
+ }
+ }
+
+ @Override
+ public void on(String event, JSONArray arguments) {
+ try {
+ switch (event) {
+ case EVENT_UPDATE_VARS:
+ Leanplum.forceContentUpdate();
+ break;
+ case EVENT_TRIGGER:
+ handleTriggerEvent(arguments);
+ break;
+ case EVENT_GET_VIEW_HIERARCHY:
+ LeanplumUIEditorWrapper.getInstance().startUpdating();
+ LeanplumUIEditorWrapper.getInstance().sendUpdate();
+ break;
+ case EVENT_PREVIEW_UPDATE_RULES:
+ previewUpdateRules(arguments);
+ break;
+ case EVENT_GET_VARIABLES:
+ handleGetVariablesEvent();
+ break;
+ case EVENT_GET_ACTIONS:
+ handleGetActionsEvent();
+ break;
+ case EVENT_REGISTER_DEVICE:
+ handleRegisterDeviceEvent(arguments);
+ break;
+ default:
+ break;
+ }
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ }
+ };
+
+ try {
+ sio = new SocketIOClient(new URI("http://" + Constants.SOCKET_HOST + ":" +
+ Constants.SOCKET_PORT), socketIOClientHandler);
+ } catch (URISyntaxException e) {
+ Log.e(e.getMessage());
+ }
+ connect();
+ Timer reconnectTimer = new Timer();
+ reconnectTimer.schedule(new TimerTask() {
+ @Override
+ public void run() {
+ try {
+ reconnect();
+ } catch (Throwable t) {
+ Util.handleException(t);
+ }
+ }
+ }, 0, 5000);
+ }
+
+ /**
+ * Connect to the remote socket.
+ */
+ private void connect() {
+ connecting = true;
+ sio.connect();
+ }
+
+ /**
+ * Disconnect from the remote socket.
+ */
+ private void reconnect() {
+ if (!connected && !connecting) {
+ connect();
+ }
+ }
+
+ /**
+ * Send a given event and data to the remote socket server.
+ *
+ * @param eventName The name of the event.
+ * @param data The data to be sent to the remote server.
+ */
+ public <T> void sendEvent(String eventName, Map<String, T> data) {
+ try {
+ Log.p("Sending event: " + eventName + " & data over socket:\n" + data);
+ sio.emit(eventName,
+ new JSONArray(Collections.singletonList(JsonConverter.mapToJsonObject(data))));
+ } catch (JSONException e) {
+ Log.e("Failed to create JSON data object: " + e.getMessage());
+ }
+ }
+
+ /**
+ * Handles the "trigger" event received from server.
+ *
+ * @param arguments The arguments received from server.
+ */
+ void handleTriggerEvent(JSONArray arguments) {
+ // Trigger a custom action.
+ try {
+ JSONObject payload = arguments.getJSONObject(0);
+ JSONObject actionJson = payload.getJSONObject(Constants.Params.ACTION);
+ if (actionJson != null) {
+ String messageId = payload.getString(Constants.Params.MESSAGE_ID);
+ boolean isRooted = payload.getBoolean("isRooted");
+ String actionType = actionJson.getString(Constants.Values.ACTION_ARG);
+ Map<String, Object> defaultDefinition = CollectionUtil.uncheckedCast(
+ VarCache.actionDefinitions().get(actionType));
+ Map<String, Object> defaultArgs = null;
+ if (defaultDefinition != null) {
+ defaultArgs = CollectionUtil.uncheckedCast(defaultDefinition.get("values"));
+ }
+ Map<String, Object> action = JsonConverter.mapFromJson(actionJson);
+ action = CollectionUtil.uncheckedCast(VarCache.mergeHelper(defaultArgs, action));
+ ActionContext context = new ActionContext(actionType, action, messageId);
+ context.preventRealtimeUpdating();
+ ((BaseActionContext) context).setIsRooted(isRooted);
+ ((BaseActionContext) context).setIsPreview(true);
+ context.update();
+ LeanplumInternal.triggerAction(context);
+ ActionManager.getInstance().recordMessageImpression(messageId);
+ }
+ } catch (JSONException e) {
+ Log.e("Error getting action info", e);
+ }
+ }
+
+ /**
+ * Handles the "getVariables" event received from server.
+ */
+ public void handleGetVariablesEvent() {
+ boolean sentValues = VarCache.sendVariablesIfChanged();
+ VarCache.maybeUploadNewFiles();
+ sendEvent(EVENT_CONTENT_RESPONSE, Util.newMap("updated", sentValues));
+ }
+
+ /**
+ * Handles the "getActions" event received from server.
+ */
+ void handleGetActionsEvent() {
+ boolean sentValues = VarCache.sendActionsIfChanged();
+ VarCache.maybeUploadNewFiles();
+ sendEvent(EVENT_CONTENT_RESPONSE, Util.newMap("updated", sentValues));
+ }
+
+ /**
+ * Handles the "registerDevice" event received from server.
+ *
+ * @param arguments The arguments received from server.
+ */
+ void handleRegisterDeviceEvent(JSONArray arguments) {
+ LeanplumInternal.onHasStartedAndRegisteredAsDeveloper();
+ String emailArg = null;
+ try {
+ emailArg = arguments.getJSONObject(0).getString("email");
+ } catch (JSONException e) {
+ Log.v("Socket - No developer e-mail provided.");
+ }
+ final String email = (emailArg == null) ? "a Leanplum account" : emailArg;
+ OsHandler.getInstance().post(new Runnable() {
+ @Override
+ public void run() {
+ LeanplumActivityHelper.queueActionUponActive(new VariablesChangedCallback() {
+ @Override
+ public void variablesChanged() {
+ Activity activity = LeanplumActivityHelper.getCurrentActivity();
+ AlertDialog.Builder alert = new AlertDialog.Builder(activity);
+ alert.setTitle(TAG);
+ alert.setMessage("Your device is registered to " + email + ".");
+ alert.setPositiveButton("OK", new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ }
+ });
+ alert.show();
+ }
+ });
+ }
+ });
+ }
+
+ void previewUpdateRules(JSONArray arguments) {
+ JSONObject packetData;
+ try {
+ packetData = arguments.getJSONObject(0);
+ } catch (Exception e) {
+ Log.e("Error parsing data");
+ return;
+ }
+
+ if (!packetData.optBoolean("closed")) {
+ LeanplumUIEditorWrapper.getInstance().startUpdating();
+ } else {
+ LeanplumUIEditorWrapper.getInstance().stopUpdating();
+ }
+
+ LeanplumEditorMode mode;
+ int intMode = packetData.optInt("mode");
+ if (intMode >= LeanplumEditorMode.values().length) {
+ Log.p("Invalid editor mode in packet");
+ mode = LeanplumEditorMode.LP_EDITOR_MODE_INTERFACE;
+ } else {
+ mode = LeanplumEditorMode.values()[intMode];
+ }
+ LeanplumUIEditorWrapper.getInstance().setMode(mode);
+
+ JSONArray rules = packetData.optJSONArray("rules");
+ if (rules != null) {
+ List<Map<String, Object>> ruleDiffs = JsonConverter.listFromJson(rules);
+ VarCache.applyUpdateRuleDiffs(ruleDiffs);
+ }
+
+ LeanplumUIEditorWrapper.getInstance().sendUpdateDelayedDefault();
+ }
+
+ /**
+ * Returns whether the socket connection is established
+ *
+ * @return true if connected
+ */
+ public boolean isConnected() {
+ return connected;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/internal/SocketIOClient.java
@@ -0,0 +1,280 @@
+// Copyright (c) 2009-2012 James Coglan
+// Copyright (c) 2012 Eric Butler
+// Copyright (c) 2012 Koushik Dutta
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the 'Software'), to deal in
+// the Software without restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+// Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+// From https://github.com/koush/android-websockets
+
+package com.leanplum.internal;
+
+import android.os.Looper;
+
+import com.leanplum.Leanplum;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.HashSet;
+
+class SocketIOClient {
+ interface Handler {
+ void onConnect();
+
+ void on(String event, JSONArray arguments);
+
+ void onDisconnect(int code, String reason);
+
+ void onError(Exception error);
+ }
+
+ private String mURL;
+ private Handler mHandler;
+ private String mSession;
+ private int mHeartbeat;
+ private WebSocketClient mClient;
+ private android.os.Handler mSendHandler;
+ private Looper mSendLooper;
+
+ public SocketIOClient(URI uri, Handler handler) {
+ // remove trailing "/" from URI, in case user provided e.g. http://test.com/
+ mURL = uri.toString().replaceAll("/$", "") + "/socket.io/1/";
+ mHandler = handler;
+ }
+
+ private static String userAgentString() {
+ String appName = (Leanplum.getContext() != null) ?
+ Util.getApplicationName(Leanplum.getContext()) + "/" + Util.getVersionName() : "websocket";
+ return appName + "(" + Request.appId() + "; " + Constants.CLIENT + "; "
+ + Constants.LEANPLUM_VERSION + "/" + Constants.LEANPLUM_PACKAGE_IDENTIFIER + ")";
+ }
+
+ private String downloadUriAsString()
+ throws IOException {
+ URL url = new URL(this.mURL);
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+
+ try {
+ InputStream inputStream = connection.getInputStream();
+ BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
+
+ String tempStr;
+ StringBuffer stringBuffer = new StringBuffer();
+
+ while ((tempStr = bufferedReader.readLine()) != null) {
+ stringBuffer.append(tempStr);
+ }
+
+ bufferedReader.close();
+ inputStream.close();
+ return stringBuffer.toString();
+
+ } finally {
+ if (connection != null) {
+ connection.disconnect();
+ }
+ }
+ }
+
+ private static byte[] readToEndAsArray(InputStream input) throws IOException {
+ DataInputStream dis = new DataInputStream(input);
+ byte[] stuff = new byte[1024];
+ ByteArrayOutputStream buff = new ByteArrayOutputStream();
+ int read;
+ while ((read = dis.read(stuff)) != -1) {
+ buff.write(stuff, 0, read);
+ }
+
+ return buff.toByteArray();
+ }
+
+ private static String readToEnd(InputStream input) throws IOException {
+ return new String(readToEndAsArray(input));
+ }
+
+ public void emit(String name, JSONArray args) throws JSONException {
+ final JSONObject event = new JSONObject();
+ event.put("name", name);
+ event.put("args", args);
+ // Log.d("Emitting event: " + event.toString());
+ mSendHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mClient.send(String.format("5:::%s", event.toString()));
+ }
+ });
+ }
+
+ private void connectSession() throws URISyntaxException {
+ mClient = new WebSocketClient(new URI(mURL + "websocket/" + mSession),
+ new WebSocketClient.Listener() {
+ @Override
+ public void onMessage(byte[] data) {
+ cleanup();
+ mHandler.onError(new Exception("Unexpected binary data"));
+ }
+
+ @Override
+ public void onMessage(String message) {
+ try {
+ // Log.d("Message: " + message);
+ String[] parts = message.split(":", 4);
+ int code = Integer.parseInt(parts[0]);
+ switch (code) {
+ case 1:
+ onConnect();
+ break;
+ case 2:
+ // heartbeat
+ mClient.send("2::");
+ break;
+ case 3:
+ // message
+ case 4:
+ // json message
+ throw new Exception("message type not supported");
+ case 5: {
+ final String messageId = parts[1];
+ final String dataString = parts[3];
+ JSONObject data = new JSONObject(dataString);
+ String event = data.getString("name");
+ JSONArray args;
+ try {
+ args = data.getJSONArray("args");
+ } catch (JSONException e) {
+ args = new JSONArray();
+ }
+ if (!"".equals(messageId)) {
+ mSendHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mClient.send(String.format("6:::%s", messageId));
+ }
+ });
+ }
+ mHandler.on(event, args);
+ break;
+ }
+ case 6:
+ // ACK
+ break;
+ case 7:
+ // error
+ throw new Exception(message);
+ case 8:
+ // noop
+ break;
+ default:
+ throw new Exception("unknown code");
+ }
+ } catch (Exception ex) {
+ cleanup();
+ onError(ex);
+ }
+ }
+
+ @Override
+ public void onError(Exception error) {
+ cleanup();
+ mHandler.onError(error);
+ }
+
+ @Override
+ public void onDisconnect(int code, String reason) {
+ cleanup();
+ // attempt reconnect with same session?
+ mHandler.onDisconnect(code, reason);
+ }
+
+ @Override
+ public void onConnect() {
+ mSendHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ mSendHandler.postDelayed(this, mHeartbeat);
+ mClient.send("2:::");
+ }
+ }, mHeartbeat);
+ mHandler.onConnect();
+ }
+ }, null);
+ mClient.connect();
+ }
+
+ public void disconnect() throws IOException {
+ cleanup();
+ }
+
+ private void cleanup() {
+ if (mClient != null) {
+ mClient.disconnect();
+ mClient = null;
+ }
+
+ if (mSendLooper != null) {
+ mSendLooper.quit();
+ }
+ mSendLooper = null;
+ mSendHandler = null;
+ }
+
+ public void connect() {
+ if (mClient != null)
+ return;
+ new Thread() {
+ public void run() {
+
+ try {
+ String line = downloadUriAsString();
+ String[] parts = line.split(":");
+ mSession = parts[0];
+ String heartbeat = parts[1];
+ if (!"".equals(heartbeat))
+ mHeartbeat = Integer.parseInt(heartbeat) / 2 * 1000;
+ String transportsLine = parts[3];
+ String[] transports = transportsLine.split(",");
+ HashSet<String> set = new HashSet<>(Arrays.asList(transports));
+ if (!set.contains("websocket"))
+ throw new Exception("websocket not supported");
+
+ Looper.prepare();
+ mSendLooper = Looper.myLooper();
+ mSendHandler = new android.os.Handler();
+
+ connectSession();
+
+ Looper.loop();
+ } catch (Exception e) {
+ mHandler.onError(e);
+ }
+ }
+ }.start();
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/internal/Util.java
@@ -0,0 +1,972 @@
+/*
+ * 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.annotation.SuppressLint;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.provider.Settings.Secure;
+import android.support.annotation.RequiresPermission;
+import android.text.TextUtils;
+import android.util.TypedValue;
+
+import com.google.android.gms.ads.identifier.AdvertisingIdClient;
+import com.leanplum.Leanplum;
+import com.leanplum.LeanplumActivityHelper;
+import com.leanplum.LeanplumDeviceIdMode;
+import com.leanplum.LeanplumException;
+import com.leanplum.internal.Constants.Methods;
+import com.leanplum.internal.Constants.Params;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.json.JSONTokener;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLConnection;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.UnsupportedCharsetException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLSocketFactory;
+
+/**
+ * Leanplum utilities.
+ *
+ * @author Andrew First
+ */
+public class Util {
+ private static final Executor asyncExecutor = Executors.newCachedThreadPool();
+
+ private static final String ACCESS_WIFI_STATE_PERMISSION = "android.permission.ACCESS_WIFI_STATE";
+
+ private static String appName = null;
+ private static String versionName = null;
+
+ private static boolean hasPlayServicesCalled = false;
+ private static boolean hasPlayServices = false;
+
+ public static class DeviceIdInfo {
+ public final String id;
+ public boolean limitAdTracking;
+
+ public DeviceIdInfo(String id) {
+ this.id = id;
+ }
+
+ public DeviceIdInfo(String id, boolean limitAdTracking) {
+ this.id = id;
+ this.limitAdTracking = limitAdTracking;
+ }
+ }
+
+ /**
+ * Gets MD5 hash of given string.
+ *
+ * @param string String for which want to have MD5 hash.
+ * @return String with MD5 hash of given string.
+ */
+ private static String md5(String string) throws Exception {
+ MessageDigest messageDigest = MessageDigest.getInstance("MD5");
+ messageDigest.update(string.getBytes(Charset.forName("UTF-8")));
+ byte digest[] = messageDigest.digest();
+
+ StringBuilder result = new StringBuilder();
+ for (byte dig : digest) {
+ result.append(String.format("%02x", dig));
+ }
+ return result.toString();
+ }
+
+ /**
+ * Gets SHA-256 hash of given string.
+ */
+ public static String sha256(String string) throws NoSuchAlgorithmException {
+ MessageDigest messageDigest = MessageDigest.getInstance("SHA256");
+ messageDigest.update(string.getBytes(Charset.forName("UTF-8")));
+ byte digest[] = messageDigest.digest();
+
+ StringBuilder result = new StringBuilder();
+ for (byte dig : digest) {
+ result.append(String.format("%02x", dig));
+ }
+ return result.toString();
+ }
+
+ private static String checkDeviceId(String deviceIdMethod, String deviceId) {
+ if (deviceId != null) {
+ if (!isValidDeviceId(deviceId)) {
+ Log.e("Invalid device id generated (" + deviceIdMethod + "): " + deviceId);
+ return null;
+ }
+ }
+ return deviceId;
+ }
+
+ @RequiresPermission(ACCESS_WIFI_STATE_PERMISSION)
+ private static String getWifiMacAddressHash(Context context) {
+ String logPrefix = "Skipping wifi device id; ";
+ if (context.checkCallingOrSelfPermission(ACCESS_WIFI_STATE_PERMISSION) !=
+ PackageManager.PERMISSION_GRANTED) {
+ Log.v(logPrefix + "no wifi state permissions.");
+ return null;
+ }
+ try {
+ WifiManager manager = (WifiManager) context.getApplicationContext()
+ .getSystemService(Context.WIFI_SERVICE);
+ WifiInfo wifiInfo = manager.getConnectionInfo();
+ if (wifiInfo == null) {
+ Log.i(logPrefix + "null WifiInfo.");
+ return null;
+ }
+ @SuppressLint("HardwareIds")
+ String macAddress = wifiInfo.getMacAddress();
+ if (macAddress == null || macAddress.isEmpty()) {
+ Log.i(logPrefix + "no mac address returned.");
+ return null;
+ }
+ if (Constants.INVALID_MAC_ADDRESS.equals(macAddress)) {
+ // Note(ed): this is the expected case for Marshmallow and later, as they return
+ // INVALID_MAC_ADDRESS; we intend to fall back to the Android id for Marshmallow devices.
+ Log.v(logPrefix + "Marshmallow and later returns a fake MAC address.");
+ return null;
+ }
+ @SuppressLint("HardwareIds")
+ String deviceId = md5(wifiInfo.getMacAddress());
+ Log.v("Using wifi device id: " + deviceId);
+ return checkDeviceId("mac address", deviceId);
+ } catch (Exception e) {
+ Log.w("Error getting wifi MAC address.");
+ }
+ return null;
+ }
+
+ /**
+ * Retrieves the advertising ID. Requires Google Play Services. Note: This method must not run on
+ * the main thread.
+ */
+ private static DeviceIdInfo getAdvertisingId(Context caller) throws Exception {
+ try {
+ AdvertisingIdClient.Info info = AdvertisingIdClient.getAdvertisingIdInfo(caller);
+ if (info != null) {
+ String advertisingId = info.getId();
+ String deviceId = checkDeviceId("advertising id", advertisingId);
+ if (deviceId != null) {
+ boolean limitedTracking = info.isLimitAdTrackingEnabled();
+ return new DeviceIdInfo(deviceId, limitedTracking);
+ }
+ }
+ } catch (Throwable t) {
+ Log.e("Error getting advertising ID. Google Play Services are not available: ", t);
+ }
+ return null;
+ }
+
+ private static String getAndroidId(Context context) {
+ @SuppressLint("HardwareIds")
+ String androidId = Secure.getString(context.getContentResolver(), Secure.ANDROID_ID);
+ if (androidId == null || androidId.isEmpty()) {
+ Log.i("Skipping Android device id; no id returned.");
+ return null;
+ }
+ if (Constants.INVALID_ANDROID_ID.equals(androidId)) {
+ Log.v("Skipping Android device id; got invalid " + "device id: " + androidId);
+ return null;
+ }
+ Log.v("Using Android device id: " + androidId);
+ return checkDeviceId("android id", androidId);
+ }
+
+ /**
+ * Final fallback device id -- generate a random device id.
+ */
+ private static String generateRandomDeviceId() {
+ // Mark random IDs to be able to identify them.
+ String randomId = UUID.randomUUID().toString() + "-LP";
+ Log.v("Using generated device id: " + randomId);
+ return randomId;
+ }
+
+ private static boolean isValidForCharset(String id, String charsetName) {
+ CharsetEncoder encoder = null;
+ try {
+ Charset charset = Charset.forName(charsetName);
+ encoder = charset.newEncoder();
+ } catch (UnsupportedCharsetException e) {
+ Log.w("Unsupported charset: " + charsetName);
+ }
+ if (encoder != null && !encoder.canEncode(id)) {
+ Log.v("Invalid id (contains invalid characters): " + id);
+ return false;
+ }
+ return true;
+ }
+
+ public static boolean isValidUserId(String userId) {
+ String logPrefix = "Invalid user id ";
+ if (userId == null || userId.isEmpty()) {
+ Log.v(logPrefix + "(sentinel): " + userId);
+ return false;
+ }
+ if (userId.length() > Constants.MAX_USER_ID_LENGTH) {
+ Log.v(logPrefix + "(too long): " + userId);
+ return false;
+ }
+ if (userId.contains("\n")) {
+ Log.v(logPrefix + "(contains newline): " + userId);
+ return false;
+ }
+ if (userId.contains("\"") || userId.contains("\'")) {
+ Log.v(logPrefix + "(contains quotes): " + userId);
+ return false;
+ }
+ return isValidForCharset(userId, "UTF-8");
+ }
+
+ public static boolean isValidDeviceId(String deviceId) {
+ String logPrefix = "Invalid device id ";
+ if (deviceId == null || deviceId.isEmpty() ||
+ Constants.INVALID_ANDROID_ID.equals(deviceId) ||
+ Constants.INVALID_MAC_ADDRESS_HASH.equals(deviceId) ||
+ Constants.OLD_INVALID_MAC_ADDRESS_HASH.equals(deviceId)) {
+ Log.v(logPrefix + "(sentinel): " + deviceId);
+ return false;
+ }
+ if (deviceId.length() > Constants.MAX_DEVICE_ID_LENGTH) {
+ Log.v(logPrefix + "(too long): " + deviceId);
+ return false;
+ }
+ if (deviceId.contains("[")) {
+ Log.v(logPrefix + "(contains brackets): " + deviceId);
+ return false;
+ }
+ if (deviceId.contains("\n")) {
+ Log.v(logPrefix + "(contains newline): " + deviceId);
+ return false;
+ }
+ if (deviceId.contains(",")) {
+ Log.v(logPrefix + "(contains comma): " + deviceId);
+ return false;
+ }
+ if (deviceId.contains("\"") || deviceId.contains("\'")) {
+ Log.v(logPrefix + "(contains quotes): " + deviceId);
+ return false;
+ }
+ return isValidForCharset(deviceId, "US-ASCII");
+ }
+
+ @RequiresPermission(ACCESS_WIFI_STATE_PERMISSION)
+ public static DeviceIdInfo getDeviceId(LeanplumDeviceIdMode mode) {
+ Context context = Leanplum.getContext();
+
+ if (mode.equals(LeanplumDeviceIdMode.ADVERTISING_ID)) {
+ try {
+ DeviceIdInfo info = getAdvertisingId(context);
+ if (info != null) {
+ return info;
+ }
+ } catch (Exception e) {
+ Log.e("Error getting advertising ID", e);
+ }
+ }
+
+ if (isSimulator() || mode.equals(LeanplumDeviceIdMode.ANDROID_ID)) {
+ String androidId = getAndroidId(context);
+ if (androidId != null) {
+ return new DeviceIdInfo(getAndroidId(context));
+ }
+ }
+
+ String macAddressHash = getWifiMacAddressHash(context);
+ if (macAddressHash != null) {
+ return new DeviceIdInfo(macAddressHash);
+ }
+
+ String androidId = getAndroidId(context);
+ if (androidId != null) {
+ return new DeviceIdInfo(androidId);
+ }
+
+ return new DeviceIdInfo(generateRandomDeviceId());
+ }
+
+ public static String getVersionName() {
+ if (versionName != null) {
+ return versionName;
+ }
+ Context context = Leanplum.getContext();
+ try {
+ versionName = LeanplumManifestHelper.getAppVersionName();
+ // If we didn't get application version name from AndroidManifest.xml - will try to get it
+ // from PackageInfo.
+ if (TextUtils.isEmpty(versionName)) {
+ PackageInfo pInfo = context.getPackageManager().getPackageInfo(
+ context.getPackageName(), 0);
+ versionName = pInfo.versionName;
+ }
+ } catch (Exception e) {
+ Log.w("Could not extract versionName from Manifest or PackageInfo.");
+ }
+ return versionName;
+ }
+
+ public static String getDeviceModel() {
+ if (isSimulator()) {
+ return "Android Emulator";
+ }
+ String manufacturer = Build.MANUFACTURER;
+ String model = Build.MODEL;
+ if (model.startsWith(manufacturer)) {
+ return capitalize(model);
+ } else {
+ return capitalize(manufacturer) + " " + model;
+ }
+ }
+
+ public static String getApplicationName(Context context) {
+ if (appName != null) {
+ return appName;
+ }
+ int stringId = context.getApplicationInfo().labelRes;
+ if (stringId == 0) {
+ appName = context.getApplicationInfo().loadLabel(context.getPackageManager()).toString();
+ } else {
+ appName = context.getString(stringId);
+ }
+ return appName;
+ }
+
+ private static String capitalize(String s) {
+ if (s == null || s.length() == 0) {
+ return "";
+ }
+ char first = s.charAt(0);
+ if (Character.isUpperCase(first)) {
+ return s;
+ } else {
+ return Character.toUpperCase(first) + s.substring(1);
+ }
+ }
+
+ @SuppressWarnings("SameReturnValue")
+ public static String getSystemName() {
+ return "Android OS";
+ }
+
+ @SuppressWarnings("SameReturnValue")
+ public static String getSystemVersion() {
+ return Build.VERSION.RELEASE;
+ }
+
+ public static boolean isSimulator() {
+ String model = android.os.Build.MODEL.toLowerCase(Locale.getDefault());
+ return model.contains("google_sdk")
+ || model.contains("emulator")
+ || model.contains("sdk");
+ }
+
+ public static String getDeviceName() {
+ if (isSimulator()) {
+ return "Android Emulator";
+ }
+ return getDeviceModel();
+ }
+
+ public static String getLocale() {
+ String language = Locale.getDefault().getLanguage();
+ if ("".equals(language)) {
+ language = "xx";
+ }
+ String country = Locale.getDefault().getCountry();
+ if ("".equals(country)) {
+ country = "XX";
+ }
+ return language + "_" + country;
+ }
+
+ /**
+ * Builds a query from Map containing parameters.
+ *
+ * @param params Params used to build a query.
+ * @return Query string or empty string in case params are null.
+ */
+ private static String getQuery(Map<String, Object> params) {
+ if (params == null) {
+ return "";
+ }
+ Uri.Builder builder = new Uri.Builder();
+ for (Map.Entry<String, Object> pair : params.entrySet()) {
+ if (pair.getValue() == null) {
+ Log.w("Request parameter for key: " + pair.getKey() + " is null.");
+ continue;
+ }
+ builder.appendQueryParameter(pair.getKey(), pair.getValue().toString());
+ }
+ return builder.build().getEncodedQuery();
+ }
+
+ public static HttpURLConnection operation(
+ String hostName,
+ String path,
+ Map<String, Object> params,
+ String httpMethod,
+ boolean ssl,
+ int timeoutSeconds) throws IOException {
+ if ("GET".equals(httpMethod)) {
+ path = attachGetParameters(path, params);
+ }
+ HttpURLConnection urlConnection = createHttpUrlConnection(hostName, path,
+ httpMethod, ssl, timeoutSeconds);
+
+ if (!"GET".equals(httpMethod)) {
+ attachPostParameters(params, urlConnection);
+ }
+
+ if (Constants.enableVerboseLoggingInDevelopmentMode
+ && Constants.isDevelopmentModeEnabled) {
+ Log.d("Sending request at path " + path + " with parameters " + params);
+ }
+ return urlConnection;
+ }
+
+ /**
+ * Converts and attaches GET parameters to specified path.
+ *
+ * @param path Path on which to attach parameters.
+ * @param params Params to convert and attach.
+ * @return Path with attached parameters.
+ */
+ private static String attachGetParameters(String path, Map<String, Object> params) {
+ if (params == null) {
+ return path;
+ }
+ Uri.Builder builder = Uri.parse(path).buildUpon();
+ for (Map.Entry<String, Object> pair : params.entrySet()) {
+ if (pair.getValue() == null) {
+ continue;
+ }
+ builder.appendQueryParameter(pair.getKey(), pair.getValue().toString());
+ }
+ return builder.build().toString();
+ }
+
+ /**
+ * Converts and writes POST parameters directly to an option http connection.
+ *
+ * @param params Params to post.
+ * @param urlConnection URL connection on which to write parameters.
+ * @throws IOException Throws in case it fails.
+ */
+ private static void attachPostParameters(Map<String, Object> params,
+ HttpURLConnection urlConnection) throws IOException {
+ OutputStream os = urlConnection.getOutputStream();
+ BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));
+ String query = getQuery(params);
+ writer.write(query);
+ writer.close();
+ os.close();
+ }
+
+ public static HttpURLConnection createHttpUrlConnection(String hostName,
+ String path, String httpMethod, boolean ssl, int timeoutSeconds)
+ throws IOException {
+ String fullPath;
+ if (path.startsWith("http")) {
+ fullPath = path;
+ } else {
+ fullPath = (ssl ? "https://" : "http://") + hostName + "/" + path;
+ }
+ return createHttpUrlConnection(fullPath, httpMethod, ssl, timeoutSeconds);
+ }
+
+ static HttpURLConnection createHttpUrlConnection(
+ String fullPath, String httpMethod, boolean ssl, int timeoutSeconds)
+ throws IOException {
+ URL url = new URL(fullPath);
+ HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
+ if (ssl) {
+ SSLSocketFactory socketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
+ ((HttpsURLConnection) urlConnection).setSSLSocketFactory(socketFactory);
+ }
+ urlConnection.setReadTimeout(timeoutSeconds * 1000);
+ urlConnection.setConnectTimeout(timeoutSeconds * 1000);
+ urlConnection.setRequestMethod(httpMethod);
+ urlConnection.setDoOutput(!"GET".equals(httpMethod));
+ urlConnection.setDoInput(true);
+ urlConnection.setUseCaches(false);
+ urlConnection.setInstanceFollowRedirects(true);
+ Context context = Leanplum.getContext();
+ urlConnection.setRequestProperty("User-Agent",
+ getApplicationName(context) + "/" + getVersionName() + "/" + Request.appId() + "/" +
+ Constants.CLIENT + "/" + Constants.LEANPLUM_VERSION + "/" + getSystemName() + "/" +
+ getSystemVersion() + "/" + Constants.LEANPLUM_PACKAGE_IDENTIFIER);
+ return urlConnection;
+ }
+
+ /**
+ * Writes the filesToUpload to a new HttpURLConnection using the multipart form data format.
+ *
+ * @return the connection that the files were uploaded using
+ */
+ public static HttpURLConnection uploadFilesOperation(
+ String key,
+ List<File> filesToUpload,
+ List<InputStream> streams,
+ String hostName,
+ String path,
+ Map<String, Object> params,
+ String httpMethod,
+ boolean ssl,
+ int timeoutSeconds) throws IOException {
+
+ HttpURLConnection urlConnection = createHttpUrlConnection(hostName, path,
+ httpMethod, ssl, timeoutSeconds);
+
+ final String BOUNDARY = "==================================leanplum";
+ final String LINE_END = "\r\n";
+ final String TWO_HYPHENS = "--";
+ final String CONTENT_TYPE = "Content-Type: application/octet-stream";
+
+ // Make a connection to the server
+ urlConnection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + BOUNDARY);
+ urlConnection.setRequestProperty("Connection", "Keep-Alive");
+
+ DataOutputStream outputStream = new DataOutputStream(urlConnection.getOutputStream());
+
+ // Create the header for the request with the parameters
+ for (Map.Entry<String, Object> entry : params.entrySet()) {
+ String paramData = TWO_HYPHENS + BOUNDARY + LINE_END
+ + "Content-Disposition: form-data; name=\"" + entry.getKey() + "\"" + LINE_END
+ + LINE_END
+ + entry.getValue() + LINE_END;
+ outputStream.writeBytes(paramData);
+ }
+
+ // Main file writing loop
+ for (int i = 0; i < filesToUpload.size(); i++) {
+ File fileToUpload = filesToUpload.get(i);
+ String contentDisposition = String.format(Locale.getDefault(), "Content-Disposition: " +
+ "form-data; name=\"%s%d\";filename=\"%s\"",
+ key, i, fileToUpload.getName());
+
+ // Create the header for the file
+ String fileHeader = TWO_HYPHENS + BOUNDARY + LINE_END
+ + contentDisposition + LINE_END
+ + CONTENT_TYPE + LINE_END
+ + LINE_END;
+ outputStream.writeBytes(fileHeader);
+
+ // Read in the actual file
+ InputStream is = (i < streams.size()) ? streams.get(i) : new FileInputStream(fileToUpload);
+ byte[] buffer = new byte[4096];
+ int bytesRead;
+ try {
+ while ((bytesRead = is.read(buffer)) != -1) {
+ outputStream.write(buffer, 0, bytesRead);
+ }
+ } catch (NullPointerException e) {
+ Log.e("Unable to read file while uploading " + filesToUpload.get(i));
+ return null;
+ } finally {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (IOException e) {
+ Log.w("Failed to close InputStream: " + e);
+ }
+ }
+ }
+
+ // End the output for this file
+ outputStream.writeBytes(LINE_END);
+ }
+
+ // End the output for the request
+ String endOfRequest = TWO_HYPHENS + BOUNDARY + TWO_HYPHENS + LINE_END;
+ outputStream.writeBytes(endOfRequest);
+
+ outputStream.flush();
+ outputStream.close();
+ return urlConnection;
+ }
+
+ public static void saveResponse(URLConnection op, OutputStream outputStream) throws IOException {
+ InputStream is = op.getInputStream();
+ byte[] buffer = new byte[4096];
+ int bytesRead;
+ while ((bytesRead = is.read(buffer)) != -1) {
+ outputStream.write(buffer, 0, bytesRead);
+ }
+ outputStream.close();
+ }
+
+ private static String getResponse(HttpURLConnection op) throws IOException {
+ InputStream inputStream;
+ if (op.getResponseCode() < 400) {
+ inputStream = op.getInputStream();
+ } else {
+ inputStream = op.getErrorStream();
+ }
+ BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
+ StringBuilder builder = new StringBuilder();
+ for (String line; (line = reader.readLine()) != null; ) {
+ builder.append(line).append("\n");
+ }
+
+ try {
+ inputStream.close();
+ reader.close();
+ } catch (Exception ignored) {
+ }
+
+ return builder.toString();
+ }
+
+ public static JSONObject getJsonResponse(HttpURLConnection op)
+ throws JSONException, IOException {
+ String response = getResponse(op);
+ if (Constants.enableVerboseLoggingInDevelopmentMode
+ && Constants.isDevelopmentModeEnabled) {
+ Log.d("Received response " + response);
+ }
+ JSONTokener tokener = new JSONTokener(response);
+ return new JSONObject(tokener);
+ }
+
+ /**
+ * Check whether the device has a network connection. WARNING: Does not check for available
+ * internet connection! use isOnline()
+ *
+ * @return Whether a network connection is available or not.
+ */
+ public static boolean isConnected() {
+ try {
+ Context context = Leanplum.getContext();
+ ConnectivityManager manager = (ConnectivityManager) context.getSystemService(
+ Context.CONNECTIVITY_SERVICE);
+ if (manager == null) {
+ return false;
+ }
+ NetworkInfo netInfo = manager.getActiveNetworkInfo();
+ return !(netInfo == null || !netInfo.isConnectedOrConnecting());
+ } catch (Exception e) {
+ Log.e("Error getting connectivity info", e);
+ return false;
+ }
+ }
+
+ public static <T> T multiIndex(Map<?, ?> map, Object... indices) {
+ if (map == null) {
+ return null;
+ }
+ Object current = map;
+ for (Object index : indices) {
+ if (!((Map<?, ?>) current).containsKey(index)) {
+ return null;
+ }
+ current = ((Map<?, ?>) current).get(index);
+ }
+ return CollectionUtil.uncheckedCast(current);
+ }
+
+ public static <T> void executeAsyncTask(AsyncTask<T, ?, ?> task, T... params) {
+ if (Build.VERSION.SDK_INT >= 11) {
+ task.executeOnExecutor(asyncExecutor, params);
+ } else {
+ task.execute(params);
+ }
+ }
+
+ /**
+ * Check the device to make sure it has the Google Play Services APK. If it doesn't, display a
+ * dialog that allows users to download the APK from the Google Play Store or enable it in the
+ * device's system settings.
+ */
+ public static boolean hasPlayServices() {
+ if (hasPlayServicesCalled) {
+ return hasPlayServices;
+ }
+ Context context = Leanplum.getContext();
+ PackageManager packageManager = context.getPackageManager();
+ PackageInfo packageInfo;
+ try {
+ packageInfo = packageManager.getPackageInfo("com.google.android.gms",
+ PackageManager.GET_SIGNATURES);
+ } catch (PackageManager.NameNotFoundException e) {
+ hasPlayServicesCalled = true;
+ hasPlayServices = false;
+ return false;
+ }
+ if (packageInfo.versionCode < 4242000) {
+ Log.i("Google Play services version is too old: " + packageInfo.versionCode);
+ hasPlayServicesCalled = true;
+ hasPlayServices = false;
+ return false;
+ }
+ ApplicationInfo info;
+ try {
+ info = packageManager.getApplicationInfo("com.google.android.gms", 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ hasPlayServicesCalled = true;
+ hasPlayServices = false;
+ return false;
+ }
+ hasPlayServicesCalled = true;
+ hasPlayServices = info.enabled;
+ return info.enabled;
+ }
+
+ public static boolean isInBackground() {
+ return (LeanplumActivityHelper.getCurrentActivity() == null ||
+ LeanplumActivityHelper.isActivityPaused());
+ }
+
+ /**
+ * Include install time and last app update time in start API params the first time that the app
+ * runs with Leanplum.
+ */
+ public static void initializePreLeanplumInstall(Map<String, Object> params) {
+ Context context = Leanplum.getContext();
+ SharedPreferences preferences = context.getSharedPreferences("__leanplum__",
+ Context.MODE_PRIVATE);
+ if (preferences.getBoolean(Constants.Keys.INSTALL_TIME_INITIALIZED, false)) {
+ return;
+ }
+
+ PackageManager packageManager = context.getPackageManager();
+ String packageName = context.getPackageName();
+ setInstallTime(params, packageManager, packageName);
+ setUpdateTime(params, packageManager, packageName);
+
+ SharedPreferences.Editor editor = preferences.edit();
+ editor.putBoolean(Constants.Keys.INSTALL_TIME_INITIALIZED, true);
+ try {
+ editor.apply();
+ } catch (NoSuchMethodError e) {
+ editor.commit();
+ }
+ }
+
+ /**
+ * Set install time from package manager and update time from apk file modification time.
+ */
+ private static void setInstallTime(Map<String, Object> params, PackageManager packageManager,
+ String packageName) {
+ try {
+ PackageInfo info = packageManager.getPackageInfo(packageName, 0);
+ params.put(Params.INSTALL_DATE, "" + (info.firstInstallTime / 1000.0));
+ } catch (NameNotFoundException e) {
+ Log.w("Failed to find package info: " + e);
+ }
+ }
+
+ /**
+ * Set update time from apk file modification time.
+ */
+ private static void setUpdateTime(Map<String, Object> params, PackageManager packageManager,
+ String packageName) {
+ try {
+ ApplicationInfo info = packageManager.getApplicationInfo(packageName, 0);
+ File apkFile = new File(info.sourceDir);
+ if (apkFile.exists()) {
+ params.put(Constants.Params.UPDATE_DATE, "" + (apkFile.lastModified() / 1000.0));
+ }
+ } catch (Throwable t) {
+ Log.w("Failed to find package info: " + t);
+ }
+ }
+
+ /**
+ * Handles uncaught exceptions in the SDK.
+ */
+ public static void handleException(Throwable t) {
+ if (t instanceof OutOfMemoryError) {
+ if (Constants.isDevelopmentModeEnabled) {
+ throw (OutOfMemoryError) t;
+ }
+ return;
+ }
+
+ // Propagate Leanplum generated exceptions.
+ if (t instanceof LeanplumException) {
+ if (Constants.isDevelopmentModeEnabled) {
+ throw (LeanplumException) t;
+ }
+ return;
+ }
+
+ Log.e("INTERNAL ERROR", t);
+
+ String versionName;
+ try {
+ versionName = getVersionName();
+ } catch (Throwable t2) {
+ versionName = "(Unknown)";
+ }
+
+ try {
+ Map<String, Object> params = new HashMap<>();
+ params.put(Params.TYPE, Constants.Values.SDK_ERROR);
+
+ String message = t.getMessage();
+ if (message != null) {
+ message = t.toString() + " (" + message + ')';
+ } else {
+ message = t.toString();
+ }
+ params.put(Params.MESSAGE, message);
+
+ StringWriter stringWriter = new StringWriter();
+ PrintWriter writer = new PrintWriter(stringWriter);
+ t.printStackTrace(writer);
+ params.put("stackTrace", stringWriter.toString());
+
+ params.put(Params.VERSION_NAME, versionName);
+ Request.post(Methods.LOG, params).send();
+ } catch (Throwable t2) {
+ Log.e("Unable to send error report.", t2);
+ }
+ }
+
+ /**
+ * Constructs a {@link HashMap} with the given keys and values.
+ */
+ public static <K, V> Map<K, V> newMap(K firstKey, V firstValue, Object... otherValues) {
+ if (otherValues.length % 2 == 1) {
+ throw new IllegalArgumentException("Must supply an even number of values.");
+ }
+
+ Map<K, V> map = new HashMap<>();
+ map.put(firstKey, firstValue);
+ for (int i = 0; i < otherValues.length; i += 2) {
+ K otherKey = CollectionUtil.uncheckedCast(otherValues[i]);
+ V otherValue = CollectionUtil.uncheckedCast(otherValues[i + 1]);
+ map.put(otherKey, otherValue);
+ }
+ return map;
+ }
+
+ /**
+ * Generates a Resource name from resourceId located in res/ folder.
+ *
+ * @param resourceId id of the resource, must be greater then 0.
+ * @return resourceName in format folder/file.extension.
+ */
+ public static String generateResourceNameFromId(int resourceId) {
+ try {
+ if (resourceId <= 0) {
+ Log.w("Provided resource id is invalid.");
+ return null;
+ }
+ Resources resources = Leanplum.getContext().getResources();
+ // Get entryName from resourceId, which represents a file name in res/ directory.
+ String entryName = resources.getResourceEntryName(resourceId);
+ // Get typeName from resourceId, which represents a folder where file is located in
+ // res/ directory.
+ String typeName = resources.getResourceTypeName(resourceId);
+
+ // By using TypedValue we can get full path of a file with extension.
+ TypedValue value = new TypedValue();
+ resources.getValue(resourceId, value, true);
+
+ // Regex matching to find real file extension, "image.img.png" will produce "png".
+ String[] fullFileName = value.string.toString().split("\\.(?=[^\\.]+$)");
+ String extension = "";
+ // If extension is found, we will append dot before it.
+ if (fullFileName.length == 2) {
+ extension = "." + fullFileName[1];
+ }
+
+ // Return full resource name in format: drawable/image.png
+ return typeName + "/" + entryName + extension;
+ } catch (Exception e) {
+ Log.w("Failed to generate resource name from provided resource id: ", e);
+ Util.handleException(e);
+ }
+ return null;
+ }
+
+ /**
+ * Generates resource Id based on Resource name.
+ *
+ * @param resourceName name of the resource including folder and file extension.
+ * @return id of the resource if found, 0 otherwise.
+ */
+ public static int generateIdFromResourceName(String resourceName) {
+ // Split resource name to extract folder and file name.
+ String[] parts = resourceName.split("/");
+ if (parts.length == 2) {
+ Resources resources = Leanplum.getContext().getResources();
+ // Type name represents folder where file is contained.
+ String typeName = parts[0];
+ String fileName = parts[1];
+ String entryName = fileName;
+ // Since fileName contains extension we have to remove it,
+ // to be able to get resource id.
+ String[] fileParts = fileName.split("\\.(?=[^\\.]+$)");
+ if (fileParts.length == 2) {
+ entryName = fileParts[0];
+ }
+ // Get identifier for a file in specified directory
+ if (!TextUtils.isEmpty(typeName) && !TextUtils.isEmpty(entryName)) {
+ return resources.getIdentifier(entryName, typeName, Leanplum.getContext().getPackageName());
+ }
+ }
+ Log.w("Could not extract resource id from provided resource name: ", resourceName);
+ return 0;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/internal/VarCache.java
@@ -0,0 +1,899 @@
+/*
+ * 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.Context;
+import android.content.SharedPreferences;
+
+import com.leanplum.ActionContext;
+import com.leanplum.CacheUpdateBlock;
+import com.leanplum.Leanplum;
+import com.leanplum.LocationManager;
+import com.leanplum.Var;
+import com.leanplum.internal.FileManager.HashResults;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.InputStream;
+import java.lang.reflect.Array;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Variable cache.
+ *
+ * @author Andrew First.
+ */
+public class VarCache {
+ private static final Map<String, Var<?>> vars = new ConcurrentHashMap<>();
+ private static final Map<String, Object> fileAttributes = new HashMap<>();
+ private static final Map<String, InputStream> fileStreams = new HashMap<>();
+
+ /**
+ * The default values set by the client. This is not thread-safe so traversals should be
+ * synchronized.
+ */
+ public static final Map<String, Object> valuesFromClient = new HashMap<>();
+
+ private static final Map<String, String> defaultKinds = new HashMap<>();
+ private static final Map<String, Object> actionDefinitions = new HashMap<>();
+ private static final String LEANPLUM = "__leanplum__";
+ private static Map<String, Object> diffs = new HashMap<>();
+ private static Map<String, Object> regions = new HashMap<>();
+ private static Map<String, Object> messageDiffs = new HashMap<>();
+ private static List<Map<String, Object>> updateRuleDiffs;
+ private static List<Map<String, Object>> eventRuleDiffs;
+ private static Map<String, Object> devModeValuesFromServer;
+ private static Map<String, Object> devModeFileAttributesFromServer;
+ private static Map<String, Object> devModeActionDefinitionsFromServer;
+ private static List<Map<String, Object>> variants = new ArrayList<>();
+ private static CacheUpdateBlock updateBlock;
+ private static CacheUpdateBlock interfaceUpdateBlock;
+ private static CacheUpdateBlock eventsUpdateBlock;
+ private static boolean hasReceivedDiffs = false;
+ private static Map<String, Object> messages = new HashMap<>();
+ private static Object merged;
+ private static boolean silent;
+ private static int contentVersion;
+ private static Map<String, Object> userAttributes;
+
+ private static final String NAME_COMPONENT_REGEX = "(?:[^\\.\\[.(\\\\]+|\\\\.)+";
+ private static final Pattern NAME_COMPONENT_PATTERN = Pattern.compile(NAME_COMPONENT_REGEX);
+
+ public static String[] getNameComponents(String name) {
+ Matcher matcher = NAME_COMPONENT_PATTERN.matcher(name);
+ List<String> components = new ArrayList<>();
+ while (matcher.find()) {
+ components.add(name.substring(matcher.start(), matcher.end()));
+ }
+ return components.toArray(new String[0]);
+ }
+
+ private static Object traverse(Object collection, Object key, boolean autoInsert) {
+ if (collection == null) {
+ return null;
+ }
+ if (collection instanceof Map) {
+ Map<Object, Object> castedCollection = CollectionUtil.uncheckedCast(collection);
+ Object result = castedCollection.get(key);
+ if (autoInsert && result == null && key instanceof String) {
+ result = new HashMap<String, Object>();
+ castedCollection.put(key, result);
+ }
+ return result;
+ } else if (collection instanceof List) {
+ List<Object> castedList = CollectionUtil.uncheckedCast(collection);
+ Object result = castedList.get((Integer) key);
+ if (autoInsert && result == null) {
+ result = new HashMap<String, Object>();
+ castedList.set((Integer) key, result);
+ }
+ return result;
+ }
+ return null;
+ }
+
+ public static boolean registerFile(
+ String stringValue, String defaultValue,
+ InputStream defaultStream, boolean isResource, String resourceHash, int resourceSize) {
+ if (Constants.isDevelopmentModeEnabled) {
+ if (!Constants.isNoop()) {
+ if (defaultStream == null) {
+ return false;
+ }
+ Map<String, Object> variationAttributes = new HashMap<>();
+ Map<String, Object> attributes = new HashMap<>();
+ if (isResource) {
+ attributes.put(Constants.Keys.HASH, resourceHash);
+ attributes.put(Constants.Keys.SIZE, resourceSize);
+ } else {
+ if (Constants.hashFilesToDetermineModifications && Util.isSimulator()) {
+ HashResults result = FileManager.fileMD5HashCreateWithPath(defaultStream);
+ if (result != null) {
+ attributes.put(Constants.Keys.HASH, result.hash);
+ attributes.put(Constants.Keys.SIZE, result.size);
+ }
+ } else {
+ int size = FileManager.getFileSize(
+ FileManager.fileValue(stringValue, defaultValue, null));
+ attributes.put(Constants.Keys.SIZE, size);
+ }
+ }
+ variationAttributes.put("", attributes);
+ fileAttributes.put(stringValue, variationAttributes);
+ fileStreams.put(stringValue, defaultStream);
+ maybeUploadNewFiles();
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private static void updateValues(String name, String[] nameComponents, Object value, String kind,
+ Map<String, Object> values, Map<String, String> kinds) {
+ Object valuesPtr = values;
+ if (nameComponents != null && nameComponents.length > 0) {
+ for (int i = 0; i < nameComponents.length - 1; i++) {
+ valuesPtr = traverse(valuesPtr, nameComponents[i], true);
+ }
+ if (valuesPtr instanceof Map) {
+ Map<String, Object> map = CollectionUtil.uncheckedCast(valuesPtr);
+ map.put(nameComponents[nameComponents.length - 1], value);
+ }
+ }
+ if (kinds != null) {
+ kinds.put(name, kind);
+ }
+ }
+
+ public static void registerVariable(Var<?> var) {
+ vars.put(var.name(), var);
+ synchronized (valuesFromClient) {
+ updateValues(
+ var.name(), var.nameComponents(), var.defaultValue(),
+ var.kind(), valuesFromClient, defaultKinds);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <T> Var<T> getVariable(String name) {
+ return (Var<T>) vars.get(name);
+ }
+
+ private static void computeMergedDictionary() {
+ synchronized (valuesFromClient) {
+ merged = mergeHelper(valuesFromClient, diffs);
+ }
+ }
+
+ public static Object mergeHelper(Object vars, Object diff) {
+ if (diff == null) {
+ return vars;
+ }
+ if (diff instanceof Number
+ || diff instanceof Boolean
+ || diff instanceof String
+ || diff instanceof Character
+ || vars instanceof Number
+ || vars instanceof Boolean
+ || vars instanceof String
+ || vars instanceof Character) {
+ return diff;
+ }
+
+ Iterable<?> diffKeys = (diff instanceof Map) ? ((Map<?, ?>) diff).keySet() : (Iterable<?>) diff;
+ Iterable<?> varsKeys = (vars instanceof Map) ? ((Map<?, ?>) vars).keySet() : (Iterable<?>) vars;
+ Map<?, ?> diffMap = (diff instanceof Map) ? ((Map<?, ?>) diff) : null;
+ Map<?, ?> varsMap = (vars instanceof Map) ? ((Map<?, ?>) vars) : null;
+
+ // Infer that the diffs is an array if the vars value doesn't exist to tell us the type.
+ boolean isArray = false;
+ if (vars == null) {
+ if (diff instanceof Map && ((Map<?, ?>) diff).size() > 0) {
+ isArray = true;
+ for (Object var : diffKeys) {
+ if (!(var instanceof String)) {
+ isArray = false;
+ break;
+ }
+ String str = ((String) var);
+ if (str.length() < 3 || str.charAt(0) != '[' || str.charAt(str.length() - 1) != ']') {
+ isArray = false;
+ break;
+ }
+ String varSubscript = str.substring(1, str.length() - 1);
+ if (!("" + Integer.getInteger(varSubscript)).equals(varSubscript)) {
+ isArray = false;
+ break;
+ }
+ }
+ }
+ }
+
+ // Merge arrays.
+ if (vars instanceof List || isArray) {
+ ArrayList<Object> merged = new ArrayList<>();
+ for (Object var : varsKeys) {
+ merged.add(var);
+ }
+ for (Object varSubscript : diffKeys) {
+ String strSubscript = (String) varSubscript;
+ int subscript = Integer.parseInt(strSubscript.substring(1, strSubscript.length() - 1));
+ Object var = diffMap != null ? diffMap.get(strSubscript) : null;
+ while (subscript >= merged.size()) {
+ merged.add(null);
+ }
+ merged.set(subscript, mergeHelper(merged.get(subscript), var));
+ }
+ return merged;
+ }
+
+ // Merge dictionaries.
+ if (vars instanceof Map || diff instanceof Map) {
+ HashMap<Object, Object> merged = new HashMap<>();
+ if (varsKeys != null) {
+ for (Object var : varsKeys) {
+ if (diffMap != null && varsMap != null) {
+ Object diffVar = diffMap.get(var);
+ Object value = varsMap.get(var);
+ if (diffVar == null && value != null) {
+ merged.put(var, value);
+ }
+ }
+ }
+ }
+ for (Object var : diffKeys) {
+ Object diffsValue = diffMap != null ? diffMap.get(var) : null;
+ Object varsValue = varsMap != null ? varsMap.get(var) : null;
+ Object mergedValues = mergeHelper(varsValue, diffsValue);
+ merged.put(var, mergedValues);
+ }
+ return merged;
+ }
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static <T> T getMergedValueFromComponentArray(Object[] components, Object values) {
+ Object mergedPtr = values;
+ for (Object component : components) {
+ mergedPtr = traverse(mergedPtr, component, false);
+ }
+ return (T) mergedPtr;
+ }
+
+ public static <T> T getMergedValueFromComponentArray(Object[] components) {
+ return getMergedValueFromComponentArray(components,
+ merged != null ? merged : valuesFromClient);
+ }
+
+ public static Map<String, Object> getDiffs() {
+ return diffs;
+ }
+
+ public static Map<String, Object> getMessageDiffs() {
+ return messageDiffs;
+ }
+
+ public static List<Map<String, Object>> getUpdateRuleDiffs() {
+ return updateRuleDiffs;
+ }
+
+ public static List<Map<String, Object>> getEventRuleDiffs() {
+ return eventRuleDiffs;
+ }
+
+ public static Map<String, Object> regions() {
+ return regions;
+ }
+
+ public static boolean hasReceivedDiffs() {
+ return hasReceivedDiffs;
+ }
+
+ public static void loadDiffs() {
+ if (Constants.isNoop()) {
+ return;
+ }
+ Context context = Leanplum.getContext();
+ SharedPreferences defaults = context.getSharedPreferences(LEANPLUM, Context.MODE_PRIVATE);
+ if (Request.token() == null) {
+ applyVariableDiffs(
+ new HashMap<String, Object>(),
+ new HashMap<String, Object>(),
+ new ArrayList<Map<String, Object>>(),
+ new ArrayList<Map<String, Object>>(),
+ new HashMap<String, Object>(),
+ new ArrayList<Map<String, Object>>());
+ return;
+ }
+ try {
+ // Crypt functions return input text if there was a problem.
+ AESCrypt aesContext = new AESCrypt(Request.appId(), Request.token());
+ String variables = aesContext.decodePreference(
+ defaults, Constants.Defaults.VARIABLES_KEY, "{}");
+ String messages = aesContext.decodePreference(
+ defaults, Constants.Defaults.MESSAGES_KEY, "{}");
+ String updateRules = aesContext.decodePreference(
+ defaults, Constants.Defaults.UPDATE_RULES_KEY, "[]");
+ String eventRules = aesContext.decodePreference(
+ defaults, Constants.Defaults.EVENT_RULES_KEY, "[]");
+ String regions = aesContext.decodePreference(defaults, Constants.Defaults.REGIONS_KEY, "{}");
+ String variants = aesContext.decodePreference(defaults, Constants.Keys.VARIANTS, "[]");
+ applyVariableDiffs(
+ JsonConverter.fromJson(variables),
+ JsonConverter.fromJson(messages),
+ JsonConverter.<Map<String, Object>>listFromJson(new JSONArray(updateRules)),
+ JsonConverter.<Map<String, Object>>listFromJson(new JSONArray(eventRules)),
+ JsonConverter.fromJson(regions),
+ JsonConverter.<Map<String, Object>>listFromJson(new JSONArray(variants)));
+ String deviceId = aesContext.decodePreference(defaults, Constants.Params.DEVICE_ID, null);
+ if (deviceId != null) {
+ if (Util.isValidDeviceId(deviceId)) {
+ Request.setDeviceId(deviceId);
+ } else {
+ Log.w("Invalid stored device id found: \"" + deviceId + "\"; discarding.");
+ }
+ }
+ String userId = aesContext.decodePreference(defaults, Constants.Params.USER_ID, null);
+ if (userId != null) {
+ if (Util.isValidUserId(userId)) {
+ Request.setUserId(userId);
+ } else {
+ Log.w("Invalid stored user id found: \"" + userId + "\"; discarding.");
+ }
+ }
+ String loggingEnabled = aesContext.decodePreference(defaults, Constants.Keys.LOGGING_ENABLED,
+ "false");
+ if (Boolean.parseBoolean(loggingEnabled)) {
+ Constants.loggingEnabled = true;
+ }
+ } catch (Exception e) {
+ Log.e("Could not load variable diffs.\n" + Log.getStackTraceString(e));
+ }
+ userAttributes();
+ }
+
+ public static void saveDiffs() {
+ 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();
+
+ // Crypt functions return input text if there was a problem.
+ AESCrypt aesContext = new AESCrypt(Request.appId(), Request.token());
+ String variablesCipher = aesContext.encrypt(JsonConverter.toJson(diffs));
+ editor.putString(Constants.Defaults.VARIABLES_KEY, variablesCipher);
+
+ String messagesCipher = aesContext.encrypt(JsonConverter.toJson(messages));
+ editor.putString(Constants.Defaults.MESSAGES_KEY, messagesCipher);
+
+ try {
+ String updateRulesCipher = aesContext.encrypt(
+ JsonConverter.listToJsonArray(updateRuleDiffs).toString());
+ editor.putString(Constants.Defaults.UPDATE_RULES_KEY, updateRulesCipher);
+ } catch (JSONException e) {
+ Log.e("Error converting updateRuleDiffs to JSON", e);
+ }
+
+ try {
+ String eventRulesCipher = aesContext.encrypt(
+ JsonConverter.listToJsonArray(eventRuleDiffs).toString());
+ editor.putString(Constants.Defaults.EVENT_RULES_KEY, eventRulesCipher);
+ } catch (JSONException e) {
+ Log.e("Error converting eventRuleDiffs to JSON", e);
+ }
+
+ String regionsCipher = aesContext.encrypt(JsonConverter.toJson(regions));
+ editor.putString(Constants.Defaults.REGIONS_KEY, regionsCipher);
+
+ try {
+ String variantsJson = JsonConverter.listToJsonArray(variants).toString();
+ editor.putString(Constants.Keys.VARIANTS, aesContext.encrypt(variantsJson));
+ } catch (JSONException e1) {
+ Log.e("Error converting " + variants + " to JSON.\n" + Log.getStackTraceString(e1));
+ }
+ editor.putString(Constants.Params.DEVICE_ID, aesContext.encrypt(Request.deviceId()));
+ editor.putString(Constants.Params.USER_ID, aesContext.encrypt(Request.userId()));
+ editor.putString(Constants.Keys.LOGGING_ENABLED,
+ aesContext.encrypt(String.valueOf(Constants.loggingEnabled)));
+ try {
+ editor.apply();
+ } catch (NoSuchMethodError e) {
+ editor.commit();
+ }
+ }
+
+ /**
+ * Convert a resId to a resPath.
+ */
+ static int getResIdFromPath(String resPath) {
+ int resId = 0;
+ try {
+ String path = resPath.replace("res/", "");
+ path = path.substring(0, path.lastIndexOf('.')); // remove file extension
+ String name = path.substring(path.lastIndexOf('/') + 1);
+ String type = path.substring(0, path.lastIndexOf('/'));
+ type = type.replace('/', '.');
+ Context context = Leanplum.getContext();
+ resId = context.getResources().getIdentifier(name, type, context.getPackageName());
+ } catch (Exception e) {
+ // Fall back to 0 on any exception
+ }
+ return resId;
+ }
+
+ /**
+ * Update file variables stream info with override info, so that override files don't require
+ * downloads if they're already available.
+ */
+ private static void fileVariableFinish() {
+ for (String name : new HashMap<>(vars).keySet()) {
+ Var<?> var = vars.get(name);
+ String overrideFile = var.stringValue;
+ if (var.isResource && (var.kind().equals(Constants.Kinds.FILE)) && overrideFile != null &&
+ !var.defaultValue().equals(overrideFile)) {
+ Map<String, Object> variationAttributes = CollectionUtil.uncheckedCast(fileAttributes.get
+ (overrideFile));
+ InputStream stream = fileStreams.get(overrideFile);
+ if (variationAttributes != null && stream != null) {
+ var.setOverrideResId(getResIdFromPath(var.stringValue()));
+ }
+ }
+ }
+ }
+
+ public static void applyVariableDiffs(
+ Map<String, Object> diffs,
+ Map<String, Object> messages,
+ List<Map<String, Object>> updateRules,
+ List<Map<String, Object>> eventRules,
+ Map<String, Object> regions,
+ List<Map<String, Object>> variants) {
+ if (diffs != null) {
+ VarCache.diffs = diffs;
+ computeMergedDictionary();
+
+ // Update variables with new values.
+ // Have to copy the dictionary because a dictionary variable may add a new sub-variable,
+ // modifying the variable dictionary.
+ for (String name : new HashMap<>(vars).keySet()) {
+ vars.get(name).update();
+ }
+ fileVariableFinish();
+ }
+
+ if (messages != null) {
+ // Store messages.
+ messageDiffs = messages;
+ Map<String, Object> newMessages = new HashMap<>();
+ for (Map.Entry<String, Object> entry : messages.entrySet()) {
+ Map<String, Object> messageConfig = CollectionUtil.uncheckedCast(entry.getValue());
+ Map<String, Object> newConfig = new HashMap<>(messageConfig);
+ Map<String, Object> actionArgs = CollectionUtil.uncheckedCast(messageConfig.get(Constants
+ .Keys.VARS));
+ Map<String, Object> defaultArgs = Util.multiIndex(actionDefinitions,
+ newConfig.get(Constants.Params.ACTION), "values");
+ Map<String, Object> vars = CollectionUtil.uncheckedCast(mergeHelper(defaultArgs,
+ actionArgs));
+ newMessages.put(entry.getKey(), newConfig);
+ newConfig.put(Constants.Keys.VARS, vars);
+ }
+
+ VarCache.messages = newMessages;
+ for (Map.Entry<String, Object> entry : VarCache.messages.entrySet()) {
+ String name = entry.getKey();
+ Map<String, Object> messageConfig = CollectionUtil.uncheckedCast(VarCache.messages.get
+ (name));
+ if (messageConfig.get("action") != null) {
+ Map<String, Object> actionArgs =
+ CollectionUtil.uncheckedCast(messageConfig.get(Constants.Keys.VARS));
+ new ActionContext(
+ messageConfig.get("action").toString(), actionArgs, name).update();
+ }
+ }
+ }
+
+ if (regions != null) {
+ VarCache.regions = regions;
+ }
+
+ if (messages != null || regions != null) {
+ Set<String> foregroundRegionNames = new HashSet<>();
+ Set<String> backgroundRegionNames = new HashSet<>();
+ ActionManager.getForegroundandBackgroundRegionNames(foregroundRegionNames,
+ backgroundRegionNames);
+ LocationManager locationManager = ActionManager.getLocationManager();
+ if (locationManager != null) {
+ locationManager.setRegionsData(regions, foregroundRegionNames, backgroundRegionNames);
+ }
+ }
+
+ boolean interfaceUpdated = false;
+ if (updateRules != null) {
+ interfaceUpdated = !(updateRules.equals(updateRuleDiffs));
+ updateRuleDiffs = new ArrayList<>(updateRules);
+ VarCache.downloadUpdateRulesImages();
+ }
+
+ boolean eventsUpdated = false;
+ if (eventRules != null) {
+ eventsUpdated = !(eventRules.equals(eventRuleDiffs));
+ eventRuleDiffs = new ArrayList<>(eventRules);
+ }
+
+ if (variants != null) {
+ VarCache.variants = variants;
+ }
+
+ contentVersion++;
+
+ if (!silent) {
+ saveDiffs();
+ triggerHasReceivedDiffs();
+
+ if (interfaceUpdated && interfaceUpdateBlock != null) {
+ interfaceUpdateBlock.updateCache();
+ }
+
+ if (eventsUpdated && eventsUpdateBlock != null) {
+ eventsUpdateBlock.updateCache();
+ }
+ }
+ }
+
+ static void applyUpdateRuleDiffs(List<Map<String, Object>> updateRuleDiffs) {
+ VarCache.updateRuleDiffs = updateRuleDiffs;
+ VarCache.downloadUpdateRulesImages();
+ if (interfaceUpdateBlock != null) {
+ interfaceUpdateBlock.updateCache();
+ }
+ VarCache.saveDiffs();
+ }
+
+ private static void downloadUpdateRulesImages() {
+ for (Map value : VarCache.updateRuleDiffs) {
+ List changes = (List) value.get("changes");
+ for (Object change : changes) {
+ Map<String, String> castedChange = CollectionUtil.uncheckedCast(change);
+ String key = castedChange.get("key");
+ if (key != null && key.contains("image")) {
+ String name = castedChange.get("value");
+ FileManager.maybeDownloadFile(true, name, null, null, null);
+ }
+ }
+ }
+ }
+
+ public static int contentVersion() {
+ return contentVersion;
+ }
+
+ @SuppressWarnings("SameParameterValue")
+ private static boolean areActionDefinitionsEqual(
+ Map<String, Object> a, Map<String, Object> b) {
+ if ((a == null || b == null) || (a.size() != b.size())) {
+ return false;
+ }
+ for (Map.Entry<String, Object> entry : a.entrySet()) {
+ Map<String, Object> aItem = CollectionUtil.uncheckedCast(entry.getValue());
+ Map<String, Object> bItem = CollectionUtil.uncheckedCast(b.get(entry.getKey()));
+ if (bItem == null || aItem == null) {
+ return false;
+ }
+
+ Object aKind = aItem.get("kind");
+ Object aValues = aItem.get("values");
+ Object aKinds = aItem.get("kinds");
+ Object aOptions = aItem.get("options");
+ if (aKind != null && !aKind.equals(bItem.get("kind")) ||
+ aValues != null && !aValues.equals(bItem.get("values")) ||
+ aKinds != null && !aKinds.equals(bItem.get("kinds")) ||
+ (aOptions == null) != (bItem.get("options") == null) ||
+ aOptions != null && aOptions.equals(bItem.get("options"))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static void triggerHasReceivedDiffs() {
+ hasReceivedDiffs = true;
+ if (updateBlock != null) {
+ updateBlock.updateCache();
+ }
+ }
+
+ static boolean sendVariablesIfChanged() {
+ return sendContentIfChanged(true, false);
+ }
+
+ static boolean sendActionsIfChanged() {
+ return sendContentIfChanged(false, true);
+ }
+
+ private static boolean sendContentIfChanged(boolean variables, boolean actions) {
+ boolean changed = false;
+ if (variables && devModeValuesFromServer != null
+ && !valuesFromClient.equals(devModeValuesFromServer)) {
+ changed = true;
+ }
+ if (actions && !areActionDefinitionsEqual(
+ actionDefinitions, devModeActionDefinitionsFromServer)) {
+ changed = true;
+ }
+
+ if (changed) {
+ HashMap<String, Object> params = new HashMap<>();
+ if (variables) {
+ params.put(Constants.Params.VARS, JsonConverter.toJson(valuesFromClient));
+ params.put(Constants.Params.KINDS, JsonConverter.toJson(defaultKinds));
+ }
+ if (actions) {
+ params.put(Constants.Params.ACTION_DEFINITIONS, JsonConverter.toJson(actionDefinitions));
+ }
+ params.put(Constants.Params.FILE_ATTRIBUTES, JsonConverter.toJson(fileAttributes));
+ Request.post(Constants.Methods.SET_VARS, params).sendIfConnected();
+ }
+
+ return changed;
+ }
+
+ static void maybeUploadNewFiles() {
+ // First check to make sure we have all the data we need
+ if (Constants.isNoop()
+ || devModeFileAttributesFromServer == null
+ || !Leanplum.hasStartedAndRegisteredAsDeveloper()
+ || !Constants.enableFileUploadingInDevelopmentMode) {
+ return;
+ }
+
+ List<String> filenames = new ArrayList<>();
+ List<JSONObject> fileData = new ArrayList<>();
+ List<InputStream> streams = new ArrayList<>();
+ int totalSize = 0;
+ for (Map.Entry<String, Object> entry : fileAttributes.entrySet()) {
+ String name = entry.getKey();
+ Map<String, Object> variationAttributes = CollectionUtil.uncheckedCast(entry.getValue());
+ Map<String, Object> serverVariationAttributes =
+ CollectionUtil.uncheckedCast(devModeFileAttributesFromServer.get(name));
+ Map<String, Object> localAttributes = CollectionUtil.uncheckedCast(variationAttributes.get
+ (""));
+ Map<String, Object> serverAttributes = CollectionUtil.uncheckedCast(
+ (serverVariationAttributes != null ? serverVariationAttributes.get("") : null));
+ if (FileManager.isNewerLocally(localAttributes, serverAttributes)) {
+ Log.v("Will upload file " + name + ". Local attributes: " +
+ localAttributes + "; server attributes: " + serverAttributes);
+
+ String hash = (String) localAttributes.get(Constants.Keys.HASH);
+ if (hash == null) {
+ hash = "";
+ }
+
+ String variationPath = FileManager.fileRelativeToAppBundle(name);
+
+ // Upload in batch if we can't put any more files in
+ if ((totalSize > Constants.Files.MAX_UPLOAD_BATCH_SIZES && filenames.size() > 0)
+ || filenames.size() >= Constants.Files.MAX_UPLOAD_BATCH_FILES) {
+ Map<String, Object> params = new HashMap<>();
+ params.put(Constants.Params.DATA, fileData.toString());
+
+ Request.post(Constants.Methods.UPLOAD_FILE, params).sendFilesNow(filenames,
+ streams);
+
+ filenames = new ArrayList<>();
+ fileData = new ArrayList<>();
+ streams = new ArrayList<>();
+ totalSize = 0;
+ }
+
+ // Add the current file to the lists and update size
+ Object size = localAttributes.get(Constants.Keys.SIZE);
+ totalSize += (Integer) size;
+ filenames.add(variationPath);
+ JSONObject fileDatum = new JSONObject();
+ try {
+ fileDatum.put(Constants.Keys.HASH, hash);
+ fileDatum.put(Constants.Keys.SIZE, localAttributes.get(Constants.Keys.SIZE) + "");
+ fileDatum.put(Constants.Keys.FILENAME, name);
+ fileData.add(fileDatum);
+ } catch (JSONException e) {
+ // HASH, SIZE, or FILENAME are null, which they never should be (they're constants).
+ Log.e("Unable to upload files.\n" + Log.getStackTraceString(e));
+ fileData.add(new JSONObject());
+ }
+ streams.add(fileStreams.get(name));
+ }
+ }
+
+ if (filenames.size() > 0) {
+ Map<String, Object> params = new HashMap<>();
+ params.put(Constants.Params.DATA, fileData.toString());
+ Request.post(Constants.Methods.UPLOAD_FILE, params).sendFilesNow(filenames, streams);
+ }
+ }
+
+ /**
+ * Sets whether values should be saved and callbacks triggered when the variable values get
+ * updated.
+ */
+ public static void setSilent(boolean silent) {
+ VarCache.silent = silent;
+ }
+
+ public static boolean silent() {
+ return silent;
+ }
+
+ public static void setDevModeValuesFromServer(Map<String, Object> values,
+ Map<String, Object> fileAttributes, Map<String, Object> actionDefinitions) {
+ devModeValuesFromServer = values;
+ devModeActionDefinitionsFromServer = actionDefinitions;
+ devModeFileAttributesFromServer = fileAttributes;
+ }
+
+ public static void onUpdate(CacheUpdateBlock block) {
+ updateBlock = block;
+ }
+
+ public static void onInterfaceUpdate(CacheUpdateBlock block) {
+ interfaceUpdateBlock = block;
+ }
+
+ public static void onEventsUpdate(CacheUpdateBlock block) {
+ eventsUpdateBlock = block;
+ }
+
+ public static List<Map<String, Object>> variants() {
+ return variants;
+ }
+
+ public static Map<String, Object> actionDefinitions() {
+ return actionDefinitions;
+ }
+
+ public static Map<String, Object> messages() {
+ return messages;
+ }
+
+ public static void registerActionDefinition(
+ String name, int kind, List<ActionArg<?>> args,
+ Map<String, Object> options) {
+ Map<String, Object> values = new HashMap<>();
+ Map<String, String> kinds = new HashMap<>();
+ List<String> order = new ArrayList<>();
+ for (ActionArg<?> arg : args) {
+ updateValues(arg.name(), getNameComponents(arg.name()),
+ arg.defaultValue(), arg.kind(), values, kinds);
+ order.add(arg.name());
+ }
+ Map<String, Object> definition = new HashMap<>();
+ definition.put("kind", kind);
+ definition.put("values", values);
+ definition.put("kinds", kinds);
+ definition.put("order", order);
+ definition.put("options", options);
+ actionDefinitions.put(name, definition);
+ }
+
+ public static <T> String kindFromValue(T defaultValue) {
+ String kind = null;
+ if (defaultValue instanceof Integer
+ || defaultValue instanceof Long
+ || defaultValue instanceof Short
+ || defaultValue instanceof Character
+ || defaultValue instanceof Byte
+ || defaultValue instanceof BigInteger) {
+ kind = Constants.Kinds.INT;
+ } else if (defaultValue instanceof Float
+ || defaultValue instanceof Double
+ || defaultValue instanceof BigDecimal) {
+ kind = Constants.Kinds.FLOAT;
+ } else if (defaultValue instanceof String) {
+ kind = Constants.Kinds.STRING;
+ } else if (defaultValue instanceof List
+ || defaultValue instanceof Array) {
+ kind = Constants.Kinds.ARRAY;
+ } else if (defaultValue instanceof Map) {
+ kind = Constants.Kinds.DICTIONARY;
+ } else if (defaultValue instanceof Boolean) {
+ kind = Constants.Kinds.BOOLEAN;
+ }
+ return kind;
+ }
+
+ static Map<String, Object> userAttributes() {
+ if (userAttributes == null) {
+ Context context = Leanplum.getContext();
+ SharedPreferences defaults = context.getSharedPreferences(LEANPLUM, Context.MODE_PRIVATE);
+ AESCrypt aesContext = new AESCrypt(Request.appId(), Request.token());
+ try {
+ userAttributes = JsonConverter.fromJson(
+ aesContext.decodePreference(defaults, Constants.Defaults.ATTRIBUTES_KEY, "{}"));
+ } catch (Exception e) {
+ Log.e("Could not load user attributes.\n" + Log.getStackTraceString(e));
+ userAttributes = new HashMap<>();
+ }
+ }
+ return userAttributes;
+ }
+
+ public static void saveUserAttributes() {
+ if (Constants.isNoop() || Request.appId() == null || userAttributes == null) {
+ return;
+ }
+ Context context = Leanplum.getContext();
+ SharedPreferences defaults = context.getSharedPreferences(LEANPLUM, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = defaults.edit();
+ // Crypt functions return input text if there was a problem.
+ String plaintext = JsonConverter.toJson(userAttributes);
+ AESCrypt aesContext = new AESCrypt(Request.appId(), Request.token());
+ editor.putString(Constants.Defaults.ATTRIBUTES_KEY, aesContext.encrypt(plaintext));
+ try {
+ editor.apply();
+ } catch (NoSuchMethodError e) {
+ editor.commit();
+ }
+ }
+
+ /**
+ * Resets the VarCache to stock state.
+ */
+ public static void reset() {
+ vars.clear();
+ fileAttributes.clear();
+ fileStreams.clear();
+ valuesFromClient.clear();
+ defaultKinds.clear();
+ actionDefinitions.clear();
+ diffs.clear();
+ messageDiffs.clear();
+ regions.clear();
+ devModeValuesFromServer = null;
+ devModeFileAttributesFromServer = null;
+ devModeActionDefinitionsFromServer = null;
+ variants.clear();
+ updateBlock = null;
+ hasReceivedDiffs = false;
+ messages = null;
+ merged = null;
+ silent = false;
+ contentVersion = 0;
+ userAttributes = null;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/internal/WebSocketClient.java
@@ -0,0 +1,290 @@
+// Copyright (c) 2009-2012 James Coglan
+// Copyright (c) 2012 Eric Butler
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the 'Software'), to deal in
+// the Software without restriction, including without limitation the rights to use,
+// copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+// Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+// Source: https://github.com/codebutler/android-websockets/blob/master/src/main/java/com/codebutler/android_websockets/WebSocketClient.java
+
+package com.leanplum.internal;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.text.TextUtils;
+import android.util.Base64;
+
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.GeneralSecurityException;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.util.List;
+
+import javax.net.SocketFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+
+import ch.boye.httpclientandroidlib.Header;
+import ch.boye.httpclientandroidlib.HttpException;
+import ch.boye.httpclientandroidlib.HttpStatus;
+import ch.boye.httpclientandroidlib.NameValuePair;
+import ch.boye.httpclientandroidlib.StatusLine;
+import ch.boye.httpclientandroidlib.client.HttpResponseException;
+import ch.boye.httpclientandroidlib.message.BasicLineParser;
+import ch.boye.httpclientandroidlib.message.BasicNameValuePair;
+
+class WebSocketClient {
+ private static final String TAG = "WebSocketClient";
+
+ private URI mURI;
+ private Listener mListener;
+ private java.net.Socket mSocket;
+ private Thread mThread;
+ private HandlerThread mHandlerThread;
+ private Handler mHandler;
+ private List<BasicNameValuePair> mExtraHeaders;
+ private HybiParser mParser;
+
+ private final Object mSendLock = new Object();
+
+ private static TrustManager[] sTrustManagers;
+
+ public static void setTrustManagers(TrustManager[] tm) {
+ sTrustManagers = tm;
+ }
+
+ public WebSocketClient(URI uri, Listener listener, List<BasicNameValuePair> extraHeaders) {
+ mURI = uri;
+ mListener = listener;
+ mExtraHeaders = extraHeaders;
+ mParser = new HybiParser(this);
+
+ mHandlerThread = new HandlerThread("websocket-thread");
+ mHandlerThread.start();
+ mHandler = new Handler(mHandlerThread.getLooper());
+ }
+
+ public Listener getListener() {
+ return mListener;
+ }
+
+ public void connect() {
+ if (mThread != null && mThread.isAlive()) {
+ return;
+ }
+
+ mThread = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ int port = (mURI.getPort() != -1) ? mURI.getPort() : (mURI.getScheme().equals("wss") ? 443 : 80);
+
+ String path = TextUtils.isEmpty(mURI.getPath()) ? "/" : mURI.getPath();
+ if (!TextUtils.isEmpty(mURI.getQuery())) {
+ path += "?" + mURI.getQuery();
+ }
+
+ String originScheme = mURI.getScheme().equals("wss") ? "https" : "http";
+ URI origin = null;
+ try {
+ origin = new URI(originScheme, "//" + mURI.getHost(), null);
+ } catch (URISyntaxException e) {
+ Util.handleException(e);
+ }
+
+ SocketFactory factory;
+ try {
+ factory = mURI.getScheme().equals("wss") ? getSSLSocketFactory() : SocketFactory.getDefault();
+ } catch (GeneralSecurityException e) {
+ Util.handleException(e);
+ return;
+ }
+ try {
+ mSocket = factory.createSocket(mURI.getHost(), port);
+ } catch (IOException e) {
+ Util.handleException(e);
+ }
+
+ PrintWriter out = new PrintWriter(mSocket.getOutputStream());
+ out.print("GET " + path + " HTTP/1.1\r\n");
+ out.print("Upgrade: websocket\r\n");
+ out.print("Connection: Upgrade\r\n");
+ out.print("Host: " + mURI.getHost() + "\r\n");
+ out.print("Origin: " + (origin != null ? origin.toString() : "unknown") + "\r\n");
+ out.print("Sec-WebSocket-Key: " + createSecret() + "\r\n");
+ out.print("Sec-WebSocket-Version: 13\r\n");
+ if (mExtraHeaders != null) {
+ for (NameValuePair pair : mExtraHeaders) {
+ out.print(String.format("%s: %s\r\n", pair.getName(), pair.getValue()));
+ }
+ }
+ out.print("\r\n");
+ out.flush();
+
+ HybiParser.HappyDataInputStream stream = new HybiParser.HappyDataInputStream(mSocket.getInputStream());
+
+ // Read HTTP response status line.
+
+ StatusLine statusLine = parseStatusLine(readLine(stream));
+
+ if (statusLine == null) {
+ throw new HttpException("Received no reply from server.");
+ } else if (statusLine.getStatusCode() != HttpStatus.SC_SWITCHING_PROTOCOLS) {
+ throw new HttpResponseException(statusLine.getStatusCode(), statusLine.getReasonPhrase());
+ }
+
+ // Read HTTP response headers.
+ String line;
+ while (!TextUtils.isEmpty(line = readLine(stream))) {
+ Header header = parseHeader(line);
+ if (header.getName().equals("Sec-WebSocket-Accept")) {
+ // FIXME: Verify the response...
+ }
+ }
+
+ mListener.onConnect();
+
+ // Now decode websocket frames.
+ mParser.start(stream);
+
+ } catch (EOFException ex) {
+ Log.d("WebSocket EOF!", ex);
+ mListener.onDisconnect(0, "EOF");
+
+ } catch (SSLException ex) {
+ // Connection reset by peer
+ Log.d("Websocket SSL error!", ex);
+ mListener.onDisconnect(0, "SSL");
+
+ } catch (Exception e) {
+ mListener.onError(e);
+ }
+ }
+ });
+ mThread.start();
+ }
+
+ public void disconnect() {
+ if (mSocket != null) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ if (mSocket != null) {
+ mSocket.close();
+ mSocket = null;
+ }
+ } catch (IOException e) {
+ Log.d("Error while disconnecting", e);
+ mListener.onError(e);
+ }
+ }
+ });
+ }
+ }
+
+ public void send(String data) {
+ sendFrame(mParser.frame(data));
+ }
+
+ public void send(byte[] data) {
+ sendFrame(mParser.frame(data));
+ }
+
+ private StatusLine parseStatusLine(String line) {
+ if (TextUtils.isEmpty(line)) {
+ return null;
+ }
+ return BasicLineParser.parseStatusLine(line, new BasicLineParser());
+ }
+
+ private Header parseHeader(String line) {
+ return BasicLineParser.parseHeader(line, new BasicLineParser());
+ }
+
+ // Can't use BufferedReader because it buffers past the HTTP data.
+ private String readLine(HybiParser.HappyDataInputStream reader) throws IOException {
+ int readChar = reader.read();
+ if (readChar == -1) {
+ return null;
+ }
+ StringBuilder string = new StringBuilder("");
+ while (readChar != '\n') {
+ if (readChar != '\r') {
+ string.append((char) readChar);
+ }
+
+ readChar = reader.read();
+ if (readChar == -1) {
+ return null;
+ }
+ }
+ return string.toString();
+ }
+
+ private String createSecret() {
+ byte[] nonce = new byte[16];
+ for (int i = 0; i < 16; i++) {
+ nonce[i] = (byte) (Math.random() * 256);
+ }
+ return Base64.encodeToString(nonce, Base64.DEFAULT).trim();
+ }
+
+ void sendFrame(final byte[] frame) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ synchronized (mSendLock) {
+ if (mSocket != null) {
+ OutputStream outputStream = mSocket.getOutputStream();
+ outputStream.write(frame);
+ outputStream.flush();
+ }
+ }
+ } catch (IOException e) {
+ mListener.onError(e);
+ }
+ }
+ });
+ }
+
+ interface Listener {
+ void onConnect();
+
+ void onMessage(String message);
+
+ void onMessage(byte[] data);
+
+ void onDisconnect(int code, String reason);
+
+ void onError(Exception error);
+ }
+
+ private SSLSocketFactory getSSLSocketFactory() throws NoSuchAlgorithmException, KeyManagementException {
+ SSLContext context = SSLContext.getInstance("TLS");
+ context.init(null, sTrustManagers, null);
+ return context.getSocketFactory();
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/messagetemplates/Alert.java
@@ -0,0 +1,88 @@
+/*
+ * 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.messagetemplates;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+
+import com.leanplum.ActionArgs;
+import com.leanplum.ActionContext;
+import com.leanplum.Leanplum;
+import com.leanplum.LeanplumActivityHelper;
+import com.leanplum.callbacks.ActionCallback;
+import com.leanplum.callbacks.PostponableAction;
+import com.leanplum.messagetemplates.MessageTemplates.Args;
+import com.leanplum.messagetemplates.MessageTemplates.Values;
+
+import static com.leanplum.messagetemplates.MessageTemplates.getApplicationName;
+
+/**
+ * Registers a Leanplum action that displays a system alert dialog.
+ *
+ * @author Andrew First
+ */
+public class Alert {
+ private static final String NAME = "Alert";
+
+ public static void register(Context currentContext) {
+ Leanplum.defineAction(
+ NAME,
+ Leanplum.ACTION_KIND_MESSAGE | Leanplum.ACTION_KIND_ACTION,
+ new ActionArgs().with(Args.TITLE, getApplicationName(currentContext))
+ .with(Args.MESSAGE, Values.ALERT_MESSAGE)
+ .with(Args.DISMISS_TEXT, Values.OK_TEXT)
+ .withAction(Args.DISMISS_ACTION, null), new ActionCallback() {
+
+ @Override
+ public boolean onResponse(final ActionContext context) {
+ LeanplumActivityHelper.queueActionUponActive(new PostponableAction() {
+ @Override
+ public void run() {
+ Activity activity = LeanplumActivityHelper.getCurrentActivity();
+ if (activity == null) {
+ return;
+ }
+ AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(
+ activity);
+ alertDialogBuilder
+ .setTitle(context.stringNamed(Args.TITLE))
+ .setMessage(context.stringNamed(Args.MESSAGE))
+ .setCancelable(false)
+ .setPositiveButton(context.stringNamed(Args.DISMISS_TEXT),
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ context.runActionNamed(Args.DISMISS_ACTION);
+ }
+ });
+ AlertDialog alertDialog = alertDialogBuilder.create();
+ if (!activity.isFinishing()) {
+ alertDialog.show();
+ }
+ }
+ });
+ return true;
+ }
+ });
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/messagetemplates/BaseMessageDialog.java
@@ -0,0 +1,615 @@
+/*
+ * 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.messagetemplates;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Point;
+import android.graphics.Typeface;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.shapes.RoundRectShape;
+import android.graphics.drawable.shapes.Shape;
+import android.os.Build;
+import android.os.Handler;
+import android.text.TextUtils;
+import android.util.TypedValue;
+import android.view.Display;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup.LayoutParams;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.DecelerateInterpolator;
+import android.webkit.WebChromeClient;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import com.leanplum.ActionContext;
+import com.leanplum.Leanplum;
+import com.leanplum.utils.BitmapUtil;
+import com.leanplum.utils.SizeUtil;
+import com.leanplum.views.BackgroundImageView;
+import com.leanplum.views.CloseButton;
+
+import org.json.JSONObject;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.util.Map;
+
+/**
+ * Base dialog used to display the Center Popup, Interstitial, Web Interstitial, HTML template.
+ *
+ * @author Martin Yanakiev, Anna Orlova
+ */
+public class BaseMessageDialog extends Dialog {
+ protected RelativeLayout dialogView;
+ protected BaseMessageOptions options;
+ protected WebInterstitialOptions webOptions;
+ protected HTMLOptions htmlOptions;
+ protected Activity activity;
+ protected WebView webView;
+
+ private boolean isWeb = false;
+ private boolean isHtml = false;
+ private boolean isClosing = false;
+
+ protected BaseMessageDialog(Activity activity, boolean fullscreen, BaseMessageOptions options,
+ WebInterstitialOptions webOptions, HTMLOptions htmlOptions) {
+ super(activity, getTheme(activity));
+
+ SizeUtil.init(activity);
+ this.activity = activity;
+ this.options = options;
+ this.webOptions = webOptions;
+ this.htmlOptions = htmlOptions;
+ if (webOptions != null) {
+ isWeb = true;
+ }
+ if (htmlOptions != null) {
+ isHtml = true;
+ }
+ dialogView = new RelativeLayout(activity);
+ RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
+ LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+ dialogView.setBackgroundColor(Color.TRANSPARENT);
+ dialogView.setLayoutParams(layoutParams);
+
+ RelativeLayout view = createContainerView(activity, fullscreen);
+ view.setId(108);
+ dialogView.addView(view, view.getLayoutParams());
+
+ if ((!isWeb || (webOptions != null && webOptions.hasDismissButton())) && !isHtml) {
+ CloseButton closeButton = createCloseButton(activity, fullscreen, view);
+ dialogView.addView(closeButton, closeButton.getLayoutParams());
+ }
+
+ setContentView(dialogView, dialogView.getLayoutParams());
+
+ dialogView.setAnimation(createFadeInAnimation());
+
+ if (!fullscreen) {
+ Window window = getWindow();
+ if (window == null) {
+ return;
+ }
+ if (!isHtml) {
+ window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+ if (Build.VERSION.SDK_INT >= 14) {
+ window.setDimAmount(0.7f);
+ }
+ } else {
+ window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+ window.setFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
+ WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
+ if (htmlOptions != null &&
+ MessageTemplates.Args.HTML_ALIGN_BOTTOM.equals(htmlOptions.getHtmlAlign())) {
+ dialogView.setGravity(Gravity.BOTTOM);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasFocus) {
+ try {
+ if (webView != null && Build.VERSION.SDK_INT >= 11) {
+ if (hasFocus) {
+ webView.onResume();
+ } else {
+ webView.onPause();
+ }
+ }
+ } catch (Throwable ignore) {
+ }
+ super.onWindowFocusChanged(hasFocus);
+ }
+
+ private Animation createFadeInAnimation() {
+ Animation fadeIn = new AlphaAnimation(0, 1);
+ fadeIn.setInterpolator(new DecelerateInterpolator());
+ fadeIn.setDuration(350);
+ return fadeIn;
+ }
+
+ private Animation createFadeOutAnimation() {
+ Animation fadeOut = new AlphaAnimation(1, 0);
+ fadeOut.setInterpolator(new AccelerateInterpolator());
+ fadeOut.setDuration(350);
+ return fadeOut;
+ }
+
+ @Override
+ public void cancel() {
+ if (isClosing) {
+ return;
+ }
+ isClosing = true;
+ Animation animation = createFadeOutAnimation();
+ animation.setAnimationListener(new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ BaseMessageDialog.super.cancel();
+ Handler handler = new Handler();
+ handler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ if (isHtml && webView != null) {
+ webView.stopLoading();
+ webView.loadUrl("");
+ if (dialogView != null) {
+ dialogView.removeAllViews();
+ }
+ webView.removeAllViews();
+ webView.destroy();
+ }
+ }
+ }, 10);
+ }
+ });
+ dialogView.startAnimation(animation);
+ }
+
+ private CloseButton createCloseButton(Activity context, boolean fullscreen, View parent) {
+ CloseButton closeButton = new CloseButton(context);
+ closeButton.setId(103);
+ RelativeLayout.LayoutParams closeLayout = new RelativeLayout.LayoutParams(
+ LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ if (fullscreen) {
+ closeLayout.addRule(RelativeLayout.ALIGN_PARENT_TOP, dialogView.getId());
+ closeLayout.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, dialogView.getId());
+ closeLayout.setMargins(0, SizeUtil.dp5, SizeUtil.dp5, 0);
+ } else {
+ closeLayout.addRule(RelativeLayout.ALIGN_TOP, parent.getId());
+ closeLayout.addRule(RelativeLayout.ALIGN_RIGHT, parent.getId());
+ closeLayout.setMargins(0, -SizeUtil.dp7, -SizeUtil.dp7, 0);
+ }
+ closeButton.setLayoutParams(closeLayout);
+ closeButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View arg0) {
+ cancel();
+ }
+ });
+ return closeButton;
+ }
+
+ @SuppressWarnings("deprecation")
+ private RelativeLayout createContainerView(Activity context, boolean fullscreen) {
+ RelativeLayout view = new RelativeLayout(context);
+
+ // Positions the dialog.
+ RelativeLayout.LayoutParams layoutParams;
+ if (fullscreen) {
+ layoutParams = new RelativeLayout.LayoutParams(
+ LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+ } else if (isHtml) {
+ int height = SizeUtil.dpToPx(context, htmlOptions.getHtmlHeight());
+ layoutParams = new RelativeLayout.LayoutParams(
+ LayoutParams.MATCH_PARENT, height);
+ } else {
+
+ // Make sure the dialog fits on screen.
+ Display display = context.getWindowManager().getDefaultDisplay();
+ Point size = new Point();
+ if (Build.VERSION.SDK_INT >= 13) {
+ display.getSize(size);
+ } else {
+ size = new Point(display.getHeight(), display.getHeight());
+ }
+
+ int width = SizeUtil.dpToPx(context, ((CenterPopupOptions) options).getWidth());
+ int height = SizeUtil.dpToPx(context, ((CenterPopupOptions) options).getHeight());
+
+ int maxWidth = size.x - SizeUtil.dp20;
+ int maxHeight = size.y - SizeUtil.dp20;
+ double aspectRatio = width / (double) height;
+ if (width > maxWidth && (int) (width / aspectRatio) < maxHeight) {
+ width = maxWidth;
+ height = (int) (width / aspectRatio);
+ }
+ if (height > maxHeight && (int) (height * aspectRatio) < maxWidth) {
+ height = maxHeight;
+ width = (int) (height * aspectRatio);
+ }
+
+ layoutParams = new RelativeLayout.LayoutParams(width, height);
+ layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE);
+ }
+
+ view.setLayoutParams(layoutParams);
+
+ ShapeDrawable footerBackground = new ShapeDrawable();
+ footerBackground.setShape(createRoundRect(fullscreen ? 0 : SizeUtil.dp20));
+ footerBackground.getPaint().setColor(0x00000000);
+ if (Build.VERSION.SDK_INT >= 16) {
+ view.setBackground(footerBackground);
+ } else {
+ view.setBackgroundDrawable(footerBackground);
+ }
+
+ if (!isWeb && !isHtml) {
+ ImageView image = createBackgroundImageView(context, fullscreen);
+ view.addView(image, image.getLayoutParams());
+
+ View title = createTitleView(context);
+ title.setId(104);
+ view.addView(title, title.getLayoutParams());
+
+ View button = createAcceptButton(context);
+ button.setId(105);
+ view.addView(button, button.getLayoutParams());
+
+ View message = createMessageView(context);
+ ((RelativeLayout.LayoutParams) message.getLayoutParams())
+ .addRule(RelativeLayout.BELOW, title.getId());
+ ((RelativeLayout.LayoutParams) message.getLayoutParams())
+ .addRule(RelativeLayout.ABOVE, button.getId());
+ view.addView(message, message.getLayoutParams());
+ } else if (isWeb) {
+ WebView webView = createWebView(context);
+ view.addView(webView, webView.getLayoutParams());
+ } else {
+ webView = createHtml(context);
+ view.addView(webView, webView.getLayoutParams());
+ }
+
+ return view;
+ }
+
+ private Shape createRoundRect(int cornerRadius) {
+ int c = cornerRadius;
+ float[] outerRadii = new float[] {c, c, c, c, c, c, c, c};
+ return new RoundRectShape(outerRadii, null, null);
+ }
+
+ @SuppressWarnings("deprecation")
+ private ImageView createBackgroundImageView(Context context, boolean fullscreen) {
+ BackgroundImageView view = new BackgroundImageView(context, fullscreen);
+ view.setScaleType(ImageView.ScaleType.CENTER_CROP);
+ int cornerRadius;
+ if (!fullscreen) {
+ cornerRadius = SizeUtil.dp20;
+ } else {
+ cornerRadius = 0;
+ }
+ view.setImageBitmap(options.getBackgroundImage());
+ ShapeDrawable footerBackground = new ShapeDrawable();
+ footerBackground.setShape(createRoundRect(cornerRadius));
+ footerBackground.getPaint().setColor(options.getBackgroundColor());
+ if (Build.VERSION.SDK_INT >= 16) {
+ view.setBackground(footerBackground);
+ } else {
+ view.setBackgroundDrawable(footerBackground);
+ }
+ RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
+ LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+ view.setLayoutParams(layoutParams);
+ return view;
+ }
+
+ private RelativeLayout createTitleView(Context context) {
+ RelativeLayout view = new RelativeLayout(context);
+ view.setLayoutParams(new LayoutParams(
+ LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
+
+ TextView title = new TextView(context);
+ title.setPadding(0, SizeUtil.dp5, 0, SizeUtil.dp5);
+ title.setGravity(Gravity.CENTER);
+ title.setText(options.getTitle());
+ title.setTextColor(options.getTitleColor());
+ title.setTextSize(TypedValue.COMPLEX_UNIT_SP, SizeUtil.textSize0);
+ title.setTypeface(null, Typeface.BOLD);
+ RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
+ LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+ layoutParams.addRule(RelativeLayout.CENTER_HORIZONTAL, RelativeLayout.TRUE);
+ layoutParams.addRule(RelativeLayout.CENTER_VERTICAL, RelativeLayout.TRUE);
+ title.setLayoutParams(layoutParams);
+
+ view.addView(title, title.getLayoutParams());
+ return view;
+ }
+
+ private TextView createMessageView(Context context) {
+ TextView view = new TextView(context);
+ RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
+ LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+ view.setLayoutParams(layoutParams);
+ view.setGravity(Gravity.CENTER);
+ view.setText(options.getMessageText());
+ view.setTextColor(options.getMessageColor());
+ view.setTextSize(TypedValue.COMPLEX_UNIT_SP, SizeUtil.textSize0_1);
+ return view;
+ }
+
+ private WebView createWebView(Context context) {
+ WebView view = new WebView(context);
+ RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
+ LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+ view.setLayoutParams(layoutParams);
+ view.setWebViewClient(new WebViewClient() {
+ @SuppressWarnings("deprecation")
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView wView, String url) {
+ if (url.contains(webOptions.getCloseUrl())) {
+ cancel();
+ String[] urlComponents = url.split("\\?");
+ if (urlComponents.length > 1) {
+ String queryString = urlComponents[1];
+ String[] parameters = queryString.split("&");
+ for (String parameter : parameters) {
+ String[] parameterComponents = parameter.split("=");
+ if (parameterComponents.length > 1 && parameterComponents[0].equals("result")) {
+ Leanplum.track(parameterComponents[1]);
+ }
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+ });
+ view.loadUrl(webOptions.getUrl());
+ return view;
+ }
+
+ /**
+ * Create WebView with HTML template.
+ *
+ * @param context Current context.
+ * @return WebVew WebVew with HTML template.
+ */
+ @SuppressLint("SetJavaScriptEnabled")
+ private WebView createHtml(Context context) {
+ dialogView.setVisibility(View.GONE);
+ final WebView webView = new WebView(context);
+ webView.setBackgroundColor(Color.TRANSPARENT);
+ webView.setVerticalScrollBarEnabled(false);
+ webView.setHorizontalScrollBarEnabled(false);
+ webView.setOnTouchListener(new View.OnTouchListener() {
+ public boolean onTouch(View v, MotionEvent event) {
+ return (event.getAction() == MotionEvent.ACTION_MOVE);
+ }
+ });
+ webView.canGoBack();
+ // Disable long click.
+ webView.setLongClickable(false);
+ webView.setHapticFeedbackEnabled(false);
+ webView.setOnLongClickListener(new View.OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View v) {
+ return true;
+ }
+ });
+
+ WebSettings webViewSettings = webView.getSettings();
+ if (Build.VERSION.SDK_INT >= 17) {
+ webViewSettings.setMediaPlaybackRequiresUserGesture(false);
+ }
+ webViewSettings.setAppCacheEnabled(true);
+ webViewSettings.getSaveFormData();
+ webViewSettings.setAllowFileAccess(true);
+ webViewSettings.setJavaScriptEnabled(true);
+ webViewSettings.setDomStorageEnabled(true);
+ webViewSettings.setJavaScriptCanOpenWindowsAutomatically(true);
+ webViewSettings.setLoadWithOverviewMode(true);
+ webViewSettings.setLoadsImagesAutomatically(true);
+
+ if (Build.VERSION.SDK_INT >= 16) {
+ webViewSettings.setAllowFileAccessFromFileURLs(true);
+ webViewSettings.setAllowUniversalAccessFromFileURLs(true);
+ }
+ if (Build.VERSION.SDK_INT >= 11) {
+ webViewSettings.setBuiltInZoomControls(false);
+ webViewSettings.setDisplayZoomControls(false);
+ }
+ webViewSettings.setSupportZoom(false);
+
+ RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
+ LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+ webView.setLayoutParams(layoutParams);
+ final Dialog currentDialog = this;
+ webView.setWebChromeClient(new WebChromeClient());
+ webView.setWebViewClient(new WebViewClient() {
+ @SuppressWarnings("deprecation")
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView wView, String url) {
+ // Open URL event.
+ if (url.contains(htmlOptions.getOpenUrl())) {
+ dialogView.setVisibility(View.VISIBLE);
+ if (activity != null && !activity.isFinishing()) {
+ currentDialog.show();
+ }
+ return true;
+ }
+
+ // Close URL event.
+ if (url.contains(htmlOptions.getCloseUrl())) {
+ cancel();
+ String queryComponentsFromUrl = queryComponentsFromUrl(url, "result");
+ if (!TextUtils.isEmpty(queryComponentsFromUrl)) {
+ Leanplum.track(queryComponentsFromUrl);
+ }
+ return true;
+ }
+
+ // Track URL event.
+ if (url.contains(htmlOptions.getTrackUrl())) {
+ String eventName = queryComponentsFromUrl(url, "event");
+ if (!TextUtils.isEmpty(eventName)) {
+ Double value = Double.parseDouble(queryComponentsFromUrl(url, "value"));
+ String info = queryComponentsFromUrl(url, "info");
+ Map<String, Object> paramsMap = null;
+
+ try {
+ paramsMap = ActionContext.mapFromJson(new JSONObject(queryComponentsFromUrl(url,
+ "parameters")));
+ } catch (Exception ignored) {
+ }
+
+ if (queryComponentsFromUrl(url, "isMessageEvent").equals("true")) {
+ ActionContext actionContext = htmlOptions.getActionContext();
+ actionContext.trackMessageEvent(eventName, value, info, paramsMap);
+ } else {
+ Leanplum.track(eventName, value, info, paramsMap);
+ }
+ }
+ return true;
+ }
+
+ // Action URL or track action URL event.
+ if (url.contains(htmlOptions.getActionUrl()) ||
+ url.contains(htmlOptions.getTrackActionUrl())) {
+ cancel();
+ String queryComponentsFromUrl = queryComponentsFromUrl(url, "action");
+ try {
+ queryComponentsFromUrl = URLDecoder.decode(queryComponentsFromUrl, "UTF-8");
+ } catch (UnsupportedEncodingException ignored) {
+ }
+
+ ActionContext actionContext = htmlOptions.getActionContext();
+ if (!TextUtils.isEmpty(queryComponentsFromUrl) && actionContext != null) {
+ if (url.contains(htmlOptions.getActionUrl())) {
+ actionContext.runActionNamed(queryComponentsFromUrl);
+ } else {
+ actionContext.runTrackedActionNamed(queryComponentsFromUrl);
+ }
+ }
+ return true;
+ }
+
+ return false;
+ }
+ });
+ String html = htmlOptions.getHtmlTemplate();
+
+ webView.loadDataWithBaseURL(null, html, "text/html", "UTF-8", null);
+
+ return webView;
+ }
+
+ /**
+ * Get query components from URL.
+ *
+ * @param url URL string.
+ * @param components Name of components.
+ * @return String String with query components.
+ */
+ private String queryComponentsFromUrl(String url, String components) {
+ String componentsFromUrl = "";
+ String[] urlComponents = url.split("\\?");
+ if (urlComponents.length > 1) {
+ String queryString = urlComponents[1];
+ String[] parameters = queryString.split("&");
+ for (String parameter : parameters) {
+ String[] parameterComponents = parameter.split("=");
+ if (parameterComponents.length > 1 && parameterComponents[0].equals(components)) {
+ componentsFromUrl = parameterComponents[1];
+ }
+ }
+ }
+ try {
+ componentsFromUrl = URLDecoder.decode(componentsFromUrl, "UTF-8");
+ } catch (Exception ignored) {
+ }
+ return componentsFromUrl;
+ }
+
+ private TextView createAcceptButton(Context context) {
+ TextView view = new TextView(context);
+ RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(
+ LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ layoutParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM, RelativeLayout.TRUE);
+ layoutParams.addRule(RelativeLayout.CENTER_HORIZONTAL, RelativeLayout.TRUE);
+ layoutParams.setMargins(0, 0, 0, SizeUtil.dp5);
+
+ view.setPadding(SizeUtil.dp20, SizeUtil.dp5, SizeUtil.dp20, SizeUtil.dp5);
+ view.setLayoutParams(layoutParams);
+ view.setText(options.getAcceptButtonText());
+ view.setTextColor(options.getAcceptButtonTextColor());
+ view.setTypeface(null, Typeface.BOLD);
+
+ BitmapUtil.stateBackgroundDarkerByPercentage(view,
+ options.getAcceptButtonBackgroundColor(), 30);
+
+ view.setTextSize(TypedValue.COMPLEX_UNIT_SP, SizeUtil.textSize0_1);
+ view.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View arg0) {
+ if (!isClosing) {
+ options.accept();
+ cancel();
+ }
+ }
+ });
+ return view;
+ }
+
+ private static int getTheme(Activity activity) {
+ boolean full = (activity.getWindow().getAttributes().flags &
+ WindowManager.LayoutParams.FLAG_FULLSCREEN) == WindowManager.LayoutParams.FLAG_FULLSCREEN;
+ if (full) {
+ return android.R.style.Theme_Translucent_NoTitleBar_Fullscreen;
+ } else {
+ return android.R.style.Theme_Translucent_NoTitleBar;
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/messagetemplates/BaseMessageOptions.java
@@ -0,0 +1,170 @@
+/*
+ * 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.messagetemplates;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Color;
+import android.util.Log;
+
+import com.leanplum.ActionArgs;
+import com.leanplum.ActionContext;
+import com.leanplum.messagetemplates.MessageTemplates.Args;
+import com.leanplum.utils.BitmapUtil;
+
+import java.io.InputStream;
+
+/**
+ * Options used by Center Popup and Interstitial.
+ *
+ * @author Martin Yanakiev
+ */
+abstract class BaseMessageOptions {
+ private ActionContext context;
+ private String title;
+ private int titleColor;
+ private String messageText;
+ private int messageColor;
+ private Bitmap backgroundImage;
+ private int backgroundColor;
+ private String acceptButtonText;
+ private int acceptButtonBackgroundColor;
+ private int acceptButtonTextColor;
+
+ protected BaseMessageOptions(ActionContext context) {
+ this.context = context;
+ setTitle(context.stringNamed(Args.TITLE_TEXT));
+ setTitleColor(context.numberNamed(Args.TITLE_COLOR).intValue());
+ setMessageText(context.stringNamed(Args.MESSAGE_TEXT));
+ setMessageColor(context.numberNamed(Args.MESSAGE_COLOR).intValue());
+ InputStream imageStream = context.streamNamed(Args.BACKGROUND_IMAGE);
+ if (imageStream != null) {
+ try {
+ setBackgroundImage(BitmapFactory.decodeStream(imageStream));
+ } catch (Exception e) {
+ Log.e("Leanplum", "Error loading background image", e);
+ }
+ }
+ setBackgroundColor(context.numberNamed(Args.BACKGROUND_COLOR).intValue());
+ setAcceptButtonText(context.stringNamed(Args.ACCEPT_BUTTON_TEXT));
+ setAcceptButtonBackgroundColor(context.numberNamed(
+ Args.ACCEPT_BUTTON_BACKGROUND_COLOR).intValue());
+ setAcceptButtonTextColor(context.numberNamed(
+ Args.ACCEPT_BUTTON_TEXT_COLOR).intValue());
+ }
+
+ public int getBackgroundColor() {
+ return backgroundColor;
+ }
+
+ private void setBackgroundColor(int backgroundColor) {
+ this.backgroundColor = backgroundColor;
+ }
+
+ public String getAcceptButtonText() {
+ return acceptButtonText;
+ }
+
+ private void setAcceptButtonText(String acceptButtonText) {
+ this.acceptButtonText = acceptButtonText;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ private void setTitle(String title) {
+ this.title = title;
+ }
+
+ public int getTitleColor() {
+ return titleColor;
+ }
+
+ private void setTitleColor(int titleColor) {
+ this.titleColor = titleColor;
+ }
+
+ public String getMessageText() {
+ return messageText;
+ }
+
+ private void setMessageText(String messageText) {
+ this.messageText = messageText;
+ }
+
+ public int getMessageColor() {
+ return messageColor;
+ }
+
+ private void setMessageColor(int messageColor) {
+ this.messageColor = messageColor;
+ }
+
+ public Bitmap getBackgroundImage() {
+ return backgroundImage;
+ }
+
+ public Bitmap getBackgroundImageRounded(int pixels) {
+ return BitmapUtil.getRoundedCornerBitmap(backgroundImage, pixels);
+ }
+
+ private void setBackgroundImage(Bitmap backgroundImage) {
+ this.backgroundImage = backgroundImage;
+ }
+
+ public int getAcceptButtonBackgroundColor() {
+ return acceptButtonBackgroundColor;
+ }
+
+ private void setAcceptButtonBackgroundColor(int color) {
+ this.acceptButtonBackgroundColor = color;
+ }
+
+ public int getAcceptButtonTextColor() {
+ return acceptButtonTextColor;
+ }
+
+ private void setAcceptButtonTextColor(int color) {
+ this.acceptButtonTextColor = color;
+ }
+
+ public void accept() {
+ context.runTrackedActionNamed(Args.ACCEPT_ACTION);
+ }
+
+ public static ActionArgs toArgs(Context currentContext) {
+ return new ActionArgs()
+ .with(Args.TITLE_TEXT,
+ MessageTemplates.getApplicationName(currentContext))
+ .withColor(Args.TITLE_COLOR, Color.BLACK)
+ .with(Args.MESSAGE_TEXT, MessageTemplates.Values.POPUP_MESSAGE)
+ .withColor(Args.MESSAGE_COLOR, Color.BLACK)
+ .withFile(Args.BACKGROUND_IMAGE, null)
+ .withColor(Args.BACKGROUND_COLOR, Color.WHITE)
+ .with(Args.ACCEPT_BUTTON_TEXT, MessageTemplates.Values.OK_TEXT)
+ .withColor(Args.ACCEPT_BUTTON_BACKGROUND_COLOR, Color.WHITE)
+ .withColor(Args.ACCEPT_BUTTON_TEXT_COLOR, Color.argb(255, 0, 122, 255))
+ .withAction(Args.ACCEPT_ACTION, null);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/messagetemplates/CenterPopup.java
@@ -0,0 +1,76 @@
+/*
+ * 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.messagetemplates;
+
+import android.app.Activity;
+import android.content.Context;
+
+import com.leanplum.ActionContext;
+import com.leanplum.Leanplum;
+import com.leanplum.LeanplumActivityHelper;
+import com.leanplum.callbacks.ActionCallback;
+import com.leanplum.callbacks.PostponableAction;
+import com.leanplum.callbacks.VariablesChangedCallback;
+
+/**
+ * Registers a Leanplum action that displays a custom center popup dialog.
+ *
+ * @author Andrew First
+ */
+public class CenterPopup extends BaseMessageDialog {
+ private static final String NAME = "Center Popup";
+
+ public CenterPopup(Activity activity, CenterPopupOptions options) {
+ super(activity, false, options, null, null);
+ this.options = options;
+ }
+
+ public static void register(Context currentContext) {
+ Leanplum.defineAction(NAME, Leanplum.ACTION_KIND_MESSAGE | Leanplum.ACTION_KIND_ACTION,
+ CenterPopupOptions.toArgs(currentContext), new ActionCallback() {
+ @Override
+ public boolean onResponse(final ActionContext context) {
+ Leanplum.addOnceVariablesChangedAndNoDownloadsPendingHandler(
+ new VariablesChangedCallback() {
+ @Override
+ public void variablesChanged() {
+ LeanplumActivityHelper.queueActionUponActive(new PostponableAction() {
+ @Override
+ public void run() {
+ Activity activity = LeanplumActivityHelper.getCurrentActivity();
+ if (activity == null) {
+ return;
+ }
+ CenterPopup popup = new CenterPopup(activity,
+ new CenterPopupOptions(context));
+ if (!activity.isFinishing()) {
+ popup.show();
+ }
+ }
+ });
+ }
+ });
+ return true;
+ }
+ });
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/messagetemplates/CenterPopupOptions.java
@@ -0,0 +1,66 @@
+/*
+ * 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.messagetemplates;
+
+import android.content.Context;
+
+import com.leanplum.ActionArgs;
+import com.leanplum.ActionContext;
+import com.leanplum.messagetemplates.MessageTemplates.Args;
+
+/**
+ * Options used by {@link CenterPopup}.
+ *
+ * @author Martin Yanakiev
+ */
+public class CenterPopupOptions extends BaseMessageOptions {
+ private int width;
+ private int height;
+
+ public CenterPopupOptions(ActionContext context) {
+ super(context);
+ setWidth(context.numberNamed(Args.LAYOUT_WIDTH).intValue());
+ setHeight(context.numberNamed(Args.LAYOUT_HEIGHT).intValue());
+ }
+
+ public int getWidth() {
+ return width;
+ }
+
+ private void setWidth(int width) {
+ this.width = width;
+ }
+
+ public int getHeight() {
+ return height;
+ }
+
+ private void setHeight(int height) {
+ this.height = height;
+ }
+
+ public static ActionArgs toArgs(Context currentContext) {
+ return BaseMessageOptions.toArgs(currentContext)
+ .with(Args.LAYOUT_WIDTH, MessageTemplates.Values.CENTER_POPUP_WIDTH)
+ .with(Args.LAYOUT_HEIGHT, MessageTemplates.Values.CENTER_POPUP_HEIGHT);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/messagetemplates/Confirm.java
@@ -0,0 +1,93 @@
+/*
+ * 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.messagetemplates;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+
+import com.leanplum.ActionArgs;
+import com.leanplum.ActionContext;
+import com.leanplum.Leanplum;
+import com.leanplum.LeanplumActivityHelper;
+import com.leanplum.callbacks.ActionCallback;
+import com.leanplum.callbacks.PostponableAction;
+import com.leanplum.messagetemplates.MessageTemplates.Args;
+import com.leanplum.messagetemplates.MessageTemplates.Values;
+
+import static com.leanplum.messagetemplates.MessageTemplates.getApplicationName;
+
+/**
+ * Registers a Leanplum action that displays a system confirm dialog.
+ *
+ * @author Andrew First
+ */
+class Confirm {
+ private static final String NAME = "Confirm";
+
+ public static void register(Context currentContext) {
+ Leanplum.defineAction(
+ NAME,
+ Leanplum.ACTION_KIND_MESSAGE | Leanplum.ACTION_KIND_ACTION,
+ new ActionArgs().with(Args.TITLE, getApplicationName(currentContext))
+ .with(Args.MESSAGE, Values.CONFIRM_MESSAGE)
+ .with(Args.ACCEPT_TEXT, Values.YES_TEXT)
+ .with(Args.CANCEL_TEXT, Values.NO_TEXT)
+ .withAction(Args.ACCEPT_ACTION, null)
+ .withAction(Args.CANCEL_ACTION, null), new ActionCallback() {
+
+ @Override
+ public boolean onResponse(final ActionContext context) {
+ LeanplumActivityHelper.queueActionUponActive(new PostponableAction() {
+ @Override
+ public void run() {
+ Activity activity = LeanplumActivityHelper.getCurrentActivity();
+ AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(
+ activity);
+ alertDialogBuilder
+ .setTitle(context.stringNamed(Args.TITLE))
+ .setMessage(context.stringNamed(Args.MESSAGE))
+ .setCancelable(false)
+ .setPositiveButton(context.stringNamed(Args.ACCEPT_TEXT),
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ context.runTrackedActionNamed(Args.ACCEPT_ACTION);
+ }
+ })
+ .setNegativeButton(context.stringNamed(Args.CANCEL_TEXT),
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ context.runActionNamed(Args.CANCEL_ACTION);
+ }
+ });
+ AlertDialog alertDialog = alertDialogBuilder.create();
+ if (activity != null && !activity.isFinishing()) {
+ alertDialog.show();
+ }
+ }
+ });
+ return true;
+ }
+ });
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/messagetemplates/HTMLOptions.java
@@ -0,0 +1,270 @@
+/*
+ * 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.messagetemplates;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.leanplum.ActionArgs;
+import com.leanplum.ActionContext;
+import com.leanplum.Leanplum;
+
+import org.json.JSONException;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Map;
+
+/**
+ * Options used by {@link HTMLTemplate}.
+ *
+ * @author Anna Orlova
+ */
+class HTMLOptions {
+ private String closeUrl;
+ private String openUrl;
+ private String trackUrl;
+ private String actionUrl;
+ private String trackActionUrl;
+ private String htmlTemplate;
+ private ActionContext actionContext;
+ private String htmlAlign;
+ private int htmlHeight;
+
+ HTMLOptions(ActionContext context) {
+ this.setActionContext(context);
+ this.setHtmlTemplate(getTemplate(context));
+ this.setCloseUrl(context.stringNamed(MessageTemplates.Args.CLOSE_URL));
+ this.setOpenUrl(context.stringNamed(MessageTemplates.Args.OPEN_URL));
+ this.setTrackUrl(context.stringNamed(MessageTemplates.Args.TRACK_URL));
+ this.setActionUrl(context.stringNamed(MessageTemplates.Args.ACTION_URL));
+ this.setTrackActionUrl(context.stringNamed(MessageTemplates.Args.TRACK_ACTION_URL));
+ this.setHtmlAlign(context.stringNamed(MessageTemplates.Args.HTML_ALIGN));
+ this.setHtmlHeight(context.numberNamed(MessageTemplates.Args.HTML_HEIGHT).intValue());
+ }
+
+ /**
+ * Read data from file as String.
+ *
+ * @param context ActionContext.
+ * @param name Name of file.
+ * @return String String with data of file.
+ */
+ @SuppressWarnings("SameParameterValue")
+ private static String readFileAsString(ActionContext context, String name) {
+ if (context == null || TextUtils.isEmpty(name)) {
+ return null;
+ }
+
+ String str;
+ InputStream inputStream = context.streamNamed(name);
+ StringBuilder buf = new StringBuilder();
+ BufferedReader reader = null;
+
+ try {
+ reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
+ while ((str = reader.readLine()) != null) {
+ buf.append(str).append("\n");
+ }
+ reader.close();
+ } catch (IOException e) {
+ Log.e("Leanplum", "Fail to get HTML template.");
+ } finally {
+ try {
+ if (inputStream != null) {
+ inputStream.close();
+ }
+ if (reader != null) {
+ reader.close();
+ }
+ } catch (Exception e) {
+ Log.w("Leanplum", "Failed to close InputStream or BufferedReader: " + e);
+ }
+ }
+ return buf.toString();
+ }
+
+ /**
+ * Replace all keys with __file__ prefix to keys without __file__ prefix and replace value of
+ * those keys with local path to file.
+ *
+ * @param map Map with arguments from ActionContext.
+ * @param htmlTemplateName Name of file with HTML template.
+ * @return Map Map with updated arguments.
+ */
+ private static Map<String, Object> replaceFileToLocalPath(Map<String, Object> map,
+ String htmlTemplateName) {
+ if (map == null) {
+ return null;
+ }
+ String[] keyArray = map.keySet().toArray(new String[map.keySet().size()]);
+ for (String key : keyArray) {
+ if (map.get(key) instanceof Map) {
+ @SuppressWarnings("unchecked")
+ Map<String, Object> mapValue = (Map<String, Object>) map.get(key);
+ replaceFileToLocalPath(mapValue, htmlTemplateName);
+ } else if (key.contains(MessageTemplates.Values.FILE_PREFIX) &&
+ !key.equals(htmlTemplateName)) {
+ String filePath = ActionContext.filePath((String) map.get(key));
+ if (filePath == null) {
+ continue;
+ }
+ File f = new File(filePath);
+ String localPath = "file://" + f.getAbsolutePath();
+ if (localPath.contains(Leanplum.getContext().getPackageName())) {
+ map.put(key.replace(MessageTemplates.Values.FILE_PREFIX, ""),
+ localPath.replace(" ", "%20"));
+ }
+ map.remove(key);
+ }
+ }
+ return map;
+ }
+
+ /**
+ * Get HTML template file.
+ *
+ * @param context ActionContext.
+ * @return String String with data of HTML template file.
+ */
+ private static String getTemplate(ActionContext context) {
+ if (context == null) {
+ return null;
+ }
+
+ String htmlTemplate = readFileAsString(context, MessageTemplates.Values.HTML_TEMPLATE_PREFIX);
+ Map<String, Object> htmlArgs = replaceFileToLocalPath(context.getArgs(),
+ MessageTemplates.Values.HTML_TEMPLATE_PREFIX);
+ if (htmlArgs == null || TextUtils.isEmpty(htmlTemplate)) {
+ return null;
+ }
+
+ htmlArgs.put("messageId", context.getMessageId());
+ if (context.getContextualValues() != null && context.getContextualValues().arguments != null) {
+ htmlArgs.put("displayEvent", context.getContextualValues().arguments);
+ }
+
+ String htmlString = "";
+ try {
+ htmlString = (htmlTemplate.replace("##Vars##",
+ ActionContext.mapToJsonObject(htmlArgs).toString()));
+ } catch (JSONException e) {
+ Log.e("Leanplum", "Cannot convert map of arguments to JSON object.");
+ }
+ return htmlString.replace("\\/", "/");
+ }
+
+ /**
+ * @return boolean True if it's full screen template.
+ */
+ boolean isFullScreen() {
+ return htmlHeight == 0;
+ }
+
+ int getHtmlHeight() {
+ return htmlHeight;
+ }
+
+ private void setHtmlHeight(int htmlHeight) {
+ this.htmlHeight = htmlHeight;
+ }
+
+ String getHtmlAlign() {
+ return htmlAlign;
+ }
+
+ private void setHtmlAlign(String htmlAlign) {
+ this.htmlAlign = htmlAlign;
+ }
+
+ ActionContext getActionContext() {
+ return actionContext;
+ }
+
+ private void setActionContext(ActionContext actionContext) {
+ //noinspection AccessStaticViaInstance
+ this.actionContext = actionContext;
+ }
+
+ String getHtmlTemplate() {
+ return htmlTemplate;
+ }
+
+ private void setHtmlTemplate(String htmlTemplate) {
+ this.htmlTemplate = htmlTemplate;
+ }
+
+ String getTrackActionUrl() {
+ return trackActionUrl;
+ }
+
+ private void setTrackActionUrl(String trackActionUrl) {
+ this.trackActionUrl = trackActionUrl;
+ }
+
+ String getTrackUrl() {
+ return trackUrl;
+ }
+
+ private void setTrackUrl(String trackUrl) {
+ this.trackUrl = trackUrl;
+ }
+
+ String getOpenUrl() {
+ return openUrl;
+ }
+
+ private void setOpenUrl(String openUrl) {
+ this.openUrl = openUrl;
+ }
+
+ String getActionUrl() {
+ return actionUrl;
+ }
+
+ private void setActionUrl(String actionUrl) {
+ this.actionUrl = actionUrl;
+ }
+
+ String getCloseUrl() {
+ return closeUrl;
+ }
+
+ private void setCloseUrl(String closeUrl) {
+ this.closeUrl = closeUrl;
+ }
+
+ public static ActionArgs toArgs() {
+ return new ActionArgs()
+ .with(MessageTemplates.Args.CLOSE_URL, MessageTemplates.Values.DEFAULT_CLOSE_URL)
+ .with(MessageTemplates.Args.OPEN_URL, MessageTemplates.Values.DEFAULT_OPEN_URL)
+ .with(MessageTemplates.Args.ACTION_URL, MessageTemplates.Values.DEFAULT_ACTION_URL)
+ .with(MessageTemplates.Args.TRACK_ACTION_URL,
+ MessageTemplates.Values.DEFAULT_TRACK_ACTION_URL)
+ .with(MessageTemplates.Args.TRACK_URL, MessageTemplates.Values.DEFAULT_TRACK_URL)
+ .with(MessageTemplates.Args.HTML_ALIGN, MessageTemplates.Values.DEFAULT_HTML_ALING)
+ .with(MessageTemplates.Args.HTML_HEIGHT, MessageTemplates.Values.DEFAULT_HTML_HEIGHT);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/messagetemplates/HTMLTemplate.java
@@ -0,0 +1,96 @@
+/*
+ * 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.messagetemplates;
+
+import android.app.Activity;
+import android.support.annotation.NonNull;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import com.leanplum.ActionContext;
+import com.leanplum.Leanplum;
+import com.leanplum.LeanplumActivityHelper;
+import com.leanplum.callbacks.ActionCallback;
+import com.leanplum.callbacks.PostponableAction;
+import com.leanplum.callbacks.VariablesChangedCallback;
+
+/**
+ * Registers a Leanplum action that displays a HTML message.
+ *
+ * @author Anna Orlova
+ */
+@SuppressWarnings("WeakerAccess")
+public class HTMLTemplate extends BaseMessageDialog {
+ private static final String NAME = "HTML";
+
+ public HTMLTemplate(Activity activity, HTMLOptions htmlOptions) {
+ super(activity, htmlOptions.isFullScreen(), null, null, htmlOptions);
+ this.htmlOptions = htmlOptions;
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(@NonNull MotionEvent ev) {
+ if (!htmlOptions.isFullScreen()) {
+ if (htmlOptions.getHtmlAlign().equals(MessageTemplates.Args.HTML_ALIGN_TOP) && ev.getY()
+ > htmlOptions.getHtmlHeight() ||
+ htmlOptions.getHtmlAlign().equals(MessageTemplates.Args.HTML_ALIGN_BOTTOM) && ev.getY()
+ < dialogView.getHeight() - htmlOptions.getHtmlHeight()) {
+ activity.dispatchTouchEvent(ev);
+ }
+ }
+ return super.dispatchTouchEvent(ev);
+ }
+
+ public static void register() {
+ Leanplum.defineAction(NAME, Leanplum.ACTION_KIND_MESSAGE | Leanplum.ACTION_KIND_ACTION,
+ HTMLOptions.toArgs(), new ActionCallback() {
+ @Override
+ public boolean onResponse(final ActionContext context) {
+ Leanplum.addOnceVariablesChangedAndNoDownloadsPendingHandler(
+ new VariablesChangedCallback() {
+ @Override
+ public void variablesChanged() {
+ LeanplumActivityHelper.queueActionUponActive(
+ new PostponableAction() {
+ @Override
+ public void run() {
+ try {
+ HTMLOptions htmlOptions = new HTMLOptions(context);
+ if (htmlOptions.getHtmlTemplate() == null) {
+ return;
+ }
+ final Activity activity = LeanplumActivityHelper.getCurrentActivity();
+ if (activity != null && !activity.isFinishing()) {
+ new HTMLTemplate(activity, htmlOptions);
+ }
+ } catch (Throwable t) {
+ Log.e("Leanplum", "Fail on show HTML In-App message.", t);
+ }
+ }
+ });
+ }
+ });
+ return true;
+ }
+ });
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/messagetemplates/Interstitial.java
@@ -0,0 +1,78 @@
+/*
+ * 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.messagetemplates;
+
+import android.app.Activity;
+import android.content.Context;
+
+import com.leanplum.ActionContext;
+import com.leanplum.Leanplum;
+import com.leanplum.LeanplumActivityHelper;
+import com.leanplum.callbacks.ActionCallback;
+import com.leanplum.callbacks.PostponableAction;
+import com.leanplum.callbacks.VariablesChangedCallback;
+
+/**
+ * Registers a Leanplum action that displays a fullscreen interstitial.
+ *
+ * @author Andrew First
+ */
+public class Interstitial extends BaseMessageDialog {
+ private static final String NAME = "Interstitial";
+
+ public Interstitial(Activity activity, InterstitialOptions options) {
+ super(activity, true, options, null, null);
+ this.options = options;
+ }
+
+ public static void register(Context currentContext) {
+ Leanplum.defineAction(NAME, Leanplum.ACTION_KIND_MESSAGE | Leanplum.ACTION_KIND_ACTION,
+ InterstitialOptions.toArgs(currentContext),
+ new ActionCallback() {
+ @Override
+ public boolean onResponse(final ActionContext context) {
+ Leanplum.addOnceVariablesChangedAndNoDownloadsPendingHandler(
+ new VariablesChangedCallback() {
+ @Override
+ public void variablesChanged() {
+ LeanplumActivityHelper.queueActionUponActive(
+ new PostponableAction() {
+ @Override
+ public void run() {
+ Activity activity = LeanplumActivityHelper.getCurrentActivity();
+ if (activity == null) {
+ return;
+ }
+ Interstitial interstitial = new Interstitial(activity,
+ new InterstitialOptions(context));
+ if (!activity.isFinishing()) {
+ interstitial.show();
+ }
+ }
+ });
+ }
+ });
+ return true;
+ }
+ });
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/messagetemplates/InterstitialOptions.java
@@ -0,0 +1,45 @@
+/*
+ * 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.messagetemplates;
+
+import android.content.Context;
+
+import com.leanplum.ActionArgs;
+import com.leanplum.ActionContext;
+
+/**
+ * Options used by {@link Interstitial}.
+ *
+ * @author Martin Yanakiev
+ */
+public class InterstitialOptions extends BaseMessageOptions {
+ public InterstitialOptions(ActionContext context) {
+ super(context);
+ // Set specific properties for interstitial popup.
+ }
+
+ public static ActionArgs toArgs(Context currentContext) {
+ return BaseMessageOptions.toArgs(currentContext)
+ .with(MessageTemplates.Args.MESSAGE_TEXT, MessageTemplates.Values.INTERSTITIAL_MESSAGE);
+ // Add specific args for interstitial popup.
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/messagetemplates/MessageTemplates.java
@@ -0,0 +1,127 @@
+/*
+ * 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.messagetemplates;
+
+import android.content.Context;
+
+/**
+ * Registers all of the built-in message templates.
+ *
+ * @author Andrew First
+ */
+public class MessageTemplates {
+ static class Args {
+ // Open URL
+ static final String URL = "URL";
+
+ // Alert/confirm arguments.
+ static final String TITLE = "Title";
+ static final String MESSAGE = "Message";
+ static final String ACCEPT_TEXT = "Accept text";
+ static final String CANCEL_TEXT = "Cancel text";
+ static final String DISMISS_TEXT = "Dismiss text";
+ static final String ACCEPT_ACTION = "Accept action";
+ static final String CANCEL_ACTION = "Cancel action";
+ static final String DISMISS_ACTION = "Dismiss action";
+
+ // Center popup/interstitial arguments.
+ static final String TITLE_TEXT = "Title.Text";
+ static final String TITLE_COLOR = "Title.Color";
+ static final String MESSAGE_TEXT = "Message.Text";
+ static final String MESSAGE_COLOR = "Message.Color";
+ static final String ACCEPT_BUTTON_TEXT = "Accept button.Text";
+ static final String ACCEPT_BUTTON_BACKGROUND_COLOR = "Accept button.Background color";
+ static final String ACCEPT_BUTTON_TEXT_COLOR = "Accept button.Text color";
+ static final String BACKGROUND_IMAGE = "Background image";
+ static final String BACKGROUND_COLOR = "Background color";
+ static final String LAYOUT_WIDTH = "Layout.Width";
+ static final String LAYOUT_HEIGHT = "Layout.Height";
+ static final String HTML_HEIGHT = "HTML Height";
+ static final String HTML_ALIGN = "HTML Align";
+ static final String HTML_ALIGN_TOP = "Top";
+ static final String HTML_ALIGN_BOTTOM = "Bottom";
+
+ // Web interstitial arguments.
+ static final String CLOSE_URL = "Close URL";
+ static final String HAS_DISMISS_BUTTON = "Has dismiss button";
+
+ // HTML Template arguments.
+ static final String OPEN_URL = "Open URL";
+ static final String TRACK_URL = "Track URL";
+ static final String ACTION_URL = "Action URL";
+ static final String TRACK_ACTION_URL = "Track Action URL";
+ }
+
+ static class Values {
+ static final String ALERT_MESSAGE = "Alert message goes here.";
+ static final String CONFIRM_MESSAGE = "Confirmation message goes here.";
+ static final String POPUP_MESSAGE = "Popup message goes here.";
+ static final String INTERSTITIAL_MESSAGE = "Interstitial message goes here.";
+ static final String OK_TEXT = "OK";
+ static final String YES_TEXT = "Yes";
+ static final String NO_TEXT = "No";
+ static final int CENTER_POPUP_WIDTH = 300;
+ static final int CENTER_POPUP_HEIGHT = 250;
+ static final int DEFAULT_HTML_HEIGHT = 0;
+ static final String DEFAULT_HTML_ALING = Args.HTML_ALIGN_TOP;
+
+ // Open URL.
+ static final String DEFAULT_URL = "http://www.example.com";
+
+ // Web interstitial values.
+ static final String DEFAULT_CLOSE_URL = "http://leanplum:close";
+ static final boolean DEFAULT_HAS_DISMISS_BUTTON = true;
+
+ // HTML Template values.
+ public static final String FILE_PREFIX = "__file__";
+ public static final String HTML_TEMPLATE_PREFIX = "__file__Template";
+ static final String DEFAULT_OPEN_URL = "http://leanplum:loadFinished";
+ static final String DEFAULT_TRACK_URL = "http://leanplum:track";
+ static final String DEFAULT_ACTION_URL = "http://leanplum:runAction";
+ static final String DEFAULT_TRACK_ACTION_URL = "http://leanplum:runTrackedAction";
+
+ }
+
+ private static boolean registered = false;
+
+ static String getApplicationName(Context context) {
+ int stringId = context.getApplicationInfo().labelRes;
+ if (stringId == 0) {
+ return context.getApplicationInfo().loadLabel(context.getPackageManager()).toString();
+ }
+ return context.getString(stringId);
+ }
+
+ public synchronized static void register(Context currentContext) {
+ if (registered) {
+ return;
+ }
+ registered = true;
+ OpenURL.register();
+ Alert.register(currentContext);
+ Confirm.register(currentContext);
+ CenterPopup.register(currentContext);
+ Interstitial.register(currentContext);
+ WebInterstitial.register();
+ HTMLTemplate.register();
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/messagetemplates/OpenURL.java
@@ -0,0 +1,111 @@
+/*
+ * 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.messagetemplates;
+
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.util.Log;
+
+import com.leanplum.ActionArgs;
+import com.leanplum.ActionContext;
+import com.leanplum.Leanplum;
+import com.leanplum.LeanplumActivityHelper;
+import com.leanplum.callbacks.ActionCallback;
+import com.leanplum.callbacks.PostponableAction;
+import com.leanplum.messagetemplates.MessageTemplates.Args;
+import com.leanplum.messagetemplates.MessageTemplates.Values;
+
+import java.util.List;
+
+/**
+ * Registers a Leanplum action that opens a particular URL. If the URL cannot be handled by the
+ * system URL handler, you can add your own action responder using {@link Leanplum#onAction} that
+ * handles the URL how you want.
+ *
+ * @author Andrew First
+ */
+class OpenURL {
+ private static final String NAME = "Open URL";
+
+ public static void register() {
+ Leanplum.defineAction(NAME, Leanplum.ACTION_KIND_ACTION,
+ new ActionArgs().with(Args.URL, Values.DEFAULT_URL), new ActionCallback() {
+ @Override
+ public boolean onResponse(ActionContext context) {
+ String url = context.stringNamed(Args.URL);
+ final Intent uriIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+ // Calling startActivity() from outside of an Activity context requires the
+ // FLAG_ACTIVITY_NEW_TASK flag.
+ if (!(Leanplum.getContext() instanceof Activity)) {
+ uriIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ }
+ try {
+ if (Leanplum.getContext() != null) {
+ LeanplumActivityHelper.queueActionUponActive(
+ new PostponableAction() {
+ @Override
+ public void run() {
+ Context context = Leanplum.getContext();
+ if (context == null) {
+ return;
+ }
+ List<ResolveInfo> resolveInfoList = context.getPackageManager().
+ queryIntentActivities(uriIntent, 0);
+ // If url can be handled by current app - set package name to intent, so url
+ // will be open by current app. Skip chooser dialog.
+ if (resolveInfoList != null && resolveInfoList.size() != 0) {
+ for (ResolveInfo resolveInfo : resolveInfoList) {
+ if (resolveInfo != null && resolveInfo.activityInfo != null &&
+ resolveInfo.activityInfo.name != null) {
+ if (resolveInfo.activityInfo.name.contains(
+ context.getPackageName())) {
+ uriIntent.setPackage(resolveInfo.activityInfo.packageName);
+ }
+ }
+ }
+ try {
+ // Even if we have valid destination, startActivity can crash if
+ // activity we are trying to open is not exported in manifest.
+ context.startActivity(uriIntent);
+ } catch (ActivityNotFoundException e) {
+ Log.e("Leanplum", "Activity you are trying to start doesn't exist or " +
+ "isn't exported in manifest: " + e);
+ }
+ }
+ }
+ });
+ return true;
+ } else {
+ return false;
+ }
+ } catch (ActivityNotFoundException e) {
+ Log.e("Leanplum", "Unable to handle URL " + url);
+ return false;
+ }
+ }
+ });
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/messagetemplates/WebInterstitial.java
@@ -0,0 +1,78 @@
+/*
+ * 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.messagetemplates;
+
+import android.app.Activity;
+import android.content.Context;
+
+import com.leanplum.ActionContext;
+import com.leanplum.Leanplum;
+import com.leanplum.LeanplumActivityHelper;
+import com.leanplum.callbacks.ActionCallback;
+import com.leanplum.callbacks.PostponableAction;
+
+/**
+ * Registers a Leanplum action that displays a fullscreen Web Interstitial.
+ *
+ * @author Atanas Dobrev
+ */
+public class WebInterstitial extends BaseMessageDialog {
+ private static final String NAME = "Web Interstitial";
+
+ public WebInterstitial(Activity activity, WebInterstitialOptions options) {
+ super(activity, true, null, options, null);
+ this.webOptions = options;
+ }
+
+ /**
+ * Deprecated: Use {@link WebInterstitial#register()}.
+ */
+ @Deprecated
+ @SuppressWarnings("unused")
+ public static void register(Context currentContext) {
+ register();
+ }
+
+ public static void register() {
+ Leanplum.defineAction(NAME, Leanplum.ACTION_KIND_MESSAGE | Leanplum.ACTION_KIND_ACTION,
+ WebInterstitialOptions.toArgs(), new ActionCallback() {
+ @Override
+ public boolean onResponse(final ActionContext context) {
+ LeanplumActivityHelper.queueActionUponActive(new PostponableAction() {
+ @Override
+ public void run() {
+ Activity activity = LeanplumActivityHelper.getCurrentActivity();
+ if (activity == null) {
+ return;
+ }
+ WebInterstitial webInterstitial = new WebInterstitial(activity,
+ new WebInterstitialOptions(context));
+ if (!activity.isFinishing()) {
+ webInterstitial.show();
+ }
+ }
+ });
+ return true;
+ }
+ });
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/messagetemplates/WebInterstitialOptions.java
@@ -0,0 +1,87 @@
+/*
+ * 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.messagetemplates;
+
+import android.content.Context;
+
+import com.leanplum.ActionArgs;
+import com.leanplum.ActionContext;
+import com.leanplum.messagetemplates.MessageTemplates.Args;
+import com.leanplum.messagetemplates.MessageTemplates.Values;
+
+/**
+ * Options used by {@link WebInterstitial}.
+ *
+ * @author Atanas Dobrev
+ */
+@SuppressWarnings("WeakerAccess")
+public class WebInterstitialOptions {
+ private String url;
+ private String closeUrl;
+ private boolean hasDismissButton;
+
+ protected WebInterstitialOptions(ActionContext context) {
+ this.setUrl(context.stringNamed(Args.URL));
+ this.setHasDismissButton(context.booleanNamed(Args.HAS_DISMISS_BUTTON));
+ this.setCloseUrl(context.stringNamed(Args.CLOSE_URL));
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ private void setUrl(String url) {
+ this.url = url;
+ }
+
+ public boolean hasDismissButton() {
+ return hasDismissButton;
+ }
+
+ private void setHasDismissButton(boolean hasDismissButton) {
+ this.hasDismissButton = hasDismissButton;
+ }
+
+ public String getCloseUrl() {
+ return closeUrl;
+ }
+
+ private void setCloseUrl(String closeUrl) {
+ this.closeUrl = closeUrl;
+ }
+
+ /**
+ * Deprecated: Use {@link WebInterstitialOptions#toArgs()}.
+ */
+ @Deprecated
+ @SuppressWarnings("unused")
+ public static ActionArgs toArgs(Context currentContext) {
+ return toArgs();
+ }
+
+ public static ActionArgs toArgs() {
+ return new ActionArgs()
+ .with(Args.URL, Values.DEFAULT_URL)
+ .with(Args.CLOSE_URL, Values.DEFAULT_CLOSE_URL)
+ .with(Args.HAS_DISMISS_BUTTON, Values.DEFAULT_HAS_DISMISS_BUTTON);
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/utils/BitmapUtil.java
@@ -0,0 +1,241 @@
+/*
+ * 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.utils;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.BitmapFactory;
+import android.graphics.PorterDuff.Mode;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.StateListDrawable;
+import android.graphics.drawable.shapes.RoundRectShape;
+import android.os.Build;
+import android.util.DisplayMetrics;
+import android.view.View;
+import android.view.WindowManager;
+
+import com.leanplum.internal.Log;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+
+/**
+ * Bitmap manipulation utilities.
+ *
+ * @author Martin Yanakiev, Anna Orlova
+ */
+public class BitmapUtil {
+ // Layout_height for imageView is 192dp
+ // https://android.googlesource.com/platform/frameworks/base
+ // /+/6387d2f6dae27ba6e8481883325adad96d3010f4/core/res/res/layout
+ // /notification_template_big_picture.xml
+ private static final int BIG_PICTURE_MAX_HEIGHT_DP = 192;
+
+ public static Bitmap getRoundedCornerBitmap(Bitmap bitmap, int pixels) {
+ if (bitmap == null) {
+ return null;
+ }
+
+ Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(),
+ Config.ARGB_8888);
+ android.graphics.Canvas canvas = new android.graphics.Canvas(output);
+
+ final int color = 0xff000000;
+ final android.graphics.Paint paint = new android.graphics.Paint();
+ final android.graphics.Rect rect = new android.graphics.Rect(0, 0,
+ bitmap.getWidth(), bitmap.getHeight());
+ final android.graphics.RectF rectF = new android.graphics.RectF(rect);
+
+ paint.setAntiAlias(true);
+ canvas.drawARGB(0, 0, 0, 0);
+ paint.setColor(color);
+ canvas.drawRoundRect(rectF, pixels, pixels, paint);
+
+ paint.setXfermode(new android.graphics.PorterDuffXfermode(Mode.SRC_IN));
+ canvas.drawBitmap(bitmap, rect, rect, paint);
+
+ return output;
+ }
+
+ public static void stateBackgroundDarkerByPercentage(View v,
+ int normalStateColor, int percentage) {
+ int darker = getDarker(normalStateColor, percentage);
+ stateBackground(v, normalStateColor, darker);
+ }
+
+ public static int getDarker(int color, int percentage) {
+ if (percentage < 0 || percentage > 100)
+ percentage = 0;
+ double d = ((100 - percentage) / (double) 100);
+ int a = (color >> 24) & 0xFF;
+ int r = (int) (((color >> 16) & 0xFF) * d) & 0xFF;
+ int g = (int) (((color >> 8) & 0xFF) * d) & 0xFF;
+ int b = (int) ((color & 0xFF) * d) & 0xFF;
+ a = a << 24;
+ r = r << 16;
+ g = g << 8;
+ return a | r | g | b;
+ }
+
+ public static void stateBackground(View v, int normalStateColor,
+ int pressedStateColor) {
+ if (Build.VERSION.SDK_INT >= 16) {
+ v.setBackground(getBackground(normalStateColor, pressedStateColor));
+ } else {
+ v.setBackgroundColor(normalStateColor);
+ }
+ }
+
+ private static Drawable getBackground(int normalStateColor,
+ int pressedStateColor) {
+ StateListDrawable background = new StateListDrawable();
+ int c = SizeUtil.dp10;
+ float[] r = new float[] {c, c, c, c, c, c, c, c};
+ RoundRectShape rr = new RoundRectShape(r, null, null);
+ ShapeDrawable cd = new ShapeDrawable();
+ cd.setShape(rr);
+ cd.getPaint().setColor(pressedStateColor);
+ background.addState(new int[] {android.R.attr.state_pressed,
+ android.R.attr.state_focused}, cd);
+ background.addState(new int[] {-android.R.attr.state_pressed,
+ android.R.attr.state_focused}, cd);
+ background.addState(new int[] {android.R.attr.state_pressed,
+ -android.R.attr.state_focused}, cd);
+ ShapeDrawable cd1 = new ShapeDrawable();
+ cd1.setShape(rr);
+ cd1.getPaint().setColor(normalStateColor);
+ background.addState(new int[] {-android.R.attr.state_pressed,
+ -android.R.attr.state_focused}, cd1);
+ return background;
+ }
+
+ /**
+ * Method to calculate a sample size value that is a power of two based on a target width and
+ * height. From official Android documentation:
+ * https://developer.android.com/training/displaying-bitmaps/load-bitmap.html
+ *
+ * @param reqWidth The requested width of the image.
+ * @param reqHeight The requested height of the image.
+ * @return The calculated inSampleSize - power of two.
+ */
+ private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth,
+ int reqHeight) {
+ // Raw height and width of image.
+ final int height = options.outHeight;
+ final int width = options.outWidth;
+ int inSampleSize = 1;
+
+ if (height > reqHeight || width > reqWidth) {
+ final int halfHeight = height / 2;
+ final int halfWidth = width / 2;
+
+ // Calculate the largest inSampleSize value that is a power of 2 and keeps both
+ // height and width larger than the requested height and width.
+ while ((halfHeight / inSampleSize) >= reqHeight
+ && (halfWidth / inSampleSize) >= reqWidth) {
+ inSampleSize *= 2;
+ }
+ }
+
+ return inSampleSize;
+ }
+
+ /**
+ * Download a scaled bitmap.
+ *
+ * @param imageUrl The string of URL image.
+ * @param width The requested width of the image.
+ * @param height The requested height of the image.
+ * @return The scaled bitmap downloaded form URL.
+ */
+ private static Bitmap getBitmapFromUrl(String imageUrl, int width, int height) {
+ InputStream input = null;
+ try {
+ input = new URL(imageUrl).openStream();
+
+ // First decode with inJustDecodeBounds=true to check dimensions.
+ final BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeStream(input, null, options);
+
+ closeStream(input);
+
+ input = new URL(imageUrl).openStream();
+ options.inSampleSize = calculateInSampleSize(options, width, height);
+ // Decode bitmap with inSampleSize set.
+ options.inJustDecodeBounds = false;
+ return BitmapFactory.decodeStream(input, null, options);
+ } catch (IOException e) {
+ Log.e(String.format("IOException in image download for URL: %s.", imageUrl), e);
+ return null;
+ } finally {
+ closeStream(input);
+ }
+ }
+
+ /**
+ * Create a scaled bitmap.
+ *
+ * @param context The application context.
+ * @param imageUrl The string of URL image.
+ * @return The scaled bitmap.
+ */
+ public static Bitmap getScaledBitmap(Context context, String imageUrl) {
+ // Processing an image depending on the current screen size to avoid central crop if the image
+ // ratio is more than 2:1. Google aspect ~2:1 - page 78
+ // http://commondatastorage.googleapis.com/io2012/presentations/live%20to%20website/105.pdf
+ WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ DisplayMetrics displayMetrics = new DisplayMetrics();
+ windowManager.getDefaultDisplay().getMetrics(displayMetrics);
+
+ int pixelsHeight = Math.round(displayMetrics.density * BIG_PICTURE_MAX_HEIGHT_DP + 0.5f);
+ int pixelsWidth = Math.min(2 * pixelsHeight, displayMetrics.widthPixels);
+
+ Bitmap bitmap = getBitmapFromUrl(imageUrl, pixelsWidth, pixelsHeight);
+ try {
+ bitmap = Bitmap.createScaledBitmap(bitmap, pixelsWidth, pixelsHeight, true);
+ } catch (Exception e) {
+ Log.e("Failed on scale image " + imageUrl + " to (" + pixelsWidth + ", " + pixelsHeight + ")",
+ e);
+ }
+
+ return bitmap;
+ }
+
+ /**
+ * Method to close InputStream.
+ *
+ * @param inputStream The InputStream which must be closed.
+ */
+ private static void closeStream(InputStream inputStream) {
+ try {
+ if (inputStream != null) {
+ inputStream.close();
+ }
+ } catch (IOException e) {
+ Log.e("IOException during closing of image download stream.", e);
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/utils/SharedPreferencesUtil.java
@@ -0,0 +1,78 @@
+/*
+ * 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.utils;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+/**
+ * Shared preferences manipulation utilities.
+ *
+ * @author Anna Orlova
+ */
+public class SharedPreferencesUtil {
+ public static final String DEFAULT_STRING_VALUE = "";
+
+ /**
+ * Gets string value for key from shared preferences.
+ *
+ * @param context Application context.
+ * @param sharedPreferenceName Shared preference name.
+ * @param key Key of preference.
+ * @return String Value for key, if here no value - return DEFAULT_STRING_VALUE.
+ */
+ public static String getString(Context context, String sharedPreferenceName, String key) {
+ final SharedPreferences sharedPreferences = getPreferences(context, sharedPreferenceName);
+ return sharedPreferences.getString(key, DEFAULT_STRING_VALUE);
+ }
+
+ /**
+ * Get application shared preferences with sharedPreferenceName name.
+ *
+ * @param context Application context.
+ * @param sharedPreferenceName Shared preference name.
+ * @return Application's {@code SharedPreferences}.
+ */
+ private static SharedPreferences getPreferences(Context context, String sharedPreferenceName) {
+ return context.getSharedPreferences(sharedPreferenceName, Context.MODE_PRIVATE);
+ }
+
+ /**
+ * Sets string value for provided key to shared preference with sharedPreferenceName name.
+ *
+ * @param context application context.
+ * @param sharedPreferenceName shared preference name.
+ * @param key key of preference.
+ * @param value value of preference.
+ */
+ public static void setString(Context context, String sharedPreferenceName, String key,
+ String value) {
+ final SharedPreferences sharedPreferences = getPreferences(context, sharedPreferenceName);
+ SharedPreferences.Editor editor = sharedPreferences.edit();
+ editor.putString(key, value);
+ try {
+ editor.apply();
+ } catch (NoSuchMethodError e) {
+ editor.commit();
+ }
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/utils/SizeUtil.java
@@ -0,0 +1,117 @@
+/*
+ * 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.utils;
+
+import android.app.Activity;
+import android.content.Context;
+import android.util.DisplayMetrics;
+import android.view.WindowManager;
+
+/**
+ * Utilities for converting between different size units.
+ *
+ * @author Martin Yanakiev
+ */
+public class SizeUtil {
+ public static int dp2;
+ public static int dp30;
+ public static int dp5;
+ public static int dp20;
+ public static int dp18;
+ public static int dp16;
+ public static int dp14;
+ public static int dp10;
+ public static int dp7;
+ public static int dp100;
+ public static int dp200;
+ public static int dp250;
+ public static int dp50;
+
+ public static final int textSize0_2 = 16;
+ public static final int textSize0_1 = 18;
+ public static final int textSize0 = 20;
+ public static final int textSize1 = 22;
+ public static final int textSize2 = 24;
+
+ private static boolean hasInited = false;
+
+ public static void init(Context context) {
+ if (hasInited) {
+ return;
+ }
+ hasInited = true;
+ dp30 = dpToPx(context, 30);
+ dp5 = dpToPx(context, 5);
+ dp20 = dpToPx(context, 20);
+ dp10 = dpToPx(context, 10);
+ dp7 = dpToPx(context, 7);
+ dp18 = dpToPx(context, 18);
+ dp16 = dpToPx(context, 16);
+ dp14 = dpToPx(context, 14);
+ dp100 = dpToPx(context, 100);
+ dp200 = dpToPx(context, 200);
+ dp250 = dpToPx(context, 250);
+ dp2 = dpToPx(context, 2);
+ dp50 = dpToPx(context, 50);
+ }
+
+ public static int dpToPx(Context context, int dp) {
+ init(context);
+ DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+ return Math.round(dp
+ * (displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT));
+ }
+
+ public static int pxToDp(Context context, int px) {
+ init(context);
+ DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+ return Math.round(px
+ / (displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT));
+ }
+
+ public static int spToPx(Context context, int sp) {
+ init(context);
+ DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+ return (int) (sp * displayMetrics.scaledDensity);
+ }
+
+ public static int pxToSp(Context context, int px) {
+ init(context);
+ DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+ return (int) (px / displayMetrics.scaledDensity);
+ }
+
+ public static int getStatusBarHeight(Activity activity) {
+ init(activity);
+ boolean full = (activity.getWindow().getAttributes().flags & WindowManager.LayoutParams.FLAG_FULLSCREEN) == WindowManager.LayoutParams.FLAG_FULLSCREEN;
+ if (full) {
+ return 0;
+ }
+ int result = 0;
+ int resourceId = activity.getResources().getIdentifier("status_bar_height",
+ "dimen", "android");
+ if (resourceId > 0) {
+ result = activity.getResources().getDimensionPixelSize(resourceId);
+ }
+ return result;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/views/BackgroundImageView.java
@@ -0,0 +1,101 @@
+/*
+ * 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.views;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.PorterDuff.Mode;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.RelativeLayout.LayoutParams;
+
+import com.leanplum.utils.BitmapUtil;
+import com.leanplum.utils.SizeUtil;
+
+/**
+ * The image background on a Center Popup or Interstitial dialog.
+ *
+ * @author Martin Yanakiev
+ */
+public class BackgroundImageView extends ImageView {
+ private Paint paint = new Paint();
+ private boolean fullscreen;
+ private Matrix emptyMatrix = new Matrix();
+ private boolean loadedBitmap;
+
+ public BackgroundImageView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init();
+ }
+
+ public BackgroundImageView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public BackgroundImageView(Context context, boolean fullscreen) {
+ super(context);
+ init();
+ this.fullscreen = fullscreen;
+ }
+
+ private void init() {
+ paint.setColor(0xFF00FF00);
+ paint.setStrokeWidth(2);
+ paint.setStyle(Style.FILL_AND_STROKE);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (fullscreen) {
+ return;
+ }
+ if (loadedBitmap) {
+ loadedBitmap = false;
+ return;
+ }
+ Bitmap bitmap = loadBitmapFromView(this);
+ canvas.drawColor(Color.TRANSPARENT, Mode.CLEAR);
+ bitmap = BitmapUtil.getRoundedCornerBitmap(bitmap, SizeUtil.dp20);
+ canvas.drawBitmap(bitmap, emptyMatrix, paint);
+ }
+
+ public Bitmap loadBitmapFromView(View view) {
+ if (view.getMeasuredHeight() <= 0) {
+ view.measure(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+ }
+ Bitmap bitmap = Bitmap.createBitmap(view.getMeasuredWidth(), view.getMeasuredHeight(),
+ Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
+ loadedBitmap = true;
+ view.draw(canvas);
+ return bitmap;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/thirdparty/com/leanplum/views/CloseButton.java
@@ -0,0 +1,114 @@
+/*
+ * 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.views;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.leanplum.utils.SizeUtil;
+
+/**
+ * The close button on a Center Popup or Interstitial dialog.
+ *
+ * @author Martin Yanakiev
+ */
+public class CloseButton extends View {
+ private Paint circlePaint = new Paint();
+ private Paint circlePressedPaint = new Paint();
+ private Paint linePaint = new Paint();
+ private float size;
+ private float x1;
+ private float y1;
+ private float x2;
+ private float y2;
+ private boolean isPressed = false;
+
+ public CloseButton(Context context) {
+ super(context);
+ initLabelView();
+ }
+
+ public CloseButton(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initLabelView();
+ }
+
+ private void initLabelView() {
+ circlePaint.setAntiAlias(true);
+ circlePaint.setColor(0xFFDDDDDD);
+ circlePaint.setStrokeWidth(2);
+ circlePaint.setStyle(Style.FILL_AND_STROKE);
+ circlePressedPaint.setAntiAlias(true);
+ circlePressedPaint.setColor(0xFF999999);
+ circlePressedPaint.setStrokeWidth(2);
+ circlePressedPaint.setStyle(Style.FILL_AND_STROKE);
+ linePaint.setAntiAlias(true);
+ linePaint.setColor(0xFF000000);
+ linePaint.setStrokeWidth(3);
+ linePaint.setStyle(Style.FILL_AND_STROKE);
+ size = SizeUtil.dp30;
+ x1 = size * (2 / (float) 6);
+ x2 = size * (4 / (float) 6);
+ y1 = size * (2 / (float) 6);
+ y2 = size * (4 / (float) 6);
+ }
+
+ @Override
+ public boolean performClick() {
+ return super.performClick();
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ isPressed = true;
+ invalidate();
+ return true;
+ } else if (event.getAction() == MotionEvent.ACTION_UP) {
+ isPressed = false;
+ invalidate();
+ performClick();
+ return true;
+ }
+ return super.onTouchEvent(event);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, widthMeasureSpec);
+ setMeasuredDimension((int) size, (int) size);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ Paint backgroundPaint = isPressed ? circlePressedPaint : circlePaint;
+ canvas.drawCircle(getWidth() / 2f, getHeight() / 2f, (getWidth() / 2f) - 1, backgroundPaint);
+ canvas.drawLine(x1, y1, x2, y2, linePaint);
+ canvas.drawLine(x2, y1, x1, y2, linePaint);
+ }
+}