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' }