handledMessageIDs = new ArrayDeque<>(MAX_HANDLED_MESSAGE_IDS_COUNT);
+
@Override
public void onReceive(Context context, Intent intent) {
if (intent == null) {
- Logger.internal(TAG, "null intent");
+ Logger.internal(TAG, "null intent. Ignoring.");
return;
}
- String msgType = intent.getStringExtra("message_type");
- if (msgType == null || "gcm".equalsIgnoreCase(msgType)) {
- // Android O forbids us to directly start a service in the background, so use JobScheduler
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
- try {
- JobScheduler scheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
- if (scheduler == null) {
- Logger.internal(TAG, "Could not get Job Scheduler system service");
- return;
- }
-
- final Bundle intentExtras = intent.getExtras();
- if (intentExtras == null || intentExtras.isEmpty()) {
- Logger.internal(
- TAG,
- "Intent extras were empty, not scheduling push notification presenter job"
- );
- return;
- }
-
- final Bundle jobExtras = new Bundle();
- jobExtras.putBundle(BatchPushJobService.JOB_EXTRA_PUSH_DATA_KEY, intentExtras);
-
- JobInfo job = new JobInfo.Builder(
- JobHelper.generateUniqueJobId(scheduler),
- new ComponentName(context, BatchPushJobService.class)
- )
- .setOverrideDeadline(3600000) // one hour
- .setTransientExtras(jobExtras)
- .build();
-
- if (scheduler.schedule(job) == JobScheduler.RESULT_FAILURE) {
- Logger.internal(TAG, "Failed to schedule the push notification presenter job");
- } else {
- Logger.internal(TAG, "Successfully scheduled the push notification presenter job");
- }
- } catch (JobHelper.GenerationException e) {
- Logger.internal(TAG, "Could not find a suitable job ID", e);
- } catch (Exception e1) {
- Logger.internal(TAG, "Could schedule Batch push presentation job", e1);
- }
- } else {
- // Explicitly specify that BatchPushService will handle the intent and start it.
- ComponentName comp = new ComponentName(context.getPackageName(), BatchPushService.class.getName());
- startWakefulService(context, intent.setComponent(comp));
+ if (isFCMMessage(intent)) {
+ final String messageID = getGoogleMessageID(intent);
+ if (isDuplicateMessage(messageID)) {
+ Logger.info(TAG, "Got a duplicate message_id from FCM, ignoring.");
+ return;
+ }
+ if (presentNotification(context, intent)) {
+ markMessageAsHandled(messageID);
}
} else {
Logger.internal(TAG, "Intent was not a push message.");
}
}
+
+ private boolean isFCMMessage(Intent intent) {
+ // FCM has multiple push types, which we should not handle
+ final String type = intent.getStringExtra("message_type");
+ return type == null || "gcm".equalsIgnoreCase(type);
+ }
+
+ @VisibleForTesting
+ protected boolean presentNotification(@NonNull Context context, @NonNull Intent intent) {
+ // This method will try to avoid starting a job if possible to avoid potential delays
+ // in notification display. Up to a certain frequency, high priority FCM messages should go
+ // into that "fast path".
+
+ // High priority FCM pushes have the RECEIVER_FOREGROUND flag. If it is missing, we can skip
+ // trying to start a normal service on O and start a Job.
+ @SuppressLint("InlinedApi")
+ boolean isHighPriorityPush = (intent.getFlags() & Intent.FLAG_RECEIVER_FOREGROUND) != 0;
+
+ // If we're on Android O and a normal priority push, start a Job, startService is guaranteed
+ // to fail.
+ if (!isHighPriorityPush && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+ Logger.internal(TAG, "Normal priority notification: scheduling a Job");
+ return scheduleJob(context, intent);
+ } else {
+ // We're either on a high priority push or on an old Android version, start service directly.
+ // This can happen for multiple reasons:
+ // - User has forced background restrictions
+ // - FCM's temporary restriction exclusion has already expired
+ // - Something else failed
+ // Fallback on a Job if possible
+ Logger.internal(TAG, "High priority notification/legacy Android: starting service");
+ try {
+ startPresentationService(context, intent);
+ return true;
+ } catch (IllegalStateException e) {
+ // This exception can happen on Android O, fallback on scheduling a Job.
+ // On earlier Android versions, it should not happen.
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+ Logger.internal(TAG, "Failed to start service, scheduling Job");
+ return scheduleJob(context, intent);
+ } else {
+ Logger.error(TAG, "Could not start notification presentation service:", e);
+ return false;
+ }
+ }
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ private boolean scheduleJob(@NonNull Context context, @NonNull Intent intent) {
+ try {
+ JobScheduler scheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
+ if (scheduler == null) {
+ Logger.internal(TAG, "Could not get Job Scheduler system service");
+ return false;
+ }
+
+ final Bundle intentExtras = intent.getExtras();
+ if (intentExtras == null || intentExtras.isEmpty()) {
+ Logger.internal(TAG, "Intent extras were empty, not scheduling push notification presenter job");
+ return false;
+ }
+
+ final Bundle jobExtras = new Bundle();
+ jobExtras.putBundle(BatchPushJobService.JOB_EXTRA_PUSH_DATA_KEY, intentExtras);
+ int jobId = (int) (Math.random() * Integer.MAX_VALUE);
+ JobInfo job = new JobInfo.Builder(jobId, new ComponentName(context, BatchPushJobService.class))
+ .setOverrideDeadline(3600000) // one hour
+ .setTransientExtras(jobExtras)
+ .build();
+
+ if (scheduler.schedule(job) == JobScheduler.RESULT_FAILURE) {
+ Logger.internal(TAG, "Failed to schedule the push notification presenter job");
+ return false;
+ }
+
+ Logger.internal(TAG, "Successfully scheduled the push notification presenter job");
+ return true;
+ } catch (Exception e1) {
+ Logger.internal(TAG, "Could schedule Batch push presentation job", e1);
+ }
+ return false;
+ }
+
+ private void startPresentationService(@NonNull Context context, @NonNull Intent intent) {
+ // Explicitly specify that BatchPushService will handle the intent and start it.
+ ComponentName comp = new ComponentName(context.getPackageName(), BatchPushService.class.getName());
+ if (GenericHelper.isWakeLockPermissionAvailable(context)) {
+ startWakefulService(context, intent.setComponent(comp));
+ } else {
+ context.startService(intent.setComponent(comp));
+ }
+ }
+
+ private boolean isDuplicateMessage(@Nullable String msgID) {
+ // Can't deduplicate a message ID if we do not have one
+ if (msgID == null) {
+ return false;
+ }
+
+ return handledMessageIDs.contains(msgID);
+ }
+
+ private void markMessageAsHandled(@Nullable String msgID) {
+ if (msgID == null) {
+ return;
+ }
+
+ handledMessageIDs.add(msgID);
+ if (handledMessageIDs.size() > MAX_HANDLED_MESSAGE_IDS_COUNT) {
+ handledMessageIDs.pollFirst();
+ }
+ }
+
+ // Extract the Firebase Message identifier from the payload
+ @Nullable
+ private String getGoogleMessageID(@NonNull Intent intent) {
+ // GCM apparently can use both keys to store the message ID.
+ // The google. key can't be controlled via the custom payload, not sure about
+ // message ID.
+ // FCM checks for both, so do the same.
+ final String googleMessageID = intent.getStringExtra("google.message_id");
+ if (!TextUtils.isEmpty(googleMessageID)) {
+ return googleMessageID;
+ }
+
+ final String messageID = intent.getStringExtra("message_id");
+ if (!TextUtils.isEmpty(messageID)) {
+ return messageID;
+ }
+
+ return null;
+ }
+
+ @VisibleForTesting
+ static int getHandledMessageIDsSize() {
+ return handledMessageIDs.size();
+ }
+
+ @VisibleForTesting
+ static void resetHandledMessageIDs() {
+ handledMessageIDs.clear();
+ }
}
diff --git a/Sources/sdk/src/main/java/com/batch/android/BatchPushNotificationPresenter.java b/Sources/sdk/src/main/java/com/batch/android/BatchPushNotificationPresenter.java
index a090172..e224048 100644
--- a/Sources/sdk/src/main/java/com/batch/android/BatchPushNotificationPresenter.java
+++ b/Sources/sdk/src/main/java/com/batch/android/BatchPushNotificationPresenter.java
@@ -172,7 +172,7 @@ public static void presentNotification(
ApplicationInfo appInfo = context.getApplicationInfo();
- if (!BatchPushHelper.isPushValid(context, batchData)) {
+ if (!BatchPushHelper.canDisplayPush(context, batchData)) {
return;
}
@@ -221,7 +221,7 @@ public static void presentNotification(
//TODO: Figure out a better place to register the channel (not on every notification display...)
// Problem is that we have to do it here in case the app gets updated, and we never get the context when
// set up in Application
- batchChannelsManager.registerBatchChannelIfNeeded(context);
+ batchChannelsManager.registerBatchChannelIfNeeded(context, false);
/*
* Small icon
@@ -377,6 +377,7 @@ public static void presentNotification(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
userFlags = userFlags | PendingIntent.FLAG_IMMUTABLE;
}
+ //noinspection WrongConstant
launchIntent.addFlags(userFlags);
}
}
@@ -544,9 +545,6 @@ public static void presentNotification(
} else {
Logger.info("Batch.Push: notification can't be displayed, skipping event dispatcher...");
}
-
- // The push has been shown, make it as such
- BatchPushHelper.markPushAsShown(context, pushId);
}
/**
@@ -599,8 +597,6 @@ private static boolean trySendLandingToForegroundApp(Context context, Bundle ext
MessagingModuleProvider
.get()
.displayMessage(context, new BatchLandingMessage(extras, messageJSON), false);
- // A push that's a mobile landing is considered shown
- BatchPushHelper.markPushAsShown(context, batchData.getPushId());
return true;
}
} catch (PayloadParsingException | JSONException e) {
diff --git a/Sources/sdk/src/main/java/com/batch/android/BatchPushService.java b/Sources/sdk/src/main/java/com/batch/android/BatchPushService.java
index ee0c198..68f0bbd 100644
--- a/Sources/sdk/src/main/java/com/batch/android/BatchPushService.java
+++ b/Sources/sdk/src/main/java/com/batch/android/BatchPushService.java
@@ -8,7 +8,8 @@
/**
* Batch's service for handling the push messages and show a notification
*
- * This is a legacy implementation, and should not be used on versions higher than Android O
+ * This can be used on Android O, if eligibility has been verified beforehand and startService
+ * exceptions are handled.
*
*/
@PublicSDK
diff --git a/Sources/sdk/src/main/java/com/batch/android/BatchWebservice.java b/Sources/sdk/src/main/java/com/batch/android/BatchWebservice.java
index 8b9dd30..fe3d982 100644
--- a/Sources/sdk/src/main/java/com/batch/android/BatchWebservice.java
+++ b/Sources/sdk/src/main/java/com/batch/android/BatchWebservice.java
@@ -7,12 +7,9 @@
import com.batch.android.core.ParameterKeys;
import com.batch.android.core.Parameters;
import com.batch.android.core.SystemParameterHelper;
-import com.batch.android.core.SystemParameterShortName;
import com.batch.android.core.Webservice;
import com.batch.android.core.WebserviceErrorCause;
import com.batch.android.di.providers.ParametersProvider;
-import com.batch.android.di.providers.PushModuleProvider;
-import com.batch.android.di.providers.TrackerModuleProvider;
import com.batch.android.di.providers.WebserviceMetricsProvider;
import com.batch.android.json.JSONArray;
import com.batch.android.json.JSONObject;
@@ -21,7 +18,6 @@
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Date;
import java.util.List;
import java.util.Map;
@@ -60,21 +56,6 @@ protected BatchWebservice(Context context, RequestType type, String baseURLForma
// -------------------------------------->
- /**
- * Prepend the API Key into the url parameters
- *
- * @param parameters
- * @return the same parameters with Batch key preprended
- */
- private static String[] addBatchApiKey(String[] parameters) {
- final String[] retParams = new String[parameters.length + 1];
- retParams[0] = Batch.getAPIKey();
- System.arraycopy(parameters, 0, retParams, 1, parameters.length);
- return retParams;
- }
-
- // -------------------------------------->
-
@Override
protected void addDefaultHeaders() {
super.addDefaultHeaders();
@@ -114,126 +95,7 @@ protected PostDataProvider getPostDataProvider() {
/*
* Build ids object
*/
- JSONObject ids = new JSONObject();
-
- /*
- * Add modules data
- */
- try {
- ids.put("m_e", TrackerModuleProvider.get().getState());
- ids.put("m_p", PushModuleProvider.get().getState());
- } catch (Exception e) {
- Logger.internal(TAG, "Error while adding module parameters into parameters", e);
- }
-
- /*
- * Build all parameters
- */
- String baseIdsParameterString = ParametersProvider
- .get(applicationContext)
- .get(ParameterKeys.WEBSERVICE_IDS_PARAMETERS);
- String[] baseParameters;
- if (!TextUtils.isEmpty(baseIdsParameterString)) {
- baseParameters = baseIdsParameterString.split(",");
- } else {
- baseParameters = new String[] {};
- }
-
- String advancedIdsParameterString = ParametersProvider
- .get(applicationContext)
- .get(ParameterKeys.WEBSERVICE_IDS_ADVANCED_PARAMETERS);
- String[] advancedParameters;
- if (!TextUtils.isEmpty(advancedIdsParameterString) && Batch.shouldUseAdvancedDeviceInformation()) {
- advancedParameters = advancedIdsParameterString.split(",");
- } else {
- advancedParameters = new String[] {};
- }
-
- String[] parameters;
-
- if (advancedParameters.length == 0) {
- parameters = baseParameters;
- } else if (baseParameters.length == 0) {
- parameters = advancedParameters;
- } else {
- parameters = Arrays.copyOf(baseParameters, baseParameters.length + advancedParameters.length);
- System.arraycopy(advancedParameters, 0, parameters, baseParameters.length, advancedParameters.length);
- }
-
- for (String parameter : parameters) {
- try {
- if (SystemParameterShortName.INSTALL_ID.shortName.equals(parameter)) {
- Install install = Batch.getInstall();
- String val = install.getInstallID();
-
- if (val != null) {
- ids.put(parameter, val);
- }
- } else if (SystemParameterShortName.DEVICE_INSTALL_DATE.shortName.equals(parameter)) {
- Install install = Batch.getInstall();
- Date val = install.getInstallDate();
-
- if (val != null) {
- ids.put(parameter, Webservice.formatDate(val));
- }
- } else if (SystemParameterShortName.SERVER_ID.shortName.equals(parameter)) {
- String val = ParametersProvider.get(applicationContext).get(ParameterKeys.SERVER_ID_KEY);
- if (val != null) {
- ids.put(parameter, val);
- }
- } else if (SystemParameterShortName.SESSION_ID.shortName.equals(parameter)) {
- String val = Batch.getSessionID();
- if (val != null) {
- ids.put(parameter, val);
- }
- } else if (SystemParameterShortName.CUSTOM_USER_ID.shortName.equals(parameter)) {
- User user = Batch.getUser();
- if (user != null) {
- String customID = user.getCustomID();
- if (customID != null) {
- ids.put(parameter, customID);
- }
- }
- } else if (SystemParameterShortName.ADVERTISING_ID.shortName.equals(parameter)) {
- if (Batch.shouldUseAdvertisingID()) {
- AdvertisingID advertisingID = Batch.getAdvertisingID();
- if (advertisingID != null) {
- boolean isIdfaAvailable = advertisingID.isReady() && advertisingID.isNotNull();
- if (isIdfaAvailable) {
- ids.put(parameter, advertisingID.get());
- }
- }
- }
- } else if (SystemParameterShortName.ADVERTISING_ID_OPTIN.shortName.equals(parameter)) {
- AdvertisingID advertisingID = Batch.getAdvertisingID();
- if (advertisingID != null) {
- boolean isIdfaAvailable = advertisingID.isReady();
- if (isIdfaAvailable) {
- ids.put(parameter, !advertisingID.isLimited());
- }
- }
- } else if (SystemParameterShortName.BRIDGE_VERSION.shortName.equals(parameter)) {
- String val = SystemParameterHelper.getBridgeVersion();
-
- if (val != null && !val.isEmpty()) {
- ids.put(parameter, val);
- }
- } else if (SystemParameterShortName.PLUGIN_VERSION.shortName.equals(parameter)) {
- String val = SystemParameterHelper.getPluginVersion();
-
- if (val != null && !val.isEmpty()) {
- ids.put(parameter, val);
- }
- } else {
- String val = SystemParameterHelper.getValue(parameter, applicationContext);
- if (val != null) {
- ids.put(parameter, val);
- }
- }
- } catch (Exception e) {
- Logger.internal(TAG, "Error while adding " + parameter + " post id", e);
- }
- }
+ JSONObject ids = WebserviceParameterUtils.getWebserviceIdsAsJson(applicationContext);
/*
* Add ids object to post params
diff --git a/Sources/sdk/src/main/java/com/batch/android/DisplayReceiptWebservice.java b/Sources/sdk/src/main/java/com/batch/android/DisplayReceiptWebservice.java
index ff53e15..19f7c23 100644
--- a/Sources/sdk/src/main/java/com/batch/android/DisplayReceiptWebservice.java
+++ b/Sources/sdk/src/main/java/com/batch/android/DisplayReceiptWebservice.java
@@ -2,32 +2,25 @@
import android.content.Context;
import com.batch.android.core.Logger;
+import com.batch.android.core.MessagePackWebservice;
import com.batch.android.core.ParameterKeys;
import com.batch.android.core.Parameters;
import com.batch.android.core.TaskRunnable;
-import com.batch.android.core.Webservice;
-import com.batch.android.displayreceipt.DisplayReceipt;
import com.batch.android.post.DisplayReceiptPostDataProvider;
-import com.batch.android.post.PostDataProvider;
import com.batch.android.webservice.listener.DisplayReceiptWebserviceListener;
import java.net.MalformedURLException;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-class DisplayReceiptWebservice extends Webservice implements TaskRunnable {
+class DisplayReceiptWebservice extends MessagePackWebservice implements TaskRunnable {
private static final String TAG = "DisplayReceiptWebservice";
- private static final String MSGPACK_SCHEMA_VERSION = "1.0.0";
- private DisplayReceiptWebserviceListener listener;
- private DisplayReceiptPostDataProvider dataProvider;
-
- // -------------------------------------->
+ private final DisplayReceiptWebserviceListener listener;
/**
- * @param context
- * @param baseURLFormat an url to format with api key (ex : http://sample.com/%s/sample)
+ * @param context Android context
+ * @param listener
+ * @param dataProvider
+ * @param parameters
* @throws MalformedURLException
*/
protected DisplayReceiptWebservice(
@@ -36,58 +29,17 @@ protected DisplayReceiptWebservice(
DisplayReceiptPostDataProvider dataProvider,
String... parameters
) throws MalformedURLException {
- super(context, RequestType.POST, Parameters.DISPLAY_RECEIPT_WS_URL, addSchemaVersion(parameters));
+ super(context, dataProvider, Parameters.DISPLAY_RECEIPT_WS_URL, addSchemaVersion(parameters));
if (listener == null) {
- throw new NullPointerException("listener==null");
- }
-
- if (dataProvider == null || dataProvider.isEmpty()) {
- throw new NullPointerException("receipt provider is empty");
+ throw new NullPointerException("Listener is null");
}
-
this.listener = listener;
- this.dataProvider = dataProvider;
- }
-
- // -------------------------------------->
-
- /**
- * Prepend the schema version into the url parameters
- *
- * @param parameters
- * @return
- */
- private static String[] addSchemaVersion(String[] parameters) {
- final String[] retParams = new String[parameters.length + 1];
- retParams[0] = MSGPACK_SCHEMA_VERSION;
- System.arraycopy(parameters, 0, retParams, 1, parameters.length);
- return retParams;
- }
-
- @Override
- protected Map getHeaders() {
- HashMap header = new HashMap<>();
- header.put("x-batch-protocol-version", MSGPACK_SCHEMA_VERSION);
- header.put("x-batch-sdk-version", Parameters.SDK_VERSION);
- return header;
- }
-
- @Override
- protected PostDataProvider> getPostDataProvider() {
- return dataProvider;
- }
-
- // -------------------------------------->
-
- @Override
- public String getTaskIdentifier() {
- return "Batch/receiptws";
}
@Override
public void run() {
try {
- Logger.internal(TAG, "display receipt webservice started");
+ Logger.internal(TAG, "Webservice started");
executeRequest();
listener.onSuccess();
} catch (WebserviceError error) {
@@ -95,11 +47,9 @@ public void run() {
}
}
- // ----------------------------------------->
-
@Override
- protected String getURLSorterPatternParameterKey() {
- return ParameterKeys.DISPLAY_RECEIPT_WS_URLSORTER_PATTERN_KEY;
+ public String getTaskIdentifier() {
+ return "Batch/receiptws";
}
@Override
@@ -107,31 +57,6 @@ protected String getCryptorTypeParameterKey() {
return ParameterKeys.DISPLAY_RECEIPT_WS_CRYPTORTYPE_KEY;
}
- @Override
- protected String getCryptorModeParameterKey() {
- return ParameterKeys.DISPLAY_RECEIPT_WS_CRYPTORMODE_KEY;
- }
-
- @Override
- protected String getPostCryptorTypeParameterKey() {
- return ParameterKeys.DISPLAY_RECEIPT_WS_POST_CRYPTORTYPE_KEY;
- }
-
- @Override
- protected String getReadCryptorTypeParameterKey() {
- return ParameterKeys.DISPLAY_RECEIPT_WS_READ_CRYPTORTYPE_KEY;
- }
-
- @Override
- protected String getSpecificConnectTimeoutKey() {
- return ParameterKeys.DISPLAY_RECEIPT_WS_CONNECT_TIMEOUT_KEY;
- }
-
- @Override
- protected String getSpecificReadTimeoutKey() {
- return ParameterKeys.DISPLAY_RECEIPT_WS_READ_TIMEOUT_KEY;
- }
-
@Override
protected String getSpecificRetryCountKey() {
return ParameterKeys.DISPLAY_RECEIPT_WS_RETRYCOUNT_KEY;
diff --git a/Sources/sdk/src/main/java/com/batch/android/LocalCampaignsJITWebservice.java b/Sources/sdk/src/main/java/com/batch/android/LocalCampaignsJITWebservice.java
new file mode 100644
index 0000000..c75a55c
--- /dev/null
+++ b/Sources/sdk/src/main/java/com/batch/android/LocalCampaignsJITWebservice.java
@@ -0,0 +1,75 @@
+package com.batch.android;
+
+import android.content.Context;
+import com.batch.android.core.Logger;
+import com.batch.android.core.MessagePackWebservice;
+import com.batch.android.core.ParameterKeys;
+import com.batch.android.core.Parameters;
+import com.batch.android.core.TaskRunnable;
+import com.batch.android.metrics.MetricRegistry;
+import com.batch.android.post.LocalCampaignsJITPostDataProvider;
+import com.batch.android.webservice.listener.LocalCampaignsJITWebserviceListener;
+import java.net.MalformedURLException;
+import java.util.List;
+
+class LocalCampaignsJITWebservice extends MessagePackWebservice implements TaskRunnable {
+
+ private static final String TAG = "LocalCampaignsJITWebservice";
+
+ /**
+ * Web service callback
+ */
+ private final LocalCampaignsJITWebserviceListener listener;
+
+ protected LocalCampaignsJITWebservice(
+ Context context,
+ LocalCampaignsJITWebserviceListener listener,
+ LocalCampaignsJITPostDataProvider dataProvider,
+ String... parameters
+ ) throws MalformedURLException {
+ super(context, dataProvider, Parameters.LOCAL_CAMPAIGNS_JIT_WS_URL, addBatchApiKey(parameters));
+ if (listener == null) {
+ throw new NullPointerException("Listener is null");
+ }
+ this.listener = listener;
+ }
+
+ @Override
+ public String getTaskIdentifier() {
+ return "Batch/localcampaignsjitws";
+ }
+
+ @Override
+ public void run() {
+ Logger.internal(TAG, "Webservice started");
+ MetricRegistry.localCampaignsJITResponseTime.startTimer();
+ try {
+ byte[] response = executeRequest();
+ MetricRegistry.localCampaignsJITResponseTime.observeDuration();
+ MetricRegistry.localCampaignsJITCount.labels("OK").inc();
+ LocalCampaignsJITPostDataProvider dataProvider = (LocalCampaignsJITPostDataProvider) getPostDataProvider();
+ List eligibleCampaigns = dataProvider.unpack(response);
+ this.listener.onSuccess(eligibleCampaigns);
+ } catch (WebserviceError error) {
+ MetricRegistry.localCampaignsJITResponseTime.observeDuration();
+ MetricRegistry.localCampaignsJITCount.labels("KO").inc();
+ Logger.internal(TAG, error.getReason().toString(), error.getCause());
+ this.listener.onFailure(error);
+ }
+ }
+
+ @Override
+ protected String getSpecificConnectTimeoutKey() {
+ return ParameterKeys.LOCAL_CAMPAIGNS_JIT_WS_CONNECT_TIMEOUT_KEY;
+ }
+
+ @Override
+ protected String getSpecificReadTimeoutKey() {
+ return ParameterKeys.LOCAL_CAMPAIGNS_JIT_WS_READ_TIMEOUT_KEY;
+ }
+
+ @Override
+ protected String getSpecificRetryCountKey() {
+ return ParameterKeys.LOCAL_CAMPAIGNS_JIT_WS_RETRYCOUNT_KEY;
+ }
+}
diff --git a/Sources/sdk/src/main/java/com/batch/android/LocalCampaignsWebservice.java b/Sources/sdk/src/main/java/com/batch/android/LocalCampaignsWebservice.java
index 75daa3d..c9e2535 100644
--- a/Sources/sdk/src/main/java/com/batch/android/LocalCampaignsWebservice.java
+++ b/Sources/sdk/src/main/java/com/batch/android/LocalCampaignsWebservice.java
@@ -7,6 +7,7 @@
import com.batch.android.core.TaskRunnable;
import com.batch.android.di.providers.CampaignManagerProvider;
import com.batch.android.json.JSONObject;
+import com.batch.android.metrics.MetricRegistry;
import com.batch.android.query.LocalCampaignsQuery;
import com.batch.android.query.Query;
import com.batch.android.query.QueryType;
@@ -15,7 +16,6 @@
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.List;
-import java.util.stream.Collectors;
/**
* Webservice to ask the server for all type of local campaigns (be in-app or notification)
@@ -55,13 +55,14 @@ public void run() {
try {
Logger.internal(TAG, "local campaigns webservice started");
webserviceMetrics.onWebserviceStarted(this);
-
+ MetricRegistry.localCampaignsSyncResponseTime.startTimer();
/*
* Read response
*/
JSONObject response = null;
try {
response = getStandardResponseBodyIfValid();
+ MetricRegistry.localCampaignsSyncResponseTime.observeDuration();
webserviceMetrics.onWebserviceFinished(this, true);
} catch (WebserviceError error) {
Logger.internal(
@@ -69,6 +70,7 @@ public void run() {
"Error while getting local campaigns list : " + error.getReason().toString(),
error.getCause()
);
+ MetricRegistry.localCampaignsSyncResponseTime.observeDuration();
webserviceMetrics.onWebserviceFinished(this, false);
switch (error.getReason()) {
@@ -85,7 +87,6 @@ public void run() {
listener.onError(FailReason.UNEXPECTED_ERROR);
break;
}
-
return;
}
@@ -116,9 +117,7 @@ public void run() {
CampaignManagerProvider.get().deleteSavedCampaignsAsync(applicationContext);
} else {
// else we save them
- CampaignManagerProvider
- .get()
- .saveCampaignsAsync(applicationContext, localCampaignsResponse.getCampaignsToSave());
+ CampaignManagerProvider.get().saveCampaignsAsync(applicationContext, localCampaignsResponse);
responses.add(localCampaignsResponse);
}
} else {
diff --git a/Sources/sdk/src/main/java/com/batch/android/MetricWebservice.java b/Sources/sdk/src/main/java/com/batch/android/MetricWebservice.java
new file mode 100644
index 0000000..7ea9773
--- /dev/null
+++ b/Sources/sdk/src/main/java/com/batch/android/MetricWebservice.java
@@ -0,0 +1,52 @@
+package com.batch.android;
+
+import android.content.Context;
+import com.batch.android.core.Logger;
+import com.batch.android.core.MessagePackWebservice;
+import com.batch.android.core.ParameterKeys;
+import com.batch.android.core.Parameters;
+import com.batch.android.core.TaskRunnable;
+import com.batch.android.post.MetricPostDataProvider;
+import com.batch.android.webservice.listener.MetricWebserviceListener;
+import java.net.MalformedURLException;
+
+class MetricWebservice extends MessagePackWebservice implements TaskRunnable {
+
+ private static final String TAG = "MetricWebservice";
+
+ private final MetricWebserviceListener listener;
+
+ protected MetricWebservice(
+ Context context,
+ MetricWebserviceListener listener,
+ MetricPostDataProvider dataProvider,
+ String... parameters
+ ) throws MalformedURLException {
+ super(context, dataProvider, Parameters.METRIC_WS_URL, parameters);
+ if (listener == null) {
+ throw new NullPointerException("Listener is null");
+ }
+ this.listener = listener;
+ }
+
+ @Override
+ public void run() {
+ Logger.internal(TAG, "Webservice started");
+ try {
+ executeRequest();
+ this.listener.onSuccess();
+ } catch (WebserviceError error) {
+ this.listener.onFailure(error);
+ }
+ }
+
+ @Override
+ public String getTaskIdentifier() {
+ return "Batch/metricsws";
+ }
+
+ @Override
+ protected String getSpecificRetryCountKey() {
+ return ParameterKeys.METRIC_WS_RETRYCOUNT_KEY;
+ }
+}
diff --git a/Sources/sdk/src/main/java/com/batch/android/WebserviceLauncher.java b/Sources/sdk/src/main/java/com/batch/android/WebserviceLauncher.java
index 8304a19..3412af4 100644
--- a/Sources/sdk/src/main/java/com/batch/android/WebserviceLauncher.java
+++ b/Sources/sdk/src/main/java/com/batch/android/WebserviceLauncher.java
@@ -7,10 +7,15 @@
import com.batch.android.di.providers.LocalCampaignsWebserviceListenerImplProvider;
import com.batch.android.di.providers.TaskExecutorProvider;
import com.batch.android.event.Event;
+import com.batch.android.localcampaigns.model.LocalCampaign;
import com.batch.android.post.DisplayReceiptPostDataProvider;
+import com.batch.android.post.LocalCampaignsJITPostDataProvider;
+import com.batch.android.post.MetricPostDataProvider;
import com.batch.android.push.Registration;
import com.batch.android.runtime.RuntimeManager;
import com.batch.android.webservice.listener.DisplayReceiptWebserviceListener;
+import com.batch.android.webservice.listener.LocalCampaignsJITWebserviceListener;
+import com.batch.android.webservice.listener.MetricWebserviceListener;
import com.batch.android.webservice.listener.TrackerWebserviceListener;
import com.batch.android.webservice.listener.impl.AttributesCheckWebserviceListenerImpl;
import com.batch.android.webservice.listener.impl.AttributesSendWebserviceListenerImpl;
@@ -119,6 +124,27 @@ public static TaskRunnable initOptOutTrackerWebservice(
}
}
+ /**
+ * Create an instance of the metrics webservice and return the runnable
+ *
+ * @param context android context
+ * @param dataProvider provider
+ * @param listener listener
+ * @return instance of the webservice ready to be run
+ */
+ public static TaskRunnable initMetricWebservice(
+ Context context,
+ MetricPostDataProvider dataProvider,
+ MetricWebserviceListener listener
+ ) {
+ try {
+ return new MetricWebservice(context, listener, dataProvider);
+ } catch (Exception e) {
+ Logger.internal(TAG, "Error while initializing metrics webservice", e);
+ return null;
+ }
+ }
+
/**
* Launch the push webservice
*/
@@ -200,4 +226,21 @@ public static boolean launchLocalCampaignsWebservice(RuntimeManager runtimeManag
return false;
}
}
+
+ public static boolean launchLocalCampaignsJITWebservice(
+ RuntimeManager runtimeManager,
+ List campaigns,
+ LocalCampaignsJITWebserviceListener listener
+ ) {
+ LocalCampaignsJITPostDataProvider dataProvider = new LocalCampaignsJITPostDataProvider(campaigns);
+ try {
+ TaskExecutorProvider
+ .get(runtimeManager.getContext())
+ .submit(new LocalCampaignsJITWebservice(runtimeManager.getContext(), listener, dataProvider));
+ return true;
+ } catch (Exception e) {
+ Logger.internal(TAG, "Error while initializing Local Campaigns JIT WS", e);
+ return false;
+ }
+ }
}
diff --git a/Sources/sdk/src/main/java/com/batch/android/WebserviceParameterUtils.java b/Sources/sdk/src/main/java/com/batch/android/WebserviceParameterUtils.java
new file mode 100644
index 0000000..0628a04
--- /dev/null
+++ b/Sources/sdk/src/main/java/com/batch/android/WebserviceParameterUtils.java
@@ -0,0 +1,174 @@
+package com.batch.android;
+
+import android.content.Context;
+import android.text.TextUtils;
+import androidx.annotation.NonNull;
+import com.batch.android.core.Logger;
+import com.batch.android.core.ParameterKeys;
+import com.batch.android.core.SystemParameterHelper;
+import com.batch.android.core.SystemParameterShortName;
+import com.batch.android.core.Webservice;
+import com.batch.android.di.providers.ParametersProvider;
+import com.batch.android.di.providers.PushModuleProvider;
+import com.batch.android.di.providers.TrackerModuleProvider;
+import com.batch.android.json.JSONObject;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Utility class to easily get webservice parameters
+ */
+public class WebserviceParameterUtils {
+
+ private static final String TAG = "WebserviceParameterUtils";
+
+ /**
+ * Get webservice ids parameters as map
+ * @param context context
+ * @return A Map of parameters to attach in a webservice
+ */
+ public static Map getWebserviceIdsAsMap(Context context) {
+ return buildIds(context);
+ }
+
+ /**
+ * Get webservice ids parameters as json object
+ * @param context context
+ * @return A JSONObject of parameters to attach in a webservice
+ */
+ public static JSONObject getWebserviceIdsAsJson(Context context) {
+ return new JSONObject(buildIds(context));
+ }
+
+ /**
+ * Build ids
+ * @param context context
+ * @return map of ids
+ */
+ private static Map buildIds(@NonNull Context context) {
+ /*
+ * Build ids object
+ */
+ Map ids = new HashMap<>();
+
+ /*
+ * Add modules data
+ */
+ try {
+ ids.put("m_e", TrackerModuleProvider.get().getState());
+ ids.put("m_p", PushModuleProvider.get().getState());
+ } catch (Exception e) {
+ Logger.internal(TAG, "Error while adding module parameters into parameters", e);
+ }
+
+ /*
+ * Build all parameters
+ */
+ String baseIdsParameterString = ParametersProvider.get(context).get(ParameterKeys.WEBSERVICE_IDS_PARAMETERS);
+ String[] baseParameters;
+ if (!TextUtils.isEmpty(baseIdsParameterString)) {
+ baseParameters = baseIdsParameterString.split(",");
+ } else {
+ baseParameters = new String[] {};
+ }
+
+ String advancedIdsParameterString = ParametersProvider
+ .get(context)
+ .get(ParameterKeys.WEBSERVICE_IDS_ADVANCED_PARAMETERS);
+ String[] advancedParameters;
+ if (!TextUtils.isEmpty(advancedIdsParameterString) && Batch.shouldUseAdvancedDeviceInformation()) {
+ advancedParameters = advancedIdsParameterString.split(",");
+ } else {
+ advancedParameters = new String[] {};
+ }
+
+ String[] parameters;
+
+ if (advancedParameters.length == 0) {
+ parameters = baseParameters;
+ } else if (baseParameters.length == 0) {
+ parameters = advancedParameters;
+ } else {
+ parameters = Arrays.copyOf(baseParameters, baseParameters.length + advancedParameters.length);
+ System.arraycopy(advancedParameters, 0, parameters, baseParameters.length, advancedParameters.length);
+ }
+
+ for (String parameter : parameters) {
+ try {
+ if (SystemParameterShortName.INSTALL_ID.shortName.equals(parameter)) {
+ Install install = Batch.getInstall();
+ String val = install.getInstallID();
+
+ if (val != null) {
+ ids.put(parameter, val);
+ }
+ } else if (SystemParameterShortName.DEVICE_INSTALL_DATE.shortName.equals(parameter)) {
+ Install install = Batch.getInstall();
+ Date val = install.getInstallDate();
+
+ if (val != null) {
+ ids.put(parameter, Webservice.formatDate(val));
+ }
+ } else if (SystemParameterShortName.SERVER_ID.shortName.equals(parameter)) {
+ String val = ParametersProvider.get(context).get(ParameterKeys.SERVER_ID_KEY);
+ if (val != null) {
+ ids.put(parameter, val);
+ }
+ } else if (SystemParameterShortName.SESSION_ID.shortName.equals(parameter)) {
+ String val = Batch.getSessionID();
+ if (val != null) {
+ ids.put(parameter, val);
+ }
+ } else if (SystemParameterShortName.CUSTOM_USER_ID.shortName.equals(parameter)) {
+ User user = Batch.getUser();
+ if (user != null) {
+ String customID = user.getCustomID();
+ if (customID != null) {
+ ids.put(parameter, customID);
+ }
+ }
+ } else if (SystemParameterShortName.ADVERTISING_ID.shortName.equals(parameter)) {
+ if (Batch.shouldUseAdvertisingID()) {
+ AdvertisingID advertisingID = Batch.getAdvertisingID();
+ if (advertisingID != null) {
+ boolean isIdfaAvailable = advertisingID.isReady() && advertisingID.isNotNull();
+ if (isIdfaAvailable) {
+ ids.put(parameter, advertisingID.get());
+ }
+ }
+ }
+ } else if (SystemParameterShortName.ADVERTISING_ID_OPTIN.shortName.equals(parameter)) {
+ AdvertisingID advertisingID = Batch.getAdvertisingID();
+ if (advertisingID != null) {
+ boolean isIdfaAvailable = advertisingID.isReady();
+ if (isIdfaAvailable) {
+ ids.put(parameter, !advertisingID.isLimited());
+ }
+ }
+ } else if (SystemParameterShortName.BRIDGE_VERSION.shortName.equals(parameter)) {
+ String val = SystemParameterHelper.getBridgeVersion();
+
+ if (val != null && !val.isEmpty()) {
+ ids.put(parameter, val);
+ }
+ } else if (SystemParameterShortName.PLUGIN_VERSION.shortName.equals(parameter)) {
+ String val = SystemParameterHelper.getPluginVersion();
+
+ if (val != null && !val.isEmpty()) {
+ ids.put(parameter, val);
+ }
+ } else {
+ String val = SystemParameterHelper.getValue(parameter, context);
+ if (val != null) {
+ ids.put(parameter, val);
+ }
+ }
+ } catch (Exception e) {
+ Logger.internal(TAG, "Error while adding " + parameter + " post id", e);
+ }
+ }
+ return ids;
+ }
+}
diff --git a/Sources/sdk/src/main/java/com/batch/android/actions/ClipboardActionRunnable.java b/Sources/sdk/src/main/java/com/batch/android/actions/ClipboardActionRunnable.java
index 181aa22..6844046 100644
--- a/Sources/sdk/src/main/java/com/batch/android/actions/ClipboardActionRunnable.java
+++ b/Sources/sdk/src/main/java/com/batch/android/actions/ClipboardActionRunnable.java
@@ -18,6 +18,8 @@
public class ClipboardActionRunnable implements UserActionRunnable {
private static final String TAG = "ClipboardBuiltinAction";
+ private static final String BASE_ERROR_MSG = "Could not perform clipboard action: ";
+
public static final String IDENTIFIER = ActionModule.RESERVED_ACTION_IDENTIFIER_PREFIX + "clipboard";
@Override
@@ -27,10 +29,15 @@ public void performAction(
@NonNull JSONObject args,
@Nullable UserActionSource source
) {
+ if (context == null) {
+ Logger.internal(TAG, BASE_ERROR_MSG + "no context.");
+ return;
+ }
+
try {
String text = args.getString("t");
if (text == null) {
- Logger.internal(TAG, "Could not perform clipboard action : text's null");
+ Logger.internal(TAG, BASE_ERROR_MSG + "text's null.");
return;
}
String description = args.optString("d", "text");
diff --git a/Sources/sdk/src/main/java/com/batch/android/actions/NotificationPermissionActionRunnable.java b/Sources/sdk/src/main/java/com/batch/android/actions/NotificationPermissionActionRunnable.java
new file mode 100644
index 0000000..c347b2e
--- /dev/null
+++ b/Sources/sdk/src/main/java/com/batch/android/actions/NotificationPermissionActionRunnable.java
@@ -0,0 +1,44 @@
+package com.batch.android.actions;
+
+import android.content.Context;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.batch.android.UserActionRunnable;
+import com.batch.android.UserActionSource;
+import com.batch.android.core.Logger;
+import com.batch.android.core.NotificationPermissionHelper;
+import com.batch.android.json.JSONObject;
+import com.batch.android.module.ActionModule;
+
+public class NotificationPermissionActionRunnable implements UserActionRunnable {
+
+ private static final String TAG = "NotificationPermissionAction";
+ public static final String IDENTIFIER =
+ ActionModule.RESERVED_ACTION_IDENTIFIER_PREFIX + "android_request_notifications";
+
+ @Override
+ public void performAction(
+ @Nullable Context context,
+ @NonNull String identifier,
+ @NonNull JSONObject args,
+ @Nullable UserActionSource source
+ ) {
+ if (context == null) {
+ Logger.error(TAG, "Tried to perform a notif. permission request action, but no context was available");
+ return;
+ }
+
+ final NotificationPermissionHelper notificationPermissionHelper = new NotificationPermissionHelper();
+ notificationPermissionHelper.experimentalUseChannelCreationOnOldTargets =
+ args.reallyOptBoolean(
+ "_useChannel",
+ notificationPermissionHelper.experimentalUseChannelCreationOnOldTargets
+ );
+ notificationPermissionHelper.experimentalForceChannelCreation =
+ args.reallyOptBoolean(
+ "_forceChannelCreation",
+ notificationPermissionHelper.experimentalForceChannelCreation
+ );
+ notificationPermissionHelper.requestPermission(context);
+ }
+}
diff --git a/Sources/sdk/src/main/java/com/batch/android/core/GenericHelper.java b/Sources/sdk/src/main/java/com/batch/android/core/GenericHelper.java
index 2141439..57ea515 100644
--- a/Sources/sdk/src/main/java/com/batch/android/core/GenericHelper.java
+++ b/Sources/sdk/src/main/java/com/batch/android/core/GenericHelper.java
@@ -3,8 +3,10 @@
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.Resources;
+import android.os.Build;
import android.util.DisplayMetrics;
import android.view.WindowManager;
+import androidx.annotation.NonNull;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Locale;
@@ -27,6 +29,27 @@ public static boolean checkPermission(String permission, Context context) {
return (res == PackageManager.PERMISSION_GRANTED);
}
+ public static boolean isWakeLockPermissionAvailable(@NonNull Context context) {
+ try {
+ return GenericHelper.checkPermission("android.permission.WAKE_LOCK", context);
+ } catch (Exception e) {
+ Logger.error("Error while checking android.permission.WAKE_LOCK permission", e);
+ return false;
+ }
+ }
+
+ public static boolean targets12LOrOlder(@NonNull Context context) {
+ // Note: any prerelease Android SDK, even older than 13, will return true here.
+ // We do not care about that edge case.
+ try {
+ int targetSdkVersion = context.getApplicationContext().getApplicationInfo().targetSdkVersion;
+ return targetSdkVersion <= Build.VERSION_CODES.S_V2;
+ } catch (Exception e) {
+ Logger.error("Could not check current target API level", e);
+ return true;
+ }
+ }
+
/**
* Read the MD5 of a content
*
diff --git a/Sources/sdk/src/main/java/com/batch/android/core/JobHelper.java b/Sources/sdk/src/main/java/com/batch/android/core/JobHelper.java
deleted file mode 100644
index 64b9d73..0000000
--- a/Sources/sdk/src/main/java/com/batch/android/core/JobHelper.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package com.batch.android.core;
-
-import android.app.job.JobInfo;
-import android.app.job.JobScheduler;
-import android.os.Build;
-import androidx.annotation.NonNull;
-import androidx.annotation.RequiresApi;
-import java.util.List;
-
-/**
- * Simple helper for Android 21+ Jobs
- */
-
-@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
-public class JobHelper {
-
- private static final int MAX_GENERATION_ATTEMPTS = 20;
-
- public static synchronized int generateUniqueJobId(@NonNull JobScheduler scheduler) throws GenerationException {
- // Consider letting developers override this
- int generatedId;
-
- for (int attempts = 0; attempts <= MAX_GENERATION_ATTEMPTS; attempts++) {
- generatedId = (int) (Math.random() * Integer.MAX_VALUE);
-
- if (!jobListContainsJobId(scheduler.getAllPendingJobs(), generatedId)) {
- return generatedId;
- }
- }
-
- throw new GenerationException("Could not generate an unique id: attempts exhausted");
- }
-
- private static boolean jobListContainsJobId(List jobList, int jobId) {
- if (jobList == null || jobList.size() == 0) {
- return false;
- }
-
- for (JobInfo job : jobList) {
- if (job.getId() == jobId) {
- return true;
- }
- }
-
- return false;
- }
-
- public static class GenerationException extends Exception {
-
- public GenerationException(String message) {
- super(message);
- }
- }
-}
diff --git a/Sources/sdk/src/main/java/com/batch/android/core/MessagePackWebservice.java b/Sources/sdk/src/main/java/com/batch/android/core/MessagePackWebservice.java
new file mode 100644
index 0000000..bae766a
--- /dev/null
+++ b/Sources/sdk/src/main/java/com/batch/android/core/MessagePackWebservice.java
@@ -0,0 +1,94 @@
+package com.batch.android.core;
+
+import android.content.Context;
+import com.batch.android.post.MessagePackPostDataProvider;
+import java.net.MalformedURLException;
+import java.util.HashMap;
+import java.util.Map;
+
+public abstract class MessagePackWebservice extends Webservice implements TaskRunnable {
+
+ private static final String MSGPACK_SCHEMA_VERSION = "1.0.0";
+
+ private final MessagePackPostDataProvider> dataProvider;
+
+ protected MessagePackWebservice(
+ Context context,
+ MessagePackPostDataProvider> dataProvider,
+ String urlPattern,
+ String... parameters
+ ) throws MalformedURLException {
+ super(context, RequestType.POST, urlPattern, parameters);
+ if (dataProvider == null || dataProvider.isEmpty()) {
+ throw new NullPointerException("Provider is empty");
+ }
+ this.dataProvider = dataProvider;
+ }
+
+ /**
+ * Prepend the schema version into the url parameters
+ *
+ * Only used for display receipt
+ * @param parameters parameters
+ * @return parameters
+ */
+ protected static String[] addSchemaVersion(String[] parameters) {
+ final String[] retParams = new String[parameters.length + 1];
+ retParams[0] = MSGPACK_SCHEMA_VERSION;
+ System.arraycopy(parameters, 0, retParams, 1, parameters.length);
+ return retParams;
+ }
+
+ @Override
+ protected Map getHeaders() {
+ HashMap header = new HashMap<>();
+ header.put("x-batch-protocol-version", MSGPACK_SCHEMA_VERSION);
+ header.put("x-batch-sdk-version", Parameters.SDK_VERSION);
+ return header;
+ }
+
+ @Override
+ protected MessagePackPostDataProvider> getPostDataProvider() {
+ return dataProvider;
+ }
+
+ @Override
+ protected String getPostCryptorTypeParameterKey() {
+ return ParameterKeys.MESSAGE_PACK_WS_POST_CRYPTORTYPE_KEY;
+ }
+
+ @Override
+ protected String getReadCryptorTypeParameterKey() {
+ return ParameterKeys.MESSAGE_PACK_WS_READ_CRYPTORTYPE_KEY;
+ }
+
+ @Override
+ protected String getURLSorterPatternParameterKey() {
+ return ParameterKeys.MESSAGE_PACK_WS_URLSORTER_PATTERN_KEY;
+ }
+
+ @Override
+ protected String getCryptorTypeParameterKey() {
+ return ParameterKeys.MESSAGE_PACK_WS_CRYPTORTYPE_KEY;
+ }
+
+ @Override
+ protected String getCryptorModeParameterKey() {
+ return ParameterKeys.MESSAGE_PACK_WS_CRYPTORMODE_KEY;
+ }
+
+ @Override
+ protected String getSpecificConnectTimeoutKey() {
+ return ParameterKeys.MESSAGE_PACK_WS_CONNECT_TIMEOUT_KEY;
+ }
+
+ @Override
+ protected String getSpecificReadTimeoutKey() {
+ return ParameterKeys.MESSAGE_PACK_WS_READ_TIMEOUT_KEY;
+ }
+
+ @Override
+ protected String getSpecificRetryCountKey() {
+ return ParameterKeys.MESSAGE_PACK_WS_RETRYCOUNT_KEY;
+ }
+}
diff --git a/Sources/sdk/src/main/java/com/batch/android/core/NotificationPermissionHelper.java b/Sources/sdk/src/main/java/com/batch/android/core/NotificationPermissionHelper.java
new file mode 100644
index 0000000..bc2da73
--- /dev/null
+++ b/Sources/sdk/src/main/java/com/batch/android/core/NotificationPermissionHelper.java
@@ -0,0 +1,99 @@
+package com.batch.android.core;
+
+import android.app.Activity;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.os.Build;
+import androidx.annotation.NonNull;
+import com.batch.android.BatchNotificationChannelsManagerPrivateHelper;
+import com.batch.android.di.providers.BatchNotificationChannelsManagerProvider;
+
+public class NotificationPermissionHelper {
+
+ private static final String TAG = "NotificationPermission";
+ private static final String BASE_TARGET_LOG_MESSAGE = "App is targeting Android ";
+
+ public static final String PERMISSION_NOTIFICATION = "android.permission.POST_NOTIFICATIONS";
+
+ // This will be removed once T hits stable
+ // We want a bit of flexibility as we don't know what changes Google will make
+ public boolean experimentalUseChannelCreationOnOldTargets = false;
+ public boolean experimentalForceChannelCreation = false;
+
+ public void requestPermission(@NonNull Context context) {
+ // TODO: Test this method. Can't be done until Android T is supported in Robolectric
+ Logger.internal(TAG, "Requesting notification permission.");
+
+ // TODO: Do nothing on Android < 13.
+ // As of writing, Android T betas are API level 32 (same as 12L), so allow 12L to pass this check.
+ // We could implement a codename check but as calling this code on older Android does nothing,
+ // it's not worth the hassle.
+ // Note: if we want to implement a proper check, we could look at Build.VERSION.CODENAME == "Tiramisu"
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S_V2) {
+ return;
+ }
+
+ if (
+ context.getSystemService(NotificationManager.class).areNotificationsEnabled() &&
+ GenericHelper.checkPermission(PERMISSION_NOTIFICATION, context)
+ ) {
+ Logger.internal(TAG, "Notifications are already enabled, not requesting permission.");
+ return;
+ }
+
+ if (GenericHelper.targets12LOrOlder(context)) {
+ Logger.internal(TAG, BASE_TARGET_LOG_MESSAGE + "12L or lower.");
+ // Android documentation and Chromium sources
+ // say that you can't request the permission if you don't target T, but current testing
+ // shows otherwise: as long as the notification channel isn't created, an app that doesn't
+ // target android 13 can request the permission. Creating the channels shows the permission
+ // popup. So, by default
+ //
+ // Note: we allow the caller to bypass this and request the way docs say we should, with
+ // the caveat of it not working if the channel ID is overridden.
+ if (experimentalUseChannelCreationOnOldTargets) {
+ Logger.internal(TAG, "Requesting permission by creating channel.");
+ requestPermissionFromOlderSDK(context);
+ return;
+ }
+ } else {
+ Logger.internal(TAG, BASE_TARGET_LOG_MESSAGE + "13.");
+ }
+
+ // Try to get the current activity
+ // We may already have one: in that case, calling getBaseContext()
+ // would make us lose it, don't call it blindly.
+ // On the other hand, androidx's ContextThemeWrapper is not a superclass of Activity
+ // so we need to call getBaseContext and hope to get it.
+ // Otherwise, give up, we might be able to find a context by looping but lets not go down
+ // that road just yet.
+ if (!(context instanceof Activity)) {
+ if (context instanceof androidx.appcompat.view.ContextThemeWrapper) {
+ context = ((androidx.appcompat.view.ContextThemeWrapper) context).getBaseContext();
+ } else if (context instanceof android.view.ContextThemeWrapper) {
+ context = ((android.view.ContextThemeWrapper) context).getBaseContext();
+ }
+ }
+
+ if (context instanceof Activity) {
+ final Activity activity = (Activity) context;
+ activity.runOnUiThread(() -> {
+ activity.requestPermissions(new String[] { PERMISSION_NOTIFICATION }, 0);
+ });
+ } else {
+ // Should we have a metric here?
+ Logger.internal(TAG, "Cannot request notification permission: no suitable context.");
+ }
+ }
+
+ // Request the permission the google way: by creating the notification channel.
+ // Note: if the user has a channel id override, this will not work unless forced, which is also
+ // a controllable experiment.
+ public void requestPermissionFromOlderSDK(@NonNull Context context) {
+ BatchNotificationChannelsManagerPrivateHelper.registerBatchChannelIfNeeded(
+ BatchNotificationChannelsManagerProvider.get(),
+ context,
+ experimentalForceChannelCreation
+ );
+ }
+}
diff --git a/Sources/sdk/src/main/java/com/batch/android/core/ParameterKeys.java b/Sources/sdk/src/main/java/com/batch/android/core/ParameterKeys.java
index 2d6ec79..ecd5410 100644
--- a/Sources/sdk/src/main/java/com/batch/android/core/ParameterKeys.java
+++ b/Sources/sdk/src/main/java/com/batch/android/core/ParameterKeys.java
@@ -84,17 +84,24 @@ public final class ParameterKeys
public final static String INBOX_WS_CONNECT_TIMEOUT_KEY = "ws.inbox.connect.timeout";
public final static String INBOX_WS_READ_TIMEOUT_KEY = "ws.inbox.read.timeout";
- public final static String DISPLAY_RECEIPT_WS_URLSORTER_PATTERN_KEY = "ws.displayreceipt.pattern";
- public final static String DISPLAY_RECEIPT_WS_CRYPTORTYPE_KEY = "ws.displayreceipt.getcryptor.type";
- public final static String DISPLAY_RECEIPT_WS_CRYPTORMODE_KEY = "ws.displayreceipt.getcryptor.mode";
- public final static String DISPLAY_RECEIPT_WS_POST_CRYPTORTYPE_KEY = "ws.displayreceipt.postcryptor.type";
- public final static String DISPLAY_RECEIPT_WS_READ_CRYPTORTYPE_KEY = "ws.displayreceipt.readcryptor.type";
- public final static String DISPLAY_RECEIPT_WS_RETRYCOUNT_KEY = "ws.displayreceipt.retry";
- public final static String DISPLAY_RECEIPT_WS_CONNECT_TIMEOUT_KEY = "ws.displayreceipt.connect.timeout";
- public final static String DISPLAY_RECEIPT_WS_READ_TIMEOUT_KEY = "ws.displayreceipt.read.timeout";
+ // Default MsgPack webservice parameters
+ public final static String MESSAGE_PACK_WS_POST_CRYPTORTYPE_KEY = "ws.msgpack.postcryptor.type";
+ public final static String MESSAGE_PACK_WS_READ_CRYPTORTYPE_KEY = "ws.msgpack.readcryptor.type";
+ public final static String MESSAGE_PACK_WS_URLSORTER_PATTERN_KEY = "ws.msgpack.pattern";
+ public final static String MESSAGE_PACK_WS_CRYPTORTYPE_KEY = "ws.msgpack.getcryptor.type";
+ public final static String MESSAGE_PACK_WS_CRYPTORMODE_KEY = "ws.msgpack.getcryptor.mode";
+ public final static String MESSAGE_PACK_WS_RETRYCOUNT_KEY = "ws.msgpack.retry";
+ public final static String MESSAGE_PACK_WS_CONNECT_TIMEOUT_KEY = "ws.msgpack.connect.timeout";
+ public final static String MESSAGE_PACK_WS_READ_TIMEOUT_KEY = "ws.msgpack.read.timeout";
- public final static String LOCAL_CAMPAIGNS_WS_INITIAL_DELAY = "lc.wsdelay.initial";
+ public final static String DISPLAY_RECEIPT_WS_CRYPTORTYPE_KEY = "ws.displayreceipt.getcryptor.type";
+ public final static String DISPLAY_RECEIPT_WS_RETRYCOUNT_KEY = "ws.displayreceipt.retry";
+ public final static String METRIC_WS_RETRYCOUNT_KEY = "ws.metrics.retry";
+ public final static String LOCAL_CAMPAIGNS_JIT_WS_RETRYCOUNT_KEY = "ws.localcampaignsjit.retry";
+ public final static String LOCAL_CAMPAIGNS_JIT_WS_READ_TIMEOUT_KEY = "ws.localcampaignsjit.read.timeout";
+ public final static String LOCAL_CAMPAIGNS_JIT_WS_CONNECT_TIMEOUT_KEY = "ws.localcampaignsjit.connect.timeout";
+ public final static String LOCAL_CAMPAIGNS_WS_INITIAL_DELAY = "lc.wsdelay.initial";
public final static String WS_CIPHERV2_LAST_FAILURE_KEY = "ws.cipherv2.lastfailure";
public final static String DEFAULT_RETRY_NUMBER_KEY = "ws.defaultRetry";
public final static String DEFAULT_CONNECT_TIMEOUT_KEY = "ws.defaultconnectTimeout";
@@ -127,7 +134,6 @@ public final class ParameterKeys
public final static String USER_PROFILE_LANGUAGE_KEY = "u_c_l";
public final static String USER_PROFILE_REGION_KEY = "u_c_r";
-
public final static String LIB_CURRENTVERSION_KEY = "app.version.current";
public final static String LIB_PREVIOUSVERSION_KEY = "app.version.previous";
}
diff --git a/Sources/sdk/src/main/java/com/batch/android/core/Parameters.java b/Sources/sdk/src/main/java/com/batch/android/core/Parameters.java
index bdc33ff..89a352f 100644
--- a/Sources/sdk/src/main/java/com/batch/android/core/Parameters.java
+++ b/Sources/sdk/src/main/java/com/batch/android/core/Parameters.java
@@ -124,6 +124,14 @@ public final class Parameters {
* URL of the display receipt WS
*/
public static final String DISPLAY_RECEIPT_WS_URL = "https://dr" + BuildConfig.WS_DOMAIN + "/a/%s";
+ /**
+ * URL of the metrics WS
+ */
+ public static final String METRIC_WS_URL = "https://wsmetrics.batch.com/api-sdk";
+ /**
+ * URL of the local campaigns JIT (check just in time) WS
+ */
+ public static final String LOCAL_CAMPAIGNS_JIT_WS_URL = BASE_WS_URL + "/lc_jit/%s";
// -------------------------------------------------->
@@ -153,10 +161,16 @@ public final class Parameters {
appParameters.put(ParameterKeys.INBOX_WS_READ_CRYPTORTYPE_KEY, "5");
appParameters.put(ParameterKeys.INBOX_WS_POST_CRYPTORTYPE_KEY, "5");
appParameters.put(ParameterKeys.INBOX_WS_RETRYCOUNT_KEY, "0");
- appParameters.put(ParameterKeys.DISPLAY_RECEIPT_WS_CRYPTORTYPE_KEY, "5");
- appParameters.put(ParameterKeys.DISPLAY_RECEIPT_WS_RETRYCOUNT_KEY, "0");
appParameters.put(ParameterKeys.ATTR_LOCAL_CAMPAIGNS_WS_READ_CRYPTORTYPE_KEY, "5");
appParameters.put(ParameterKeys.ATTR_LOCAL_CAMPAIGNS_WS_POST_CRYPTORTYPE_KEY, "5");
+
+ appParameters.put(ParameterKeys.DISPLAY_RECEIPT_WS_CRYPTORTYPE_KEY, "5");
+ appParameters.put(ParameterKeys.DISPLAY_RECEIPT_WS_RETRYCOUNT_KEY, "0");
+ appParameters.put(ParameterKeys.METRIC_WS_RETRYCOUNT_KEY, "0");
+ appParameters.put(ParameterKeys.LOCAL_CAMPAIGNS_JIT_WS_RETRYCOUNT_KEY, "0");
+ appParameters.put(ParameterKeys.LOCAL_CAMPAIGNS_JIT_WS_READ_TIMEOUT_KEY, "1000");
+ appParameters.put(ParameterKeys.LOCAL_CAMPAIGNS_JIT_WS_CONNECT_TIMEOUT_KEY, "1000");
+
appParameters.put(ParameterKeys.LOCAL_CAMPAIGNS_WS_INITIAL_DELAY, "5");
appParameters.put(ParameterKeys.EVENT_TRACKER_STATE, "2");
appParameters.put(ParameterKeys.EVENT_TRACKER_INITIAL_DELAY, "10000");
diff --git a/Sources/sdk/src/main/java/com/batch/android/core/Webservice.java b/Sources/sdk/src/main/java/com/batch/android/core/Webservice.java
index eebb67e..ebfc9a2 100644
--- a/Sources/sdk/src/main/java/com/batch/android/core/Webservice.java
+++ b/Sources/sdk/src/main/java/com/batch/android/core/Webservice.java
@@ -4,6 +4,7 @@
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import com.batch.android.Batch;
import com.batch.android.core.URLBuilder.CryptorMode;
import com.batch.android.core.Webservice.WebserviceError.Reason;
import com.batch.android.di.providers.OptOutModuleProvider;
@@ -53,6 +54,12 @@ public abstract class Webservice {
*/
private static final int WEBSERVICE_ERROR_INVALID_CIPHER = 487;
+ /**
+ * Default retry-after delay (in seconds) when server is overloaded
+ * and no header is provided.
+ */
+ private static final int DEFAULT_RETRY_AFTER = 60;
+
/**
* Debug interceptor. Allows the sample to tweak the SDK behaviour
* Can be disabled using {@link Parameters#ENABLE_WS_INTERCEPTOR}
@@ -166,6 +173,19 @@ protected void addGetParameter(String key, String value) {
builder.addGETParameter(key, value);
}
+ /**
+ * Prepend the API Key into the url parameters
+ *
+ * @param parameters
+ * @return the same parameters with Batch key prepended
+ */
+ protected static String[] addBatchApiKey(String[] parameters) {
+ final String[] retParams = new String[parameters.length + 1];
+ retParams[0] = Batch.getAPIKey();
+ System.arraycopy(parameters, 0, retParams, 1, parameters.length);
+ return retParams;
+ }
+
/**
* Return the specific GET parameters you want to add to the request
* You should override this method to provide custom get parameters
@@ -400,7 +420,7 @@ public byte[] executeRequest() throws WebserviceError {
HttpURLConnection connection = null;
WebserviceError error = null;
- int errorCode = -1;
+ int responseCode = -1;
/*
* Execute request, with retry
@@ -417,11 +437,10 @@ public byte[] executeRequest() throws WebserviceError {
try {
try {
connection = buildConnection();
-
connection.connect();
} catch (IOException ce) {
error = new WebserviceError(WebserviceError.Reason.NETWORK_ERROR, ce);
- errorCode = -1;
+ responseCode = -1;
count++;
continue;
} catch (Exception e) {
@@ -432,16 +451,16 @@ public byte[] executeRequest() throws WebserviceError {
in = new BufferedInputStream(connection.getInputStream());
} catch (SocketTimeoutException e) {
error = new WebserviceError(WebserviceError.Reason.NETWORK_ERROR, e);
- errorCode = -1;
+ responseCode = -1;
count++;
continue;
} catch (IOException ioe) {
// Silently continue since error will be handled by isResponseValid();
}
- errorCode = connection.getResponseCode();
+ responseCode = connection.getResponseCode();
- if (isResponseValid(errorCode)) {
+ if (isResponseValid(responseCode)) {
// Treat GZIP stream.
String header = connection.getHeaderField("Content-Encoding");
if (header != null && header.equals("gzip")) {
@@ -479,12 +498,11 @@ public byte[] executeRequest() throws WebserviceError {
return ba;
} else {
- int responseCode = connection.getResponseCode();
- error =
- new WebserviceError(
- getResponseErrorCause(connection.getResponseCode()),
- new IOException("Response code : " + responseCode)
- );
+ Reason reason = getResponseErrorCause(responseCode);
+ error = new WebserviceError(reason, new IOException("Response code : " + responseCode));
+ if (reason == Reason.TOO_MANY_REQUESTS) {
+ error.setRetryAfter(connection.getHeaderFieldInt("Retry-After", DEFAULT_RETRY_AFTER));
+ }
if (responseCode == WEBSERVICE_ERROR_INVALID_CIPHER) {
enabledDowngradedMode();
}
@@ -514,7 +532,7 @@ public byte[] executeRequest() throws WebserviceError {
}
count++;
- } while (count <= getMaxRetryCount() && shouldRetry(errorCode));
+ } while (count <= getMaxRetryCount() && shouldRetry(responseCode));
throw error;
}
@@ -705,6 +723,10 @@ public static WebserviceError.Reason getResponseErrorCause(int statusCode) {
return Reason.UNEXPECTED_ERROR;
}
+ if (statusCode == 429) {
+ return Reason.TOO_MANY_REQUESTS;
+ }
+
if (statusCode == 404) {
return Reason.NOT_FOUND_ERROR;
}
@@ -1050,6 +1072,14 @@ public static class WebserviceError extends Throwable {
*/
private Reason reason;
+ /**
+ * Number of seconds we have to wait before sending another request to a webservice who failed.
+ *
+ * Server can respond with an HTTP status code 429 ({@link Reason#TOO_MANY_REQUESTS})
+ * and specify the time we have to wait with a 'Retry-After' header.
+ */
+ private int retryAfter = 0;
+
// ------------------------------------------>
/**
@@ -1105,6 +1135,11 @@ public enum Reason {
*/
SERVER_ERROR,
+ /**
+ * Server overloaded (429)
+ */
+ TOO_MANY_REQUESTS,
+
/**
* Server returns a not found status (404)
*/
@@ -1135,6 +1170,21 @@ public enum Reason {
*/
SDK_OPTED_OUT,
}
+
+ /**
+ * Get the time to wait before sending another request
+ * @return retryAfter (in milliseconds)
+ */
+ public int getRetryAfterInMillis() {
+ return retryAfter * 1000;
+ }
+
+ /**
+ * Set the time (in seconds) to wait before sending another request
+ */
+ public void setRetryAfter(int retryAfter) {
+ this.retryAfter = retryAfter;
+ }
}
// -------------------------------------------------->
diff --git a/Sources/sdk/src/main/java/com/batch/android/eventdispatcher/DispatcherSerializer.java b/Sources/sdk/src/main/java/com/batch/android/eventdispatcher/DispatcherSerializer.java
new file mode 100644
index 0000000..f3eac1a
--- /dev/null
+++ b/Sources/sdk/src/main/java/com/batch/android/eventdispatcher/DispatcherSerializer.java
@@ -0,0 +1,57 @@
+package com.batch.android.eventdispatcher;
+
+import androidx.annotation.NonNull;
+import com.batch.android.BatchEventDispatcher;
+import com.batch.android.json.JSONException;
+import com.batch.android.json.JSONObject;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Simple class to serialize event dispatchers
+ */
+public class DispatcherSerializer {
+
+ public static final String FIREBASE_DISPATCHER_NAME = "firebase";
+ public static final String AT_INTERNET_DISPATCHER_NAME = "at_internet";
+ public static final String MIXPANEL_DISPATCHER_NAME = "mixpanel";
+ public static final String GOOGLE_ANALYTICS_DISPATCHER_NAME = "google_analytics";
+ public static final String BATCH_PLUGINS_DISPATCHER_NAME = "batch_plugins";
+
+ private static final String CUSTOM_DISPATCHER_NAME = "other";
+
+ /**
+ * List of dispatchers handled by Batch
+ */
+ private static final List knownDispatchers = Arrays.asList(
+ FIREBASE_DISPATCHER_NAME,
+ AT_INTERNET_DISPATCHER_NAME,
+ MIXPANEL_DISPATCHER_NAME,
+ GOOGLE_ANALYTICS_DISPATCHER_NAME,
+ BATCH_PLUGINS_DISPATCHER_NAME
+ );
+
+ /**
+ * Serialize a list of dispatchers
+ *
+ * @param dispatchers dispatchers to serialize
+ * @return json object
+ */
+ @NonNull
+ public static JSONObject serialize(@NonNull Set dispatchers) {
+ JSONObject json = new JSONObject();
+ for (BatchEventDispatcher dispatcher : dispatchers) {
+ try {
+ if (dispatcher.getName() == null) {
+ continue;
+ }
+ String name = knownDispatchers.contains(dispatcher.getName())
+ ? dispatcher.getName()
+ : CUSTOM_DISPATCHER_NAME;
+ json.put(name, dispatcher.getVersion());
+ } catch (JSONException ignored) {}
+ }
+ return json;
+ }
+}
diff --git a/Sources/sdk/src/main/java/com/batch/android/inbox/InboxDatasource.java b/Sources/sdk/src/main/java/com/batch/android/inbox/InboxDatasource.java
index 96d3c2f..5fb3211 100644
--- a/Sources/sdk/src/main/java/com/batch/android/inbox/InboxDatasource.java
+++ b/Sources/sdk/src/main/java/com/batch/android/inbox/InboxDatasource.java
@@ -377,8 +377,8 @@ protected boolean insert(InboxNotificationContentInternal notification, long fet
final ContentValues values = new ContentValues();
values.put(InboxDatabaseHelper.COLUMN_NOTIFICATION_ID, notification.identifiers.identifier);
values.put(InboxDatabaseHelper.COLUMN_SEND_ID, notification.identifiers.sendID);
- values.put(InboxDatabaseHelper.COLUMN_TITLE, notification.title);
- values.put(InboxDatabaseHelper.COLUMN_BODY, notification.body);
+ values.put(InboxDatabaseHelper.COLUMN_TITLE, notification.title != null ? notification.title : "");
+ values.put(InboxDatabaseHelper.COLUMN_BODY, notification.body != null ? notification.body : "");
values.put(InboxDatabaseHelper.COLUMN_UNREAD, notification.isUnread ? 1 : 0);
values.put(InboxDatabaseHelper.COLUMN_DATE, notification.date.getTime());
diff --git a/Sources/sdk/src/main/java/com/batch/android/inbox/InboxFetcherInternal.java b/Sources/sdk/src/main/java/com/batch/android/inbox/InboxFetcherInternal.java
index 2ce3691..c1f903d 100644
--- a/Sources/sdk/src/main/java/com/batch/android/inbox/InboxFetcherInternal.java
+++ b/Sources/sdk/src/main/java/com/batch/android/inbox/InboxFetcherInternal.java
@@ -63,6 +63,8 @@ public class InboxFetcherInternal {
private InboxDatasource datasource;
+ private boolean filterSilentNotifications = true;
+
private InboxFetcherInternal(
@NonNull TrackerModule trackerModule,
@Nullable InboxDatasource datasource,
@@ -161,6 +163,10 @@ public void setFetchLimit(int fetchLimit) {
this.fetchLimit = fetchLimit;
}
+ public void setFilterSilentNotifications(boolean filterSilentNotifications) {
+ this.filterSilentNotifications = filterSilentNotifications;
+ }
+
public boolean isEndReached() {
return endReached || fetchedNotifications.size() >= fetchLimit;
}
@@ -243,12 +249,19 @@ public void markAsDeleted(BatchInboxNotificationContent notification) {
}
@NonNull
- private static List convertInternalModelsToPublic(
+ private List convertInternalModelsToPublic(
@NonNull List privateNotifications
) {
final List res = new ArrayList<>();
for (InboxNotificationContentInternal privateNotification : privateNotifications) {
- res.add(PrivateNotificationContentHelper.getPublicContent(privateNotification));
+ final BatchInboxNotificationContent publicContent = PrivateNotificationContentHelper.getPublicContent(
+ privateNotification
+ );
+ if (filterSilentNotifications && publicContent.isSilent()) {
+ Logger.verbose(TAG, "Filtering silent notification");
+ continue;
+ }
+ res.add(publicContent);
}
return res;
}
@@ -501,7 +514,7 @@ private List getEventDatas(InboxNotificationContentInternal notifica
@NonNull
public List getPublicFetchedNotifications() {
synchronized (fetchedNotifications) {
- return convertInternalModelsToPublic(this.fetchedNotifications);
+ return convertInternalModelsToPublic(fetchedNotifications);
}
}
diff --git a/Sources/sdk/src/main/java/com/batch/android/localcampaigns/CampaignManager.java b/Sources/sdk/src/main/java/com/batch/android/localcampaigns/CampaignManager.java
index 2a0dff3..f9573a5 100644
--- a/Sources/sdk/src/main/java/com/batch/android/localcampaigns/CampaignManager.java
+++ b/Sources/sdk/src/main/java/com/batch/android/localcampaigns/CampaignManager.java
@@ -1,27 +1,29 @@
package com.batch.android.localcampaigns;
+import static com.batch.android.localcampaigns.model.LocalCampaign.SyncedJITResult;
+import static java.util.concurrent.TimeUnit.DAYS;
import static java.util.concurrent.TimeUnit.SECONDS;
import android.content.Context;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.batch.android.LoggerLevel;
+import com.batch.android.WebserviceLauncher;
import com.batch.android.core.DateProvider;
import com.batch.android.core.Logger;
import com.batch.android.core.Parameters;
+import com.batch.android.core.SystemDateProvider;
+import com.batch.android.core.Webservice;
import com.batch.android.date.BatchDate;
import com.batch.android.di.providers.RuntimeManagerProvider;
-import com.batch.android.di.providers.SecureDateProviderProvider;
import com.batch.android.di.providers.TaskExecutorProvider;
-import com.batch.android.json.JSONArray;
import com.batch.android.json.JSONException;
import com.batch.android.json.JSONObject;
import com.batch.android.localcampaigns.model.LocalCampaign;
import com.batch.android.localcampaigns.persistence.LocalCampaignsFilePersistence;
import com.batch.android.localcampaigns.persistence.LocalCampaignsPersistence;
import com.batch.android.localcampaigns.persistence.PersistenceException;
-import com.batch.android.localcampaigns.serialization.LocalCampaignDeserializer;
-import com.batch.android.localcampaigns.serialization.LocalCampaignSerializer;
import com.batch.android.localcampaigns.signal.Signal;
import com.batch.android.localcampaigns.trigger.EventLocalCampaignTrigger;
import com.batch.android.processor.Module;
@@ -30,11 +32,14 @@
import com.batch.android.query.response.LocalCampaignsResponse;
import com.batch.android.query.serialization.deserializers.LocalCampaignsResponseDeserializer;
import com.batch.android.query.serialization.serializers.LocalCampaignsResponseSerializer;
+import com.batch.android.webservice.listener.LocalCampaignsJITWebserviceListener;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -51,36 +56,78 @@
public class CampaignManager {
private static final String TAG = "CampaignManager";
+
private static final String PERSISTENCE_LOCAL_CAMPAIGNS_FILE_NAME = "com.batch.localcampaigns.persist.json";
- private DateProvider dateProvider = SecureDateProviderProvider.get();
+ /**
+ * Delay to wait before calling the jit webservice again after a fail
+ */
+ private static final int DEFAULT_RETRY_AFTER = 60_000; //ms
+
+ /**
+ * Delay before deleting local campaigns cache if there is no update from server. (15 days)
+ */
+ private static final long CACHE_EXPIRATION_DELAY = DAYS.toMillis(15);
+
+ /**
+ * Max number of campaigns to send to the server for JIT sync.
+ */
+ private static final int MAX_CAMPAIGNS_JIT_THRESHOLD = 5;
+
+ /**
+ * Min delay between two JIT sync (in ms)
+ */
+ private static final int MIN_DELAY_BETWEEN_JIT_SYNC = 15_000;
+
+ /**
+ * Period during cached local campaign requiring a JIT sync is considered as up-to-date.
+ */
+ private static final int JIT_CAMPAIGN_CACHE_PERIOD = 30_000;
+
+ /**
+ * Date provider
+ */
+ private final DateProvider dateProvider = new SystemDateProvider();
- private LocalCampaignsSQLTracker viewTracker;
+ private final LocalCampaignsTracker viewTracker;
private LocalCampaignsPersistence persistor = new LocalCampaignsFilePersistence();
private final List campaignList = new ArrayList<>();
+ private LocalCampaignsResponse.GlobalCappings cappings;
+
private final Object campaignListLock = new Object();
+ /**
+ * Timestamp to wait before JIT service will be available again
+ * Is set when JIT succeed or when server respond with HTTP code 429 and specify a retry-after header.
+ */
+ private long nextAvailableJITTimestamp;
+
/**
* Tells if we loaded at least one time the campaigns list (an empty campains list could mean that
* we didn't load or that the result is empty)
*/
- private AtomicBoolean campaignsLoaded = new AtomicBoolean(false);
+ private final AtomicBoolean campaignsLoaded = new AtomicBoolean(false);
/**
* Cached list of event names that can potentially triggers the display of a local campaign
*/
private Set watchedEventNames = new HashSet<>();
- public CampaignManager(@NonNull LocalCampaignsSQLTracker viewTracker) {
+ /**
+ * Cached list of synced JIT campaigns
+ */
+ private final Map syncedJITCampaigns = new HashMap<>();
+
+ public CampaignManager(@NonNull LocalCampaignsTracker viewTracker) {
this.viewTracker = viewTracker;
}
@Provide
public static CampaignManager provide() {
- return new CampaignManager(new LocalCampaignsSQLTracker());
+ return new CampaignManager(new LocalCampaignsTracker());
}
/**
@@ -136,13 +183,21 @@ public void deleteAllCampaigns(Context context, boolean persist) throws Persiste
}
}
+ public interface JITElectionCampaignListener {
+ void onCampaignElected(@Nullable LocalCampaign electedCampaign);
+ }
+
/**
- * Get the higher priority campaign between all of those that are satisfied by the latest application event
+ * Get all campaign between all of those that are satisfied by the latest application event
+ * and sort them by priority
* This is the campaign that you'll want to display
*/
- public LocalCampaign getCampaignToDisplay(@NonNull Signal signal) {
+ @NonNull
+ public List getEligibleCampaignsSortedByPriority(@NonNull Signal signal) {
synchronized (this.campaignListLock) {
List eligibleCampaigns = new ArrayList<>();
+
+ // Getting campaign eligible for the given signal
for (LocalCampaign campaign : campaignList) {
boolean satisfiesTrigger = false;
for (LocalCampaign.Trigger trigger : campaign.triggers) {
@@ -162,6 +217,8 @@ public LocalCampaign getCampaignToDisplay(@NonNull Signal signal) {
eligibleCampaigns.add(campaign);
}
+
+ // Sorting eligible campaigns by server priority
Collections.sort(
eligibleCampaigns,
Collections.reverseOrder((o1, o2) -> {
@@ -172,17 +229,151 @@ public LocalCampaign getCampaignToDisplay(@NonNull Signal signal) {
return (x < y) ? -1 : ((x == y) ? 0 : 1);
})
);
+ return eligibleCampaigns;
+ }
+ }
- if (eligibleCampaigns.size() > 0) {
- return eligibleCampaigns.get(0);
+ /**
+ * Get eligible campaigns requiring a JIT sync
+ * @param eligibleCampaigns regardless of the JIT sync
+ * @return all eligible campaigns requiring a JIT sync (max: {@link CampaignManager#MAX_CAMPAIGNS_JIT_THRESHOLD})
+ */
+ @NonNull
+ public List getFirstEligibleCampaignsRequiringSync(List eligibleCampaigns) {
+ List eligibleCampaignsRequiringSync = new ArrayList<>();
+ int i = 0;
+ for (LocalCampaign campaign : eligibleCampaigns) {
+ if (i >= MAX_CAMPAIGNS_JIT_THRESHOLD) {
+ break;
+ }
+ if (campaign.requiresJustInTimeSync) {
+ eligibleCampaignsRequiringSync.add(campaign);
} else {
- Logger.internal(TAG, "No eligible campaign was found");
+ break;
}
+ i++;
}
+ return eligibleCampaignsRequiringSync;
+ }
+ /**
+ * Get the first eligible campaign not requiring a JIT sync
+ * @param eligibleCampaigns regardless of the JIT sync
+ * @return the first eligible campaign not requiring a JIT sync
+ */
+ @Nullable
+ public LocalCampaign getFirstCampaignNotRequiringJITSync(@NonNull List eligibleCampaigns) {
+ for (LocalCampaign campaign : eligibleCampaigns) {
+ if (!campaign.requiresJustInTimeSync) {
+ return campaign;
+ }
+ }
return null;
}
+ /**
+ * Checking with server if campaigns are still eligible
+ * @param eligibleCampaignsRequiringSync campaigns to check
+ * @param listener callback
+ */
+ public void verifyCampaignsEligibilityFromServer(
+ @NonNull List eligibleCampaignsRequiringSync,
+ @NonNull JITElectionCampaignListener listener
+ ) {
+ // Assert campaign list are not empty
+ if (eligibleCampaignsRequiringSync.isEmpty()) {
+ listener.onCampaignElected(null);
+ return;
+ }
+
+ if (!isJITServiceAvailable()) {
+ listener.onCampaignElected(null);
+ return;
+ }
+
+ WebserviceLauncher.launchLocalCampaignsJITWebservice(
+ RuntimeManagerProvider.get(),
+ eligibleCampaignsRequiringSync,
+ new LocalCampaignsJITWebserviceListener() {
+ @Override
+ public void onSuccess(List eligibleCampaignIds) {
+ // Saving next jit available timestamp
+ nextAvailableJITTimestamp = dateProvider.getCurrentDate().getTime() + MIN_DELAY_BETWEEN_JIT_SYNC;
+
+ // Handling jit response
+ if (eligibleCampaignIds.isEmpty()) {
+ listener.onCampaignElected(null);
+ } else {
+ for (LocalCampaign campaign : eligibleCampaignsRequiringSync) {
+ SyncedJITResult syncedJITCampaignState = new SyncedJITResult(
+ dateProvider.getCurrentDate().getTime()
+ );
+ if (!eligibleCampaignIds.contains(campaign.id)) {
+ eligibleCampaignsRequiringSync.remove(campaign);
+ syncedJITCampaignState.eligible = false;
+ } else {
+ syncedJITCampaignState.eligible = true;
+ }
+ syncedJITCampaigns.put(campaign.id, syncedJITCampaignState);
+ }
+ if (eligibleCampaignsRequiringSync.isEmpty()) {
+ // Should not happen
+ listener.onCampaignElected(null);
+ } else {
+ listener.onCampaignElected(eligibleCampaignsRequiringSync.get(0));
+ }
+ }
+ }
+
+ @Override
+ public void onFailure(Webservice.WebserviceError error) {
+ // Saving next jit available timestamp
+ long retryAfter = error.getRetryAfterInMillis() != 0
+ ? error.getRetryAfterInMillis()
+ : DEFAULT_RETRY_AFTER;
+ nextAvailableJITTimestamp = dateProvider.getCurrentDate().getTime() + retryAfter;
+ listener.onCampaignElected(null);
+ }
+ }
+ );
+ }
+
+ /**
+ * Check if JIT sync is available
+ *
+ * Meaning MIN_DELAY_BETWEEN_JIT_SYNC or last 'retryAfter' time respond by server is passed.
+ * @return true if JIT service is available
+ */
+ public synchronized boolean isJITServiceAvailable() {
+ return dateProvider.getCurrentDate().getTime() >= nextAvailableJITTimestamp;
+ }
+
+ /**
+ * Check if the given campaign has been already synced recently
+ * @param campaign to check
+ * @return a {@link SyncedJITResult.State}
+ */
+ public SyncedJITResult.State getSyncedJITCampaignState(LocalCampaign campaign) {
+ if (!campaign.requiresJustInTimeSync) {
+ //Should not happen but ensure we do not sync for a non-jit campaign
+ return SyncedJITResult.State.ELIGIBLE;
+ }
+
+ if (!syncedJITCampaigns.containsKey(campaign.id)) {
+ return SyncedJITResult.State.REQUIRES_SYNC;
+ }
+
+ SyncedJITResult syncedJITResult = syncedJITCampaigns.get(campaign.id);
+ if (syncedJITResult == null) {
+ return SyncedJITResult.State.REQUIRES_SYNC;
+ }
+
+ if (dateProvider.getCurrentDate().getTime() >= (syncedJITResult.timestamp + JIT_CAMPAIGN_CACHE_PERIOD)) {
+ return SyncedJITResult.State.REQUIRES_SYNC;
+ }
+ return syncedJITResult.eligible ? SyncedJITResult.State.ELIGIBLE : SyncedJITResult.State.NOT_ELIGIBLE;
+ }
+
/**
* Checks if an event name will triggers at least one campaign, allowing for a fast pre-filter to check if it is worth
* checking other conditions for campaigns with an event triggers
@@ -198,6 +389,22 @@ public List getCampaignList() {
return new ArrayList<>(campaignList);
}
+ /**
+ * Get the global in-app cappings
+ * @return cappings
+ */
+ public LocalCampaignsResponse.GlobalCappings getCappings() {
+ return cappings;
+ }
+
+ /**
+ * Set the global in-app cappings
+ * @param cappings
+ */
+ public void setCappings(LocalCampaignsResponse.GlobalCappings cappings) {
+ this.cappings = cappings;
+ }
+
/**
* Removes campaign that will never be ok, even in the future:
* - Expired campaigns
@@ -321,6 +528,44 @@ protected boolean isCampaignDisplayable(LocalCampaign campaign) {
return true;
}
+ /**
+ * Check if Global Cappings has been reached
+ * @return true if cappings are reached
+ */
+ public boolean isOverGlobalCappings() {
+ if (cappings == null) {
+ // No cappings
+ return false;
+ }
+
+ if (cappings.getSession() != null && viewTracker.getSessionViewsCount() >= cappings.getSession()) {
+ Logger.internal(TAG, "Session capping has been reached");
+ return true;
+ }
+
+ List timeBasedCappings = cappings.getTimeBasedCappings();
+ if (timeBasedCappings != null) {
+ for (LocalCampaignsResponse.GlobalCappings.TimeBasedCapping timeBasedCapping : timeBasedCappings) {
+ if (timeBasedCapping.getDuration() != null && timeBasedCapping.getViews() != null) {
+ long timestamp = dateProvider.getCurrentDate().getTime() - (timeBasedCapping.getDuration() * 1000);
+ try {
+ if (viewTracker.getNumberOfViewEventsSince(timestamp) >= timeBasedCapping.getViews()) {
+ Logger.internal(TAG, "Time-based cappings have been reached");
+ return true;
+ }
+ } catch (ViewTrackerUnavailableException e) {
+ Logger.internal(
+ TAG,
+ "View tracker is unavailable. Campaigns will be prevented from displaying."
+ );
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
/**
* Update the set of watched event names
* This method is not thread safe: do not call it without some kind of lock
@@ -337,22 +582,24 @@ private void updateWatchedEventNames() {
watchedEventNames = newWatchedEvents;
}
- public void saveCampaigns(@NonNull Context context, @NonNull List campaigns) {
+ public void saveCampaigns(@NonNull Context context, @NonNull LocalCampaignsResponse response) {
try {
- LocalCampaignSerializer serializer = new LocalCampaignSerializer();
+ LocalCampaignsResponseSerializer serializer = new LocalCampaignsResponseSerializer();
JSONObject jsonData = new JSONObject();
- jsonData.put("campaigns", serializer.serializeList(campaigns));
+ jsonData.put("campaigns", serializer.serializeCampaigns(response.getCampaignsToSave()));
+ jsonData.putOpt("cappings", serializer.serializeCappings(response.getCappings()));
+ jsonData.putOpt("cache_date", dateProvider.getCurrentDate().getTime());
persistor.persistData(context, jsonData, PERSISTENCE_LOCAL_CAMPAIGNS_FILE_NAME);
} catch (PersistenceException e) {
- Logger.internal(TAG, "Can't persist local campaigns", e);
+ Logger.internal(TAG, "Can't persist local campaigns response", e);
} catch (JSONException e) {
- Logger.internal(TAG, "Can't serialize local campaigns before the save operation", e);
+ Logger.internal(TAG, "Can't serialize local campaigns response before the save operation", e);
e.printStackTrace();
}
}
- public void saveCampaignsAsync(@NonNull final Context context, @NonNull final List campaigns) {
- TaskExecutorProvider.get(context).execute(() -> saveCampaigns(context, campaigns));
+ public void saveCampaignsAsync(@NonNull final Context context, @NonNull final LocalCampaignsResponse response) {
+ TaskExecutorProvider.get(context).execute(() -> saveCampaigns(context, response));
}
public void deleteSavedCampaigns(@NonNull final Context context) {
@@ -389,10 +636,23 @@ public boolean loadSavedCampaignResponse(@NonNull final Context context) {
return false;
}
- LocalCampaignDeserializer localCampaignDeserializer = new LocalCampaignDeserializer();
+ // Ensure cache is not too old.
+ Long expirationDate = campaignsRawData.reallyOptLong("cache_date", null);
+ if (expirationDate != null) {
+ expirationDate += CACHE_EXPIRATION_DELAY;
+ if (expirationDate <= dateProvider.getCurrentDate().getTime()) {
+ Logger.internal(TAG, "Local campaign cache is too old, deleting it.");
+ deleteSavedCampaignsAsync(context);
+ return false;
+ }
+ }
+
+ LocalCampaignsResponseDeserializer localCampaignResponseDeserializer = new LocalCampaignsResponseDeserializer(
+ campaignsRawData
+ );
try {
- JSONArray jsonCampaigns = campaignsRawData.getJSONArray("campaigns");
- List campaigns = localCampaignDeserializer.deserializeList(jsonCampaigns);
+ List campaigns = localCampaignResponseDeserializer.deserializeCampaigns();
+ cappings = localCampaignResponseDeserializer.deserializeCappings();
updateCampaignList(campaigns);
} catch (Exception ex) {
Logger.internal(TAG, "Can't convert json to LocalCampaignsResponse : " + ex.toString());
diff --git a/Sources/sdk/src/main/java/com/batch/android/localcampaigns/LocalCampaignTrackDbHelper.java b/Sources/sdk/src/main/java/com/batch/android/localcampaigns/LocalCampaignTrackDbHelper.java
index d5bec75..9aba21a 100644
--- a/Sources/sdk/src/main/java/com/batch/android/localcampaigns/LocalCampaignTrackDbHelper.java
+++ b/Sources/sdk/src/main/java/com/batch/android/localcampaigns/LocalCampaignTrackDbHelper.java
@@ -8,7 +8,7 @@
public final class LocalCampaignTrackDbHelper extends SQLiteOpenHelper {
- public static final int DATABASE_VERSION = 1;
+ public static final int DATABASE_VERSION = 2;
public static final String DATABASE_NAME = "LocalCampaignsSQLTracker.db";
public static class LocalCampaignEntry implements BaseColumns {
@@ -18,8 +18,18 @@ public static class LocalCampaignEntry implements BaseColumns {
public static final String COLUMN_NAME_CAMPAIGN_KIND = "kind";
public static final String COLUMN_NAME_CAMPAIGN_LAST_OCCURRENCE = "last_oc";
public static final String COLUMN_NAME_CAMPAIGN_COUNT = "count";
+
+ // New table added in version 2 to store every view event tracked
+ public static final String TABLE_VIEW_EVENTS_NAME = "view_events";
+ public static final String COLUMN_NAME_VE_CAMPAIGN_ID = "campaign_id";
+ public static final String COLUMN_NAME_VE_TIMESTAMP = "timestamp_ms";
+
+ public static final String TRIGGER_VIEW_EVENTS_NAME = "trigger_clean_view_events";
}
+ /**
+ * SQL request to create the initial view tracker table to count view events per campaign
+ */
private static final String SQL_CREATE_ENTRIES =
"CREATE TABLE " +
LocalCampaignEntry.TABLE_NAME +
@@ -40,8 +50,53 @@ public static class LocalCampaignEntry implements BaseColumns {
LocalCampaignEntry.COLUMN_NAME_CAMPAIGN_KIND +
") on conflict replace)";
+ /**
+ * SQL request to delete the view tracker table
+ */
private static final String SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS " + LocalCampaignEntry.TABLE_NAME;
+ /**
+ * SQL request to create the view event table to store every view events
+ * Must be clean when size is 100 entries
+ */
+ private static final String SQL_CREATE_VIEW_EVENTS_TABLE =
+ "CREATE TABLE " +
+ LocalCampaignEntry.TABLE_VIEW_EVENTS_NAME +
+ " (" +
+ LocalCampaignEntry._ID +
+ " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ LocalCampaignEntry.COLUMN_NAME_VE_CAMPAIGN_ID +
+ " TEXT," +
+ LocalCampaignEntry.COLUMN_NAME_VE_TIMESTAMP +
+ " INTEGER NOT NULL" +
+ ")";
+
+ /**
+ * SQL request to create a trigger when a new view events is inserted.
+ * When triggered, check if the table has more than 100 rows and delete the oldest.
+ */
+ private static final String SQL_CREATE_TRIGGER_VIEW_EVENT_DELETE_ROWS =
+ "CREATE TRIGGER " +
+ LocalCampaignEntry.TRIGGER_VIEW_EVENTS_NAME +
+ " AFTER INSERT ON " +
+ LocalCampaignEntry.TABLE_VIEW_EVENTS_NAME +
+ " BEGIN" +
+ " DELETE FROM " +
+ LocalCampaignEntry.TABLE_VIEW_EVENTS_NAME +
+ " WHERE " +
+ LocalCampaignEntry.COLUMN_NAME_VE_TIMESTAMP +
+ "=(" +
+ " SELECT min(" +
+ LocalCampaignEntry.COLUMN_NAME_VE_TIMESTAMP +
+ ") " +
+ " FROM " +
+ LocalCampaignEntry.TABLE_VIEW_EVENTS_NAME +
+ " )" +
+ " AND (SELECT count(*) from " +
+ LocalCampaignEntry.TABLE_VIEW_EVENTS_NAME +
+ " )>100;" +
+ " END;";
+
public LocalCampaignTrackDbHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@@ -49,11 +104,16 @@ public LocalCampaignTrackDbHelper(Context context) {
@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
sqLiteDatabase.execSQL(SQL_CREATE_ENTRIES);
+ sqLiteDatabase.execSQL(SQL_CREATE_VIEW_EVENTS_TABLE);
+ sqLiteDatabase.execSQL(SQL_CREATE_TRIGGER_VIEW_EVENT_DELETE_ROWS);
}
@Override
- public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {
- // Empty now
+ public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
+ if (oldVersion < 2) {
+ sqLiteDatabase.execSQL(SQL_CREATE_VIEW_EVENTS_TABLE);
+ sqLiteDatabase.execSQL(SQL_CREATE_TRIGGER_VIEW_EVENT_DELETE_ROWS);
+ }
}
/**
diff --git a/Sources/sdk/src/main/java/com/batch/android/localcampaigns/LocalCampaignsSQLTracker.java b/Sources/sdk/src/main/java/com/batch/android/localcampaigns/LocalCampaignsSQLTracker.java
index a776a1c..4e3d966 100644
--- a/Sources/sdk/src/main/java/com/batch/android/localcampaigns/LocalCampaignsSQLTracker.java
+++ b/Sources/sdk/src/main/java/com/batch/android/localcampaigns/LocalCampaignsSQLTracker.java
@@ -14,7 +14,7 @@
import java.util.List;
import java.util.Map;
-public final class LocalCampaignsSQLTracker implements ViewTracker {
+public class LocalCampaignsSQLTracker implements ViewTracker {
private static final String TAG = "LocalCampaignsSQLTracker";
private LocalCampaignTrackDbHelper dbHelper;
@@ -88,6 +88,16 @@ public ViewTracker.CountedViewEvent trackViewEvent(@NonNull String campaignID)
new String[] { campaignID, Integer.toString(ev.count), Long.toString(ev.lastOccurrence) }
);
+ database.execSQL(
+ "INSERT INTO " +
+ LocalCampaignEntry.TABLE_VIEW_EVENTS_NAME +
+ " (" +
+ LocalCampaignEntry.COLUMN_NAME_VE_CAMPAIGN_ID +
+ ", " +
+ LocalCampaignEntry.COLUMN_NAME_VE_TIMESTAMP +
+ ") VALUES (?, ?)",
+ new String[] { campaignID, Long.toString(ev.lastOccurrence) }
+ );
return ev;
}
@@ -191,6 +201,31 @@ public long campaignLastOccurrence(@NonNull String campaignID) throws ViewTracke
return lastOccurence;
}
+ @Override
+ public int getNumberOfViewEventsSince(long timestamp) throws ViewTrackerUnavailableException {
+ ensureWritableDatabase();
+ int total = 0;
+ Cursor countCursor = database.rawQuery(
+ "SELECT COUNT(*) " +
+ " FROM " +
+ LocalCampaignEntry.TABLE_VIEW_EVENTS_NAME +
+ " WHERE " +
+ LocalCampaignEntry.COLUMN_NAME_VE_TIMESTAMP +
+ " > ?",
+ new String[] { Long.toString(timestamp) }
+ );
+ if (countCursor.moveToFirst()) {
+ total = countCursor.getInt(0);
+ }
+ countCursor.close();
+ return total;
+ }
+
+ public void deleteViewEvents() throws ViewTrackerUnavailableException {
+ ensureWritableDatabase();
+ database.execSQL("DELETE FROM " + LocalCampaignEntry.TABLE_VIEW_EVENTS_NAME);
+ }
+
private void ensureWritableDatabase() throws ViewTrackerUnavailableException {
if (database == null) {
if (dbHelper == null) {
diff --git a/Sources/sdk/src/main/java/com/batch/android/localcampaigns/LocalCampaignsTracker.java b/Sources/sdk/src/main/java/com/batch/android/localcampaigns/LocalCampaignsTracker.java
new file mode 100644
index 0000000..ea79c51
--- /dev/null
+++ b/Sources/sdk/src/main/java/com/batch/android/localcampaigns/LocalCampaignsTracker.java
@@ -0,0 +1,39 @@
+package com.batch.android.localcampaigns;
+
+import androidx.annotation.NonNull;
+
+public final class LocalCampaignsTracker extends LocalCampaignsSQLTracker {
+
+ /**
+ * Count of the views tracked during the user session.
+ * This counter is reset when a new session start.
+ */
+ private int sessionViewsCount = 0;
+
+ /**
+ * Reset the session view count
+ */
+ public void resetSessionViewsCount() {
+ this.sessionViewsCount = 0;
+ }
+
+ /**
+ * Get the count of in-apps viewed during the session
+ * @return sessionViewsCount
+ */
+ public int getSessionViewsCount() {
+ return sessionViewsCount;
+ }
+
+ /**
+ * Track
+ * @param campaignID Campaign ID
+ * @return
+ * @throws ViewTrackerUnavailableException
+ */
+ @Override
+ public CountedViewEvent trackViewEvent(@NonNull String campaignID) throws ViewTrackerUnavailableException {
+ sessionViewsCount++;
+ return super.trackViewEvent(campaignID);
+ }
+}
diff --git a/Sources/sdk/src/main/java/com/batch/android/localcampaigns/ViewTracker.java b/Sources/sdk/src/main/java/com/batch/android/localcampaigns/ViewTracker.java
index d84176c..f961643 100644
--- a/Sources/sdk/src/main/java/com/batch/android/localcampaigns/ViewTracker.java
+++ b/Sources/sdk/src/main/java/com/batch/android/localcampaigns/ViewTracker.java
@@ -41,6 +41,14 @@ public interface ViewTracker {
*/
long campaignLastOccurrence(@NonNull String campaignId) throws ViewTrackerUnavailableException;
+ /**
+ * Get the number of view event tracked since a given timestamp
+ * @param timestamp date (timestamp in ms)
+ * @return total view events since the given date
+ * @throws ViewTrackerUnavailableException exception
+ */
+ int getNumberOfViewEventsSince(long timestamp) throws ViewTrackerUnavailableException;
+
class CountedViewEvent {
@NonNull
diff --git a/Sources/sdk/src/main/java/com/batch/android/localcampaigns/model/LocalCampaign.java b/Sources/sdk/src/main/java/com/batch/android/localcampaigns/model/LocalCampaign.java
index 8bfb331..155ffb2 100644
--- a/Sources/sdk/src/main/java/com/batch/android/localcampaigns/model/LocalCampaign.java
+++ b/Sources/sdk/src/main/java/com/batch/android/localcampaigns/model/LocalCampaign.java
@@ -126,6 +126,11 @@ public class LocalCampaign {
@Nullable
public JSONObject customPayload;
+ /**
+ * Flag indicating if this campaign must be verified from the server before being displayed
+ */
+ public boolean requiresJustInTimeSync;
+
public void generateOccurrenceID() {
try {
eventData.put("i", Long.toString(System.currentTimeMillis()));
@@ -175,4 +180,34 @@ public Output(@NonNull JSONObject payload) {
*/
protected abstract boolean displayMessage(LocalCampaign campaign);
}
+
+ /**
+ * Class used to cache the result of a LocalCampaign after a JIT sync.
+ * Keep the timestamp of the sync and whether the campaign was eligible or not.
+ */
+ public static class SyncedJITResult {
+
+ /**
+ * Possible states for a synced JIT campaign
+ */
+ public enum State {
+ ELIGIBLE,
+ NOT_ELIGIBLE,
+ REQUIRES_SYNC,
+ }
+
+ /**
+ * Timestamp of the sync
+ */
+ public long timestamp;
+
+ /**
+ * Whether the campaign was eligible or not after the sync
+ */
+ public boolean eligible;
+
+ public SyncedJITResult(long timestamp) {
+ this.timestamp = timestamp;
+ }
+ }
}
diff --git a/Sources/sdk/src/main/java/com/batch/android/localcampaigns/output/ActionOutput.java b/Sources/sdk/src/main/java/com/batch/android/localcampaigns/output/ActionOutput.java
new file mode 100644
index 0000000..640ff8d
--- /dev/null
+++ b/Sources/sdk/src/main/java/com/batch/android/localcampaigns/output/ActionOutput.java
@@ -0,0 +1,64 @@
+package com.batch.android.localcampaigns.output;
+
+import android.content.Context;
+import android.text.TextUtils;
+import androidx.annotation.NonNull;
+import com.batch.android.core.Logger;
+import com.batch.android.di.providers.ActionModuleProvider;
+import com.batch.android.di.providers.RuntimeManagerProvider;
+import com.batch.android.json.JSONObject;
+import com.batch.android.localcampaigns.model.LocalCampaign;
+import com.batch.android.module.LocalCampaignsModule;
+import com.batch.android.processor.Module;
+import com.batch.android.processor.Provide;
+import com.batch.android.runtime.RuntimeManager;
+
+@Module
+public class ActionOutput extends LocalCampaign.Output {
+
+ public ActionOutput(@NonNull JSONObject payload) {
+ super(payload);
+ }
+
+ @Provide
+ public static ActionOutput provide(@NonNull JSONObject payload) {
+ return new ActionOutput(payload);
+ }
+
+ @Override
+ protected boolean displayMessage(LocalCampaign campaign) {
+ final RuntimeManager runtimeManager = RuntimeManagerProvider.get();
+ Context targetContext = runtimeManager.getActivity();
+ if (targetContext == null) {
+ Logger.warning(
+ LocalCampaignsModule.TAG,
+ "Could not find an activity to run the action on, falling back on context."
+ );
+
+ targetContext = runtimeManager.getContext();
+ }
+ if (targetContext == null) {
+ Logger.warning(
+ LocalCampaignsModule.TAG,
+ "Could not find any context to run the action on: action might fail."
+ );
+ }
+
+ String actionIdentifier = payload.reallyOptString("action", null);
+ if (TextUtils.isEmpty(actionIdentifier)) {
+ Logger.error(LocalCampaignsModule.TAG, "Invalid action name, stopping.");
+ return false;
+ }
+
+ JSONObject actionArgs = payload.optJSONObject("args");
+
+ if (actionArgs == null) {
+ actionArgs = new JSONObject();
+ }
+
+ // Maybe add a UserActionSource in the future if this becomes a real product
+ // InAppMessageUserActionSource isn't right for the job here, as it's tightly coupled
+ // to the landings.
+ return ActionModuleProvider.get().performAction(targetContext, actionIdentifier, actionArgs, null);
+ }
+}
diff --git a/Sources/sdk/src/main/java/com/batch/android/localcampaigns/serialization/LocalCampaignDeserializer.java b/Sources/sdk/src/main/java/com/batch/android/localcampaigns/serialization/LocalCampaignDeserializer.java
index 0d96753..a2aa5fd 100644
--- a/Sources/sdk/src/main/java/com/batch/android/localcampaigns/serialization/LocalCampaignDeserializer.java
+++ b/Sources/sdk/src/main/java/com/batch/android/localcampaigns/serialization/LocalCampaignDeserializer.java
@@ -4,16 +4,15 @@
import com.batch.android.core.Logger;
import com.batch.android.date.TimezoneAwareDate;
import com.batch.android.date.UTCDate;
+import com.batch.android.di.providers.ActionOutputProvider;
import com.batch.android.di.providers.LandingOutputProvider;
import com.batch.android.json.JSONArray;
import com.batch.android.json.JSONException;
import com.batch.android.json.JSONObject;
import com.batch.android.localcampaigns.model.LocalCampaign;
-import com.batch.android.localcampaigns.trigger.CampaignsLoadedTrigger;
-import com.batch.android.localcampaigns.trigger.CampaignsRefreshedTrigger;
+import com.batch.android.localcampaigns.output.ActionOutput;
import com.batch.android.localcampaigns.trigger.EventLocalCampaignTrigger;
import com.batch.android.localcampaigns.trigger.NextSessionTrigger;
-import com.batch.android.localcampaigns.trigger.NowTrigger;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
@@ -110,6 +109,8 @@ public LocalCampaign deserialize(JSONObject json) throws JSONException {
campaign.customPayload = json.optJSONObject("customPayload");
+ campaign.requiresJustInTimeSync = json.reallyOptBoolean("requireJIT", false);
+
return campaign;
}
@@ -155,6 +156,8 @@ private LocalCampaign.Output parseOutput(JSONObject json) throws JSONException {
JSONObject payload = json.getJSONObject("payload");
if ("LANDING".equals(type)) {
output = LandingOutputProvider.get(payload);
+ } else if ("ACTION".equals(type)) {
+ output = ActionOutputProvider.get(payload);
} else {
throw new JSONException("Invalid campaign output type");
}
@@ -202,12 +205,8 @@ private LocalCampaign.Trigger parseTrigger(JSONObject json) throws JSONException
type = type.toUpperCase(Locale.US);
switch (type) {
+ // Workaround to handle deprecated ASAP trigger as NEXT_SESSION (post-sync)
case "NOW":
- return new NowTrigger();
- case "CAMPAIGNS_REFRESHED":
- return new CampaignsRefreshedTrigger();
- case "CAMPAIGNS_LOADED":
- return new CampaignsLoadedTrigger();
case "NEXT_SESSION":
return new NextSessionTrigger();
case "EVENT":
diff --git a/Sources/sdk/src/main/java/com/batch/android/localcampaigns/serialization/LocalCampaignSerializer.java b/Sources/sdk/src/main/java/com/batch/android/localcampaigns/serialization/LocalCampaignSerializer.java
index 3314242..ea0a6a6 100644
--- a/Sources/sdk/src/main/java/com/batch/android/localcampaigns/serialization/LocalCampaignSerializer.java
+++ b/Sources/sdk/src/main/java/com/batch/android/localcampaigns/serialization/LocalCampaignSerializer.java
@@ -62,6 +62,7 @@ public JSONObject serialize(LocalCampaign campaign) throws JSONException {
if (campaign.customPayload != null) {
jsonCampaign.put("customPayload", campaign.customPayload);
}
+ jsonCampaign.put("requireJIT", campaign.requiresJustInTimeSync);
return jsonCampaign;
}
diff --git a/Sources/sdk/src/main/java/com/batch/android/localcampaigns/signal/CampaignsLoadedSignal.java b/Sources/sdk/src/main/java/com/batch/android/localcampaigns/signal/CampaignsLoadedSignal.java
deleted file mode 100644
index ece12c5..0000000
--- a/Sources/sdk/src/main/java/com/batch/android/localcampaigns/signal/CampaignsLoadedSignal.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.batch.android.localcampaigns.signal;
-
-import com.batch.android.localcampaigns.model.LocalCampaign;
-import com.batch.android.localcampaigns.trigger.CampaignsLoadedTrigger;
-import com.batch.android.localcampaigns.trigger.NextSessionTrigger;
-import com.batch.android.localcampaigns.trigger.NowTrigger;
-
-/**
- * Event that occurs when the in-app campaign list has been updated from any source
- *
- * It actually matches the empty trigger, which shows campaigns when the list is loaded
- * (be it at the first SDK start from disk, or when we get an answer from the backend)
- */
-
-public class CampaignsLoadedSignal implements Signal {
-
- public boolean satisfiesTrigger(LocalCampaign.Trigger trigger) {
- return (
- trigger instanceof NowTrigger ||
- trigger instanceof CampaignsLoadedTrigger ||
- trigger instanceof NextSessionTrigger
- );
- }
-}
diff --git a/Sources/sdk/src/main/java/com/batch/android/localcampaigns/signal/CampaignsRefreshedSignal.java b/Sources/sdk/src/main/java/com/batch/android/localcampaigns/signal/CampaignsRefreshedSignal.java
deleted file mode 100644
index e5aa99a..0000000
--- a/Sources/sdk/src/main/java/com/batch/android/localcampaigns/signal/CampaignsRefreshedSignal.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package com.batch.android.localcampaigns.signal;
-
-import com.batch.android.localcampaigns.model.LocalCampaign;
-import com.batch.android.localcampaigns.trigger.CampaignsRefreshedTrigger;
-import com.batch.android.localcampaigns.trigger.NowTrigger;
-
-/**
- * Event that occurs when the in-app campaign list has been updated from any source
- *
- * It actually matches the empty trigger, which shows campaigns when the list is loaded
- * (be it at the first SDK start from disk, or when we get an answer from the backend)
- */
-
-public class CampaignsRefreshedSignal implements Signal {
-
- public boolean satisfiesTrigger(LocalCampaign.Trigger trigger) {
- return trigger instanceof NowTrigger || trigger instanceof CampaignsRefreshedTrigger;
- }
-}
diff --git a/Sources/sdk/src/main/java/com/batch/android/localcampaigns/trigger/CampaignsLoadedTrigger.java b/Sources/sdk/src/main/java/com/batch/android/localcampaigns/trigger/CampaignsLoadedTrigger.java
deleted file mode 100644
index a206e3f..0000000
--- a/Sources/sdk/src/main/java/com/batch/android/localcampaigns/trigger/CampaignsLoadedTrigger.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.batch.android.localcampaigns.trigger;
-
-import com.batch.android.localcampaigns.model.LocalCampaign;
-
-public class CampaignsLoadedTrigger implements LocalCampaign.Trigger {
-
- @Override
- public String getType() {
- return "CAMPAIGNS_LOADED";
- }
-}
diff --git a/Sources/sdk/src/main/java/com/batch/android/localcampaigns/trigger/CampaignsRefreshedTrigger.java b/Sources/sdk/src/main/java/com/batch/android/localcampaigns/trigger/CampaignsRefreshedTrigger.java
deleted file mode 100644
index ef0ca55..0000000
--- a/Sources/sdk/src/main/java/com/batch/android/localcampaigns/trigger/CampaignsRefreshedTrigger.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.batch.android.localcampaigns.trigger;
-
-import com.batch.android.localcampaigns.model.LocalCampaign;
-
-public class CampaignsRefreshedTrigger implements LocalCampaign.Trigger {
-
- @Override
- public String getType() {
- return "CAMPAIGNS_REFRESHED";
- }
-}
diff --git a/Sources/sdk/src/main/java/com/batch/android/localcampaigns/trigger/NowTrigger.java b/Sources/sdk/src/main/java/com/batch/android/localcampaigns/trigger/NowTrigger.java
deleted file mode 100644
index 08567d5..0000000
--- a/Sources/sdk/src/main/java/com/batch/android/localcampaigns/trigger/NowTrigger.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package com.batch.android.localcampaigns.trigger;
-
-import com.batch.android.localcampaigns.model.LocalCampaign;
-
-/**
- * Trigger displaying campaigns as soon as possible
- */
-
-public class NowTrigger implements LocalCampaign.Trigger {
-
- @Override
- public String getType() {
- return "NOW";
- }
-}
diff --git a/Sources/sdk/src/main/java/com/batch/android/metrics/MetricManager.java b/Sources/sdk/src/main/java/com/batch/android/metrics/MetricManager.java
new file mode 100644
index 0000000..d212c0a
--- /dev/null
+++ b/Sources/sdk/src/main/java/com/batch/android/metrics/MetricManager.java
@@ -0,0 +1,180 @@
+package com.batch.android.metrics;
+
+import android.content.Context;
+import com.batch.android.WebserviceLauncher;
+import com.batch.android.core.DateProvider;
+import com.batch.android.core.Logger;
+import com.batch.android.core.SystemDateProvider;
+import com.batch.android.core.TaskRunnable;
+import com.batch.android.core.Webservice;
+import com.batch.android.di.providers.RuntimeManagerProvider;
+import com.batch.android.metrics.model.Counter;
+import com.batch.android.metrics.model.Metric;
+import com.batch.android.metrics.model.Observation;
+import com.batch.android.post.MetricPostDataProvider;
+import com.batch.android.processor.Module;
+import com.batch.android.processor.Provide;
+import com.batch.android.processor.Singleton;
+import com.batch.android.webservice.listener.MetricWebserviceListener;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+@Module
+@Singleton
+public class MetricManager {
+
+ private static final String TAG = "MetricManager";
+
+ /**
+ * Delay to wait before calling the metric webservice again after a fail
+ */
+ private static final int DEFAULT_RETRY_AFTER = 60_000; //ms
+
+ /**
+ * Delay to wait if other metrics are to send
+ */
+ private static final int DELAY_BEFORE_SENDING = 1000; //ms
+
+ /**
+ * List of metrics registered
+ */
+ private final List> metrics = new ArrayList<>();
+
+ /**
+ * Flag indicating whether we has started sending metrics
+ */
+ private final AtomicBoolean isSending = new AtomicBoolean();
+
+ /**
+ * Single thread scheduled executor
+ */
+ private final ScheduledExecutorService sendExecutor = Executors.newSingleThreadScheduledExecutor();
+
+ /**
+ * Timestamp to wait before metrics webservice will be available again
+ */
+ private long nextMetricServiceAvailableTimestamp;
+
+ /**
+ * System date provider
+ */
+ private final DateProvider dateProvider = new SystemDateProvider();
+
+ @Provide
+ public static MetricManager provide() {
+ return new MetricManager();
+ }
+
+ public void addMetric(Metric> metric) {
+ synchronized (metrics) {
+ this.metrics.add(metric);
+ }
+ }
+
+ /**
+ * Get metrics to send
+ *
+ * @return metrics
+ */
+ private List> getMetricsToSend() {
+ synchronized (metrics) {
+ List> metricsToSend = new ArrayList<>();
+ for (Metric> metric : metrics) {
+ if (metric.hasChildren()) {
+ for (Object child : metric.getChildren().values()) {
+ if (child instanceof Counter) {
+ Counter counter = (Counter) child;
+ if (counter.hasChanged()) {
+ metricsToSend.add(new Counter((Counter) child));
+ counter.reset();
+ }
+ } else {
+ Observation observation = ((Observation) child);
+ if (observation.hasChanged()) {
+ metricsToSend.add(new Observation((Observation) child));
+ observation.reset();
+ }
+ }
+ }
+ } else {
+ if (metric.hasChanged()) {
+ if (metric instanceof Counter) {
+ metricsToSend.add(new Counter((Counter) metric));
+ } else {
+ metricsToSend.add(new Observation((Observation) metric));
+ }
+ metric.reset();
+ }
+ }
+ }
+ return metricsToSend;
+ }
+ }
+
+ /**
+ * Check if the metric webservice is available.
+ *
+ * @return true if service is available or false if we have to wait
+ */
+ private boolean isMetricServiceAvailable() {
+ return dateProvider.getCurrentDate().getTime() >= nextMetricServiceAvailableTimestamp;
+ }
+
+ /**
+ * Send the metrics
+ */
+ public void sendMetrics() {
+ if (isSending.get()) {
+ // We are already sending metrics
+ return;
+ }
+
+ if (!isMetricServiceAvailable()) {
+ // Server looks like overloaded, we wait
+ return;
+ }
+
+ isSending.set(true);
+ Context context = RuntimeManagerProvider.get().getContext();
+
+ sendExecutor.schedule(
+ () -> {
+ List> metricsToSend = getMetricsToSend();
+ if (metricsToSend.isEmpty()) {
+ return;
+ }
+ MetricPostDataProvider dataProvider = new MetricPostDataProvider(metricsToSend);
+ TaskRunnable runnable = WebserviceLauncher.initMetricWebservice(
+ context,
+ dataProvider,
+ new MetricWebserviceListener() {
+ @Override
+ public void onSuccess() {
+ Logger.info(TAG, "Metrics sent with success.");
+ isSending.set(false);
+ }
+
+ @Override
+ public void onFailure(Webservice.WebserviceError error) {
+ Logger.info(TAG, "Fail sending metrics.");
+ long retryAfter = error.getRetryAfterInMillis() != 0
+ ? error.getRetryAfterInMillis()
+ : DEFAULT_RETRY_AFTER;
+ nextMetricServiceAvailableTimestamp = dateProvider.getCurrentDate().getTime() + retryAfter;
+ isSending.set(false);
+ }
+ }
+ );
+ if (runnable != null) {
+ runnable.run();
+ }
+ },
+ DELAY_BEFORE_SENDING,
+ TimeUnit.MILLISECONDS
+ );
+ }
+}
diff --git a/Sources/sdk/src/main/java/com/batch/android/metrics/MetricRegistry.java b/Sources/sdk/src/main/java/com/batch/android/metrics/MetricRegistry.java
new file mode 100644
index 0000000..95afa7b
--- /dev/null
+++ b/Sources/sdk/src/main/java/com/batch/android/metrics/MetricRegistry.java
@@ -0,0 +1,27 @@
+package com.batch.android.metrics;
+
+import com.batch.android.metrics.model.Counter;
+import com.batch.android.metrics.model.Observation;
+
+/**
+ * Simple class to centralize registered metrics
+ */
+public final class MetricRegistry {
+
+ // Monitor local campaigns JIT response time
+ public static final Observation localCampaignsJITResponseTime = new Observation(
+ "sdk_local_campaigns_jit_ws_duration"
+ )
+ .register();
+
+ // Monitor local campaign ws call by status ("OK", "KO")
+ public static final Counter localCampaignsJITCount = new Counter("sdk_local_campaigns_jit_ws_count")
+ .labelNames("status")
+ .register();
+
+ // Monitor local campaigns sync response time
+ public static final Observation localCampaignsSyncResponseTime = new Observation(
+ "sdk_local_campaigns_sync_ws_duration"
+ )
+ .register();
+}
diff --git a/Sources/sdk/src/main/java/com/batch/android/metrics/model/Counter.java b/Sources/sdk/src/main/java/com/batch/android/metrics/model/Counter.java
new file mode 100644
index 0000000..c292b3d
--- /dev/null
+++ b/Sources/sdk/src/main/java/com/batch/android/metrics/model/Counter.java
@@ -0,0 +1,47 @@
+package com.batch.android.metrics.model;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class Counter extends Metric {
+
+ private float value;
+
+ public Counter(Counter counter) {
+ super(counter.name);
+ this.type = counter.type;
+ this.value = counter.value;
+ this.values = new ArrayList<>(counter.values);
+ this.children = new ConcurrentHashMap<>(counter.children);
+ this.labelNames = counter.labelNames;
+ this.labelValues = counter.labelValues;
+ }
+
+ public Counter(String name) {
+ super(name);
+ type = Type.COUNTER;
+ values = new ArrayList<>();
+ }
+
+ @Override
+ protected Counter newChild(List labels) {
+ Counter counter = new Counter(name).labelNames(labelNames.toArray(new String[0]));
+ counter.labelValues = labels;
+ return counter;
+ }
+
+ @Override
+ public void reset() {
+ value = 0f;
+ values.clear();
+ children.clear();
+ }
+
+ public void inc() {
+ value++;
+ values.clear();
+ values.add(value);
+ update();
+ }
+}
diff --git a/Sources/sdk/src/main/java/com/batch/android/metrics/model/Metric.java b/Sources/sdk/src/main/java/com/batch/android/metrics/model/Metric.java
new file mode 100644
index 0000000..94dae71
--- /dev/null
+++ b/Sources/sdk/src/main/java/com/batch/android/metrics/model/Metric.java
@@ -0,0 +1,110 @@
+package com.batch.android.metrics.model;
+
+import com.batch.android.di.providers.MetricManagerProvider;
+import com.batch.android.msgpack.MessagePackHelper;
+import com.batch.android.msgpack.core.MessageBufferPacker;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+public abstract class Metric {
+
+ protected interface Type {
+ String COUNTER = "counter";
+ String OBSERVATION = "observation";
+ }
+
+ protected final String name;
+
+ protected String type;
+
+ protected List values;
+
+ protected List labelNames;
+
+ protected List labelValues;
+
+ protected ConcurrentMap, Child> children = new ConcurrentHashMap<>();
+
+ public Metric(String name) {
+ this.name = name;
+ }
+
+ public Child register() {
+ MetricManagerProvider.get().addMetric(this);
+ return (Child) this;
+ }
+
+ public Child labelNames(String... labels) {
+ this.labelNames = Arrays.asList(labels);
+ return (Child) this;
+ }
+
+ public Child labels(String... labels) {
+ Child child = this.children.get(Arrays.asList(labels));
+ if (child == null) {
+ List labelsValues = Arrays.asList(labels);
+ child = newChild(labelsValues);
+ this.children.put(labelsValues, child);
+ }
+ return child;
+ }
+
+ public abstract void reset();
+
+ protected abstract Child newChild(List labels);
+
+ public void pack(MessageBufferPacker packer) throws Exception {
+ Map objectMap = new HashMap<>();
+ objectMap.put("name", name);
+ objectMap.put("type", type);
+ objectMap.put("values", values);
+ if (labelNames != null && labelValues != null && labelNames.size() == labelValues.size()) {
+ Map labels = new HashMap<>();
+ for (int i = 0; i < labelNames.size(); i++) {
+ labels.put(labelNames.get(i), labelValues.get(i));
+ }
+ objectMap.put("labels", labels);
+ }
+ MessagePackHelper.packObject(packer, objectMap);
+ }
+
+ protected void update() {
+ MetricManagerProvider.get().sendMetrics();
+ }
+
+ public boolean hasChanged() {
+ return values.size() > 0;
+ }
+
+ public boolean hasChildren() {
+ return children.size() > 0;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public List getValues() {
+ return values;
+ }
+
+ public List getLabelNames() {
+ return labelNames;
+ }
+
+ public List getLabelValues() {
+ return labelValues;
+ }
+
+ public ConcurrentMap, Child> getChildren() {
+ return children;
+ }
+}
diff --git a/Sources/sdk/src/main/java/com/batch/android/metrics/model/Observation.java b/Sources/sdk/src/main/java/com/batch/android/metrics/model/Observation.java
new file mode 100644
index 0000000..31032e7
--- /dev/null
+++ b/Sources/sdk/src/main/java/com/batch/android/metrics/model/Observation.java
@@ -0,0 +1,62 @@
+package com.batch.android.metrics.model;
+
+import static java.lang.System.currentTimeMillis;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class Observation extends Metric {
+
+ private long startTime;
+
+ private AtomicBoolean observing = new AtomicBoolean();
+
+ public Observation(String name) {
+ super(name);
+ type = Type.OBSERVATION;
+ values = new ArrayList<>();
+ }
+
+ public Observation(Observation observation) {
+ super(observation.name);
+ this.type = observation.type;
+ this.values = observation.values;
+ this.values = new ArrayList<>(observation.values);
+ this.children = new ConcurrentHashMap<>(observation.children);
+ this.labelNames = observation.labelNames;
+ this.labelValues = observation.labelValues;
+ this.observing = observation.observing;
+ this.startTime = observation.startTime;
+ }
+
+ @Override
+ protected Observation newChild(List labels) {
+ Observation observation = new Observation(name).labelNames(labelNames.toArray(new String[0]));
+ observation.labelValues = labels;
+ return observation;
+ }
+
+ @Override
+ public void reset() {
+ values.clear();
+ children.clear();
+ }
+
+ public void startTimer() {
+ startTime = currentTimeMillis();
+ observing.set(true);
+ }
+
+ public void observeDuration() {
+ observing.set(false);
+ float duration = (currentTimeMillis() - startTime) / 1000f;
+ values.add(duration);
+ update();
+ }
+
+ public boolean isObserving() {
+ return observing.get();
+ }
+}
diff --git a/Sources/sdk/src/main/java/com/batch/android/module/ActionModule.java b/Sources/sdk/src/main/java/com/batch/android/module/ActionModule.java
index 1c4a366..6bb96ad 100644
--- a/Sources/sdk/src/main/java/com/batch/android/module/ActionModule.java
+++ b/Sources/sdk/src/main/java/com/batch/android/module/ActionModule.java
@@ -13,6 +13,7 @@
import com.batch.android.actions.DeeplinkActionRunnable;
import com.batch.android.actions.GroupActionRunnable;
import com.batch.android.actions.LocalCampaignsRefreshActionRunnable;
+import com.batch.android.actions.NotificationPermissionActionRunnable;
import com.batch.android.actions.RatingActionRunnable;
import com.batch.android.actions.UserDataBuiltinActionRunnable;
import com.batch.android.actions.UserEventBuiltinActionRunnable;
@@ -250,6 +251,11 @@ private void registerBuiltinActions() {
RatingActionRunnable.IDENTIFIER,
new UserAction(RatingActionRunnable.IDENTIFIER, new RatingActionRunnable())
);
+
+ registeredActions.put(
+ NotificationPermissionActionRunnable.IDENTIFIER,
+ new UserAction(NotificationPermissionActionRunnable.IDENTIFIER, new NotificationPermissionActionRunnable())
+ );
}
public int getDrawableIdForNameOrAlias(@NonNull Context context, @Nullable String drawableName) {
diff --git a/Sources/sdk/src/main/java/com/batch/android/module/BatchModule.java b/Sources/sdk/src/main/java/com/batch/android/module/BatchModule.java
index 1a0a2fd..f22100d 100644
--- a/Sources/sdk/src/main/java/com/batch/android/module/BatchModule.java
+++ b/Sources/sdk/src/main/java/com/batch/android/module/BatchModule.java
@@ -1,5 +1,7 @@
package com.batch.android.module;
+import android.content.Context;
+import androidx.annotation.NonNull;
import com.batch.android.runtime.State;
/**
@@ -24,6 +26,15 @@ public abstract class BatchModule {
// ----------------------------------->
+ /**
+ * Called by Batch as soon as a context is available in the runtimeManager
+ * For convenience, the application context is available as a parameter.
+ * LocalBroadcastManager is also up.
+ */
+ public void batchContextBecameAvailable(@NonNull Context applicationContext) {
+ // Override this method
+ }
+
/**
* Called by Batch before batch start
* NB : Context & activity are already available from the runtimeManager
diff --git a/Sources/sdk/src/main/java/com/batch/android/module/BatchModuleMaster.java b/Sources/sdk/src/main/java/com/batch/android/module/BatchModuleMaster.java
index 1fbadb5..bcd3464 100644
--- a/Sources/sdk/src/main/java/com/batch/android/module/BatchModuleMaster.java
+++ b/Sources/sdk/src/main/java/com/batch/android/module/BatchModuleMaster.java
@@ -1,5 +1,7 @@
package com.batch.android.module;
+import android.content.Context;
+import androidx.annotation.NonNull;
import com.batch.android.di.providers.ActionModuleProvider;
import com.batch.android.di.providers.DisplayReceiptModuleProvider;
import com.batch.android.di.providers.EventDispatcherModuleProvider;
@@ -58,6 +60,13 @@ public int getState() {
return 1;
}
+ @Override
+ public void batchContextBecameAvailable(@NonNull Context applicationContext) {
+ for (BatchModule module : modules) {
+ module.batchContextBecameAvailable(applicationContext);
+ }
+ }
+
@Override
public void batchWillStart() {
for (BatchModule module : modules) {
diff --git a/Sources/sdk/src/main/java/com/batch/android/module/DisplayReceiptModule.java b/Sources/sdk/src/main/java/com/batch/android/module/DisplayReceiptModule.java
index 7972cd7..996c14d 100644
--- a/Sources/sdk/src/main/java/com/batch/android/module/DisplayReceiptModule.java
+++ b/Sources/sdk/src/main/java/com/batch/android/module/DisplayReceiptModule.java
@@ -9,7 +9,6 @@
import com.batch.android.BatchDisplayReceiptJobService;
import com.batch.android.WebserviceLauncher;
import com.batch.android.core.InternalPushData;
-import com.batch.android.core.JobHelper;
import com.batch.android.core.Logger;
import com.batch.android.core.TaskRunnable;
import com.batch.android.core.Webservice;
@@ -119,9 +118,9 @@ public void scheduleDisplayReceipt(Context context, @NonNull InternalPushData pu
Logger.internal(TAG, "Could not get Job Scheduler system service");
return;
}
-
+ int jobId = (int) (Math.random() * Integer.MAX_VALUE);
JobInfo.Builder builder = new JobInfo.Builder(
- JobHelper.generateUniqueJobId(scheduler),
+ jobId,
new ComponentName(context, BatchDisplayReceiptJobService.class)
)
.setOverrideDeadline(dma * 1000L)
@@ -140,10 +139,8 @@ public void scheduleDisplayReceipt(Context context, @NonNull InternalPushData pu
} else {
Logger.internal(TAG, "Successfully scheduled the display receipt job");
}
- } catch (JobHelper.GenerationException e) {
- Logger.internal(TAG, "Could not find a suitable job ID", e);
} catch (Exception e1) {
- Logger.internal(TAG, "Could schedule Batch display receipt job", e1);
+ Logger.internal(TAG, "Could not schedule Batch display receipt job", e1);
}
} else {
sendReceipt(context, false);
diff --git a/Sources/sdk/src/main/java/com/batch/android/module/EventDispatcherModule.java b/Sources/sdk/src/main/java/com/batch/android/module/EventDispatcherModule.java
index 9d7b296..88e9b9e 100644
--- a/Sources/sdk/src/main/java/com/batch/android/module/EventDispatcherModule.java
+++ b/Sources/sdk/src/main/java/com/batch/android/module/EventDispatcherModule.java
@@ -2,6 +2,7 @@
import android.content.Context;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import com.batch.android.Batch;
import com.batch.android.BatchEventDispatcher;
import com.batch.android.core.DiscoveryServiceHelper;
@@ -9,6 +10,8 @@
import com.batch.android.di.providers.OptOutModuleProvider;
import com.batch.android.eventdispatcher.DispatcherDiscoveryService;
import com.batch.android.eventdispatcher.DispatcherRegistrar;
+import com.batch.android.eventdispatcher.DispatcherSerializer;
+import com.batch.android.json.JSONObject;
import com.batch.android.processor.Module;
import com.batch.android.processor.Provide;
import com.batch.android.processor.Singleton;
@@ -114,11 +117,34 @@ public void loadDispatcherFromContext(Context context) {
BatchEventDispatcher dispatcher = registrar.getDispatcher(context);
if (dispatcher != null) {
addEventDispatcher(dispatcher);
- printLoadedDispatcher(dispatcher.getClass().getName());
+ String dispatcherName = dispatcher.getClass().getName();
+ if (dispatcher.getName() != null) {
+ dispatcherName = dispatcher.getName();
+ } else {
+ Logger.warning(
+ TAG,
+ "The version of your event dispatcher: " +
+ dispatcherName +
+ " is outdated, please update it."
+ );
+ }
+ printLoadedDispatcher(dispatcherName);
}
} catch (Throwable e) {
Logger.error(String.format("Could not instantiate %s", name), e);
}
}
}
+
+ /**
+ * Get dispatchers as json object used for the analytics
+ * @return A JSONObject of dispatcher Name:Version
+ */
+ @Nullable
+ public JSONObject getDispatchersAnalyticRepresentation() {
+ synchronized (eventDispatchers) {
+ JSONObject json = DispatcherSerializer.serialize(eventDispatchers);
+ return json.length() > 0 ? json : null;
+ }
+ }
}
diff --git a/Sources/sdk/src/main/java/com/batch/android/module/LocalCampaignsModule.java b/Sources/sdk/src/main/java/com/batch/android/module/LocalCampaignsModule.java
index 59eb32a..9509596 100644
--- a/Sources/sdk/src/main/java/com/batch/android/module/LocalCampaignsModule.java
+++ b/Sources/sdk/src/main/java/com/batch/android/module/LocalCampaignsModule.java
@@ -1,5 +1,7 @@
package com.batch.android.module;
+import static com.batch.android.localcampaigns.model.LocalCampaign.SyncedJITResult;
+
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -9,16 +11,13 @@
import com.batch.android.compat.LocalBroadcastManager;
import com.batch.android.core.Logger;
import com.batch.android.core.NamedThreadFactory;
-import com.batch.android.core.ParameterKeys;
import com.batch.android.di.providers.CampaignManagerProvider;
import com.batch.android.di.providers.LocalBroadcastManagerProvider;
-import com.batch.android.di.providers.ParametersProvider;
import com.batch.android.di.providers.RuntimeManagerProvider;
import com.batch.android.di.providers.TaskExecutorProvider;
import com.batch.android.localcampaigns.CampaignManager;
import com.batch.android.localcampaigns.model.LocalCampaign;
import com.batch.android.localcampaigns.persistence.PersistenceException;
-import com.batch.android.localcampaigns.signal.CampaignsLoadedSignal;
import com.batch.android.localcampaigns.signal.EventTrackedSignal;
import com.batch.android.localcampaigns.signal.NewSessionSignal;
import com.batch.android.localcampaigns.signal.PublicEventTrackedSignal;
@@ -27,10 +26,11 @@
import com.batch.android.processor.Provide;
import com.batch.android.processor.Singleton;
import com.batch.android.runtime.SessionManager;
-import java.util.Timer;
-import java.util.TimerTask;
+import java.util.LinkedList;
+import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicBoolean;
/**
* Batch's Local Campaigns Messaging Module.
@@ -42,22 +42,45 @@ public class LocalCampaignsModule extends BatchModule {
public static final String TAG = "LocalCampaigns";
- private CampaignManager campaignManager;
+ /**
+ * Campaign manager instance
+ */
+ private final CampaignManager campaignManager;
+
+ /**
+ * Flag indicating whether we already tried to load the campaigns in cache
+ */
private boolean triedToReadSavedCampaign = false;
/**
- * Executor responsible for handling the campaign event based trigger
+ * Signals in queue
*/
- private ExecutorService triggerExecutor = Executors.newSingleThreadExecutor(new NamedThreadFactory());
+ private final LinkedList signalQueue = new LinkedList<>();
- private BroadcastReceiver localBroadcastReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- onLocalBroadcast(intent);
- }
- };
+ /**
+ * Flag indicating whether we are ready to process a signal.
+ *
+ * Meaning local campaigns have been synchronized from server, else signals are enqueue.
+ * This flag shouldn't be true when campaigns are loaded from cache, only after
+ * a synchronisation with the server.
+ */
+ private final AtomicBoolean isReady = new AtomicBoolean(false);
- private boolean broadcastReceiverRegistered = false;
+ /**
+ * Flag indicating whether we are waiting for the end of JIT synchronization.
+ * All signal processed during this time will be enqueue until the end.
+ */
+ private final AtomicBoolean isWaitingJITSync = new AtomicBoolean(false);
+
+ /**
+ * Executor responsible for handling the campaign event based trigger
+ */
+ private final ExecutorService triggerExecutor = Executors.newSingleThreadExecutor(new NamedThreadFactory());
+
+ /**
+ * Flag indicating whether the new session broadcast receiver is registered
+ */
+ private boolean isNewSessionBroadcastReceiverRegistered = false;
private LocalCampaignsModule(CampaignManager campaignManager) {
this.campaignManager = campaignManager;
@@ -82,7 +105,43 @@ public int getState() {
//endregion
+ /**
+ * Start sending a signal
+ * If another one is already processing, signal is added to queue.
+ * @param signal signal to send
+ */
public void sendSignal(@NonNull Signal signal) {
+ if (isReady.get()) {
+ processSignal(signal);
+ } else {
+ enqueueSignal(signal);
+ }
+ }
+
+ /**
+ * Add a signal to the queue
+ * @param signal signal to enqueue
+ */
+ private void enqueueSignal(@NonNull Signal signal) {
+ synchronized (signalQueue) {
+ // Ensure we are still processing the signal or synchronizing campaigns
+ if (isReady.get() && !isWaitingJITSync.get()) {
+ sendSignal(signal);
+ } else {
+ Logger.internal(
+ TAG,
+ "Local Campaign module isn't ready, enqueueing signal: " + signal.getClass().getSimpleName()
+ );
+ signalQueue.add(signal);
+ }
+ }
+ }
+
+ /**
+ * Process a signal
+ * @param signal signal to process
+ */
+ private void processSignal(@NonNull Signal signal) {
if (signal instanceof EventTrackedSignal) {
// Skip processing the signal if the event is not watched to avoid useless work
// Otherwise, transform the signal in a more specialized one for public events,
@@ -101,94 +160,216 @@ public void sendSignal(@NonNull Signal signal) {
}
}
- displayMessage(signal);
+ // Ensure we are not over global in-app cappings
+ if (campaignManager.isOverGlobalCappings()) {
+ return;
+ }
+
+ Signal finalSignal = signal;
+ triggerExecutor.submit(() -> {
+ if (isWaitingJITSync.get()) {
+ Logger.internal(TAG, "JIT sync in progress, enqueue signal.");
+ enqueueSignal(finalSignal);
+ } else {
+ electCampaignForSignal(finalSignal);
+ }
+ });
}
- public void wipeData(@NonNull Context context) {
- try {
- campaignManager.deleteAllCampaigns(context, true);
- } catch (PersistenceException e) {
- Logger.internal(TAG, "Could not delete persisted campaigns", e);
+ /**
+ * Elect the right campaign for a given signal and display it.
+ *
+ * Election process is the following :
+ * - Get all eligible campaigns sorted by priority for a signal:
+ * - If no eligible campaigns found:
+ * Do nothing
+ * - Else: Look if the first one is requiring a JIT sync :
+ * - Yes: Check if we need to make a new JIT sync (meaning last call for this campaign is older than {@link CampaignManager#JIT_CAMPAIGN_CACHE_PERIOD})
+ * - Yes: Check if JIT service is available :
+ * - Yes: Sync all campaigns requiring a JIT sync limited by {@link CampaignManager#MAX_CAMPAIGNS_JIT_THRESHOLD}) and stopping at the first campaign that not requiring JIT:
+ * - If server respond with no eligible campaigns :
+ * - Display the first campaign not requiring a JIT sync (if there's one else do noting)
+ * - else :
+ * - Display the first campaign verified by the server
+ * - No: Display the first campaign not requiring a JIT sync (if there's one else do noting)
+ * -No: Display it
+ * - No: Display it
+ */
+ private void electCampaignForSignal(final @NonNull Signal signal) {
+ // Get all eligible campaigns (sorted by priority) regardless of the JIT sync
+ List eligibleCampaigns = campaignManager.getEligibleCampaignsSortedByPriority(signal);
+
+ if (!eligibleCampaigns.isEmpty()) {
+ // Get the first elected campaign
+ LocalCampaign firstElectedCampaign = eligibleCampaigns.get(0);
+ if (firstElectedCampaign.requiresJustInTimeSync && signal instanceof EventTrackedSignal) {
+ SyncedJITResult.State syncedCampaignState = campaignManager.getSyncedJITCampaignState(
+ firstElectedCampaign
+ );
+ if (syncedCampaignState == SyncedJITResult.State.ELIGIBLE) {
+ // Last succeed JIT sync for this campaign is NOT older than 30 sec, considering eligibility up to date.
+ Logger.internal(TAG, "Skipping JIT sync since this campaign has been already synced recently.");
+ displayMessage(firstElectedCampaign);
+ } else if (
+ syncedCampaignState == SyncedJITResult.State.REQUIRES_SYNC &&
+ campaignManager.isJITServiceAvailable()
+ ) {
+ // JIT available, getting all campaigns to sync
+ List eligibleCampaignsRequiringSync = campaignManager.getFirstEligibleCampaignsRequiringSync(
+ eligibleCampaigns
+ );
+ LocalCampaign fallbackCampaign = campaignManager.getFirstCampaignNotRequiringJITSync(
+ eligibleCampaigns
+ );
+ isWaitingJITSync.set(true);
+ campaignManager.verifyCampaignsEligibilityFromServer(
+ eligibleCampaignsRequiringSync,
+ electedCampaign -> {
+ if (electedCampaign != null) {
+ Logger.internal(TAG, "Elected campaign has been synchronized with JIT.");
+ displayMessage(electedCampaign);
+ } else if (fallbackCampaign != null) {
+ Logger.internal(
+ TAG,
+ "JIT respond with no eligible campaigns or with error. Fallback on offline campaign."
+ );
+ displayMessage(fallbackCampaign);
+ } else {
+ Logger.info(TAG, "Ne eligible campaigns found after the JIT sync.");
+ }
+ isWaitingJITSync.set(false);
+ dequeueSignals();
+ }
+ );
+ } else {
+ // JIT not available or campaign is cached and not eligible, fallback on the first eligible campaign not requiring a JIT sync
+ LocalCampaign firstEligibleCampaignNotRequiringJITSync = campaignManager.getFirstCampaignNotRequiringJITSync(
+ eligibleCampaigns
+ );
+ if (firstEligibleCampaignNotRequiringJITSync != null) {
+ Logger.internal(
+ TAG,
+ "JIT not available or campaign is cached and not eligible, fallback on offline campaign."
+ );
+ displayMessage(firstEligibleCampaignNotRequiringJITSync);
+ }
+ }
+ } else {
+ // First elected campaign is not requiring a JIT sync, display it !
+ Logger.internal(TAG, "Elected campaign not requiring a sync, display it.");
+ displayMessage(firstElectedCampaign);
+ }
+ } else {
+ Logger.internal(TAG, "No eligible campaigns found.");
}
}
/**
- * Displays the campaign for the specified signal and track view using the ViewTracker
+ * Display the local campaign message
+ * @param campaign to display
*/
- private void displayMessage(final @NonNull Signal signal) {
- triggerExecutor.submit(() -> {
- LocalCampaign campaign = campaignManager.getCampaignToDisplay(signal);
- if (campaign != null) {
- campaign.generateOccurrenceID();
- campaign.displayMessage();
- }
- });
+ private void displayMessage(@NonNull LocalCampaign campaign) {
+ campaign.generateOccurrenceID();
+ campaign.displayMessage();
}
- private void onLocalBroadcast(Intent intent) {
- if (SessionManager.INTENT_NEW_SESSION.equals(intent.getAction())) {
- sendSignal(new NewSessionSignal());
+ /**
+ * Make this module ready to process signals enqueued.
+ */
+ private void makeReady() {
+ isReady.set(true);
+ dequeueSignals();
+ }
- WebserviceLauncher.launchLocalCampaignsWebservice(RuntimeManagerProvider.get());
+ /**
+ * Dequeue all signals
+ */
+ private void dequeueSignals() {
+ synchronized (signalQueue) {
+ LinkedList enqueuedSignals = new LinkedList<>(signalQueue);
+ signalQueue.clear();
+
+ if (!enqueuedSignals.isEmpty()) {
+ Logger.info(TAG, "Replaying " + enqueuedSignals.size() + " local campaign signals");
+ }
+
+ for (Signal signal : enqueuedSignals) {
+ processSignal(signal);
+ }
}
}
- @Override
- public void batchDidStart() {
- campaignManager.openViewTracker();
+ /**
+ * Release the signal queue when the local campaigns webservice is finished
+ */
+ public void onLocalCampaignsWebserviceFinished() {
+ makeReady();
+ }
+
+ /**
+ * Delete all campaigns from the manager
+ * @param context context
+ */
+ public void wipeData(@NonNull Context context) {
+ try {
+ campaignManager.deleteAllCampaigns(context, true);
+ } catch (PersistenceException e) {
+ Logger.internal(TAG, "Could not delete persisted campaigns", e);
+ }
+ }
- if (!broadcastReceiverRegistered) {
- LocalBroadcastManager lbm = LocalBroadcastManagerProvider.getSingleton();
- if (lbm != null) {
- broadcastReceiverRegistered = true;
- IntentFilter filter = new IntentFilter();
- filter.addAction(SessionManager.INTENT_NEW_SESSION);
- lbm.registerReceiver(localBroadcastReceiver, filter);
+ /**
+ * Broadcast receiver listening on new_session intent.
+ */
+ private final BroadcastReceiver newSessionBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (SessionManager.INTENT_NEW_SESSION.equals(intent.getAction())) {
+ isReady.set(false);
+ sendSignal(new NewSessionSignal());
+ WebserviceLauncher.launchLocalCampaignsWebservice(RuntimeManagerProvider.get());
}
}
+ };
- // Load saved campaigns and call webservice
- final Context context = RuntimeManagerProvider.get().getContext();
- SessionManager sessionManager = RuntimeManagerProvider.get().getSessionManager();
+ /**
+ * Register the broadcast receiver for "new_session" intent if needed.
+ * @param context used to instantiate the LocalBroadcastManager singleton if its not.
+ */
+ public void registerBroadcastReceiverIfNeeded(@NonNull Context context) {
+ if (!isNewSessionBroadcastReceiverRegistered) {
+ LocalBroadcastManager lbm = LocalBroadcastManagerProvider.get(context);
+ isNewSessionBroadcastReceiverRegistered = true;
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(SessionManager.INTENT_NEW_SESSION);
+ lbm.registerReceiver(newSessionBroadcastReceiver, filter);
+ }
+ }
- if (
- context != null && sessionManager != null && sessionManager.isSessionActive() && !triedToReadSavedCampaign
- ) {
+ /**
+ * Load the saved campaigns from cache
+ * @param context used to instantiate the TaskExecutor singleton if its not.
+ */
+ private void loadSavedCampaigns(@NonNull Context context) {
+ campaignManager.openViewTracker();
+ if (!triedToReadSavedCampaign) {
TaskExecutorProvider
.get(context)
.submit(() -> {
- // Try loading via local file
if (campaignManager.hasSavedCampaigns(context)) {
- if (campaignManager.loadSavedCampaignResponse(context)) {
- sendSignal(new CampaignsLoadedSignal());
- }
- triedToReadSavedCampaign = true;
+ campaignManager.loadSavedCampaignResponse(context);
}
-
- int delay = 0;
-
- try {
- delay =
- Integer.valueOf(
- ParametersProvider.get(context).get(ParameterKeys.LOCAL_CAMPAIGNS_WS_INITIAL_DELAY)
- );
- } catch (NumberFormatException ignored) {}
-
- // Try loading via webservice
- new Timer()
- .schedule(
- new TimerTask() {
- @Override
- public void run() {
- WebserviceLauncher.launchLocalCampaignsWebservice(RuntimeManagerProvider.get());
- }
- },
- delay * 1000
- );
});
+ triedToReadSavedCampaign = true;
}
}
+ @Override
+ public void batchContextBecameAvailable(@NonNull Context applicationContext) {
+ registerBroadcastReceiverIfNeeded(applicationContext);
+ loadSavedCampaigns(applicationContext);
+ }
+
@Override
public void batchDidStop() {
campaignManager.closeViewTracker();
diff --git a/Sources/sdk/src/main/java/com/batch/android/module/PushModule.java b/Sources/sdk/src/main/java/com/batch/android/module/PushModule.java
index 933c6d0..acdee20 100644
--- a/Sources/sdk/src/main/java/com/batch/android/module/PushModule.java
+++ b/Sources/sdk/src/main/java/com/batch/android/module/PushModule.java
@@ -705,7 +705,6 @@ public void onNotificationDisplayed(Context context, Intent intent) {
try {
if (shouldDisplayPush(context, intent)) {
InternalPushData pushData = InternalPushData.getPushDataForReceiverIntent(intent);
- BatchPushHelper.markPushAsShown(context, pushData.getPushId());
if (pushData.getReceiptMode() == InternalPushData.ReceiptMode.DISPLAY) {
displayReceiptModule.scheduleDisplayReceipt(context, pushData);
@@ -723,7 +722,6 @@ public void onNotificationDisplayed(Context context, RemoteMessage message) {
try {
if (shouldDisplayPush(context, message)) {
InternalPushData pushData = InternalPushData.getPushDataForFirebaseMessage(message);
- BatchPushHelper.markPushAsShown(context, pushData.getPushId());
if (pushData.getReceiptMode() == InternalPushData.ReceiptMode.DISPLAY) {
displayReceiptModule.scheduleDisplayReceipt(context, pushData);
diff --git a/Sources/sdk/src/main/java/com/batch/android/msgpack/MessagePackHelper.java b/Sources/sdk/src/main/java/com/batch/android/msgpack/MessagePackHelper.java
index 34b8c19..bb33918 100644
--- a/Sources/sdk/src/main/java/com/batch/android/msgpack/MessagePackHelper.java
+++ b/Sources/sdk/src/main/java/com/batch/android/msgpack/MessagePackHelper.java
@@ -3,6 +3,7 @@
import com.batch.android.msgpack.core.MessageBufferPacker;
import com.batch.android.msgpack.value.Value;
import java.math.BigInteger;
+import java.net.URI;
import java.util.List;
import java.util.Map;
@@ -30,6 +31,8 @@ public static void packObject(MessageBufferPacker packer, Object object) throws
packer.packBoolean((Boolean) object);
} else if (object instanceof String) {
packer.packString((String) object);
+ } else if (object instanceof URI) {
+ packer.packString(object.toString());
} else if (object instanceof Value) {
((Value) object).writeTo(packer);
} else if (object instanceof Map) {
diff --git a/Sources/sdk/src/main/java/com/batch/android/post/DisplayReceiptPostDataProvider.java b/Sources/sdk/src/main/java/com/batch/android/post/DisplayReceiptPostDataProvider.java
index bf95570..e1b4fae 100644
--- a/Sources/sdk/src/main/java/com/batch/android/post/DisplayReceiptPostDataProvider.java
+++ b/Sources/sdk/src/main/java/com/batch/android/post/DisplayReceiptPostDataProvider.java
@@ -1,15 +1,15 @@
package com.batch.android.post;
-import com.batch.android.core.Logger;
import com.batch.android.displayreceipt.DisplayReceipt;
import com.batch.android.msgpack.core.MessageBufferPacker;
import com.batch.android.msgpack.core.MessagePack;
import java.util.Collection;
-public class DisplayReceiptPostDataProvider implements PostDataProvider> {
+public class DisplayReceiptPostDataProvider extends MessagePackPostDataProvider> {
private static final String TAG = "DisplayReceiptPostDataProvider";
- private Collection receipts;
+
+ private final Collection receipts;
public DisplayReceiptPostDataProvider(Collection receipts) {
this.receipts = receipts;
@@ -20,7 +20,8 @@ public Collection getRawData() {
return receipts;
}
- private byte[] pack() throws Exception {
+ @Override
+ byte[] pack() throws Exception {
MessageBufferPacker packer = MessagePack.newDefaultBufferPacker();
packer.packArrayHeader(receipts.size());
@@ -32,25 +33,7 @@ private byte[] pack() throws Exception {
}
@Override
- public byte[] getData() {
- if (receipts == null || receipts.size() == 0) {
- return new byte[0];
- }
-
- try {
- return pack();
- } catch (Exception e) {
- Logger.internal(TAG, "Could not pack receipt list", e);
- return new byte[0];
- }
- }
-
public boolean isEmpty() {
return this.receipts.isEmpty();
}
-
- @Override
- public String getContentType() {
- return "application/msgpack";
- }
}
diff --git a/Sources/sdk/src/main/java/com/batch/android/post/JSONPostDataProvider.java b/Sources/sdk/src/main/java/com/batch/android/post/JSONPostDataProvider.java
index 70d4719..fdab6c7 100644
--- a/Sources/sdk/src/main/java/com/batch/android/post/JSONPostDataProvider.java
+++ b/Sources/sdk/src/main/java/com/batch/android/post/JSONPostDataProvider.java
@@ -48,6 +48,11 @@ public String getContentType() {
return "application/json";
}
+ @Override
+ public boolean isEmpty() {
+ return data == null || data.length() == 0;
+ }
+
@Override
public JSONObject getRawData() {
return data;
diff --git a/Sources/sdk/src/main/java/com/batch/android/post/LocalCampaignsJITPostDataProvider.java b/Sources/sdk/src/main/java/com/batch/android/post/LocalCampaignsJITPostDataProvider.java
new file mode 100644
index 0000000..72ec78b
--- /dev/null
+++ b/Sources/sdk/src/main/java/com/batch/android/post/LocalCampaignsJITPostDataProvider.java
@@ -0,0 +1,119 @@
+package com.batch.android.post;
+
+import android.content.Context;
+import androidx.annotation.NonNull;
+import com.batch.android.WebserviceParameterUtils;
+import com.batch.android.core.Logger;
+import com.batch.android.di.providers.CampaignManagerProvider;
+import com.batch.android.di.providers.RuntimeManagerProvider;
+import com.batch.android.di.providers.SQLUserDatasourceProvider;
+import com.batch.android.localcampaigns.ViewTracker;
+import com.batch.android.localcampaigns.model.LocalCampaign;
+import com.batch.android.msgpack.MessagePackHelper;
+import com.batch.android.msgpack.core.MessageBufferPacker;
+import com.batch.android.msgpack.core.MessagePack;
+import com.batch.android.msgpack.core.MessageUnpacker;
+import com.batch.android.user.SQLUserDatasource;
+import com.batch.android.user.UserAttribute;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class LocalCampaignsJITPostDataProvider extends MessagePackPostDataProvider> {
+
+ private static final String TAG = "LocalCampaignsJITPostDataProvider";
+
+ private static final String IDS_KEY = "ids";
+ private static final String CAMPAIGNS_KEY = "campaigns";
+ private static final String ATTRIBUTES_KEY = "attributes";
+ private static final String VIEWS_KEY = "views";
+ private static final String COUNT_KEY = "count";
+ private static final String ELIGIBLE_CAMPAIGNS_KEY = "eligibleCampaigns";
+
+ private final Collection campaigns;
+
+ public LocalCampaignsJITPostDataProvider(Collection campaigns) {
+ this.campaigns = campaigns;
+ }
+
+ @Override
+ public Collection getRawData() {
+ return campaigns;
+ }
+
+ @Override
+ byte[] pack() throws Exception {
+ ViewTracker viewTracker = CampaignManagerProvider.get().getViewTracker();
+ MessageBufferPacker packer = MessagePack.newDefaultBufferPacker();
+
+ Map postData = new HashMap<>();
+
+ // Adding system ids
+ Map ids = new HashMap<>();
+ Context context = RuntimeManagerProvider.get().getContext();
+ if (context != null) {
+ ids = WebserviceParameterUtils.getWebserviceIdsAsMap(context);
+ }
+
+ // Adding campaigns ids to check
+ List campaignIds = new ArrayList<>();
+ for (LocalCampaign campaign : campaigns) {
+ campaignIds.add(campaign.id);
+ }
+
+ // Adding views count for each campaign
+ Map counts = viewTracker.getViewCounts(campaignIds);
+ Map views = new HashMap<>();
+ for (Map.Entry entry : counts.entrySet()) {
+ Map countMap = new HashMap<>();
+ countMap.put(COUNT_KEY, entry.getValue());
+ views.put(entry.getKey(), countMap);
+ }
+
+ // Adding attributes
+ final SQLUserDatasource datasource = SQLUserDatasourceProvider.get(context);
+ Map attributes = UserAttribute.getServerMapRepresentation(datasource.getAttributes());
+
+ postData.put(IDS_KEY, ids);
+ postData.put(CAMPAIGNS_KEY, campaignIds);
+ postData.put(ATTRIBUTES_KEY, attributes);
+ postData.put(VIEWS_KEY, views);
+
+ MessagePackHelper.packObject(packer, postData);
+ packer.close();
+ return packer.toByteArray();
+ }
+
+ @NonNull
+ public List unpack(byte[] data) {
+ List eligibleCampaigns = new ArrayList<>();
+ MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(data);
+ try {
+ // Unpack root map header
+ unpacker.unpackMapHeader();
+
+ // Unpack "eligibleCampaigns" key
+ String key = unpacker.unpackString();
+
+ if (ELIGIBLE_CAMPAIGNS_KEY.equals(key)) {
+ // Unpack list of campaign id
+ int eligibleCampaignsSize = unpacker.unpackArrayHeader();
+ for (int i = 0; i < eligibleCampaignsSize; i++) {
+ String campaignId = unpacker.unpackString();
+ eligibleCampaigns.add(campaignId);
+ }
+ }
+ } catch (IOException e) {
+ Logger.internal(TAG, "Could not unpack campaign jit response.");
+ }
+ return eligibleCampaigns;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return campaigns.isEmpty();
+ }
+}
diff --git a/Sources/sdk/src/main/java/com/batch/android/post/MessagePackPostDataProvider.java b/Sources/sdk/src/main/java/com/batch/android/post/MessagePackPostDataProvider.java
new file mode 100644
index 0000000..b947d02
--- /dev/null
+++ b/Sources/sdk/src/main/java/com/batch/android/post/MessagePackPostDataProvider.java
@@ -0,0 +1,25 @@
+package com.batch.android.post;
+
+import com.batch.android.core.Logger;
+
+public abstract class MessagePackPostDataProvider implements PostDataProvider {
+
+ private static final String TAG = "MessagePackPostDataProvider";
+
+ abstract byte[] pack() throws Exception;
+
+ @Override
+ public byte[] getData() {
+ try {
+ return pack();
+ } catch (Exception e) {
+ Logger.internal(TAG, "Could not pack data", e);
+ return new byte[0];
+ }
+ }
+
+ @Override
+ public String getContentType() {
+ return "application/msgpack";
+ }
+}
diff --git a/Sources/sdk/src/main/java/com/batch/android/post/MetricPostDataProvider.java b/Sources/sdk/src/main/java/com/batch/android/post/MetricPostDataProvider.java
new file mode 100644
index 0000000..b697ce7
--- /dev/null
+++ b/Sources/sdk/src/main/java/com/batch/android/post/MetricPostDataProvider.java
@@ -0,0 +1,38 @@
+package com.batch.android.post;
+
+import com.batch.android.metrics.model.Metric;
+import com.batch.android.msgpack.core.MessageBufferPacker;
+import com.batch.android.msgpack.core.MessagePack;
+import java.util.Collection;
+
+public class MetricPostDataProvider extends MessagePackPostDataProvider>> {
+
+ private static final String TAG = "DisplayReceiptPostDataProvider";
+
+ private final Collection> metrics;
+
+ public MetricPostDataProvider(Collection> metrics) {
+ this.metrics = metrics;
+ }
+
+ @Override
+ public Collection> getRawData() {
+ return metrics;
+ }
+
+ @Override
+ byte[] pack() throws Exception {
+ MessageBufferPacker packer = MessagePack.newDefaultBufferPacker();
+ packer.packArrayHeader(metrics.size());
+ for (Metric> data : metrics) {
+ data.pack(packer);
+ }
+ packer.close();
+ return packer.toByteArray();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return this.metrics.isEmpty();
+ }
+}
diff --git a/Sources/sdk/src/main/java/com/batch/android/post/ParametersPostDataProvider.java b/Sources/sdk/src/main/java/com/batch/android/post/ParametersPostDataProvider.java
index 655cf82..0225f06 100644
--- a/Sources/sdk/src/main/java/com/batch/android/post/ParametersPostDataProvider.java
+++ b/Sources/sdk/src/main/java/com/batch/android/post/ParametersPostDataProvider.java
@@ -75,4 +75,9 @@ public byte[] getData() {
public String getContentType() {
return "application/x-www-form-urlencoded";
}
+
+ @Override
+ public boolean isEmpty() {
+ return params.isEmpty();
+ }
}
diff --git a/Sources/sdk/src/main/java/com/batch/android/post/PostDataProvider.java b/Sources/sdk/src/main/java/com/batch/android/post/PostDataProvider.java
index 3b89812..5d4f625 100644
--- a/Sources/sdk/src/main/java/com/batch/android/post/PostDataProvider.java
+++ b/Sources/sdk/src/main/java/com/batch/android/post/PostDataProvider.java
@@ -27,4 +27,10 @@ public interface PostDataProvider {
* @return
*/
String getContentType();
+
+ /**
+ * Checks whether this provider is empty or not.
+ * @return true if empty
+ */
+ boolean isEmpty();
}
diff --git a/Sources/sdk/src/main/java/com/batch/android/push/GCMAbstractRegistrationProvider.java b/Sources/sdk/src/main/java/com/batch/android/push/GCMAbstractRegistrationProvider.java
index b9e08f1..868986c 100644
--- a/Sources/sdk/src/main/java/com/batch/android/push/GCMAbstractRegistrationProvider.java
+++ b/Sources/sdk/src/main/java/com/batch/android/push/GCMAbstractRegistrationProvider.java
@@ -71,7 +71,7 @@ public void checkLibraryAvailability() throws PushRegistrationProviderAvailabili
throw new PushRegistrationProviderAvailabilityException("Batch.Push : Permission C2D_MESSAGE is missing.");
}
- if (!isWakeLockPermissionAvailable()) {
+ if (!GenericHelper.isWakeLockPermissionAvailable(context)) {
throw new PushRegistrationProviderAvailabilityException("Batch.Push : Permission WAKE_LOCK is missing.");
}
}
@@ -100,13 +100,4 @@ private boolean isC2DMessagePermissionAvailable() {
return false;
}
}
-
- private boolean isWakeLockPermissionAvailable() {
- try {
- return GenericHelper.checkPermission("android.permission.WAKE_LOCK", context);
- } catch (Exception e) {
- Logger.error(TAG, "Error while checking android.permission.WAKE_LOCK permission", e);
- return false;
- }
- }
}
diff --git a/Sources/sdk/src/main/java/com/batch/android/query/response/LocalCampaignsResponse.java b/Sources/sdk/src/main/java/com/batch/android/query/response/LocalCampaignsResponse.java
index d5937e6..68cd550 100644
--- a/Sources/sdk/src/main/java/com/batch/android/query/response/LocalCampaignsResponse.java
+++ b/Sources/sdk/src/main/java/com/batch/android/query/response/LocalCampaignsResponse.java
@@ -32,6 +32,11 @@ public class LocalCampaignsResponse extends Response {
*/
private Long minDisplayInterval;
+ /**
+ * Global in-app cappings
+ */
+ private GlobalCappings cappings;
+
public LocalCampaignsResponse(String queryID) {
super(QueryType.LOCAL_CAMPAIGNS, queryID);
}
@@ -84,6 +89,82 @@ public boolean hasError() {
return error != null;
}
+ @Nullable
+ public GlobalCappings getCappings() {
+ return cappings;
+ }
+
+ public void setCappings(GlobalCappings cappings) {
+ this.cappings = cappings;
+ }
+
+ public boolean hasCappings() {
+ return cappings != null;
+ }
+
+ /**
+ * Global In-App Cappings
+ */
+ public static class GlobalCappings {
+
+ /**
+ * Time-Based Capping
+ * Eg: Display no more than 3 in-apps every 1 hours
+ */
+ public static class TimeBasedCapping {
+
+ /**
+ * Number of views allowed
+ */
+ private final Integer views;
+
+ /**
+ * Capping duration (in seconds)
+ */
+ private final Integer duration;
+
+ public TimeBasedCapping(Integer views, Integer duration) {
+ this.views = views;
+ this.duration = duration;
+ }
+
+ @Nullable
+ public Integer getViews() {
+ return views;
+ }
+
+ @Nullable
+ public Integer getDuration() {
+ return duration;
+ }
+ }
+
+ /**
+ * Number of in-apps displayable during a user session
+ */
+ private final Integer session;
+
+ /**
+ * List of time-based cappings
+ */
+ private final List timeBasedCappings;
+
+ public GlobalCappings(Integer session, List timeBasedCappings) {
+ this.session = session;
+ this.timeBasedCappings = timeBasedCappings;
+ }
+
+ @Nullable
+ public Integer getSession() {
+ return session;
+ }
+
+ @Nullable
+ public List getTimeBasedCappings() {
+ return timeBasedCappings;
+ }
+ }
+
public static class Error {
/**
diff --git a/Sources/sdk/src/main/java/com/batch/android/query/serialization/deserializers/LocalCampaignsResponseDeserializer.java b/Sources/sdk/src/main/java/com/batch/android/query/serialization/deserializers/LocalCampaignsResponseDeserializer.java
index 7e0e7a0..9f7c988 100644
--- a/Sources/sdk/src/main/java/com/batch/android/query/serialization/deserializers/LocalCampaignsResponseDeserializer.java
+++ b/Sources/sdk/src/main/java/com/batch/android/query/serialization/deserializers/LocalCampaignsResponseDeserializer.java
@@ -1,5 +1,7 @@
package com.batch.android.query.serialization.deserializers;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import com.batch.android.core.Logger;
import com.batch.android.json.JSONArray;
import com.batch.android.json.JSONException;
@@ -53,16 +55,84 @@ public LocalCampaignsResponse deserialize() throws JSONException {
JSONArray jsonCampaigns = json.optJSONArray("campaigns");
List campaigns = localCampaignDeserializer.deserializeList(jsonCampaigns);
response.setCampaigns(campaigns);
+
response.setMinDisplayInterval(minDisplayInterval);
+
+ LocalCampaignsResponse.GlobalCappings cappings = deserializeCappings();
+ response.setCappings(cappings);
+
return response;
}
+ /**
+ * Only deserialize the local campaigns from the json response
+ * @return A list of LocalCampaign
+ */
+ @NonNull
+ public List deserializeCampaigns() {
+ JSONArray jsonCampaigns = json.optJSONArray("campaigns");
+ return localCampaignDeserializer.deserializeList(jsonCampaigns);
+ }
+
+ /**
+ * Only deserialize the global in-app cappings from the json response
+ *
+ * @return the LocalCampaignsResponse.GlobalCappings
+ * @throws JSONException parsing exception
+ */
+ @Nullable
+ public LocalCampaignsResponse.GlobalCappings deserializeCappings() throws JSONException {
+ LocalCampaignsResponse.GlobalCappings cappings = null;
+ if (json != null && json.hasNonNull("cappings")) {
+ JSONObject jsonCappings = json.getJSONObject("cappings");
+
+ Integer session = jsonCappings.reallyOptInteger("session", null);
+
+ List timeBasedCappings = null;
+
+ if (jsonCappings.hasNonNull("time")) {
+ timeBasedCappings = parseTimeBasedCappings(jsonCappings.getJSONArray("time"));
+ }
+ cappings = new LocalCampaignsResponse.GlobalCappings(session, timeBasedCappings);
+ }
+ return cappings;
+ }
+
+ /**
+ * Parse a json array into a list of Time-Based Cappings
+ *
+ * @param json time based capping array
+ * @return the LocalCampaignsResponse.GlobalCappings.TimeBasedCapping list
+ */
+ @Nullable
+ private List parseTimeBasedCappings(JSONArray json) {
+ List timeBasedCappings = new ArrayList<>();
+ for (int i = 0; i < json.length(); i++) {
+ try {
+ JSONObject jsonTimeBasedCapping = json.getJSONObject(i);
+ Integer views = jsonTimeBasedCapping.reallyOptInteger("views", null);
+ Integer duration = jsonTimeBasedCapping.reallyOptInteger("duration", null);
+ if (views != null && views != 0 && duration != null && duration != 0) {
+ LocalCampaignsResponse.GlobalCappings.TimeBasedCapping timeBasedCapping = new LocalCampaignsResponse.GlobalCappings.TimeBasedCapping(
+ views,
+ duration
+ );
+ timeBasedCappings.add(timeBasedCapping);
+ }
+ } catch (Exception e) {
+ Logger.internal(TAG, "An error occurred while parsing an In-App TimeBasedCapping. Skipping.", e);
+ }
+ }
+ return timeBasedCappings.isEmpty() ? null : timeBasedCappings;
+ }
+
/**
* Parse error response if there's one
*
* @return LocalCampaignsResponse.Error || null
* @throws JSONException parsing exception
*/
+ @Nullable
private LocalCampaignsResponse.Error parseError() throws JSONException {
LocalCampaignsResponse.Error error = null;
if (json != null && json.hasNonNull("error")) {
diff --git a/Sources/sdk/src/main/java/com/batch/android/query/serialization/serializers/LocalCampaignsResponseSerializer.java b/Sources/sdk/src/main/java/com/batch/android/query/serialization/serializers/LocalCampaignsResponseSerializer.java
index 2aa597a..ca0f393 100644
--- a/Sources/sdk/src/main/java/com/batch/android/query/serialization/serializers/LocalCampaignsResponseSerializer.java
+++ b/Sources/sdk/src/main/java/com/batch/android/query/serialization/serializers/LocalCampaignsResponseSerializer.java
@@ -1,42 +1,33 @@
package com.batch.android.query.serialization.serializers;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import com.batch.android.json.JSONArray;
import com.batch.android.json.JSONException;
import com.batch.android.json.JSONObject;
+import com.batch.android.localcampaigns.model.LocalCampaign;
import com.batch.android.localcampaigns.serialization.LocalCampaignSerializer;
import com.batch.android.query.response.LocalCampaignsResponse;
+import java.util.List;
/**
* Serializer class for {@link LocalCampaignsResponse}
*/
public class LocalCampaignsResponseSerializer {
- /**
- * Initial local campaign response object
- */
- private final LocalCampaignsResponse response;
-
/**
* Local campaign serializer
*/
private final LocalCampaignSerializer localCampaignSerializer = new LocalCampaignSerializer();
/**
- * Constructor
- *
- * @param response initial response object
- */
- public LocalCampaignsResponseSerializer(LocalCampaignsResponse response) {
- this.response = response;
- }
-
- /**
+ * (Method not used)
* Serialize a LocalCampaignsResponse to a JSONObject
*
* @return local campaigns response serialized
* @throws JSONException parsing exception
*/
- public JSONObject serialize() throws JSONException {
+ public JSONObject serialize(LocalCampaignsResponse response) throws JSONException {
if (response == null) {
throw new JSONException("Cannot serialize a null response");
}
@@ -48,6 +39,46 @@ public JSONObject serialize() throws JSONException {
}
JSONArray jsonCampaigns = localCampaignSerializer.serializeList(response.getCampaigns());
jsonLocalCampaignResponse.put("campaigns", jsonCampaigns);
+
+ JSONObject jsonCappings = serializeCappings(response.getCappings());
+ jsonLocalCampaignResponse.putOpt("cappings", jsonCappings);
return jsonLocalCampaignResponse;
}
+
+ /**
+ * Serialize a list of campaigns into a json array
+ * @param campaigns to serialize
+ * @return serialized campaigns
+ * @throws JSONException parsing exception
+ */
+ @NonNull
+ public JSONArray serializeCampaigns(List campaigns) throws JSONException {
+ return localCampaignSerializer.serializeList(campaigns);
+ }
+
+ /**
+ * Serialize global in-app cappings into a json object
+ * @param cappings to serialize
+ * @return serialized cappings
+ * @throws JSONException parsing exception
+ */
+ @Nullable
+ public JSONObject serializeCappings(LocalCampaignsResponse.GlobalCappings cappings) throws JSONException {
+ if (cappings != null) {
+ JSONObject jsonGlobalCapping = new JSONObject();
+ jsonGlobalCapping.putOpt("session", cappings.getSession());
+ if (cappings.getTimeBasedCappings() != null) {
+ JSONArray jsonTimeBasedCappings = new JSONArray();
+ for (LocalCampaignsResponse.GlobalCappings.TimeBasedCapping timeBasedCapping : cappings.getTimeBasedCappings()) {
+ JSONObject jsonTimeBasedCapping = new JSONObject();
+ jsonTimeBasedCapping.put("views", timeBasedCapping.getViews());
+ jsonTimeBasedCapping.put("duration", timeBasedCapping.getDuration());
+ jsonTimeBasedCappings.put(jsonTimeBasedCapping);
+ }
+ jsonGlobalCapping.put("time", jsonTimeBasedCappings);
+ }
+ return jsonGlobalCapping;
+ }
+ return null;
+ }
}
diff --git a/Sources/sdk/src/main/java/com/batch/android/runtime/RuntimeManager.java b/Sources/sdk/src/main/java/com/batch/android/runtime/RuntimeManager.java
index 7be5c42..c8b2ce1 100644
--- a/Sources/sdk/src/main/java/com/batch/android/runtime/RuntimeManager.java
+++ b/Sources/sdk/src/main/java/com/batch/android/runtime/RuntimeManager.java
@@ -6,6 +6,7 @@
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.batch.android.core.Logger;
import com.batch.android.debug.FindMyInstallationHelper;
@@ -365,6 +366,7 @@ public void setContext(Context context) {
*
* @return context or null
*/
+ @Nullable
public Context getContext() {
return context;
}
diff --git a/Sources/sdk/src/main/java/com/batch/android/runtime/SessionManager.java b/Sources/sdk/src/main/java/com/batch/android/runtime/SessionManager.java
index 49a8e11..03a7fcc 100644
--- a/Sources/sdk/src/main/java/com/batch/android/runtime/SessionManager.java
+++ b/Sources/sdk/src/main/java/com/batch/android/runtime/SessionManager.java
@@ -8,11 +8,15 @@
import android.content.res.Configuration;
import android.os.Bundle;
import android.os.SystemClock;
+import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.batch.android.core.ExcludedActivityHelper;
import com.batch.android.core.Logger;
import com.batch.android.core.Parameters;
+import com.batch.android.di.providers.CampaignManagerProvider;
import com.batch.android.di.providers.LocalBroadcastManagerProvider;
+import com.batch.android.di.providers.LocalCampaignsModuleProvider;
+import com.batch.android.localcampaigns.LocalCampaignsTracker;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
@@ -57,11 +61,6 @@ public String getSessionIdentifier() {
return sessionIdentifier;
}
- public synchronized boolean isSessionActive() {
- invalidateSessionIfNeeded();
- return sessionActive;
- }
-
private synchronized void invalidateSessionIfNeeded() {
if (
sessionActive &&
@@ -73,11 +72,17 @@ private synchronized void invalidateSessionIfNeeded() {
}
}
- synchronized void startNewSessionIfNeeded(Context c) {
+ synchronized void startNewSessionIfNeeded(@NonNull Context c) {
invalidateSessionIfNeeded();
if (!sessionActive) {
sessionActive = true;
sessionIdentifier = UUID.randomUUID().toString();
+
+ // Reset the view tracker session count
+ LocalCampaignsTracker tracker = (LocalCampaignsTracker) CampaignManagerProvider.get().getViewTracker();
+ tracker.resetSessionViewsCount();
+
+ // Broadcast the new session intent
Logger.internal(TAG, "Starting a new session, id: '" + sessionIdentifier + "'");
LocalBroadcastManagerProvider.get(c).sendBroadcast(new Intent(INTENT_NEW_SESSION));
}
diff --git a/Sources/sdk/src/main/java/com/batch/android/webservice/listener/DisplayReceiptWebserviceListener.java b/Sources/sdk/src/main/java/com/batch/android/webservice/listener/DisplayReceiptWebserviceListener.java
index 82a8828..e71df79 100644
--- a/Sources/sdk/src/main/java/com/batch/android/webservice/listener/DisplayReceiptWebserviceListener.java
+++ b/Sources/sdk/src/main/java/com/batch/android/webservice/listener/DisplayReceiptWebserviceListener.java
@@ -14,7 +14,7 @@ public interface DisplayReceiptWebserviceListener {
/**
* Called when a request fail
*
- * @param error
+ * @param error webservice request error
*/
void onFailure(Webservice.WebserviceError error);
}
diff --git a/Sources/sdk/src/main/java/com/batch/android/webservice/listener/LocalCampaignsJITWebserviceListener.java b/Sources/sdk/src/main/java/com/batch/android/webservice/listener/LocalCampaignsJITWebserviceListener.java
new file mode 100644
index 0000000..2af3dfb
--- /dev/null
+++ b/Sources/sdk/src/main/java/com/batch/android/webservice/listener/LocalCampaignsJITWebserviceListener.java
@@ -0,0 +1,22 @@
+package com.batch.android.webservice.listener;
+
+import com.batch.android.core.Webservice;
+import java.util.List;
+
+/**
+ * Listener for LocalCampaignsJITWebservice
+ */
+
+public interface LocalCampaignsJITWebserviceListener {
+ /**
+ * Called on success
+ */
+ void onSuccess(List eligibleCampaigns);
+
+ /**
+ * Called on error
+ *
+ * @param error webservice error
+ */
+ void onFailure(Webservice.WebserviceError error);
+}
diff --git a/Sources/sdk/src/main/java/com/batch/android/webservice/listener/MetricWebserviceListener.java b/Sources/sdk/src/main/java/com/batch/android/webservice/listener/MetricWebserviceListener.java
new file mode 100644
index 0000000..9c17594
--- /dev/null
+++ b/Sources/sdk/src/main/java/com/batch/android/webservice/listener/MetricWebserviceListener.java
@@ -0,0 +1,20 @@
+package com.batch.android.webservice.listener;
+
+import com.batch.android.core.Webservice;
+
+/**
+ * Listener for the metric webservice
+ */
+public interface MetricWebserviceListener {
+ /**
+ * Called when a request succeed
+ */
+ void onSuccess();
+
+ /**
+ * Called when a request fail
+ *
+ * @param error error
+ */
+ void onFailure(Webservice.WebserviceError error);
+}
diff --git a/Sources/sdk/src/main/java/com/batch/android/webservice/listener/impl/LocalCampaignsWebserviceListenerImpl.java b/Sources/sdk/src/main/java/com/batch/android/webservice/listener/impl/LocalCampaignsWebserviceListenerImpl.java
index d5a78c5..c591be1 100644
--- a/Sources/sdk/src/main/java/com/batch/android/webservice/listener/impl/LocalCampaignsWebserviceListenerImpl.java
+++ b/Sources/sdk/src/main/java/com/batch/android/webservice/listener/impl/LocalCampaignsWebserviceListenerImpl.java
@@ -5,7 +5,6 @@
import com.batch.android.di.providers.CampaignManagerProvider;
import com.batch.android.di.providers.LocalCampaignsModuleProvider;
import com.batch.android.localcampaigns.CampaignManager;
-import com.batch.android.localcampaigns.signal.CampaignsRefreshedSignal;
import com.batch.android.module.LocalCampaignsModule;
import com.batch.android.processor.Module;
import com.batch.android.processor.Provide;
@@ -20,6 +19,7 @@
public class LocalCampaignsWebserviceListenerImpl implements LocalCampaignsWebserviceListener {
private LocalCampaignsModule localCampaignsModule;
+
private CampaignManager campaignManager;
private LocalCampaignsWebserviceListenerImpl(
@@ -48,10 +48,12 @@ public void onSuccess(List responses) {
@Override
public void onError(FailReason reason) {
Logger.internal(LocalCampaignsModule.TAG, "Error while refreshing local campaigns: " + reason.toString());
+ localCampaignsModule.onLocalCampaignsWebserviceFinished();
}
private void handleInAppResponse(LocalCampaignsResponse response) {
+ campaignManager.setCappings(response.getCappings());
campaignManager.updateCampaignList(response.getCampaigns());
- localCampaignsModule.sendSignal(new CampaignsRefreshedSignal());
+ localCampaignsModule.onLocalCampaignsWebserviceFinished();
}
}
diff --git a/Sources/sdk/src/test/java/com/batch/android/BatchPushMessageReceiverTest.kt b/Sources/sdk/src/test/java/com/batch/android/BatchPushMessageReceiverTest.kt
new file mode 100644
index 0000000..a467f32
--- /dev/null
+++ b/Sources/sdk/src/test/java/com/batch/android/BatchPushMessageReceiverTest.kt
@@ -0,0 +1,98 @@
+package com.batch.android
+
+import android.content.Context
+import android.content.Intent
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import org.junit.After
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.*
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class BatchPushMessageReceiverTest {
+
+ companion object {
+ const val EXTRA_MSG_TYPE = "message_type"
+ }
+
+ val context: Context = ApplicationProvider.getApplicationContext()
+
+ @After
+ fun tearDown() {
+ BatchPushMessageReceiver.resetHandledMessageIDs()
+ }
+
+ @Test
+ fun testMessageDeduplication() {
+ val receiver = ObservablePushMessageReceiver()
+
+ // Test that unique message IDs are all handled
+ for (i in 0..BatchPushMessageReceiver.MAX_HANDLED_MESSAGE_IDS_COUNT + 10) {
+ receiver.reset()
+ receiver.onReceive(context, makeMessageIntent(null))
+ Assert.assertTrue(receiver.presentNotificationCalled)
+ }
+
+ // We looped more than MAX_HANDLED_MESSAGE_IDS_COUNT, test that the receiver isn't leaking
+ // memory by storing infinite IDs
+ Assert.assertEquals(BatchPushMessageReceiver.MAX_HANDLED_MESSAGE_IDS_COUNT,
+ BatchPushMessageReceiver.getHandledMessageIDsSize())
+
+ // Test that multiple pushes are deduplicated
+ val duplicateMessageID = UUID.randomUUID().toString()
+ receiver.reset()
+ receiver.onReceive(context, makeMessageIntent(duplicateMessageID))
+ Assert.assertTrue(receiver.presentNotificationCalled)
+ receiver.reset()
+ receiver.onReceive(context, makeMessageIntent(null))
+ Assert.assertTrue(receiver.presentNotificationCalled)
+ receiver.reset()
+ receiver.onReceive(context, makeMessageIntent(duplicateMessageID))
+ Assert.assertFalse(receiver.presentNotificationCalled)
+ }
+
+ @Test
+ fun testIgnoresNonFCMMessages() {
+ val receiver = ObservablePushMessageReceiver()
+ val intent = Intent()
+
+ receiver.onReceive(context, intent)
+ Assert.assertTrue(receiver.presentNotificationCalled)
+
+ receiver.reset()
+ intent.putExtra(EXTRA_MSG_TYPE, "gcm")
+ Assert.assertFalse(receiver.presentNotificationCalled)
+
+ receiver.reset()
+ intent.putExtra(EXTRA_MSG_TYPE, "data")
+ Assert.assertFalse(receiver.presentNotificationCalled)
+ }
+
+ private fun makeMessageIntent(forceID: String?): Intent {
+ val identifier = forceID ?: UUID.randomUUID().toString()
+
+ return Intent().apply {
+ putExtra("msg", "test message")
+ putExtra("com.batch", "{}")
+ putExtra(EXTRA_MSG_TYPE, "gcm")
+ putExtra("google.message_id", identifier)
+ }
+ }
+}
+
+private class ObservablePushMessageReceiver: BatchPushMessageReceiver() {
+ var presentNotificationCalled = false
+
+ override fun presentNotification(context: Context, intent: Intent): Boolean {
+ presentNotificationCalled = true
+ return true
+ }
+
+ fun reset() {
+ presentNotificationCalled = false
+ }
+}
\ No newline at end of file
diff --git a/Sources/sdk/src/test/java/com/batch/android/inbox/InboxNotificationContentParsingTest.kt b/Sources/sdk/src/test/java/com/batch/android/inbox/InboxNotificationContentParsingTest.kt
new file mode 100644
index 0000000..ef3c907
--- /dev/null
+++ b/Sources/sdk/src/test/java/com/batch/android/inbox/InboxNotificationContentParsingTest.kt
@@ -0,0 +1,110 @@
+package com.batch.android.inbox
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.batch.android.Batch
+import com.batch.android.BatchInboxNotificationContent
+import com.batch.android.BatchNotificationSource
+import com.batch.android.PrivateNotificationContentHelper
+import com.batch.android.json.JSONObject
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+import java.util.*
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class InboxNotificationContentParsingTest {
+
+ val now = Date()
+ val notifID = UUID.randomUUID().toString()
+
+ @Test
+ fun testPayloadParsing() {
+ // Test multiple combinations of payloads
+
+ var payload = addMessageToPayload(makeBaseNotificationPayload(), "foo", "bar")
+ var content: BatchInboxNotificationContent = makePublicNotification(payload)
+ Assert.assertEquals("foo", content.body)
+ Assert.assertEquals("bar", content.title)
+ Assert.assertTrue(content.isUnread)
+ Assert.assertFalse(content.isDeleted)
+ Assert.assertFalse(content.isSilent)
+ Assert.assertEquals("https://batch.com", content.pushPayload.deeplink)
+ Assert.assertEquals(now, content.date)
+ Assert.assertEquals(notifID, content.notificationIdentifier)
+ Assert.assertEquals(BatchNotificationSource.TRANSACTIONAL, content.source)
+
+ payload = addMessageToPayload(makeBaseNotificationPayload(), "foo", null)
+ payload.put("read", true)
+ payload.getJSONObject("payload").getJSONObject("com.batch").put("t", "c")
+ content = makePublicNotification(payload)
+ Assert.assertEquals("foo", content.body)
+ Assert.assertNull(content.title)
+ Assert.assertFalse(content.isUnread)
+ Assert.assertFalse(content.isDeleted)
+ Assert.assertFalse(content.isSilent)
+ Assert.assertEquals(BatchNotificationSource.CAMPAIGN, content.source)
+ }
+
+ @Test
+ fun testSilentNotificationDetection() {
+ // Test that a notification with no title and no body is silent
+
+ var payload = addMessageToPayload(makeBaseNotificationPayload(), null, null)
+ var content: BatchInboxNotificationContent = makePublicNotification(payload)
+ Assert.assertTrue(content.isSilent)
+
+ // Test that a notification with a title and no body is silent
+ payload = addMessageToPayload(makeBaseNotificationPayload(), null, "bar")
+ content = makePublicNotification(payload)
+ Assert.assertTrue(content.isSilent)
+
+ // Test that a notification with a title and body BUT with the "s" flag in com.batch is silent
+ payload = addMessageToPayload(makeBaseNotificationPayload(), null, null)
+ payload.getJSONObject("payload").getJSONObject("com.batch").put("s", true)
+ content = makePublicNotification(payload)
+ Assert.assertTrue(content.isSilent)
+
+ // Test that a notification with a body is not silent
+ payload = addMessageToPayload(makeBaseNotificationPayload(), "foo", null)
+ content = makePublicNotification(payload)
+ Assert.assertFalse(content.isSilent)
+ }
+
+ private fun makeBaseNotificationPayload(): JSONObject {
+ return JSONObject().apply {
+ put("notificationId", notifID)
+ put("sendId", "abcdeff")
+ put("notificationTime", now.time)
+ put("payload", JSONObject().apply {
+ put("com.batch", JSONObject().apply {
+ put("t", "t")
+ put("at", JSONObject().apply {
+ put("u", "https://batch.com")
+ })
+ put("od", JSONObject().apply {
+ put("n", "5a3c93c0-7a3b-0000-0000-69f412b0000000")
+ })
+ put("l", "https://batch.com")
+ put("i", "6y4g8guj-u1586420592376_000000")
+ })
+ })
+ }
+ }
+
+ private fun addMessageToPayload(rootPayload: JSONObject, body: String?, title: String?): JSONObject {
+ val payload = rootPayload.getJSONObject("payload")
+ body?.let {
+ payload.put(Batch.Push.BODY_KEY, body)
+ }
+ title?.let {
+ payload.put(Batch.Push.TITLE_KEY, title)
+ }
+ return rootPayload
+ }
+
+ private fun makePublicNotification(payload: JSONObject): BatchInboxNotificationContent {
+ return InboxFetchWebserviceClient.parseNotification(payload).let(PrivateNotificationContentHelper::getPublicContent)
+ }
+}
\ No newline at end of file
diff --git a/Sources/sdk/src/test/java/com/batch/android/localcampaigns/CampaignManagerTest.java b/Sources/sdk/src/test/java/com/batch/android/localcampaigns/CampaignManagerTest.java
index c449a46..9bab711 100644
--- a/Sources/sdk/src/test/java/com/batch/android/localcampaigns/CampaignManagerTest.java
+++ b/Sources/sdk/src/test/java/com/batch/android/localcampaigns/CampaignManagerTest.java
@@ -1,8 +1,10 @@
package com.batch.android.localcampaigns;
+import static com.batch.android.localcampaigns.model.LocalCampaign.SyncedJITResult;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
@@ -27,12 +29,18 @@
import java.io.InputStreamReader;
import java.lang.reflect.Field;
import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.UUID;
+import java.util.concurrent.TimeUnit;
import org.junit.After;
+import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.powermock.reflect.Whitebox;
@RunWith(AndroidJUnit4.class)
@MediumTest
@@ -41,14 +49,14 @@ public class CampaignManagerTest {
private Context context;
private CampaignManager campaignManager;
private JSONObject jsonCampaigns;
- private LocalCampaignsSQLTracker tracker;
+ private LocalCampaignsTracker tracker;
@Before
public void setUp() throws IOException, JSONException {
context = ApplicationProvider.getApplicationContext();
RuntimeManagerProvider.get().setContext(context);
- tracker = new LocalCampaignsSQLTracker();
+ tracker = new LocalCampaignsTracker();
campaignManager = new CampaignManager(tracker);
// Read fake campaigns file
@@ -73,7 +81,7 @@ public void testSaveCampaigns() throws NoSuchFieldException, IllegalAccessExcept
removeExistingSave();
assertFalse(campaignManager.hasSavedCampaigns(context));
LocalCampaignsResponse response = new LocalCampaignsResponseDeserializer(jsonCampaigns).deserialize();
- campaignManager.saveCampaigns(context, response.getCampaigns());
+ campaignManager.saveCampaigns(context, response);
assertTrue(campaignManager.hasSavedCampaigns(context));
removeExistingSave();
@@ -100,12 +108,14 @@ public void testSaveAndLoadCampaigns() throws NoSuchFieldException, IllegalAcces
// Save using the code that will actually save the response in production, rather
// than reimplementing it in the tests.
LocalCampaignsResponse response = new LocalCampaignsResponseDeserializer(jsonCampaigns).deserialize();
- campaignManager.saveCampaigns(context, response.getCampaigns());
+ campaignManager.saveCampaigns(context, response);
assertTrue(campaignManager.hasSavedCampaigns(context));
assertEquals(0, campaignManager.getCampaignList().size());
+ assertNull(campaignManager.getCappings());
campaignManager.loadSavedCampaignResponse(context);
assertEquals(1, campaignManager.getCampaignList().size());
+ assertNotNull(campaignManager.getCappings());
removeExistingSave();
assertFalse(campaignManager.hasSavedCampaigns(context));
@@ -115,11 +125,31 @@ public void testSaveAndLoadCampaigns() throws NoSuchFieldException, IllegalAcces
public void testLoadCampaigns() throws NoSuchFieldException, IllegalAccessException, JSONException {
removeExistingSave();
LocalCampaignsResponse response = new LocalCampaignsResponseDeserializer(jsonCampaigns).deserialize();
- campaignManager.saveCampaigns(context, response.getCampaigns());
+ campaignManager.saveCampaigns(context, response);
assertTrue(campaignManager.getCampaignList().isEmpty());
+ assertNull(campaignManager.getCappings());
campaignManager.loadSavedCampaignResponse(context);
assertFalse(campaignManager.getCampaignList().isEmpty());
+ assertNotNull(campaignManager.getCappings());
+ }
+
+ @Test
+ public void testLoadExpiredCampaigns() throws JSONException, NoSuchFieldException, IllegalAccessException {
+ removeExistingSave();
+
+ final BatchDate fakeCurrentDate = new UTCDate(0);
+ Field dateProviderField = CampaignManager.class.getDeclaredField("dateProvider");
+ dateProviderField.setAccessible(true);
+ dateProviderField.set(campaignManager, (DateProvider) () -> fakeCurrentDate);
+
+ LocalCampaignsResponse response = new LocalCampaignsResponseDeserializer(jsonCampaigns).deserialize();
+ campaignManager.saveCampaigns(context, response);
+
+ fakeCurrentDate.setTime(TimeUnit.DAYS.toMillis(15));
+
+ campaignManager.loadSavedCampaignResponse(context);
+ assertTrue(campaignManager.getCampaignList().isEmpty());
}
@Test
@@ -171,6 +201,45 @@ public void testCapping()
assertTrue(campaignManager.isCampaignOverCapping(campaign, true));
}
+ @Test
+ public void testGlobalCapping()
+ throws JSONException, NoSuchFieldException, IllegalAccessException, ViewTrackerUnavailableException {
+ // Load cappings from the json local campaign response (2/session & 1/h)
+ reloadCampaigns();
+
+ // Setting fake date provider
+ final BatchDate fakeCurrentDate = new UTCDate(0);
+ Field dateProviderField = CampaignManager.class.getDeclaredField("dateProvider");
+ dateProviderField.setAccessible(true);
+ dateProviderField.set(campaignManager, (DateProvider) () -> fakeCurrentDate);
+
+ DateProvider oldDateProvider = tracker.getDateProvider();
+ tracker.setDateProvider(() -> fakeCurrentDate);
+
+ assertFalse(campaignManager.isOverGlobalCappings());
+
+ campaignManager.getViewTracker().trackViewEvent("campaign_id");
+
+ // We have reached time-based capping
+ assertTrue(campaignManager.isOverGlobalCappings());
+
+ // Adding 1h
+ fakeCurrentDate.setTime(3600 * 1000);
+
+ // time-based capping released
+ assertFalse(campaignManager.isOverGlobalCappings());
+
+ campaignManager.getViewTracker().trackViewEvent("campaign_id");
+
+ // Adding 1h
+ fakeCurrentDate.setTime(3600 * 1000);
+
+ // We have reached the session capping
+ assertTrue(campaignManager.isOverGlobalCappings());
+
+ tracker.setDateProvider(oldDateProvider);
+ }
+
@Test
public void testGracePeriod()
throws NoSuchFieldException, IllegalAccessException, ViewTrackerUnavailableException, JSONException {
@@ -289,9 +358,81 @@ public void testPriority() throws NoSuchFieldException, IllegalAccessException {
campaignManager.updateCampaignList(fakeCampaigns);
- LocalCampaign campainToDisplay = campaignManager.getCampaignToDisplay(trigger -> true);
+ List eligibleCampaigns = campaignManager.getEligibleCampaignsSortedByPriority(trigger -> true);
+
+ assertFalse(eligibleCampaigns.isEmpty());
+ assertSame(highPriorityCampaign, eligibleCampaigns.get(0));
+ }
+
+ @Test
+ public void testGetEligibleCampaignsRequiringSync() {
+ List fakeCampaigns = new ArrayList<>();
+ fakeCampaigns.add(createFakeCampaignWithPriority(0));
+ fakeCampaigns.add(createFakeCampaignWithPriority(10));
+ Assert.assertEquals(0, campaignManager.getFirstEligibleCampaignsRequiringSync(fakeCampaigns).size());
+ LocalCampaign jitCampaign = createFakeCampaignWithPriority(0);
+ jitCampaign.requiresJustInTimeSync = true;
+ fakeCampaigns.add(0, jitCampaign);
+ Assert.assertEquals(jitCampaign, campaignManager.getFirstEligibleCampaignsRequiringSync(fakeCampaigns).get(0));
+ }
+
+ @Test
+ public void testGetFirstCampaignNotRequiringJITSync() {
+ List fakeCampaigns = new ArrayList<>();
+ LocalCampaign jitCampaign = createFakeCampaignWithPriority(0);
+ jitCampaign.requiresJustInTimeSync = true;
+ fakeCampaigns.add(jitCampaign);
+ LocalCampaign expectedCampaign = createFakeCampaignWithPriority(0);
+ fakeCampaigns.add(expectedCampaign);
+ Assert.assertEquals(expectedCampaign, campaignManager.getFirstCampaignNotRequiringJITSync(fakeCampaigns));
+ }
+
+ @Test
+ public void testIsJITServiceAvailable() {
+ Assert.assertTrue(campaignManager.isJITServiceAvailable());
+ Whitebox.setInternalState(campaignManager, "nextAvailableJITTimestamp", new Date().getTime() + 1000L);
+ Assert.assertFalse(campaignManager.isJITServiceAvailable());
+ }
+
+ @Test
+ public void testGetSyncedJITCampaignState() throws NoSuchFieldException, IllegalAccessException {
+ // Get synced jit campaign from manager
+ Field syncedCampaignsField = CampaignManager.class.getDeclaredField("syncedJITCampaigns");
+ syncedCampaignsField.setAccessible(true);
+ Map syncedCampaigns = (HashMap) syncedCampaignsField.get(
+ campaignManager
+ );
+
+ // Replace the SecureDateProvider with a fake DateProvider
+ final BatchDate fakeCurrentDate = new UTCDate(0);
+ Field dateProviderField = CampaignManager.class.getDeclaredField("dateProvider");
+ dateProviderField.setAccessible(true);
+ dateProviderField.set(campaignManager, (DateProvider) () -> fakeCurrentDate);
+
+ // Ensure non-jit campaign is return as eligible
+ LocalCampaign campaign = createFakeCampaignWithPriority(0);
+ Assert.assertEquals(SyncedJITResult.State.ELIGIBLE, campaignManager.getSyncedJITCampaignState(campaign));
+
+ // Ensure non-cached jit campaign requires a sync
+ campaign.requiresJustInTimeSync = true;
+ assert syncedCampaigns != null;
+ Assert.assertEquals(SyncedJITResult.State.REQUIRES_SYNC, campaignManager.getSyncedJITCampaignState(campaign));
+
+ // Adding fake synced jit result in cache
+ SyncedJITResult result = new SyncedJITResult(fakeCurrentDate.getTime());
+ result.eligible = false;
+ syncedCampaigns.put(campaign.id, result);
+
+ // Ensure cached jit campaign is not eligible
+ Assert.assertEquals(SyncedJITResult.State.NOT_ELIGIBLE, campaignManager.getSyncedJITCampaignState(campaign));
+
+ // Ensure cached jit campaign is eligible
+ result.eligible = true;
+ Assert.assertEquals(SyncedJITResult.State.ELIGIBLE, campaignManager.getSyncedJITCampaignState(campaign));
- assertSame(highPriorityCampaign, campainToDisplay);
+ // Ensure cached jit campaign requires a new sync
+ fakeCurrentDate.setTime(30_000);
+ Assert.assertEquals(SyncedJITResult.State.REQUIRES_SYNC, campaignManager.getSyncedJITCampaignState(campaign));
}
private void removeExistingSave() throws NoSuchFieldException, IllegalAccessException {
@@ -315,7 +456,7 @@ private void removeExistingSave() throws NoSuchFieldException, IllegalAccessExce
private void reloadCampaigns() throws NoSuchFieldException, IllegalAccessException, JSONException {
removeExistingSave();
LocalCampaignsResponse response = new LocalCampaignsResponseDeserializer(jsonCampaigns).deserialize();
- campaignManager.saveCampaigns(context, response.getCampaigns());
+ campaignManager.saveCampaigns(context, response);
campaignManager.loadSavedCampaignResponse(context);
}
diff --git a/Sources/sdk/src/test/java/com/batch/android/localcampaigns/EventTriggerTest.java b/Sources/sdk/src/test/java/com/batch/android/localcampaigns/EventTriggerTest.java
index 6fb0e8e..cfec041 100644
--- a/Sources/sdk/src/test/java/com/batch/android/localcampaigns/EventTriggerTest.java
+++ b/Sources/sdk/src/test/java/com/batch/android/localcampaigns/EventTriggerTest.java
@@ -2,6 +2,7 @@
import static org.mockito.ArgumentMatchers.argThat;
+import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import androidx.test.rule.ActivityTestRule;
@@ -43,7 +44,7 @@ public void setUp() {
LocalCampaignsModule module = DITestUtils.mockSingletonDependency(LocalCampaignsModule.class, null);
simulateBatchStart(activityRule.getActivity());
- module.batchDidStart();
+ module.batchContextBecameAvailable(ApplicationProvider.getApplicationContext());
}
@After
@@ -90,6 +91,9 @@ public void testCampaignDisplayedAfterEventTracked()
CampaignManagerProvider.get().updateCampaignList(Collections.singletonList(campaign));
+ // Simulate synchro is finished
+ LocalCampaignsModuleProvider.get().onLocalCampaignsWebserviceFinished();
+
// Track the event which is linked to the Local Campaign
Batch.User.trackEvent(EVENT_NAME_TEST);
diff --git a/Sources/sdk/src/test/java/com/batch/android/localcampaigns/LocalCampaignsModuleTests.java b/Sources/sdk/src/test/java/com/batch/android/localcampaigns/LocalCampaignsModuleTests.java
new file mode 100644
index 0000000..a502774
--- /dev/null
+++ b/Sources/sdk/src/test/java/com/batch/android/localcampaigns/LocalCampaignsModuleTests.java
@@ -0,0 +1,70 @@
+package com.batch.android.localcampaigns;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import com.batch.android.di.DI;
+import com.batch.android.di.DITestUtils;
+import com.batch.android.di.providers.CampaignManagerProvider;
+import com.batch.android.di.providers.LandingOutputProvider;
+import com.batch.android.di.providers.LocalCampaignsModuleProvider;
+import com.batch.android.json.JSONObject;
+import com.batch.android.localcampaigns.model.LocalCampaign;
+import com.batch.android.localcampaigns.signal.NewSessionSignal;
+import com.batch.android.localcampaigns.signal.Signal;
+import com.batch.android.localcampaigns.trigger.NextSessionTrigger;
+import com.batch.android.module.LocalCampaignsModule;
+import java.lang.reflect.Field;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class LocalCampaignsModuleTests {
+
+ @Before
+ public void setup() {
+ DI.reset();
+ LocalCampaignsModule module = DITestUtils.mockSingletonDependency(LocalCampaignsModule.class, null);
+ module.batchDidStart();
+ }
+
+ @After
+ public void teardown() {
+ DI.reset();
+ }
+
+ @Test
+ public void testSignalQueue() throws NoSuchFieldException, IllegalAccessException, InterruptedException {
+ LocalCampaignsModule module = LocalCampaignsModuleProvider.get();
+
+ Field signalQueueField = LocalCampaignsModule.class.getDeclaredField("signalQueue");
+ signalQueueField.setAccessible(true);
+ LinkedList signalQueue = (LinkedList) signalQueueField.get(module);
+
+ Field isReadyField = LocalCampaignsModule.class.getDeclaredField("isReady");
+ isReadyField.setAccessible(true);
+ AtomicBoolean isReady = (AtomicBoolean) isReadyField.get(module);
+
+ assert signalQueue != null;
+ assert isReady != null;
+
+ Assert.assertFalse(isReady.get());
+ Assert.assertEquals(0, signalQueue.size());
+
+ module.sendSignal(new NewSessionSignal());
+ Assert.assertEquals(1, signalQueue.size());
+
+ // Simulate synchro is finished
+ module.onLocalCampaignsWebserviceFinished();
+
+ Assert.assertTrue(isReady.get());
+ Assert.assertEquals(0, signalQueue.size());
+ }
+}
diff --git a/Sources/sdk/src/test/java/com/batch/android/localcampaigns/LocalCampaignsResponseFactory.java b/Sources/sdk/src/test/java/com/batch/android/localcampaigns/LocalCampaignsResponseFactory.java
index 988b0ae..16a79c1 100644
--- a/Sources/sdk/src/test/java/com/batch/android/localcampaigns/LocalCampaignsResponseFactory.java
+++ b/Sources/sdk/src/test/java/com/batch/android/localcampaigns/LocalCampaignsResponseFactory.java
@@ -12,12 +12,27 @@
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
+import java.util.List;
public class LocalCampaignsResponseFactory {
public LocalCampaignsResponse createLocalCampaignsResponse() throws JSONException {
LocalCampaignsResponse response = new LocalCampaignsResponse("dummy_id");
response.setCampaigns(new ArrayList<>());
+
+ // Global cappings
+ List timeBasedCappings = new ArrayList<>();
+ LocalCampaignsResponse.GlobalCappings.TimeBasedCapping timeBasedCapping = new LocalCampaignsResponse.GlobalCappings.TimeBasedCapping(
+ 2,
+ 3600
+ );
+ timeBasedCappings.add(timeBasedCapping);
+ LocalCampaignsResponse.GlobalCappings cappings = new LocalCampaignsResponse.GlobalCappings(
+ 2,
+ timeBasedCappings
+ );
+ response.setCappings(cappings);
+ // Local campaigns
LocalCampaign campaign = new LocalCampaign();
campaign.id = "25876676";
campaign.capping = 3;
@@ -36,6 +51,7 @@ public LocalCampaignsResponse createLocalCampaignsResponse() throws JSONExceptio
"{\n \"type\":\"LANDING\",\n \"payload\":{\n \"kind\":\"universal\",\n \"id\":\"webtest\",\n \"did\":\"webtest\",\n \"hero\":\"https://static.batch.com.s3.eu-west-1.amazonaws.com/documentation/logo_batch_full_178.png\",\n \"h1\":\"WOW\",\n \"h2\":\"Ho\",\n \"h3\":\"Subtitle\",\n \"body\":\"This is a NEXT_SESSION triggered campaign.\",\n \"close\":true,\n \"cta\":[\n {\n \"l\":\"Okay!\",\n \"a\":null,\n \"args\":{\n \n }\n },\n {\n \"l\":\"Okay!2\",\n \"a\":null,\n \"args\":{\n \n }\n }\n ],\n \"style\":\"#image-cnt {blur: 200;} #image {border-radius: 10; margin-left: 30; margin-right: 30; margin-top: 40;} #placeholder{background-color:#018BAA;}#content {\\n background-color: #018BFF;\\n height: 100%\\n padding-top: 24;\\n padding-left: 20;\\n padding-right: 20;\\n padding-bottom: 20;\\n}\\n#h1 {\\n color: #018BFF;\\n padding-left: 15;\\n padding-right: 15;\\n padding-top: 4;\\n padding-bottom: 4;\\n border-radius: 12;\\n background-color:white;\\n font-weight: bold;\\n font-size: 12;\\n height: 24;\\n width: auto;\\n}\\n#h2 {\\n margin-top: 24;\\n color: white;\\n font-weight: bold;\\n font-size: 35;\\n}\\n#body {\\n color: #80C5FF;\\n}\\n#cta1 {\\n color: #018BFF;\\n padding-left: 60;\\n padding-right: 60;\\n padding-top: 10;\\n padding-bottom: 10;\\n border-radius: 4;\\n background-color:white;\\n font-weight: bold;\\n font-size: 18;\\n}\\n#close {\\n glyph-width: 1.5;\\n glyph-padding: 11;\\n background-color: #212C3C;\\n margin-top: 30;\\n margin-right: 30;\\n}\"\n }\n}"
)
);
+ campaign.requiresJustInTimeSync = true;
response.getCampaigns().add(campaign);
return response;
}
diff --git a/Sources/sdk/src/test/java/com/batch/android/localcampaigns/LocalCampaignSQLTrackerTest.java b/Sources/sdk/src/test/java/com/batch/android/localcampaigns/LocalCampaignsTrackerTest.java
similarity index 73%
rename from Sources/sdk/src/test/java/com/batch/android/localcampaigns/LocalCampaignSQLTrackerTest.java
rename to Sources/sdk/src/test/java/com/batch/android/localcampaigns/LocalCampaignsTrackerTest.java
index 1b7ab88..61e114e 100644
--- a/Sources/sdk/src/test/java/com/batch/android/localcampaigns/LocalCampaignSQLTrackerTest.java
+++ b/Sources/sdk/src/test/java/com/batch/android/localcampaigns/LocalCampaignsTrackerTest.java
@@ -5,6 +5,8 @@
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import com.batch.android.core.DateProvider;
+import com.batch.android.core.SystemDateProvider;
import java.lang.reflect.Field;
import org.junit.Assert;
import org.junit.Before;
@@ -13,7 +15,7 @@
@RunWith(AndroidJUnit4.class)
@SmallTest
-public class LocalCampaignSQLTrackerTest {
+public class LocalCampaignsTrackerTest {
private Context appContext;
private Field dbHelperField;
@@ -124,6 +126,38 @@ public void testCampaignLastOccurence() throws ViewTrackerUnavailableException {
tracker.close();
}
+ @Test
+ public void testGetNumberOfViewEventsSince() throws ViewTrackerUnavailableException {
+ // Clear database
+ appContext.deleteDatabase(LocalCampaignTrackDbHelper.DATABASE_NAME);
+
+ LocalCampaignsSQLTracker tracker = new LocalCampaignsSQLTracker();
+ tracker.open(appContext);
+
+ DateProvider dateProvider = new SystemDateProvider();
+
+ //0 tracked view events
+ Assert.assertEquals(
+ 0,
+ tracker.getNumberOfViewEventsSince(dateProvider.getCurrentDate().getTime() - (60 * 1000))
+ );
+
+ // track view event
+ tracker.trackViewEvent("campaign_id");
+
+ long timestamp = dateProvider.getCurrentDate().getTime();
+ // 1 tracked view event since 1 sec
+ Assert.assertEquals(1, tracker.getNumberOfViewEventsSince(timestamp - (1000)));
+
+ // Adding 1 sec
+ timestamp += 1000;
+
+ // 0 tracked view event since 1 sec
+ Assert.assertEquals(0, tracker.getNumberOfViewEventsSince(timestamp - (1000)));
+
+ tracker.close();
+ }
+
@Test
// Tests that the db being not opened results in a specific exception
public void testUnavailabilityException() {
@@ -135,4 +169,16 @@ public void testUnavailabilityException() {
}
Assert.fail("A ViewTrackerUnavailableException should have been thrown");
}
+
+ @Test
+ public void testSessionViewsCount() throws ViewTrackerUnavailableException {
+ LocalCampaignsTracker tracker = new LocalCampaignsTracker();
+ tracker.open(appContext);
+ Assert.assertEquals(0, tracker.getSessionViewsCount());
+ tracker.trackViewEvent("campaign_id");
+ Assert.assertEquals(1, tracker.getSessionViewsCount());
+ tracker.close();
+ tracker.resetSessionViewsCount();
+ Assert.assertEquals(0, tracker.getSessionViewsCount());
+ }
}
diff --git a/Sources/sdk/src/test/java/com/batch/android/localcampaigns/output/ActionOutputTest.kt b/Sources/sdk/src/test/java/com/batch/android/localcampaigns/output/ActionOutputTest.kt
new file mode 100644
index 0000000..a312125
--- /dev/null
+++ b/Sources/sdk/src/test/java/com/batch/android/localcampaigns/output/ActionOutputTest.kt
@@ -0,0 +1,129 @@
+package com.batch.android.localcampaigns.output
+
+import android.content.Context
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.batch.android.JSONObjectMockitoMatcher
+import com.batch.android.di.DI
+import com.batch.android.di.DITest
+import com.batch.android.di.DITestUtils
+import com.batch.android.json.JSONObject
+import com.batch.android.localcampaigns.model.LocalCampaign
+import com.batch.android.module.ActionModule
+import com.batch.android.runtime.RuntimeManager
+import org.junit.After
+import org.junit.Assert
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito
+import org.powermock.api.mockito.PowerMockito
+
+@Suppress("UsePropertyAccessSyntax")
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class ActionOutputTest {
+
+ @After
+ fun cleanup() {
+ DI.reset()
+ }
+
+ @Test
+ fun testExecutesAction() {
+ val context: Context = ApplicationProvider.getApplicationContext()
+ val runtimeManager = DITestUtils.mockSingletonDependency(RuntimeManager::class.java, null)
+ PowerMockito
+ .doReturn(context)
+ .`when`(runtimeManager)
+ .getContext()
+
+ val actionModuleSpy = DITestUtils.mockSingletonDependency(ActionModule::class.java, null)
+ PowerMockito
+ .doReturn(true)
+ .`when`(actionModuleSpy)
+ .performAction(any(), anyString(), any(JSONObject::class.java), any())
+
+ val fakeCampaign = LocalCampaign()
+ fakeCampaign.id = "foobar"
+
+ var output = ActionOutput(JSONObject().apply {
+ put("action", "foo")
+ })
+ Assert.assertTrue(output.displayMessage(fakeCampaign))
+
+ Mockito.verify(actionModuleSpy).performAction(Mockito.eq(context),
+ Mockito.eq("foo"),
+ JSONObjectMockitoMatcher.eq(JSONObject()),
+ Mockito.isNull())
+
+ output = ActionOutput(JSONObject().apply {
+ put("action", "foobar")
+ // Invalid args type
+ put("args", 2)
+ })
+ Assert.assertTrue(output.displayMessage(fakeCampaign))
+
+ Mockito.verify(actionModuleSpy).performAction(Mockito.eq(context),
+ Mockito.eq("foobar"),
+ JSONObjectMockitoMatcher.eq(JSONObject()),
+ Mockito.isNull())
+
+ output = ActionOutput(JSONObject().apply {
+ put("action", "foobaz")
+ put("args", JSONObject().apply {
+ put("arg1", "val1")
+ })
+ })
+ Assert.assertTrue(output.displayMessage(fakeCampaign))
+
+ Mockito.verify(actionModuleSpy).performAction(Mockito.eq(context),
+ Mockito.eq("foobaz"),
+ JSONObjectMockitoMatcher.eq(JSONObject().apply {
+ put("arg1", "val1")
+ }),
+ Mockito.isNull())
+ }
+
+ @Test
+ fun testDoesNotCrashWithInvalidPayload() {
+ val actionModuleSpy = DITestUtils.mockSingletonDependency(ActionModule::class.java, null)
+ PowerMockito
+ .doReturn(false)
+ .`when`(actionModuleSpy)
+ .performAction(any(), anyString(), any(JSONObject::class.java), any())
+
+ val runtimeManager = DITestUtils.mockSingletonDependency(RuntimeManager::class.java, null)
+ PowerMockito
+ .doReturn(null)
+ .`when`(runtimeManager)
+ .getContext()
+
+ val fakeCampaign = LocalCampaign()
+ fakeCampaign.id = "foobar"
+
+ var output = ActionOutput(JSONObject())
+ Assert.assertFalse(output.displayMessage(fakeCampaign))
+
+ output = ActionOutput(JSONObject().apply {
+ put("foo", "bar")
+ })
+ Assert.assertFalse(output.displayMessage(fakeCampaign))
+
+ Mockito.verifyNoInteractions(actionModuleSpy)
+
+ output = ActionOutput(JSONObject().apply {
+ put("action", 2)
+ })
+ Assert.assertFalse(output.displayMessage(fakeCampaign))
+
+ output = ActionOutput(JSONObject().apply {
+ put("action", JSONObject())
+ })
+ Assert.assertFalse(output.displayMessage(fakeCampaign))
+
+ // Don't call "verifyNoInteractions" as "action" is stringified if of the wrong type
+ }
+}
\ No newline at end of file
diff --git a/Sources/sdk/src/test/java/com/batch/android/localcampaigns/serialization/LocalCampaignsResponseSerializationTest.java b/Sources/sdk/src/test/java/com/batch/android/localcampaigns/serialization/LocalCampaignsResponseSerializationTest.java
index 07e3bd2..60d4118 100644
--- a/Sources/sdk/src/test/java/com/batch/android/localcampaigns/serialization/LocalCampaignsResponseSerializationTest.java
+++ b/Sources/sdk/src/test/java/com/batch/android/localcampaigns/serialization/LocalCampaignsResponseSerializationTest.java
@@ -8,6 +8,7 @@
import com.batch.android.json.JSONObject;
import com.batch.android.localcampaigns.LocalCampaignsResponseFactory;
import com.batch.android.localcampaigns.model.LocalCampaign;
+import com.batch.android.localcampaigns.output.ActionOutput;
import com.batch.android.localcampaigns.output.LandingOutput;
import com.batch.android.query.response.LocalCampaignsResponse;
import com.batch.android.query.serialization.deserializers.LocalCampaignsResponseDeserializer;
@@ -32,10 +33,26 @@ public void setUp() {
@Test
public void testValidSerialization() throws JSONException {
LocalCampaignsResponse response = factory.createLocalCampaignsResponse();
- LocalCampaignsResponseSerializer serializer = new LocalCampaignsResponseSerializer(response);
- JSONObject serializedResponse = serializer.serialize();
+ LocalCampaignsResponseSerializer serializer = new LocalCampaignsResponseSerializer();
+ JSONObject serializedResponse = serializer.serialize(response);
+
Assert.assertEquals(response.getQueryID(), serializedResponse.getString("id"));
Assert.assertFalse(serializedResponse.has("minDisplayInterval"));
+
+ // Global cappings
+ LocalCampaignsResponse.GlobalCappings cappings = response.getCappings();
+ JSONObject jsonGlobalCappings = serializer.serializeCappings(cappings);
+ Assert.assertEquals(cappings.getSession(), Integer.valueOf(jsonGlobalCappings.getInt("session")));
+ Assert.assertEquals(
+ cappings.getTimeBasedCappings().get(0).getViews(),
+ Integer.valueOf(jsonGlobalCappings.getJSONArray("time").getJSONObject(0).getInt("views"))
+ );
+ Assert.assertEquals(
+ cappings.getTimeBasedCappings().get(0).getDuration(),
+ Integer.valueOf(jsonGlobalCappings.getJSONArray("time").getJSONObject(0).getInt("duration"))
+ );
+
+ // Local campaigns
Assert.assertTrue(serializedResponse.hasNonNull("campaigns"));
LocalCampaign campaign = response.getCampaigns().get(0);
JSONObject serializedCampaign = serializedResponse.getJSONArray("campaigns").getJSONObject(0);
@@ -69,6 +86,7 @@ public void testValidSerialization() throws JSONException {
campaign.output.payload,
serializedCampaign.getJSONObject("output").getJSONObject("payload")
);
+ Assert.assertTrue(serializedCampaign.getBoolean("requireJIT"));
}
@Test
@@ -82,7 +100,27 @@ public void testValidDeserialization() throws JSONException, IOException {
Assert.assertFalse(response.hasError());
Assert.assertTrue(response.hasCampaigns());
Assert.assertNull(response.getMinDisplayInterval());
- Assert.assertEquals(response.getCampaigns().size(), 1);
+ Assert.assertEquals(response.getCampaigns().size(), 2);
+
+ // Global cappings
+ JSONObject jsonCappings = validJsonCampaignsResponse.getJSONObject("cappings");
+ JSONObject jsonTimeBasedCappings = jsonCappings.getJSONArray("time").getJSONObject(0);
+ Assert.assertNotNull(response.getCappings());
+ Assert.assertNotNull(response.getCappings().getSession());
+ Assert.assertNotNull(response.getCappings().getTimeBasedCappings());
+ Assert.assertTrue(response.hasCappings());
+ Assert.assertEquals(response.getCappings().getSession().intValue(), jsonCappings.getInt("session"));
+ Assert.assertEquals(response.getCappings().getTimeBasedCappings().size(), 1);
+ Assert.assertEquals(
+ response.getCappings().getTimeBasedCappings().get(0).getViews().intValue(),
+ jsonTimeBasedCappings.getInt("views")
+ );
+ Assert.assertEquals(
+ response.getCappings().getTimeBasedCappings().get(0).getDuration().intValue(),
+ jsonTimeBasedCappings.getInt("duration")
+ );
+
+ // Local Campaign
JSONObject jsonCampaign = validJsonCampaignsResponse.getJSONArray("campaigns").getJSONObject(0);
LocalCampaign campaign = response.getCampaigns().get(0);
Assert.assertEquals(jsonCampaign.getString("campaignId"), campaign.id);
@@ -112,6 +150,13 @@ public void testValidDeserialization() throws JSONException, IOException {
Assert.assertEquals(jsonTriggers.getJSONObject(0).getString("type"), campaign.triggers.get(0).getType());
Assert.assertTrue(campaign.output instanceof LandingOutput);
Assert.assertEquals(jsonCampaign.getJSONObject("output").getJSONObject("payload"), campaign.output.payload);
+ Assert.assertTrue(campaign.requiresJustInTimeSync);
+
+ // Test ACTION deserialization
+ jsonCampaign = validJsonCampaignsResponse.getJSONArray("campaigns").getJSONObject(1);
+ campaign = response.getCampaigns().get(1);
+ Assert.assertTrue(campaign.output instanceof ActionOutput);
+ Assert.assertEquals(jsonCampaign.getJSONObject("output").getJSONObject("payload"), campaign.output.payload);
}
@Test
@@ -122,6 +167,7 @@ public void testErrorDeserialization() throws JSONException {
LocalCampaignsResponse response = deserializer.deserialize();
Assert.assertTrue(response.hasError());
Assert.assertFalse(response.hasCampaigns());
+ Assert.assertFalse(response.hasCappings());
}
@Test
@@ -132,5 +178,6 @@ public void testEmptyDeserialization() throws JSONException {
LocalCampaignsResponse response = deserializer.deserialize();
Assert.assertFalse(response.hasError());
Assert.assertFalse(response.hasCampaigns());
+ Assert.assertFalse(response.hasCappings());
}
}
diff --git a/Sources/sdk/src/test/java/com/batch/android/localcampaigns/signal/CampaignsLoadedSignalTest.java b/Sources/sdk/src/test/java/com/batch/android/localcampaigns/signal/CampaignsLoadedSignalTest.java
deleted file mode 100644
index 30b8929..0000000
--- a/Sources/sdk/src/test/java/com/batch/android/localcampaigns/signal/CampaignsLoadedSignalTest.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package com.batch.android.localcampaigns.signal;
-
-import com.batch.android.localcampaigns.model.LocalCampaign;
-import com.batch.android.localcampaigns.trigger.CampaignsLoadedTrigger;
-import com.batch.android.localcampaigns.trigger.CampaignsRefreshedTrigger;
-import com.batch.android.localcampaigns.trigger.EventLocalCampaignTrigger;
-import com.batch.android.localcampaigns.trigger.NextSessionTrigger;
-import com.batch.android.localcampaigns.trigger.NowTrigger;
-import org.junit.Assert;
-import org.junit.Test;
-
-public class CampaignsLoadedSignalTest {
-
- @Test
- public void testSatisfiesTrigger() {
- Signal signal = new CampaignsLoadedSignal();
-
- Assert.assertTrue(signal.satisfiesTrigger(new NowTrigger()));
- Assert.assertTrue(signal.satisfiesTrigger(new CampaignsLoadedTrigger()));
- Assert.assertTrue(signal.satisfiesTrigger(new NextSessionTrigger()));
-
- Assert.assertFalse(signal.satisfiesTrigger(new CampaignsRefreshedTrigger()));
- Assert.assertFalse(signal.satisfiesTrigger(new EventLocalCampaignTrigger("eventname", null)));
- Assert.assertFalse(
- signal.satisfiesTrigger(
- new LocalCampaign.Trigger() {
- @Override
- public String getType() {
- return null;
- }
- }
- )
- );
- }
-}
diff --git a/Sources/sdk/src/test/java/com/batch/android/localcampaigns/signal/CampaignsRefreshedSignalTest.java b/Sources/sdk/src/test/java/com/batch/android/localcampaigns/signal/CampaignsRefreshedSignalTest.java
deleted file mode 100644
index 93e75c6..0000000
--- a/Sources/sdk/src/test/java/com/batch/android/localcampaigns/signal/CampaignsRefreshedSignalTest.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package com.batch.android.localcampaigns.signal;
-
-import com.batch.android.localcampaigns.model.LocalCampaign;
-import com.batch.android.localcampaigns.trigger.CampaignsLoadedTrigger;
-import com.batch.android.localcampaigns.trigger.CampaignsRefreshedTrigger;
-import com.batch.android.localcampaigns.trigger.EventLocalCampaignTrigger;
-import com.batch.android.localcampaigns.trigger.NextSessionTrigger;
-import com.batch.android.localcampaigns.trigger.NowTrigger;
-import org.junit.Assert;
-import org.junit.Test;
-
-public class CampaignsRefreshedSignalTest {
-
- @Test
- public void testSatisfiesTrigger() {
- Signal signal = new CampaignsRefreshedSignal();
-
- Assert.assertTrue(signal.satisfiesTrigger(new NowTrigger()));
- Assert.assertTrue(signal.satisfiesTrigger(new CampaignsRefreshedTrigger()));
-
- Assert.assertFalse(signal.satisfiesTrigger(new CampaignsLoadedTrigger()));
- Assert.assertFalse(signal.satisfiesTrigger(new NextSessionTrigger()));
- Assert.assertFalse(signal.satisfiesTrigger(new EventLocalCampaignTrigger("eventname", null)));
- Assert.assertFalse(
- signal.satisfiesTrigger(
- new LocalCampaign.Trigger() {
- @Override
- public String getType() {
- return null;
- }
- }
- )
- );
- }
-}
diff --git a/Sources/sdk/src/test/java/com/batch/android/localcampaigns/signal/EventTrackedSignalTest.java b/Sources/sdk/src/test/java/com/batch/android/localcampaigns/signal/EventTrackedSignalTest.java
index 6cd2af1..e56f6a3 100644
--- a/Sources/sdk/src/test/java/com/batch/android/localcampaigns/signal/EventTrackedSignalTest.java
+++ b/Sources/sdk/src/test/java/com/batch/android/localcampaigns/signal/EventTrackedSignalTest.java
@@ -1,11 +1,8 @@
package com.batch.android.localcampaigns.signal;
import com.batch.android.localcampaigns.model.LocalCampaign;
-import com.batch.android.localcampaigns.trigger.CampaignsLoadedTrigger;
-import com.batch.android.localcampaigns.trigger.CampaignsRefreshedTrigger;
import com.batch.android.localcampaigns.trigger.EventLocalCampaignTrigger;
import com.batch.android.localcampaigns.trigger.NextSessionTrigger;
-import com.batch.android.localcampaigns.trigger.NowTrigger;
import org.junit.Assert;
import org.junit.Test;
@@ -18,9 +15,6 @@ public void testSatisfiesTrigger() {
Assert.assertTrue(signal.satisfiesTrigger(new EventLocalCampaignTrigger(eventName, null)));
- Assert.assertFalse(signal.satisfiesTrigger(new NowTrigger()));
- Assert.assertFalse(signal.satisfiesTrigger(new CampaignsRefreshedTrigger()));
- Assert.assertFalse(signal.satisfiesTrigger(new CampaignsLoadedTrigger()));
Assert.assertFalse(signal.satisfiesTrigger(new NextSessionTrigger()));
Assert.assertFalse(
signal.satisfiesTrigger(
diff --git a/Sources/sdk/src/test/java/com/batch/android/localcampaigns/signal/NewSessionSignalTest.java b/Sources/sdk/src/test/java/com/batch/android/localcampaigns/signal/NewSessionSignalTest.java
index 4596987..e7135d9 100644
--- a/Sources/sdk/src/test/java/com/batch/android/localcampaigns/signal/NewSessionSignalTest.java
+++ b/Sources/sdk/src/test/java/com/batch/android/localcampaigns/signal/NewSessionSignalTest.java
@@ -1,11 +1,8 @@
package com.batch.android.localcampaigns.signal;
import com.batch.android.localcampaigns.model.LocalCampaign;
-import com.batch.android.localcampaigns.trigger.CampaignsLoadedTrigger;
-import com.batch.android.localcampaigns.trigger.CampaignsRefreshedTrigger;
import com.batch.android.localcampaigns.trigger.EventLocalCampaignTrigger;
import com.batch.android.localcampaigns.trigger.NextSessionTrigger;
-import com.batch.android.localcampaigns.trigger.NowTrigger;
import org.junit.Assert;
import org.junit.Test;
@@ -17,9 +14,6 @@ public void testSatisfiesTrigger() {
Assert.assertTrue(signal.satisfiesTrigger(new NextSessionTrigger()));
- Assert.assertFalse(signal.satisfiesTrigger(new CampaignsRefreshedTrigger()));
- Assert.assertFalse(signal.satisfiesTrigger(new CampaignsLoadedTrigger()));
- Assert.assertFalse(signal.satisfiesTrigger(new NowTrigger()));
Assert.assertFalse(signal.satisfiesTrigger(new EventLocalCampaignTrigger("eventname", null)));
Assert.assertFalse(
signal.satisfiesTrigger(
diff --git a/Sources/sdk/src/test/java/com/batch/android/localcampaigns/signal/PublicEventTrackedSignalTest.java b/Sources/sdk/src/test/java/com/batch/android/localcampaigns/signal/PublicEventTrackedSignalTest.java
index 4cc1a07..2448ceb 100644
--- a/Sources/sdk/src/test/java/com/batch/android/localcampaigns/signal/PublicEventTrackedSignalTest.java
+++ b/Sources/sdk/src/test/java/com/batch/android/localcampaigns/signal/PublicEventTrackedSignalTest.java
@@ -3,11 +3,8 @@
import com.batch.android.json.JSONException;
import com.batch.android.json.JSONObject;
import com.batch.android.localcampaigns.model.LocalCampaign;
-import com.batch.android.localcampaigns.trigger.CampaignsLoadedTrigger;
-import com.batch.android.localcampaigns.trigger.CampaignsRefreshedTrigger;
import com.batch.android.localcampaigns.trigger.EventLocalCampaignTrigger;
import com.batch.android.localcampaigns.trigger.NextSessionTrigger;
-import com.batch.android.localcampaigns.trigger.NowTrigger;
import com.batch.android.module.UserModule;
import org.junit.Assert;
import org.junit.Test;
@@ -39,9 +36,6 @@ public void testSatisfiesTrigger() throws JSONException {
signal.satisfiesTrigger(new EventLocalCampaignTrigger(eventName, "toto"))
);
- Assert.assertFalse(signal.satisfiesTrigger(new NowTrigger()));
- Assert.assertFalse(signal.satisfiesTrigger(new CampaignsRefreshedTrigger()));
- Assert.assertFalse(signal.satisfiesTrigger(new CampaignsLoadedTrigger()));
Assert.assertFalse(signal.satisfiesTrigger(new NextSessionTrigger()));
Assert.assertFalse(
signal.satisfiesTrigger(
diff --git a/Sources/sdk/src/test/java/com/batch/android/metrics/CounterMatcher.java b/Sources/sdk/src/test/java/com/batch/android/metrics/CounterMatcher.java
new file mode 100644
index 0000000..677d170
--- /dev/null
+++ b/Sources/sdk/src/test/java/com/batch/android/metrics/CounterMatcher.java
@@ -0,0 +1,22 @@
+package com.batch.android.metrics;
+
+import com.batch.android.metrics.model.Counter;
+import org.mockito.ArgumentMatcher;
+
+public class CounterMatcher implements ArgumentMatcher {
+
+ private final Counter expected;
+
+ public CounterMatcher(Counter counter) {
+ this.expected = counter;
+ }
+
+ @Override
+ public boolean matches(Counter argument) {
+ return (
+ expected.getName().equals(argument.getName()) &&
+ expected.getType().equals(argument.getType()) &&
+ expected.getValues().equals(argument.getValues())
+ );
+ }
+}
diff --git a/Sources/sdk/src/test/java/com/batch/android/metrics/MetricManagerTest.java b/Sources/sdk/src/test/java/com/batch/android/metrics/MetricManagerTest.java
new file mode 100644
index 0000000..98d5b6b
--- /dev/null
+++ b/Sources/sdk/src/test/java/com/batch/android/metrics/MetricManagerTest.java
@@ -0,0 +1,78 @@
+package com.batch.android.metrics;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.MediumTest;
+import com.batch.android.di.DITest;
+import com.batch.android.di.DITestUtils;
+import com.batch.android.metrics.model.Counter;
+import com.batch.android.metrics.model.Metric;
+import com.batch.android.metrics.model.Observation;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.powermock.api.mockito.PowerMockito;
+import org.powermock.reflect.Whitebox;
+
+@RunWith(AndroidJUnit4.class)
+@MediumTest
+public class MetricManagerTest extends DITest {
+
+ @Test
+ public void testAddMetric() {
+ MetricManager manager = PowerMockito.spy(DITestUtils.mockSingletonDependency(MetricManager.class, null));
+ Counter counter = new Counter("counter_test_metric");
+ manager.addMetric(counter);
+ Mockito.verify(manager, Mockito.times(1)).addMetric(Mockito.argThat(new CounterMatcher(counter)));
+ }
+
+ @Test
+ public void testGetMetricsToSend() throws Exception {
+ MetricManager manager = PowerMockito.spy(DITestUtils.mockSingletonDependency(MetricManager.class, null));
+ PowerMockito.doNothing().when(manager).sendMetrics();
+ Counter counter = new Counter("counter_test_metric").register();
+ counter.inc();
+
+ Observation observation = new Observation("observation_test_metric").labelNames("label1", "label2").register();
+ observation.labels("value1", "value2").startTimer();
+ observation.labels("value1", "value2").observeDuration();
+ observation.labels("value3", "value3").startTimer();
+
+ //Making copy of metrics because the reset method will be called when getMetricsToSend is done
+ List> expected = new ArrayList<>();
+ expected.add(new Counter(counter));
+ expected.add(new Observation(observation.labels("value1", "value2")));
+
+ List> actual = Whitebox.invokeMethod(manager, "getMetricsToSend");
+
+ Assert.assertEquals(expected.size(), actual.size());
+
+ for (int i = 0; i < expected.size(); i++) {
+ Metric> expectedMetric = expected.get(i);
+ Metric> actualMetric = actual.get(i);
+ Assert.assertEquals(expectedMetric.getName(), actualMetric.getName());
+ Assert.assertEquals(expectedMetric.getType(), actualMetric.getType());
+ Assert.assertEquals(expectedMetric.getLabelNames(), actualMetric.getLabelNames());
+ Assert.assertEquals(expectedMetric.getLabelValues(), actualMetric.getLabelValues());
+ Assert.assertEquals(expectedMetric.getValues(), actualMetric.getValues());
+ }
+ }
+
+ @Test
+ public void testIsWaiting() throws NoSuchFieldException, IllegalAccessException {
+ MetricManager manager = DITestUtils.mockSingletonDependency(MetricManager.class, null);
+
+ Field isWaitingField = MetricManager.class.getDeclaredField("isSending");
+ isWaitingField.setAccessible(true);
+ AtomicBoolean isSending = (AtomicBoolean) isWaitingField.get(manager);
+
+ Counter counter = new Counter("counter_test_metric").register();
+ Assert.assertFalse(isSending.get());
+ counter.inc();
+ Assert.assertTrue(isSending.get());
+ }
+}
diff --git a/Sources/sdk/src/test/java/com/batch/android/post/DisplayReceiptPostDataProviderTest.java b/Sources/sdk/src/test/java/com/batch/android/post/DisplayReceiptPostDataProviderTest.java
index d0aa223..374c743 100644
--- a/Sources/sdk/src/test/java/com/batch/android/post/DisplayReceiptPostDataProviderTest.java
+++ b/Sources/sdk/src/test/java/com/batch/android/post/DisplayReceiptPostDataProviderTest.java
@@ -2,6 +2,8 @@
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
import com.batch.android.displayreceipt.DisplayReceipt;
import java.util.ArrayList;
@@ -58,4 +60,15 @@ public void testData() {
assertArrayEquals(receiptList.toArray(), provider.getRawData().toArray());
assertArrayEquals(body, provider.getData());
}
+
+ @Test
+ public void testIsEmpty() {
+ List receiptList = new ArrayList<>();
+ DisplayReceiptPostDataProvider provider = new DisplayReceiptPostDataProvider(receiptList);
+ assertTrue(provider.isEmpty());
+
+ receiptList.add(null);
+ provider = new DisplayReceiptPostDataProvider(receiptList);
+ assertFalse(provider.isEmpty());
+ }
}
diff --git a/Sources/sdk/src/test/java/com/batch/android/post/JSONPostDataProviderTest.java b/Sources/sdk/src/test/java/com/batch/android/post/JSONPostDataProviderTest.java
index 2cca9c3..b7fc810 100644
--- a/Sources/sdk/src/test/java/com/batch/android/post/JSONPostDataProviderTest.java
+++ b/Sources/sdk/src/test/java/com/batch/android/post/JSONPostDataProviderTest.java
@@ -1,8 +1,10 @@
package com.batch.android.post;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
+import com.batch.android.json.JSONException;
import com.batch.android.json.JSONObject;
import org.junit.Test;
@@ -34,6 +36,17 @@ public void testReadData() throws Exception {
assertTrue("decoded data is not equals to input", areEquals(input, new JSONObject(new String(data))));
}
+ @Test
+ public void testIsEmpty() throws JSONException {
+ JSONObject input = new JSONObject();
+ JSONPostDataProvider provider = new JSONPostDataProvider(input);
+ assertTrue(provider.isEmpty());
+
+ input.put("key", "value");
+ provider = new JSONPostDataProvider(input);
+ assertFalse(provider.isEmpty());
+ }
+
/**
* Are those 2 json object equals
*
diff --git a/Sources/sdk/src/test/java/com/batch/android/post/LocalCampaignsJITPostDataProviderTest.java b/Sources/sdk/src/test/java/com/batch/android/post/LocalCampaignsJITPostDataProviderTest.java
new file mode 100644
index 0000000..57df34b
--- /dev/null
+++ b/Sources/sdk/src/test/java/com/batch/android/post/LocalCampaignsJITPostDataProviderTest.java
@@ -0,0 +1,89 @@
+package com.batch.android.post;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import com.batch.android.core.ByteArrayHelper;
+import com.batch.android.di.providers.CampaignManagerProvider;
+import com.batch.android.di.providers.SQLUserDatasourceProvider;
+import com.batch.android.localcampaigns.LocalCampaignsTracker;
+import com.batch.android.localcampaigns.model.LocalCampaign;
+import com.batch.android.metrics.model.Counter;
+import com.batch.android.metrics.model.Metric;
+import com.batch.android.metrics.model.Observation;
+import com.batch.android.user.SQLUserDatasource;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for MetricPostDataProvider
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class LocalCampaignsJITPostDataProviderTest {
+
+ @Test
+ public void testPack() {
+ Context context = ApplicationProvider.getApplicationContext();
+
+ LocalCampaignsTracker tracker = (LocalCampaignsTracker) CampaignManagerProvider.get().getViewTracker();
+ tracker.open(context);
+
+ SQLUserDatasource datasource = SQLUserDatasourceProvider.get(context);
+
+ List campaigns = new ArrayList<>();
+ LocalCampaign campaign1 = new LocalCampaign();
+ campaign1.id = "c1";
+ LocalCampaign campaign2 = new LocalCampaign();
+ campaign2.id = "c2";
+ LocalCampaign campaign3 = new LocalCampaign();
+ campaign3.id = "c3";
+ campaigns.add(campaign1);
+ campaigns.add(campaign2);
+ campaigns.add(campaign3);
+
+ byte[] expectedData = ByteArrayHelper.hexToBytes(
+ "84A963616D706169676E7393A26331A26332A26333A369647380AA6174747269627574657380A5766965777383A2633381A5636F756E7400A2633181A5636F756E7400A2633281A5636F756E7400"
+ );
+
+ LocalCampaignsJITPostDataProvider provider = new LocalCampaignsJITPostDataProvider(campaigns);
+
+ assertEquals("application/msgpack", provider.getContentType());
+ assertArrayEquals(campaigns.toArray(), provider.getRawData().toArray());
+ assertArrayEquals(expectedData, provider.getData());
+
+ tracker.close();
+ datasource.close();
+ }
+
+ @Test
+ public void testUnpack() {
+ String fakeHexResponse = "81b1656c696769626c6543616d706169676e7392a26332a26333";
+ byte[] fakeResponse = ByteArrayHelper.hexToBytes(fakeHexResponse);
+ LocalCampaignsJITPostDataProvider provider = new LocalCampaignsJITPostDataProvider(new ArrayList<>());
+ List eligibleCampaigns = provider.unpack(fakeResponse);
+ Assert.assertEquals("c2", eligibleCampaigns.get(0));
+ Assert.assertEquals("c3", eligibleCampaigns.get(1));
+ }
+
+ @Test
+ public void testIsEmpty() {
+ List campaigns = new ArrayList<>();
+ LocalCampaignsJITPostDataProvider provider = new LocalCampaignsJITPostDataProvider(campaigns);
+ assertTrue(provider.isEmpty());
+
+ LocalCampaign campaign = new LocalCampaign();
+ campaigns.add(campaign);
+ provider = new LocalCampaignsJITPostDataProvider(campaigns);
+ assertFalse(provider.isEmpty());
+ }
+}
diff --git a/Sources/sdk/src/test/java/com/batch/android/post/MetricPostDataProviderTest.java b/Sources/sdk/src/test/java/com/batch/android/post/MetricPostDataProviderTest.java
new file mode 100644
index 0000000..7daad3f
--- /dev/null
+++ b/Sources/sdk/src/test/java/com/batch/android/post/MetricPostDataProviderTest.java
@@ -0,0 +1,60 @@
+package com.batch.android.post;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+import com.batch.android.metrics.model.Counter;
+import com.batch.android.metrics.model.Metric;
+import com.batch.android.metrics.model.Observation;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for MetricPostDataProvider
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class MetricPostDataProviderTest {
+
+ @Test
+ public void testData() {
+ List> metrics = new ArrayList<>();
+ Counter counter = new Counter("counter_test_metric");
+ Observation observation = new Observation("observation_test_metric");
+ counter.inc();
+ metrics.add(counter);
+ metrics.add(observation);
+
+ // prettier-ignore
+ byte[] body = new byte[]{-110, -125, -90, 118, 97, 108, 117, 101, 115, -111, -54, 63, -128,
+ 0, 0, -92, 110, 97, 109, 101, -77, 99, 111, 117, 110, 116, 101, 114, 95, 116, 101,
+ 115, 116, 95, 109, 101, 116, 114, 105, 99, -92, 116, 121, 112, 101, -89, 99, 111,
+ 117, 110, 116, 101, 114, -125, -90, 118, 97, 108, 117, 101, 115, -112, -92, 110, 97,
+ 109, 101, -73, 111, 98, 115, 101, 114, 118, 97, 116, 105, 111, 110, 95, 116, 101,
+ 115, 116, 95, 109, 101, 116, 114, 105, 99, -92, 116, 121, 112, 101, -85, 111, 98,
+ 115, 101, 114, 118, 97, 116, 105, 111, 110};
+
+ MetricPostDataProvider provider = new MetricPostDataProvider(metrics);
+ assertEquals("application/msgpack", provider.getContentType());
+ assertArrayEquals(metrics.toArray(), provider.getRawData().toArray());
+ assertArrayEquals(body, provider.getData());
+ }
+
+ @Test
+ public void testIsEmpty() {
+ List> metrics = new ArrayList<>();
+ MetricPostDataProvider provider = new MetricPostDataProvider(metrics);
+ assertTrue(provider.isEmpty());
+
+ Counter counter = new Counter("test_metric");
+ metrics.add(counter);
+ provider = new MetricPostDataProvider(metrics);
+ assertFalse(provider.isEmpty());
+ }
+}
diff --git a/Sources/sdk/src/test/java/com/batch/android/post/ParametersPostDataProviderTest.java b/Sources/sdk/src/test/java/com/batch/android/post/ParametersPostDataProviderTest.java
index a43b2b7..b21de1d 100644
--- a/Sources/sdk/src/test/java/com/batch/android/post/ParametersPostDataProviderTest.java
+++ b/Sources/sdk/src/test/java/com/batch/android/post/ParametersPostDataProviderTest.java
@@ -1,6 +1,9 @@
package com.batch.android.post;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
import java.util.HashMap;
import java.util.Map;
@@ -31,4 +34,15 @@ public void testReadData() throws Exception {
byte[] data = provider.getData();
assertEquals("decoded data is not equals to input", key + "=" + value, new String(data));
}
+
+ @Test
+ public void testIsEmpty() {
+ Map input = new HashMap<>();
+ ParametersPostDataProvider provider = new ParametersPostDataProvider(input);
+ assertTrue(provider.isEmpty());
+
+ input.put("key", "value");
+ provider = new ParametersPostDataProvider(input);
+ assertFalse(provider.isEmpty());
+ }
}
diff --git a/Sources/sdk/src/test/resources/fake_geo_campaigns.json b/Sources/sdk/src/test/resources/fake_geo_campaigns.json
index defeecd..bc7e322 100644
--- a/Sources/sdk/src/test/resources/fake_geo_campaigns.json
+++ b/Sources/sdk/src/test/resources/fake_geo_campaigns.json
@@ -2,6 +2,14 @@
"queries": [
{
"id": "c2b63fc3-bb8c-4acd-9441-aa8202211884",
+ "cappings": {
+ "session": 2,
+ "time": [
+ {"views": 1, "duration": 3600},
+ {"views": 0, "duration": 3600},
+ {"views": 1, "duration": 0}
+ ]
+ },
"campaigns": [
{
"campaignId": "next_session_triggered_campaign",
@@ -15,6 +23,7 @@
"maximumApiLevel": 30,
"priority": 2,
"minDisplayInterval": 3,
+ "requireJIT": true,
"startDate": {
"ts": 1499960145,
"userTZ": false
@@ -57,6 +66,27 @@
"style": "#image-cnt {blur: 200;} #image {border-radius: 10; margin-left: 30; margin-right: 30; margin-top: 40;} #placeholder{background-color:#018BAA;}#content {\n background-color: #018BFF;\n height: 100%\n padding-top: 24;\n padding-left: 20;\n padding-right: 20;\n padding-bottom: 20;\n}\n#h1 {\n color: #018BFF;\n padding-left: 15;\n padding-right: 15;\n padding-top: 4;\n padding-bottom: 4;\n border-radius: 12;\n background-color:white;\n font-weight: bold;\n font-size: 12;\n height: 24;\n width: auto;\n}\n#h2 {\n margin-top: 24;\n color: white;\n font-weight: bold;\n font-size: 35;\n}\n#body {\n color: #80C5FF;\n}\n#cta1 {\n color: #018BFF;\n padding-left: 60;\n padding-right: 60;\n padding-top: 10;\n padding-bottom: 10;\n border-radius: 4;\n background-color:white;\n font-weight: bold;\n font-size: 18;\n}\n#close {\n glyph-width: 1.5;\n glyph-padding: 11;\n background-color: #212C3C;\n margin-top: 30;\n margin-right: 30;\n}"
}
}
+ },
+ {
+ "campaignId": "action_output_campaign",
+ "triggers": [
+ {
+ "type": "NEXT_SESSION"
+ }
+ ],
+ "eventData": {
+ "type": "l",
+ "foo": "bar"
+ },
+ "output": {
+ "type": "ACTION",
+ "payload": {
+ "action": "batch.deeplink",
+ "args": {
+ "l": "https://batch.com"
+ }
+ }
+ }
}
]
}
diff --git a/proguard-mappings/1.19.0/checksum.md5 b/proguard-mappings/1.19.0/checksum.md5
new file mode 100644
index 0000000..fe1110f
--- /dev/null
+++ b/proguard-mappings/1.19.0/checksum.md5
@@ -0,0 +1 @@
+MD5 (public-sdk/Batch.aar) = 8f5b46273e791d26ce6aa461075cd946
diff --git a/proguard-mappings/1.19.0/checksum.sha b/proguard-mappings/1.19.0/checksum.sha
new file mode 100644
index 0000000..acb9bad
--- /dev/null
+++ b/proguard-mappings/1.19.0/checksum.sha
@@ -0,0 +1 @@
+13eb19c2148ff3c3c4a5206cb621318f27a4f0a2 public-sdk/Batch.aar
diff --git a/proguard-mappings/1.19.0/mapping.txt b/proguard-mappings/1.19.0/mapping.txt
new file mode 100644
index 0000000..34f09ea
--- /dev/null
+++ b/proguard-mappings/1.19.0/mapping.txt
@@ -0,0 +1,9151 @@
+# compiler: R8
+# compiler_version: 3.1.66
+# pg_map_id: f3315f2
+# common_typos_disable
+# {"id":"com.android.tools.r8.mapping","version":"1.0"}
+com.batch.android.AdsIdentifierProviderAvailabilityException -> com.batch.android.AdsIdentifierProviderAvailabilityException:
+ 1:1:void (java.lang.String):9:9 ->
+com.batch.android.AdvertisingID -> com.batch.android.a:
+ java.lang.String advertisingID -> a
+ boolean limited -> b
+ boolean advertisingIdReady -> c
+ java.lang.String UNAVAILABLE_AD_ID -> e
+ java.lang.String TAG -> d
+ 1:1:void ():48:48 ->
+ 2:8:void ():44:50 ->
+ 1:1:java.lang.String access$002(com.batch.android.AdvertisingID,java.lang.String):16:16 -> a
+ 2:2:boolean access$102(com.batch.android.AdvertisingID,boolean):16:16 -> a
+ 3:7:java.lang.String get():105:109 -> a
+ 8:8:java.lang.String get():106:106 -> a
+ 1:1:boolean access$202(com.batch.android.AdvertisingID,boolean):16:16 -> b
+ 2:11:void initAdvertisingID():57:66 -> b
+ 12:12:void initAdvertisingID():62:62 -> b
+ 1:5:boolean isLimited():119:123 -> c
+ 6:6:boolean isLimited():120:120 -> c
+ 1:1:boolean isNotNull():132:132 -> d
+ 1:1:boolean isReady():94:94 -> e
+com.batch.android.AdvertisingID$1 -> com.batch.android.a$a:
+ com.batch.android.AdvertisingID this$0 -> a
+ 1:1:void (com.batch.android.AdvertisingID):67:67 ->
+ 1:2:void onError(java.lang.Exception):78:79 -> onError
+ 1:4:void onSuccess(java.lang.String,boolean):70:73 -> onSuccess
+com.batch.android.AttributesCheckWebservice -> com.batch.android.b:
+ java.lang.String TAG -> v
+ com.batch.android.webservice.listener.AttributesCheckWebserviceListener listener -> u
+ long version -> s
+ java.lang.String transactionID -> t
+ 1:16:void (android.content.Context,long,java.lang.String,com.batch.android.webservice.listener.AttributesCheckWebserviceListener):51:66 ->
+ 17:17:void (android.content.Context,long,java.lang.String,com.batch.android.webservice.listener.AttributesCheckWebserviceListener):61:61 ->
+ 18:18:void (android.content.Context,long,java.lang.String,com.batch.android.webservice.listener.AttributesCheckWebserviceListener):57:57 ->
+ 19:19:void (android.content.Context,long,java.lang.String,com.batch.android.webservice.listener.AttributesCheckWebserviceListener):53:53 ->
+ 1:1:java.lang.String getSpecificConnectTimeoutKey():180:180 -> A
+ 1:1:java.lang.String getSpecificReadTimeoutKey():185:185 -> B
+ 1:1:java.lang.String getSpecificRetryCountKey():190:190 -> C
+ 1:1:java.lang.String getURLSorterPatternParameterKey():155:155 -> F
+ 1:1:java.lang.String getPropertyParameterKey():150:150 -> H
+ 1:3:java.util.List getQueries():73:75 -> I
+ 1:1:java.lang.String getTaskIdentifier():143:143 -> a
+ 1:1:java.lang.String getCryptorModeParameterKey():165:165 -> o
+ 1:1:java.lang.String getCryptorTypeParameterKey():160:160 -> p
+ 1:52:void run():83:134 -> run
+ 53:53:void run():128:128 -> run
+ 54:68:void run():94:108 -> run
+ 69:69:void run():105:105 -> run
+ 70:70:void run():102:102 -> run
+ 71:109:void run():99:137 -> run
+ 1:1:java.lang.String getPostCryptorTypeParameterKey():170:170 -> v
+ 1:1:java.lang.String getReadCryptorTypeParameterKey():175:175 -> y
+com.batch.android.AttributesCheckWebservice$1 -> com.batch.android.b$a:
+ int[] $SwitchMap$com$batch$android$core$Webservice$WebserviceError$Reason -> a
+ 1:1:void ():97:97 ->
+com.batch.android.AttributesSendWebservice -> com.batch.android.c:
+ java.lang.String TAG -> w
+ java.util.Map attributes -> t
+ com.batch.android.webservice.listener.AttributesSendWebserviceListener listener -> v
+ long version -> s
+ java.util.Map tags -> u
+ 1:21:void (android.content.Context,long,java.util.Map,java.util.Map,com.batch.android.webservice.listener.AttributesSendWebserviceListener):58:78 ->
+ 22:22:void (android.content.Context,long,java.util.Map,java.util.Map,com.batch.android.webservice.listener.AttributesSendWebserviceListener):72:72 ->
+ 23:23:void (android.content.Context,long,java.util.Map,java.util.Map,com.batch.android.webservice.listener.AttributesSendWebserviceListener):68:68 ->
+ 24:24:void (android.content.Context,long,java.util.Map,java.util.Map,com.batch.android.webservice.listener.AttributesSendWebserviceListener):64:64 ->
+ 25:25:void (android.content.Context,long,java.util.Map,java.util.Map,com.batch.android.webservice.listener.AttributesSendWebserviceListener):60:60 ->
+ 1:1:java.lang.String getSpecificConnectTimeoutKey():189:189 -> A
+ 1:1:java.lang.String getSpecificReadTimeoutKey():194:194 -> B
+ 1:1:java.lang.String getSpecificRetryCountKey():199:199 -> C
+ 1:1:java.lang.String getURLSorterPatternParameterKey():164:164 -> F
+ 1:1:java.lang.String getPropertyParameterKey():159:159 -> H
+ 1:3:java.util.List getQueries():85:87 -> I
+ 1:1:java.lang.String getTaskIdentifier():152:152 -> a
+ 1:1:java.lang.String getCryptorModeParameterKey():174:174 -> o
+ 1:1:java.lang.String getCryptorTypeParameterKey():169:169 -> p
+ 1:49:void run():95:143 -> run
+ 50:50:void run():137:137 -> run
+ 51:65:void run():106:120 -> run
+ 66:66:void run():117:117 -> run
+ 67:67:void run():114:114 -> run
+ 68:103:void run():111:146 -> run
+ 1:1:java.lang.String getPostCryptorTypeParameterKey():179:179 -> v
+ 1:1:java.lang.String getReadCryptorTypeParameterKey():184:184 -> y
+com.batch.android.AttributesSendWebservice$1 -> com.batch.android.c$a:
+ int[] $SwitchMap$com$batch$android$core$Webservice$WebserviceError$Reason -> a
+ 1:1:void ():109:109 ->
+com.batch.android.Batch -> com.batch.android.Batch:
+ android.content.Intent newIntent -> f
+ com.batch.android.module.BatchModule moduleMaster -> k
+ java.lang.String sessionID -> h
+ boolean didLogOptOutWarning -> i
+ com.batch.android.AdvertisingID advertisingID -> b
+ android.content.BroadcastReceiver receiver -> e
+ com.batch.android.User user -> d
+ java.lang.Boolean lastNotificationAuthorizationStatus -> j
+ com.batch.android.Install install -> c
+ com.batch.android.core.ExcludedActivityHelper excludedActivityHelper -> g
+ com.batch.android.Config config -> a
+ 1:71:void ():110:180 ->
+ 1:1:void ():185:185 ->
+ void manageUpdate(java.lang.String,java.lang.String) -> a
+ 1:1:com.batch.android.Install access$000():78:78 -> a
+ 2:3:void lambda$getAPIKey$0(java.lang.StringBuilder,com.batch.android.runtime.State):198:199 -> a
+ 4:11:com.batch.android.runtime.State lambda$setConfig$1(com.batch.android.Config,com.batch.android.runtime.State):250:257 -> a
+ 12:13:void lambda$getLoggerLevel$5(java.util.concurrent.atomic.AtomicReference,com.batch.android.runtime.State):341:342 -> a
+ 14:22:void _optOut(android.content.Context,boolean,com.batch.android.BatchOptOutResultListener):510:518 -> a
+ 23:23:void _optOut(android.content.Context,boolean,com.batch.android.BatchOptOutResultListener):506:506 -> a
+ 24:27:void lambda$_optOut$7(android.content.Context,java.lang.Void):513:516 -> a
+ 28:28:void lambda$_optOut$8(com.batch.android.BatchOptOutResultListener,java.lang.Exception):520:520 -> a
+ 29:400:void doBatchStart(android.content.Context,boolean,boolean):2018:2389 -> a
+ 401:680:com.batch.android.runtime.State lambda$doBatchStart$9(com.batch.android.runtime.RuntimeManager,boolean,android.content.Context,boolean,java.util.concurrent.atomic.AtomicBoolean,java.lang.StringBuilder,com.batch.android.runtime.State):2025:2304 -> a
+ 681:711:com.batch.android.runtime.State lambda$doBatchStart$9(com.batch.android.runtime.RuntimeManager,boolean,android.content.Context,boolean,java.util.concurrent.atomic.AtomicBoolean,java.lang.StringBuilder,com.batch.android.runtime.State):2302:2332 -> a
+ 712:729:com.batch.android.runtime.State lambda$doBatchStart$9(com.batch.android.runtime.RuntimeManager,boolean,android.content.Context,boolean,java.util.concurrent.atomic.AtomicBoolean,java.lang.StringBuilder,com.batch.android.runtime.State):2331:2348 -> a
+ 730:731:void lambda$doBatchStart$10(com.batch.android.runtime.RuntimeManager,java.util.concurrent.atomic.AtomicBoolean,java.lang.StringBuilder,boolean,com.batch.android.runtime.State):2368:2369 -> a
+ 732:732:void lambda$doBatchStart$10(com.batch.android.runtime.RuntimeManager,java.util.concurrent.atomic.AtomicBoolean,java.lang.StringBuilder,boolean,com.batch.android.runtime.State):2366:2366 -> a
+ 733:797:com.batch.android.runtime.State lambda$onStop$11(boolean,android.content.Context,boolean,com.batch.android.runtime.State):2408:2472 -> a
+ 798:798:com.batch.android.runtime.State lambda$onStop$11(boolean,android.content.Context,boolean,com.batch.android.runtime.State):2460:2460 -> a
+ 799:799:void lambda$onWebserviceExecutorWorkFinished$12(java.util.concurrent.atomic.AtomicBoolean,com.batch.android.runtime.State):2495:2495 -> a
+ 800:811:com.batch.android.runtime.State lambda$doStop$13(com.batch.android.runtime.State):2517:2528 -> a
+ 812:812:void checkForNotificationAuthorizationChange(android.content.Context):2622:2622 -> a
+ 813:828:void checkForNotificationAuthorizationChange(android.content.Context):2620:2635 -> a
+ 829:847:void checkForNotificationAuthorizationChange(android.content.Context):2634:2652 -> a
+ 848:855:void lambda$checkForNotificationAuthorizationChange$14(android.content.Context,com.batch.android.runtime.RuntimeManager):2641:2648 -> a
+ 1:1:void access$100():78:78 -> b
+ 2:3:void lambda$shouldUseAdvancedDeviceInformation$3(java.util.concurrent.atomic.AtomicBoolean,com.batch.android.runtime.State):291:292 -> b
+ 4:5:void lambda$getSessionID$6(java.lang.StringBuilder,com.batch.android.runtime.State):360:361 -> b
+ 6:84:void onStop(android.content.Context,boolean,boolean):2404:2482 -> b
+ 1:1:void access$200():78:78 -> c
+ 2:3:void lambda$shouldUseAdvertisingID$2(java.util.concurrent.atomic.AtomicBoolean,com.batch.android.runtime.State):273:274 -> c
+ 1:1:void copyBatchExtras(android.content.Intent,android.content.Intent):398:398 -> copyBatchExtras
+ 2:2:void copyBatchExtras(android.os.Bundle,android.os.Bundle):411:411 -> copyBatchExtras
+ 1:2:void lambda$shouldUseGoogleInstanceID$4(java.util.concurrent.atomic.AtomicBoolean,com.batch.android.runtime.State):311:312 -> d
+ 3:6:void clearCachedInstallData():2551:2554 -> d
+ 1:22:void doStop():2513:2534 -> e
+ 1:1:com.batch.android.AdvertisingID getAdvertisingID():2565:2565 -> f
+ 1:1:com.batch.android.Install getInstall():2574:2574 -> g
+ 1:11:java.lang.String getAPIKey():194:204 -> getAPIKey
+ 1:1:java.lang.String getBroadcastPermissionName(android.content.Context):421:421 -> getBroadcastPermissionName
+ 1:10:com.batch.android.LoggerLevel getLoggerLevel():337:346 -> getLoggerLevel
+ 1:12:java.lang.String getSessionID():355:366 -> getSessionID
+ 1:9:com.batch.android.BatchUserProfile getUserProfile():225:233 -> getUserProfile
+ 1:1:com.batch.android.User getUser():2583:2583 -> h
+ 1:10:void onWebserviceExecutorWorkFinished():2492:2501 -> i
+ 1:1:boolean isOptedOut(android.content.Context):553:553 -> isOptedOut
+ 2:2:boolean isOptedOut(android.content.Context):551:551 -> isOptedOut
+ 1:3:boolean isRunningInDevMode():381:383 -> isRunningInDevMode
+ 1:23:void updateVersionManagement():2593:2615 -> j
+ 1:3:void onCreate(android.app.Activity):1929:1931 -> onCreate
+ 1:1:void onDestroy(android.app.Activity):2011:2011 -> onDestroy
+ 1:2:void onNewIntent(android.app.Activity,android.content.Intent):1990:1991 -> onNewIntent
+ 1:1:void onServiceCreate(android.content.Context,boolean):1969:1969 -> onServiceCreate
+ 1:1:void onServiceDestroy(android.content.Context):1980:1980 -> onServiceDestroy
+ 1:1:void onStart(android.app.Activity):1948:1948 -> onStart
+ 1:1:void onStop(android.app.Activity):2001:2001 -> onStop
+ 1:1:void optIn(android.content.Context):540:540 -> optIn
+ 2:2:void optIn(android.content.Context):538:538 -> optIn
+ 1:1:void optOut(android.content.Context):447:447 -> optOut
+ 2:2:void optOut(android.content.Context,com.batch.android.BatchOptOutResultListener):463:463 -> optOut
+ 1:1:void optOutAndWipeData(android.content.Context):477:477 -> optOutAndWipeData
+ 2:2:void optOutAndWipeData(android.content.Context,com.batch.android.BatchOptOutResultListener):496:496 -> optOutAndWipeData
+ 1:2:void setConfig(com.batch.android.Config):248:249 -> setConfig
+ 1:1:void setFindMyInstallationEnabled(boolean):565:565 -> setFindMyInstallationEnabled
+ 1:10:boolean shouldUseAdvancedDeviceInformation():287:296 -> shouldUseAdvancedDeviceInformation
+ 1:10:boolean shouldUseAdvertisingID():269:278 -> shouldUseAdvertisingID
+ 1:10:boolean shouldUseGoogleInstanceID():307:316 -> shouldUseGoogleInstanceID
+com.batch.android.Batch$1 -> com.batch.android.Batch$a:
+com.batch.android.Batch$Actions -> com.batch.android.Batch$Actions:
+ 1:1:void ():1848:1848 ->
+ 1:1:void addDrawableAlias(java.lang.String,int):1888:1888 -> addDrawableAlias
+ 1:1:boolean performAction(android.content.Context,java.lang.String,com.batch.android.json.JSONObject):1905:1905 -> performAction
+ 1:1:void register(com.batch.android.UserAction):1860:1860 -> register
+ 1:1:void setDeeplinkInterceptor(com.batch.android.BatchDeeplinkInterceptor):1914:1914 -> setDeeplinkInterceptor
+ 1:1:void unregister(java.lang.String):1872:1872 -> unregister
+com.batch.android.Batch$Debug -> com.batch.android.Batch$Debug:
+ 1:1:void ():576:576 ->
+ 1:2:void startDebugActivity(android.content.Context):587:588 -> startDebugActivity
+com.batch.android.Batch$EventDispatcher -> com.batch.android.Batch$EventDispatcher:
+ 1:1:void ():1152:1152 ->
+ 1:1:void addDispatcher(com.batch.android.BatchEventDispatcher):1161:1161 -> addDispatcher
+ 1:1:boolean removeDispatcher(com.batch.android.BatchEventDispatcher):1170:1170 -> removeDispatcher
+com.batch.android.Batch$EventDispatcher$Type -> com.batch.android.Batch$EventDispatcher$Type:
+ com.batch.android.Batch$EventDispatcher$Type[] $VALUES -> a
+ 1:9:void ():1179:1187 ->
+ 10:10:void ():1177:1177 ->
+ 1:1:void (java.lang.String,int):1178:1178 ->
+ 1:1:boolean isMessagingEvent():1194:1194 -> isMessagingEvent
+ 1:1:boolean isNotificationEvent():1190:1190 -> isNotificationEvent
+ 1:1:com.batch.android.Batch$EventDispatcher$Type valueOf(java.lang.String):1177:1177 -> valueOf
+ 1:1:com.batch.android.Batch$EventDispatcher$Type[] values():1177:1177 -> values
+com.batch.android.Batch$Inbox -> com.batch.android.Batch$Inbox:
+ 1:1:void ():600:600 ->
+ 1:2:com.batch.android.BatchInboxFetcher getFetcher(android.content.Context):614:615 -> getFetcher
+ 3:3:com.batch.android.BatchInboxFetcher getFetcher(android.content.Context):612:612 -> getFetcher
+ 4:4:com.batch.android.BatchInboxFetcher getFetcher(android.content.Context,java.lang.String,java.lang.String):636:636 -> getFetcher
+ 5:5:com.batch.android.BatchInboxFetcher getFetcher(android.content.Context,java.lang.String,java.lang.String):634:634 -> getFetcher
+ 6:6:com.batch.android.BatchInboxFetcher getFetcher(java.lang.String,java.lang.String):651:651 -> getFetcher
+com.batch.android.Batch$InternalBroadcastReceiver -> com.batch.android.Batch$b:
+ 1:1:void ():2665:2665 ->
+ 2:2:void (com.batch.android.Batch$1):2665:2665 ->
+ 1:12:void onReceive(android.content.Context,android.content.Intent):2673:2684 -> onReceive
+ 13:13:void onReceive(android.content.Context,android.content.Intent):2681:2681 -> onReceive
+com.batch.android.Batch$Messaging -> com.batch.android.Batch$Messaging:
+ 1:1:void ():1522:1522 ->
+ 1:1:boolean hasPendingMessage():1824:1824 -> hasPendingMessage
+ 1:1:boolean isDoNotDisturbEnabled():1815:1815 -> isDoNotDisturbEnabled
+ 1:1:com.batch.android.BatchBannerView loadBanner(android.content.Context,com.batch.android.BatchMessage):1765:1765 -> loadBanner
+ 1:1:androidx.fragment.app.DialogFragment loadFragment(android.content.Context,com.batch.android.BatchMessage):1746:1746 -> loadFragment
+ 1:1:com.batch.android.BatchMessage popPendingMessage():1837:1837 -> popPendingMessage
+ 1:1:void setAutomaticMode(boolean):1700:1700 -> setAutomaticMode
+ 1:1:void setDoNotDisturbEnabled(boolean):1808:1808 -> setDoNotDisturbEnabled
+ 1:1:void setLifecycleListener(com.batch.android.Batch$Messaging$LifecycleListener):1722:1722 -> setLifecycleListener
+ 1:1:void setShowForegroundLandings(boolean):1689:1689 -> setShowForegroundLandings
+ 1:1:void setTypefaceOverride(android.graphics.Typeface,android.graphics.Typeface):1713:1713 -> setTypefaceOverride
+ 1:1:void show(android.content.Context,com.batch.android.BatchMessage):1788:1788 -> show
+ 2:2:void show(android.content.Context,com.batch.android.BatchMessage):1786:1786 -> show
+ 3:3:void show(android.content.Context,com.batch.android.BatchMessage):1783:1783 -> show
+com.batch.android.Batch$Messaging$DisplayHint -> com.batch.android.Batch$Messaging$DisplayHint:
+ android.view.View view -> b
+ com.batch.android.Batch$Messaging$DisplayHintStrategy strategy -> a
+ 1:3:void (android.view.View,com.batch.android.Batch$Messaging$DisplayHintStrategy):1644:1646 ->
+ 1:1:com.batch.android.Batch$Messaging$DisplayHint embed(android.widget.FrameLayout):1672:1672 -> embed
+ 2:2:com.batch.android.Batch$Messaging$DisplayHint embed(android.widget.FrameLayout):1669:1669 -> embed
+ 1:1:com.batch.android.Batch$Messaging$DisplayHint findUsingView(android.view.View):1659:1659 -> findUsingView
+ 2:2:com.batch.android.Batch$Messaging$DisplayHint findUsingView(android.view.View):1656:1656 -> findUsingView
+com.batch.android.Batch$Messaging$DisplayHintStrategy -> com.batch.android.Batch$Messaging$a:
+ com.batch.android.Batch$Messaging$DisplayHintStrategy[] $VALUES -> c
+ com.batch.android.Batch$Messaging$DisplayHintStrategy EMBED -> b
+ com.batch.android.Batch$Messaging$DisplayHintStrategy TRANSVERSE_HIERARCHY -> a
+ 1:2:void ():1629:1630 ->
+ 3:3:void ():1628:1628 ->
+ 1:1:void (java.lang.String,int):1628:1628 ->
+ 1:1:com.batch.android.Batch$Messaging$DisplayHintStrategy valueOf(java.lang.String):1628:1628 -> valueOf
+ 1:1:com.batch.android.Batch$Messaging$DisplayHintStrategy[] values():1628:1628 -> values
+com.batch.android.Batch$Push -> com.batch.android.Batch$Push:
+ 1:1:void ():663:663 ->
+ 1:1:void appendBatchData(android.content.Intent,android.content.Intent):831:831 -> appendBatchData
+ 2:2:void appendBatchData(android.os.Bundle,android.content.Intent):842:842 -> appendBatchData
+ 3:3:void appendBatchData(com.google.firebase.messaging.RemoteMessage,android.content.Intent):853:853 -> appendBatchData
+ 1:1:void dismissNotifications():744:744 -> dismissNotifications
+ 1:1:void displayNotification(android.content.Context,android.content.Intent):1017:1017 -> displayNotification
+ 2:2:void displayNotification(android.content.Context,android.content.Intent,boolean):1028:1028 -> displayNotification
+ 3:3:void displayNotification(android.content.Context,android.content.Intent,com.batch.android.BatchNotificationInterceptor):1043:1043 -> displayNotification
+ 4:4:void displayNotification(android.content.Context,android.content.Intent,com.batch.android.BatchNotificationInterceptor,boolean):1061:1061 -> displayNotification
+ 5:5:void displayNotification(android.content.Context,com.google.firebase.messaging.RemoteMessage):1068:1068 -> displayNotification
+ 6:6:void displayNotification(android.content.Context,com.google.firebase.messaging.RemoteMessage,com.batch.android.BatchNotificationInterceptor):1080:1080 -> displayNotification
+ 1:1:com.batch.android.BatchNotificationChannelsManager getChannelsManager():731:731 -> getChannelsManager
+ 1:1:java.lang.String getLastKnownPushToken():1123:1123 -> getLastKnownPushToken
+ 1:1:java.util.EnumSet getNotificationsType(android.content.Context):754:754 -> getNotificationsType
+ 1:1:boolean isBatchPush(android.content.Intent):781:781 -> isBatchPush
+ 2:2:boolean isBatchPush(com.google.firebase.messaging.RemoteMessage):793:793 -> isBatchPush
+ 1:1:boolean isManualDisplayModeActivated():810:810 -> isManualDisplayModeActivated
+ 1:1:android.app.PendingIntent makePendingIntent(android.content.Context,android.content.Intent,android.os.Bundle):887:887 -> makePendingIntent
+ 2:2:android.app.PendingIntent makePendingIntent(android.content.Context,android.content.Intent,android.os.Bundle):884:884 -> makePendingIntent
+ 3:3:android.app.PendingIntent makePendingIntent(android.content.Context,android.content.Intent,android.os.Bundle):880:880 -> makePendingIntent
+ 4:4:android.app.PendingIntent makePendingIntent(android.content.Context,android.content.Intent,android.os.Bundle):876:876 -> makePendingIntent
+ 5:5:android.app.PendingIntent makePendingIntent(android.content.Context,android.content.Intent,com.google.firebase.messaging.RemoteMessage):921:921 -> makePendingIntent
+ 6:6:android.app.PendingIntent makePendingIntent(android.content.Context,android.content.Intent,com.google.firebase.messaging.RemoteMessage):918:918 -> makePendingIntent
+ 7:7:android.app.PendingIntent makePendingIntent(android.content.Context,android.content.Intent,com.google.firebase.messaging.RemoteMessage):914:914 -> makePendingIntent
+ 8:8:android.app.PendingIntent makePendingIntent(android.content.Context,android.content.Intent,com.google.firebase.messaging.RemoteMessage):910:910 -> makePendingIntent
+ 1:1:android.app.PendingIntent makePendingIntentForDeeplink(android.content.Context,java.lang.String,android.os.Bundle):953:953 -> makePendingIntentForDeeplink
+ 2:2:android.app.PendingIntent makePendingIntentForDeeplink(android.content.Context,java.lang.String,android.os.Bundle):950:950 -> makePendingIntentForDeeplink
+ 3:3:android.app.PendingIntent makePendingIntentForDeeplink(android.content.Context,java.lang.String,android.os.Bundle):946:946 -> makePendingIntentForDeeplink
+ 4:4:android.app.PendingIntent makePendingIntentForDeeplink(android.content.Context,java.lang.String,android.os.Bundle):942:942 -> makePendingIntentForDeeplink
+ 5:5:android.app.PendingIntent makePendingIntentForDeeplink(android.content.Context,java.lang.String,com.google.firebase.messaging.RemoteMessage):985:985 -> makePendingIntentForDeeplink
+ 6:6:android.app.PendingIntent makePendingIntentForDeeplink(android.content.Context,java.lang.String,com.google.firebase.messaging.RemoteMessage):982:982 -> makePendingIntentForDeeplink
+ 7:7:android.app.PendingIntent makePendingIntentForDeeplink(android.content.Context,java.lang.String,com.google.firebase.messaging.RemoteMessage):978:978 -> makePendingIntentForDeeplink
+ 8:8:android.app.PendingIntent makePendingIntentForDeeplink(android.content.Context,java.lang.String,com.google.firebase.messaging.RemoteMessage):974:974 -> makePendingIntentForDeeplink
+ 1:1:void onNotificationDisplayed(android.content.Context,android.content.Intent):1100:1100 -> onNotificationDisplayed
+ 2:2:void onNotificationDisplayed(android.content.Context,com.google.firebase.messaging.RemoteMessage):1110:1110 -> onNotificationDisplayed
+ 1:1:void refreshRegistration():1140:1140 -> refreshRegistration
+ 1:1:void setAdditionalIntentFlags(java.lang.Integer):1090:1090 -> setAdditionalIntentFlags
+ 1:1:void setGCMSenderId(java.lang.String):692:692 -> setGCMSenderId
+ 1:1:void setLargeIcon(android.graphics.Bitmap):723:723 -> setLargeIcon
+ 1:1:void setManualDisplay(boolean):820:820 -> setManualDisplay
+ 1:1:void setNotificationInterceptor(com.batch.android.BatchNotificationInterceptor):1132:1132 -> setNotificationInterceptor
+ 1:1:void setNotificationsColor(int):803:803 -> setNotificationsColor
+ 1:1:void setNotificationsType(java.util.EnumSet):769:769 -> setNotificationsType
+ 1:1:void setSmallIconResourceId(int):701:701 -> setSmallIconResourceId
+ 1:1:void setSound(android.net.Uri):714:714 -> setSound
+ 1:1:boolean shouldDisplayPush(android.content.Context,android.content.Intent):995:995 -> shouldDisplayPush
+ 2:2:boolean shouldDisplayPush(android.content.Context,com.google.firebase.messaging.RemoteMessage):1007:1007 -> shouldDisplayPush
+com.batch.android.Batch$User -> com.batch.android.Batch$User:
+ 1:1:void ():1278:1278 ->
+ 1:1:com.batch.android.BatchUserDataEditor editor():1357:1357 -> editor
+ 1:1:void fetchAttributes(android.content.Context,com.batch.android.BatchAttributesFetchListener):1370:1370 -> fetchAttributes
+ 1:1:void fetchTagCollections(android.content.Context,com.batch.android.BatchTagCollectionsFetchListener):1383:1383 -> fetchTagCollections
+ 1:1:com.batch.android.BatchUserDataEditor getEditor():1346:1346 -> getEditor
+ 1:1:java.lang.String getIdentifier(android.content.Context):1335:1335 -> getIdentifier
+ 2:2:java.lang.String getIdentifier(android.content.Context):1332:1332 -> getIdentifier
+ 1:3:java.lang.String getInstallationID():1288:1290 -> getInstallationID
+ 1:1:java.lang.String getLanguage(android.content.Context):1307:1307 -> getLanguage
+ 2:2:java.lang.String getLanguage(android.content.Context):1304:1304 -> getLanguage
+ 1:1:java.lang.String getRegion(android.content.Context):1321:1321 -> getRegion
+ 2:2:java.lang.String getRegion(android.content.Context):1318:1318 -> getRegion
+ 1:1:void printDebugInformation():1507:1507 -> printDebugInformation
+ 1:1:void trackEvent(java.lang.String):1393:1393 -> trackEvent
+ 2:2:void trackEvent(java.lang.String,java.lang.String):1404:1404 -> trackEvent
+ 3:10:void trackEvent(java.lang.String,java.lang.String,com.batch.android.json.JSONObject):1420:1427 -> trackEvent
+ 11:19:void trackEvent(java.lang.String,java.lang.String,com.batch.android.BatchEventData):1442:1450 -> trackEvent
+ 1:1:void trackLocation(android.location.Location):1464:1464 -> trackLocation
+ 1:1:void trackTransaction(double):1474:1474 -> trackTransaction
+ 2:11:void trackTransaction(double,com.batch.android.json.JSONObject):1488:1497 -> trackTransaction
+com.batch.android.BatchActionActivity -> com.batch.android.BatchActionActivity:
+ java.lang.String TAG -> a
+ 1:1:void ():20:20 ->
+ 1:1:android.content.Intent addPayloadToIntent(android.content.Intent,android.os.Bundle):28:28 -> a
+ 2:5:androidx.core.app.TaskStackBuilder addPayloadToTaskStackBuilder(androidx.core.app.TaskStackBuilder,android.os.Bundle):38:41 -> a
+ 6:40:void launchDeeplink(android.content.Intent,java.lang.String):56:90 -> a
+ 41:46:void launchDeeplink(android.content.Intent,java.lang.String):81:86 -> a
+ 47:94:void launchDeeplink(android.content.Intent,java.lang.String):64:111 -> a
+ 95:100:void launchDeeplink(android.content.Intent,java.lang.String):102:107 -> a
+ 101:126:void launchDeeplink(android.content.Intent,java.lang.String):94:119 -> a
+ 1:2:void onDestroy():163:164 -> onDestroy
+ 1:19:void onStart():124:142 -> onStart
+ 20:20:void onStart():136:136 -> onStart
+ 21:39:void onStart():134:152 -> onStart
+ 1:2:void onStop():157:158 -> onStop
+com.batch.android.BatchActionService -> com.batch.android.BatchActionService:
+ java.lang.String TAG -> a
+ java.lang.String ACTION_EXTRA_IDENTIFIER -> c
+ java.lang.String INTENT_ACTION -> b
+ java.lang.String ACTION_EXTRA_DISMISS_NOTIFICATION_ID -> e
+ java.lang.String ACTION_EXTRA_ARGS -> d
+ 1:1:void ():28:28 ->
+ 1:47:void onHandleIntent(android.content.Intent):33:79 -> onHandleIntent
+com.batch.android.BatchActivityLifecycleHelper -> com.batch.android.BatchActivityLifecycleHelper:
+ 1:1:void ():19:19 ->
+ 1:1:void onActivityCreated(android.app.Activity,android.os.Bundle):23:23 -> onActivityCreated
+ 1:1:void onActivityDestroyed(android.app.Activity):47:47 -> onActivityDestroyed
+ 1:1:void onActivityStarted(android.app.Activity):28:28 -> onActivityStarted
+ 1:1:void onActivityStopped(android.app.Activity):39:39 -> onActivityStopped
+com.batch.android.BatchAlertContent -> com.batch.android.BatchAlertContent:
+ java.lang.String trackingIdentifier -> a
+ java.lang.String body -> c
+ com.batch.android.BatchAlertContent$CTA acceptCTA -> e
+ java.lang.String title -> b
+ java.lang.String cancelLabel -> d
+ 1:8:void (com.batch.android.messaging.model.AlertMessage):26:33 ->
+ 1:1:com.batch.android.BatchAlertContent$CTA getAcceptCTA():59:59 -> getAcceptCTA
+ 1:1:java.lang.String getBody():49:49 -> getBody
+ 1:1:java.lang.String getCancelLabel():54:54 -> getCancelLabel
+ 1:1:java.lang.String getTitle():44:44 -> getTitle
+ 1:1:java.lang.String getTrackingIdentifier():39:39 -> getTrackingIdentifier
+com.batch.android.BatchAlertContent$CTA -> com.batch.android.BatchAlertContent$CTA:
+ com.batch.android.json.JSONObject args -> c
+ java.lang.String label -> a
+ java.lang.String action -> b
+ 1:8:void (com.batch.android.messaging.model.CTA):71:78 ->
+ 1:1:java.lang.String getAction():90:90 -> getAction
+ 1:1:com.batch.android.json.JSONObject getArgs():95:95 -> getArgs
+ 1:1:java.lang.String getLabel():85:85 -> getLabel
+com.batch.android.BatchBannerContent -> com.batch.android.BatchBannerContent:
+ java.lang.String mediaAccessibilityDescription -> g
+ java.lang.String mediaURL -> f
+ java.lang.Long autoCloseTimeMillis -> i
+ java.util.List ctas -> d
+ com.batch.android.BatchBannerContent$Action globalTapAction -> e
+ java.lang.String trackingIdentifier -> a
+ boolean showCloseButton -> h
+ java.lang.String body -> c
+ java.lang.String title -> b
+ 1:1:void (com.batch.android.messaging.model.BannerMessage):36:36 ->
+ 2:34:void (com.batch.android.messaging.model.BannerMessage):24:56 ->
+ 1:1:java.lang.Long getAutoCloseTimeMillis():93:93 -> getAutoCloseTimeMillis
+ 1:1:java.lang.String getBody():69:69 -> getBody
+ 1:1:java.util.List getCtas():73:73 -> getCtas
+ 1:1:com.batch.android.BatchBannerContent$Action getGlobalTapAction():77:77 -> getGlobalTapAction
+ 1:1:java.lang.String getMediaAccessibilityDescription():85:85 -> getMediaAccessibilityDescription
+ 1:1:java.lang.String getMediaURL():81:81 -> getMediaURL
+ 1:1:java.lang.String getTitle():65:65 -> getTitle
+ 1:1:java.lang.String getTrackingIdentifier():61:61 -> getTrackingIdentifier
+ 1:1:boolean isShowCloseButton():89:89 -> isShowCloseButton
+com.batch.android.BatchBannerContent$Action -> com.batch.android.BatchBannerContent$Action:
+ com.batch.android.json.JSONObject args -> b
+ java.lang.String action -> a
+ 1:7:void (com.batch.android.messaging.model.Action):103:109 ->
+ 1:1:java.lang.String getAction():116:116 -> getAction
+ 1:1:com.batch.android.json.JSONObject getArgs():121:121 -> getArgs
+com.batch.android.BatchBannerContent$CTA -> com.batch.android.BatchBannerContent$CTA:
+ java.lang.String label -> c
+ 1:2:void (com.batch.android.messaging.model.CTA):131:132 ->
+ 1:1:java.lang.String getLabel():137:137 -> getLabel
+com.batch.android.BatchBannerView -> com.batch.android.BatchBannerView:
+ com.batch.android.messaging.model.BannerMessage message -> b
+ com.batch.android.messaging.view.formats.EmbeddedBannerContainer shownContainer -> c
+ com.batch.android.MessagingAnalyticsDelegate analyticsDelegate -> e
+ com.batch.android.BatchMessage rawMessage -> a
+ boolean shown -> d
+ 1:1:void (com.batch.android.BatchMessage,com.batch.android.messaging.model.BannerMessage,com.batch.android.MessagingAnalyticsDelegate):39:39 ->
+ 2:13:void (com.batch.android.BatchMessage,com.batch.android.messaging.model.BannerMessage,com.batch.android.MessagingAnalyticsDelegate):31:42 ->
+ 1:8:void lambda$show$0(android.view.View):123:130 -> a
+ 9:16:void lambda$embed$1(android.widget.FrameLayout):163:170 -> a
+ 1:2:void dismiss(boolean):186:187 -> dismiss
+ 1:9:void embed(android.widget.FrameLayout):152:160 -> embed
+ 10:10:void embed(android.widget.FrameLayout):149:149 -> embed
+ 1:25:void show(android.app.Activity):64:88 -> show
+ 26:26:void show(android.app.Activity):58:58 -> show
+ 27:35:void show(android.view.View):112:120 -> show
+ 36:36:void show(android.view.View):109:109 -> show
+com.batch.android.BatchBannerViewPrivateHelper -> com.batch.android.d:
+ 1:1:void ():10:10 ->
+ 1:1:com.batch.android.BatchBannerView newInstance(com.batch.android.BatchMessage,com.batch.android.messaging.model.BannerMessage,com.batch.android.MessagingAnalyticsDelegate):17:17 -> a
+com.batch.android.BatchDeeplinkInterceptor -> com.batch.android.BatchDeeplinkInterceptor:
+ 1:1:android.content.Intent getFallbackIntent(android.content.Context):31:31 -> getFallbackIntent
+com.batch.android.BatchDisplayReceiptJobService -> com.batch.android.BatchDisplayReceiptJobService:
+ java.lang.String TAG -> a
+ 1:1:void ():18:18 ->
+ 1:3:boolean onStartJob(android.app.job.JobParameters):24:26 -> onStartJob
+com.batch.android.BatchDisplayReceiptJobService$SendReceiptTask -> com.batch.android.BatchDisplayReceiptJobService$a:
+ java.lang.ref.WeakReference originService -> a
+ android.app.job.JobParameters originJobParameters -> b
+ 1:3:void (android.app.job.JobService,android.app.job.JobParameters):40:42 ->
+ 1:10:java.lang.Void doInBackground(java.lang.Void[]):47:56 -> a
+ 1:1:java.lang.Object doInBackground(java.lang.Object[]):35:35 -> doInBackground
+com.batch.android.BatchEventData -> com.batch.android.BatchEventData:
+ java.util.Map attributes -> a
+ int MAXIMUM_STRING_LENGTH -> f
+ int MAXIMUM_URL_LENGTH -> g
+ int MAXIMUM_VALUES -> d
+ java.util.Set tags -> b
+ int MAXIMUM_TAGS -> e
+ boolean convertedFromLegacyAPI -> c
+ 1:1:void ():37:37 ->
+ 2:5:void ():35:38 ->
+ 6:6:void (com.batch.android.json.JSONObject):41:41 ->
+ 7:38:void (com.batch.android.json.JSONObject):35:66 ->
+ 1:1:int lambda$new$0(java.lang.String,java.lang.String):46:46 -> a
+ 2:2:java.util.Map getAttributes():78:78 -> a
+ 3:20:boolean enforceURIValue(java.net.URI):286:303 -> a
+ 21:21:boolean enforceDateValue(java.util.Date):315:315 -> a
+ 22:23:boolean enforceAttributeName(java.lang.String):323:324 -> a
+ 1:10:com.batch.android.BatchEventData addTag(java.lang.String):96:105 -> addTag
+ 1:1:boolean getConvertedFromLegacyAPI():86:86 -> b
+ 2:3:boolean enforceAttributesCount(java.lang.String):253:254 -> b
+ 1:1:java.util.Set getTags():82:82 -> c
+ 2:11:boolean enforceStringValue(java.lang.String):264:273 -> c
+ 1:2:void init():73:74 -> d
+ 3:3:java.lang.String normalizeKey(java.lang.String):337:337 -> d
+ 1:14:com.batch.android.json.JSONObject toInternalJSON():233:246 -> e
+ 1:2:com.batch.android.BatchEventData put(java.lang.String,java.lang.String):120:121 -> put
+ 3:4:com.batch.android.BatchEventData put(java.lang.String,java.net.URI):135:136 -> put
+ 5:6:com.batch.android.BatchEventData put(java.lang.String,float):150:151 -> put
+ 7:8:com.batch.android.BatchEventData put(java.lang.String,double):165:166 -> put
+ 9:10:com.batch.android.BatchEventData put(java.lang.String,int):180:181 -> put
+ 11:12:com.batch.android.BatchEventData put(java.lang.String,long):195:196 -> put
+ 13:14:com.batch.android.BatchEventData put(java.lang.String,boolean):210:211 -> put
+ 15:16:com.batch.android.BatchEventData put(java.lang.String,java.util.Date):225:226 -> put
+com.batch.android.BatchEventData$TypedAttribute -> com.batch.android.BatchEventData$a:
+ com.batch.android.user.AttributeType type -> b
+ java.lang.Object value -> a
+ 1:3:void (java.lang.Object,com.batch.android.user.AttributeType):345:347 ->
+com.batch.android.BatchEventDataPrivateHelper -> com.batch.android.e:
+ 1:1:void ():12:12 ->
+ 1:33:java.util.Map getAttributesFromEventData(com.batch.android.BatchEventData):15:47 -> a
+ 34:34:java.util.Map getAttributesFromEventData(com.batch.android.BatchEventData):44:44 -> a
+ 35:39:java.util.Map getAttributesFromEventData(com.batch.android.BatchEventData):36:40 -> a
+ 40:44:java.util.Map getAttributesFromEventData(com.batch.android.BatchEventData):28:32 -> a
+ 1:1:boolean getConvertedFromLegacyAPIFromEvent(com.batch.android.BatchEventData):60:60 -> b
+ 1:1:java.util.Set getTagsFromEventData(com.batch.android.BatchEventData):56:56 -> c
+com.batch.android.BatchEventDataPrivateHelper$1 -> com.batch.android.e$a:
+ int[] $SwitchMap$com$batch$android$user$AttributeType -> a
+ 1:1:void ():26:26 ->
+com.batch.android.BatchImageContent -> com.batch.android.BatchImageContent:
+ com.batch.android.BatchImageContent$Action globalTapAction -> a
+ long globalTapDelay -> b
+ int autoCloseDelay -> g
+ boolean isFullscreen -> h
+ com.batch.android.messaging.Size2D imageSize -> f
+ boolean allowSwipeToDismiss -> c
+ java.lang.String imageDescription -> e
+ java.lang.String imageURL -> d
+ 1:11:void (com.batch.android.messaging.model.ImageMessage):27:37 ->
+ 1:1:int getAutoCloseDelay():75:75 -> getAutoCloseDelay
+ 1:1:com.batch.android.BatchImageContent$Action getGlobalTapAction():102:102 -> getGlobalTapAction
+ 1:1:long getGlobalTapDelay():98:98 -> getGlobalTapDelay
+ 1:1:java.lang.String getImageDescription():86:86 -> getImageDescription
+ 1:4:android.graphics.Point getImageSize():79:82 -> getImageSize
+ 1:1:java.lang.String getImageURL():90:90 -> getImageURL
+ 1:1:boolean isAllowSwipeToDismiss():94:94 -> isAllowSwipeToDismiss
+ 1:1:boolean isFullscreen():71:71 -> isFullscreen
+com.batch.android.BatchImageContent$Action -> com.batch.android.BatchImageContent$Action:
+ com.batch.android.json.JSONObject args -> b
+ java.lang.String action -> a
+ 1:7:void (com.batch.android.messaging.model.Action):48:54 ->
+ 1:1:java.lang.String getAction():61:61 -> getAction
+ 1:1:com.batch.android.json.JSONObject getArgs():66:66 -> getArgs
+com.batch.android.BatchInAppMessage -> com.batch.android.BatchInAppMessage:
+ com.batch.android.json.JSONObject customPayload -> d
+ java.lang.String campaignId -> f
+ java.lang.String LANDING_PAYLOAD_KEY -> i
+ com.batch.android.json.JSONObject landingPayload -> c
+ java.lang.String CAMPAIGN_TOKEN_KEY -> k
+ java.lang.String CUSTOM_PAYLOAD_KEY -> j
+ com.batch.android.BatchInAppMessage$Content cachedContent -> h
+ java.lang.String CAMPAIGN_EVENT_DATA_KEY -> m
+ java.lang.String CAMPAIGN_ID_KEY -> l
+ java.lang.String campaignToken -> e
+ com.batch.android.json.JSONObject eventData -> g
+ 1:6:void (java.lang.String,java.lang.String,com.batch.android.json.JSONObject,com.batch.android.json.JSONObject,com.batch.android.json.JSONObject):73:78 ->
+ 1:25:com.batch.android.BatchInAppMessage getInstanceFromBundle(android.os.Bundle):39:63 -> a
+ 26:26:com.batch.android.BatchInAppMessage getInstanceFromBundle(android.os.Bundle):50:50 -> a
+ 27:34:android.os.Bundle getBundleRepresentation():103:110 -> a
+ 1:1:com.batch.android.json.JSONObject getCustomPayloadInternal():91:91 -> b
+ 1:1:com.batch.android.json.JSONObject getJSON():83:83 -> c
+ 1:1:java.lang.String getKind():96:96 -> d
+ 1:1:java.lang.String getCampaignId():115:115 -> e
+ 1:1:com.batch.android.json.JSONObject getEventData():119:119 -> f
+ 1:1:java.lang.String getCampaignToken():178:178 -> getCampaignToken
+ 1:23:com.batch.android.BatchInAppMessage$Content getContent():146:168 -> getContent
+ 1:8:com.batch.android.json.JSONObject getCustomPayload():125:132 -> getCustomPayload
+com.batch.android.BatchInboxFetcher -> com.batch.android.BatchInboxFetcher:
+ android.os.Handler handler -> b
+ com.batch.android.inbox.InboxFetcherInternal impl -> a
+ 1:1:void (com.batch.android.inbox.InboxFetcherInternal):38:38 ->
+ 2:5:void (com.batch.android.inbox.InboxFetcherInternal):36:39 ->
+ 1:1:android.os.Handler access$000(com.batch.android.BatchInboxFetcher):32:32 -> a
+ 1:22:void fetchNewNotifications(com.batch.android.BatchInboxFetcher$OnNewNotificationsFetchedListener):123:144 -> fetchNewNotifications
+ 1:19:void fetchNextPage(com.batch.android.BatchInboxFetcher$OnNextPageFetchedListener):155:173 -> fetchNextPage
+ 1:1:java.util.List getFetchedNotifications():111:111 -> getFetchedNotifications
+ 1:1:boolean hasMore():76:76 -> hasMore
+ 1:1:void markAllAsRead():92:92 -> markAllAsRead
+ 1:1:void markAsDeleted(com.batch.android.BatchInboxNotificationContent):101:101 -> markAsDeleted
+ 1:1:void markAsRead(com.batch.android.BatchInboxNotificationContent):85:85 -> markAsRead
+ 1:1:void setFetchLimit(int):57:57 -> setFetchLimit
+ 1:1:void setFilterSilentNotifications(boolean):67:67 -> setFilterSilentNotifications
+ 1:1:void setHandlerOverride(android.os.Handler):184:184 -> setHandlerOverride
+ 1:1:void setMaxPageSize(int):47:47 -> setMaxPageSize
+com.batch.android.BatchInboxFetcher$1 -> com.batch.android.BatchInboxFetcher$a:
+ com.batch.android.BatchInboxFetcher this$0 -> b
+ com.batch.android.BatchInboxFetcher$OnNewNotificationsFetchedListener val$originalListener -> a
+ 1:1:void (com.batch.android.BatchInboxFetcher,com.batch.android.BatchInboxFetcher$OnNewNotificationsFetchedListener):126:126 ->
+ 1:1:void lambda$onFetchSuccess$0(com.batch.android.BatchInboxFetcher$OnNewNotificationsFetchedListener,java.util.List,boolean,boolean):134:134 -> a
+ 2:2:void lambda$onFetchFailure$1(com.batch.android.BatchInboxFetcher$OnNewNotificationsFetchedListener,java.lang.String):140:140 -> a
+ 1:1:void onFetchFailure(java.lang.String):140:140 -> onFetchFailure
+ 1:1:void onFetchSuccess(java.util.List,boolean,boolean):133:133 -> onFetchSuccess
+com.batch.android.BatchInboxFetcher$2 -> com.batch.android.BatchInboxFetcher$b:
+ com.batch.android.BatchInboxFetcher this$0 -> b
+ com.batch.android.BatchInboxFetcher$OnNextPageFetchedListener val$originalListener -> a
+ 1:1:void (com.batch.android.BatchInboxFetcher,com.batch.android.BatchInboxFetcher$OnNextPageFetchedListener):158:158 ->
+ 1:1:void lambda$onFetchSuccess$0(com.batch.android.BatchInboxFetcher$OnNextPageFetchedListener,java.util.List,boolean):164:164 -> a
+ 2:2:void lambda$onFetchFailure$1(com.batch.android.BatchInboxFetcher$OnNextPageFetchedListener,java.lang.String):169:169 -> a
+ 1:1:void onFetchFailure(java.lang.String):169:169 -> onFetchFailure
+ 1:1:void onFetchSuccess(java.util.List,boolean):164:164 -> onFetchSuccess
+com.batch.android.BatchInboxNotificationContent -> com.batch.android.BatchInboxNotificationContent:
+ com.batch.android.inbox.InboxNotificationContentInternal internalContent -> a
+ com.batch.android.BatchPushPayload batchPushPayloadCache -> b
+ 1:1:void (com.batch.android.inbox.InboxNotificationContentInternal):27:27 ->
+ 2:10:void (com.batch.android.inbox.InboxNotificationContentInternal):20:28 ->
+ 1:1:java.lang.String getBody():48:48 -> getBody
+ 1:1:java.util.Date getDate():73:73 -> getDate
+ 1:1:java.lang.String getNotificationIdentifier():38:38 -> getNotificationIdentifier
+ 1:5:com.batch.android.BatchPushPayload getPushPayload():107:111 -> getPushPayload
+ 1:1:java.util.Map getRawPayload():97:97 -> getRawPayload
+ 1:1:com.batch.android.BatchNotificationSource getSource():53:53 -> getSource
+ 1:1:java.lang.String getTitle():43:43 -> getTitle
+ 1:1:boolean isDeleted():68:68 -> isDeleted
+ 1:1:boolean isSilent():85:85 -> isSilent
+ 1:1:boolean isUnread():57:57 -> isUnread
+com.batch.android.BatchInterstitialContent -> com.batch.android.BatchInterstitialContent:
+ java.lang.String mediaAccessibilityDescription -> g
+ java.lang.String mediaURL -> f
+ java.util.List ctas -> e
+ java.lang.String trackingIdentifier -> a
+ boolean showCloseButton -> h
+ java.lang.String title -> c
+ java.lang.String header -> b
+ java.lang.String body -> d
+ 1:1:void