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