diff --git a/CHANGELOG.md b/CHANGELOG.md
index 38422b622..cc42a758a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,8 @@
## CHANGE LOG.
+### December 24, 2024
+* [CleverTap Android SDK v7.1.0](docs/CTCORECHANGELOG.md)
+
### November 29, 2024
* [CleverTap Android SDK v7.0.3](docs/CTCORECHANGELOG.md)
diff --git a/README.md b/README.md
index d97d6fb99..ec310e727 100644
--- a/README.md
+++ b/README.md
@@ -26,7 +26,7 @@ We publish the SDK to `mavenCentral` as an `AAR` file. Just declare it as depend
```groovy
dependencies {
- implementation "com.clevertap.android:clevertap-android-sdk:7.0.3"
+ implementation "com.clevertap.android:clevertap-android-sdk:7.1.0"
}
```
@@ -34,7 +34,7 @@ Alternatively, you can download and add the AAR file included in this repo in yo
```groovy
dependencies {
- implementation (name: "clevertap-android-sdk-7.0.3", ext: 'aar')
+ implementation (name: "clevertap-android-sdk-7.1.0", ext: 'aar')
}
```
@@ -46,10 +46,10 @@ Add the Firebase Messaging library and Android Support Library v4 as dependencie
```groovy
dependencies {
- implementation "com.clevertap.android:clevertap-android-sdk:7.0.3"
+ implementation "com.clevertap.android:clevertap-android-sdk:7.1.0"
implementation "androidx.core:core:1.9.0"
implementation "com.google.firebase:firebase-messaging:23.0.6"
- implementation "com.google.android.gms:play-services-ads:22.3.0" // Required only if you enable Google ADID collection in the SDK (turned off by default).
+ implementation "com.google.android.gms:play-services-ads:23.6.0" // Required only if you enable Google ADID collection in the SDK (turned off by default).
}
```
@@ -70,7 +70,7 @@ Also be sure to include the `google-services.json` classpath in your Project lev
}
dependencies {
- classpath "com.android.tools.build:gradle:8.2.2"
+ classpath "com.android.tools.build:gradle:8.7.0"
classpath "com.google.gms:google-services:4.4.0"
// NOTE: Do not place your application dependencies here; they belong
diff --git a/clevertap-core/src/androidTest/kotlin/PIFlushWorkInstrumentationTest.kt b/clevertap-core/src/androidTest/kotlin/PIFlushWorkInstrumentationTest.kt
index b82539725..c31d09463 100644
--- a/clevertap-core/src/androidTest/kotlin/PIFlushWorkInstrumentationTest.kt
+++ b/clevertap-core/src/androidTest/kotlin/PIFlushWorkInstrumentationTest.kt
@@ -12,12 +12,13 @@ import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.testing.SynchronousExecutor
import androidx.work.testing.WorkManagerTestInitHelper
+import com.clevertap.android.sdk.AnalyticsManagerBundler.notificationViewedJson
+import com.clevertap.android.sdk.AnalyticsManagerBundler.wzrkBundleToJson
import com.clevertap.android.sdk.CleverTapAPI
import com.clevertap.android.sdk.CleverTapAPI.LogLevel.VERBOSE
import com.clevertap.android.sdk.CleverTapInstanceConfig
import com.clevertap.android.sdk.Constants
import com.clevertap.android.sdk.pushnotification.work.CTFlushPushImpressionsWork
-import com.clevertap.android.sdk.utils.CTJsonConverter
import org.hamcrest.CoreMatchers.*
import org.hamcrest.MatcherAssert.*
import org.json.JSONObject
@@ -83,14 +84,7 @@ class PIFlushWorkInstrumentationTest{
}
listOf(Pair(defaultInstance,bundle),Pair(ctInstance1,bundle1), Pair(ctInstance2,bundle2)).map {
- val event = JSONObject()
- try {
- val notif: JSONObject = CTJsonConverter.getWzrkFields(it.second)
- event.put("evtName", Constants.NOTIFICATION_VIEWED_EVENT_NAME)
- event.put("evtData", notif)
- } catch (ignored: Throwable) {
- //no-op
- }
+ val event = notificationViewedJson(it.second)
Pair(it.first,event)
}.forEach {
it.first!!.coreState!!.databaseManager.queuePushNotificationViewedEventToDB(myContext, it.second)
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/AnalyticsManager.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/AnalyticsManager.java
index 5b66da21d..4d60a96d7 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/AnalyticsManager.java
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/AnalyticsManager.java
@@ -18,6 +18,7 @@
import com.clevertap.android.sdk.task.CTExecutorFactory;
import com.clevertap.android.sdk.task.Task;
import com.clevertap.android.sdk.utils.CTJsonConverter;
+import com.clevertap.android.sdk.utils.Clock;
import com.clevertap.android.sdk.utils.UriHelper;
import com.clevertap.android.sdk.validation.ValidationResult;
import com.clevertap.android.sdk.validation.ValidationResultFactory;
@@ -34,48 +35,41 @@
import org.json.JSONException;
import org.json.JSONObject;
+import kotlin.jvm.functions.Function0;
+
public class AnalyticsManager extends BaseAnalyticsManager {
private final CTLockManager ctLockManager;
-
private final HashMap installReferrerMap = new HashMap<>(8);
-
private final BaseEventQueueManager baseEventQueueManager;
-
private final BaseCallbackManager callbackManager;
-
private final CleverTapInstanceConfig config;
-
private final Context context;
-
private final ControllerManager controllerManager;
-
private final CoreMetaData coreMetaData;
-
private final DeviceInfo deviceInfo;
-
private final ValidationResultStack validationResultStack;
-
private final Validator validator;
-
private final InAppResponse inAppResponse;
-
- private final HashMap notificationIdTagMap = new HashMap<>();
-
+ private final Clock currentTimeProvider;
private final Object notificationMapLock = new Object();
- private final HashMap notificationViewedIdTagMap = new HashMap<>();
-
- AnalyticsManager(Context context,
- CleverTapInstanceConfig config,
- BaseEventQueueManager baseEventQueueManager,
- Validator validator,
- ValidationResultStack validationResultStack,
- CoreMetaData coreMetaData,
- DeviceInfo deviceInfo,
- BaseCallbackManager callbackManager, ControllerManager controllerManager,
- final CTLockManager ctLockManager,
- InAppResponse inAppResponse) {
+ private final HashMap notificationIdTagMap = new HashMap<>();
+ private final HashMap notificationViewedIdTagMap = new HashMap<>();
+
+ AnalyticsManager(
+ Context context,
+ CleverTapInstanceConfig config,
+ BaseEventQueueManager baseEventQueueManager,
+ Validator validator,
+ ValidationResultStack validationResultStack,
+ CoreMetaData coreMetaData,
+ DeviceInfo deviceInfo,
+ BaseCallbackManager callbackManager, ControllerManager controllerManager,
+ final CTLockManager ctLockManager,
+ InAppResponse inAppResponse,
+ Clock currentTimeProvider
+ ) {
this.context = context;
this.config = config;
this.baseEventQueueManager = baseEventQueueManager;
@@ -87,6 +81,7 @@ public class AnalyticsManager extends BaseAnalyticsManager {
this.ctLockManager = ctLockManager;
this.controllerManager = controllerManager;
this.inAppResponse = inAppResponse;
+ this.currentTimeProvider = currentTimeProvider;
}
@Override
@@ -464,8 +459,7 @@ public void pushNotificationClickedEvent(final Bundle extras) {
}
boolean shouldProcess = (accountId == null && config.isDefaultInstance())
- || config.getAccountId()
- .equals(accountId);
+ || config.getAccountId().equals(accountId);
if (!shouldProcess) {
config.getLogger().debug(config.getAccountId(),
@@ -474,60 +468,12 @@ public void pushNotificationClickedEvent(final Bundle extras) {
}
if (extras.containsKey(Constants.INAPP_PREVIEW_PUSH_PAYLOAD_KEY)) {
- Task task = CTExecutorFactory.executors(config).postAsyncSafelyTask();
- task.execute("testInappNotification",new Callable() {
- @Override
- public Void call() {
- try {
- String inappPreviewPayloadType = extras.getString(Constants.INAPP_PREVIEW_PUSH_PAYLOAD_TYPE_KEY);
- String inappPreviewString = extras.getString(Constants.INAPP_PREVIEW_PUSH_PAYLOAD_KEY);
- JSONObject inappPreviewPayload = new JSONObject(inappPreviewString);
-
- JSONArray inappNotifs = new JSONArray();
- if (Constants.INAPP_IMAGE_INTERSTITIAL_TYPE.equals(inappPreviewPayloadType)
- || Constants.INAPP_ADVANCED_BUILDER_TYPE.equals(inappPreviewPayloadType)) {
- inappNotifs.put(getHalfInterstitialInApp(inappPreviewPayload));
- } else {
- inappNotifs.put(inappPreviewPayload);
- }
-
- JSONObject inAppResponseJson = new JSONObject();
- inAppResponseJson.put(Constants.INAPP_JSON_RESPONSE_KEY, inappNotifs);
-
- inAppResponse.processResponse(inAppResponseJson, null, context);
- } catch (Throwable t) {
- Logger.v("Failed to display inapp notification from push notification payload", t);
- }
- return null;
- }
- });
+ handleInAppPreview(extras);
return;
}
if (extras.containsKey(Constants.INBOX_PREVIEW_PUSH_PAYLOAD_KEY)) {
- Task task = CTExecutorFactory.executors(config).postAsyncSafelyTask();
- task.execute("testInboxNotification",new Callable() {
- @Override
- public Void call() {
- try {
- Logger.v("Received inbox via push payload: " + extras
- .getString(Constants.INBOX_PREVIEW_PUSH_PAYLOAD_KEY));
- JSONObject r = new JSONObject();
- JSONArray inboxNotifs = new JSONArray();
- r.put(Constants.INBOX_JSON_RESPONSE_KEY, inboxNotifs);
- JSONObject testPushObject = new JSONObject(
- extras.getString(Constants.INBOX_PREVIEW_PUSH_PAYLOAD_KEY));
- testPushObject.put("_id", String.valueOf(System.currentTimeMillis() / 1000));
- inboxNotifs.put(testPushObject);
-
- CleverTapResponse cleverTapResponse = new InboxResponse(config, ctLockManager, callbackManager, controllerManager);
- cleverTapResponse.processResponse(r, null, context);
- } catch (Throwable t) {
- Logger.v("Failed to process inbox message from push notification payload", t);
- }
- return null;
- }
- });
+ handleInboxPreview(extras);
return;
}
@@ -538,13 +484,16 @@ public Void call() {
if (!extras.containsKey(Constants.NOTIFICATION_ID_TAG) || (extras.getString(Constants.NOTIFICATION_ID_TAG) == null)) {
config.getLogger().debug(config.getAccountId(),
- "Push notification ID Tag is null, not processing Notification Clicked event for: " + extras
- .toString());
+ "Push notification ID Tag is null, not processing Notification Clicked event for: " + extras);
return;
}
// Check for dupe notification views; if same notficationdId within specified time interval (5 secs) don't process
- boolean isDuplicate = checkDuplicateNotificationIds(extras, notificationIdTagMap, Constants.NOTIFICATION_ID_TAG_INTERVAL);
+ boolean isDuplicate = checkDuplicateNotificationIds(
+ dedupeCheckKey(extras),
+ notificationIdTagMap,
+ Constants.NOTIFICATION_ID_TAG_INTERVAL
+ );
if (isDuplicate) {
config.getLogger().debug(config.getAccountId(),
"Already processed Notification Clicked event for " + extras.toString()
@@ -552,26 +501,12 @@ public Void call() {
return;
}
- JSONObject event = new JSONObject();
- JSONObject notif = new JSONObject();
try {
- for (String x : extras.keySet()) {
- if (!x.startsWith(Constants.WZRK_PREFIX)) {
- continue;
- }
- Object value = extras.get(x);
- notif.put(x, value);
- }
+ // convert bundle to json
+ JSONObject event = AnalyticsManagerBundler.notificationViewedJson(extras);
- event.put("evtName", Constants.NOTIFICATION_CLICKED_EVENT_NAME);
- event.put("evtData", notif);
baseEventQueueManager.queueEvent(context, event, Constants.RAISED_EVENT);
-
- try {
- coreMetaData.setWzrkParams(getWzrkFields(extras));
- } catch (Throwable t) {
- // no-op
- }
+ coreMetaData.setWzrkParams(AnalyticsManagerBundler.wzrkBundleToJson(extras));
} catch (Throwable t) {
// We won't get here
}
@@ -583,6 +518,62 @@ public Void call() {
}
}
+ private void handleInboxPreview(Bundle extras) {
+ Task task = CTExecutorFactory.executors(config).postAsyncSafelyTask();
+ task.execute("testInboxNotification",new Callable() {
+ @Override
+ public Void call() {
+ try {
+ Logger.v("Received inbox via push payload: " + extras
+ .getString(Constants.INBOX_PREVIEW_PUSH_PAYLOAD_KEY));
+ JSONObject r = new JSONObject();
+ JSONArray inboxNotifs = new JSONArray();
+ r.put(Constants.INBOX_JSON_RESPONSE_KEY, inboxNotifs);
+ JSONObject testPushObject = new JSONObject(
+ extras.getString(Constants.INBOX_PREVIEW_PUSH_PAYLOAD_KEY));
+ testPushObject.put("_id", String.valueOf(System.currentTimeMillis() / 1000));
+ inboxNotifs.put(testPushObject);
+
+ CleverTapResponse cleverTapResponse = new InboxResponse(config, ctLockManager, callbackManager, controllerManager);
+ cleverTapResponse.processResponse(r, null, context);
+ } catch (Throwable t) {
+ Logger.v("Failed to process inbox message from push notification payload", t);
+ }
+ return null;
+ }
+ });
+ }
+
+ private void handleInAppPreview(Bundle extras) {
+ Task task = CTExecutorFactory.executors(config).postAsyncSafelyTask();
+ task.execute("testInappNotification",new Callable() {
+ @Override
+ public Void call() {
+ try {
+ String inappPreviewPayloadType = extras.getString(Constants.INAPP_PREVIEW_PUSH_PAYLOAD_TYPE_KEY);
+ String inappPreviewString = extras.getString(Constants.INAPP_PREVIEW_PUSH_PAYLOAD_KEY);
+ JSONObject inappPreviewPayload = new JSONObject(inappPreviewString);
+
+ JSONArray inappNotifs = new JSONArray();
+ if (Constants.INAPP_IMAGE_INTERSTITIAL_TYPE.equals(inappPreviewPayloadType)
+ || Constants.INAPP_ADVANCED_BUILDER_TYPE.equals(inappPreviewPayloadType)) {
+ inappNotifs.put(getHalfInterstitialInApp(inappPreviewPayload));
+ } else {
+ inappNotifs.put(inappPreviewPayload);
+ }
+
+ JSONObject inAppResponseJson = new JSONObject();
+ inAppResponseJson.put(Constants.INAPP_JSON_RESPONSE_KEY, inappNotifs);
+
+ inAppResponse.processResponse(inAppResponseJson, null, context);
+ } catch (Throwable t) {
+ Logger.v("Failed to display inapp notification from push notification payload", t);
+ }
+ return null;
+ }
+ });
+ }
+
private JSONObject getHalfInterstitialInApp(final JSONObject inapp) throws JSONException {
String inAppConfig = inapp.optString(Constants.INAPP_IMAGE_INTERSTITIAL_CONFIG);
String htmlContent = wrapImageInterstitialContent(inAppConfig);
@@ -650,33 +641,28 @@ public void pushNotificationViewedEvent(Bundle extras) {
return;
}
- if (!extras.containsKey(Constants.NOTIFICATION_ID_TAG) || (extras.getString(Constants.NOTIFICATION_ID_TAG)
- == null)) {
+ if (!extras.containsKey(Constants.NOTIFICATION_ID_TAG)
+ || (extras.getString(Constants.NOTIFICATION_ID_TAG) == null)) {
config.getLogger().debug(config.getAccountId(),
- "Push notification ID Tag is null, not processing Notification Viewed event for: " + extras
- .toString());
+ "Push notification ID Tag is null, not processing Notification Viewed event for: " + extras);
return;
}
// Check for dupe notification views; if same notficationdId within specified time interval (2 secs) don't process
- boolean isDuplicate = checkDuplicateNotificationIds(extras, notificationViewedIdTagMap,
- Constants.NOTIFICATION_VIEWED_ID_TAG_INTERVAL);
+ boolean isDuplicate = checkDuplicateNotificationIds(
+ dedupeCheckKey(extras),
+ notificationViewedIdTagMap,
+ Constants.NOTIFICATION_VIEWED_ID_TAG_INTERVAL
+ );
if (isDuplicate) {
config.getLogger().debug(config.getAccountId(),
- "Already processed Notification Viewed event for " + extras.toString() + ", dropping duplicate.");
+ "Already processed Notification Viewed event for " + extras + ", dropping duplicate.");
return;
}
- config.getLogger().debug("Recording Notification Viewed event for notification: " + extras.toString());
+ config.getLogger().debug("Recording Notification Viewed event for notification: " + extras);
- JSONObject event = new JSONObject();
- try {
- JSONObject notif = getWzrkFields(extras);
- event.put("evtName", Constants.NOTIFICATION_VIEWED_EVENT_NAME);
- event.put("evtData", notif);
- } catch (Throwable ignored) {
- //no-op
- }
+ JSONObject event = AnalyticsManagerBundler.notificationViewedJson(extras);
baseEventQueueManager.queueEvent(context, event, Constants.NV_EVENT);
}
@@ -1163,18 +1149,43 @@ private void _pushMultiValue(ArrayList originalValues, String key, Strin
}
}
- private boolean checkDuplicateNotificationIds(Bundle extras, HashMap notificationTagMap,
- int interval) {
+ String dedupeCheckKey(Bundle extras) {
+ // This flag is used so that we can release in phased manner, eventually the check has to go away.
+ Object doDedupeCheck = extras.get(Constants.WZRK_DEDUPE);
+
+ boolean check = false;
+ if (doDedupeCheck != null) {
+ if (doDedupeCheck instanceof String) {
+ check = "true".equalsIgnoreCase((String) doDedupeCheck);
+ }
+ if (doDedupeCheck instanceof Boolean) {
+ check = (Boolean) doDedupeCheck;
+ }
+ }
+
+ String notificationIdTag;
+ if (check) {
+ notificationIdTag = extras.getString(Constants.WZRK_PUSH_ID);
+ } else {
+ notificationIdTag = extras.getString(Constants.NOTIFICATION_ID_TAG);
+ }
+ return notificationIdTag;
+ }
+
+ private boolean checkDuplicateNotificationIds(
+ String notificationIdTag,
+ HashMap notificationTagMap,
+ int interval
+ ) {
synchronized (notificationMapLock) {
// default to false; only return true if we are sure we've seen this one before
boolean isDupe = false;
try {
- String notificationIdTag = extras.getString(Constants.NOTIFICATION_ID_TAG);
- long now = System.currentTimeMillis();
+ long now = currentTimeProvider.currentTimeMillis();
if (notificationTagMap.containsKey(notificationIdTag)) {
long timestamp;
// noinspection ConstantConditions
- timestamp = (Long) notificationTagMap.get(notificationIdTag);
+ timestamp = notificationTagMap.get(notificationIdTag);
// same notificationId within time internal treat as dupe
if (now - timestamp < interval) {
isDupe = true;
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/AnalyticsManagerBundler.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/AnalyticsManagerBundler.kt
new file mode 100644
index 000000000..45586483e
--- /dev/null
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/AnalyticsManagerBundler.kt
@@ -0,0 +1,42 @@
+package com.clevertap.android.sdk
+
+import android.os.Bundle
+import org.json.JSONException
+import org.json.JSONObject
+
+object AnalyticsManagerBundler {
+
+ @Throws(JSONException::class)
+ @JvmStatic
+ fun wzrkBundleToJson(root: Bundle): JSONObject {
+ val fields = JSONObject()
+ for (s in root.keySet()) {
+ val o = root[s]
+ if (o is Bundle) {
+ val wzrkFields = wzrkBundleToJson(o)
+ val keys = wzrkFields.keys()
+ while (keys.hasNext()) {
+ val k = keys.next()
+ fields.put(k, wzrkFields[k])
+ }
+ } else if (s.startsWith(Constants.WZRK_PREFIX)) {
+ fields.put(s, root[s])
+ }
+ }
+
+ return fields
+ }
+
+ @JvmStatic
+ fun notificationViewedJson(root: Bundle): JSONObject {
+ val event = JSONObject()
+ try {
+ val notif = wzrkBundleToJson(root)
+ event.put("evtName", Constants.NOTIFICATION_VIEWED_EVENT_NAME)
+ event.put("evtData", notif)
+ } catch (ignored: Throwable) {
+ //no-op
+ }
+ return event
+ }
+}
\ No newline at end of file
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapAPI.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapAPI.java
index 4d14d7676..7adf70fb3 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapAPI.java
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapAPI.java
@@ -70,6 +70,7 @@
import com.clevertap.android.sdk.pushnotification.amp.CTPushAmpListener;
import com.clevertap.android.sdk.task.CTExecutorFactory;
import com.clevertap.android.sdk.task.Task;
+import com.clevertap.android.sdk.usereventlogs.UserEventLog;
import com.clevertap.android.sdk.utils.UriHelper;
import com.clevertap.android.sdk.validation.ManifestValidator;
import com.clevertap.android.sdk.validation.ValidationResult;
@@ -83,6 +84,8 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
@@ -1244,6 +1247,7 @@ private CleverTapAPI(final Context context, final CleverTapInstanceConfig config
task = CTExecutorFactory.executors(config).postAsyncSafelyTask();
task.execute("setStatesAsync", () -> {
CleverTapAPI.this.coreState.getSessionManager().setLastVisitTime();
+ CleverTapAPI.this.coreState.getSessionManager().setUserLastVisitTs();
CleverTapAPI.this.coreState.getDeviceInfo().setDeviceNetworkInfoReportingFromStorage();
CleverTapAPI.this.coreState.getDeviceInfo().setCurrentUserOptOutStateFromStorage();
return null;
@@ -1608,8 +1612,12 @@ void setCoreState(final CoreState cleverTapState) {
*
* @param event The event for which you want to get the total count
* @return Total count in int
+ *
+ * @deprecated since v7.1.0
. Use {@link #getUserEventLogCount(String)} instead.
+ * getUserEventLogCount() provides user-specific event counts.
*/
@SuppressWarnings({"unused"})
+ @Deprecated(since = "7.1.0")
public int getCount(String event) {
EventDetail eventDetail = coreState.getLocalDataStore().getEventDetail(event);
if (eventDetail != null) {
@@ -1619,6 +1627,29 @@ public int getCount(String event) {
return -1;
}
+ /**
+ * Retrieves the count of logged events for a specific event name associated with the current
+ * user/{@link CleverTapAPI#getCleverTapID(OnInitCleverTapIDListener) CleverTap ID}.
+ * This operation involves a database query and should be called from a background thread.
+ *
+ * Example usage:
+ *
+ *
+ * // Call from background thread
+ * int itemSelectedCount = getUserEventLogCount("item_selected")
+ *
+ *
+ * @param eventName Name of the event to get the count for (e.g., "navigation_clicked", "item_selected")
+ * @return The number of times the specified event has occurred for current user, or -1 if there was an error
+ */
+ @WorkerThread
+ public int getUserEventLogCount(String eventName) {
+ if (!getConfig().isPersonalizationEnabled()) {
+ return -1;
+ }
+ return coreState.getLocalDataStore().readUserEventLogCount(eventName);
+ }
+
/**
* Returns an EventDetail object for the particular event passed. EventDetail consists of event name, count, first
* time
@@ -1626,12 +1657,39 @@ public int getCount(String event) {
*
* @param event The event name for which you want the Event details
* @return The {@link EventDetail} object
+ * @deprecated since v7.1.0
. Use {@link #getUserEventLog(String)} instead.
+ * getUserEventLog() provides user-specific event log.
*/
@SuppressWarnings({"unused"})
+ @Deprecated(since = "7.1.0")
public EventDetail getDetails(String event) {
return coreState.getLocalDataStore().getEventDetail(event);
}
+ /**
+ * Retrieves user-specific event log associated with the current user/
+ * {@link CleverTapAPI#getCleverTapID(OnInitCleverTapIDListener) CleverTap ID}.
+ * This operation involves a database query and should be called from a background thread.
+ *
+ * Example usage:
+ *
+ *
+ * // Call from background thread
+ * UserEventLog log = getUserEventLog("navigation_clicked")
+ * long firstOccurrence = log.firstTs
+ *
+ *
+ * @param eventName Name of the event to get the log for (e.g., "navigation_clicked", "item_selected")
+ * @return {@link UserEventLog} or null if the event log does not exist or there was an error
+ */
+ @WorkerThread
+ public UserEventLog getUserEventLog(String eventName) {
+ if (!getConfig().isPersonalizationEnabled()) {
+ return null;
+ }
+ return coreState.getLocalDataStore().readUserEventLog(eventName);
+ }
+
/**
* Returns the device push token or null
*
@@ -1728,8 +1786,11 @@ public CleverTapDisplayUnit getDisplayUnitForId(String unitID) {
*
* @param event The event name for which you want the first time timestamp
* @return The timestamp in int
+ * @deprecated since v7.1.0
. Use {@link #getUserEventLog(String)} instead.
+ * It provides user-specific event log with first occurrence timestamp.
*/
@SuppressWarnings({"unused"})
+ @Deprecated(since = "7.1.0")
public int getFirstTime(String event) {
EventDetail eventDetail = coreState.getLocalDataStore().getEventDetail(event);
if (eventDetail != null) {
@@ -1766,12 +1827,41 @@ public void setGeofenceCallback(GeofenceCallback geofenceCallback) {
* Returns a Map of event names and corresponding event details of all the events raised
*
* @return A Map of Event Name and its corresponding EventDetail object
+ * @deprecated since v7.1.0
. Use {@link #getUserEventLogHistory()} instead.
+ * getUserEventLogHistory() provides user-specific event logs.
*/
@SuppressWarnings({"unused"})
+ @Deprecated(since = "7.1.0")
public Map getHistory() {
return coreState.getLocalDataStore().getEventHistory(context);
}
+ /**
+ * Retrieves history of all event logs associated with the current user/{@link CleverTapAPI#getCleverTapID(OnInitCleverTapIDListener) CleverTap ID} in the ascending order of lastTs.
+ * This operation involves a database query and should be called from a background thread.
+ *
+ * Example usage:
+ *
+ *
+ * // Call from background thread
+ * Map<String, UserEventLog> history = getUserEventLogHistory()
+ *
+ *
+ * @return Map of event name to {@link UserEventLog} for all events by current user, or empty map if there was an error
+ */
+ @WorkerThread
+ public Map getUserEventLogHistory() {
+ Map history = new LinkedHashMap<>();
+ if (!getConfig().isPersonalizationEnabled()) {
+ return history;
+ }
+ List logs = coreState.getLocalDataStore().readUserEventLogs();
+ for (UserEventLog log : logs) {
+ history.put(log.getEventName(), log);
+ }
+ return history;
+ }
+
/**
* Returns the InAppNotificationListener object
*
@@ -1883,8 +1973,11 @@ public int getInboxMessageUnreadCount() {
*
* @param event The event name for which you want the last time timestamp
* @return The timestamp in int
+ * @deprecated since v7.1.0
. Use {@link #getUserEventLog(String)} instead.
+ * It provides user-specific event log with last occurrence timestamp.
*/
@SuppressWarnings({"unused"})
+ @Deprecated(since = "7.1.0")
public int getLastTime(String event) {
EventDetail eventDetail = coreState.getLocalDataStore().getEventDetail(event);
if (eventDetail != null) {
@@ -1922,12 +2015,33 @@ public void setLocation(Location location) {
* Returns the timestamp of the previous visit
*
* @return Timestamp of previous visit in int
+ * @deprecated since v7.1.0
. Use {@link #getUserLastVisitTs()} instead.
+ * getUserLastVisitTs() provides user-specific last visit timestamp.
*/
@SuppressWarnings({"unused"})
+ @Deprecated(since = "7.1.0")
public int getPreviousVisitTime() {
return coreState.getSessionManager().getLastVisitTime();
}
+ /**
+ * Retrieves timestamp of last visit by current user/{@link CleverTapAPI#getCleverTapID(OnInitCleverTapIDListener) CleverTap ID}.
+ *
+ * Example usage:
+ *
+ *
+ * long lastVisitTs = getUserLastVisitTs()
+ *
+ *
+ * @return Timestamp of last visit by current user, or -1 if there was an error
+ */
+ public long getUserLastVisitTs() {
+ if (!getConfig().isPersonalizationEnabled()) {
+ return -1;
+ }
+ return coreState.getSessionManager().getUserLastVisitTs();
+ }
+
/**
* Return the user profile property value for the specified key.
* Date related property values are returned as number of seconds since January 1, 1970, 00:00:00 GMT
@@ -2002,8 +2116,11 @@ public int getTimeElapsed() {
* Returns the total number of times the app has been launched
*
* @return Total number of app launches in int
+ * @deprecated since v7.1.0
. Use {@link #getUserAppLaunchCount()} instead.
+ * getUserAppLaunchCount() provides user-specific app launch count.
*/
@SuppressWarnings({"unused"})
+ @Deprecated(since = "7.1.0")
public int getTotalVisits() {
EventDetail ed = coreState.getLocalDataStore().getEventDetail(Constants.APP_LAUNCHED_EVENT);
if (ed != null) {
@@ -2013,6 +2130,27 @@ public int getTotalVisits() {
return 0;
}
+ /**
+ * Retrieves number of times app launched by current user/{@link CleverTapAPI#getCleverTapID(OnInitCleverTapIDListener) CleverTap ID}.
+ * This operation involves a database query and should be called from a background thread.
+ *
+ * Example usage:
+ *
+ *
+ * // Call from background thread
+ * int launchCount = getUserAppLaunchCount()
+ *
+ *
+ * @return Number of times app launched by current user, or -1 if there was an error
+ */
+ @WorkerThread
+ public int getUserAppLaunchCount() {
+ if (!getConfig().isPersonalizationEnabled()) {
+ return -1;
+ }
+ return coreState.getLocalDataStore().readUserEventLogCount(Constants.APP_LAUNCHED_EVENT);
+ }
+
/**
* Returns a UTMDetail object which consists of UTM parameters like source, medium & campaign
*
@@ -2219,7 +2357,7 @@ public CTProductConfigController productConfig() {
getConfig().getLogger().debug(getAccountId(),
"Product config is not supported with analytics only configuration");
}
- return coreState.getCtProductConfigController();
+ return coreState.getCtProductConfigController(context);
}
/**
@@ -3104,13 +3242,14 @@ private static CleverTapInstanceConfig getDefaultConfig(Context context) {
String spikyProxyDomain = manifest.getSpikeyProxyDomain();
String handshakeDomain = manifest.getHandshakeDomain();
if (accountId == null || accountToken == null) {
- Logger.i(
- "Account ID or Account token is missing from AndroidManifest.xml, unable to create default instance");
+ Logger.i("Account ID or Account token is missing from AndroidManifest.xml, unable to create default instance");
return null;
}
if (accountRegion == null) {
Logger.i("Account Region not specified in the AndroidManifest - using default region");
}
+
+ // todo lp pass manifest info here
CleverTapInstanceConfig defaultInstanceConfig = CleverTapInstanceConfig.createDefaultInstance(context, accountId, accountToken, accountRegion);
if (proxyDomain != null && !proxyDomain.trim().isEmpty()) {
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapFactory.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapFactory.java
index 201c12192..f5329e951 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapFactory.java
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapFactory.java
@@ -33,6 +33,7 @@
import com.clevertap.android.sdk.task.CTExecutorFactory;
import com.clevertap.android.sdk.task.MainLooperHandler;
import com.clevertap.android.sdk.task.Task;
+import com.clevertap.android.sdk.utils.Clock;
import com.clevertap.android.sdk.validation.ValidationResultStack;
import com.clevertap.android.sdk.validation.Validator;
import com.clevertap.android.sdk.variables.CTVariables;
@@ -44,7 +45,7 @@ class CleverTapFactory {
static CoreState getCoreState(Context context, CleverTapInstanceConfig cleverTapInstanceConfig,
String cleverTapID) {
- CoreState coreState = new CoreState(context);
+ CoreState coreState = new CoreState();
TemplatesManager templatesManager = TemplatesManager.createInstance(cleverTapInstanceConfig);
coreState.setTemplatesManager(templatesManager);
@@ -97,7 +98,7 @@ static CoreState getCoreState(Context context, CleverTapInstanceConfig cleverTap
coreState.setDeviceInfo(deviceInfo);
deviceInfo.onInitDeviceInfo(cleverTapID);
- LocalDataStore localDataStore = new LocalDataStore(context, config, cryptHandler, deviceInfo);
+ LocalDataStore localDataStore = new LocalDataStore(context, config, cryptHandler, deviceInfo, baseDatabaseManager);
coreState.setLocalDataStore(localDataStore);
ProfileValueHandler profileValueHandler = new ProfileValueHandler(validator, validationResultStack);
@@ -118,7 +119,7 @@ static CoreState getCoreState(Context context, CleverTapInstanceConfig cleverTap
ctLockManager, callbackManager, deviceInfo, baseDatabaseManager);
coreState.setControllerManager(controllerManager);
- TriggersMatcher triggersMatcher = new TriggersMatcher();
+ TriggersMatcher triggersMatcher = new TriggersMatcher(localDataStore);
TriggerManager triggersManager = new TriggerManager(context, config.getAccountId(), deviceInfo);
ImpressionManager impressionManager = new ImpressionManager(storeRegistry);
LimitsMatcher limitsMatcher = new LimitsMatcher(impressionManager, triggersManager);
@@ -266,7 +267,8 @@ public Void call() throws Exception {
callbackManager,
controllerManager,
ctLockManager,
- inAppResponseForSendTestInApp
+ inAppResponseForSendTestInApp,
+ Clock.Companion.getSYSTEM()
);
coreState.setAnalyticsManager(analyticsManager);
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapInstanceConfig.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapInstanceConfig.java
index 4cf2375f3..903d0077e 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapInstanceConfig.java
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapInstanceConfig.java
@@ -2,8 +2,6 @@
import static com.clevertap.android.sdk.pushnotification.PushNotificationUtil.getAll;
import static com.clevertap.android.sdk.utils.CTJsonConverter.toArray;
-import static com.clevertap.android.sdk.utils.CTJsonConverter.toJsonArray;
-import static com.clevertap.android.sdk.utils.CTJsonConverter.toList;
import android.content.Context;
import android.os.Parcel;
@@ -11,8 +9,10 @@
import android.text.TextUtils;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
+import androidx.annotation.VisibleForTesting;
import com.clevertap.android.sdk.Constants.IdentityType;
import com.clevertap.android.sdk.cryption.CryptHandler;
@@ -40,73 +40,86 @@ public CleverTapInstanceConfig[] newArray(int size) {
};
private String accountId;
-
private String accountRegion;
-
private String accountToken;
-
private String proxyDomain;
-
private String spikyProxyDomain;
-
private String customHandshakeDomain;
-
- @NonNull
- private ArrayList allowedPushTypes = getAll();
-
+ @NonNull private ArrayList allowedPushTypes = getAll();
private boolean analyticsOnly;
-
private boolean backgroundSync;
-
private boolean beta;
-
private boolean createdPostAppLaunch;
-
private int debugLevel;
-
private boolean disableAppLaunchedEvent;
-
private boolean enableCustomCleverTapId;
-
private String fcmSenderId;
-
private boolean isDefaultInstance;
-
private Logger logger;
-
private String packageName;
-
private boolean personalization;
-
private String[] identityKeys = Constants.NULL_STRING_ARRAY;
-
private boolean sslPinning;
-
private boolean useGoogleAdId;
private int encryptionLevel;
-
@SuppressWarnings("unused")
- public static CleverTapInstanceConfig createInstance(Context context, @NonNull String accountId,
- @NonNull String accountToken) {
+ public static CleverTapInstanceConfig createInstance(
+ Context context,
+ @NonNull String accountId,
+ @NonNull String accountToken
+ ) {
+ return CleverTapInstanceConfig.createInstance(context, accountId, accountToken, null);
+ }
+ @SuppressWarnings({"unused"})
+ public static CleverTapInstanceConfig createInstance(
+ @NonNull Context context,
+ @NonNull String accountId,
+ @NonNull String accountToken,
+ @Nullable String accountRegion
+ ) {
//noinspection ConstantConditions
if (accountId == null || accountToken == null) {
Logger.i("CleverTap accountId and accountToken cannot be null");
return null;
}
- return new CleverTapInstanceConfig(context, accountId, accountToken, null, false);
+ ManifestInfo manifestInfo = ManifestInfo.getInstance(context);
+ return CleverTapInstanceConfig.createInstanceWithManifest(manifestInfo, accountId, accountToken, accountRegion, false);
}
- @SuppressWarnings({"unused"})
- public static CleverTapInstanceConfig createInstance(Context context, @NonNull String accountId,
- @NonNull String accountToken, String accountRegion) {
- //noinspection ConstantConditions
- if (accountId == null || accountToken == null) {
- Logger.i("CleverTap accountId and accountToken cannot be null");
+
+ // convenience to construct the internal only default config
+ @SuppressWarnings({"unused", "WeakerAccess"})
+ protected static CleverTapInstanceConfig createDefaultInstance(
+ @NonNull Context context,
+ @NonNull String accountId,
+ @NonNull String accountToken,
+ @Nullable String accountRegion
+ ) {
+ ManifestInfo manifestInfo = ManifestInfo.getInstance(context);
+ return CleverTapInstanceConfig.createInstanceWithManifest(manifestInfo, accountId, accountToken, accountRegion, true);
+ }
+
+ static CleverTapInstanceConfig createInstanceWithManifest(
+ @NonNull ManifestInfo manifest,
+ @NonNull String accountId,
+ @NonNull String accountToken,
+ @Nullable String accountRegion,
+ boolean isDefaultInstance
+ ) {
+ return new CleverTapInstanceConfig(manifest, accountId, accountToken, accountRegion, isDefaultInstance);
+ }
+
+ // for internal use only!
+ @SuppressWarnings({"unused", "WeakerAccess"})
+ @Nullable
+ protected static CleverTapInstanceConfig createInstance(@NonNull String jsonString) {
+ try {
+ return new CleverTapInstanceConfig(jsonString);
+ } catch (Throwable t) {
return null;
}
- return new CleverTapInstanceConfig(context, accountId, accountToken, accountRegion, false);
}
CleverTapInstanceConfig(CleverTapInstanceConfig config) {
@@ -134,9 +147,13 @@ public static CleverTapInstanceConfig createInstance(Context context, @NonNull S
this.encryptionLevel = config.encryptionLevel;
}
- private
- CleverTapInstanceConfig(Context context, String accountId, String accountToken, String accountRegion,
- boolean isDefault) {
+ private CleverTapInstanceConfig(
+ ManifestInfo manifest,
+ String accountId,
+ String accountToken,
+ String accountRegion,
+ boolean isDefault
+ ) {
this.accountId = accountId;
this.accountToken = accountToken;
this.accountRegion = accountRegion;
@@ -147,7 +164,6 @@ public static CleverTapInstanceConfig createInstance(Context context, @NonNull S
this.logger = new Logger(this.debugLevel);
this.createdPostAppLaunch = false;
- ManifestInfo manifest = ManifestInfo.getInstance(context);
this.useGoogleAdId = manifest.useGoogleAdId();
this.disableAppLaunchedEvent = manifest.isAppLaunchedDisabled();
this.sslPinning = manifest.isSSLPinningEnabled();
@@ -515,22 +531,4 @@ String toJSONString() {
private String getDefaultSuffix(@NonNull String tag) {
return "[" + ((!TextUtils.isEmpty(tag) ? ":" + tag : "") + ":" + accountId + "]");
}
-
- // convenience to construct the internal only default config
- @SuppressWarnings({"unused", "WeakerAccess"})
- protected static CleverTapInstanceConfig createDefaultInstance(Context context, @NonNull String accountId,
- @NonNull String accountToken, String accountRegion) {
- return new CleverTapInstanceConfig(context, accountId, accountToken, accountRegion, true);
- }
-
- // for internal use only!
- @SuppressWarnings({"unused", "WeakerAccess"})
- protected static CleverTapInstanceConfig createInstance(@NonNull String jsonString) {
- try {
- return new CleverTapInstanceConfig(jsonString);
- } catch (Throwable t) {
- return null;
- }
- }
-
}
\ No newline at end of file
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/Constants.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/Constants.java
index e33557d89..f15164cec 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/Constants.java
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/Constants.java
@@ -24,26 +24,6 @@ public interface Constants {
@NonNull
String TYPE_PHONE = "Phone";
-
- String LABEL_ACCOUNT_ID = "CLEVERTAP_ACCOUNT_ID";
- String LABEL_TOKEN = "CLEVERTAP_TOKEN";
- String LABEL_NOTIFICATION_ICON = "CLEVERTAP_NOTIFICATION_ICON";
- String LABEL_INAPP_EXCLUDE = "CLEVERTAP_INAPP_EXCLUDE";
- String LABEL_REGION = "CLEVERTAP_REGION";
- String LABEL_PROXY_DOMAIN = "CLEVERTAP_PROXY_DOMAIN";
- String LABEL_SPIKY_PROXY_DOMAIN = "CLEVERTAP_SPIKY_PROXY_DOMAIN";
- String LABEL_CLEVERTAP_HANDSHAKE_DOMAIN = "CLEVERTAP_HANDSHAKE_DOMAIN";
- String LABEL_DISABLE_APP_LAUNCH = "CLEVERTAP_DISABLE_APP_LAUNCHED";
- String LABEL_SSL_PINNING = "CLEVERTAP_SSL_PINNING";
- String LABEL_BACKGROUND_SYNC = "CLEVERTAP_BACKGROUND_SYNC";
- String LABEL_CUSTOM_ID = "CLEVERTAP_USE_CUSTOM_ID";
- String LABEL_USE_GOOGLE_AD_ID = "CLEVERTAP_USE_GOOGLE_AD_ID";
- String LABEL_FCM_SENDER_ID = "FCM_SENDER_ID";
- String LABEL_PACKAGE_NAME = "CLEVERTAP_APP_PACKAGE";
- String LABEL_BETA = "CLEVERTAP_BETA";
- String LABEL_INTENT_SERVICE = "CLEVERTAP_INTENT_SERVICE";
- String LABEL_ENCRYPTION_LEVEL = "CLEVERTAP_ENCRYPTION_LEVEL";
- String LABEL_DEFAULT_CHANNEL_ID = "CLEVERTAP_DEFAULT_CHANNEL_ID";
String FCM_FALLBACK_NOTIFICATION_CHANNEL_ID = "fcm_fallback_notification_channel";
String FCM_FALLBACK_NOTIFICATION_CHANNEL_NAME = "Misc";
String CLEVERTAP_OPTOUT = "ct_optout";
@@ -225,12 +205,6 @@ public interface Constants {
String KEY_TEXT = "text";
String KEY_KEY = "key";
String KEY_VALUE = "value";
- String KEY_EVENT_NAME = "eventName";
- String KEY_EVENT_PROPERTIES = "eventProperties";
- String KEY_ITEM_PROPERTIES = "itemProperties";
- String KEY_GEO_RADIUS_PROPERTIES = "geoRadius";
- String KEY_PROFILE_ATTR_NAME = "profileAttrName";
- String KEY_PROPERTY_VALUE = "propertyValue";
String KEY_COLOR = "color";
String KEY_MESSAGE = "message";
String KEY_HIDE_CLOSE = "close";
@@ -249,11 +223,13 @@ public interface Constants {
String KEY_ENCRYPTION_LEVEL = "encryptionLevel";
String KEY_ENCRYPTION_FLAG_STATUS = "encryptionFlagStatus";
String WZRK_PUSH_ID = "wzrk_pid";
+ String WZRK_DEDUPE = "wzrk_dd";
String WZRK_PUSH_SILENT = "wzrk_pn_s";
String EXTRAS_FROM = "extras_from";
String NOTIF_MSG = "nm";
String NOTIF_TITLE = "nt";
String NOTIF_ICON = "ico";
+ String NOTIF_HIDE_APP_LARGE_ICON = "wzrk_hide_large_icon";
String WZRK_ACTIONS = "wzrk_acts";
String WZRK_BIG_PICTURE = "wzrk_bp";
String WZRK_MSG_SUMMARY = "wzrk_nms";
@@ -272,8 +248,6 @@ public interface Constants {
String INAPP_SUPPRESSED = "suppressed";
String INAPP_SS_EVAL_META = "inapps_eval";
String INAPP_SUPPRESSED_META = "inapps_suppressed";
- String INAPP_OPERATOR = "operator";
- String INAPP_PROPERTYNAME = "propertyName";
String INAPP_WHEN_TRIGGERS = "whenTriggers";
String INAPP_WHEN_LIMITS = "whenLimit";
String INAPP_FC_LIMITS = "frequencyLimits";
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CoreState.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/CoreState.java
index d51caf06a..a663430b1 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CoreState.java
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/CoreState.java
@@ -1,6 +1,7 @@
package com.clevertap.android.sdk;
import android.content.Context;
+
import com.clevertap.android.sdk.cryption.CryptHandler;
import com.clevertap.android.sdk.db.BaseDatabaseManager;
import com.clevertap.android.sdk.events.BaseEventQueueManager;
@@ -23,8 +24,6 @@
public class CoreState {
- private final Context context;
-
private BaseLocationManager baseLocationManager;
private CleverTapInstanceConfig config;
@@ -111,10 +110,6 @@ public void setParser(final Parser parser) {
this.parser = parser;
}
- CoreState(final Context context) {
- this.context = context;
- }
-
public ActivityLifeCycleManager getActivityLifeCycleManager() {
return activityLifeCycleManager;
}
@@ -189,8 +184,8 @@ void setCoreMetaData(final CoreMetaData coreMetaData) {
*
*/
@Deprecated
- public CTProductConfigController getCtProductConfigController() {
- initProductConfig();
+ public CTProductConfigController getCtProductConfigController(Context context) {
+ initProductConfig(context);
return getControllerManager().getCTProductConfigController();
}
@@ -336,7 +331,7 @@ public ProfileValueHandler getProfileValueHandler() {
*
*/
@Deprecated
- private void initProductConfig() {
+ private void initProductConfig(Context context) {
if (getConfig().isAnalyticsOnly()) {
getConfig().getLogger()
.debug(getConfig().getAccountId(), "Product Config is not enabled for this instance");
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/LocalDataStore.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/LocalDataStore.java
index 64f3109a1..611a1793f 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/LocalDataStore.java
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/LocalDataStore.java
@@ -12,19 +12,30 @@
import com.clevertap.android.sdk.cryption.CryptHandler;
import com.clevertap.android.sdk.cryption.CryptUtils;
+import com.clevertap.android.sdk.db.BaseDatabaseManager;
import com.clevertap.android.sdk.db.DBAdapter;
import com.clevertap.android.sdk.events.EventDetail;
+import com.clevertap.android.sdk.usereventlogs.UserEventLog;
+//import org.apache.commons.lang3.RandomStringUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Iterator;
+import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import kotlin.Pair;
+import kotlin.collections.CollectionsKt;
+import kotlin.collections.MapsKt;
+
@SuppressWarnings("unused")
@RestrictTo(Scope.LIBRARY)
public class LocalDataStore {
@@ -38,28 +49,35 @@ public class LocalDataStore {
private final Context context;
private final CryptHandler cryptHandler;
-
- private DBAdapter dbAdapter;
+ private final BaseDatabaseManager baseDatabaseManager;
private final ExecutorService es;
private final String eventNamespace = "local_events";
private final DeviceInfo deviceInfo;
+ private final Set userNormalizedEventLogKeys = Collections.synchronizedSet(new HashSet<>());
+ private final Map normalizedEventNames = new HashMap<>();
- LocalDataStore(Context context, CleverTapInstanceConfig config, CryptHandler cryptHandler, DeviceInfo deviceInfo) {
+ LocalDataStore(Context context, CleverTapInstanceConfig config, CryptHandler cryptHandler, DeviceInfo deviceInfo, BaseDatabaseManager baseDatabaseManager) {
this.context = context;
this.config = config;
this.es = Executors.newFixedThreadPool(1);
this.cryptHandler = cryptHandler;
this.deviceInfo = deviceInfo;
+ this.baseDatabaseManager = baseDatabaseManager;
}
@WorkerThread
public void changeUser() {
+ userNormalizedEventLogKeys.clear();
resetLocalProfileSync();
}
+ /**
+ * @deprecated since v7.1.0
. Use {@link #readUserEventLog(String)}
+ */
+ @Deprecated(since = "7.1.0")
EventDetail getEventDetail(String eventName) {
try {
if (!isPersonalisationEnabled()) {
@@ -77,7 +95,10 @@ EventDetail getEventDetail(String eventName) {
return null;
}
}
-
+ /**
+ * @deprecated since v7.1.0
. Use {@link #readUserEventLogs()}
+ */
+ @Deprecated(since = "7.1.0")
Map getEventHistory(Context context) {
try {
String namespace;
@@ -100,6 +121,10 @@ Map getEventHistory(Context context) {
}
}
+ /**
+ * @deprecated since v7.1.0
. Use {@link #persistUserEventLog(String)}
+ */
+ @Deprecated(since = "7.1.0")
@WorkerThread
public void persistEvent(Context context, JSONObject event, int type) {
@@ -115,6 +140,203 @@ public void persistEvent(Context context, JSONObject event, int type) {
getConfigLogger().verbose(getConfigAccountId(), "Failed to sync with upstream", t);
}
}
+ @WorkerThread
+ public boolean persistUserEventLogsInBulk(Set eventNames){
+ Set> setOfActualAndNormalizedEventNamePair = new HashSet<>();
+ CollectionsKt.mapTo(eventNames, setOfActualAndNormalizedEventNamePair,
+ (actualEventName) -> new Pair<>(actualEventName, getOrPutNormalizedEventName(actualEventName)));
+ return upsertUserEventLogsInBulk(setOfActualAndNormalizedEventNamePair);
+ }
+
+ @WorkerThread
+ public boolean persistUserEventLog(String eventName) {
+
+ if (eventName == null) {
+ return false;
+ }
+
+ Logger logger = config.getLogger();
+ String accountId = config.getAccountId();
+ try {
+ logger.verbose(accountId,"UserEventLog: Persisting EventLog for event "+eventName);
+ if (isUserEventLogExists(eventName)){
+ logger.verbose(accountId,"UserEventLog: Updating EventLog for event "+eventName);
+ return updateUserEventLog(eventName);
+ } else {
+ logger.verbose(accountId,"UserEventLog: Inserting EventLog for event "+eventName);
+ return insertUserEventLog(eventName );
+ }
+ /*
+ * ==========TESTING BLOCK START ==========
+ */
+ /*cleanUpExtraEvents(50);
+
+ UserEventLog userEventLog = readUserEventLog(eventName);
+ logger.verbose(accountId,"UserEventLog: EventLog for event "+eventName+" = "+userEventLog);
+
+ List list = readUserEventLogs();
+ logger.verbose(accountId,"UserEventLog: All EventLog list for User "+list);
+
+ List list1 = readEventLogsForAllUsers();
+ logger.verbose(accountId,"UserEventLog: All user EventLog list "+list1);
+
+ int count = readUserEventLogCount(eventName);
+ logger.verbose(accountId,"UserEventLog: EventLog count for event "+eventName+" = "+count);
+
+ long logFirstTs = readUserEventLogFirstTs(eventName);
+ logger.verbose(accountId,"UserEventLog: EventLog firstTs for event "+eventName+" = "+logFirstTs);
+
+ long logLastTs = readUserEventLogLastTs(eventName);
+ logger.verbose(accountId,"UserEventLog: EventLog lastTs for event "+eventName+" = "+logLastTs);
+
+ boolean isUserEventLogFirstTime = isUserEventLogFirstTime(eventName);
+ logger.verbose(accountId,"UserEventLog: EventLog isUserEventLogFirstTime for event "+eventName+" = "+isUserEventLogFirstTime);*/
+ /*
+ * ==========TESTING BLOCK END ==========
+ */
+ } catch (Throwable t) {
+ logger.verbose(accountId, "UserEventLog: Failed to insert user event log: for event" + eventName, t);
+ return false;
+ }
+ }
+
+ @WorkerThread
+ private boolean updateEventByDeviceIdAndNormalizedEventName(String deviceID, String normalizedEventName) {
+ DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context);
+ boolean updatedEventByDeviceID = dbAdapter.userEventLogDAO().updateEventByDeviceIdAndNormalizedEventName(deviceID, normalizedEventName);
+ getConfigLogger().verbose("updatedEventByDeviceID = " + updatedEventByDeviceID);
+ return updatedEventByDeviceID;
+ }
+
+ @WorkerThread
+ public boolean updateUserEventLog(String eventName) {
+ String deviceID = deviceInfo.getDeviceID();
+ String normalizedEventName = getOrPutNormalizedEventName(eventName);
+ return updateEventByDeviceIdAndNormalizedEventName(deviceID, normalizedEventName);
+ }
+
+ @WorkerThread
+ private boolean upsertUserEventLogsInBulk(Set> setOfActualAndNormalizedEventNamePair) {
+ String deviceID = deviceInfo.getDeviceID();
+ DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context);
+ boolean upsertEventByDeviceID = dbAdapter.userEventLogDAO()
+ .upsertEventsByDeviceIdAndNormalizedEventName(deviceID, setOfActualAndNormalizedEventNamePair);
+ getConfigLogger().verbose("upsertEventByDeviceID = " + upsertEventByDeviceID);
+ return upsertEventByDeviceID;
+ }
+
+ @WorkerThread
+ private long insertEvent(String deviceID, String actualEventName, String normalizedEventName) {
+ DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context);
+ long rowId = dbAdapter.userEventLogDAO().insertEvent(deviceID, actualEventName, normalizedEventName);
+ getConfigLogger().verbose("inserted rowId = " + rowId);
+ return rowId;
+ }
+
+ private String getOrPutNormalizedEventName(String actualEventName) {
+ return MapsKt.getOrPut(normalizedEventNames, actualEventName,
+ () -> Utils.getNormalizedName(actualEventName));
+ }
+
+ @WorkerThread
+ public boolean insertUserEventLog(String eventName) {
+ String deviceID = deviceInfo.getDeviceID();
+ String normalizedEventName = getOrPutNormalizedEventName(eventName);
+ long rowId = insertEvent(deviceID, eventName, normalizedEventName);
+ return rowId >= 0;
+ }
+
+ @WorkerThread
+ private boolean eventExistsByDeviceIdAndNormalizedEventName(String deviceID, String normalizedEventName) {
+ DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context);
+ boolean eventExists = dbAdapter.userEventLogDAO().eventExistsByDeviceIdAndNormalizedEventName(deviceID, normalizedEventName);
+ getConfigLogger().verbose("eventExists = "+eventExists);
+ return eventExists;
+ }
+
+ @WorkerThread
+ public boolean isUserEventLogExists(String eventName) {
+ String deviceID = deviceInfo.getDeviceID();
+ String normalizedEventName = getOrPutNormalizedEventName(eventName);
+ return eventExistsByDeviceIdAndNormalizedEventName(deviceID, normalizedEventName);
+ }
+
+ @WorkerThread
+ private boolean eventExistsByDeviceIdAndNormalizedEventNameAndCount(String deviceID, String normalizedEventName, int count) {
+ DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context);
+ boolean eventExistsByDeviceIDAndCount = dbAdapter.userEventLogDAO()
+ .eventExistsByDeviceIdAndNormalizedEventNameAndCount(deviceID, normalizedEventName, count);
+
+ getConfigLogger().verbose("eventExistsByDeviceIDAndCount = " + eventExistsByDeviceIDAndCount);
+ return eventExistsByDeviceIDAndCount;
+ }
+
+ @WorkerThread
+ public boolean isUserEventLogFirstTime(String eventName) {
+ String normalizedEventName = getOrPutNormalizedEventName(eventName);
+ if (userNormalizedEventLogKeys.contains(normalizedEventName)) {
+ return false;
+ }
+
+ String deviceID = deviceInfo.getDeviceID();
+ int count = readEventCountByDeviceIdAndNormalizedEventName(deviceID, normalizedEventName);
+ if (count > 1) {
+ userNormalizedEventLogKeys.add(normalizedEventName);
+ }
+ return count == 1;
+ }
+
+ @WorkerThread
+ public boolean cleanUpExtraEvents(int threshold, int numberOfRowsToCleanup){
+ DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context);
+ boolean cleanUpExtraEvents = dbAdapter.userEventLogDAO().cleanUpExtraEvents(threshold, numberOfRowsToCleanup);
+ getConfigLogger().verbose("cleanUpExtraEvents boolean= "+cleanUpExtraEvents);
+ return cleanUpExtraEvents;
+ }
+
+ @WorkerThread
+ private UserEventLog readEventByDeviceIdAndNormalizedEventName(String deviceID, String normalizedEventName) {
+ DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context);
+ return dbAdapter.userEventLogDAO().readEventByDeviceIdAndNormalizedEventName(deviceID, normalizedEventName);
+ }
+
+ @WorkerThread
+ public UserEventLog readUserEventLog(String eventName) {
+ String deviceID = deviceInfo.getDeviceID();
+ String normalizedEventName = getOrPutNormalizedEventName(eventName);
+ return readEventByDeviceIdAndNormalizedEventName(deviceID, normalizedEventName);
+ }
+
+ @WorkerThread
+ private int readEventCountByDeviceIdAndNormalizedEventName(String deviceID, String normalizedEventName) {
+ DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context);
+ return dbAdapter.userEventLogDAO().readEventCountByDeviceIdAndNormalizedEventName(deviceID, normalizedEventName);
+ }
+
+ @WorkerThread
+ public int readUserEventLogCount(String eventName) {
+ String deviceID = deviceInfo.getDeviceID();
+ String normalizedEventName = getOrPutNormalizedEventName(eventName);
+ return readEventCountByDeviceIdAndNormalizedEventName(deviceID, normalizedEventName);
+ }
+
+ @WorkerThread
+ private List allEventsByDeviceID(String deviceID) {
+ DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context);
+ return dbAdapter.userEventLogDAO().allEventsByDeviceID(deviceID);
+ }
+
+ @WorkerThread
+ public List readUserEventLogs(){
+ String deviceID = deviceInfo.getDeviceID();
+ return allEventsByDeviceID(deviceID);
+ }
+
+ @WorkerThread
+ public List readEventLogsForAllUsers() {
+ DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context);
+ return dbAdapter.userEventLogDAO().allEvents();
+ }
@WorkerThread
public void setDataSyncFlag(JSONObject event) {
@@ -184,6 +406,10 @@ public Object getProfileProperty(String key) {
}
}
+ /**
+ * @deprecated since v7.1.0
in favor of DB. See {@link UserEventLog}
+ */
+ @Deprecated(since = "7.1.0")
private EventDetail decodeEventDetails(String name, String encoded) {
if (encoded == null) {
return null;
@@ -194,6 +420,10 @@ private EventDetail decodeEventDetails(String name, String encoded) {
Integer.parseInt(parts[2]), name);
}
+ /**
+ * @deprecated since v7.1.0
in favor of DB. See {@link UserEventLog}
+ */
+ @Deprecated(since = "7.1.0")
private String encodeEventDetails(int first, int last, int count) {
return count + "|" + first + "|" + last;
}
@@ -220,6 +450,10 @@ private int getLocalCacheExpiryInterval(int defaultInterval) {
return getIntFromPrefs("local_cache_expires_in", defaultInterval);
}
+ /**
+ * @deprecated since v7.1.0
in favor of DB. See {@link UserEventLog}
+ */
+ @Deprecated(since = "7.1.0")
private String getStringFromPrefs(String rawKey, String defaultValue, String nameSpace) {
if (this.config.isDefaultInstance()) {
String _new = StorageHelper
@@ -242,9 +476,7 @@ void inflateLocalProfileAsync(final Context context) {
this.postAsyncSafely("LocalDataStore#inflateLocalProfileAsync", new Runnable() {
@Override
public void run() {
- if (dbAdapter == null) {
- dbAdapter = new DBAdapter(context, config);
- }
+ DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context);
synchronized (PROFILE_FIELDS_IN_THIS_SESSION) {
try {
JSONObject profile = dbAdapter.fetchUserProfileByAccountIdAndDeviceID(accountID, deviceInfo.getDeviceID());
@@ -294,6 +526,10 @@ private boolean isPersonalisationEnabled() {
return this.config.isPersonalizationEnabled();
}
+ /**
+ * @deprecated since v7.1.0
. Use {@link #persistUserEventLog(String)}
+ */
+ @Deprecated(since = "7.1.0")
@SuppressWarnings("ConstantConditions")
@SuppressLint("CommitPrefEdits")
private void persistEvent(Context context, JSONObject event) {
@@ -353,6 +589,7 @@ public void run() {
if (!passFlag)
CryptUtils.updateEncryptionFlagOnFailure(context, config, Constants.ENCRYPTION_FLAG_DB_SUCCESS, cryptHandler);
+ DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context);
long status = dbAdapter.storeUserProfile(profileID, deviceInfo.getDeviceID(), jsonObjectEncrypted);
getConfigLogger().verbose(getConfigAccountId(),
"Persist Local Profile complete with status " + status + " for id " + profileID);
@@ -433,7 +670,6 @@ private void _removeProfileField(String key) {
}
}
}
-
private void _setProfileField(String key, Object value) {
if (value == null) {
return;
@@ -454,10 +690,25 @@ private void _setProfileField(String key, Object value) {
* @param fields, a map of key value pairs to be updated locally. The value will be null if that key needs to be
* removed
*/
+// int k = 0;
public void updateProfileFields(Map fields) {
if(fields.isEmpty())
return;
-
+ /*Set events = new HashSet<>();
+ for (int i = 0; i < 5000; i++) {
+ String s = "profile field - "+k+"-"+i;//RandomStringUtils.randomAlphanumeric(512);
+ events.add(s);
+ }
+ k++;*/
+ long start = System.nanoTime();
+ persistUserEventLogsInBulk(fields.keySet());
+// persistUserEventLogsInBulk(events);
+ /*for (String key : events)
+ {
+ persistUserEventLog(key);
+ }*/
+ long end = System.nanoTime();
+ config.getLogger().verbose(config.getAccountId(),"UserEventLog: persistUserEventLog execution time = "+(end - start)+" nano seconds");
for (Map.Entry entry : fields.entrySet()) {
String key = entry.getKey();
Object newValue = entry.getValue();
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/ManifestInfo.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/ManifestInfo.java
index b9cca9a55..c10465c76 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/ManifestInfo.java
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/ManifestInfo.java
@@ -6,58 +6,89 @@
import android.os.Bundle;
import android.text.TextUtils;
import androidx.annotation.RestrictTo;
-
+import androidx.annotation.VisibleForTesting;
+
+/**
+ * Parser for android manifest and picks up fields from manifest once to be references
+ *
+ * Should be singleton and initialised only once -> need to validate.
+ */
+// todo lp Remove context dependency from here
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class ManifestInfo {
- private static String accountId;
+ private static final String LABEL_ACCOUNT_ID = "CLEVERTAP_ACCOUNT_ID";
+ private static final String LABEL_TOKEN = "CLEVERTAP_TOKEN";
+ public static final String LABEL_NOTIFICATION_ICON = "CLEVERTAP_NOTIFICATION_ICON";
+ private static final String LABEL_INAPP_EXCLUDE = "CLEVERTAP_INAPP_EXCLUDE";
+ private static final String LABEL_REGION = "CLEVERTAP_REGION";
+ private static final String LABEL_PROXY_DOMAIN = "CLEVERTAP_PROXY_DOMAIN";
+ private static final String LABEL_SPIKY_PROXY_DOMAIN = "CLEVERTAP_SPIKY_PROXY_DOMAIN";
+ private static final String LABEL_CLEVERTAP_HANDSHAKE_DOMAIN = "CLEVERTAP_HANDSHAKE_DOMAIN";
+ private static final String LABEL_DISABLE_APP_LAUNCH = "CLEVERTAP_DISABLE_APP_LAUNCHED";
+ private static final String LABEL_SSL_PINNING = "CLEVERTAP_SSL_PINNING";
+ private static final String LABEL_BACKGROUND_SYNC = "CLEVERTAP_BACKGROUND_SYNC";
+ private static final String LABEL_CUSTOM_ID = "CLEVERTAP_USE_CUSTOM_ID";
+ private static final String LABEL_USE_GOOGLE_AD_ID = "CLEVERTAP_USE_GOOGLE_AD_ID";
+ private static final String LABEL_FCM_SENDER_ID = "FCM_SENDER_ID";
+ private static final String LABEL_PACKAGE_NAME = "CLEVERTAP_APP_PACKAGE";
+ private static final String LABEL_BETA = "CLEVERTAP_BETA";
+ private static final String LABEL_INTENT_SERVICE = "CLEVERTAP_INTENT_SERVICE";
+ private static final String LABEL_ENCRYPTION_LEVEL = "CLEVERTAP_ENCRYPTION_LEVEL";
+ private static final String LABEL_DEFAULT_CHANNEL_ID = "CLEVERTAP_DEFAULT_CHANNEL_ID";
+
+ private static ManifestInfo instance; // singleton
- private static String accountToken;
+ public synchronized static ManifestInfo getInstance(Context context) {
+ if (instance == null) {
+ instance = new ManifestInfo(context);
+ }
+ return instance;
+ }
- private static String accountRegion;
+ static void changeCredentials(String id, String token, String region) {
+ accountId = id;
+ accountToken = token;
+ accountRegion = region;
+ }
- private static String proxyDomain;
+ static void changeCredentials(String id, String token, String _proxyDomain, String _spikyProxyDomain) {
+ accountId = id;
+ accountToken = token;
+ proxyDomain = _proxyDomain;
+ spikyProxyDomain = _spikyProxyDomain;
+ }
- private static String spikyProxyDomain;
+ static void changeCredentials(String id, String token, String _proxyDomain, String _spikyProxyDomain, String customHandshakeDomain) {
+ accountId = id;
+ accountToken = token;
+ proxyDomain = _proxyDomain;
+ spikyProxyDomain = _spikyProxyDomain;
+ handshakeDomain = customHandshakeDomain;
+ }
+ // Have to keep static due to change creds
+ private static String accountId;
+ private static String accountToken;
+ private static String accountRegion;
+ private static String proxyDomain;
+ private static String spikyProxyDomain;
private static String handshakeDomain;
- private static boolean useADID;
-
- private static boolean appLaunchedDisabled;
-
- private static String notificationIcon;
-
- private static ManifestInfo instance;
-
- private static String excludedActivitiesForInApps;
-
- private static boolean sslPinning;
-
- private static boolean backgroundSync;
-
- private static boolean useCustomID;
-
- private static String fcmSenderId;
-
- private static String packageName;
-
- private static boolean beta;
-
- private static String intentServiceName;
-
+ private final boolean useADID;
+ private final boolean appLaunchedDisabled;
+ private final String notificationIcon;
+ private final String excludedActivitiesForInApps;
+ private final boolean sslPinning;
+ private final boolean backgroundSync;
+ private final boolean useCustomID;
+ private final String fcmSenderId;
+ private final String packageName;
+ private final boolean beta;
+ private final String intentServiceName;
private final String devDefaultPushChannelId;
-
private final String[] profileKeys;
-
- private static int encryptionLevel;
-
- public synchronized static ManifestInfo getInstance(Context context) {
- if (instance == null) {
- instance = new ManifestInfo(context);
- }
- return instance;
- }
+ private final int encryptionLevel;
private ManifestInfo(Context context) {
Bundle metaData = null;
@@ -71,58 +102,123 @@ private ManifestInfo(Context context) {
if (metaData == null) {
metaData = new Bundle();
}
+
+ // start -> assign these if they did not happen in changeCredentials
if (accountId == null) {
- accountId = _getManifestStringValueForKey(metaData, Constants.LABEL_ACCOUNT_ID);
+ accountId = _getManifestStringValueForKey(metaData, ManifestInfo.LABEL_ACCOUNT_ID);
}
if (accountToken == null) {
- accountToken = _getManifestStringValueForKey(metaData, Constants.LABEL_TOKEN);
+ accountToken = _getManifestStringValueForKey(metaData, ManifestInfo.LABEL_TOKEN);
}
if (accountRegion == null) {
- accountRegion = _getManifestStringValueForKey(metaData, Constants.LABEL_REGION);
+ accountRegion = _getManifestStringValueForKey(metaData, ManifestInfo.LABEL_REGION);
}
if (proxyDomain == null) {
- proxyDomain = _getManifestStringValueForKey(metaData, Constants.LABEL_PROXY_DOMAIN);
+ proxyDomain = _getManifestStringValueForKey(metaData, ManifestInfo.LABEL_PROXY_DOMAIN);
}
if (spikyProxyDomain == null) {
- spikyProxyDomain = _getManifestStringValueForKey(metaData, Constants.LABEL_SPIKY_PROXY_DOMAIN);
+ spikyProxyDomain = _getManifestStringValueForKey(metaData, ManifestInfo.LABEL_SPIKY_PROXY_DOMAIN);
}
if (handshakeDomain == null) {
- handshakeDomain = _getManifestStringValueForKey(metaData, Constants.LABEL_CLEVERTAP_HANDSHAKE_DOMAIN);
+ handshakeDomain = _getManifestStringValueForKey(metaData, ManifestInfo.LABEL_CLEVERTAP_HANDSHAKE_DOMAIN);
+ }
+ // end -> assign these if they did not happen in changeCredentials
+
+ notificationIcon = _getManifestStringValueForKey(metaData, ManifestInfo.LABEL_NOTIFICATION_ICON);
+ useADID = "1".equals(_getManifestStringValueForKey(metaData, ManifestInfo.LABEL_USE_GOOGLE_AD_ID));
+ appLaunchedDisabled = "1".equals(_getManifestStringValueForKey(metaData, ManifestInfo.LABEL_DISABLE_APP_LAUNCH));
+ excludedActivitiesForInApps = _getManifestStringValueForKey(metaData, ManifestInfo.LABEL_INAPP_EXCLUDE);
+ sslPinning = "1".equals(_getManifestStringValueForKey(metaData, ManifestInfo.LABEL_SSL_PINNING));
+ backgroundSync = "1".equals(_getManifestStringValueForKey(metaData, ManifestInfo.LABEL_BACKGROUND_SYNC));
+ useCustomID = "1".equals(_getManifestStringValueForKey(metaData, ManifestInfo.LABEL_CUSTOM_ID));
+
+ String fcmSenderIdTemp;
+ fcmSenderIdTemp = _getManifestStringValueForKey(metaData, ManifestInfo.LABEL_FCM_SENDER_ID);
+ if (fcmSenderIdTemp != null) {
+ fcmSenderIdTemp = fcmSenderIdTemp.replace("id:", "");
}
- notificationIcon = _getManifestStringValueForKey(metaData, Constants.LABEL_NOTIFICATION_ICON);
- useADID = "1".equals(_getManifestStringValueForKey(metaData, Constants.LABEL_USE_GOOGLE_AD_ID));
- appLaunchedDisabled = "1".equals(_getManifestStringValueForKey(metaData, Constants.LABEL_DISABLE_APP_LAUNCH));
- excludedActivitiesForInApps = _getManifestStringValueForKey(metaData, Constants.LABEL_INAPP_EXCLUDE);
- sslPinning = "1".equals(_getManifestStringValueForKey(metaData, Constants.LABEL_SSL_PINNING));
- backgroundSync = "1".equals(_getManifestStringValueForKey(metaData, Constants.LABEL_BACKGROUND_SYNC));
- useCustomID = "1".equals(_getManifestStringValueForKey(metaData, Constants.LABEL_CUSTOM_ID));
- fcmSenderId = _getManifestStringValueForKey(metaData, Constants.LABEL_FCM_SENDER_ID);
+ fcmSenderId = fcmSenderIdTemp;
+
+ int encLvlTemp;
try {
- int parsedEncryptionLevel = Integer.parseInt(_getManifestStringValueForKey(metaData,Constants.LABEL_ENCRYPTION_LEVEL));
- if(parsedEncryptionLevel >= 0 && parsedEncryptionLevel <= 1){
- encryptionLevel = parsedEncryptionLevel;
- }
- else{
- encryptionLevel = 0;
+ int parsedEncryptionLevel = Integer.parseInt(_getManifestStringValueForKey(metaData, ManifestInfo.LABEL_ENCRYPTION_LEVEL));
+
+ if (parsedEncryptionLevel >= 0 && parsedEncryptionLevel <= 1) {
+ encLvlTemp = parsedEncryptionLevel;
+ } else {
+ encLvlTemp = 0;
Logger.v("Supported encryption levels are only 0 and 1. Setting it to 0 by default");
}
- } catch (Throwable t){
- encryptionLevel = 0;
+ } catch (Throwable t) {
+ encLvlTemp = 0;
Logger.v("Unable to parse encryption level from the Manifest, Setting it to 0 by default", t.getCause());
}
+ encryptionLevel = encLvlTemp;
- if (fcmSenderId != null) {
- fcmSenderId = fcmSenderId.replace("id:", "");
+ packageName = _getManifestStringValueForKey(metaData, ManifestInfo.LABEL_PACKAGE_NAME);
+ beta = "1".equals(_getManifestStringValueForKey(metaData, ManifestInfo.LABEL_BETA));
+ intentServiceName = _getManifestStringValueForKey(metaData, ManifestInfo.LABEL_INTENT_SERVICE);
+ devDefaultPushChannelId = _getManifestStringValueForKey(metaData, ManifestInfo.LABEL_DEFAULT_CHANNEL_ID);
+ profileKeys = parseProfileKeys(metaData);
+ }
+
+ ManifestInfo(
+ String accountId,
+ String accountToken,
+ String accountRegion,
+ String proxyDomain,
+ String spikyProxyDomain,
+ String handshakeDomain,
+ boolean useADID,
+ boolean appLaunchedDisabled,
+ String notificationIcon,
+ String excludedActivitiesForInApps,
+ boolean sslPinning,
+ boolean backgroundSync,
+ boolean useCustomID,
+ String fcmSenderId,
+ String packageName,
+ boolean beta,
+ String intentServiceName,
+ String devDefaultPushChannelId,
+ String[] profileKeys,
+ int encryptionLevel
+ ) {
+
+ // assign these if they did not happen in change creds
+ if (ManifestInfo.accountId == null) {
+ ManifestInfo.accountId = accountId;
}
- packageName = _getManifestStringValueForKey(metaData, Constants.LABEL_PACKAGE_NAME);
- beta = "1".equals(_getManifestStringValueForKey(metaData, Constants.LABEL_BETA));
- if (intentServiceName == null) {
- intentServiceName = _getManifestStringValueForKey(metaData, Constants.LABEL_INTENT_SERVICE);
+ if (ManifestInfo.accountToken == null) {
+ ManifestInfo.accountToken = accountToken;
+ }
+ if (ManifestInfo.accountRegion == null) {
+ ManifestInfo.accountRegion = accountRegion;
+ }
+ if (ManifestInfo.proxyDomain == null) {
+ ManifestInfo.proxyDomain = proxyDomain;
+ }
+ if (ManifestInfo.spikyProxyDomain == null) {
+ ManifestInfo.spikyProxyDomain = spikyProxyDomain;
+ }
+ if (ManifestInfo.handshakeDomain == null) {
+ ManifestInfo.handshakeDomain = handshakeDomain;
}
- devDefaultPushChannelId = _getManifestStringValueForKey(metaData, Constants.LABEL_DEFAULT_CHANNEL_ID);
-
- profileKeys = parseProfileKeys(metaData);
+ this.useADID = useADID;
+ this.appLaunchedDisabled = appLaunchedDisabled;
+ this.notificationIcon = notificationIcon;
+ this.excludedActivitiesForInApps = excludedActivitiesForInApps;
+ this.sslPinning = sslPinning;
+ this.backgroundSync = backgroundSync;
+ this.useCustomID = useCustomID;
+ this.fcmSenderId = fcmSenderId;
+ this.packageName = packageName;
+ this.beta = beta;
+ this.intentServiceName = intentServiceName;
+ this.devDefaultPushChannelId = devDefaultPushChannelId;
+ this.profileKeys = profileKeys;
+ this.encryptionLevel = encryptionLevel;
}
public String getAccountId() {
@@ -219,27 +315,6 @@ private String[] parseProfileKeys(final Bundle metaData) {
: Constants.NULL_STRING_ARRAY;
}
- static void changeCredentials(String id, String token, String region) {
- accountId = id;
- accountToken = token;
- accountRegion = region;
- }
-
- static void changeCredentials(String id, String token, String _proxyDomain, String _spikyProxyDomain) {
- accountId = id;
- accountToken = token;
- proxyDomain = _proxyDomain;
- spikyProxyDomain = _spikyProxyDomain;
- }
-
- static void changeCredentials(String id, String token, String _proxyDomain, String _spikyProxyDomain, String customHandshakeDomain) {
- accountId = id;
- accountToken = token;
- proxyDomain = _proxyDomain;
- spikyProxyDomain = _spikyProxyDomain;
- handshakeDomain = customHandshakeDomain;
- }
-
/**
* This returns string representation of int,boolean,string,float value of given key
*
@@ -247,7 +322,7 @@ static void changeCredentials(String id, String token, String _proxyDomain, Stri
* @param name key of bundle
* @return string representation of int,boolean,string,float
*/
- private static String _getManifestStringValueForKey(Bundle manifest, String name) {
+ private String _getManifestStringValueForKey(Bundle manifest, String name) {
try {
Object o = manifest.get(name);
return (o != null) ? o.toString() : null;
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/SessionManager.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/SessionManager.java
index d93866e34..8fc42d075 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/SessionManager.java
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/SessionManager.java
@@ -2,14 +2,21 @@
import android.content.Context;
import android.content.SharedPreferences;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.WorkerThread;
+
import com.clevertap.android.sdk.events.EventDetail;
+import com.clevertap.android.sdk.usereventlogs.UserEventLog;
import com.clevertap.android.sdk.validation.Validator;
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class SessionManager extends BaseSessionManager {
private long appLastSeen = 0;
private int lastVisitTime;
+ private long userLastVisitTs;
private final CoreMetaData cleverTapMetaData;
@@ -85,6 +92,11 @@ void setLastVisitTime() {
lastVisitTime = ed.getLastTime();
}
}
+ @WorkerThread
+ void setUserLastVisitTs() {
+ UserEventLog appLaunchedEventLog = localDataStore.readUserEventLog(Constants.APP_LAUNCHED_EVENT);
+ userLastVisitTs = appLaunchedEventLog != null ? appLaunchedEventLog.getLastTs() : -1;
+ }
private void createSession(final Context context) {
int sessionId = getNow();
@@ -118,4 +130,7 @@ int getNow() {
return (int) (System.currentTimeMillis() / 1000);
}
+ public long getUserLastVisitTs() {
+ return userLastVisitTs;
+ }
}
\ No newline at end of file
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/db/CtDatabase.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/db/CtDatabase.kt
index 6486c6a4f..764c1579f 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/db/CtDatabase.kt
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/db/CtDatabase.kt
@@ -28,7 +28,7 @@ class DatabaseHelper internal constructor(val context: Context, val config: Clev
companion object {
- private const val DATABASE_VERSION = 4
+ private const val DATABASE_VERSION = 5
private const val DB_LIMIT = 20 * 1024 * 1024 //20mb
}
@@ -41,6 +41,7 @@ class DatabaseHelper internal constructor(val context: Context, val config: Clev
override fun onCreate(db: SQLiteDatabase) {
logger.verbose("Creating CleverTap DB")
executeStatement(db, CREATE_EVENTS_TABLE)
+ executeStatement(db, CREATE_USER_EVENT_LOGS_TABLE)
executeStatement(db, CREATE_PROFILE_EVENTS_TABLE)
executeStatement(db, CREATE_USER_PROFILES_TABLE)
executeStatement(db, CREATE_INBOX_MESSAGES_TABLE)
@@ -87,6 +88,10 @@ class DatabaseHelper internal constructor(val context: Context, val config: Clev
migrateUserProfilesTable(db)
}
}
+
+ if (oldVersion < 5) {
+ executeStatement(db, CREATE_USER_EVENT_LOGS_TABLE)// when app updates [1,2,3,4] to 5
+ }
}
private fun getDeviceIdForAccountIdFromPrefs(accountId: String): String {
@@ -190,14 +195,15 @@ class DatabaseHelper internal constructor(val context: Context, val config: Clev
}
}
-enum class Table(val tableName: String) {
+ enum class Table(val tableName: String) {
EVENTS("events"),
PROFILE_EVENTS("profileEvents"),
USER_PROFILES("userProfiles"),
INBOX_MESSAGES("inboxMessages"),
PUSH_NOTIFICATIONS("pushNotifications"),
UNINSTALL_TS("uninstallTimestamp"),
- PUSH_NOTIFICATION_VIEWED("notificationViewed")
+ PUSH_NOTIFICATION_VIEWED("notificationViewed"),
+ USER_EVENT_LOGS_TABLE("userEventLogs")
}
object Column {
@@ -212,6 +218,11 @@ object Column {
const val CAMPAIGN = "campaignId"
const val WZRKPARAMS = "wzrkParams"
const val DEVICE_ID = "deviceID"
+ const val EVENT_NAME = "eventName"
+ const val NORMALIZED_EVENT_NAME = "normalizedEventName"
+ const val FIRST_TS = "firstTs"
+ const val LAST_TS = "lastTs"
+ const val COUNT = "count"
}
private val CREATE_EVENTS_TABLE = """
@@ -222,6 +233,18 @@ private val CREATE_EVENTS_TABLE = """
);
"""
+private val CREATE_USER_EVENT_LOGS_TABLE = """
+ CREATE TABLE ${Table.USER_EVENT_LOGS_TABLE.tableName} (
+ ${Column.DEVICE_ID} STRING NOT NULL,
+ ${Column.EVENT_NAME} STRING NOT NULL,
+ ${Column.NORMALIZED_EVENT_NAME} STRING NOT NULL,
+ ${Column.FIRST_TS} INTEGER NOT NULL,
+ ${Column.LAST_TS} INTEGER NOT NULL,
+ ${Column.COUNT} INTEGER NOT NULL,
+ PRIMARY KEY (${Column.DEVICE_ID}, ${Column.NORMALIZED_EVENT_NAME})
+ );
+"""
+
private val CREATE_PROFILE_EVENTS_TABLE = """
CREATE TABLE ${PROFILE_EVENTS.tableName} (
${Column.ID} INTEGER PRIMARY KEY AUTOINCREMENT,
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/db/DBAdapter.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/db/DBAdapter.kt
index f32848731..136f3f211 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/db/DBAdapter.kt
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/db/DBAdapter.kt
@@ -11,8 +11,11 @@ import com.clevertap.android.sdk.Constants
import com.clevertap.android.sdk.db.Table.INBOX_MESSAGES
import com.clevertap.android.sdk.db.Table.PUSH_NOTIFICATIONS
import com.clevertap.android.sdk.db.Table.UNINSTALL_TS
+import com.clevertap.android.sdk.db.Table.USER_EVENT_LOGS_TABLE
import com.clevertap.android.sdk.db.Table.USER_PROFILES
import com.clevertap.android.sdk.inbox.CTMessageDAO
+import com.clevertap.android.sdk.usereventlogs.UserEventLogDAO
+import com.clevertap.android.sdk.usereventlogs.UserEventLogDAOImpl
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
@@ -26,19 +29,21 @@ internal class DBAdapter(context: Context, config: CleverTapInstanceConfig) {
//Notification Inbox Messages Table fields
- private const val DB_UPDATE_ERROR = -1L
+ internal const val DB_UPDATE_ERROR = -1L
- private const val DB_OUT_OF_MEMORY_ERROR = -2L
+ internal const val DB_OUT_OF_MEMORY_ERROR = -2L
@Suppress("unused")
private const val DB_UNDEFINED_CODE = -3L
private const val DATABASE_NAME = "clevertap"
- private const val NOT_ENOUGH_SPACE_LOG =
+ internal const val NOT_ENOUGH_SPACE_LOG =
"There is not enough space left on the device to store data, data discarded"
}
+ @Volatile
+ private var userEventLogDao: UserEventLogDAO? = null
private val logger = config.logger
private val dbHelper: DatabaseHelper = DatabaseHelper(context, config, getDatabaseName(config), logger)
@@ -618,6 +623,23 @@ internal class DBAdapter(context: Context, config: CleverTapInstanceConfig) {
}
}
+ /**
+ * -----------------------------
+ * -----------DAO---------------
+ * -----------------------------
+ */
+ @WorkerThread
+ fun userEventLogDAO(): UserEventLogDAO {
+ return userEventLogDao ?: synchronized(this) {
+ userEventLogDao ?: UserEventLogDAOImpl(
+ dbHelper,
+ logger,
+ USER_EVENT_LOGS_TABLE
+ ).also { userEventLogDao = it }
+
+ }
+ }
+
@WorkerThread
private fun belowMemThreshold(): Boolean {
return dbHelper.belowMemThreshold()
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/db/DBManager.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/db/DBManager.kt
index d600fc26d..eef4e3ecf 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/db/DBManager.kt
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/db/DBManager.kt
@@ -17,6 +17,11 @@ internal class DBManager(
private val ctLockManager: CTLockManager
) : BaseDatabaseManager {
+ private companion object {
+ private const val USER_EVENT_LOG_ROWS_PER_USER = 2_048 + 256 // events + profile props
+ private const val USER_EVENT_LOG_ROWS_THRESHOLD = 5 * USER_EVENT_LOG_ROWS_PER_USER
+ }
+
private var dbAdapter: DBAdapter? = null
@WorkerThread
@@ -30,6 +35,8 @@ internal class DBManager(
dbAdapter.cleanupStaleEvents(PROFILE_EVENTS)
dbAdapter.cleanupStaleEvents(PUSH_NOTIFICATION_VIEWED)
dbAdapter.cleanUpPushNotifications()
+ dbAdapter.userEventLogDAO()
+ .cleanUpExtraEvents(USER_EVENT_LOG_ROWS_THRESHOLD, USER_EVENT_LOG_ROWS_PER_USER)
}
return dbAdapter
}
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/events/EventQueueManager.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/events/EventQueueManager.java
index fb0ec4107..c21480984 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/events/EventQueueManager.java
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/events/EventQueueManager.java
@@ -4,8 +4,10 @@
import android.content.Context;
import android.location.Location;
+
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
+
import com.clevertap.android.sdk.BaseCallbackManager;
import com.clevertap.android.sdk.CTLockManager;
import com.clevertap.android.sdk.CleverTapInstanceConfig;
@@ -29,14 +31,16 @@
import com.clevertap.android.sdk.task.Task;
import com.clevertap.android.sdk.validation.ValidationResult;
import com.clevertap.android.sdk.validation.ValidationResultStack;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
import java.util.Iterator;
import java.util.Map;
import java.util.TimeZone;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
public class EventQueueManager extends BaseEventQueueManager implements FailureFlushListener {
@@ -115,7 +119,7 @@ public void addToQueue(final Context context, final JSONObject event, final int
if (eventType == Constants.NV_EVENT) {
config.getLogger()
.verbose(config.getAccountId(), "Pushing Notification Viewed event onto separate queue");
- processPushNotificationViewedEvent(context, event);
+ processPushNotificationViewedEvent(context, event, eventType);
} else if(eventType == Constants.DEFINE_VARS_EVENT) {
processDefineVarsEvent(context, event);
} else {
@@ -310,16 +314,43 @@ public void processEvent(final Context context, final JSONObject event, final in
}
localDataStore.setDataSyncFlag(event);
baseDatabaseManager.queueEventToDB(context, event, eventType);
- updateLocalStore(context, event, eventType);
- scheduleQueueFlush(context);
+ initInAppEvaluation(context, event, eventType);
+
+ scheduleQueueFlush(context);
} catch (Throwable e) {
config.getLogger().verbose(config.getAccountId(), "Failed to queue event: " + event.toString(), e);
}
}
}
- public void processPushNotificationViewedEvent(final Context context, final JSONObject event) {
+ public void initInAppEvaluation(Context context, JSONObject event, int eventType) {
+ String eventName = eventMediator.getEventName(event);
+ Location userLocation = cleverTapMetaData.getLocationFromUser();
+ updateLocalStore(eventName, eventType);
+
+ if (eventMediator.isChargedEvent(event)) {
+ controllerManager.getInAppController()
+ .onQueueChargedEvent(eventMediator.getChargedEventDetails(event),
+ eventMediator.getChargedEventItemDetails(event), userLocation);
+ } else if (!NetworkManager.isNetworkOnline(context) && eventMediator.isEvent(event)) {
+ // in case device is offline just evaluate all events
+ controllerManager.getInAppController().onQueueEvent(eventName,
+ eventMediator.getEventProperties(event), userLocation);
+ } else if (eventType == Constants.PROFILE_EVENT) {
+ // in case profile event, evaluate for user attribute changes
+ Map> userAttributeChangedProperties
+ = eventMediator.computeUserAttributeChangeProperties(event);
+ controllerManager.getInAppController()
+ .onQueueProfileEvent(userAttributeChangedProperties, userLocation);
+ } else if (!eventMediator.isAppLaunchedEvent(event) && eventMediator.isEvent(event)) {
+ // in case device is online only evaluate non-appLaunched events
+ controllerManager.getInAppController().onQueueEvent(eventName,
+ eventMediator.getEventProperties(event), userLocation);
+ }
+ }
+
+ public void processPushNotificationViewedEvent(final Context context, final JSONObject event, final int eventType) {
synchronized (ctLockManager.getEventLock()) {
try {
int session = cleverTapMetaData.getCurrentSessionId();
@@ -333,6 +364,7 @@ public void processPushNotificationViewedEvent(final Context context, final JSON
}
config.getLogger().verbose(config.getAccountId(), "Pushing Notification Viewed event onto DB");
baseDatabaseManager.queuePushNotificationViewedEventToDB(context, event);
+ initInAppEvaluation(context, event, eventType);
config.getLogger()
.verbose(config.getAccountId(), "Pushing Notification Viewed event onto queue flush");
schedulePushNotificationViewedQueueFlush(context);
@@ -455,50 +487,24 @@ public Future> queueEvent(final Context context, final JSONObject event, final
@Override
@WorkerThread
public Void call() {
-
- Location userLocation = cleverTapMetaData.getLocationFromUser();
-
- if (eventMediator.isChargedEvent(event)) {
- controllerManager.getInAppController()
- .onQueueChargedEvent(eventMediator.getChargedEventDetails(event),
- eventMediator.getChargedEventItemDetails(event), userLocation);
- } else if (!NetworkManager.isNetworkOnline(context) && eventMediator.isEvent(event)) {
- // in case device is offline just evaluate all events
- controllerManager.getInAppController().onQueueEvent(eventMediator.getEventName(event),
- eventMediator.getEventProperties(event), userLocation);
- } else if (eventType == Constants.PROFILE_EVENT) {
- // in case profile event, evaluate for user attribute changes
- Map> userAttributeChangedProperties
- = eventMediator.computeUserAttributeChangeProperties(event);
- controllerManager.getInAppController()
- .onQueueProfileEvent(userAttributeChangedProperties, userLocation);
- } else if (!eventMediator.isAppLaunchedEvent(event) && eventMediator.isEvent(event)) {
- // in case device is online only evaluate non-appLaunched events
- controllerManager.getInAppController().onQueueEvent(eventMediator.getEventName(event),
- eventMediator.getEventProperties(event), userLocation);
- }
-
if (eventMediator.shouldDropEvent(event, eventType)) {
return null;
}
if (eventMediator.shouldDeferProcessingEvent(event, eventType)) {
config.getLogger().debug(config.getAccountId(),
"App Launched not yet processed, re-queuing event " + event + "after 2s");
- mainLooperHandler.postDelayed(new Runnable() {
- @Override
- public void run() {
- Task task = CTExecutorFactory.executors(config).postAsyncSafelyTask();
- task.execute("queueEventWithDelay", new Callable() {
- @Override
- @WorkerThread
- public Void call() {
- sessionManager.lazyCreateSession(context);
- pushInitialEventsAsync();
- addToQueue(context, event, eventType);
- return null;
- }
- });
- }
+ mainLooperHandler.postDelayed(() -> {
+ Task task1 = CTExecutorFactory.executors(config).postAsyncSafelyTask();
+ task1.execute("queueEventWithDelay", new Callable() {
+ @Override
+ @WorkerThread
+ public Void call() {
+ sessionManager.lazyCreateSession(context);
+ pushInitialEventsAsync();
+ addToQueue(context, event, eventType);
+ return null;
+ }
+ });
}, 2000);
} else {
if (eventType == Constants.FETCH_EVENT || eventType == Constants.NV_EVENT) {
@@ -586,11 +592,10 @@ public void run() {
mainLooperHandler.post(pushNotificationViewedRunnable);
}
- //Util
- // only call async
- private void updateLocalStore(final Context context, final JSONObject event, final int type) {
+ @WorkerThread
+ private void updateLocalStore(final String eventName, final int type) {
if (type == Constants.RAISED_EVENT) {
- localDataStore.persistEvent(context, event, type);
+ localDataStore.persistUserEventLog(eventName);
}
}
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/evaluation/TriggerAdapter.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/evaluation/TriggerAdapter.kt
index 18853f700..2dd733cd2 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/evaluation/TriggerAdapter.kt
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/evaluation/TriggerAdapter.kt
@@ -77,29 +77,31 @@ class TriggerAdapter(triggerJSON: JSONObject) {
/**
* The name of the event associated with the trigger conditions.
*/
- val eventName: String = triggerJSON.optString(Constants.KEY_EVENT_NAME, "")
+ val eventName: String = triggerJSON.optString(KEY_EVENT_NAME, "")
/**
* The JSONArray containing event property trigger conditions.
*/
- val properties: JSONArray? = triggerJSON.optJSONArray(Constants.KEY_EVENT_PROPERTIES)
+ val properties: JSONArray? = triggerJSON.optJSONArray(KEY_EVENT_PROPERTIES)
/**
* The JSONArray containing item property trigger conditions.Used for Charged event.
*/
- val items: JSONArray? = triggerJSON.optJSONArray(Constants.KEY_ITEM_PROPERTIES)
+ val items: JSONArray? = triggerJSON.optJSONArray(KEY_ITEM_PROPERTIES)
/**
* The JSONArray containing Geographic radius trigger conditions.
* Used for location-based trigger conditions within a specified geographical radius.
*/
- val geoRadiusArray: JSONArray? = triggerJSON.optJSONArray(Constants.KEY_GEO_RADIUS_PROPERTIES)
+ val geoRadiusArray: JSONArray? = triggerJSON.optJSONArray(KEY_GEO_RADIUS_PROPERTIES)
/**
* The string associated with the attribute name for changes in the user-profile
* Used for user attribute changes trigger conditions
*/
- val profileAttrName: String? = triggerJSON.optString(Constants.KEY_PROFILE_ATTR_NAME, null)
+ val profileAttrName: String? = triggerJSON.optString(KEY_PROFILE_ATTR_NAME, null)
+
+ val firstTimeOnly: Boolean = triggerJSON.optBoolean(KEY_FIRST_TIME_ONLY, false)
/**
* Get the count of event property trigger conditions.
@@ -119,6 +121,18 @@ class TriggerAdapter(triggerJSON: JSONObject) {
val geoRadiusCount: Int
get() = geoRadiusArray?.length() ?: 0
+ internal companion object {
+ const val KEY_FIRST_TIME_ONLY = "firstTimeOnly"
+ const val KEY_EVENT_NAME = "eventName"
+ const val KEY_EVENT_PROPERTIES = "eventProperties"
+ const val KEY_ITEM_PROPERTIES = "itemProperties"
+ const val KEY_GEO_RADIUS_PROPERTIES = "geoRadius"
+ const val KEY_PROFILE_ATTR_NAME = "profileAttrName"
+ const val KEY_PROPERTY_VALUE = "propertyValue"
+ const val INAPP_OPERATOR = "operator"
+ const val INAPP_PROPERTYNAME = "propertyName"
+ }
+
/**
* Internal function to create a TriggerCondition from a JSON property object.
*
@@ -127,12 +141,12 @@ class TriggerAdapter(triggerJSON: JSONObject) {
*/
@VisibleForTesting
fun triggerConditionFromJSON(property: JSONObject): TriggerCondition {
- val value = TriggerValue(property.opt(Constants.KEY_PROPERTY_VALUE))
+ val value = TriggerValue(property.opt(KEY_PROPERTY_VALUE))
- val operator = property.optTriggerOperator(Constants.INAPP_OPERATOR)
+ val operator = property.optTriggerOperator(INAPP_OPERATOR)
return TriggerCondition(
- property.optString(Constants.INAPP_PROPERTYNAME, ""),
+ property.optString(INAPP_PROPERTYNAME, ""),
operator,
value
)
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/evaluation/TriggersMatcher.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/evaluation/TriggersMatcher.kt
index e5b9682ea..896020803 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/evaluation/TriggersMatcher.kt
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/evaluation/TriggersMatcher.kt
@@ -1,7 +1,9 @@
package com.clevertap.android.sdk.inapp.evaluation
import android.location.Location
+import androidx.annotation.WorkerThread
import androidx.annotation.VisibleForTesting
+import com.clevertap.android.sdk.LocalDataStore
import com.clevertap.android.sdk.Logger
import com.clevertap.android.sdk.Utils
import com.clevertap.android.sdk.isValid
@@ -13,7 +15,7 @@ import com.clevertap.android.sdk.isValid
*
* @constructor Creates an instance of the `TriggersMatcher` class.
*/
-class TriggersMatcher {
+class TriggersMatcher(private val localDataStore: LocalDataStore) {
/**
* Matches a standard event against a set of trigger conditions.
@@ -58,6 +60,10 @@ class TriggersMatcher {
return false
}
+ if (!matchFirstTimeOnly(trigger)) {
+ return false
+ }
+
if (event.isChargedEvent() && !matchChargedItemConditions(trigger, event)) {
return false
}
@@ -69,6 +75,15 @@ class TriggersMatcher {
return true
}
+ @WorkerThread
+ private fun matchFirstTimeOnly(trigger: TriggerAdapter): Boolean {
+ if (!trigger.firstTimeOnly) {
+ return true
+ }
+ val keyToCheckFirstTime: String = trigger.profileAttrName ?: trigger.eventName
+ return localDataStore.isUserEventLogFirstTime(keyToCheckFirstTime)
+ }
+
private fun matchPropertyConditions(
triggerAdapter: TriggerAdapter,
event: EventAdapter
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/pushnotification/CoreNotificationRenderer.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/pushnotification/CoreNotificationRenderer.java
index 81c98968c..6cec5904f 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/pushnotification/CoreNotificationRenderer.java
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/pushnotification/CoreNotificationRenderer.java
@@ -57,7 +57,6 @@ public String getTitle(final Bundle extras, final Context context) {
@Override
public Builder renderNotification(final Bundle extras, final Context context,
final Builder nb, final CleverTapInstanceConfig config, final int notificationId) {
- String icoPath = extras.getString(Constants.NOTIF_ICON);// uncommon
// uncommon - START
NotificationCompat.Style style;
@@ -123,9 +122,13 @@ public Builder renderNotification(final Bundle extras, final Context context,
.setStyle(style)
.setSmallIcon(smallIcon);
- // uncommon
- nb.setLargeIcon(Utils.getNotificationBitmapWithTimeout(icoPath, true, context,
- config, Constants.PN_LARGE_ICON_DOWNLOAD_TIMEOUT_IN_MILLIS).getBitmap());//uncommon
+ String icoPath = extras.getString(Constants.NOTIF_ICON);// uncommon
+ boolean showIcon = !"true".equalsIgnoreCase(extras.getString(Constants.NOTIF_HIDE_APP_LARGE_ICON));
+ if (showIcon) {
+ // uncommon
+ nb.setLargeIcon(Utils.getNotificationBitmapWithTimeout(icoPath, true, context,
+ config, Constants.PN_LARGE_ICON_DOWNLOAD_TIMEOUT_IN_MILLIS).getBitmap());//uncommon
+ }
// Uncommon - START
// add actions if any
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/usereventlogs/UserEventLog.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/usereventlogs/UserEventLog.kt
new file mode 100644
index 000000000..88ef522eb
--- /dev/null
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/usereventlogs/UserEventLog.kt
@@ -0,0 +1,26 @@
+package com.clevertap.android.sdk.usereventlogs
+
+/**
+ * Data class representing an event log for user actions in the CleverTap SDK.
+ *
+ * This class stores information about a specific event including its name, occurrence timestamps,
+ * frequency count, and the associated GUID/user identifier. It tracks both the original event name
+ * and its normalized version (lowercase, no spaces) for consistent processing.
+ *
+ * @property eventName The original name of the event as provided by the user
+ * @property normalizedEventName The processed version of the event name with spaces removed and converted to lowercase
+ * @property firstTs Timestamp (in milliseconds) of when this event was first recorded
+ * @property lastTs Timestamp (in milliseconds) of when this event was most recently recorded
+ * @property countOfEvents The total number of times this event has occurred
+ * @property deviceID GUID/deviceID of the user where these events occurred
+ *
+ * @see com.clevertap.android.sdk.Utils.getNormalizedName
+ */
+data class UserEventLog(
+ val eventName: String, // The name of the event
+ val normalizedEventName: String, // normalized version of the name of the event
+ val firstTs: Long, // The timestamp of the first occurrence of the event
+ val lastTs: Long, // The timestamp of the last occurrence of the event
+ val countOfEvents: Int, // The number of times the event has occurred
+ val deviceID: String // The GUID/deviceID of the user where the event occurred
+)
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/usereventlogs/UserEventLogDAO.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/usereventlogs/UserEventLogDAO.kt
new file mode 100644
index 000000000..70816d2bf
--- /dev/null
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/usereventlogs/UserEventLogDAO.kt
@@ -0,0 +1,47 @@
+package com.clevertap.android.sdk.usereventlogs
+
+import androidx.annotation.WorkerThread
+
+interface UserEventLogDAO {
+
+ // Insert a new event by deviceID
+ @WorkerThread
+ fun insertEvent(deviceID: String, eventName: String, normalizedEventName: String): Long
+
+ // Update an event by deviceID
+ @WorkerThread
+ fun updateEventByDeviceIdAndNormalizedEventName(deviceID: String, normalizedEventName: String): Boolean
+
+ @WorkerThread
+ fun upsertEventsByDeviceIdAndNormalizedEventName(
+ deviceID: String,
+ setOfActualAndNormalizedEventNamePair: Set>
+ ): Boolean
+
+ // Read an event by deviceID
+ @WorkerThread
+ fun readEventByDeviceIdAndNormalizedEventName(deviceID: String, normalizedEventName: String): UserEventLog?
+
+ // Read an event count by deviceID
+ @WorkerThread
+ fun readEventCountByDeviceIdAndNormalizedEventName(deviceID: String, normalizedEventName: String): Int
+
+ // Check if an event exists by deviceID
+ @WorkerThread
+ fun eventExistsByDeviceIdAndNormalizedEventName(deviceID: String, normalizedEventName: String): Boolean
+
+ // Check if an event exists by deviceID and count
+ @WorkerThread
+ fun eventExistsByDeviceIdAndNormalizedEventNameAndCount(deviceID: String, normalizedEventName: String, count: Int): Boolean
+
+ // Get all events for a particular deviceID
+ @WorkerThread
+ fun allEventsByDeviceID(deviceID: String): List
+
+ // Get all events
+ @WorkerThread
+ fun allEvents(): List
+
+ @WorkerThread
+ fun cleanUpExtraEvents(rowsThreshold: Int, numberOfRowsToCleanup: Int): Boolean
+}
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/usereventlogs/UserEventLogDAOImpl.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/usereventlogs/UserEventLogDAOImpl.kt
new file mode 100644
index 000000000..46a8308b8
--- /dev/null
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/usereventlogs/UserEventLogDAOImpl.kt
@@ -0,0 +1,331 @@
+package com.clevertap.android.sdk.usereventlogs
+
+import android.content.ContentValues
+import android.database.Cursor
+import android.database.sqlite.SQLiteDatabase
+import androidx.annotation.WorkerThread
+import com.clevertap.android.sdk.Logger
+import com.clevertap.android.sdk.Utils
+import com.clevertap.android.sdk.db.Column
+import com.clevertap.android.sdk.db.DBAdapter.Companion.DB_OUT_OF_MEMORY_ERROR
+import com.clevertap.android.sdk.db.DBAdapter.Companion.DB_UPDATE_ERROR
+import com.clevertap.android.sdk.db.DBAdapter.Companion.NOT_ENOUGH_SPACE_LOG
+import com.clevertap.android.sdk.db.DatabaseHelper
+import com.clevertap.android.sdk.db.Table
+
+internal class UserEventLogDAOImpl(
+ private val db: DatabaseHelper,
+ private val logger: Logger,
+ private val table: Table
+) : UserEventLogDAO {
+
+ // Replace multiple params with single POJO param if param length increases
+ @WorkerThread
+ override fun insertEvent(
+ deviceID: String,
+ eventName: String,
+ normalizedEventName: String
+ ): Long {
+ if (!db.belowMemThreshold()) {
+ logger.verbose(NOT_ENOUGH_SPACE_LOG)
+ return DB_OUT_OF_MEMORY_ERROR
+ }
+ val tableName = table.tableName
+ logger.verbose("Inserting event $eventName with deviceID = $deviceID in $tableName")
+ val now = Utils.getNowInMillis()
+ val values = ContentValues().apply {
+ put(Column.EVENT_NAME, eventName)
+ put(Column.NORMALIZED_EVENT_NAME, normalizedEventName)
+ put(Column.FIRST_TS, now)
+ put(Column.LAST_TS, now)
+ put(Column.COUNT, 1)
+ put(Column.DEVICE_ID, deviceID)
+ }
+ return try {
+ db.writableDatabase.insertWithOnConflict(
+ tableName,
+ null,
+ values,
+ SQLiteDatabase.CONFLICT_REPLACE
+ )
+ } catch (e: Exception) {
+ logger.verbose("Error adding row to table $tableName Recreating DB. Exception: $e")
+ db.deleteDatabase()
+ DB_UPDATE_ERROR
+ }
+ }
+
+ @WorkerThread
+ override fun updateEventByDeviceIdAndNormalizedEventName(deviceID: String, normalizedEventName: String): Boolean {
+ val tableName = table.tableName
+ val now = Utils.getNowInMillis()
+
+ return try {
+ val query = """
+ UPDATE $tableName
+ SET
+ ${Column.COUNT} = ${Column.COUNT} + 1,
+ ${Column.LAST_TS} = ?
+ WHERE ${Column.DEVICE_ID} = ?
+ AND ${Column.NORMALIZED_EVENT_NAME} = ?;
+ """.trimIndent()
+
+ logger.verbose("Updating event $normalizedEventName with deviceID = $deviceID in $tableName")
+ db.writableDatabase.execSQL(query, arrayOf(now, deviceID, normalizedEventName))
+ true
+ } catch (e: Exception) {
+ logger.verbose("Could not update event in database $tableName.", e)
+ false
+ }
+ }
+
+ @WorkerThread
+ override fun upsertEventsByDeviceIdAndNormalizedEventName(
+ deviceID: String,
+ setOfActualAndNormalizedEventNamePair: Set>
+ ): Boolean {
+ val tableName = table.tableName
+ logger.verbose("UserEventLog: upsert EventLog for bulk events")
+ return try {
+ db.writableDatabase.beginTransaction()
+ setOfActualAndNormalizedEventNamePair.forEach {
+ if (eventExistsByDeviceIdAndNormalizedEventName(deviceID, it.second)) {
+ logger.verbose("UserEventLog: Updating EventLog for event $it")
+ updateEventByDeviceIdAndNormalizedEventName(deviceID, it.second)
+ } else {
+ logger.verbose("UserEventLog: Inserting EventLog for event $it")
+ insertEvent(deviceID, it.first, it.second)
+ }
+ }
+ db.writableDatabase.setTransactionSuccessful()
+ db.writableDatabase.endTransaction()
+ true
+ } catch (e: Exception) {
+ logger.verbose("Failed to perform bulk upsert on table $tableName", e)
+ try {
+ db.writableDatabase.endTransaction()
+ } catch (e: Exception) {
+ logger.verbose("Failed to end transaction on table $tableName", e)
+ }
+ false
+ }
+ }
+
+ @WorkerThread
+ override fun readEventByDeviceIdAndNormalizedEventName(deviceID: String, normalizedEventName: String): UserEventLog? {
+ val tName = table.tableName
+ val selection = "${Column.DEVICE_ID} = ? AND ${Column.NORMALIZED_EVENT_NAME} = ?"
+ val selectionArgs = arrayOf(deviceID, normalizedEventName)
+ return try {
+ db.readableDatabase.query(
+ tName, null, selection, selectionArgs, null, null, null, null
+ )?.use { cursor ->
+ if (cursor.moveToFirst()) {
+ val eventLog = UserEventLog(
+ eventName = cursor.getString(cursor.getColumnIndexOrThrow(Column.EVENT_NAME)),
+ normalizedEventName = cursor.getString(cursor.getColumnIndexOrThrow(Column.NORMALIZED_EVENT_NAME)),
+ firstTs = cursor.getLong(cursor.getColumnIndexOrThrow(Column.FIRST_TS)),
+ lastTs = cursor.getLong(cursor.getColumnIndexOrThrow(Column.LAST_TS)),
+ countOfEvents = cursor.getInt(cursor.getColumnIndexOrThrow(Column.COUNT)),
+ deviceID = cursor.getString(cursor.getColumnIndexOrThrow(Column.DEVICE_ID))
+ )
+ eventLog
+ } else {
+ null
+ }
+ }
+ } catch (e: Exception) {
+ logger.verbose("Could not fetch records out of database $tName.", e)
+ null
+ }
+ }
+
+ @WorkerThread
+ override fun readEventCountByDeviceIdAndNormalizedEventName(deviceID: String, normalizedEventName: String): Int {
+ val tName = table.tableName
+ val selection = "${Column.DEVICE_ID} = ? AND ${Column.NORMALIZED_EVENT_NAME} = ?"
+ val selectionArgs = arrayOf(deviceID, normalizedEventName)
+ val projection = arrayOf(Column.COUNT)
+
+ return try {
+ db.readableDatabase.query(
+ tName, projection, selection, selectionArgs, null, null, null, null
+ )?.use { cursor ->
+ if (cursor.moveToFirst()) {
+ cursor.getInt(cursor.getColumnIndexOrThrow(Column.COUNT))
+ } else {
+ 0
+ }
+ } ?: -1
+ } catch (e: Exception) {
+ logger.verbose("Could not fetch records out of database $tName.", e)
+ -1
+ }
+ }
+
+ @WorkerThread
+ override fun eventExistsByDeviceIdAndNormalizedEventName(deviceID: String, normalizedEventName: String): Boolean {
+ val tName = table.tableName
+ val selection = "${Column.DEVICE_ID} = ? AND ${Column.NORMALIZED_EVENT_NAME} = ?"
+ val selectionArgs = arrayOf(deviceID, normalizedEventName)
+ val resultColumn = "eventExists"
+
+ val query = """
+ SELECT EXISTS(
+ SELECT 1
+ FROM $tName
+ WHERE $selection
+ ) AS $resultColumn;
+ """.trimIndent()
+
+ return try {
+ db.readableDatabase.rawQuery(query, selectionArgs)?.use { cursor ->
+ if (cursor.moveToFirst()) {
+ cursor.getInt(cursor.getColumnIndexOrThrow(resultColumn)) == 1
+ } else {
+ false
+ }
+ } ?: false
+ } catch (e: Exception) {
+ logger.verbose("Could not fetch records out of database $tName.", e)
+ false
+ }
+ }
+
+ @WorkerThread
+ override fun eventExistsByDeviceIdAndNormalizedEventNameAndCount(deviceID: String, normalizedEventName: String, count: Int): Boolean {
+ val tName = table.tableName
+ val selection = "${Column.DEVICE_ID} = ? AND ${Column.NORMALIZED_EVENT_NAME} = ? AND ${Column.COUNT} = ?"
+ val selectionArgs = arrayOf(deviceID, normalizedEventName, count.toString())
+ val resultColumn = "eventExists"
+
+ val query = """
+ SELECT EXISTS(
+ SELECT 1
+ FROM $tName
+ WHERE $selection
+ ) AS $resultColumn;
+ """.trimIndent()
+ return try {
+ db.readableDatabase.rawQuery(query, selectionArgs)?.use { cursor ->
+ if (cursor.moveToFirst()) {
+ cursor.getInt(cursor.getColumnIndexOrThrow(resultColumn)) == 1
+ } else {
+ false
+ }
+ } ?: false
+ } catch (e: Exception) {
+ logger.verbose("Could not fetch records out of database $tName.", e)
+ false
+ }
+ }
+
+ // Create index on deviceID,lastTs column if this method is frequently used
+ @WorkerThread
+ override fun allEventsByDeviceID(deviceID: String): List {
+ val tName = table.tableName
+ val eventList = mutableListOf()
+ val selection = "${Column.DEVICE_ID} = ?"
+ val selectionArgs = arrayOf(deviceID)
+ val orderBy = "${Column.LAST_TS} ASC"
+
+ return try {
+ db.readableDatabase.query(
+ tName, null, selection, selectionArgs, null, null, orderBy, null
+ )?.use { cursor ->
+ while (cursor.moveToNext()) {
+ val eventLog = UserEventLog(
+ eventName = cursor.getString(cursor.getColumnIndexOrThrow(Column.EVENT_NAME)),
+ normalizedEventName = cursor.getString(cursor.getColumnIndexOrThrow(Column.NORMALIZED_EVENT_NAME)),
+ firstTs = cursor.getLong(cursor.getColumnIndexOrThrow(Column.FIRST_TS)),
+ lastTs = cursor.getLong(cursor.getColumnIndexOrThrow(Column.LAST_TS)),
+ countOfEvents = cursor.getInt(cursor.getColumnIndexOrThrow(Column.COUNT)),
+ deviceID = cursor.getString(cursor.getColumnIndexOrThrow(Column.DEVICE_ID))
+ )
+ eventList.add(eventLog)
+ }
+ eventList
+ } ?: emptyList()
+ } catch (e: Exception) {
+ logger.verbose("Could not fetch records out of database $tName.", e)
+ emptyList()
+ }
+ }
+
+ // Create index on lastTs column if this method is frequently used
+ @WorkerThread
+ override fun allEvents(): List {
+ val tName = table.tableName
+ val eventList = mutableListOf()
+ val orderBy = "${Column.LAST_TS} ASC"
+
+ return try {
+ db.readableDatabase.query(
+ tName, null, null, null, null, null, orderBy
+ )?.use { cursor ->
+ while (cursor.moveToNext()) {
+ val eventLog = UserEventLog(
+ eventName = cursor.getString(cursor.getColumnIndexOrThrow(Column.EVENT_NAME)),
+ normalizedEventName = cursor.getString(cursor.getColumnIndexOrThrow(Column.NORMALIZED_EVENT_NAME)),
+ firstTs = cursor.getLong(cursor.getColumnIndexOrThrow(Column.FIRST_TS)),
+ lastTs = cursor.getLong(cursor.getColumnIndexOrThrow(Column.LAST_TS)),
+ countOfEvents = cursor.getInt(cursor.getColumnIndexOrThrow(Column.COUNT)),
+ deviceID = cursor.getString(cursor.getColumnIndexOrThrow(Column.DEVICE_ID))
+ )
+ eventList.add(eventLog)
+ }
+ eventList
+ } ?: emptyList()
+ } catch (e: Exception) {
+ logger.verbose("Could not fetch records out of database $tName.", e)
+ emptyList()
+ }
+ }
+
+ @WorkerThread
+ override fun cleanUpExtraEvents(threshold: Int, numberOfRowsToCleanup: Int): Boolean {
+ if (threshold <= 0) {
+ logger.verbose("Invalid threshold value: $threshold. Threshold should be greater than 0")
+ return false
+ }
+ if (numberOfRowsToCleanup < 0) {
+ logger.verbose("Invalid numberOfRowsToCleanup value: $numberOfRowsToCleanup. Should be greater than or equal to 0")
+ return false
+ }
+ if (numberOfRowsToCleanup >= threshold) {
+ logger.verbose("Invalid numberOfRowsToCleanup value: $numberOfRowsToCleanup. Should be less than threshold: $threshold")
+ return false
+ }
+
+ val tName = table.tableName
+ val numberOfRowsToKeep = threshold - numberOfRowsToCleanup
+
+ return try {
+ // SQL query to delete only the least recently used rows, using a subquery with LIMIT
+ // When above threshold is reached, delete in such a way that (threshold - numberOfRowsToCleanup) rows exists after cleanup
+ val query = """
+ DELETE FROM $tName
+ WHERE (${Column.NORMALIZED_EVENT_NAME}, ${Column.DEVICE_ID}) IN (
+ SELECT ${Column.NORMALIZED_EVENT_NAME}, ${Column.DEVICE_ID}
+ FROM $tName
+ ORDER BY ${Column.LAST_TS} ASC
+ LIMIT (
+ SELECT CASE
+ WHEN COUNT(*) > ? THEN COUNT(*) - ?
+ ELSE 0
+ END
+ FROM $tName
+ )
+ );
+ """.trimIndent()
+
+ // Execute the delete query with the threshold as an argument
+ db.writableDatabase.execSQL(query, arrayOf(threshold,numberOfRowsToKeep))
+ logger.verbose("If row count is above $threshold then only keep $numberOfRowsToKeep rows in $tName")
+ true
+ } catch (e: Exception) {
+ logger.verbose("Error cleaning up extra events in $tName.", e)
+ false
+ }
+ }
+}
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/CTJsonConverter.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/CTJsonConverter.java
index b1338a851..a313a24c4 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/CTJsonConverter.java
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/utils/CTJsonConverter.java
@@ -187,25 +187,6 @@ public static JSONArray getRenderedTargetList(DBAdapter dbAdapter) {
return renderedTargets;
}
- public static JSONObject getWzrkFields(Bundle root) throws JSONException {
- final JSONObject fields = new JSONObject();
- for (String s : root.keySet()) {
- final Object o = root.get(s);
- if (o instanceof Bundle) {
- final JSONObject wzrkFields = getWzrkFields((Bundle) o);
- final Iterator keys = wzrkFields.keys();
- while (keys.hasNext()) {
- final String k = keys.next();
- fields.put(k, wzrkFields.get(k));
- }
- } else if (s.startsWith(Constants.WZRK_PREFIX)) {
- fields.put(s, root.get(s));
- }
- }
-
- return fields;
- }
-
public static JSONObject getWzrkFields(CTInAppNotification root) throws JSONException {
final JSONObject fields = new JSONObject();
JSONObject jsonObject = root.getJsonDescription();
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/variables/Var.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/variables/Var.java
index 6f3cf4a61..a0d240b5a 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/variables/Var.java
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/variables/Var.java
@@ -2,6 +2,8 @@
import android.text.TextUtils;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
import com.clevertap.android.sdk.Logger;
import com.clevertap.android.sdk.Utils;
import com.clevertap.android.sdk.variables.callbacks.VariableCallback;
@@ -238,6 +240,7 @@ public T value() {
}
}
+ @Nullable
String rawFileValue() {
if (CTVariableUtils.FILE.equals(kind)) {
return stringValue;
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/variables/VarCache.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/variables/VarCache.java
index 830b0cad6..5a1a7ab4e 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/variables/VarCache.java
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/variables/VarCache.java
@@ -79,7 +79,7 @@ private void storeDataInCache(@NonNull String data){
try {
StorageHelper.putString(variablesCtx, cacheKey, data);
} catch (Throwable t) {
- t.printStackTrace();
+ log("storeDataInCache failed", t);
}
}
@@ -310,7 +310,7 @@ private void startFilesDownload(
downloadAllBlock -> {
// triggers global files callbacks to client
func.invoke();
- return null;
+ return Unit.INSTANCE;
}
);
}
@@ -362,8 +362,8 @@ public String filePathFromDisk(String url) {
public void fileVarUpdated(Var fileVar) {
String url = fileVar.rawFileValue();
- if (fileResourceProvider.isFileCached(url)) {
- // if present in cache
+ if (url == null || fileResourceProvider.isFileCached(url)) {
+ // if the new url is null or if it is present in the cache - trigger FileReady directly
fileVar.triggerFileIsReady();
} else {
List> list = new ArrayList<>();
@@ -372,7 +372,7 @@ public void fileVarUpdated(Var fileVar) {
list,
downloadAllBlock -> {
fileVar.triggerFileIsReady();
- return null;
+ return Unit.INSTANCE;
}
);
}
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/video/inbox/ExoplayerHandle.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/video/inbox/ExoplayerHandle.kt
index fb42e4603..4345ac81b 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/video/inbox/ExoplayerHandle.kt
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/video/inbox/ExoplayerHandle.kt
@@ -49,7 +49,7 @@ class ExoplayerHandle : InboxVideoPlayerHandle {
.build()
.apply {
volume = 0f // start off muted
- addListener(object : Player.Listener {
+ addListener(object : ExoplayerPlayerListener() {
override fun onPlaybackStateChanged(playbackState: Int) {
when (playbackState) {
Player.STATE_BUFFERING -> {
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/video/inbox/ExoplayerPlayerListener.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/video/inbox/ExoplayerPlayerListener.kt
new file mode 100644
index 000000000..465ca439e
--- /dev/null
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/video/inbox/ExoplayerPlayerListener.kt
@@ -0,0 +1,55 @@
+package com.clevertap.android.sdk.video.inbox
+
+import com.google.android.exoplayer2.*
+import com.google.android.exoplayer2.audio.AudioAttributes
+import com.google.android.exoplayer2.metadata.Metadata
+import com.google.android.exoplayer2.text.Cue
+import com.google.android.exoplayer2.text.CueGroup
+import com.google.android.exoplayer2.trackselection.TrackSelectionParameters
+import com.google.android.exoplayer2.video.VideoSize
+
+/**
+ * This class addresses an AbstractMethodError because of the Java 8 feature of default methods in interfaces.
+ * Default methods are somewhat not supported if minSDKVersion < 24
+ */
+open class ExoplayerPlayerListener : Player.Listener {
+ override fun onEvents(player: Player, events: Player.Events) {}
+ override fun onTimelineChanged(timeline: Timeline, reason: Int) {}
+ override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {}
+ override fun onTracksChanged(tracks: Tracks) {}
+ override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) {}
+ override fun onPlaylistMetadataChanged(mediaMetadata: MediaMetadata) {}
+ override fun onIsLoadingChanged(isLoading: Boolean) {}
+ override fun onAvailableCommandsChanged(availableCommands: Player.Commands) {}
+ override fun onTrackSelectionParametersChanged(parameters: TrackSelectionParameters) {}
+ override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {}
+ override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {}
+ override fun onPlaybackSuppressionReasonChanged(playbackSuppressionReason: Int) {}
+ override fun onIsPlayingChanged(isPlaying: Boolean) {}
+ override fun onRepeatModeChanged(repeatMode: Int) {}
+ override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {}
+ override fun onPlayerError(error: PlaybackException) {}
+ override fun onPlayerErrorChanged(error: PlaybackException?) {}
+ override fun onPositionDiscontinuity(reason: Int) {}
+ override fun onPositionDiscontinuity(
+ oldPosition: Player.PositionInfo,
+ newPosition: Player.PositionInfo,
+ reason: Int
+ ) {}
+ override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) {}
+ override fun onSeekBackIncrementChanged(seekBackIncrementMs: Long) {}
+ override fun onSeekForwardIncrementChanged(seekForwardIncrementMs: Long) {}
+ override fun onMaxSeekToPreviousPositionChanged(maxSeekToPreviousPositionMs: Long) {}
+ override fun onAudioSessionIdChanged(audioSessionId: Int) {}
+ override fun onAudioAttributesChanged(audioAttributes: AudioAttributes) {}
+ override fun onVolumeChanged(volume: Float) {}
+ override fun onSkipSilenceEnabledChanged(skipSilenceEnabled: Boolean) {}
+ override fun onDeviceInfoChanged(deviceInfo: DeviceInfo) {}
+ override fun onDeviceVolumeChanged(volume: Int, muted: Boolean) {}
+ override fun onVideoSizeChanged(videoSize: VideoSize) {}
+ override fun onSurfaceSizeChanged(width: Int, height: Int) {}
+ override fun onRenderedFirstFrame() {}
+ override fun onCues(cues: MutableList) {}
+ override fun onCues(cueGroup: CueGroup) {}
+ override fun onMetadata(metadata: Metadata) {}
+}
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/video/inbox/Media3Handle.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/video/inbox/Media3Handle.kt
index b4345fe1d..a47a8e1e7 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/video/inbox/Media3Handle.kt
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/video/inbox/Media3Handle.kt
@@ -46,7 +46,7 @@ class Media3Handle: InboxVideoPlayerHandle {
.build()
.apply {
volume = 0f // start off muted
- addListener(object : Player.Listener {
+ addListener(object : Media3PlayerListener() {
override fun onPlaybackStateChanged(playbackState: Int) {
when (playbackState) {
Player.STATE_BUFFERING -> {
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/video/inbox/Media3PlayerListener.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/video/inbox/Media3PlayerListener.kt
new file mode 100644
index 000000000..fca367bc4
--- /dev/null
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/video/inbox/Media3PlayerListener.kt
@@ -0,0 +1,53 @@
+package com.clevertap.android.sdk.video.inbox
+
+import androidx.media3.common.*
+import androidx.media3.common.text.Cue
+import androidx.media3.common.text.CueGroup
+import androidx.media3.common.util.UnstableApi
+
+
+@UnstableApi
+/**
+ * This class addresses an AbstractMethodError because of the Java 8 feature of default methods in interfaces.
+ * Default methods are somewhat not supported if minSDKVersion < 24
+ */
+open class Media3PlayerListener : Player.Listener {
+ override fun onSurfaceSizeChanged(width: Int, height: Int) {}
+ override fun onRenderedFirstFrame() {}
+ @Deprecated("Deprecated in Java")
+ override fun onCues(cues: MutableList) {}
+ override fun onCues(cueGroup: CueGroup) {}
+ override fun onMetadata(metadata: Metadata) {}
+ override fun onEvents(player: Player, events: Player.Events) {}
+ override fun onTimelineChanged(timeline: Timeline, reason: Int) {}
+ override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {}
+ override fun onTracksChanged(tracks: Tracks) {}
+ override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) {}
+ override fun onPlaylistMetadataChanged(mediaMetadata: MediaMetadata) {}
+ override fun onIsLoadingChanged(isLoading: Boolean) {}
+ override fun onAvailableCommandsChanged(availableCommands: Player.Commands) {}
+ override fun onTrackSelectionParametersChanged(parameters: TrackSelectionParameters) {}
+ override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {}
+ override fun onPlaybackSuppressionReasonChanged(playbackSuppressionReason: Int) {}
+ override fun onIsPlayingChanged(isPlaying: Boolean) {}
+ override fun onRepeatModeChanged(repeatMode: Int) {}
+ override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {}
+ override fun onPlayerError(error: PlaybackException) {}
+ override fun onPlayerErrorChanged(error: PlaybackException?) {}
+ override fun onPositionDiscontinuity(
+ oldPosition: Player.PositionInfo,
+ newPosition: Player.PositionInfo,
+ reason: Int
+ ) {}
+ override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) {}
+ override fun onSeekBackIncrementChanged(seekBackIncrementMs: Long) {}
+ override fun onSeekForwardIncrementChanged(seekForwardIncrementMs: Long) {}
+ override fun onMaxSeekToPreviousPositionChanged(maxSeekToPreviousPositionMs: Long) {}
+ override fun onAudioSessionIdChanged(audioSessionId: Int) {}
+ override fun onAudioAttributesChanged(audioAttributes: AudioAttributes) {}
+ override fun onVolumeChanged(volume: Float) {}
+ override fun onSkipSilenceEnabledChanged(skipSilenceEnabled: Boolean) {}
+ override fun onDeviceInfoChanged(deviceInfo: DeviceInfo) {}
+ override fun onDeviceVolumeChanged(volume: Int, muted: Boolean) {}
+ override fun onVideoSizeChanged(videoSize: VideoSize) {}
+}
diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/ActivityLifeCycleManagerTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/ActivityLifeCycleManagerTest.kt
index ee4c4d684..bbe8d5b4c 100644
--- a/clevertap-core/src/test/java/com/clevertap/android/sdk/ActivityLifeCycleManagerTest.kt
+++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/ActivityLifeCycleManagerTest.kt
@@ -34,7 +34,7 @@ class ActivityLifeCycleManagerTest : BaseTestCase() {
@Throws(Exception::class)
override fun setUp() {
super.setUp()
- coreState = MockCoreState(appCtx,cleverTapInstanceConfig)
+ coreState = MockCoreState(cleverTapInstanceConfig)
listener = mock(CTPushProviderListener::class.java)
manifestInfo = mock(ManifestInfo::class.java)
activityLifeCycleManager = ActivityLifeCycleManager(
diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/AnalyticsManagerTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/AnalyticsManagerTest.kt
index b70d9eba4..cc58b99fa 100644
--- a/clevertap-core/src/test/java/com/clevertap/android/sdk/AnalyticsManagerTest.kt
+++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/AnalyticsManagerTest.kt
@@ -1,51 +1,86 @@
package com.clevertap.android.sdk
+import android.content.Context
+import android.os.Bundle
+import com.clevertap.android.sdk.AnalyticsManagerBundler.notificationViewedJson
import com.clevertap.android.sdk.events.BaseEventQueueManager
-import com.clevertap.android.sdk.events.EventQueueManager
import com.clevertap.android.sdk.response.InAppResponse
import com.clevertap.android.sdk.task.CTExecutorFactory
import com.clevertap.android.sdk.task.MockCTExecutors
+import com.clevertap.android.sdk.utils.Clock
import com.clevertap.android.sdk.validation.ValidationResult
import com.clevertap.android.sdk.validation.ValidationResultStack
import com.clevertap.android.sdk.validation.Validator
import com.clevertap.android.sdk.validation.Validator.ValidationContext.Profile
-import com.clevertap.android.shared.test.BaseTestCase
+import io.mockk.MockKAnnotations
+import io.mockk.called
+import io.mockk.clearAllMocks
+import io.mockk.clearStaticMockk
+import io.mockk.confirmVerified
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.mockkStatic
+import io.mockk.verify
import org.json.JSONArray
import org.json.JSONObject
-import org.junit.*
-import org.junit.runner.*
-import org.mockito.*
-import org.mockito.Mockito.*
-import org.mockito.Mockito.any
-import org.mockito.Mockito.never
-import org.mockito.Mockito.times
-import org.mockito.Mockito.verify
-import org.mockito.kotlin.*
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.skyscreamer.jsonassert.JSONAssert
import kotlin.test.assertEquals
@RunWith(RobolectricTestRunner::class)
-class AnalyticsManagerTest : BaseTestCase() {
+class AnalyticsManagerTest {
private lateinit var analyticsManagerSUT: AnalyticsManager
- private lateinit var coreState: MockCoreState
+ private lateinit var coreState: MockCoreStateKotlin
+ private val cleverTapInstanceConfig = CleverTapFixtures.provideCleverTapInstanceConfig()
+
+ @MockK(relaxed = true)
private lateinit var validator: Validator
+
+ @MockK(relaxed = true)
private lateinit var validationResultStack: ValidationResultStack
- private lateinit var baseEventQueueManager: BaseEventQueueManager
+
+ @MockK(relaxed = true)
+ private lateinit var eventQueueManager: BaseEventQueueManager
+
+ @MockK(relaxed = true)
+ private lateinit var context: Context
+
+ @MockK(relaxed = true)
+ private lateinit var inAppResponse: InAppResponse
+
+ @MockK(relaxed = true)
+ private lateinit var timeProvider: Clock
+
+ private val bundleIdCheck = Bundle().apply {
+ putString("wzrk_pn", "wzrk_pn")
+ putString("wzrk_id", "duplicate-id")
+ putString("wzrk_pid", "pid")
+ putString("wzrk_someid", "someid")
+ }
+
+ private val bundlePidCheck = Bundle().apply {
+ putString("wzrk_pn", "wzrk_pn")
+ putString("wzrk_id", "duplicate-id")
+ putString("wzrk_pid", "pid")
+ putString("wzrk_someid", "someid")
+ putBoolean("wzrk_dd", true)
+ }
@Before
- override fun setUp() {
- super.setUp()
- validator = mock(Validator::class.java)
- validationResultStack = mock(ValidationResultStack::class.java)
- baseEventQueueManager = mock(EventQueueManager::class.java)
- val inAppResponse = mock(InAppResponse::class.java)
- coreState = MockCoreState(application, cleverTapInstanceConfig)
+ fun setUp() {
+ MockKAnnotations.init(this)
+ mockkStatic(CTExecutorFactory::class)
+ every { CTExecutorFactory.executors(any()) } returns MockCTExecutors(cleverTapInstanceConfig)
+ coreState = MockCoreStateKotlin(cleverTapInstanceConfig)
analyticsManagerSUT = AnalyticsManager(
- application,
+ context,
cleverTapInstanceConfig,
- baseEventQueueManager,
+ eventQueueManager,
validator,
validationResultStack,
coreState.coreMetaData,
@@ -53,22 +88,317 @@ class AnalyticsManagerTest : BaseTestCase() {
coreState.callbackManager,
coreState.controllerManager,
coreState.ctLockManager,
- inAppResponse
+ inAppResponse,
+ timeProvider
)
}
+ @After
+ fun tearDown() {
+ // confirmVerified(validator, validationResultStack, eventQueueManager, context, inAppResponse)
+ clearStaticMockk(CTExecutorFactory::class)
+ clearAllMocks()
+ }
+
+ @Test
+ fun `clevertap does not process push notification viewed or clicked event if it is not from clevertap`() {
+ val bundle = Bundle().apply {
+ putString("some", "random")
+ putString("non clevertap", "bundle")
+ }
+
+ analyticsManagerSUT.pushNotificationViewedEvent(bundle)
+ analyticsManagerSUT.pushNotificationClickedEvent(bundle)
+
+ verify {
+ eventQueueManager wasNot called
+ }
+ confirmVerified(eventQueueManager)
+ }
+
+ @Test
+ fun `clevertap does not process push notification viewed event if wzrk_id is not present`() {
+ val bundle = Bundle().apply {
+ putString("some", "random")
+ putString("non clevertap", "bundle")
+ putString("wzrk_pid", "pid")
+ }
+
+ analyticsManagerSUT.pushNotificationViewedEvent(bundle)
+ analyticsManagerSUT.pushNotificationClickedEvent(bundle)
+ verify {
+ eventQueueManager wasNot called
+ }
+ confirmVerified(eventQueueManager)
+ }
+
+ @Test
+ fun `clevertap does not process duplicate PN viewed within 2 seconds - case 2nd notif in 200ms`() {
+ val json = notificationViewedJson(bundleIdCheck)
+
+ every { timeProvider.currentTimeMillis() } returns 10000
+
+ // send PN first time
+ analyticsManagerSUT.pushNotificationViewedEvent(bundleIdCheck)
+
+ verify {
+ eventQueueManager.queueEvent(
+ context,
+ withArg {
+ JSONAssert.assertEquals(json, it, true)
+ },
+ Constants.NV_EVENT
+ )
+ }
+
+ // setup again, 200 ms has passed
+ every { timeProvider.currentTimeMillis() } returns 10200
+
+ // Send duplicate PN
+ analyticsManagerSUT.pushNotificationViewedEvent(bundleIdCheck)
+
+ // verify it was not called again, one time was from before
+ verify(exactly = 1) {
+ eventQueueManager.queueEvent(
+ context,
+ withArg {
+ JSONAssert.assertEquals(json, it, true)
+ },
+ Constants.NV_EVENT
+ )
+ }
+ confirmVerified(eventQueueManager)
+ }
+
+ @Test
+ fun `clevertap processes PN viewed for same wzrk_id if separated by a span of greater than 2 seconds`() {
+
+ val json = notificationViewedJson(bundleIdCheck);
+
+ every { timeProvider.currentTimeMillis() } returns 10000
+
+ // send PN first time
+ analyticsManagerSUT.pushNotificationViewedEvent(bundleIdCheck)
+
+ verify(exactly = 1) {
+ eventQueueManager.queueEvent(
+ context,
+ withArg {
+ JSONAssert.assertEquals(json, it, true)
+ },
+ Constants.NV_EVENT
+ )
+ }
+
+ // setup again, 10000 ms has passed
+ every { timeProvider.currentTimeMillis() } returns 20000
+
+ // Send duplicate PN
+ analyticsManagerSUT.pushNotificationViewedEvent(bundleIdCheck)
+
+ // verify queue event called again
+ verify(exactly = 2) {
+ eventQueueManager.queueEvent(
+ context,
+ withArg {
+ JSONAssert.assertEquals(json, it, true)
+ },
+ Constants.NV_EVENT
+ )
+ }
+ confirmVerified(eventQueueManager)
+ }
+
+ @Test
+ fun `clevertap does not process PN Clicked if SDK is set to analytics only`() {
+ cleverTapInstanceConfig.isAnalyticsOnly = true
+
+ // send PN first time
+ analyticsManagerSUT.pushNotificationClickedEvent(bundleIdCheck)
+
+ verify {
+ eventQueueManager wasNot called
+ }
+ confirmVerified(eventQueueManager)
+ }
+
+ @Test
+ fun `clevertap does not process duplicate (same wzrk_id) PN clicked within 2 seconds - case 2nd click happens in 200ms`() {
+
+ val json = notificationViewedJson(bundleIdCheck)
+ every { timeProvider.currentTimeMillis() } returns 0
+
+ // send PN first time
+ analyticsManagerSUT.pushNotificationClickedEvent(bundleIdCheck)
+
+ verify(exactly = 1) {
+ eventQueueManager.queueEvent(
+ context,
+ withArg {
+ JSONAssert.assertEquals(json, it, true)
+ },
+ Constants.RAISED_EVENT
+ )
+ }
+
+ // setup again, 2000 ms has passed
+ every { timeProvider.currentTimeMillis() } returns 200
+
+ // Send duplicate PN
+ analyticsManagerSUT.pushNotificationClickedEvent(bundleIdCheck)
+
+ // verify it was not called again, one time was from before
+ verify(exactly = 1) {
+ eventQueueManager.queueEvent(
+ context,
+ withArg {
+ JSONAssert.assertEquals(json, it, true)
+ },
+ Constants.RAISED_EVENT
+ )
+ }
+ confirmVerified(eventQueueManager)
+ }
+
+ @Test
+ fun `clevertap processes PN clicked for same wzrk_id if separated by a span of greater than 5 seconds`() {
+
+ val json = notificationViewedJson(bundleIdCheck);
+ every { timeProvider.currentTimeMillis() } returns 10000
+
+ // send PN first time
+ analyticsManagerSUT.pushNotificationClickedEvent(bundleIdCheck)
+
+ verify(exactly = 1) {
+ eventQueueManager.queueEvent(
+ context,
+ withArg {
+ JSONAssert.assertEquals(json, it, true)
+ },
+ Constants.RAISED_EVENT
+ )
+ }
+
+ every { timeProvider.currentTimeMillis() } returns 15001
+
+ // Send duplicate PN
+ analyticsManagerSUT.pushNotificationClickedEvent(bundleIdCheck)
+
+ // verify queue event called again
+ verify(exactly = 2) {
+ eventQueueManager.queueEvent(
+ context,
+ withArg {
+ JSONAssert.assertEquals(json, it, true)
+ },
+ Constants.RAISED_EVENT
+ )
+ }
+ confirmVerified(eventQueueManager)
+ }
+
+ @Test
+ fun `dedupeCheckKey used wzrk_id incase wzrk_dd key is false or not present`() {
+ val key1 = analyticsManagerSUT.dedupeCheckKey(bundleIdCheck)
+ assertEquals("duplicate-id", key1)
+
+ val bundleIdCheckKeyFalse = Bundle().apply {
+ putString("wzrk_pn", "wzrk_pn")
+ putString("wzrk_id", "duplicate-id")
+ putString("wzrk_pid", "pid")
+ putString("wzrk_someid", "someid")
+ putString("wzrk_dd", "false")
+ }
+
+ val key2 = analyticsManagerSUT.dedupeCheckKey(bundleIdCheckKeyFalse)
+ assertEquals("duplicate-id", key2)
+ }
+
+ @Test
+ fun `dedupeCheckKey used wzrk_pid incase wzrk_dd key is true string or boolean`() {
+
+ val key1 = analyticsManagerSUT.dedupeCheckKey(bundlePidCheck)
+ assertEquals("pid", key1)
+
+ val bundlePidCheckString = Bundle().apply {
+ putString("wzrk_pn", "wzrk_pn")
+ putString("wzrk_id", "duplicate-id")
+ putString("wzrk_pid", "pid")
+ putString("wzrk_someid", "someid")
+ putString("wzrk_dd", "TRUE")
+ }
+
+ val key2 = analyticsManagerSUT.dedupeCheckKey(bundlePidCheckString)
+ assertEquals("pid", key2)
+ }
+
+ @Test
+ fun `clevertap dedupe check is based on wzrk_pid only if flag (wzrk_dd) is enabled`() {
+
+ // Setup
+ val notif1 = Bundle().apply {
+ putString("wzrk_pn", "wzrk_pn")
+ putString("wzrk_id", "wzrk_id_1111")
+ putString("wzrk_someid", "someid1111")
+ putString("wzrk_dd", "true")
+
+ putString("wzrk_pid", "same_pid")
+ }
+
+ val notif2 = Bundle().apply {
+ putString("wzrk_pn", "wzrk_pn")
+ putString("wzrk_id", "wzrk_id_2222")
+ putString("wzrk_someid", "someid2222")
+ putString("wzrk_dd", "true")
+
+ putString("wzrk_pid", "same_pid")
+ }
+
+ val json1 = notificationViewedJson(notif1)
+ val json2 = notificationViewedJson(notif1)
+
+ every { timeProvider.currentTimeMillis() } returns 0
+
+ // ACT : send PN first time
+ analyticsManagerSUT.pushNotificationClickedEvent(notif1)
+
+ // Validate
+ verify(exactly = 1) {
+ eventQueueManager.queueEvent(
+ context,
+ withArg {
+ JSONAssert.assertEquals(json1, it, true)
+ },
+ Constants.RAISED_EVENT
+ )
+ }
+
+ // More setup, 100ms passed
+ every { timeProvider.currentTimeMillis() } returns 100
+
+ // ACT : send PN second time
+ analyticsManagerSUT.pushNotificationClickedEvent(notif2)
+
+ // Validate
+ confirmVerified(eventQueueManager)
+ }
+
@Test
fun test_incrementValue_nullKey_noAction() {
analyticsManagerSUT.incrementValue(null, 10)
- verifyNoInteractions(validator)
+ verify {
+ validator wasNot called
+ }
}
@Test
fun test_incrementValue_nullValue_noAction() {
analyticsManagerSUT.incrementValue("abc", null)
- verifyNoInteractions(validator)
+ verify {
+ validator wasNot called
+ }
}
@Test
@@ -76,10 +406,11 @@ class AnalyticsManagerTest : BaseTestCase() {
mockCleanObjectKey("", 0)
analyticsManagerSUT.incrementValue("", 10)
- val captor = ArgumentCaptor.forClass(ValidationResult::class.java)
- verify(validationResultStack).pushValidationResult(captor.capture())
-
- assertEquals(512, captor.value.errorCode)
+ verify {
+ validationResultStack.pushValidationResult(withArg { arg ->
+ assertEquals(512, arg.errorCode)
+ })
+ }
}
@Test
@@ -87,10 +418,11 @@ class AnalyticsManagerTest : BaseTestCase() {
mockCleanObjectKey("abc", 0)
analyticsManagerSUT.decrementValue("abc", -10)
- val captor = ArgumentCaptor.forClass(ValidationResult::class.java)
- verify(validationResultStack).pushValidationResult(captor.capture())
-
- assertEquals(512, captor.value.errorCode)
+ verify {
+ validationResultStack.pushValidationResult(withArg { arg ->
+ assertEquals(512, arg.errorCode)
+ })
+ }
}
@Test
@@ -100,15 +432,16 @@ class AnalyticsManagerTest : BaseTestCase() {
val commandObj: JSONObject = JSONObject().put(Constants.COMMAND_INCREMENT, 10)
val updateObj = JSONObject().put("int_score", commandObj)
- val captor = ArgumentCaptor.forClass(JSONObject::class.java)
-
- `when`(coreState.localDataStore.getProfileProperty("int_score"))
- .thenReturn(10)
+ every { coreState.localDataStore.getProfileProperty("int_score") } returns 10
analyticsManagerSUT.incrementValue("int_score", 10)
- verify(baseEventQueueManager).pushBasicProfile(captor.capture(), anyBoolean())
- JSONAssert.assertEquals(updateObj, captor.value, true)
+ verify {
+ eventQueueManager.pushBasicProfile(
+ withArg { JSONAssert.assertEquals(updateObj, it, true) },
+ any()
+ )
+ }
}
@Test
@@ -117,15 +450,17 @@ class AnalyticsManagerTest : BaseTestCase() {
val commandObj: JSONObject = JSONObject().put(Constants.COMMAND_INCREMENT, 10.25)
val updateObj = JSONObject().put("double_score", commandObj)
- val captor = ArgumentCaptor.forClass(JSONObject::class.java)
- `when`(coreState.localDataStore.getProfileProperty("double_score"))
- .thenReturn(10.25)
+ every { coreState.localDataStore.getProfileProperty("double_score") } returns (10.25)
analyticsManagerSUT.incrementValue("double_score", 10.25)
- verify(baseEventQueueManager).pushBasicProfile(captor.capture(), anyBoolean())
- JSONAssert.assertEquals(updateObj, captor.value, true)
+ verify {
+ eventQueueManager.pushBasicProfile(
+ withArg { JSONAssert.assertEquals(updateObj, it, true) },
+ any()
+ )
+ }
}
@Test
@@ -134,29 +469,37 @@ class AnalyticsManagerTest : BaseTestCase() {
val commandObj: JSONObject = JSONObject().put(Constants.COMMAND_INCREMENT, 10.25f)
val updateObj = JSONObject().put("float_score", commandObj)
- val captor = ArgumentCaptor.forClass(JSONObject::class.java)
- `when`(coreState.localDataStore.getProfileProperty("float_score"))
- .thenReturn(10.25f)
+ every {
+ coreState.localDataStore.getProfileProperty("float_score")
+ } returns 10.25f
analyticsManagerSUT.incrementValue("float_score", 10.25f)
- verify(baseEventQueueManager).pushBasicProfile(captor.capture(), anyBoolean())
- JSONAssert.assertEquals(updateObj, captor.value, true)
+ verify {
+ eventQueueManager.pushBasicProfile(
+ withArg { JSONAssert.assertEquals(updateObj, it, true) },
+ any()
+ )
+ }
}
@Test
fun test_decrementValue_nullValue_noAction() {
analyticsManagerSUT.decrementValue("abc", null)
- verifyNoInteractions(validator)
+ verify {
+ validator wasNot called
+ }
}
@Test
fun test_decrementValue_nullKey_noAction() {
analyticsManagerSUT.decrementValue(null, 10)
- verifyNoInteractions(validator)
+ verify {
+ validator wasNot called
+ }
}
@Test
@@ -166,15 +509,18 @@ class AnalyticsManagerTest : BaseTestCase() {
val commandObj: JSONObject = JSONObject().put(Constants.COMMAND_DECREMENT, 10)
val updateObj = JSONObject().put("decr_int_score", commandObj)
- val captor = ArgumentCaptor.forClass(JSONObject::class.java)
-
- `when`(coreState.localDataStore.getProfileProperty("decr_int_score"))
- .thenReturn(30)
+ every {
+ coreState.localDataStore.getProfileProperty("decr_int_score")
+ } returns 30
analyticsManagerSUT.decrementValue("decr_int_score", 10)
- verify(baseEventQueueManager).pushBasicProfile(captor.capture(), anyBoolean())
- JSONAssert.assertEquals(updateObj, captor.value, true)
+ verify {
+ eventQueueManager.pushBasicProfile(
+ withArg { JSONAssert.assertEquals(updateObj, it, true) },
+ any()
+ )
+ }
}
@Test
@@ -184,15 +530,18 @@ class AnalyticsManagerTest : BaseTestCase() {
val commandObj: JSONObject = JSONObject().put(Constants.COMMAND_DECREMENT, 10.50)
val updateObj = JSONObject().put("decr_double_score", commandObj)
- val captor = ArgumentCaptor.forClass(JSONObject::class.java)
-
- `when`(coreState.localDataStore.getProfileProperty("decr_double_score"))
- .thenReturn(20.25)
+ every {
+ coreState.localDataStore.getProfileProperty("decr_double_score")
+ } returns 20.25
analyticsManagerSUT.decrementValue("decr_double_score", 10.50)
- verify(baseEventQueueManager).pushBasicProfile(captor.capture(), anyBoolean())
- JSONAssert.assertEquals(updateObj, captor.value, true)
+ verify {
+ eventQueueManager.pushBasicProfile(
+ withArg { JSONAssert.assertEquals(updateObj, it, true) },
+ any()
+ )
+ }
}
@Test
@@ -202,70 +551,78 @@ class AnalyticsManagerTest : BaseTestCase() {
val commandObj: JSONObject = JSONObject().put(Constants.COMMAND_DECREMENT, 10.50f)
val updateObj = JSONObject().put("decr_float_score", commandObj)
- val captor = ArgumentCaptor.forClass(JSONObject::class.java)
-
- `when`(coreState.localDataStore.getProfileProperty("decr_float_score"))
- .thenReturn(20.25f)
+ every {
+ coreState.localDataStore.getProfileProperty("decr_float_score")
+ } returns 20.25f
analyticsManagerSUT.decrementValue("decr_float_score", 10.50f)
- verify(baseEventQueueManager).pushBasicProfile(captor.capture(), anyBoolean())
- JSONAssert.assertEquals(updateObj, captor.value, true)
+ verify {
+ eventQueueManager.pushBasicProfile(
+ withArg { JSONAssert.assertEquals(updateObj, it, true) },
+ any()
+ )
+ }
}
@Test
fun test_removeValueForKey_when_key_identity() {
//Act
- analyticsManagerSUT.removeValueForKey("Identity")
+ val key = "Identity"
- //Assert
- verify(baseEventQueueManager, never()).pushBasicProfile(any(), anyBoolean())
+ mockCleanObjectKey(key, 0)
+
+ analyticsManagerSUT.removeValueForKey(key)
+
+ // Verify
+ verify(exactly = 0) {
+ eventQueueManager.pushBasicProfile(any(), any())
+ }
}
@Test
fun test_removeValueForKey_when_key_identity_is_lowercase() {
+
+ val key = "identity"
+
+ mockCleanObjectKey(key, 0)
+
//Act
- analyticsManagerSUT.removeValueForKey("identity")
+ analyticsManagerSUT.removeValueForKey(key)
- //Assert
- verify(baseEventQueueManager, never()).pushBasicProfile(any(), anyBoolean())
+ // Assert
+ // Verify
+ verify(exactly = 0) {
+ eventQueueManager.pushBasicProfile(any(), any())
+ }
}
@Test
fun test_removeValueForKey_when_NullKey_pushesEmptyKeyError() {
mockCleanObjectKey("", 0)
- //Act
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(cleverTapInstanceConfig)
- )
- analyticsManagerSUT.removeValueForKey(null)
- }
+ analyticsManagerSUT.removeValueForKey(null)
- //Assert
- val captor = ArgumentCaptor.forClass(ValidationResult::class.java)
- verify(validationResultStack).pushValidationResult(captor.capture())
- assertEquals(512, captor.value.errorCode)
+ verify {
+ validationResultStack.pushValidationResult(withArg {
+ assertEquals(512, it.errorCode)
+ })
+ }
}
@Test
fun test_removeValueForKey_when_EmptyKey_pushesEmptyKeyError() {
mockCleanObjectKey("", 0)
- //Act
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(cleverTapInstanceConfig)
- )
analyticsManagerSUT.removeValueForKey("")
- }
- //Assert
- val captor = ArgumentCaptor.forClass(ValidationResult::class.java)
- verify(validationResultStack).pushValidationResult(captor.capture())
- assertEquals(512, captor.value.errorCode)
+ // Assert
+ verify {
+ validationResultStack.pushValidationResult(withArg {
+ assertEquals(512, it.errorCode)
+ })
+ }
}
@Test
@@ -274,33 +631,27 @@ class AnalyticsManagerTest : BaseTestCase() {
val commandObj: JSONObject = JSONObject().put(Constants.COMMAND_DELETE, true)
val updateObj = JSONObject().put("abc", commandObj)
- val captor = ArgumentCaptor.forClass(JSONObject::class.java)
-
mockCleanObjectKey("abc", 0)
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(cleverTapInstanceConfig)
+ analyticsManagerSUT.removeValueForKey("abc")
+
+ verify {
+ eventQueueManager.pushBasicProfile(
+ withArg { JSONAssert.assertEquals(updateObj, it, true) },
+ any()
)
- analyticsManagerSUT.removeValueForKey("abc")
}
-
- verify(baseEventQueueManager).pushBasicProfile(captor.capture(), anyBoolean())
- JSONAssert.assertEquals(updateObj, captor.value, true)
}
@Test
fun test_addMultiValuesForKey_when_NullKey_noAction() {
- //Act
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(cleverTapInstanceConfig)
- )
- analyticsManagerSUT.addMultiValuesForKey(null, arrayListOf("a"))
- }
+ analyticsManagerSUT.addMultiValuesForKey(null, arrayListOf("a"))
//Assert
- verify(baseEventQueueManager, never()).pushBasicProfile(any(), anyBoolean())
+ // Verify
+ verify {
+ eventQueueManager wasNot called
+ }
}
@Test
@@ -309,18 +660,14 @@ class AnalyticsManagerTest : BaseTestCase() {
validationResult.`object` = ""
validationResult.errorCode = 512
- //Act
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(cleverTapInstanceConfig)
- )
- analyticsManagerSUT.addMultiValuesForKey("abc", null)
- }
+ analyticsManagerSUT.addMultiValuesForKey("abc", null)
//Assert
- val captor = ArgumentCaptor.forClass(ValidationResult::class.java)
- verify(validationResultStack).pushValidationResult(captor.capture())
- assertEquals(validationResult.errorCode, captor.value.errorCode)
+ verify {
+ validationResultStack.pushValidationResult(withArg {
+ assertEquals(validationResult.errorCode, it.errorCode)
+ })
+ }
}
@Test
@@ -329,18 +676,14 @@ class AnalyticsManagerTest : BaseTestCase() {
validationResult.`object` = ""
validationResult.errorCode = 512
- //Act
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(cleverTapInstanceConfig)
- )
- analyticsManagerSUT.addMultiValuesForKey("abc", arrayListOf())
- }
+ analyticsManagerSUT.addMultiValuesForKey("abc", arrayListOf())
//Assert
- val captor = ArgumentCaptor.forClass(ValidationResult::class.java)
- verify(validationResultStack).pushValidationResult(captor.capture())
- assertEquals(validationResult.errorCode, captor.value.errorCode)
+ verify {
+ validationResultStack.pushValidationResult(withArg {
+ assertEquals(validationResult.errorCode, it.errorCode)
+ })
+ }
}
@Test
@@ -348,63 +691,55 @@ class AnalyticsManagerTest : BaseTestCase() {
val validationResult = ValidationResult()
validationResult.`object` = null
validationResult.errorCode = 523
- `when`(validator.cleanMultiValuePropertyKey("Name"))
- .thenReturn(validationResult)
+ every {
+ validator.cleanMultiValuePropertyKey("Name")
+ } returns validationResult
- //Act
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(cleverTapInstanceConfig)
- )
- analyticsManagerSUT.addMultiValuesForKey("Name", arrayListOf("a"))
- }
+ // Act
+ analyticsManagerSUT.addMultiValuesForKey("Name", arrayListOf("a"))
- //Assert
- val captor = ArgumentCaptor.forClass(ValidationResult::class.java)
- verify(validationResultStack, times(2)).pushValidationResult(captor.capture())
- assertEquals(523, captor.firstValue.errorCode)
- assertEquals(523, captor.secondValue.errorCode)
+ // Check
+ verify(exactly = 2) {
+ validationResultStack.pushValidationResult(withArg {
+ assertEquals(523, it.errorCode)
+ })
+ }
}
@Test
fun test_addMultiValuesForKey_when_EmptyKey_emptyValueError() {
mockCleanMultiValuePropertyKey("", 0)
- //Act
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(cleverTapInstanceConfig)
- )
analyticsManagerSUT.addMultiValuesForKey("", arrayListOf("a"))
- }
- //Assert
- val captor = ArgumentCaptor.forClass(ValidationResult::class.java)
- verify(validationResultStack).pushValidationResult(captor.capture())
- assertEquals(523, captor.firstValue.errorCode)
+ // Assert
+ verify {
+ validationResultStack.pushValidationResult(withArg {
+ assertEquals(523, it.errorCode)
+ })
+ }
}
@Test
fun test_addMultiValuesForKey_when_CorrectKey_pushesBasicProfile() {
- val commandObj = JSONObject()
- commandObj.put(Constants.COMMAND_ADD, JSONArray(arrayListOf("a")))
- val fields = JSONObject()
- fields.put("abc", commandObj)
+ val commandObj = JSONObject().apply {
+ put(Constants.COMMAND_ADD, JSONArray(arrayListOf("a")))
+ }
+ val fields = JSONObject().apply {
+ put("abc", commandObj)
+ }
mockCleanMultiValuePropertyKey("abc", 0)
- //Act
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(cleverTapInstanceConfig)
+ analyticsManagerSUT.addMultiValuesForKey("abc", arrayListOf("a"))
+
+ // Assert
+ verify {
+ eventQueueManager.pushBasicProfile(
+ withArg { JSONAssert.assertEquals(fields, it, true) },
+ any()
)
- analyticsManagerSUT.addMultiValuesForKey("abc", arrayListOf("a"))
}
-
- //Assert
- val captor = ArgumentCaptor.forClass(JSONObject::class.java)
- verify(baseEventQueueManager).pushBasicProfile(captor.capture(), anyBoolean())
- JSONAssert.assertEquals(fields, captor.value, true)
}
@Test
@@ -416,18 +751,15 @@ class AnalyticsManagerTest : BaseTestCase() {
mockCleanMultiValuePropertyKey("abc", 0)
- //Act
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(cleverTapInstanceConfig)
+ analyticsManagerSUT.removeMultiValuesForKey("abc", arrayListOf("a"))
+
+ // Assert
+ verify {
+ eventQueueManager.pushBasicProfile(
+ withArg { JSONAssert.assertEquals(fields, it, true) },
+ any()
)
- analyticsManagerSUT.removeMultiValuesForKey("abc", arrayListOf("a"))
}
-
- //Assert
- val captor = ArgumentCaptor.forClass(JSONObject::class.java)
- verify(baseEventQueueManager).pushBasicProfile(captor.capture(), anyBoolean())
- JSONAssert.assertEquals(fields, captor.value, true)
}
@Test
@@ -441,63 +773,51 @@ class AnalyticsManagerTest : BaseTestCase() {
mockCleanMultiValuePropertyKey("abc", 0)
- //Act
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(cleverTapInstanceConfig)
- )
- analyticsManagerSUT.setMultiValuesForKey("abc", arrayListOf("a"))
- }
+ analyticsManagerSUT.setMultiValuesForKey("abc", arrayListOf("a"))
//Assert
- val captor = ArgumentCaptor.forClass(JSONObject::class.java)
- verify(baseEventQueueManager).pushBasicProfile(captor.capture(), anyBoolean())
- JSONAssert.assertEquals(fields, captor.value, true)
+ verify {
+ eventQueueManager.pushBasicProfile(
+ withArg { JSONAssert.assertEquals(fields, it, true) },
+ any()
+ )
+ }
}
@Test
fun test_pushProfile_when_nullProfile_noAction() {
- //Act
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(cleverTapInstanceConfig)
- )
- analyticsManagerSUT.pushProfile(null)
- }
+ analyticsManagerSUT.pushProfile(null)
//Assert
- verifyNoInteractions(validator)
+ verify {
+ validator wasNot called
+ }
}
@Test
fun test_pushProfile_when_emptyProfile_noAction() {
- //Act
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(cleverTapInstanceConfig)
- )
- analyticsManagerSUT.pushProfile(emptyMap())
- }
+ analyticsManagerSUT.pushProfile(emptyMap())
//Assert
- verifyNoInteractions(validator)
+ verify {
+ validator wasNot called
+ }
}
@Test
fun test_pushProfile_when_nullDeviceId_noAction() {
val profile = mapOf("key1" to "value1", "key2" to "value2")
- `when`(coreState.deviceInfo.deviceID).thenReturn(null)
+ every {
+ coreState.deviceInfo.deviceID
+ } returns null
- //Act
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(cleverTapInstanceConfig)
- )
- analyticsManagerSUT.pushProfile(profile)
- }
+ // Act
+ analyticsManagerSUT.pushProfile(profile)
- //Assert
- verifyNoInteractions(validator)
+ // Verify
+ verify {
+ validator wasNot called
+ }
}
@@ -507,24 +827,22 @@ class AnalyticsManagerTest : BaseTestCase() {
val validPhone = "+1234"
val profile = mapOf("Phone" to validPhone)
- `when`(coreState.deviceInfo.deviceID).thenReturn("1234")
+ every {
+ coreState.deviceInfo.deviceID
+ } returns "1234"
mockCleanObjectKey("Phone", 0)
mockCleanObjectValue(validPhone, 0)
- //Act
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(cleverTapInstanceConfig)
+ analyticsManagerSUT.pushProfile(profile)
+
+ // Checks
+ verify {
+ eventQueueManager.pushBasicProfile(
+ withArg { JSONAssert.assertEquals(JSONObject().put("Phone", validPhone), it, true) },
+ any()
)
- analyticsManagerSUT.pushProfile(profile)
}
-
- //Assert
- val basicProfileCaptor = ArgumentCaptor.forClass(JSONObject::class.java)
-
- verify(baseEventQueueManager).pushBasicProfile(basicProfileCaptor.capture(), anyBoolean())
- JSONAssert.assertEquals(JSONObject().put("Phone", validPhone), basicProfileCaptor.firstValue, true)
}
@Test
@@ -536,23 +854,21 @@ class AnalyticsManagerTest : BaseTestCase() {
mockCleanObjectValue(invalidPhone, 0)
- `when`(coreState.deviceInfo.deviceID).thenReturn("1234")
- //Act
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(cleverTapInstanceConfig)
- )
- analyticsManagerSUT.pushProfile(profile)
- }
+ every { coreState.deviceInfo.deviceID } returns "1234"
- //Assert
- val validationResultCaptor = ArgumentCaptor.forClass(ValidationResult::class.java)
- val basicProfileCaptor = ArgumentCaptor.forClass(JSONObject::class.java)
+ // Act
+ analyticsManagerSUT.pushProfile(profile)
- verify(validationResultStack).pushValidationResult(validationResultCaptor.capture())
- verify(baseEventQueueManager).pushBasicProfile(basicProfileCaptor.capture(), anyBoolean())
- assertEquals(512, validationResultCaptor.firstValue.errorCode)
- JSONAssert.assertEquals(JSONObject().put("Phone", invalidPhone), basicProfileCaptor.firstValue, true)
+ // Checks
+ verify {
+ validationResultStack.pushValidationResult(withArg { assertEquals(512, it.errorCode) })
+ }
+ verify {
+ eventQueueManager.pushBasicProfile(
+ withArg { JSONAssert.assertEquals(JSONObject().put("Phone", invalidPhone), it, true) },
+ any()
+ )
+ }
}
@Test
@@ -565,24 +881,21 @@ class AnalyticsManagerTest : BaseTestCase() {
mockCleanObjectValue("value1", 0)
mockCleanObjectValue("value2", 0)
- `when`(coreState.deviceInfo.deviceID).thenReturn("1234")
+ every { coreState.deviceInfo.deviceID } returns "1234"
- //Act
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(cleverTapInstanceConfig)
+ // Act
+ analyticsManagerSUT.pushProfile(profile)
+
+ // Checks
+ verify {
+ validationResultStack.pushValidationResult(withArg { assertEquals(512, it.errorCode) })
+ }
+ verify {
+ eventQueueManager.pushBasicProfile(
+ withArg { JSONAssert.assertEquals(JSONObject().put("key1", "value1"), it, true) },
+ any()
)
- analyticsManagerSUT.pushProfile(profile)
}
-
- //Assert
- val validationResultCaptor = ArgumentCaptor.forClass(ValidationResult::class.java)
- val basicProfileCaptor = ArgumentCaptor.forClass(JSONObject::class.java)
-
- verify(validationResultStack).pushValidationResult(validationResultCaptor.capture())
- verify(baseEventQueueManager).pushBasicProfile(basicProfileCaptor.capture(), anyBoolean())
- assertEquals(512, validationResultCaptor.value.errorCode)
- JSONAssert.assertEquals(JSONObject().put("key1", "value1"), basicProfileCaptor.firstValue, true)
}
@Test
@@ -592,28 +905,24 @@ class AnalyticsManagerTest : BaseTestCase() {
mockCleanObjectKey("key1", 0)
mockCleanObjectKey("key2", 0)
- `when`(coreState.deviceInfo.deviceID).thenReturn("1234")
+ every { coreState.deviceInfo.deviceID } returns "1234"
- `when`(validator.cleanObjectValue(any(Validator::class.java), any()))
- .thenThrow(IllegalArgumentException())
+ every { validator.cleanObjectValue(any(), any()) }throws IllegalArgumentException()
mockCleanObjectValue("value2", 0)
- //Act
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(cleverTapInstanceConfig)
+ // Act
+ analyticsManagerSUT.pushProfile(profile)
+
+ // Checks
+ verify {
+ validationResultStack.pushValidationResult(withArg { assertEquals(512, it.errorCode) })
+ }
+ verify {
+ eventQueueManager.pushBasicProfile(
+ withArg { JSONAssert.assertEquals(JSONObject().put("key2", "value2"), it, true) },
+ any()
)
- analyticsManagerSUT.pushProfile(profile)
}
-
- //Assert
- val validationResultCaptor = ArgumentCaptor.forClass(ValidationResult::class.java)
- val basicProfileCaptor = ArgumentCaptor.forClass(JSONObject::class.java)
-
- verify(validationResultStack).pushValidationResult(validationResultCaptor.capture())
- verify(baseEventQueueManager).pushBasicProfile(basicProfileCaptor.capture(), anyBoolean())
- assertEquals(512, validationResultCaptor.value.errorCode)
- JSONAssert.assertEquals(JSONObject().put("key2", "value2"), basicProfileCaptor.firstValue, true)
}
@Test
@@ -625,48 +934,47 @@ class AnalyticsManagerTest : BaseTestCase() {
mockCleanObjectValue("value1", 0)
mockCleanObjectValue("value2", 0)
- `when`(coreState.deviceInfo.deviceID).thenReturn("1234")
+ every { coreState.deviceInfo.deviceID } returns "1234"
- //Act
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(cleverTapInstanceConfig)
+ // Act
+ analyticsManagerSUT.pushProfile(profile)
+
+ // Verify
+ verify {
+ eventQueueManager.pushBasicProfile(
+ withArg { JSONAssert.assertEquals(JSONObject().put("key1", "value1").put("key2", "value2"), it, true) },
+ any()
)
- analyticsManagerSUT.pushProfile(profile)
}
-
- //Assert
- val basicProfileCaptor = ArgumentCaptor.forClass(JSONObject::class.java)
-
- verify(baseEventQueueManager).pushBasicProfile(basicProfileCaptor.capture(), anyBoolean())
- JSONAssert.assertEquals(
- JSONObject().put("key1", "value1").put("key2", "value2"),
- basicProfileCaptor.firstValue,
- true
- )
}
private fun mockCleanObjectKey(key: String?, errCode: Int) {
- `when`(validator.cleanObjectKey(key))
- .thenReturn(ValidationResult().apply {
- `object` = key
- errorCode = errCode
- })
+
+ every {
+ validator.cleanObjectKey(key)
+ } returns ValidationResult().apply {
+ `object` = key
+ errorCode = errCode
+ }
}
private fun mockCleanObjectValue(value: String?, errCode: Int) {
- `when`(validator.cleanObjectValue(value, Profile))
- .thenReturn(ValidationResult().apply {
- `object` = value
- errorCode = errCode
- })
+
+ every {
+ validator.cleanObjectValue(value, Profile)
+ } returns ValidationResult().apply {
+ `object` = value
+ errorCode = errCode
+ }
}
private fun mockCleanMultiValuePropertyKey(key: String?, errCode: Int) {
- `when`(validator.cleanMultiValuePropertyKey(key))
- .thenReturn(ValidationResult().apply {
- `object` = key
- errorCode = errCode
- })
+
+ every {
+ validator.cleanMultiValuePropertyKey(key)
+ } returns ValidationResult().apply {
+ `object` = key
+ errorCode = errCode
+ }
}
}
\ No newline at end of file
diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/CleverTapAPITest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/CleverTapAPITest.kt
index 3371a1f79..d189ef2ab 100644
--- a/clevertap-core/src/test/java/com/clevertap/android/sdk/CleverTapAPITest.kt
+++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/CleverTapAPITest.kt
@@ -6,6 +6,7 @@ import com.clevertap.android.sdk.inbox.CTInboxController
import com.clevertap.android.sdk.pushnotification.CoreNotificationRenderer
import com.clevertap.android.sdk.task.CTExecutorFactory
import com.clevertap.android.sdk.task.MockCTExecutors
+import com.clevertap.android.sdk.usereventlogs.UserEventLogTestData
import com.clevertap.android.shared.test.BaseTestCase
import com.clevertap.android.shared.test.Constant
import org.json.JSONObject
@@ -21,381 +22,308 @@ class CleverTapAPITest : BaseTestCase() {
private lateinit var corestate: MockCoreState
+ // Common setup helper functions
+ private fun executeMockExecutors(block: () -> Unit) {
+ mockStatic(CTExecutorFactory::class.java).use {
+ `when`(CTExecutorFactory.executors(any())).thenReturn(
+ MockCTExecutors(cleverTapInstanceConfig)
+ )
+ block()
+ }
+ }
+
+ private fun executeMockFactory(block: () -> Unit) {
+ mockStatic(CleverTapFactory::class.java).use {
+ `when`(CleverTapFactory.getCoreState(application, cleverTapInstanceConfig, null))
+ .thenReturn(corestate)
+ block()
+ }
+ }
+
+ private fun executeMockFactoryWithAny(block: () -> Unit) {
+ mockStatic(CleverTapFactory::class.java).use {
+ `when`(
+ CleverTapFactory.getCoreState(
+ ArgumentMatchers.any(),
+ ArgumentMatchers.any(),
+ ArgumentMatchers.any()
+ )
+ )
+ .thenReturn(corestate)
+ block()
+ }
+ }
+
+ private fun executeBasicTest(block: () -> Unit) {
+ executeMockExecutors {
+ executeMockFactory {
+ block()
+ }
+ }
+ }
+
+ private fun executeBasicTestWithAny(block: () -> Unit) {
+ executeMockExecutors {
+ executeMockFactoryWithAny {
+ block()
+ }
+ }
+ }
+
+ private fun initializeCleverTapAPI() {
+ cleverTapAPI = CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig)
+ }
+
+ private fun verifyCommonConstructorBehavior() {
+ verify(corestate.sessionManager).setLastVisitTime()
+ verify(corestate.sessionManager).setUserLastVisitTs()
+ verify(corestate.deviceInfo).setDeviceNetworkInfoReportingFromStorage()
+ verify(corestate.deviceInfo).setCurrentUserOptOutStateFromStorage()
+ val actualConfig = StorageHelper.getString(
+ application,
+ "instance:" + cleverTapInstanceConfig.accountId,
+ ""
+ )
+ assertEquals(cleverTapInstanceConfig.toJSONString(), actualConfig)
+ }
+
@Before
@Throws(Exception::class)
override fun setUp() {
super.setUp()
- corestate = MockCoreState(application, cleverTapInstanceConfig)
+ corestate = MockCoreState(cleverTapInstanceConfig)
}
- /* @Test
- fun testActivity() {
- val activity = mock(Activity::class.java)
- val bundle = Bundle()
- //create
- activity.onCreate(bundle, null)
- CleverTapAPI.onActivityCreated(activity, null)
- }*/
-
@Test
fun testCleverTapAPI_constructor_when_InitialAppEnteredForegroundTime_greater_than_5_secs() {
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(
- cleverTapInstanceConfig
- )
- )
- mockStatic(CleverTapFactory::class.java).use {
- mockStatic(Utils::class.java).use {
-
- // Arrange
- `when`(CleverTapFactory.getCoreState(application, cleverTapInstanceConfig, null))
- .thenReturn(corestate)
- `when`(Utils.getNow()).thenReturn(Int.MAX_VALUE)
-
- CoreMetaData.setInitialAppEnteredForegroundTime(0)
-
- // Act
- CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig)
-
- // Assert
- assertTrue("isCreatedPostAppLaunch must be true", cleverTapInstanceConfig.isCreatedPostAppLaunch)
- verify(corestate.sessionManager).setLastVisitTime()
- verify(corestate.deviceInfo).setDeviceNetworkInfoReportingFromStorage()
- verify(corestate.deviceInfo).setCurrentUserOptOutStateFromStorage()
- val actualConfig =
- StorageHelper.getString(application, "instance:" + cleverTapInstanceConfig.accountId, "")
- assertEquals(cleverTapInstanceConfig.toJSONString(), actualConfig)
- }
+ executeBasicTest {
+ mockStatic(Utils::class.java).use {
+ // Arrange
+ `when`(Utils.getNow()).thenReturn(Int.MAX_VALUE)
+ CoreMetaData.setInitialAppEnteredForegroundTime(0)
+
+ // Act
+ initializeCleverTapAPI()
+
+ // Assert
+ assertTrue(
+ "isCreatedPostAppLaunch must be true",
+ cleverTapInstanceConfig.isCreatedPostAppLaunch
+ )
+ verifyCommonConstructorBehavior()
}
}
}
@Test
fun testCleverTapAPI_constructor_when_InitialAppEnteredForegroundTime_less_than_5_secs() {
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(
- cleverTapInstanceConfig
- )
- )
+ executeBasicTest {
mockStatic(Utils::class.java).use {
- mockStatic(CleverTapFactory::class.java).use {
- // Arrange
- `when`(CleverTapFactory.getCoreState(application, cleverTapInstanceConfig, null))
- .thenReturn(corestate)
- `when`(Utils.getNow()).thenReturn(0)
-
- CoreMetaData.setInitialAppEnteredForegroundTime(Int.MAX_VALUE)
-
- // Act
- CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig)
-
- // Assert
- assertFalse(
- "isCreatedPostAppLaunch must be false",
- cleverTapInstanceConfig.isCreatedPostAppLaunch
- )
- verify(corestate.sessionManager).setLastVisitTime()
- verify(corestate.deviceInfo).setDeviceNetworkInfoReportingFromStorage()
- verify(corestate.deviceInfo).setCurrentUserOptOutStateFromStorage()
-
- val string =
- StorageHelper.getString(application, "instance:" + cleverTapInstanceConfig.accountId, "")
- assertEquals(cleverTapInstanceConfig.toJSONString(), string)
- }
+ // Arrange
+ `when`(Utils.getNow()).thenReturn(0)
+ CoreMetaData.setInitialAppEnteredForegroundTime(Int.MAX_VALUE)
+
+ // Act
+ initializeCleverTapAPI()
+
+ // Assert
+ assertFalse(
+ "isCreatedPostAppLaunch must be false",
+ cleverTapInstanceConfig.isCreatedPostAppLaunch
+ )
+ verifyCommonConstructorBehavior()
}
}
}
@Test
fun test_setLocationForGeofences() {
- val location = Location("")
- location.apply {
- latitude = 17.4444
- longitude = 4.444
- }
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(
- cleverTapInstanceConfig
- )
- )
-
- mockStatic(CleverTapFactory::class.java).use {
- // Arrange
- `when`(CleverTapFactory.getCoreState(application, cleverTapInstanceConfig, null))
- .thenReturn(corestate)
- cleverTapAPI = CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig)
- cleverTapAPI.setLocationForGeofences(location, 45)
- assertTrue(corestate.coreMetaData.isLocationForGeofence)
- assertEquals(corestate.coreMetaData.geofenceSDKVersion, 45)
- verify(corestate.locationManager)._setLocation(location)
+ executeBasicTest {
+ val location = Location("").apply {
+ latitude = 17.4444
+ longitude = 4.444
}
+
+ initializeCleverTapAPI()
+ cleverTapAPI.setLocationForGeofences(location, 45)
+
+ assertTrue(corestate.coreMetaData.isLocationForGeofence)
+ assertEquals(corestate.coreMetaData.geofenceSDKVersion, 45)
+ verify(corestate.locationManager)._setLocation(location)
}
}
@Test
fun test_setGeofenceCallback() {
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(
- cleverTapInstanceConfig
- )
- )
- mockStatic(CleverTapFactory::class.java).use {
- // Arrange
- `when`(CleverTapFactory.getCoreState(application, cleverTapInstanceConfig, null))
- .thenReturn(corestate)
- cleverTapAPI = CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig)
-
- val geofenceCallback = object : GeofenceCallback {
- override fun handleGeoFences(jsonObject: JSONObject?) {
- TODO("Not yet implemented")
- }
-
- override fun triggerLocation() {
- TODO("Not yet implemented")
- }
+ executeBasicTest {
+ // Arrange
+ val geofenceCallback = object : GeofenceCallback {
+ override fun handleGeoFences(jsonObject: JSONObject?) {
}
- cleverTapAPI.geofenceCallback = geofenceCallback
-
- assertEquals(geofenceCallback, cleverTapAPI.geofenceCallback)
+ override fun triggerLocation() {
+ }
}
+
+ // Act
+ initializeCleverTapAPI()
+ cleverTapAPI.geofenceCallback = geofenceCallback
+
+ // Assert
+ assertEquals(geofenceCallback, cleverTapAPI.geofenceCallback)
}
}
@Test
fun test_pushGeoFenceError() {
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(
- cleverTapInstanceConfig
- )
- )
- mockStatic(CleverTapFactory::class.java).use {
- `when`(CleverTapFactory.getCoreState(application, cleverTapInstanceConfig, null))
- .thenReturn(corestate)
- cleverTapAPI = CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig)
-
- val expectedErrorCode = 999
- val expectedErrorMessage = "Fire in the hall"
-
- cleverTapAPI.pushGeoFenceError(expectedErrorCode, expectedErrorMessage)
-
- val actualValidationResult = corestate.validationResultStack.popValidationResult()
- assertEquals(999, actualValidationResult.errorCode)
- assertEquals("Fire in the hall", actualValidationResult.errorDesc)
- }
+ executeBasicTest {
+ // Arrange
+ val expectedErrorCode = 999
+ val expectedErrorMessage = "Fire in the hall"
+
+ // Act
+ initializeCleverTapAPI()
+ cleverTapAPI.pushGeoFenceError(expectedErrorCode, expectedErrorMessage)
+
+ // Assert
+ val actualValidationResult = corestate.validationResultStack.popValidationResult()
+ assertEquals(expectedErrorCode, actualValidationResult.errorCode)
+ assertEquals(expectedErrorMessage, actualValidationResult.errorDesc)
}
}
@Test
fun test_pushGeoFenceExitedEvent() {
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(
- cleverTapInstanceConfig
- )
+ executeBasicTest {
+ // Arrange
+ val expectedJson = JSONObject("{\"key\":\"value\"}")
+ val argumentCaptor = ArgumentCaptor.forClass(JSONObject::class.java)
+
+ // Act
+ initializeCleverTapAPI()
+ cleverTapAPI.pushGeoFenceExitedEvent(expectedJson)
+
+ // Assert
+ verify(corestate.analyticsManager).raiseEventForGeofences(
+ ArgumentMatchers.anyString(),
+ argumentCaptor.capture()
)
- mockStatic(CleverTapFactory::class.java).use {
- val argumentCaptor =
- ArgumentCaptor.forClass(
- JSONObject::class.java
- )
-
- `when`(CleverTapFactory.getCoreState(application, cleverTapInstanceConfig, null))
- .thenReturn(corestate)
- cleverTapAPI = CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig)
-
- val expectedJson = JSONObject("{\"key\":\"value\"}")
-
- cleverTapAPI.pushGeoFenceExitedEvent(expectedJson)
-
- verify(corestate.analyticsManager).raiseEventForGeofences(
- ArgumentMatchers.anyString(), argumentCaptor.capture()
- )
-
- assertEquals(expectedJson, argumentCaptor.value)
- }
+ assertEquals(expectedJson, argumentCaptor.value)
}
}
@Test
fun test_pushGeoFenceEnteredEvent() {
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(
- cleverTapInstanceConfig
- )
+ executeBasicTest {
+ // Arrange
+ val expectedJson = JSONObject("{\"key\":\"value\"}")
+ val argumentCaptor = ArgumentCaptor.forClass(JSONObject::class.java)
+
+ // Act
+ initializeCleverTapAPI()
+ cleverTapAPI.pushGeofenceEnteredEvent(expectedJson)
+
+ // Assert
+ verify(corestate.analyticsManager).raiseEventForGeofences(
+ ArgumentMatchers.anyString(),
+ argumentCaptor.capture()
)
- mockStatic(CleverTapFactory::class.java).use {
- `when`(CleverTapFactory.getCoreState(application, cleverTapInstanceConfig, null))
- .thenReturn(corestate)
- cleverTapAPI = CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig)
-
- val expectedJson = JSONObject("{\"key\":\"value\"}")
-
- cleverTapAPI.pushGeofenceEnteredEvent(expectedJson)
- val argumentCaptor =
- ArgumentCaptor.forClass(
- JSONObject::class.java
- )
-
- verify(corestate.analyticsManager).raiseEventForGeofences(
- ArgumentMatchers.anyString(), argumentCaptor.capture()
- )
-
- assertEquals(expectedJson, argumentCaptor.value)
- }
+ assertEquals(expectedJson, argumentCaptor.value)
}
}
@Test
fun test_changeCredentials_whenDefaultConfigNotNull_credentialsMustNotChange() {
- mockStatic(CleverTapFactory::class.java).use {
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(
- cleverTapInstanceConfig
- )
- )
- `when`(
- CleverTapFactory.getCoreState(
- ArgumentMatchers.any(),
- ArgumentMatchers.any(),
- ArgumentMatchers.any()
- )
- )
- .thenReturn(corestate)
- CleverTapAPI.getDefaultInstance(application)
- CleverTapAPI.changeCredentials("acct123", "token123", "eu")
- val instance = ManifestInfo.getInstance(application)
- assertNotEquals("acct123", instance.accountId)
- assertNotEquals("token123", instance.acountToken)
- assertNotEquals("eu", instance.accountRegion)
+ executeBasicTestWithAny {
+ val expectedAccountId = "acct123"
+ val expectedToken = "token123"
+ val expectedRegion = "eu"
+ CleverTapAPI.getDefaultInstance(application)
+
+ val manifestInfo = ManifestInfo.getInstance(application)
+ // Act
+ CleverTapAPI.changeCredentials(expectedAccountId, expectedToken, expectedRegion)
+
+ // Assert
+ with(manifestInfo) {
+ assertNotEquals(expectedAccountId, accountId)
+ assertNotEquals(expectedToken, acountToken)
+ assertNotEquals(expectedRegion, accountRegion)
}
}
}
@Test
fun test_changeCredentials_whenDefaultConfigNull_credentialsMustChange() {
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(
- cleverTapInstanceConfig
- )
- )
+ executeMockExecutors {
+ // Arrange
+ val expectedAccountId = "acct123"
+ val expectedToken = "token123"
+ val expectedRegion = "eu"
CleverTapAPI.defaultConfig = null
- CleverTapAPI.changeCredentials("acct123", "token123", "eu")
- val instance = ManifestInfo.getInstance(application)
- assertEquals("acct123", instance.accountId)
- assertEquals("token123", instance.acountToken)
- assertEquals("eu", instance.accountRegion)
+
+ // Act
+ CleverTapAPI.changeCredentials(expectedAccountId, expectedToken, expectedRegion)
+
+ // Assert
+ val manifestInfo = ManifestInfo.getInstance(application)
+ with(manifestInfo) {
+ assertEquals(expectedAccountId, accountId)
+ assertEquals(expectedToken, acountToken)
+ assertEquals(expectedRegion, accountRegion)
+ }
}
}
@Test
fun test_createNotification_whenInstancesNull__createNotificationMustBeCalled() {
-
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(
- cleverTapInstanceConfig
- )
+ executeBasicTestWithAny {
+ val bundle = Bundle()
+ val lock = Object()
+ `when`(corestate.pushProviders.pushRenderingLock).thenReturn(lock)
+ CleverTapAPI.createNotification(application, bundle)
+ verify(corestate.pushProviders).pushNotificationRenderer =
+ any(CoreNotificationRenderer::class.java)
+ verify(corestate.pushProviders)._createNotification(
+ application,
+ bundle,
+ Constants.EMPTY_NOTIFICATION_ID
)
- mockStatic(CleverTapFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(
- cleverTapInstanceConfig
- )
- )
- `when`(
- CleverTapFactory.getCoreState(
- ArgumentMatchers.any(),
- ArgumentMatchers.any(),
- ArgumentMatchers.any()
- )
- )
- .thenReturn(corestate)
- val bundle = Bundle()
- val lock = Object()
- //CleverTapAPI.getDefaultInstance(application)
- //CleverTapAPI.setInstances(null)
- `when`(corestate.pushProviders.pushRenderingLock).thenReturn(lock)
- CleverTapAPI.createNotification(application, bundle)
- verify(corestate.pushProviders).pushNotificationRenderer = any(CoreNotificationRenderer::class.java)
- verify(corestate.pushProviders)._createNotification(
- application,
- bundle,
- Constants.EMPTY_NOTIFICATION_ID
- )
- }
}
}
@Test
fun test_createNotification_whenInstanceNotNullAndAcctIDMatches__createNotificationMustBeCalled() {
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(
- cleverTapInstanceConfig
- )
+ executeBasicTestWithAny {
+ val bundle = Bundle()
+ val lock = Object()
+ bundle.putString(Constants.WZRK_ACCT_ID_KEY, Constant.ACC_ID)
+ CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig)
+
+ `when`(corestate.pushProviders.pushRenderingLock).thenReturn(lock)
+ CleverTapAPI.createNotification(application, bundle)
+ verify(corestate.pushProviders).pushNotificationRenderer =
+ any(CoreNotificationRenderer::class.java)
+ verify(corestate.pushProviders)._createNotification(
+ application,
+ bundle,
+ Constants.EMPTY_NOTIFICATION_ID
)
- mockStatic(CleverTapFactory::class.java).use {
- `when`(
- CleverTapFactory.getCoreState(
- ArgumentMatchers.any(),
- ArgumentMatchers.any(),
- ArgumentMatchers.any()
- )
- )
- .thenReturn(corestate)
- val bundle = Bundle()
- val lock = Object()
- bundle.putString(Constants.WZRK_ACCT_ID_KEY, Constant.ACC_ID)
- CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig)
-
- `when`(corestate.pushProviders.pushRenderingLock).thenReturn(lock)
- CleverTapAPI.createNotification(application, bundle)
- verify(corestate.pushProviders).pushNotificationRenderer = any(CoreNotificationRenderer::class.java)
- verify(corestate.pushProviders)._createNotification(
- application,
- bundle,
- Constants.EMPTY_NOTIFICATION_ID
- )
- }
}
}
@Test
fun test_createNotification_whenInstanceNotNullAndAcctIdDontMatch_createNotificationMustNotBeCalled() {
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(
- cleverTapInstanceConfig
- )
+ executeMockFactoryWithAny {
+ val bundle = Bundle()
+ bundle.putString(Constants.WZRK_ACCT_ID_KEY, "acct123")
+ CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig)
+ CleverTapAPI.createNotification(application, bundle)
+ verify(corestate.pushProviders, never())._createNotification(
+ application,
+ bundle,
+ Constants.EMPTY_NOTIFICATION_ID
)
- mockStatic(CleverTapFactory::class.java).use {
- `when`(
- CleverTapFactory.getCoreState(
- ArgumentMatchers.any(),
- ArgumentMatchers.any(),
- ArgumentMatchers.any()
- )
- )
- .thenReturn(corestate)
- val bundle = Bundle()
- bundle.putString(Constants.WZRK_ACCT_ID_KEY, "acct123")
- CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig)
- CleverTapAPI.createNotification(application, bundle)
- verify(corestate.pushProviders, never())._createNotification(
- application,
- bundle,
- Constants.EMPTY_NOTIFICATION_ID
- )
- }
}
}
@@ -440,53 +368,21 @@ class CleverTapAPITest : BaseTestCase() {
@Test
fun test_processPushNotification_whenInstancesNull__processCustomPushNotificationMustBeCalled() {
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(
- cleverTapInstanceConfig
- )
- )
- mockStatic(CleverTapFactory::class.java).use {
- `when`(
- CleverTapFactory.getCoreState(
- ArgumentMatchers.any(),
- ArgumentMatchers.any(),
- ArgumentMatchers.any()
- )
- )
- .thenReturn(corestate)
- val bundle = Bundle()
- //CleverTapAPI.getDefaultInstance(application)
- //CleverTapAPI.setInstances(null)
- CleverTapAPI.processPushNotification(application, bundle)
- verify(corestate.pushProviders).processCustomPushNotification(bundle)
- }
+ executeMockFactoryWithAny {
+ val bundle = Bundle()
+ CleverTapAPI.processPushNotification(application, bundle)
+ verify(corestate.pushProviders).processCustomPushNotification(bundle)
}
}
@Test
fun test_processPushNotification_whenInstancesNotNull__processCustomPushNotificationMustBeCalled() {
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(
- cleverTapInstanceConfig
- )
- )
- mockStatic(CleverTapFactory::class.java).use {
- `when`(
- CleverTapFactory.getCoreState(
- ArgumentMatchers.any(),
- ArgumentMatchers.any(),
- ArgumentMatchers.any()
- )
- )
- .thenReturn(corestate)
- val bundle = Bundle()
- bundle.putString(Constants.WZRK_ACCT_ID_KEY, Constant.ACC_ID)
- CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig)
- CleverTapAPI.processPushNotification(application, bundle)
- verify(corestate.pushProviders).processCustomPushNotification(bundle)
- }
+ executeBasicTestWithAny {
+ val bundle = Bundle()
+ bundle.putString(Constants.WZRK_ACCT_ID_KEY, Constant.ACC_ID)
+ CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig)
+ CleverTapAPI.processPushNotification(application, bundle)
+ verify(corestate.pushProviders).processCustomPushNotification(bundle)
}
}
@@ -496,26 +392,17 @@ class CleverTapAPITest : BaseTestCase() {
val messageIDs = arrayListOf("1", "2", "3")
val inboxController = null
val controllerManager = mock(ControllerManager::class.java)
- corestate.controllerManager=controllerManager
+ corestate.controllerManager = controllerManager
// Act
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(
- cleverTapInstanceConfig
- )
- )
- mockStatic(CleverTapFactory::class.java).use {
- `when`(CleverTapFactory.getCoreState(application, cleverTapInstanceConfig, null))
- .thenReturn(corestate)
- `when`(controllerManager.ctInboxController).thenReturn(inboxController)
- cleverTapAPI = CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig)
- cleverTapAPI.deleteInboxMessagesForIDs(messageIDs)
-
- // Assert
- verify(controllerManager).ctInboxController
- verifyNoMoreInteractions(controllerManager)
- }
+ executeBasicTest {
+ `when`(controllerManager.ctInboxController).thenReturn(inboxController)
+ initializeCleverTapAPI()
+ cleverTapAPI.deleteInboxMessagesForIDs(messageIDs)
+
+ // Assert
+ verify(controllerManager).ctInboxController
+ verifyNoMoreInteractions(controllerManager)
}
}
@@ -528,23 +415,14 @@ class CleverTapAPITest : BaseTestCase() {
corestate.controllerManager = controllerManager
// Act
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(
- cleverTapInstanceConfig
- )
- )
- mockStatic(CleverTapFactory::class.java).use {
- `when`(CleverTapFactory.getCoreState(application, cleverTapInstanceConfig, null))
- .thenReturn(corestate)
- `when`(controllerManager.ctInboxController).thenReturn(inboxController)
- cleverTapAPI = CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig)
- cleverTapAPI.deleteInboxMessagesForIDs(messageIDs)
-
- // Assert
- verify(controllerManager,times(2)).ctInboxController
- verify(inboxController).deleteInboxMessagesForIDs(messageIDs)
- }
+ executeBasicTest {
+ `when`(controllerManager.ctInboxController).thenReturn(inboxController)
+ initializeCleverTapAPI()
+ cleverTapAPI.deleteInboxMessagesForIDs(messageIDs)
+
+ // Assert
+ verify(controllerManager, times(2)).ctInboxController
+ verify(inboxController).deleteInboxMessagesForIDs(messageIDs)
}
}
@@ -554,26 +432,17 @@ class CleverTapAPITest : BaseTestCase() {
val messageIDs = arrayListOf("1", "2", "3")
val inboxController = null
val controllerManager = mock(ControllerManager::class.java)
- corestate.controllerManager=controllerManager
+ corestate.controllerManager = controllerManager
// Act
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(
- cleverTapInstanceConfig
- )
- )
- mockStatic(CleverTapFactory::class.java).use {
- `when`(CleverTapFactory.getCoreState(application, cleverTapInstanceConfig, null))
- .thenReturn(corestate)
- `when`(controllerManager.ctInboxController).thenReturn(inboxController)
- cleverTapAPI = CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig)
- cleverTapAPI.markReadInboxMessagesForIDs(messageIDs)
-
- // Assert
- verify(controllerManager).ctInboxController
- verifyNoMoreInteractions(controllerManager)
- }
+ executeBasicTest {
+ `when`(controllerManager.ctInboxController).thenReturn(inboxController)
+ initializeCleverTapAPI()
+ cleverTapAPI.markReadInboxMessagesForIDs(messageIDs)
+
+ // Assert
+ verify(controllerManager).ctInboxController
+ verifyNoMoreInteractions(controllerManager)
}
}
@@ -586,61 +455,104 @@ class CleverTapAPITest : BaseTestCase() {
corestate.controllerManager = controllerManager
// Act
- mockStatic(CTExecutorFactory::class.java).use {
- `when`(CTExecutorFactory.executors(any())).thenReturn(
- MockCTExecutors(
- cleverTapInstanceConfig
- )
- )
- mockStatic(CleverTapFactory::class.java).use {
- `when`(CleverTapFactory.getCoreState(application, cleverTapInstanceConfig, null))
- .thenReturn(corestate)
- `when`(controllerManager.ctInboxController).thenReturn(inboxController)
- cleverTapAPI = CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig)
- cleverTapAPI.markReadInboxMessagesForIDs(messageIDs)
+ executeBasicTest {
+ `when`(controllerManager.ctInboxController).thenReturn(inboxController)
+ initializeCleverTapAPI()
+ cleverTapAPI.markReadInboxMessagesForIDs(messageIDs)
+
+ // Assert
+ verify(controllerManager, times(2)).ctInboxController
+ verify(inboxController).markReadInboxMessagesForIDs(messageIDs)
+ }
+ }
- // Assert
- verify(controllerManager,times(2)).ctInboxController
- verify(inboxController).markReadInboxMessagesForIDs(messageIDs)
- }
+ @Test
+ fun `test getUserEventLogCount`() {
+ // Arrange
+ val evt = UserEventLogTestData.EventNames.TEST_EVENT
+ `when`(corestate.localDataStore.readUserEventLogCount(evt)).thenReturn(1)
+
+ // Act
+ executeBasicTest {
+ initializeCleverTapAPI()
+ val userEventLogCountActual = cleverTapAPI.getUserEventLogCount(evt)
+
+ // Assert
+ assertEquals(1, userEventLogCountActual)
+ verify(corestate.localDataStore).readUserEventLogCount(evt)
}
}
-/* @Test
- fun testPushDeepLink(){
- // Arrange
- var cleverTapAPISpy : CleverTapAPI = Mockito.spy(cleverTapAPI)
- val uri = Uri.parse("https://www.google.com/")
- //Act
- cleverTapAPISpy.pushDeepLink(uri)
+ @Test
+ fun `test getUserEventLog`() {
+ // Arrange
+ val evt = UserEventLogTestData.EventNames.TEST_EVENT
+ val log = UserEventLogTestData.EventNames.sampleUserEventLogsForSameDeviceId[0]
+ `when`(corestate.localDataStore.readUserEventLog(evt)).thenReturn(log)
+
+ // Act
+ executeBasicTest {
+ initializeCleverTapAPI()
+ val userEventLogActual = cleverTapAPI.getUserEventLog(evt)
+
+ // Assert
+ assertSame(log, userEventLogActual)
+ verify(corestate.localDataStore).readUserEventLog(evt)
+ }
+ }
- //Assert
- verify(cleverTapAPISpy).pushDeepLink(uri,false)
- }
+ @Test
+ fun `test getUserEventLogHistory`() {
+ // Arrange
+ val logs = UserEventLogTestData.EventNames.sampleUserEventLogsForSameDeviceId
+ `when`(corestate.localDataStore.readUserEventLogs()).thenReturn(logs)
+
+ // Act
+ executeBasicTest {
+ initializeCleverTapAPI()
+ val historyActual = cleverTapAPI.userEventLogHistory
+
+ // Assert
+ assertEquals(2, historyActual.size)
+ assertEquals(logs[0], historyActual.values.elementAt(0))
+ assertEquals(logs[1], historyActual.values.elementAt(1))
+ verify(corestate.localDataStore).readUserEventLogs()
+ }
+ }
- @Test
- fun testPushDeviceTokenEvent(){
- // Arrange
- val ctAPI = CleverTapAPI.instanceWithConfig(application,cleverTapInstanceConfig)
- var cleverTapAPISpy = Mockito.spy(ctAPI)
+ @Test
+ fun `test getUserLastVisitTs`() {
+ val expectedUserLastVisitTs = UserEventLogTestData.TestTimestamps.SAMPLE_TIMESTAMP
+ // Arrange
+ `when`(corestate.sessionManager.userLastVisitTs).thenReturn(expectedUserLastVisitTs)
- cleverTapAPISpy.pushDeviceTokenEvent("12345",true,FCM)
- verify(cleverTapAPISpy).queueEvent(ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.anyInt())
- }
+ // Act
+ executeBasicTest {
+ initializeCleverTapAPI()
+ val lastVisitTsActual = cleverTapAPI.userLastVisitTs
- @Test
- fun testPushLink(){
- var cleverTapAPISpy : CleverTapAPI = Mockito.spy(cleverTapAPI)
- val uri = Uri.parse("https://www.google.com/")
+ // Assert
+ assertEquals(expectedUserLastVisitTs, lastVisitTsActual)
+ verify(corestate.sessionManager).userLastVisitTs
+ }
+ }
- val mockStatic = Mockito.mockStatic(StorageHelper::class.java)
- `when`(StorageHelper.getInt(ArgumentMatchers.any(), ArgumentMatchers.anyString(), ArgumentMatchers.anyInt())).thenReturn(0)
+ @Test
+ fun `test getUserAppLaunchCount`() {
+ // Arrange
+ `when`(corestate.localDataStore.readUserEventLogCount(Constants.APP_LAUNCHED_EVENT))
+ .thenReturn(5)
- //Act
- cleverTapAPISpy.pushInstallReferrer("abc","def","ghi")
+ // Act
+ executeBasicTest {
+ initializeCleverTapAPI()
+ val appLaunchCountActual = cleverTapAPI.userAppLaunchCount
- verify(cleverTapAPISpy).pushDeepLink(ArgumentMatchers.any(), ArgumentMatchers.anyBoolean())
- }*/
+ // Assert
+ assertEquals(5, appLaunchCountActual)
+ verify(corestate.localDataStore).readUserEventLogCount(Constants.APP_LAUNCHED_EVENT)
+ }
+ }
@After
fun tearDown() {
diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/CleverTapFixtures.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/CleverTapFixtures.kt
new file mode 100644
index 000000000..c44123deb
--- /dev/null
+++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/CleverTapFixtures.kt
@@ -0,0 +1,41 @@
+package com.clevertap.android.sdk
+
+import com.clevertap.android.shared.test.Constant
+
+class CleverTapFixtures {
+
+ companion object {
+
+ val manifestInfo = ManifestInfo(
+ Constant.ACC_ID,
+ Constant.ACC_TOKEN,
+ null,
+ "",
+ "",
+ "",
+ false,
+ false,
+ "notification icon",
+ null,
+ false,
+ false,
+ false,
+ "fcm:sender:id",
+ "some.app.package",
+ false,
+ "serviceName",
+ "push-channel-id",
+ emptyArray(),
+ 0
+ )
+
+ fun provideCleverTapInstanceConfig(): CleverTapInstanceConfig =
+ CleverTapInstanceConfig.createInstanceWithManifest(
+ manifestInfo,
+ Constant.ACC_ID,
+ Constant.ACC_TOKEN,
+ null,
+ true
+ )
+ }
+}
\ No newline at end of file
diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/EventQueueManagerTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/EventQueueManagerTest.kt
index d84d84e23..726e637ae 100644
--- a/clevertap-core/src/test/java/com/clevertap/android/sdk/EventQueueManagerTest.kt
+++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/EventQueueManagerTest.kt
@@ -46,7 +46,7 @@ class EventQueueManagerTest : BaseTestCase() {
cleverTapInstanceConfig
)
)
- corestate = MockCoreState(application, cleverTapInstanceConfig)
+ corestate = MockCoreState(cleverTapInstanceConfig)
eventQueueManager =
spy(
EventQueueManager(
@@ -71,6 +71,56 @@ class EventQueueManagerTest : BaseTestCase() {
}
}
+ @Test
+ fun `test queueEvent when type is raised event updates local store`() {
+ mockStatic(CTExecutorFactory::class.java).use {
+ `when`(CTExecutorFactory.executors(cleverTapInstanceConfig))
+ .thenReturn(MockCTExecutors(cleverTapInstanceConfig))
+
+ // Given
+ val event = JSONObject()
+ event.put("evtName", "test_event")
+
+ `when`(corestate.eventMediator.getEventName(event)).thenReturn("test_event")
+
+ // When
+ eventQueueManager.queueEvent(application, event, Constants.RAISED_EVENT)
+
+ // Then
+ verify(corestate.localDataStore).persistUserEventLog("test_event")
+ }
+ }
+
+ @Test
+ fun `test queueEvent when type is not raised event does not update local store`() {
+ mockStatic(CTExecutorFactory::class.java).use {
+ `when`(CTExecutorFactory.executors(cleverTapInstanceConfig))
+ .thenReturn(MockCTExecutors(cleverTapInstanceConfig))
+
+ // Given
+ val event = JSONObject()
+ event.put("evtName", "test_event")
+ `when`(corestate.eventMediator.getEventName(event)).thenReturn("test_event")
+
+ // Test for different event types that are not RAISED_EVENT
+ listOf(
+ Constants.PROFILE_EVENT,
+ Constants.FETCH_EVENT,
+ Constants.DATA_EVENT,
+ Constants.PING_EVENT,
+ Constants.PAGE_EVENT,
+ Constants.NV_EVENT
+ ).forEach { eventType ->
+
+ // When
+ eventQueueManager.queueEvent(application, event, eventType)
+
+ // Then
+ verify(corestate.localDataStore, never()).persistUserEventLog(any())
+ }
+ }
+ }
+
@Test
fun test_queueEvent_will_not_add_to_queue_when_event_should_be_dropped() {
mockStatic(CTExecutorFactory::class.java).use {
@@ -162,7 +212,6 @@ class EventQueueManagerTest : BaseTestCase() {
cleverTapInstanceConfig
)
)
- val captor = ArgumentCaptor.forClass(Runnable::class.java)
val mockInAppController = mock(InAppController::class.java)
`when`(corestate.eventMediator.shouldDropEvent(json, Constants.PROFILE_EVENT))
.thenReturn(false)
@@ -173,7 +222,6 @@ class EventQueueManagerTest : BaseTestCase() {
`when`(corestate.controllerManager.inAppController)
.thenReturn(mockInAppController)
- doNothing().`when`(eventQueueManager).addToQueue(application, json, Constants.PROFILE_EVENT)
doNothing().`when`(eventQueueManager).pushInitialEventsAsync()
doNothing().`when`(corestate.sessionManager).lazyCreateSession(application)
@@ -194,11 +242,11 @@ class EventQueueManagerTest : BaseTestCase() {
cleverTapInstanceConfig
)
)
- doNothing().`when`(eventQueueManager).processPushNotificationViewedEvent(application, json)
+ doNothing().`when`(eventQueueManager).processPushNotificationViewedEvent(application, json, Constants.NV_EVENT)
eventQueueManager.addToQueue(application, json, Constants.NV_EVENT)
- verify(eventQueueManager).processPushNotificationViewedEvent(application, json)
+ verify(eventQueueManager).processPushNotificationViewedEvent(application, json, Constants.NV_EVENT)
verify(eventQueueManager, never()).processEvent(application, json, Constants.NV_EVENT)
}
}
@@ -215,7 +263,7 @@ class EventQueueManagerTest : BaseTestCase() {
eventQueueManager.addToQueue(application, json, Constants.PROFILE_EVENT)
- verify(eventQueueManager, never()).processPushNotificationViewedEvent(application, json)
+ verify(eventQueueManager, never()).processPushNotificationViewedEvent(application, json, Constants.PROFILE_EVENT)
verify(eventQueueManager).processEvent(application, json, Constants.PROFILE_EVENT)
}
}
@@ -232,8 +280,9 @@ class EventQueueManagerTest : BaseTestCase() {
corestate.coreMetaData.currentSessionId = 1000
`when`(eventQueueManager.now).thenReturn(7000)
doNothing().`when`(eventQueueManager).flushQueueAsync(application, PUSH_NOTIFICATION_VIEWED)
+ doNothing().`when`(eventQueueManager).initInAppEvaluation(application, json, Constants.PROFILE_EVENT)
- eventQueueManager.processPushNotificationViewedEvent(application, json)
+ eventQueueManager.processPushNotificationViewedEvent(application, json, Constants.PROFILE_EVENT)
assertNull(json.optJSONObject(Constants.ERROR_KEY))
assertEquals("event", json.getString("type"))
@@ -269,9 +318,10 @@ class EventQueueManagerTest : BaseTestCase() {
corestate.coreMetaData.currentSessionId = 1000
`when`(eventQueueManager.now).thenReturn(7000)
doNothing().`when`(eventQueueManager).flushQueueAsync(application, PUSH_NOTIFICATION_VIEWED)
+ doNothing().`when`(eventQueueManager).initInAppEvaluation(application, json, Constants.PROFILE_EVENT)
// Act
- eventQueueManager.processPushNotificationViewedEvent(application, json)
+ eventQueueManager.processPushNotificationViewedEvent(application, json, Constants.PROFILE_EVENT)
// Assert
assertEquals(validationResult.errorCode, json.getJSONObject(Constants.ERROR_KEY)["c"])
diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/LocalDataStoreProvider.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/LocalDataStoreProvider.kt
deleted file mode 100644
index 6cea0fc33..000000000
--- a/clevertap-core/src/test/java/com/clevertap/android/sdk/LocalDataStoreProvider.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package com.clevertap.android.sdk
-
-import android.content.Context
-import com.clevertap.android.sdk.cryption.CryptHandler
-
-object LocalDataStoreProvider {
-
- fun provideLocalDataStore(
- context: Context,
- config: CleverTapInstanceConfig,
- cryptHandler: CryptHandler,
- deviceInfo: DeviceInfo
- ): LocalDataStore {
- return LocalDataStore(context, config, cryptHandler, deviceInfo)
- }
-}
diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/LocalDataStoreTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/LocalDataStoreTest.kt
index 634829f97..cdd84cffc 100644
--- a/clevertap-core/src/test/java/com/clevertap/android/sdk/LocalDataStoreTest.kt
+++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/LocalDataStoreTest.kt
@@ -2,18 +2,28 @@ package com.clevertap.android.sdk
import android.content.Context
import com.clevertap.android.sdk.cryption.CryptHandler
+import com.clevertap.android.sdk.db.BaseDatabaseManager
+import com.clevertap.android.sdk.db.DBAdapter
+import com.clevertap.android.sdk.db.DBManager
import com.clevertap.android.sdk.events.EventDetail
+import com.clevertap.android.sdk.usereventlogs.UserEventLog
+import com.clevertap.android.sdk.usereventlogs.UserEventLogDAO
+import com.clevertap.android.sdk.usereventlogs.UserEventLogDAOImpl
+import com.clevertap.android.sdk.usereventlogs.UserEventLogTestData
import com.clevertap.android.shared.test.BaseTestCase
import org.json.JSONObject
-import org.junit.*
-import org.junit.runner.*
-import org.mockito.*
-import org.mockito.kotlin.*
-import org.robolectric.RobolectricTestRunner
-import kotlin.test.*
-
-@RunWith(RobolectricTestRunner::class)
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.Mockito
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+
class LocalDataStoreTest : BaseTestCase() {
+ private lateinit var userEventLogDaoMock: UserEventLogDAO
+ private lateinit var baseDatabaseManager: BaseDatabaseManager
private lateinit var defConfig: CleverTapInstanceConfig
private lateinit var config: CleverTapInstanceConfig
@@ -22,6 +32,12 @@ class LocalDataStoreTest : BaseTestCase() {
private lateinit var localDataStoreWithConfigSpy: LocalDataStore
private lateinit var cryptHandler : CryptHandler
private lateinit var deviceInfo : DeviceInfo
+ private lateinit var dbAdapter: DBAdapter
+ val eventName = UserEventLogTestData.EventNames.TEST_EVENT
+ private val normalizedEventName = UserEventLogTestData.EventNames.eventNameToNormalizedMap[eventName]!!
+ private val eventNames = UserEventLogTestData.EventNames.eventNames
+ private val setOfActualAndNormalizedEventNamePair = UserEventLogTestData.EventNames.setOfActualAndNormalizedEventNamePair
+
override fun setUp() {
super.setUp()
@@ -29,10 +45,27 @@ class LocalDataStoreTest : BaseTestCase() {
defConfig = CleverTapInstanceConfig.createDefaultInstance(appCtx, "id", "token", "region")
cryptHandler = CryptHandler(0, CryptHandler.EncryptionAlgorithm.AES, "id")
deviceInfo = MockDeviceInfo(appCtx, defConfig, "id", metaData)
- localDataStoreWithDefConfig = LocalDataStore(appCtx, defConfig, cryptHandler, deviceInfo)
+ baseDatabaseManager = Mockito.mock(DBManager::class.java)
+ dbAdapter = Mockito.mock(DBAdapter::class.java)
+ userEventLogDaoMock = Mockito.mock(UserEventLogDAOImpl::class.java)
+ localDataStoreWithDefConfig = LocalDataStore(
+ appCtx,
+ defConfig,
+ cryptHandler,
+ deviceInfo,
+ baseDatabaseManager
+ )
config = CleverTapInstanceConfig.createInstance(appCtx, "id", "token", "region")
- localDataStoreWithConfig = LocalDataStore(appCtx, config, cryptHandler, deviceInfo)
+ localDataStoreWithConfig = LocalDataStore(
+ appCtx,
+ config,
+ cryptHandler,
+ deviceInfo,
+ baseDatabaseManager
+ )
localDataStoreWithConfigSpy = Mockito.spy(localDataStoreWithConfig)
+ Mockito.`when`(baseDatabaseManager.loadDBAdapter(appCtx)).thenReturn(dbAdapter)
+ Mockito.`when`(dbAdapter.userEventLogDAO()).thenReturn(userEventLogDaoMock)
}
@Test
@@ -91,10 +124,9 @@ class LocalDataStoreTest : BaseTestCase() {
fun test_getEventHistory_when_FunctionIsCalled_should_ReturnAMapOfEventNameAndDetails() {
// if context is null,exception happens and null is returnd
assertNull(localDataStoreWithDefConfig.getEventHistory(null))
- var results: Map = mutableMapOf()
//if default config is used, events are stored in local_events pref file
- results = localDataStoreWithDefConfig.getEventHistory(appCtx)
+ var results: Map = localDataStoreWithDefConfig.getEventHistory(appCtx)
assertTrue { results.isEmpty() }
assertNull(results["event"])
@@ -272,4 +304,322 @@ class LocalDataStoreTest : BaseTestCase() {
assertNull(localDataStoreWithConfig.getProfileProperty("key3"))
assertEquals(2, localDataStoreWithConfig.getProfileProperty("key4"))
}
+
+ @Test
+ fun `test persistUserEventLog when event name is null returns false`() {
+ // When
+ val result = localDataStoreWithConfig.persistUserEventLog(null)
+
+ // Then
+ assertFalse(result)
+ Mockito.verify(dbAdapter, Mockito.never()).userEventLogDAO()
+ Mockito.verify(userEventLogDaoMock, Mockito.never()).eventExistsByDeviceIdAndNormalizedEventName(anyString(), anyString())
+ }
+
+ @Test
+ fun `test persistUserEventLog when event exists updates event successfully`() {
+ // Given
+ Mockito.`when`(userEventLogDaoMock.eventExistsByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName))
+ .thenReturn(true)
+ Mockito.`when`(userEventLogDaoMock.updateEventByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName))
+ .thenReturn(true)
+
+ // When
+ val result = localDataStoreWithConfig.persistUserEventLog(eventName)
+
+ // Then
+ assertTrue(result)
+ Mockito.verify(userEventLogDaoMock).eventExistsByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName)
+ Mockito.verify(userEventLogDaoMock).updateEventByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName)
+ Mockito.verify(userEventLogDaoMock, Mockito.never()).insertEvent(anyString(), anyString(), anyString())
+ }
+
+ @Test
+ fun `test persistUserEventLog when event exists but update fails returns false`() {
+ // Given
+ Mockito.`when`(userEventLogDaoMock.eventExistsByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName))
+ .thenReturn(true)
+ Mockito.`when`(userEventLogDaoMock.updateEventByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName))
+ .thenReturn(false)
+
+ // When
+ val result = localDataStoreWithConfig.persistUserEventLog(eventName)
+
+ // Then
+ assertFalse(result)
+ }
+
+ @Test
+ fun `test persistUserEventLog when event does not exist inserts successfully`() {
+ // Given
+ Mockito.`when`(userEventLogDaoMock.eventExistsByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName))
+ .thenReturn(false)
+ Mockito.`when`(userEventLogDaoMock.insertEvent(deviceInfo.deviceID, eventName, normalizedEventName))
+ .thenReturn(1L)
+
+ // When
+ val result = localDataStoreWithConfig.persistUserEventLog(eventName)
+
+ // Then
+ assertTrue(result)
+ Mockito.verify(userEventLogDaoMock).eventExistsByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName)
+ Mockito.verify(userEventLogDaoMock).insertEvent(deviceInfo.deviceID, eventName, normalizedEventName)
+ Mockito.verify(userEventLogDaoMock, Mockito.never()).updateEventByDeviceIdAndNormalizedEventName(anyString(), anyString())
+ }
+
+ @Test
+ fun `test persistUserEventLog when event does not exist and insert fails returns false`() {
+ // Given
+ Mockito.`when`(userEventLogDaoMock.eventExistsByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName))
+ .thenReturn(false)
+ Mockito.`when`(userEventLogDaoMock.insertEvent(deviceInfo.deviceID, eventName, normalizedEventName))
+ .thenReturn(-1L)
+
+ // When
+ val result = localDataStoreWithConfig.persistUserEventLog(eventName)
+
+ // Then
+ assertFalse(result)
+ }
+
+ @Test
+ fun `test persistUserEventLog when exception occurs returns false`() {
+ // Given
+ Mockito.`when`(userEventLogDaoMock.eventExistsByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName))
+ .thenThrow(RuntimeException("DB Error"))
+
+ // When
+ val result = localDataStoreWithConfig.persistUserEventLog(eventName)
+
+ // Then
+ assertFalse(result)
+ }
+
+ @Test
+ fun `test persistUserEventLogsInBulk success`() {
+ // Given
+ Mockito.`when`(userEventLogDaoMock.upsertEventsByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, setOfActualAndNormalizedEventNamePair))
+ .thenReturn(true)
+
+ // When
+ val result = localDataStoreWithConfig.persistUserEventLogsInBulk(eventNames)
+
+ // Then
+ assertTrue(result)
+ Mockito.verify(userEventLogDaoMock).upsertEventsByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, setOfActualAndNormalizedEventNamePair)
+ }
+
+ @Test
+ fun `test persistUserEventLogsInBulk when operation fails returns false`() {
+ // Given
+ Mockito.`when`(userEventLogDaoMock.upsertEventsByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, setOfActualAndNormalizedEventNamePair))
+ .thenReturn(false)
+
+ // When
+ val result = localDataStoreWithConfig.persistUserEventLogsInBulk(eventNames)
+
+ // Then
+ assertFalse(result)
+ Mockito.verify(userEventLogDaoMock).upsertEventsByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, setOfActualAndNormalizedEventNamePair)
+ }
+
+ @Test
+ fun `test isUserEventLogFirstTime when count is 1 returns true`() {
+ // Given
+ Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName))
+ .thenReturn(1)
+
+ // When
+ val result = localDataStoreWithConfig.isUserEventLogFirstTime(eventName)
+
+ // Then
+ assertTrue(result)
+ Mockito.verify(userEventLogDaoMock).readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName)
+ }
+
+ @Test
+ fun `test isUserEventLogFirstTime when count is greater than 1 returns false`() {
+ // Given
+ Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName))
+ .thenReturn(2)
+
+ // When
+ val result = localDataStoreWithConfig.isUserEventLogFirstTime(eventName)
+
+ // Then
+ assertFalse(result)
+ Mockito.verify(userEventLogDaoMock).readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName)
+ }
+
+ @Test
+ fun `test isUserEventLogFirstTime caches result for subsequent calls`() {
+ // Given
+ Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName))
+ .thenReturn(2)
+
+ // When
+ val firstCall = localDataStoreWithConfig.isUserEventLogFirstTime(eventName)
+ val secondCall = localDataStoreWithConfig.isUserEventLogFirstTime(eventName)
+
+ // Then
+ assertFalse(firstCall)
+ assertFalse(secondCall)
+ // Should only call readEventCountByDeviceID once as result is cached
+ Mockito.verify(userEventLogDaoMock, Mockito.times(1))
+ .readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName)
+ }
+
+ @Test
+ fun `test isUserEventLogFirstTime when count is 0 returns false`() {
+ // Given
+ Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName))
+ .thenReturn(0)
+
+ // When
+ val result = localDataStoreWithConfig.isUserEventLogFirstTime(eventName)
+
+ // Then
+ assertFalse(result)
+ Mockito.verify(userEventLogDaoMock).readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName)
+ }
+
+ @Test
+ fun `test isUserEventLogFirstTime when count is -1 returns false`() {
+ // Given
+ Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName))
+ .thenReturn(-1)
+
+ // When
+ val result = localDataStoreWithConfig.isUserEventLogFirstTime(eventName)
+
+ // Then
+ assertFalse(result)
+ Mockito.verify(userEventLogDaoMock).readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName)
+ }
+
+ @Test
+ fun `test isUserEventLogFirstTime behavior with changing event counts`() {
+ // Given
+
+ // First call setup - count 0
+ Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName))
+ .thenReturn(0)
+
+ // When - First call
+ val firstCallResult = localDataStoreWithConfig.isUserEventLogFirstTime(eventName)
+
+ // Then
+ assertFalse(firstCallResult)
+ Mockito.verify(userEventLogDaoMock).readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName)
+
+ // Given - Second call setup - count 1
+ Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName))
+ .thenReturn(1)
+
+ // When - Second call
+ val secondCallResult = localDataStoreWithConfig.isUserEventLogFirstTime(eventName)
+
+ // Then
+ assertTrue(secondCallResult)
+ Mockito.verify(userEventLogDaoMock, Mockito.times(2))
+ .readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName)
+
+ // Given - Third call setup - count 2
+ Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName))
+ .thenReturn(2)
+
+ // When - Third call
+ val thirdCallResult = localDataStoreWithConfig.isUserEventLogFirstTime(eventName)
+
+ // Then
+ assertFalse(thirdCallResult)
+ Mockito.verify(userEventLogDaoMock, Mockito.times(3))
+ .readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName)
+
+ // When - Fourth call (should use cached result)
+ val fourthCallResult = localDataStoreWithConfig.isUserEventLogFirstTime(eventName)
+
+ // Then
+ assertFalse(fourthCallResult)
+ // Should not make additional DB call as result is now cached
+ Mockito.verify(userEventLogDaoMock, Mockito.times(3))
+ .readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName)
+ }
+
+ @Test
+ fun `test cleanUpExtraEvents success`() {
+ // Given
+ val threshold = 5
+ val numberOfRowsToCleanup = 2
+ Mockito.`when`(userEventLogDaoMock.cleanUpExtraEvents(threshold, numberOfRowsToCleanup))
+ .thenReturn(true)
+
+ // When
+ val result = localDataStoreWithConfig.cleanUpExtraEvents(threshold, numberOfRowsToCleanup)
+
+ // Then
+ assertTrue(result)
+ Mockito.verify(userEventLogDaoMock).cleanUpExtraEvents(threshold, numberOfRowsToCleanup)
+ }
+
+ @Test
+ fun `test readUserEventLog success`() {
+ // Given
+ val userEventLog = UserEventLogTestData.EventNames.sampleUserEventLogsForSameDeviceId[0]
+ Mockito.`when`(userEventLogDaoMock.readEventByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName))
+ .thenReturn(userEventLog)
+
+ // When
+ val result = localDataStoreWithConfig.readUserEventLog(eventName)
+
+ // Then
+ assertNotNull(result)
+ assertEquals(userEventLog, result)
+ Mockito.verify(userEventLogDaoMock).readEventByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName)
+ }
+
+ @Test
+ fun `test readUserEventLogCount returns correct count when event exists`() {
+ // Given
+ val expectedCount = 5
+ Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName))
+ .thenReturn(expectedCount)
+
+ // When
+ val result = localDataStoreWithConfig.readUserEventLogCount(eventName)
+
+ // Then
+ assertEquals(expectedCount, result)
+ Mockito.verify(userEventLogDaoMock).readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName)
+ }
+
+ @Test
+ fun `test readUserEventLogs returns correct event list for device`() {
+ // Given
+ val expectedLogs = UserEventLogTestData.EventNames.sampleUserEventLogsForSameDeviceId
+ Mockito.`when`(userEventLogDaoMock.allEventsByDeviceID(deviceInfo.deviceID))
+ .thenReturn(expectedLogs)
+
+ // When
+ val result = localDataStoreWithConfig.readUserEventLogs()
+
+ // Then
+ assertEquals(expectedLogs, result)
+ Mockito.verify(userEventLogDaoMock).allEventsByDeviceID(deviceInfo.deviceID)
+ }
+
+ @Test
+ fun `test readEventLogsForAllUsers returns correct event list`() {
+ // Given
+ val expectedLogs = UserEventLogTestData.EventNames.sampleUserEventLogsForMixedDeviceId
+ Mockito.`when`(userEventLogDaoMock.allEvents())
+ .thenReturn(expectedLogs)
+
+ // When
+ val result = localDataStoreWithConfig.readEventLogsForAllUsers()
+
+ // Then
+ assertEquals(expectedLogs, result)
+ Mockito.verify(userEventLogDaoMock).allEvents()
+ }
}
\ No newline at end of file
diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/MockCoreState.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/MockCoreState.kt
index 7adf15b12..389e573e5 100644
--- a/clevertap-core/src/test/java/com/clevertap/android/sdk/MockCoreState.kt
+++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/MockCoreState.kt
@@ -1,6 +1,5 @@
package com.clevertap.android.sdk
-import android.content.Context
import com.clevertap.android.sdk.db.DBManager
import com.clevertap.android.sdk.events.EventMediator
import com.clevertap.android.sdk.events.EventQueueManager
@@ -12,9 +11,11 @@ import com.clevertap.android.sdk.validation.ValidationResultStack
import com.clevertap.android.sdk.variables.CTVariables
import com.clevertap.android.sdk.variables.Parser
import com.clevertap.android.sdk.variables.VarCache
+import io.mockk.mockk
import org.mockito.*
-class MockCoreState(context: Context, cleverTapInstanceConfig: CleverTapInstanceConfig) : CoreState(context) {
+// todo lp check usages and eliminate context setup
+class MockCoreState(cleverTapInstanceConfig: CleverTapInstanceConfig) : CoreState() {
init {
config = cleverTapInstanceConfig
@@ -41,3 +42,31 @@ class MockCoreState(context: Context, cleverTapInstanceConfig: CleverTapInstance
controllerManager = Mockito.mock(ControllerManager::class.java)
}
}
+
+class MockCoreStateKotlin(cleverTapInstanceConfig: CleverTapInstanceConfig) : CoreState() {
+
+ init {
+ config = cleverTapInstanceConfig
+ deviceInfo = mockk(relaxed = true)
+ pushProviders = mockk(relaxed = true)
+ sessionManager = mockk(relaxed = true)
+ locationManager = mockk(relaxed = true)
+ coreMetaData = CoreMetaData()
+ callbackManager = CallbackManager(cleverTapInstanceConfig, deviceInfo)
+ validationResultStack = mockk(relaxed = true)
+ analyticsManager = mockk(relaxed = true)
+ eventMediator = mockk(relaxed = true)
+ databaseManager = mockk(relaxed = true)
+ validationResultStack = ValidationResultStack()
+ mainLooperHandler = mockk(relaxed = true)
+ networkManager = mockk(relaxed = true)
+ ctLockManager = CTLockManager()
+ localDataStore = mockk(relaxed = true)
+ baseEventQueueManager = mockk(relaxed = true)
+ inAppController = mockk(relaxed = true)
+ parser = mockk(relaxed = true)
+ ctVariables = mockk(relaxed = true)
+ varCache = mockk(relaxed = true)
+ controllerManager = mockk(relaxed = true)
+ }
+}
diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/SessionManagerTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/SessionManagerTest.kt
index cffea646f..c74dbeb66 100644
--- a/clevertap-core/src/test/java/com/clevertap/android/sdk/SessionManagerTest.kt
+++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/SessionManagerTest.kt
@@ -3,9 +3,14 @@ package com.clevertap.android.sdk
import android.content.Context
import com.clevertap.android.sdk.cryption.CryptHandler
+import com.clevertap.android.sdk.db.BaseDatabaseManager
+import com.clevertap.android.sdk.db.DBManager
import com.clevertap.android.sdk.events.EventDetail
+import com.clevertap.android.sdk.usereventlogs.UserEventLog
import com.clevertap.android.sdk.validation.Validator
import com.clevertap.android.shared.test.BaseTestCase
+import io.mockk.every
+import io.mockk.mockk
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito
@@ -23,6 +28,7 @@ class SessionManagerTest : BaseTestCase() {
private lateinit var localDataStoreDef: LocalDataStore
private lateinit var cryptHandler : CryptHandler
private lateinit var deviceInfo : DeviceInfo
+ private lateinit var baseDatabaseManager: BaseDatabaseManager
override fun setUp() {
super.setUp()
config = CleverTapInstanceConfig.createInstance(application, "id", "token", "region")
@@ -34,7 +40,14 @@ class SessionManagerTest : BaseTestCase() {
cryptHandler = CryptHandler(0, CryptHandler.EncryptionAlgorithm.AES, "id")
cryptHandler = CryptHandler(0, CryptHandler.EncryptionAlgorithm.AES, "id")
deviceInfo = MockDeviceInfo(appCtx, configDef, "id", coreMetaData)
- localDataStoreDef = LocalDataStore(application, configDef, cryptHandler, deviceInfo)
+ baseDatabaseManager = Mockito.mock(DBManager::class.java)
+ localDataStoreDef = LocalDataStore(
+ application,
+ configDef,
+ cryptHandler,
+ deviceInfo,
+ baseDatabaseManager
+ )
sessionManagerDef = SessionManager(configDef,coreMetaData,validator,localDataStoreDef)
@@ -161,5 +174,22 @@ class SessionManagerTest : BaseTestCase() {
}
+ @Test
+ fun `test setUserLastVisitTs`(){
+ val localDataStoreMockk = mockk()
+ sessionManagerDef = SessionManager(configDef,coreMetaData,validator,localDataStoreMockk)
+ val appLaunchedEventLog = UserEventLog(
+ Constants.APP_LAUNCHED_EVENT,
+ Utils.getNormalizedName(Constants.APP_LAUNCHED_EVENT),
+ 0,
+ 1000000L,
+ 1,
+ deviceInfo.deviceID
+ )
+ every { localDataStoreMockk.readUserEventLog(Constants.APP_LAUNCHED_EVENT) } returns appLaunchedEventLog
+ sessionManagerDef.setUserLastVisitTs()
+ assertEquals(appLaunchedEventLog.lastTs, sessionManagerDef.userLastVisitTs)
+ }
+
}
\ No newline at end of file
diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/db/DBAdapterTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/db/DBAdapterTest.kt
index 2f795c546..2aeca8df1 100644
--- a/clevertap-core/src/test/java/com/clevertap/android/sdk/db/DBAdapterTest.kt
+++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/db/DBAdapterTest.kt
@@ -592,6 +592,18 @@ class DBAdapterTest : BaseTestCase() {
}
}
+ @Test
+ fun `test userEventLogDAO returns singleton instance`() {
+ // When
+ val dao1 = dbAdapter.userEventLogDAO()
+ val dao2 = dbAdapter.userEventLogDAO()
+
+ // Then
+ assertNotNull(dao1)
+ assertSame(dao1, dao2) // Verify same instance is returned
+ }
+
+
private fun getCtMsgDao(
id: String = "1",
userId: String = "1",
diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/evaluation/EvaluationManagerTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/evaluation/EvaluationManagerTest.kt
index 8d94752e3..86a1c5ddf 100644
--- a/clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/evaluation/EvaluationManagerTest.kt
+++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/evaluation/EvaluationManagerTest.kt
@@ -6,6 +6,9 @@ import com.clevertap.android.sdk.inapp.TriggerManager
import com.clevertap.android.sdk.inapp.customtemplates.TemplatesManager
import com.clevertap.android.sdk.inapp.evaluation.EventType.PROFILE
import com.clevertap.android.sdk.inapp.evaluation.EventType.RAISED
+import com.clevertap.android.sdk.inapp.evaluation.TriggerAdapter.Companion.INAPP_OPERATOR
+import com.clevertap.android.sdk.inapp.evaluation.TriggerAdapter.Companion.INAPP_PROPERTYNAME
+import com.clevertap.android.sdk.inapp.evaluation.TriggerAdapter.Companion.KEY_PROPERTY_VALUE
import com.clevertap.android.sdk.inapp.evaluation.TriggerOperator.Equals
import com.clevertap.android.sdk.inapp.store.preference.InAppStore
import com.clevertap.android.sdk.inapp.store.preference.StoreRegistry
@@ -448,9 +451,9 @@ class EvaluationManagerTest : BaseTestCase() {
"${Constants.KEY_EVT_NAME}": "TestEvent",
"eventProperties": [
{
- "${Constants.INAPP_PROPERTYNAME}": "Property1",
- "${Constants.INAPP_OPERATOR}": 1,
- "${Constants.KEY_PROPERTY_VALUE}": "Value1"
+ "$INAPP_PROPERTYNAME": "Property1",
+ "$INAPP_OPERATOR": 1,
+ "$KEY_PROPERTY_VALUE": "Value1"
}
]
}
diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/evaluation/TriggerAdapterTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/evaluation/TriggerAdapterTest.kt
index fdf3313ee..d9c91768a 100644
--- a/clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/evaluation/TriggerAdapterTest.kt
+++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/evaluation/TriggerAdapterTest.kt
@@ -1,6 +1,15 @@
package com.clevertap.android.sdk.inapp.evaluation
import com.clevertap.android.sdk.Constants
+import com.clevertap.android.sdk.inapp.evaluation.TriggerAdapter.Companion.INAPP_OPERATOR
+import com.clevertap.android.sdk.inapp.evaluation.TriggerAdapter.Companion.INAPP_PROPERTYNAME
+import com.clevertap.android.sdk.inapp.evaluation.TriggerAdapter.Companion.KEY_EVENT_NAME
+import com.clevertap.android.sdk.inapp.evaluation.TriggerAdapter.Companion.KEY_EVENT_PROPERTIES
+import com.clevertap.android.sdk.inapp.evaluation.TriggerAdapter.Companion.KEY_FIRST_TIME_ONLY
+import com.clevertap.android.sdk.inapp.evaluation.TriggerAdapter.Companion.KEY_GEO_RADIUS_PROPERTIES
+import com.clevertap.android.sdk.inapp.evaluation.TriggerAdapter.Companion.KEY_ITEM_PROPERTIES
+import com.clevertap.android.sdk.inapp.evaluation.TriggerAdapter.Companion.KEY_PROFILE_ATTR_NAME
+import com.clevertap.android.sdk.inapp.evaluation.TriggerAdapter.Companion.KEY_PROPERTY_VALUE
import org.json.JSONArray
import org.json.JSONObject
import org.junit.*
@@ -12,17 +21,17 @@ class TriggerAdapterTest {
fun testPropertyAtIndexValid() {
// Arrange
val triggerJSON = JSONObject()
- triggerJSON.put(Constants.KEY_EVENT_NAME, "TestEvent")
+ triggerJSON.put(KEY_EVENT_NAME, "TestEvent")
val propertiesArray = JSONArray()
val propertyObject = JSONObject()
- propertyObject.put(Constants.INAPP_PROPERTYNAME, "Property1")
- propertyObject.put(Constants.INAPP_OPERATOR, 1)
- propertyObject.put(Constants.KEY_PROPERTY_VALUE, "Value1")
+ propertyObject.put(INAPP_PROPERTYNAME, "Property1")
+ propertyObject.put(INAPP_OPERATOR, 1)
+ propertyObject.put(KEY_PROPERTY_VALUE, "Value1")
propertiesArray.put(propertyObject)
- triggerJSON.put(Constants.KEY_EVENT_PROPERTIES, propertiesArray)
+ triggerJSON.put(KEY_EVENT_PROPERTIES, propertiesArray)
val triggerAdapter = TriggerAdapter(triggerJSON)
@@ -39,16 +48,16 @@ class TriggerAdapterTest {
fun testPropertyAtIndexInvalidIndex() {
// Arrange
val triggerJSON = JSONObject()
- triggerJSON.put(Constants.KEY_EVENT_NAME, "TestEvent")
+ triggerJSON.put(KEY_EVENT_NAME, "TestEvent")
val propertiesArray = JSONArray()
val propertyObject = JSONObject()
- propertyObject.put(Constants.INAPP_PROPERTYNAME, "Property1")
- propertyObject.put(Constants.INAPP_OPERATOR, 1)
- propertyObject.put(Constants.KEY_PROPERTY_VALUE, "Value1")
+ propertyObject.put(INAPP_PROPERTYNAME, "Property1")
+ propertyObject.put(INAPP_OPERATOR, 1)
+ propertyObject.put(KEY_PROPERTY_VALUE, "Value1")
propertiesArray.put(propertyObject)
- triggerJSON.put(Constants.KEY_EVENT_PROPERTIES, propertiesArray)
+ triggerJSON.put(KEY_EVENT_PROPERTIES, propertiesArray)
val triggerAdapter = TriggerAdapter(triggerJSON)
@@ -63,16 +72,16 @@ class TriggerAdapterTest {
fun testItemAtIndexValid() {
// Arrange
val triggerJSON = JSONObject()
- triggerJSON.put(Constants.KEY_EVENT_NAME, "TestEvent")
+ triggerJSON.put(KEY_EVENT_NAME, "TestEvent")
val itemsArray = JSONArray()
val itemObject = JSONObject()
- itemObject.put(Constants.INAPP_PROPERTYNAME, "ItemProperty1")
- itemObject.put(Constants.INAPP_OPERATOR, 2)
- itemObject.put(Constants.KEY_PROPERTY_VALUE, "ItemValue1")
+ itemObject.put(INAPP_PROPERTYNAME, "ItemProperty1")
+ itemObject.put(INAPP_OPERATOR, 2)
+ itemObject.put(KEY_PROPERTY_VALUE, "ItemValue1")
itemsArray.put(itemObject)
- triggerJSON.put(Constants.KEY_ITEM_PROPERTIES, itemsArray)
+ triggerJSON.put(KEY_ITEM_PROPERTIES, itemsArray)
val triggerAdapter = TriggerAdapter(triggerJSON)
@@ -89,16 +98,16 @@ class TriggerAdapterTest {
fun testItemAtIndexInvalidIndex() {
// Arrange
val triggerJSON = JSONObject()
- triggerJSON.put(Constants.KEY_EVENT_NAME, "TestEvent")
+ triggerJSON.put(KEY_EVENT_NAME, "TestEvent")
val itemsArray = JSONArray()
val itemObject = JSONObject()
- itemObject.put(Constants.INAPP_PROPERTYNAME, "ItemProperty1")
- itemObject.put(Constants.INAPP_OPERATOR, 2)
- itemObject.put(Constants.KEY_PROPERTY_VALUE, "ItemValue1")
+ itemObject.put(INAPP_PROPERTYNAME, "ItemProperty1")
+ itemObject.put(INAPP_OPERATOR, 2)
+ itemObject.put(KEY_PROPERTY_VALUE, "ItemValue1")
itemsArray.put(itemObject)
- triggerJSON.put(Constants.KEY_ITEM_PROPERTIES, itemsArray)
+ triggerJSON.put(KEY_ITEM_PROPERTIES, itemsArray)
val triggerAdapter = TriggerAdapter(triggerJSON)
@@ -113,10 +122,10 @@ class TriggerAdapterTest {
fun testItemPropertiesNull() {
// Arrange
val triggerJSON = JSONObject()
- triggerJSON.put(Constants.KEY_EVENT_NAME, "TestEvent")
+ triggerJSON.put(KEY_EVENT_NAME, "TestEvent")
// 'itemProperties' is null
- triggerJSON.put(Constants.KEY_ITEM_PROPERTIES, null)
+ triggerJSON.put(KEY_ITEM_PROPERTIES, null)
val triggerAdapter = TriggerAdapter(triggerJSON)
@@ -131,14 +140,14 @@ class TriggerAdapterTest {
fun testItemAtIndexObjectNull() {
// Arrange
val triggerJSON = JSONObject()
- triggerJSON.put(Constants.KEY_EVENT_NAME, "TestEvent")
+ triggerJSON.put(KEY_EVENT_NAME, "TestEvent")
val itemsArray = JSONArray()
// Object at index 0 is null
itemsArray.put(null)
itemsArray.put(JSONObject()) // Valid item object
- triggerJSON.put(Constants.KEY_ITEM_PROPERTIES, itemsArray)
+ triggerJSON.put(KEY_ITEM_PROPERTIES, itemsArray)
val triggerAdapter = TriggerAdapter(triggerJSON)
@@ -153,10 +162,10 @@ class TriggerAdapterTest {
fun testEventPropertiesNull() {
// Arrange
val triggerJSON = JSONObject()
- triggerJSON.put(Constants.KEY_EVENT_NAME, "TestEvent")
+ triggerJSON.put(KEY_EVENT_NAME, "TestEvent")
// 'eventProperties' is null
- triggerJSON.put(Constants.KEY_EVENT_PROPERTIES, null)
+ triggerJSON.put(KEY_EVENT_PROPERTIES, null)
val triggerAdapter = TriggerAdapter(triggerJSON)
@@ -171,14 +180,14 @@ class TriggerAdapterTest {
fun testPropertyAtIndexObjectNull() {
// Arrange
val triggerJSON = JSONObject()
- triggerJSON.put(Constants.KEY_EVENT_NAME, "TestEvent")
+ triggerJSON.put(KEY_EVENT_NAME, "TestEvent")
val propertiesArray = JSONArray()
// Object at index 0 is null
propertiesArray.put(null)
propertiesArray.put(JSONObject()) // Valid property object
- triggerJSON.put(Constants.KEY_EVENT_PROPERTIES, propertiesArray)
+ triggerJSON.put(KEY_EVENT_PROPERTIES, propertiesArray)
val triggerAdapter = TriggerAdapter(triggerJSON)
@@ -193,9 +202,9 @@ class TriggerAdapterTest {
fun testTriggerConditionFromJSON() {
// Arrange
val propertyObject = JSONObject()
- propertyObject.put(Constants.INAPP_PROPERTYNAME, "propertyName")
- propertyObject.put(Constants.INAPP_OPERATOR, 1)
- propertyObject.put(Constants.KEY_PROPERTY_VALUE, "TestValue")
+ propertyObject.put(INAPP_PROPERTYNAME, "propertyName")
+ propertyObject.put(INAPP_OPERATOR, 1)
+ propertyObject.put(KEY_PROPERTY_VALUE, "TestValue")
val triggerAdapter = TriggerAdapter(JSONObject())
@@ -215,7 +224,7 @@ class TriggerAdapterTest {
val propertiesArray = JSONArray()
propertiesArray.put(JSONObject())
propertiesArray.put(JSONObject())
- triggerJSON.put(Constants.KEY_EVENT_PROPERTIES, propertiesArray)
+ triggerJSON.put(KEY_EVENT_PROPERTIES, propertiesArray)
val triggerAdapter = TriggerAdapter(triggerJSON)
// Act
@@ -245,7 +254,7 @@ class TriggerAdapterTest {
val itemsArray = JSONArray()
itemsArray.put(JSONObject())
itemsArray.put(JSONObject())
- triggerJSON.put(Constants.KEY_ITEM_PROPERTIES, itemsArray)
+ triggerJSON.put(KEY_ITEM_PROPERTIES, itemsArray)
val triggerAdapter = TriggerAdapter(triggerJSON)
// Act
@@ -272,7 +281,7 @@ class TriggerAdapterTest {
fun testEventNameWithNonNullEventName() {
// Arrange
val triggerJSON = JSONObject()
- triggerJSON.put(Constants.KEY_EVENT_NAME, "SampleEventName")
+ triggerJSON.put(KEY_EVENT_NAME, "SampleEventName")
val triggerAdapter = TriggerAdapter(triggerJSON)
// Act
@@ -299,7 +308,7 @@ class TriggerAdapterTest {
fun testProfileAttrNameWithNonNullProfileAttributeName() {
// Arrange
val triggerJSON = JSONObject()
- triggerJSON.put(Constants.KEY_PROFILE_ATTR_NAME, "SampleAttr")
+ triggerJSON.put(KEY_PROFILE_ATTR_NAME, "SampleAttr")
val triggerAdapter = TriggerAdapter(triggerJSON)
// Act
@@ -329,7 +338,7 @@ class TriggerAdapterTest {
val propertiesArray = JSONArray()
propertiesArray.put(JSONObject())
propertiesArray.put(JSONObject())
- triggerJSON.put(Constants.KEY_EVENT_PROPERTIES, propertiesArray)
+ triggerJSON.put(KEY_EVENT_PROPERTIES, propertiesArray)
val triggerAdapter = TriggerAdapter(triggerJSON)
// Act
@@ -359,7 +368,7 @@ class TriggerAdapterTest {
val itemsArray = JSONArray()
itemsArray.put(JSONObject())
itemsArray.put(JSONObject())
- triggerJSON.put(Constants.KEY_ITEM_PROPERTIES, itemsArray)
+ triggerJSON.put(KEY_ITEM_PROPERTIES, itemsArray)
val triggerAdapter = TriggerAdapter(triggerJSON)
// Act
@@ -386,10 +395,10 @@ class TriggerAdapterTest {
fun testOptTriggerOperatorWithNonNullValue() {
// Arrange
val jsonObject = JSONObject()
- jsonObject.put(Constants.INAPP_OPERATOR, TriggerOperator.GreaterThan.operatorValue)
+ jsonObject.put(INAPP_OPERATOR, TriggerOperator.GreaterThan.operatorValue)
// Act
- val triggerOperator = jsonObject.optTriggerOperator(Constants.INAPP_OPERATOR)
+ val triggerOperator = jsonObject.optTriggerOperator(INAPP_OPERATOR)
// Assert
assertEquals(TriggerOperator.GreaterThan, triggerOperator)
@@ -411,7 +420,7 @@ class TriggerAdapterTest {
fun testGeoRadiusAtIndexValid() {
// Arrange
val triggerJSON = JSONObject()
- triggerJSON.put(Constants.KEY_EVENT_NAME, "TestEvent")
+ triggerJSON.put(KEY_EVENT_NAME, "TestEvent")
val geoRadiusArray = JSONArray()
val geoRadiusObject = JSONObject()
@@ -420,7 +429,7 @@ class TriggerAdapterTest {
geoRadiusObject.put("rad", 1000.0)
geoRadiusArray.put(geoRadiusObject)
- triggerJSON.put(Constants.KEY_GEO_RADIUS_PROPERTIES, geoRadiusArray)
+ triggerJSON.put(KEY_GEO_RADIUS_PROPERTIES, geoRadiusArray)
val triggerAdapter = TriggerAdapter(triggerJSON)
@@ -438,7 +447,7 @@ class TriggerAdapterTest {
fun testGeoRadiusAtIndexInvalidIndex() {
// Arrange
val triggerJSON = JSONObject()
- triggerJSON.put(Constants.KEY_EVENT_NAME, "TestEvent")
+ triggerJSON.put(KEY_EVENT_NAME, "TestEvent")
val geoRadiusArray = JSONArray()
val geoRadiusObject = JSONObject()
@@ -447,7 +456,7 @@ class TriggerAdapterTest {
geoRadiusObject.put("rad", 1000.0)
geoRadiusArray.put(geoRadiusObject)
- triggerJSON.put(Constants.KEY_GEO_RADIUS_PROPERTIES, geoRadiusArray)
+ triggerJSON.put(KEY_GEO_RADIUS_PROPERTIES, geoRadiusArray)
val triggerAdapter = TriggerAdapter(triggerJSON)
@@ -462,13 +471,13 @@ class TriggerAdapterTest {
fun testGeoRadiusAtIndexInvalidOutOfBoundIndex() {
// Arrange
val triggerJSON = JSONObject()
- triggerJSON.put(Constants.KEY_EVENT_NAME, "TestEvent")
+ triggerJSON.put(KEY_EVENT_NAME, "TestEvent")
val geoRadiusArray = JSONArray()
geoRadiusArray.put(JSONObject())
geoRadiusArray.put(JSONObject())
- triggerJSON.put(Constants.KEY_GEO_RADIUS_PROPERTIES, geoRadiusArray)
+ triggerJSON.put(KEY_GEO_RADIUS_PROPERTIES, geoRadiusArray)
val triggerAdapter = TriggerAdapter(triggerJSON)
@@ -486,7 +495,7 @@ class TriggerAdapterTest {
val itemsArray = JSONArray()
itemsArray.put(JSONObject())
itemsArray.put(JSONObject())
- triggerJSON.put(Constants.KEY_GEO_RADIUS_PROPERTIES, itemsArray)
+ triggerJSON.put(KEY_GEO_RADIUS_PROPERTIES, itemsArray)
val triggerAdapter = TriggerAdapter(triggerJSON)
// Act
@@ -516,7 +525,7 @@ class TriggerAdapterTest {
val geoRadiusArray = JSONArray()
geoRadiusArray.put(JSONObject())
geoRadiusArray.put(JSONObject())
- triggerJSON.put(Constants.KEY_GEO_RADIUS_PROPERTIES, geoRadiusArray)
+ triggerJSON.put(KEY_GEO_RADIUS_PROPERTIES, geoRadiusArray)
val triggerAdapter = TriggerAdapter(triggerJSON)
// Act
@@ -543,7 +552,7 @@ class TriggerAdapterTest {
fun testToJsonObject() {
// Arrange
val triggerJSON = JSONObject()
- triggerJSON.put(Constants.KEY_EVENT_NAME, "TestEvent")
+ triggerJSON.put(KEY_EVENT_NAME, "TestEvent")
val propertiesArray = JSONArray()
val propertyObject = JSONObject()
@@ -552,16 +561,16 @@ class TriggerAdapterTest {
propertyObject.put("value", "Value1")
propertiesArray.put(propertyObject)
- triggerJSON.put(Constants.KEY_EVENT_PROPERTIES, propertiesArray)
+ triggerJSON.put(KEY_EVENT_PROPERTIES, propertiesArray)
val itemsArray = JSONArray()
val itemObject = JSONObject()
- itemObject.put(Constants.INAPP_PROPERTYNAME, "ItemProperty1")
- itemObject.put(Constants.INAPP_OPERATOR, 2)
- itemObject.put(Constants.KEY_PROPERTY_VALUE, "ItemValue1")
+ itemObject.put(INAPP_PROPERTYNAME, "ItemProperty1")
+ itemObject.put(INAPP_OPERATOR, 2)
+ itemObject.put(KEY_PROPERTY_VALUE, "ItemValue1")
itemsArray.put(itemObject)
- triggerJSON.put(Constants.KEY_ITEM_PROPERTIES, itemsArray)
+ triggerJSON.put(KEY_ITEM_PROPERTIES, itemsArray)
val geoRadiusArray = JSONArray()
val geoRadiusObject = JSONObject()
@@ -570,14 +579,56 @@ class TriggerAdapterTest {
geoRadiusObject.put("rad", 1000.0)
geoRadiusArray.put(geoRadiusObject)
- triggerJSON.put(Constants.KEY_GEO_RADIUS_PROPERTIES, geoRadiusArray)
+ triggerJSON.put(KEY_GEO_RADIUS_PROPERTIES, geoRadiusArray)
// Assert
assertNotNull(triggerJSON)
- assertEquals("TestEvent", triggerJSON.optString(Constants.KEY_EVENT_NAME))
- assertEquals(propertiesArray, triggerJSON.optJSONArray(Constants.KEY_EVENT_PROPERTIES))
- assertEquals(itemsArray, triggerJSON.optJSONArray(Constants.KEY_ITEM_PROPERTIES))
- assertEquals(geoRadiusArray, triggerJSON.optJSONArray(Constants.KEY_GEO_RADIUS_PROPERTIES))
+ assertEquals("TestEvent", triggerJSON.optString(KEY_EVENT_NAME))
+ assertEquals(propertiesArray, triggerJSON.optJSONArray(KEY_EVENT_PROPERTIES))
+ assertEquals(itemsArray, triggerJSON.optJSONArray(KEY_ITEM_PROPERTIES))
+ assertEquals(geoRadiusArray, triggerJSON.optJSONArray(KEY_GEO_RADIUS_PROPERTIES))
+ }
+
+
+ @Test
+ fun `test firstTimeOnly with firstTimeOnly value as true`() {
+ // Arrange
+ val triggerJSON = JSONObject()
+ triggerJSON.put(KEY_FIRST_TIME_ONLY, true)
+ val triggerAdapter = TriggerAdapter(triggerJSON)
+
+ // Act
+ val actualFirstTimeOnly = triggerAdapter.firstTimeOnly
+
+ // Assert
+ assertTrue(actualFirstTimeOnly)
+ }
+
+ @Test
+ fun `test firstTimeOnly with firstTimeOnly value as false`(){
+ // Arrange
+ val triggerJSON = JSONObject()
+ triggerJSON.put(KEY_FIRST_TIME_ONLY, false)
+ val triggerAdapter = TriggerAdapter(triggerJSON)
+
+ // Act
+ val actualFirstTimeOnly = triggerAdapter.firstTimeOnly
+
+ // Assert
+ assertFalse(actualFirstTimeOnly)
+ }
+
+ @Test
+ fun `test firstTimeOnly with firstTimeOnly value as null`() {
+ // Arrange
+ val triggerJSON = JSONObject()
+ val triggerAdapter = TriggerAdapter(triggerJSON)
+
+ // Act
+ val actualFirstTimeOnly = triggerAdapter.firstTimeOnly
+
+ // Assert
+ assertFalse(actualFirstTimeOnly)
}
}
diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/evaluation/TriggersMatcherTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/evaluation/TriggersMatcherTest.kt
index b7b1f6a75..9d0ae65c0 100644
--- a/clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/evaluation/TriggersMatcherTest.kt
+++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/evaluation/TriggersMatcherTest.kt
@@ -2,21 +2,30 @@ package com.clevertap.android.sdk.inapp.evaluation
import android.location.Location
import com.clevertap.android.sdk.Constants
+import com.clevertap.android.sdk.LocalDataStore
+import com.clevertap.android.sdk.inapp.evaluation.TriggerAdapter.Companion.KEY_PROPERTY_VALUE
import com.clevertap.android.shared.test.BaseTestCase
-import io.mockk.*
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.spyk
+import io.mockk.verify
import org.json.JSONArray
import org.json.JSONObject
-import org.junit.*
-import org.junit.Assert.*
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
class TriggersMatcherTest : BaseTestCase() {
private lateinit var triggersMatcher: TriggersMatcher
+ private lateinit var localDataStore: LocalDataStore
@Before
override fun setUp() {
super.setUp()
- triggersMatcher = TriggersMatcher()
+ localDataStore = mockk(relaxed = true)
+ triggersMatcher = TriggersMatcher(localDataStore)
}
@Test
@@ -60,7 +69,7 @@ class TriggersMatcherTest : BaseTestCase() {
put("eventProperties", JSONArray().put(JSONObject().apply {
put("propertyName", "Property2")
put("op", TriggerOperator.Equals.operatorValue)
- put(Constants.KEY_PROPERTY_VALUE, "Value2")
+ put(KEY_PROPERTY_VALUE, "Value2")
}))
}
//whenTriggers.put(triggerJSON1)
@@ -71,7 +80,7 @@ class TriggersMatcherTest : BaseTestCase() {
put("eventProperties", JSONArray().put(JSONObject().apply {
put("propertyName", "Property1")
put("op", TriggerOperator.Equals.operatorValue)
- put(Constants.KEY_PROPERTY_VALUE, "Value1")
+ put(KEY_PROPERTY_VALUE, "Value1")
}))
}
//whenTriggers.put(triggerJSON2)
@@ -92,7 +101,7 @@ class TriggersMatcherTest : BaseTestCase() {
put("eventProperties", JSONArray().put(JSONObject().apply {
put("propertyName", "Property2")
put("op", TriggerOperator.Equals.operatorValue)
- put(Constants.KEY_PROPERTY_VALUE, "Value2")
+ put(KEY_PROPERTY_VALUE, "Value2")
}))
}
@@ -102,7 +111,7 @@ class TriggersMatcherTest : BaseTestCase() {
put("eventProperties", JSONArray().put(JSONObject().apply {
put("propertyName", "Property1")
put("op", TriggerOperator.Equals.operatorValue)
- put(Constants.KEY_PROPERTY_VALUE, "Value1")
+ put(KEY_PROPERTY_VALUE, "Value1")
}))
}
@@ -1285,6 +1294,101 @@ class TriggersMatcherTest : BaseTestCase() {
assertTrue(triggersMatcher.match(trigger, event))
}
+ @Test
+ fun `test match when firstTimeOnly is true and event is not first time returns false`() {
+ // Given
+ val trigger = createTriggerAdapter(
+ eventName = "EventA",
+ firstTimeOnly = true
+ )
+ val event = createEventAdapter("EventA")
+ every { localDataStore.isUserEventLogFirstTime("EventA") } returns false
+
+ // When
+ val result = triggersMatcher.match(trigger, event)
+
+ // Then
+ assertFalse(result)
+ verify { localDataStore.isUserEventLogFirstTime("EventA") }
+ }
+
+ @Test
+ fun `test match when firstTimeOnly is true and profileProperty is not first time returns false`() {
+ // Given
+ val eventName = "blood_sugar_first_time_event"
+ val profileAttrName = "Blood Sugar"
+ val trigger = createTriggerAdapter(
+ eventName = eventName,
+ firstTimeOnly = true,
+ profileAttrName = profileAttrName
+ )
+ val event = createEventAdapter(eventName = eventName, profileAttrName = profileAttrName)
+ every { localDataStore.isUserEventLogFirstTime(profileAttrName) } returns false
+
+ // When
+ val result = triggersMatcher.match(trigger, event)
+
+ // Then
+ assertFalse(result)
+ verify { localDataStore.isUserEventLogFirstTime(profileAttrName) }
+ }
+
+ @Test
+ fun `test match when firstTimeOnly is true and event is first time proceeds with other checks`() {
+ // Given
+ val trigger = createTriggerAdapter(
+ eventName = "EventA",
+ firstTimeOnly = true
+ )
+ val event = createEventAdapter("EventA")
+ every { localDataStore.isUserEventLogFirstTime("EventA") } returns true
+
+ // When
+ val result = triggersMatcher.match(trigger, event)
+
+ // Then
+ assertTrue(result)
+ verify { localDataStore.isUserEventLogFirstTime("EventA") }
+ }
+
+ @Test
+ fun `test match when firstTimeOnly is true and profileProperty is first time proceeds with other checks`() {
+ // Given
+ val eventName = "blood_sugar_first_time_event"
+ val profileAttrName = "Blood Sugar"
+ val trigger = createTriggerAdapter(
+ eventName = eventName,
+ firstTimeOnly = true,
+ profileAttrName = profileAttrName
+ )
+ val event = createEventAdapter(eventName)
+ every { localDataStore.isUserEventLogFirstTime(profileAttrName) } returns true
+
+ // When
+ val result = triggersMatcher.match(trigger, event)
+
+ // Then
+ assertTrue(result)
+ verify { localDataStore.isUserEventLogFirstTime(profileAttrName) }
+ }
+
+ @Test
+ fun `test match when firstTimeOnly is false skips firstTime check`() {
+ // Given
+ val trigger = createTriggerAdapter(
+ eventName = "EventA",
+ firstTimeOnly = false
+ )
+ val event = createEventAdapter("EventA")
+
+ // When
+ val result = triggersMatcher.match(trigger, event)
+
+ // Then
+ assertTrue(result)
+ verify(exactly = 0) { localDataStore.isUserEventLogFirstTime(any()) }
+ }
+
@Test
fun testMatch_WhenChargedEventItemPropertyConditionsAreMet_ShouldReturnTrue() {
val trigger = createTriggerAdapter(
@@ -1525,10 +1629,12 @@ class TriggersMatcherTest : BaseTestCase() {
propertyConditions: List = emptyList(),
itemConditions: List = emptyList(),
geoRadiusConditions: List = emptyList(),
- profileAttrName: String? = null
+ profileAttrName: String? = null,
+ firstTimeOnly: Boolean = false
): TriggerAdapter {
val triggerJSON = JSONObject().apply {
put("eventName", eventName)
+ put(TriggerAdapter.KEY_FIRST_TIME_ONLY, firstTimeOnly)
if(profileAttrName != null)
put("profileAttrName",profileAttrName)
if (propertyConditions.isNotEmpty()) {
@@ -1563,7 +1669,7 @@ class TriggersMatcherTest : BaseTestCase() {
return JSONObject().apply {
put("propertyName", condition.propertyName)
put("operator", condition.op.operatorValue)
- put(Constants.KEY_PROPERTY_VALUE, condition.value.value)
+ put(KEY_PROPERTY_VALUE, condition.value.value)
}
}
}
diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/network/NetworkManagerTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/network/NetworkManagerTest.kt
index d546dda9b..2182f3bb9 100644
--- a/clevertap-core/src/test/java/com/clevertap/android/sdk/network/NetworkManagerTest.kt
+++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/network/NetworkManagerTest.kt
@@ -5,11 +5,8 @@ import com.clevertap.android.sdk.CallbackManager
import com.clevertap.android.sdk.Constants
import com.clevertap.android.sdk.ControllerManager
import com.clevertap.android.sdk.CoreMetaData
-import com.clevertap.android.sdk.LocalDataStoreProvider
import com.clevertap.android.sdk.MockCoreState
import com.clevertap.android.sdk.MockDeviceInfo
-import com.clevertap.android.sdk.cryption.CryptHandler
-import com.clevertap.android.sdk.cryption.CryptHandler.EncryptionAlgorithm.AES
import com.clevertap.android.sdk.db.DBManager
import com.clevertap.android.sdk.events.EventGroup.PUSH_NOTIFICATION_VIEWED
import com.clevertap.android.sdk.events.EventGroup.REGULAR
@@ -24,12 +21,15 @@ import com.clevertap.android.sdk.response.InAppResponse
import com.clevertap.android.sdk.validation.ValidationResultStack
import com.clevertap.android.sdk.validation.Validator
import com.clevertap.android.shared.test.BaseTestCase
-import io.mockk.*
+import io.mockk.mockk
import org.json.JSONObject
-import org.junit.*
-import org.junit.runner.*
-import org.mockito.*
-import org.mockito.Mockito.*
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
import org.robolectric.RobolectricTestRunner
import kotlin.test.assertEquals
import kotlin.test.assertFalse
@@ -156,15 +156,12 @@ class NetworkManagerTest : BaseTestCase() {
private fun provideNetworkManager(): NetworkManager {
val metaData = CoreMetaData()
val deviceInfo = MockDeviceInfo(application, cleverTapInstanceConfig, "clevertapId", metaData)
- val coreState = MockCoreState(appCtx, cleverTapInstanceConfig)
+ val coreState = MockCoreState(cleverTapInstanceConfig)
val callbackManager = CallbackManager(cleverTapInstanceConfig, deviceInfo)
val lockManager = CTLockManager()
val dbManager = DBManager(cleverTapInstanceConfig, lockManager)
val controllerManager =
ControllerManager(appCtx, cleverTapInstanceConfig, lockManager, callbackManager, deviceInfo, dbManager)
- val cryptHandler = CryptHandler(0, AES, cleverTapInstanceConfig.accountId)
- val localDataStore =
- LocalDataStoreProvider.provideLocalDataStore(appCtx, cleverTapInstanceConfig, cryptHandler, deviceInfo)
val triggersManager = TriggerManager(appCtx, cleverTapInstanceConfig.accountId, deviceInfo)
val inAppResponse =
InAppResponse(
diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/usereventlogs/UserEventLogDAOImplTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/usereventlogs/UserEventLogDAOImplTest.kt
new file mode 100644
index 000000000..9913096c4
--- /dev/null
+++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/usereventlogs/UserEventLogDAOImplTest.kt
@@ -0,0 +1,1064 @@
+package com.clevertap.android.sdk.usereventlogs
+
+import android.content.Context
+import android.database.sqlite.SQLiteException
+import com.clevertap.android.sdk.CleverTapInstanceConfig
+import com.clevertap.android.sdk.Logger
+import com.clevertap.android.sdk.Utils
+import com.clevertap.android.sdk.db.DatabaseHelper
+import com.clevertap.android.sdk.db.Table
+import com.clevertap.android.shared.test.TestApplication
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.unmockkStatic
+import io.mockk.verify
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import kotlin.test.assertFalse
+
+@RunWith(RobolectricTestRunner::class)
+class UserEventLogDAOImplTest {
+
+ private lateinit var userEventLogDAO: UserEventLogDAOImpl
+ private lateinit var databaseHelper: DatabaseHelper
+ private lateinit var logger: Logger
+ private lateinit var table: Table
+ private lateinit var context: Context
+ private lateinit var config: CleverTapInstanceConfig
+
+ private val accID = "accountID"
+ private val accToken = "token"
+ private val accRegion = "sk1"
+ private val testDeviceId = UserEventLogTestData.TestDeviceIds.SAMPLE_DEVICE_ID
+ private val testEventName = UserEventLogTestData.EventNames.TEST_EVENT
+ private val testEventName2 = UserEventLogTestData.EventNames.TEST_EVENT_2
+ private val testEventNameNormalized = UserEventLogTestData.EventNames.eventNameToNormalizedMap[testEventName]!!
+ private val testEventNameNormalized2 = UserEventLogTestData.EventNames.eventNameToNormalizedMap[testEventName2]!!
+ private val setOfActualAndNormalizedEventNamePair = UserEventLogTestData.EventNames.setOfActualAndNormalizedEventNamePair
+
+
+ companion object {
+ private const val TEST_EVENT_NAME_2 = "TEST_EVENT_2"
+ private const val TEST_EVENT_NAME_2_NORMALIZED = "TEST_EVENT_2"
+ private const val TEST_DB_NAME = "test_clevertap.db"
+ private const val MOCK_TIME = 1234567890L
+ }
+
+ @Before
+ fun setUp() {
+ context = TestApplication.application
+ logger = mockk(relaxed = true)
+ config = CleverTapInstanceConfig.createInstance(context, accID, accToken, accRegion)
+ table = Table.USER_EVENT_LOGS_TABLE
+
+ databaseHelper = DatabaseHelper(context, config, TEST_DB_NAME, logger)
+ userEventLogDAO = UserEventLogDAOImpl(databaseHelper, logger, table)
+
+ mockkStatic(Utils::class)
+ every { Utils.getNowInMillis() } returns MOCK_TIME
+ }
+
+ @After
+ fun tearDown() {
+ databaseHelper.close()
+ context.getDatabasePath(TEST_DB_NAME).delete()
+ unmockkStatic(Utils::class)
+ }
+
+ @Test
+ fun `test insertEvent when below memory threshold`() {
+
+ // When
+ val result = userEventLogDAO.insertEvent(testDeviceId, testEventName, testEventNameNormalized)
+
+ // Then
+ assertTrue(result > 0)
+ }
+
+ @Test
+ fun `test insertEvent when above memory threshold`() {
+ // Given
+ val dbHelper = mockk(relaxed = true)
+ every { dbHelper.belowMemThreshold() } returns false
+ val dao = UserEventLogDAOImpl(dbHelper, logger, table)
+
+ // When
+ val result = dao.insertEvent(testDeviceId, testEventName, testEventNameNormalized)
+
+ // Then
+ assertEquals(-2L, result) // DB_OUT_OF_MEMORY_ERROR
+ }
+
+ @Test
+ fun `test insertEvent when db error occurs`() {
+ // Given
+ val dbHelper = mockk(relaxed = true)
+ every { dbHelper.belowMemThreshold() } returns true
+ every { dbHelper.writableDatabase.insertWithOnConflict(
+ any(),
+ isNull(),
+ any(),
+ any()
+ ) } throws SQLiteException()
+
+ val dao = UserEventLogDAOImpl(dbHelper, logger, table)
+
+ // When
+ val result = dao.insertEvent(testDeviceId, testEventName, testEventNameNormalized)
+
+ // Then
+ assertEquals(-1L, result) // DB_UPDATE_ERROR
+
+ verify {
+ dbHelper.deleteDatabase()
+ }
+
+ }
+
+ @Test
+ fun `test updateEventByDeviceIdAndNormalizedEventName success`() {
+ // Given
+ val insertResult = userEventLogDAO.insertEvent(testDeviceId,testEventName, testEventNameNormalized)
+ assertTrue(insertResult > 0)
+
+ // When
+ val updateResult1 = userEventLogDAO.updateEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized)
+ val updateResult2 = userEventLogDAO.updateEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized)
+
+ // Then
+ assertTrue(updateResult1)
+ assertTrue(updateResult2)
+
+ // Verify count increased
+ val count = userEventLogDAO.readEventCountByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized)
+ assertEquals(3, count)
+ }
+
+ @Test
+ fun `test updateEventByDeviceIdAndNormalizedEventName when db error occurs`() {
+ // Given
+ val dbHelper = mockk(relaxed = true)
+ every {
+ dbHelper.writableDatabase.execSQL(
+ any(),
+ any()
+ )
+ } throws SQLiteException()
+
+ val dao = UserEventLogDAOImpl(dbHelper, logger, table)
+
+ // When
+ val result = dao.updateEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized)
+
+ // Then
+ assertFalse(result)
+ }
+
+ @Test
+ fun `test updateEventByDeviceIdAndNormalizedEventName success with timestamp verification`() {
+ // Given
+ userEventLogDAO.insertEvent(testDeviceId, testEventName, testEventNameNormalized)
+
+ // Mock different time for update
+ val updateTime = MOCK_TIME + 1000
+ every { Utils.getNowInMillis() } returns updateTime
+
+ // When
+ val result = userEventLogDAO.updateEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized)
+
+ // Then
+ assertTrue(result)
+
+ // Verify event details
+ val eventLog = userEventLogDAO.readEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized)
+ assertNotNull(eventLog)
+ with(requireNotNull(eventLog)) {
+ assertEquals(2, countOfEvents)
+ assertEquals(MOCK_TIME, firstTs)
+ assertEquals(updateTime, lastTs)
+ }
+
+ verify {
+ Utils.getNowInMillis()
+ }
+ }
+
+ @Test
+ fun `test upsertEventsByDeviceIdAndNormalizedEventName with new and existing events`() {
+ // Given
+ userEventLogDAO.insertEvent(testDeviceId, testEventName, testEventNameNormalized)
+
+ // When
+ val result = userEventLogDAO.upsertEventsByDeviceIdAndNormalizedEventName(testDeviceId, setOfActualAndNormalizedEventNamePair)
+
+ // Then
+ assertTrue(result)
+ assertEquals(2, userEventLogDAO.readEventCountByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized))
+ assertEquals(1, userEventLogDAO.readEventCountByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized2))
+ }
+
+ @Test
+ fun `test upsertEventsByDeviceIdAndNormalizedEventName when db error occurs`() {
+ // Given
+ val dbHelper = mockk(relaxed = true)
+ every { dbHelper.writableDatabase.beginTransaction() } throws SQLiteException()
+
+ val dao = UserEventLogDAOImpl(dbHelper, logger, table)
+
+ // When
+ val result = dao.upsertEventsByDeviceIdAndNormalizedEventName(testDeviceId, setOfActualAndNormalizedEventNamePair)
+
+ // Then
+ assertFalse(result)
+
+ verify {
+ dbHelper.writableDatabase.beginTransaction()
+ dbHelper.writableDatabase.endTransaction() // verify that endTransaction is called even when error occurs
+ }
+ }
+
+ @Test
+ fun `test readEventByDeviceIdAndNormalizedEventName returns null when event does not exist`() {
+ // When
+ val result = userEventLogDAO.readEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized)
+
+ // Then
+ assertNull(result)
+ }
+
+ @Test
+ fun `test readEventByDeviceIdAndNormalizedEventName returns correct event log after insert`() {
+ // Given
+ userEventLogDAO.insertEvent(testDeviceId,testEventName, testEventNameNormalized)
+
+ // When
+ val result = userEventLogDAO.readEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized)
+
+ // Then
+ assertNotNull(result)
+ with(requireNotNull(result)) {
+ assertEquals(testEventName, eventName)
+ assertEquals(testEventNameNormalized, normalizedEventName)
+ assertEquals(testDeviceId, deviceID)
+ assertEquals(1, countOfEvents)
+ assertEquals(MOCK_TIME, firstTs)
+ assertEquals(MOCK_TIME, lastTs)
+ }
+ verify {
+ Utils.getNowInMillis()
+ }
+ }
+
+ @Test
+ fun `test readEventByDeviceIdAndNormalizedEventName after multiple updates`() {
+ // Given
+ userEventLogDAO.insertEvent(testDeviceId,testEventName, testEventNameNormalized)
+
+ // Mock different time for update
+ val updateTime = MOCK_TIME + 1000
+ every { Utils.getNowInMillis() } returns updateTime
+
+ userEventLogDAO.updateEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized)
+
+ // When
+ val result = userEventLogDAO.readEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized)
+
+ // Then
+ assertNotNull(result)
+ with(requireNotNull(result)) {
+ assertEquals(testEventName, eventName)
+ assertEquals(testEventNameNormalized, normalizedEventName)
+ assertEquals(testDeviceId, deviceID)
+ assertEquals(2, countOfEvents)
+ assertEquals(MOCK_TIME, firstTs) // First timestamp should remain same
+ assertEquals(updateTime, lastTs) // Last timestamp should be updated
+ }
+ verify {
+ Utils.getNowInMillis()
+ }
+ }
+
+ @Test
+ fun `test readEventByDeviceIdAndNormalizedEventName when db error occurs`() {
+ // Given
+ val dbHelper = mockk()
+ every { dbHelper.readableDatabase } throws SQLiteException()
+ val dao = UserEventLogDAOImpl(dbHelper, logger, table)
+
+ // When
+ val result = dao.readEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized)
+
+ // Then
+ assertNull(result)
+ }
+
+ @Test
+ fun `test readEventCountByDeviceIdAndNormalizedEventName returns 0 when event does not exist`() {
+ // When
+ val result = userEventLogDAO.readEventCountByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized)
+
+ // Then
+ assertEquals(0, result)
+ }
+
+ @Test
+ fun `test readEventCountByDeviceIdAndNormalizedEventName when db error occurs`() {
+ // Given
+ val dbHelper = mockk()
+ every { dbHelper.readableDatabase } throws SQLiteException()
+ val dao = UserEventLogDAOImpl(dbHelper, logger, table)
+
+ // When
+ val result = dao.readEventCountByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized)
+
+ // Then
+ assertEquals(-1, result)
+ }
+
+ @Test
+ fun `test readEventCountByDeviceIdAndNormalizedEventName returns correct count after insert`() {
+ // Given
+ userEventLogDAO.insertEvent(testDeviceId, testEventName, testEventNameNormalized)
+
+ // When
+ val result = userEventLogDAO.readEventCountByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized)
+
+ // Then
+ assertEquals(1, result)
+ }
+
+ @Test
+ fun `test readEventCountByDeviceIdAndNormalizedEventName returns correct count after multiple updates`() {
+ // Given
+ userEventLogDAO.insertEvent(testDeviceId,testEventName, testEventNameNormalized)
+ userEventLogDAO.updateEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized)
+ userEventLogDAO.updateEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized)
+
+ // When
+ val result = userEventLogDAO.readEventCountByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized)
+
+ // Then
+ assertEquals(3, result)
+ }
+
+ @Test
+ fun `test readEventByDeviceIdAndNormalizedEventName returns correct first timestamp after insert`() {
+ // Given
+ userEventLogDAO.insertEvent(testDeviceId, testEventName, testEventNameNormalized)
+
+ // When
+ val eventLog = userEventLogDAO.readEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized)
+
+ // Then
+ assertEquals(MOCK_TIME, eventLog?.firstTs)
+ }
+
+ @Test
+ fun `test readEventByDeviceIdAndNormalizedEventName returns same timestamp after updates`() {
+ // Given
+ userEventLogDAO.insertEvent(testDeviceId,testEventName, testEventNameNormalized)
+
+ // Mock different time for update
+ val updateTime = MOCK_TIME + 1000
+ every { Utils.getNowInMillis() } returns updateTime
+
+ userEventLogDAO.updateEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized)
+ userEventLogDAO.updateEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized)
+
+ // When
+ val eventLog = userEventLogDAO.readEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized)
+
+ // Then
+ assertEquals(MOCK_TIME, eventLog?.firstTs) // First timestamp should remain same after updates
+
+ verify {
+ Utils.getNowInMillis()
+ }
+ }
+
+ @Test
+ fun `test readEventByDeviceIdAndNormalizedEventName returns correct last timestamp after insert`() {
+ // Given
+ userEventLogDAO.insertEvent(testDeviceId,testEventName, testEventNameNormalized)
+
+ // When
+ val eventLog = userEventLogDAO.readEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized)
+
+ // Then
+ assertEquals(MOCK_TIME, eventLog?.lastTs)
+ }
+
+ @Test
+ fun `test readEventByDeviceIdAndNormalizedEventName returns updated last timestamp after updates`() {
+ // Given
+ userEventLogDAO.insertEvent(testDeviceId,testEventName, testEventNameNormalized)
+
+ // Mock different time for update
+ val updateTime = MOCK_TIME + 1000
+ every { Utils.getNowInMillis() } returns updateTime
+
+ userEventLogDAO.updateEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized)
+
+ // When
+ val eventLog = userEventLogDAO.readEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized)
+
+ // Then
+ assertEquals(updateTime, eventLog?.lastTs) // Last timestamp should be updated
+
+ verify {
+ Utils.getNowInMillis()
+ }
+ }
+
+ @Test
+ fun `test eventExistsByDeviceIdAndNormalizedEventName returns false when event does not exist`() {
+ // When
+ val result = userEventLogDAO.eventExistsByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized)
+
+ // Then
+ assertFalse(result)
+ }
+
+ @Test
+ fun `test eventExistsByDeviceIdAndNormalizedEventName when db error occurs`() {
+ // Given
+ val dbHelper = mockk()
+ every { dbHelper.readableDatabase } throws SQLiteException()
+ val dao = UserEventLogDAOImpl(dbHelper, logger, table)
+
+ // When
+ val result = dao.eventExistsByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized)
+
+ // Then
+ assertFalse(result)
+ }
+
+ @Test
+ fun `test eventExistsByDeviceIdAndNormalizedEventName returns true after insert`() {
+ // Given
+ userEventLogDAO.insertEvent(testDeviceId,testEventName, testEventNameNormalized)
+
+ // When
+ val result = userEventLogDAO.eventExistsByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized)
+
+ // Then
+ assertTrue(result)
+ }
+
+ @Test
+ fun `test eventExistsByDeviceIdAndNormalizedEventName returns true for specific deviceID only`() {
+ // Given
+ val otherDeviceId = "other_device_id"
+ userEventLogDAO.insertEvent(testDeviceId,testEventName, testEventNameNormalized)
+
+ // When
+ val resultForTestDevice = userEventLogDAO.eventExistsByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized)
+ val resultForOtherDevice = userEventLogDAO.eventExistsByDeviceIdAndNormalizedEventName(otherDeviceId, testEventNameNormalized)
+
+ // Then
+ assertTrue(resultForTestDevice)
+ assertFalse(resultForOtherDevice)
+ }
+
+ @Test
+ fun `test eventExistsByDeviceIdAndNormalizedEventNameAndCount returns false when event does not exist`() {
+ // When
+ val result = userEventLogDAO.eventExistsByDeviceIdAndNormalizedEventNameAndCount(testDeviceId, testEventNameNormalized, 1)
+
+ // Then
+ assertFalse(result)
+ }
+
+ @Test
+ fun `test eventExistsByDeviceIdAndNormalizedEventNameAndCount when db error occurs`() {
+ // Given
+ val dbHelper = mockk()
+ every { dbHelper.readableDatabase } throws SQLiteException()
+ val dao = UserEventLogDAOImpl(dbHelper, logger, table)
+
+ // When
+ val result = dao.eventExistsByDeviceIdAndNormalizedEventNameAndCount(testDeviceId, testEventNameNormalized, 1)
+
+ // Then
+ assertFalse(result)
+ }
+
+ @Test
+ fun `test eventExistsByDeviceIdAndNormalizedEventNameAndCount returns true for matching count`() {
+ // Given
+ userEventLogDAO.insertEvent(testDeviceId, testEventName, testEventNameNormalized)
+
+ // When
+ val result = userEventLogDAO.eventExistsByDeviceIdAndNormalizedEventNameAndCount(testDeviceId, testEventNameNormalized, 1)
+
+ // Then
+ assertTrue(result)
+ }
+
+ @Test
+ fun `test eventExistsByDeviceIdAndNormalizedEventNameAndCount returns false for non-matching count`() {
+ // Given
+ userEventLogDAO.insertEvent(testDeviceId,testEventName, testEventNameNormalized)
+
+ // When
+ val result = userEventLogDAO.eventExistsByDeviceIdAndNormalizedEventNameAndCount(testDeviceId, testEventNameNormalized, 2)
+
+ // Then
+ assertFalse(result)
+ }
+
+ @Test
+ fun `test eventExistsByDeviceIdAndNormalizedEventNameAndCount verifies count after updates`() {
+ // Given
+ userEventLogDAO.insertEvent(testDeviceId, testEventName, testEventNameNormalized)
+ userEventLogDAO.updateEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized)
+
+ // When
+ val resultForCount1 = userEventLogDAO.eventExistsByDeviceIdAndNormalizedEventNameAndCount(testDeviceId, testEventNameNormalized, 1)
+ val resultForCount2 = userEventLogDAO.eventExistsByDeviceIdAndNormalizedEventNameAndCount(testDeviceId, testEventNameNormalized, 2)
+
+ // Then
+ assertFalse(resultForCount1)
+ assertTrue(resultForCount2)
+ }
+
+ @Test fun `test eventExistsByDeviceIdAndNormalizedEventNameAndCount returns true for specific deviceID and count`(){
+ // Given
+ val otherDeviceId = "other_device_id"
+ userEventLogDAO.insertEvent(testDeviceId,testEventName, testEventNameNormalized)
+ userEventLogDAO.insertEvent(otherDeviceId, testEventName, testEventNameNormalized)
+ userEventLogDAO.updateEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized)
+
+ // When
+ val resultForTestDevice = userEventLogDAO.eventExistsByDeviceIdAndNormalizedEventNameAndCount(testDeviceId, testEventNameNormalized, 2)
+ val resultForOtherDevice = userEventLogDAO.eventExistsByDeviceIdAndNormalizedEventNameAndCount(otherDeviceId, testEventNameNormalized, 1)
+
+ // Then
+ assertTrue(resultForTestDevice)
+ assertTrue(resultForOtherDevice)
+ }
+
+ @Test
+ fun `test allEventsByDeviceID returns empty list when no events exist`() {
+ // When
+ val result = userEventLogDAO.allEventsByDeviceID(testDeviceId)
+
+ // Then
+ assertTrue(result.isEmpty())
+ }
+
+ @Test
+ fun `test allEventsByDeviceID when db error occurs`() {
+ // Given
+ val dbHelper = mockk()
+ every { dbHelper.readableDatabase } throws SQLiteException()
+ val dao = UserEventLogDAOImpl(dbHelper, logger, table)
+
+ // When
+ val result = dao.allEventsByDeviceID(testDeviceId)
+
+ // Then
+ assertTrue(result.isEmpty())
+ }
+
+ @Test
+ fun `test allEventsByDeviceID returns correct list after inserts`() {
+ // Given
+ setOfActualAndNormalizedEventNamePair.forEach {
+ userEventLogDAO.insertEvent(testDeviceId, it.first,it.second)
+ }
+
+ // When
+ val result = userEventLogDAO.allEventsByDeviceID(testDeviceId)
+
+ // Then
+ assertEquals(2, result.size)
+
+ assertTrue(result.all {
+ setOfActualAndNormalizedEventNamePair.contains(Pair(it.eventName,it.normalizedEventName)) && it.deviceID == testDeviceId
+ })
+ }
+
+ @Test
+ fun `test allEventsByDeviceID returns events for specific deviceID only`() {
+ // Given
+ val otherDeviceId = "other_device_id"
+ listOf(testDeviceId, otherDeviceId).forEach { deviceId ->
+ userEventLogDAO.insertEvent(deviceId,testEventName, testEventNameNormalized)
+ }
+
+ // When
+ val results = listOf(testDeviceId, otherDeviceId)
+ .associateWith { deviceId ->
+ userEventLogDAO.allEventsByDeviceID(deviceId)
+ }
+
+ // Then
+ results.forEach { (deviceId, events) ->
+ assertEquals(1, events.size)
+ assertTrue(events.all { it.deviceID == deviceId })
+ }
+ }
+
+ @Test
+ fun `test allEventsByDeviceID returns events ordered by lastTs`() {
+ // Given
+ userEventLogDAO.insertEvent(testDeviceId,testEventName, testEventNameNormalized)
+
+ // Mock different time for second event
+ val laterTime = MOCK_TIME + 1000
+ every { Utils.getNowInMillis() } returns laterTime
+
+ userEventLogDAO.insertEvent(testDeviceId, testEventName2, testEventNameNormalized2)
+
+ // When
+ val result = userEventLogDAO.allEventsByDeviceID(testDeviceId)
+
+ // Then
+ assertEquals(2, result.size)
+ assertEquals(testEventNameNormalized, result[0].normalizedEventName) // Earlier event first
+ assertEquals(testEventNameNormalized2, result[1].normalizedEventName) // Later event second
+ assertTrue(result[0].lastTs < result[1].lastTs)
+
+ verify {
+ Utils.getNowInMillis()
+ }
+ }
+
+ @Test
+ fun `test allEvents returns empty list when no events exist`() {
+ // When
+ val result = userEventLogDAO.allEvents()
+
+ // Then
+ assertTrue(result.isEmpty())
+ }
+
+ @Test
+ fun `test allEvents when db error occurs`() {
+ // Given
+ val dbHelper = mockk()
+ every { dbHelper.readableDatabase } throws SQLiteException()
+ val dao = UserEventLogDAOImpl(dbHelper, logger, table)
+
+ // When
+ val result = dao.allEvents()
+
+ // Then
+ assertTrue(result.isEmpty())
+ }
+
+ @Test
+ fun `test allEvents returns all events from different users`() {
+ // Given
+ val deviceIds = listOf(testDeviceId, "other_device_id")
+
+ deviceIds.forEach { deviceId ->
+ setOfActualAndNormalizedEventNamePair.forEach { pair ->
+ userEventLogDAO.insertEvent(deviceId, pair.first, pair.second)
+ }
+ }
+
+ // When
+ val result = userEventLogDAO.allEvents()
+
+ // Then
+ assertEquals(4, result.size)
+ result.all {
+ deviceIds.contains(it.deviceID) && setOfActualAndNormalizedEventNamePair.contains(
+ Pair(
+ it.eventName,
+ it.normalizedEventName
+ )
+ )
+ }
+ }
+
+ @Test
+ fun `test allEvents returns events ordered by lastTs`() {
+ // Given
+
+ setOfActualAndNormalizedEventNamePair.forEachIndexed { index, pair ->
+ val mockTime = MOCK_TIME + (index * 1000L)
+ every { Utils.getNowInMillis() } returns mockTime
+ userEventLogDAO.insertEvent(testDeviceId,pair.first, pair.second)
+ }
+
+ // When
+ val result = userEventLogDAO.allEvents()
+
+ // Then
+ assertEquals(2, result.size)
+ result.zipWithNext { a, b ->
+ assertTrue(a.lastTs <= b.lastTs)
+ }
+
+ verify {
+ Utils.getNowInMillis()
+ }
+ }
+
+ @Test
+ fun `test allEvents returns correct event details`() {
+ // Given
+ data class EventData(val deviceId: String, val name: String, val normalizedName: String, val count: Int)
+
+ val testData = listOf(
+ EventData(testDeviceId, testEventName, testEventNameNormalized, 2),
+ EventData("other_device_id", testEventName2, testEventNameNormalized2, 1)
+ )
+
+ testData.forEach { (deviceId, eventName, normalizedName, updateCount) ->
+ userEventLogDAO.insertEvent(deviceId, eventName, normalizedName)
+ repeat(updateCount - 1) {
+ userEventLogDAO.updateEventByDeviceIdAndNormalizedEventName(deviceId, normalizedName)
+ }
+ }
+
+ // When
+ val result = userEventLogDAO.allEvents()
+ .groupBy { it.deviceID }
+ .mapValues { (_, events) -> events.associateBy { it.eventName } }
+
+ // Then
+ testData.forEach { (deviceId, eventName, normalizedName, expectedCount) ->
+ val event = result[deviceId]?.get(eventName)
+ assertNotNull(event)
+ with(requireNotNull(event)) {
+ assertEquals(deviceId, this.deviceID)
+ assertEquals(eventName, this.eventName)
+ assertEquals(normalizedName, this.normalizedEventName)
+ assertEquals(expectedCount, this.countOfEvents)
+ assertEquals(MOCK_TIME, this.firstTs)
+ assertEquals(MOCK_TIME, this.lastTs)
+ }
+ }
+ }
+
+ @Test
+ fun `test cleanUpExtraEvents with zero threshold`() {
+ // When
+ val result = userEventLogDAO.cleanUpExtraEvents(0, 2)
+
+ // Then
+ assertFalse(result)
+ }
+
+ @Test
+ fun `test cleanUpExtraEvents with negative threshold`() {
+ // When
+ val result = userEventLogDAO.cleanUpExtraEvents(-5, 2)
+
+ // Then
+ assertFalse(result)
+ }
+
+ @Test
+ fun `test cleanUpExtraEvents with negative numberOfRowsToCleanup`() {
+ // When
+ val result = userEventLogDAO.cleanUpExtraEvents(5, -2)
+
+ // Then
+ assertFalse(result)
+ }
+
+ @Test
+ fun `test cleanUpExtraEvents with zero numberOfRowsToCleanup`() {
+ // When
+ val result = userEventLogDAO.cleanUpExtraEvents(5, 0)
+
+ // Then
+ assertTrue(result) // Should pass as 0 is valid now
+ }
+
+ @Test
+ fun `test cleanUpExtraEvents with numberOfRowsToCleanup equal to threshold`() {
+ // When
+ val result = userEventLogDAO.cleanUpExtraEvents(5, 5)
+
+ // Then
+ assertFalse(result)
+ }
+
+ @Test
+ fun `test cleanUpExtraEvents with numberOfRowsToCleanup greater than threshold`() {
+ // When
+ val result = userEventLogDAO.cleanUpExtraEvents(5, 6)
+
+ // Then
+ assertFalse(result)
+ }
+
+ @Test
+ fun `test cleanUpExtraEvents validation ensures database is not modified with invalid params`() {
+ // Given
+ val events = (1..5).map { "event_$it" }
+ val setOfActualAndNormalizedEventNamePair = events.map {
+ Pair(it, Utils.getNormalizedName(it))
+ }.toSet()
+ setOfActualAndNormalizedEventNamePair.forEach { pair ->
+ userEventLogDAO.insertEvent(testDeviceId, pair.first,pair.second)
+ }
+
+ // When
+ val result = userEventLogDAO.cleanUpExtraEvents(5, 6)
+
+ // Then
+ assertFalse(result)
+ assertEquals(5, userEventLogDAO.allEvents().size) // Verify no events were deleted
+ }
+
+ @Test
+ fun `test cleanUpExtraEvents when no events exist`() {
+ // When
+ val result = userEventLogDAO.cleanUpExtraEvents(5, 2)
+
+ // Then
+ assertTrue(result)
+ assertTrue(userEventLogDAO.allEvents().isEmpty())
+ }
+
+ @Test
+ fun `test cleanUpExtraEvents when db error occurs`() {
+ // Given
+ val dbHelper = mockk()
+ every { dbHelper.writableDatabase } throws SQLiteException()
+ val dao = UserEventLogDAOImpl(dbHelper, logger, table)
+
+ // When
+ val result = dao.cleanUpExtraEvents(5, 2)
+
+ // Then
+ assertFalse(result)
+ }
+
+ @Test
+ fun `test cleanUpExtraEvents deletes correct number of events when above threshold`() {
+ // Given
+ val events = (1..10).map { "event_$it" }
+ val setOfActualAndNormalizedEventNamePair = events.map {
+ Pair(it, Utils.getNormalizedName(it))
+ }.toSet()
+
+ setOfActualAndNormalizedEventNamePair.forEachIndexed { index, pair ->
+ val mockTime = MOCK_TIME + (index * 1000L)
+ every { Utils.getNowInMillis() } returns mockTime
+ userEventLogDAO.insertEvent(testDeviceId, pair.first,pair.second)
+ }
+
+ val threshold = 6
+ val numberOfRowsToCleanup = 2
+
+ // When
+ val result = userEventLogDAO.cleanUpExtraEvents(threshold, numberOfRowsToCleanup)
+
+ // Then
+ assertTrue(result)
+
+ val remainingEvents = userEventLogDAO.allEvents()
+ assertEquals(threshold - numberOfRowsToCleanup, remainingEvents.size) // Should have 4 events remaining
+
+ // Verify oldest events were deleted and newest remain
+ remainingEvents
+ .map { it.normalizedEventName }
+ .let { normalizedEventNames ->
+ // First 6 events should be deleted (10 - 4 = 6)
+ (1..6).forEach {
+ assertFalse(normalizedEventNames.contains("event_$it"))
+ }
+ // Last 4 events should remain
+ (7..10).forEach {
+ assertTrue(normalizedEventNames.contains("event_$it"))
+ }
+ }
+
+ verify {
+ Utils.getNowInMillis()
+ }
+ }
+
+ @Test
+ fun `test cleanUpExtraEvents maintains events when below threshold`() {
+ // Given
+ val events = (1..3).map { "event_$it" }
+ val setOfActualAndNormalizedEventNamePair = events.map {
+ Pair(it, Utils.getNormalizedName(it))
+ }.toSet()
+
+ setOfActualAndNormalizedEventNamePair.forEachIndexed { index, pair ->
+ val mockTime = MOCK_TIME + (index * 1000L)
+ every { Utils.getNowInMillis() } returns mockTime
+ userEventLogDAO.insertEvent(testDeviceId, pair.first,pair.second)
+ }
+
+ val threshold = 5
+ val numberOfRowsToCleanup = 2
+
+ // When
+ val result = userEventLogDAO.cleanUpExtraEvents(threshold, numberOfRowsToCleanup)
+
+ // Then
+ assertTrue(result)
+
+ userEventLogDAO.allEvents()
+ .map { it.normalizedEventName }
+ .let { normalizedEventNames ->
+ assertEquals(3, normalizedEventNames.size) // All events should remain
+ assertTrue(normalizedEventNames.containsAll(events))
+ }
+
+ verify {
+ Utils.getNowInMillis()
+ }
+ }
+
+ @Test
+ fun `test cleanUpExtraEvents maintains correct order after cleanup`() {
+ // Given
+ val eventCount = 10
+ val events = (1..eventCount).map { "event_$it" }
+ val setOfActualAndNormalizedEventNamePair = events.map {
+ Pair(it, Utils.getNormalizedName(it))
+ }.toSet()
+
+ setOfActualAndNormalizedEventNamePair.forEachIndexed { index, pair ->
+ val mockTime = MOCK_TIME + (index * 1000L)
+ every { Utils.getNowInMillis() } returns mockTime
+ userEventLogDAO.insertEvent(testDeviceId, pair.first, pair.second)
+ }
+
+ val threshold = 6
+ val numberOfRowsToCleanup = 2
+
+ // When
+ val result = userEventLogDAO.cleanUpExtraEvents(threshold, numberOfRowsToCleanup)
+
+ // Then
+ assertTrue(result)
+
+ userEventLogDAO.allEvents().let { remainingEvents ->
+ assertEquals(4, remainingEvents.size) // threshold - numberOfRowsToCleanup
+ remainingEvents.zipWithNext { a, b ->
+ assertTrue(a.lastTs <= b.lastTs)
+ }
+ }
+
+ verify {
+ Utils.getNowInMillis()
+ }
+ }
+
+ @Test
+ fun `test cleanUpExtraEvents with threshold 1 and numberOfRowsToCleanup 0 when single event exists`() {
+ // Given
+ userEventLogDAO.insertEvent(testDeviceId,testEventName, testEventNameNormalized)
+
+ // When
+ val result = userEventLogDAO.cleanUpExtraEvents(1, 0)
+
+ // Then
+ assertTrue(result)
+ userEventLogDAO.allEvents().let { events ->
+ assertEquals(1, events.size)
+ assertEquals(testEventNameNormalized, events[0].normalizedEventName)
+ }
+ }
+
+ @Test
+ fun `test cleanUpExtraEvents with threshold 1 and numberOfRowsToCleanup 0 when multiple events exist`() {
+ // Given
+ val events = (1..3).map { "event_$it" }
+ val setOfActualAndNormalizedEventNamePair = events.map {
+ Pair(it, Utils.getNormalizedName(it))
+ }.toSet()
+
+ setOfActualAndNormalizedEventNamePair.forEachIndexed { index, pair ->
+ val mockTime = MOCK_TIME + (index * 1000L)
+ every { Utils.getNowInMillis() } returns mockTime
+ userEventLogDAO.insertEvent(testDeviceId, pair.first, pair.second)
+ }
+
+ // When
+ val result = userEventLogDAO.cleanUpExtraEvents(1, 0)
+
+ // Then
+ assertTrue(result)
+ userEventLogDAO.allEvents().let { remainingEvents ->
+ assertEquals(1, remainingEvents.size)
+ assertEquals("event_3", remainingEvents[0].normalizedEventName) // Should keep the most recent event
+ assertEquals(MOCK_TIME + 2000L, remainingEvents[0].lastTs)
+ }
+
+ verify {
+ Utils.getNowInMillis()
+ }
+ }
+
+ @Test
+ fun `test cleanUpExtraEvents with threshold 1 and numberOfRowsToCleanup 0 with multiple users`() {
+ // Given
+ val devices = listOf(testDeviceId, "other_device_id")
+
+ devices.forEachIndexed { index, deviceId ->
+ val mockTime = MOCK_TIME + (index * 1000L)
+ every { Utils.getNowInMillis() } returns mockTime
+ userEventLogDAO.insertEvent(deviceId, testEventName, testEventNameNormalized)
+ }
+
+ // When
+ val result = userEventLogDAO.cleanUpExtraEvents(1, 0)
+
+ // Then
+ assertTrue(result)
+ userEventLogDAO.allEvents().let { remainingEvents ->
+ assertEquals(1, remainingEvents.size)
+ // Should keep the last inserted event
+ assertEquals("other_device_id", remainingEvents[0].deviceID)
+ }
+
+ verify {
+ Utils.getNowInMillis()
+ }
+ }
+
+ @Test
+ fun `test cleanUpExtraEvents with threshold 1 and numberOfRowsToCleanup 0 maintains order after cleanup`() {
+ // Given
+ (1..5).forEach { index ->
+ val mockTime = MOCK_TIME + (index * 1000L)
+ every { Utils.getNowInMillis() } returns mockTime
+ val eventName = "EV e n t_$index"
+ userEventLogDAO.insertEvent(testDeviceId, eventName,Utils.getNormalizedName(eventName))
+
+ // Add some updates to earlier events to mix up lastTs
+ if (index > 1) {
+ every { Utils.getNowInMillis() } returns mockTime + 100
+ userEventLogDAO.updateEventByDeviceIdAndNormalizedEventName(testDeviceId, "event_1")
+ }
+ }
+
+ // When
+ val result = userEventLogDAO.cleanUpExtraEvents(1, 0)
+
+ // Then
+ assertTrue(result)
+ userEventLogDAO.allEvents().let { remainingEvents ->
+ assertEquals(1, remainingEvents.size)
+ // Should keep event_1 as it has the latest lastTs due to updates
+ assertEquals("event_1", remainingEvents[0].normalizedEventName)
+ }
+
+ verify {
+ Utils.getNowInMillis()
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/usereventlogs/UserEventLogTestData.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/usereventlogs/UserEventLogTestData.kt
new file mode 100644
index 000000000..515ce3902
--- /dev/null
+++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/usereventlogs/UserEventLogTestData.kt
@@ -0,0 +1,70 @@
+package com.clevertap.android.sdk.usereventlogs
+
+import com.clevertap.android.sdk.Utils.getNormalizedName
+
+object UserEventLogTestData {
+ object EventNames {
+ const val TEST_EVENT = "TeS T"
+ const val TEST_EVENT_2 = "TeS T 2"
+ const val SIMPLE_TEST_EVENT = "test"
+
+ // Map of event names to their normalized versions for easy access
+ val eventNameToNormalizedMap = mapOf(
+ TEST_EVENT to getNormalizedName(TEST_EVENT),
+ TEST_EVENT_2 to getNormalizedName(TEST_EVENT_2),
+ SIMPLE_TEST_EVENT to getNormalizedName(SIMPLE_TEST_EVENT)
+ )
+
+ val eventNames = setOf(TEST_EVENT, TEST_EVENT_2)
+ val setOfActualAndNormalizedEventNamePair = eventNames.map {
+ Pair(it, eventNameToNormalizedMap[it]!!)
+ }.toSet()
+
+ // Sample test data
+ val sampleUserEventLogsForSameDeviceId = listOf(
+ UserEventLog(
+ eventName = TEST_EVENT,
+ normalizedEventName = eventNameToNormalizedMap[TEST_EVENT]!!,
+ firstTs = 1000L,
+ lastTs = 1000L,
+ countOfEvents = 1,
+ deviceID = "dId"
+ ),
+ UserEventLog(
+ eventName = TEST_EVENT_2,
+ normalizedEventName = eventNameToNormalizedMap[TEST_EVENT_2]!!,
+ firstTs = 2000L,
+ lastTs = 2000L,
+ countOfEvents = 2,
+ deviceID = "dId"
+ )
+ )
+ val sampleUserEventLogsForMixedDeviceId = listOf(
+ UserEventLog(
+ eventName = TEST_EVENT,
+ normalizedEventName = eventNameToNormalizedMap[TEST_EVENT]!!,
+ firstTs = 1000L,
+ lastTs = 1000L,
+ countOfEvents = 1,
+ deviceID = "dId1"
+ ),
+ UserEventLog(
+ eventName = TEST_EVENT_2,
+ normalizedEventName = eventNameToNormalizedMap[TEST_EVENT_2]!!,
+ firstTs = 2000L,
+ lastTs = 2000L,
+ countOfEvents = 2,
+ deviceID = "dId2"
+ )
+ )
+ }
+
+ object TestTimestamps {
+ const val SAMPLE_TIMESTAMP = 1000L
+ const val SAMPLE_TIMESTAMP_2 = 2000L
+ }
+
+ object TestDeviceIds {
+ const val SAMPLE_DEVICE_ID = "dId"
+ }
+}
\ No newline at end of file
diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/variables/VarCacheTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/variables/VarCacheTest.kt
index 1909527f9..1c06b1c99 100644
--- a/clevertap-core/src/test/java/com/clevertap/android/sdk/variables/VarCacheTest.kt
+++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/variables/VarCacheTest.kt
@@ -1,23 +1,26 @@
package com.clevertap.android.sdk.variables
+import com.clevertap.android.sdk.Constants
import com.clevertap.android.sdk.StorageHelper
import com.clevertap.android.sdk.inapp.data.CtCacheType.FILES
import com.clevertap.android.sdk.inapp.images.FileResourceProvider
import com.clevertap.android.sdk.inapp.images.repo.FileResourcesRepoImpl
+import com.clevertap.android.sdk.task.CTExecutorFactory
+import com.clevertap.android.sdk.task.MockCTExecutors
import com.clevertap.android.sdk.variables.VariableDefinitions.NullDefaultValue
import com.clevertap.android.sdk.variables.callbacks.VariableCallback
import com.clevertap.android.shared.test.BaseTestCase
import io.mockk.*
import org.junit.*
-import org.junit.Assert.*
+import org.junit.jupiter.api.parallel.Execution
+import org.junit.jupiter.api.parallel.ExecutionMode.SAME_THREAD
import org.junit.runner.*
-import org.mockito.kotlin.*
+import org.mockito.kotlin.notNull
import org.robolectric.RobolectricTestRunner
import kotlin.test.assertEquals
import kotlin.test.assertNull
@RunWith(RobolectricTestRunner::class)
-@Ignore("this is flaky on server, ran it locally")
class VarCacheTest : BaseTestCase() {
private lateinit var varCache: VarCache
@@ -30,6 +33,9 @@ class VarCacheTest : BaseTestCase() {
@Throws(Exception::class)
override fun setUp() {
super.setUp()
+ mockkStatic(CTExecutorFactory::class)
+ every { CTExecutorFactory.executors(any()) } returns MockCTExecutors()
+
fileResourcesRepoImpl = mockk(relaxed = true)
fileResourceProvider = mockk(relaxed = true)
@@ -40,6 +46,17 @@ class VarCacheTest : BaseTestCase() {
parser = Parser(ctVariables)
}
+ @After
+ fun cleanUp() {
+ //clear all varCache stored info
+ val varCacheKey = StorageHelper.storageKeyWithSuffix(
+ cleverTapInstanceConfig,
+ Constants.CACHED_VARIABLES_KEY
+ )
+ StorageHelper.removeImmediate(application, varCacheKey)
+ unmockkStatic(CTExecutorFactory::class)
+ }
+
@Test
fun `test updateDiffsAndTriggerHandlers`() {
ctVariables.init()
@@ -348,6 +365,8 @@ class VarCacheTest : BaseTestCase() {
assertEquals(2, var1.value())
verify { StorageHelper.getString(application, "variablesKey:" + cleverTapInstanceConfig.accountId, "{}") }
+
+ unmockkStatic(StorageHelper::class)
}
@Test
@@ -362,6 +381,8 @@ class VarCacheTest : BaseTestCase() {
assertEquals("http://example.com/file", var1.stringValue)
verify { fileResourcesRepoImpl.preloadFilesAndCache(listOf(Pair("http://example.com/file", FILES)), any()) }
+
+ unmockkStatic(StorageHelper::class)
}
@Test
@@ -377,9 +398,10 @@ class VarCacheTest : BaseTestCase() {
assertEquals(2, var1.value())
verify { StorageHelper.getString(application, "variablesKey:" + cleverTapInstanceConfig.accountId, "{}") }
verify { globalCallbackRunnable.run() }
+
+ unmockkStatic(StorageHelper::class)
}
- @Ignore("this is flaky")
@Test
fun `test clearUserContent`() {
Var.define("var1", 1, ctVariables)
@@ -389,11 +411,15 @@ class VarCacheTest : BaseTestCase() {
varCache.clearUserContent()
verify { StorageHelper.putString(application, "variablesKey:" + cleverTapInstanceConfig.accountId, "{}") }
+
+ unmockkStatic(StorageHelper::class)
}
@Test
fun `test fileVarUpdated when file is cached`() {
- val var1 : Var = Var.define("var1", null, "file", ctVariables)
+ // set initial value which will be considered as the "new" value when
+ // fileVarUpdated is triggered
+ val var1 : Var = Var.define("var1", "value", "file", ctVariables)
val handler1: VariableCallback = mockk(relaxed = true)
var1.addFileReadyHandler(handler1)
@@ -407,7 +433,9 @@ class VarCacheTest : BaseTestCase() {
@Test
fun `test fileVarUpdated when file is not cached`() {
- val var1 : Var = Var.define("var1", null, "file", ctVariables)
+ // set initial value which will be considered as the "new" value when
+ // fileVarUpdated is triggered
+ val var1: Var = Var.define("var1", "value", "file", ctVariables)
val handler1: VariableCallback = mockk(relaxed = true)
var1.addFileReadyHandler(handler1)
@@ -419,4 +447,43 @@ class VarCacheTest : BaseTestCase() {
verify(exactly = 0) { handler1.run() }
verify { fileResourcesRepoImpl.preloadFilesAndCache(any(), any()) }
}
+
+ @Test
+ fun `test fileVarUpdated when url becomes null`() {
+ val var1: Var = Var.define("var1", null, "file", ctVariables)
+ val handler: VariableCallback = mockk(relaxed = true)
+ every { fileResourceProvider.isFileCached(any()) } returns true
+ var1.addFileReadyHandler(handler)
+
+ // update var1 to a value
+ val fileUrl = "http://example.com/file2"
+ ctVariables.setHasVarsRequestCompleted(true)
+ varCache.updateDiffsAndTriggerHandlers(
+ mapOf(
+ "var1" to fileUrl,
+ ), {}
+ )
+
+ verify {
+ handler.setVariable(match {
+ it.rawFileValue() == fileUrl
+ })
+ }
+ verify(exactly = 1) { handler.run() }
+
+ // update var1 to null
+ ctVariables.setHasVarsRequestCompleted(true)
+ varCache.updateDiffsAndTriggerHandlers(
+ mapOf(
+ "var1" to null,
+ ), {}
+ )
+
+ verify {
+ handler.setVariable(match { fileVar ->
+ fileVar.rawFileValue() == null
+ })
+ }
+ verify(exactly = 2) { handler.run() }
+ }
}
diff --git a/clevertap-pushtemplates/src/main/java/com/clevertap/android/pushtemplates/PushTemplateReceiver.java b/clevertap-pushtemplates/src/main/java/com/clevertap/android/pushtemplates/PushTemplateReceiver.java
index b81a8b3f2..61754aba1 100644
--- a/clevertap-pushtemplates/src/main/java/com/clevertap/android/pushtemplates/PushTemplateReceiver.java
+++ b/clevertap-pushtemplates/src/main/java/com/clevertap/android/pushtemplates/PushTemplateReceiver.java
@@ -33,6 +33,7 @@
import com.clevertap.android.sdk.CleverTapAPI;
import com.clevertap.android.sdk.CleverTapInstanceConfig;
import com.clevertap.android.sdk.Constants;
+import com.clevertap.android.sdk.ManifestInfo;
import com.clevertap.android.sdk.interfaces.NotificationHandler;
import com.clevertap.android.sdk.pushnotification.CTNotificationIntentService;
import com.clevertap.android.sdk.pushnotification.LaunchPendingIntentFactory;
@@ -765,7 +766,7 @@ private void setSmallIcon(Context context) {
PackageManager pm = context.getPackageManager();
ApplicationInfo ai = pm.getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
metaData = ai.metaData;
- String x = Utils._getManifestStringValueForKey(metaData, Constants.LABEL_NOTIFICATION_ICON);
+ String x = Utils._getManifestStringValueForKey(metaData, ManifestInfo.LABEL_NOTIFICATION_ICON);
if (x == null) {
throw new IllegalArgumentException();
}
diff --git a/docs/CTCORECHANGELOG.md b/docs/CTCORECHANGELOG.md
index 945381b49..be1630e7d 100644
--- a/docs/CTCORECHANGELOG.md
+++ b/docs/CTCORECHANGELOG.md
@@ -1,5 +1,33 @@
## CleverTap Android SDK CHANGE LOG
+### Version 7.1.0 (December 24, 2024)
+
+#### New Features
+
+* Adds support for triggering InApps based on first-time event filtering in multiple triggers. Now you can create campaign triggers that combine recurring and first-time events. For example: Trigger a campaign when "Charged" occurs (every time) OR "App Launched" occurs (first time only).
+* Adds new user-level event log tracking system to store and manage user event history. New APIs include:
+ * `getUserEventLog()`: Get details about a specific event
+ * `getUserEventLogCount()`: Get count of times an event occurred
+ * `getUserLastVisitTs()`: Get timestamp of user's last app visit
+ * `getUserAppLaunchCount()`: Get total number of times user has launched the app
+ * `getUserEventLogHistory()`: Get full event history for current user
+
+#### API Changes
+
+* **Deprecated:** The old event tracking APIs tracked events at the device level rather than the user level, making it difficult to maintain accurate user-specific event histories, especially in multi-user scenarios. The following methods have been deprecated in favor of new user-specific event tracking APIs that provide more accurate, user-level analytics. These deprecated methods will be removed in future versions with prior notice:
+ * `getDetails()`: Use `getUserEventLog()` instead for user-specific event details
+ * `getCount()`: Use `getUserEventLogCount()` instead for user-specific event counts
+ * `getFirstTime()`: Use `getUserEventLog()` instead for user-specific first occurrence timestamp
+ * `getLastTime()`: Use `getUserEventLog()` instead for user-specific last occurrence timestamp
+ * `getPreviousVisitTime()`: Use `getUserLastVisitTs()` instead for user-specific last visit timestamp
+ * `getTotalVisits()`: Use `getUserAppLaunchCount()` instead for user-specific app launch count
+ * `getHistory()`: Use `getUserEventLogHistory()` instead for user-specific event history
+
+#### Bug Fixes
+* Fixes [#671](https://github.com/CleverTap/clevertap-android-sdk/issues/671) - an `AbstractMethodError` in the AppInbox feature when using audio/video.
+* Fixes issues when File type variable changes from validValue -> null
+
+
### Version 7.0.3 (November 29, 2024)
#### New Features
diff --git a/docs/CTGEOFENCE.md b/docs/CTGEOFENCE.md
index 9aaeaf173..3b21e9cda 100644
--- a/docs/CTGEOFENCE.md
+++ b/docs/CTGEOFENCE.md
@@ -17,7 +17,7 @@ Add the following dependencies to the `build.gradle`
```Groovy
implementation "com.clevertap.android:clevertap-geofence-sdk:1.3.0"
-implementation "com.clevertap.android:clevertap-android-sdk:7.0.3" // 3.9.0 and above
+implementation "com.clevertap.android:clevertap-android-sdk:7.1.0" // 3.9.0 and above
implementation "com.google.android.gms:play-services-location:21.0.0"
implementation "androidx.work:work-runtime:2.7.1" // required for FETCH_LAST_LOCATION_PERIODIC
implementation "androidx.concurrent:concurrent-futures:1.1.0" // required for FETCH_LAST_LOCATION_PERIODIC
diff --git a/docs/CTPUSHTEMPLATES.md b/docs/CTPUSHTEMPLATES.md
index 1936f78d7..c5d480a62 100644
--- a/docs/CTPUSHTEMPLATES.md
+++ b/docs/CTPUSHTEMPLATES.md
@@ -21,7 +21,7 @@ CleverTap Push Templates SDK helps you engage with your users using fancy push n
```groovy
implementation "com.clevertap.android:push-templates:1.2.4"
-implementation "com.clevertap.android:clevertap-android-sdk:7.0.3" // 4.4.0 and above
+implementation "com.clevertap.android:clevertap-android-sdk:7.1.0" // 4.4.0 and above
```
2. Add the following line to your Application class before the `onCreate()`
diff --git a/docs/EXAMPLES.md b/docs/EXAMPLES.md
index 8951afab5..4af7c661b 100644
--- a/docs/EXAMPLES.md
+++ b/docs/EXAMPLES.md
@@ -747,4 +747,79 @@ val clevertapAdditionalInstanceConfig = CleverTapInstanceConfig.createInstance(
clevertapAdditionalInstanceConfig.setEncryptionLevel(CryptHandler.EncryptionLevel.MEDIUM)
val clevertapAdditionalInstance = CleverTapAPI.instanceWithConfig(applicationContext ,clevertapAdditionalInstanceConfig)
+```
+
+### User event logging APIs
+Get user event details
+
+Java
+```java
+UserEventLog eventLog = clevertap.getUserEventLog("Product Viewed");
+if (eventLog != null) {
+ String eventName = eventLog.getEventName();
+ long firstTime = eventLog.getFirstTs();
+ long lastTime = eventLog.getLastTs();
+ int count = eventLog.getCountOfEvents();
+ String deviceId = eventLog.getDeviceID();
+} else {
+ System.out.println("Event not performed");
+}
+```
+Kotlin
+```kotlin
+clevertap.getUserEventLog("Product Viewed")?.let { eventLog ->
+ val eventName = eventLog.eventName
+ val firstTime = eventLog.firstTs
+ val lastTime = eventLog.lastTs
+ val count = eventLog.countOfEvents
+ val deviceId = eventLog.deviceID
+} ?: println("Event not performed")
+```
+Get count of event occurrences
+
+Java
+```java
+int eventCount = clevertap.getUserEventLogCount("Product Viewed");
+```
+Kotlin
+```kotlin
+val eventCount = clevertap.getUserEventLogCount("Product Viewed")
+```
+Get user's last app visit timestamp
+
+Java
+```java
+long lastVisitTs = clevertap.getUserLastVisitTs();
+```
+Kotlin
+```kotlin
+val lastVisitTs = clevertap.userLastVisitTs
+```
+Get total number of app launches by user
+
+Java
+```java
+int appLaunchCount = clevertap.getUserAppLaunchCount();
+```
+Kotlin
+```kotlin
+val appLaunchCount = cleverTapAPI?.userAppLaunchCount
+```
+Get full event history for user
+
+Java
+```java
+Map eventHistory = clevertap.getUserEventLogHistory();
+for (Map.Entry entry : eventHistory.entrySet()) {
+String eventName = entry.getKey();
+UserEventLog log = entry.getValue();
+// Process event details
+}
+```
+Kotlin
+```kotlin
+val eventHistory = clevertap.userEventLogHistory
+eventHistory?.forEach { (eventName, log) ->
+ // Process event details
+} ?: println("Events not performed")
```
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index f98e6ef52..e2ee92529 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -54,7 +54,7 @@ coroutines_test = "1.7.3"
installreferrer = "2.2"
#SDK Versions
-clevertap_android_sdk = "7.0.3"
+clevertap_android_sdk = "7.1.0"
clevertap_rendermax_sdk = "1.0.3"
clevertap_geofence_sdk = "1.3.0"
clevertap_hms_sdk = "1.3.4"
@@ -72,7 +72,7 @@ exoplayer_ui = "2.19.1"
media3 = "1.1.1"
#Play Services
-play_services_ads = "22.3.0"
+play_services_ads = "23.6.0"
play_services_location = "21.0.0"
#Gson
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 57f7924c1..54df2f803 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
#Thu May 18 16:22:56 IST 2023
distributionBase=GRADLE_USER_HOME
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
diff --git a/sample/build.gradle b/sample/build.gradle
index 51f5faff7..4a86a6483 100644
--- a/sample/build.gradle
+++ b/sample/build.gradle
@@ -18,8 +18,8 @@ android {
applicationId "com.clevertap.demo"
minSdkVersion 21
targetSdkVersion 34
- versionCode 7000003
- versionName "7.0.3"
+ versionCode 7000010
+ versionName "7.1.0"
multiDexEnabled true
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
@@ -118,7 +118,7 @@ dependencies {
implementation("androidx.concurrent:concurrent-futures:1.1.0") // Needed for geofence
implementation("com.google.firebase:firebase-messaging:23.0.6") //Needed for FCM
- implementation("com.google.android.gms:play-services-ads:20.4.0") //Needed to use Google Ad Ids
+ implementation("com.google.android.gms:play-services-ads:23.6.0") //Needed to use Google Ad Ids
//ExoPlayer Libraries for Audio/Video InApp Notifications
//implementation("com.google.android.exoplayer:exoplayer:2.19.1")
@@ -159,12 +159,12 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.72"
implementation "org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:3.1.1"*/
- remoteImplementation("com.clevertap.android:clevertap-android-sdk:7.0.3")
+ remoteImplementation("com.clevertap.android:clevertap-android-sdk:7.1.0")
remoteImplementation("com.clevertap.android:clevertap-geofence-sdk:1.3.0")
remoteImplementation("com.clevertap.android:push-templates:1.2.4")
remoteImplementation("com.clevertap.android:clevertap-hms-sdk:1.3.4")
- stagingImplementation("com.clevertap.android:clevertap-android-sdk:7.0.3")
+ stagingImplementation("com.clevertap.android:clevertap-android-sdk:7.1.0")
stagingImplementation("com.clevertap.android:clevertap-geofence-sdk:1.3.0")
stagingImplementation("com.clevertap.android:push-templates:1.2.4")
stagingImplementation("com.clevertap.android:clevertap-hms-sdk:1.3.4")
diff --git a/templates/CTCORECHANGELOG.md b/templates/CTCORECHANGELOG.md
index 945381b49..acbed87fc 100644
--- a/templates/CTCORECHANGELOG.md
+++ b/templates/CTCORECHANGELOG.md
@@ -1,5 +1,34 @@
## CleverTap Android SDK CHANGE LOG
+### Version 7.1.0 (December 24, 2024)
+
+#### New Features
+
+* Adds support for triggering InApps based on first-time event filtering in multiple triggers. Now you can create campaign triggers that combine recurring and first-time events. For example: Trigger a campaign when "Charged" occurs (every time) OR "App Launched" occurs (first time only).
+* Adds new user-level event log tracking system to store and manage user event history. New APIs include:
+ * `getUserEventLog()`: Get details about a specific event
+ * `getUserEventLogCount()`: Get count of times an event occurred
+ * `getUserLastVisitTs()`: Get timestamp of user's last app visit
+ * `getUserAppLaunchCount()`: Get total number of times user has launched the app
+ * `getUserEventLogHistory()`: Get full event history for current user
+* Adds support to hide large icon in android notification by sending wzrk_hide_large_icon key in notification payload.
+
+#### API Changes
+
+* **Deprecated:** The old event tracking APIs tracked events at the device level rather than the user level, making it difficult to maintain accurate user-specific event histories, especially in multi-user scenarios. The following methods have been deprecated in favor of new user-specific event tracking APIs that provide more accurate, user-level analytics. These deprecated methods will be removed in future versions with prior notice:
+ * `getDetails()`: Use `getUserEventLog()` instead for user-specific event details
+ * `getCount()`: Use `getUserEventLogCount()` instead for user-specific event counts
+ * `getFirstTime()`: Use `getUserEventLog()` instead for user-specific first occurrence timestamp
+ * `getLastTime()`: Use `getUserEventLog()` instead for user-specific last occurrence timestamp
+ * `getPreviousVisitTime()`: Use `getUserLastVisitTs()` instead for user-specific last visit timestamp
+ * `getTotalVisits()`: Use `getUserAppLaunchCount()` instead for user-specific app launch count
+ * `getHistory()`: Use `getUserEventLogHistory()` instead for user-specific event history
+
+#### Bug Fixes
+* Fixes [#671](https://github.com/CleverTap/clevertap-android-sdk/issues/671) - an `AbstractMethodError` in the AppInbox feature when using audio/video.
+* Fixes issues when File type variable changes from validValue -> null
+
+
### Version 7.0.3 (November 29, 2024)
#### New Features
diff --git a/templates/EXAMPLES.md b/templates/EXAMPLES.md
index 71aef4467..dbb029f4a 100644
--- a/templates/EXAMPLES.md
+++ b/templates/EXAMPLES.md
@@ -747,4 +747,79 @@ val clevertapAdditionalInstanceConfig = CleverTapInstanceConfig.createInstance(
clevertapAdditionalInstanceConfig.setEncryptionLevel(CryptHandler.EncryptionLevel.MEDIUM)
val clevertapAdditionalInstance = CleverTapAPI.instanceWithConfig(applicationContext ,clevertapAdditionalInstanceConfig)
+```
+
+### User event logging APIs
+Get user event details
+
+Java
+```java
+UserEventLog eventLog = clevertap.getUserEventLog("Product Viewed");
+if (eventLog != null) {
+ String eventName = eventLog.getEventName();
+ long firstTime = eventLog.getFirstTs();
+ long lastTime = eventLog.getLastTs();
+ int count = eventLog.getCountOfEvents();
+ String deviceId = eventLog.getDeviceID();
+} else {
+ System.out.println("Event not performed");
+}
+```
+Kotlin
+```kotlin
+clevertap.getUserEventLog("Product Viewed")?.let { eventLog ->
+ val eventName = eventLog.eventName
+ val firstTime = eventLog.firstTs
+ val lastTime = eventLog.lastTs
+ val count = eventLog.countOfEvents
+ val deviceId = eventLog.deviceID
+} ?: println("Event not performed")
+```
+Get count of event occurrences
+
+Java
+```java
+int eventCount = clevertap.getUserEventLogCount("Product Viewed");
+```
+Kotlin
+```kotlin
+val eventCount = clevertap.getUserEventLogCount("Product Viewed")
+```
+Get user's last app visit timestamp
+
+Java
+```java
+long lastVisitTs = clevertap.getUserLastVisitTs();
+```
+Kotlin
+```kotlin
+val lastVisitTs = clevertap.userLastVisitTs
+```
+Get total number of app launches by user
+
+Java
+```java
+int appLaunchCount = clevertap.getUserAppLaunchCount();
+```
+Kotlin
+```kotlin
+val appLaunchCount = cleverTapAPI?.userAppLaunchCount
+```
+Get full event history for user
+
+Java
+```java
+Map eventHistory = clevertap.getUserEventLogHistory();
+for (Map.Entry entry : eventHistory.entrySet()) {
+String eventName = entry.getKey();
+UserEventLog log = entry.getValue();
+// Process event details
+}
+```
+Kotlin
+```kotlin
+val eventHistory = clevertap.userEventLogHistory
+eventHistory?.forEach { (eventName, log) ->
+ // Process event details
+} ?: println("Events not performed")
```
\ No newline at end of file
diff --git a/test_shared/build.gradle b/test_shared/build.gradle
index 8cb37eacd..5a68a9268 100644
--- a/test_shared/build.gradle
+++ b/test_shared/build.gradle
@@ -11,6 +11,7 @@ android {
// testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
+ multiDexEnabled true
}
buildTypes {
@@ -26,6 +27,13 @@ android {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
+ packagingOptions {
+ resources {
+ excludes += "/META-INF/{AL2.0,LGPL2.1}"
+ merges += "META-INF/LICENSE.md"
+ merges += "META-INF/LICENSE-notice.md"
+ }
+ }
namespace 'com.clevertap.android.shared.test'
}