From 0e8bd87022187a2f5727ab3ba209aa455bba1bda Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Mon, 21 Oct 2024 17:49:34 +0530 Subject: [PATCH 001/120] feat(multi_triggers) : create table userEventLogs MC-2083 --- .../clevertap/android/sdk/db/CtDatabase.kt | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) 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..980603643 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) @@ -72,6 +73,7 @@ class DatabaseHelper internal constructor(val context: Context, val config: Clev executeStatement(db, INBOX_MESSAGES_COMP_ID_USERID_INDEX) executeStatement(db, NOTIFICATION_VIEWED_INDEX) migrateUserProfilesTable(db) + executeStatement(db, CREATE_USER_EVENT_LOGS_TABLE)// when app updates 1 to 5 } 2 -> { @@ -80,11 +82,17 @@ class DatabaseHelper internal constructor(val context: Context, val config: Clev executeStatement(db, CREATE_NOTIFICATION_VIEWED_TABLE) executeStatement(db, NOTIFICATION_VIEWED_INDEX) migrateUserProfilesTable(db) + executeStatement(db, CREATE_USER_EVENT_LOGS_TABLE)// when app updates 2 to 5 } 3 -> { // For DB Version 4, just migrate userProfiles table migrateUserProfilesTable(db) + executeStatement(db, CREATE_USER_EVENT_LOGS_TABLE)// when app updates 3 to 5 + } + + 4 -> { + executeStatement(db, CREATE_USER_EVENT_LOGS_TABLE)// when app updates 4 to 5 } } } @@ -197,7 +205,8 @@ enum class Table(val tableName: String) { 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 +221,10 @@ object Column { const val CAMPAIGN = "campaignId" const val WZRKPARAMS = "wzrkParams" const val DEVICE_ID = "deviceID" + const val EVENT_NAME = "eventName" + const val FIRST_TS = "firstTs" + const val LAST_TS = "lastTs" + const val COUNT = "count" } private val CREATE_EVENTS_TABLE = """ @@ -222,6 +235,16 @@ private val CREATE_EVENTS_TABLE = """ ); """ +private val CREATE_USER_EVENT_LOGS_TABLE = """ + CREATE TABLE ${Table.USER_EVENT_LOGS_TABLE.tableName} ( + ${Column.EVENT_NAME} STRING NOT NULL, + ${Column.FIRST_TS} INTEGER NOT NULL, + ${Column.LAST_TS} INTEGER NOT NULL, + ${Column.COUNT} INTEGER NOT NULL, + ${Column.DEVICE_ID} STRING NOT NULL PRIMARY KEY + ); +""" + private val CREATE_PROFILE_EVENTS_TABLE = """ CREATE TABLE ${PROFILE_EVENTS.tableName} ( ${Column.ID} INTEGER PRIMARY KEY AUTOINCREMENT, From f179cc2cfb2b291574625df88da83bc22f06ef39 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Thu, 24 Oct 2024 00:03:24 +0530 Subject: [PATCH 002/120] feat(multi_triggers) : add UserEventLog DAO interface MC-2083 --- .../sdk/userEventLogs/UserEventLogDAO.kt | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 clevertap-core/src/main/java/com/clevertap/android/sdk/userEventLogs/UserEventLogDAO.kt 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..5ae65789c --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/userEventLogs/UserEventLogDAO.kt @@ -0,0 +1,32 @@ +package com.clevertap.android.sdk.userEventLogs + +import androidx.annotation.WorkerThread + +interface UserEventLogDAO { + + // Insert a new event by deviceID + @WorkerThread + fun insertEventByDeviceID(deviceID: String, eventName: String): Long + + // Update an event by deviceID + @WorkerThread + fun updateEventByDeviceID(deviceID: String, eventName: String): Boolean + + // Read an event by deviceID + @WorkerThread + fun readEventByDeviceID(deviceID: String, eventName: String): UserEventLog? + + // Check if an event exists by deviceID + @WorkerThread + fun eventExistsByDeviceID(deviceID: String, eventName: String): Boolean + + // Get all events for a particular deviceID + @WorkerThread + fun allEventsByDeviceID(deviceID: String): List + + // Get all events + @WorkerThread + fun allEvents(): List + @WorkerThread + fun cleanUpExtraEvents(threshold: Int): Boolean +} From bb1fcd550fe8d335a913664eaef99446f7ad7814 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Thu, 24 Oct 2024 00:03:36 +0530 Subject: [PATCH 003/120] feat(multi_triggers) : add UserEventLog DAO interface implementation MC-2083 --- .../sdk/userEventLogs/UserEventLogDAOImpl.kt | 224 ++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 clevertap-core/src/main/java/com/clevertap/android/sdk/userEventLogs/UserEventLogDAOImpl.kt 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..146a6410f --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/userEventLogs/UserEventLogDAOImpl.kt @@ -0,0 +1,224 @@ +package com.clevertap.android.sdk.userEventLogs + +import android.content.ContentValues +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteException +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 { + + + @WorkerThread + override fun insertEventByDeviceID(deviceID: String, eventName: 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.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: SQLiteException) { + logger.verbose("Error adding row to table $tableName Recreating DB") + db.deleteDatabase() + DB_UPDATE_ERROR + } + } + + @WorkerThread + override fun updateEventByDeviceID(deviceID: String, eventName: String): Boolean { + val now = System.currentTimeMillis() + val tName = table.tableName + val selection = "${Column.DEVICE_ID} = ? AND ${Column.EVENT_NAME} = ?" + val selectionArgs = arrayOf(deviceID, eventName) + val projection = arrayOf(Column.COUNT) + return try { + // Below code can be replace with nested SQL query but couldn't found any difference in the performance + db.readableDatabase.query( + tName, projection, selection, selectionArgs, null, null, null, null + )?.use { cursor -> + if (cursor.moveToFirst()) { + val countOfEvents = cursor.getInt(cursor.getColumnIndexOrThrow(Column.COUNT)) + val values = ContentValues().apply { + put(Column.LAST_TS, now) + put(Column.COUNT, countOfEvents + 1) + } + val updatedRow = + db.writableDatabase.update(tName, values, selection, selectionArgs) + updatedRow > 0 + } else { + false + } + } ?: false + } catch (e: Exception) { + logger.verbose("Could not fetch records out of database $tName.", e) + false + } + } + + @WorkerThread + override fun readEventByDeviceID(deviceID: String, eventName: String): UserEventLog? { + val tName = table.tableName + val selection = "${Column.DEVICE_ID} = ? AND ${Column.EVENT_NAME} = ?" + val selectionArgs = arrayOf(deviceID, eventName) + 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)), + 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 eventExistsByDeviceID(deviceID: String, eventName: String): Boolean { + val tName = table.tableName + val selection = "${Column.DEVICE_ID} = ? AND ${Column.EVENT_NAME} = ?" + val selectionArgs = arrayOf(deviceID, eventName) + + val query = """ + SELECT EXISTS( + SELECT 1 + FROM $tName + WHERE $selection + ) AS eventExists; + """.trimIndent() + + return try { + db.readableDatabase.rawQuery(query, selectionArgs).use { cursor -> + if (cursor.moveToFirst()) { + cursor.getInt(cursor.getColumnIndexOrThrow("eventExists")) == 1 + } else { + false + } + } + } catch (e: Exception) { + logger.verbose("Could not fetch records out of database $tName.", e) + false + } + } + + @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)), + 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 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)), + 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): Boolean { + val tName = table.tableName + val rowCountQuery = "SELECT COUNT(*) FROM $tName" + val deleteQuery = "DELETE FROM $tName WHERE ${Column.DEVICE_ID} IN (" + + "SELECT ${Column.DEVICE_ID} FROM $tName ORDER BY ${Column.LAST_TS} ASC LIMIT ?)" + + return try { + db.readableDatabase.rawQuery(rowCountQuery, null)?.use { cursor -> + if (cursor.moveToFirst()) { + val rowCount = cursor.getInt(0) + if (rowCount > threshold) { + val excessRows = rowCount - threshold + db.writableDatabase.execSQL(deleteQuery, arrayOf(excessRows.toString())) + logger.verbose("$excessRows least recently used rows deleted from $tName") + } + } + } + true + } catch (e: Exception) { + logger.verbose("Error cleaning up extra events in $tName.", e) + false + } + } + + //TODO: Create indexes +} From da41603b762e6c220b1ea38b9824233df3d7338c Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Thu, 24 Oct 2024 00:04:16 +0530 Subject: [PATCH 004/120] feat(multi_triggers) : add UserEventLog data class MC-2083 --- .../clevertap/android/sdk/userEventLogs/UserEventLog.kt | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 clevertap-core/src/main/java/com/clevertap/android/sdk/userEventLogs/UserEventLog.kt 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..87392b6df --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/userEventLogs/UserEventLog.kt @@ -0,0 +1,9 @@ +package com.clevertap.android.sdk.userEventLogs + +data class UserEventLog( + val eventName: String, // 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 +) From 3e6ff680dd403db9ded28af5aeac0709dd31bda7 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Thu, 24 Oct 2024 00:07:10 +0530 Subject: [PATCH 005/120] feat(multi_triggers) : create composite key in the userEventLogs table MC-2083 --- .../main/java/com/clevertap/android/sdk/db/CtDatabase.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 980603643..7a7ff7fe0 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 @@ -198,7 +198,7 @@ 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"), @@ -241,7 +241,9 @@ private val CREATE_USER_EVENT_LOGS_TABLE = """ ${Column.FIRST_TS} INTEGER NOT NULL, ${Column.LAST_TS} INTEGER NOT NULL, ${Column.COUNT} INTEGER NOT NULL, - ${Column.DEVICE_ID} STRING NOT NULL PRIMARY KEY + ${Column.DEVICE_ID} STRING NOT NULL, + PRIMARY KEY (${Column.EVENT_NAME}, ${Column.DEVICE_ID}) + ); """ From ab4c3355f082628cb1e0b771bc5b6189c79e79a0 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Thu, 24 Oct 2024 00:08:23 +0530 Subject: [PATCH 006/120] feat(multi_triggers) : create and return UserEventLogDAO instance MC-2083 --- .../com/clevertap/android/sdk/db/DBAdapter.kt | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) 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..3dee345f8 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() From c5113ade17d623a07faf09e4bde22a9bd77b4838 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Fri, 25 Oct 2024 17:10:18 +0530 Subject: [PATCH 007/120] feat(multi_triggers) : use single query to delete extra events from userEventLogs table MC-2083 --- .../sdk/userEventLogs/UserEventLogDAOImpl.kt | 36 +++++++++++-------- 1 file changed, 22 insertions(+), 14 deletions(-) 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 index 146a6410f..c7ff65e34 100644 --- 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 @@ -137,6 +137,7 @@ internal class UserEventLogDAOImpl( } } + //TODO: Create index on deviceID,lastTs column if this method is frequently used @WorkerThread override fun allEventsByDeviceID(deviceID: String): List { val tName = table.tableName @@ -167,6 +168,7 @@ internal class UserEventLogDAOImpl( } } + //TODO: Create index on lastTs column if this method is frequently used @WorkerThread override fun allEvents(): List { val tName = table.tableName @@ -198,21 +200,28 @@ internal class UserEventLogDAOImpl( @WorkerThread override fun cleanUpExtraEvents(threshold: Int): Boolean { val tName = table.tableName - val rowCountQuery = "SELECT COUNT(*) FROM $tName" - val deleteQuery = "DELETE FROM $tName WHERE ${Column.DEVICE_ID} IN (" + - "SELECT ${Column.DEVICE_ID} FROM $tName ORDER BY ${Column.LAST_TS} ASC LIMIT ?)" return try { - db.readableDatabase.rawQuery(rowCountQuery, null)?.use { cursor -> - if (cursor.moveToFirst()) { - val rowCount = cursor.getInt(0) - if (rowCount > threshold) { - val excessRows = rowCount - threshold - db.writableDatabase.execSQL(deleteQuery, arrayOf(excessRows.toString())) - logger.verbose("$excessRows least recently used rows deleted from $tName") - } - } - } + // SQL query to delete only the least recently used rows, using a subquery with LIMIT + val query = """ + DELETE FROM $tName + WHERE (${Column.EVENT_NAME}, ${Column.DEVICE_ID}) IN ( + SELECT ${Column.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,threshold)) + logger.verbose("Cleaned up extra events in $tName, keeping only $threshold rows.") true } catch (e: Exception) { logger.verbose("Error cleaning up extra events in $tName.", e) @@ -220,5 +229,4 @@ internal class UserEventLogDAOImpl( } } - //TODO: Create indexes } From 3d9334931d4a67d84000b10cb0e54976e2c48ed9 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Mon, 28 Oct 2024 11:29:13 +0530 Subject: [PATCH 008/120] feat(multi_triggers) : replace multiple db io with single query in updateEventByDeviceID for performance MC-2083 --- .../sdk/userEventLogs/UserEventLogDAOImpl.kt | 37 ++++++++----------- 1 file changed, 15 insertions(+), 22 deletions(-) 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 index c7ff65e34..ebfe31b8b 100644 --- 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 @@ -52,31 +52,24 @@ internal class UserEventLogDAOImpl( @WorkerThread override fun updateEventByDeviceID(deviceID: String, eventName: String): Boolean { + val tableName = table.tableName val now = System.currentTimeMillis() - val tName = table.tableName - val selection = "${Column.DEVICE_ID} = ? AND ${Column.EVENT_NAME} = ?" - val selectionArgs = arrayOf(deviceID, eventName) - val projection = arrayOf(Column.COUNT) + return try { - // Below code can be replace with nested SQL query but couldn't found any difference in the performance - db.readableDatabase.query( - tName, projection, selection, selectionArgs, null, null, null, null - )?.use { cursor -> - if (cursor.moveToFirst()) { - val countOfEvents = cursor.getInt(cursor.getColumnIndexOrThrow(Column.COUNT)) - val values = ContentValues().apply { - put(Column.LAST_TS, now) - put(Column.COUNT, countOfEvents + 1) - } - val updatedRow = - db.writableDatabase.update(tName, values, selection, selectionArgs) - updatedRow > 0 - } else { - false - } - } ?: false + val query = """ + UPDATE $tableName + SET + ${Column.COUNT} = ${Column.COUNT} + 1, + ${Column.LAST_TS} = ? + WHERE ${Column.DEVICE_ID} = ? + AND ${Column.EVENT_NAME} = ?; + """.trimIndent() + + logger.verbose("Updating event $eventName with deviceID = $deviceID in $tableName") + db.writableDatabase.execSQL(query, arrayOf(now, deviceID, eventName)) + true } catch (e: Exception) { - logger.verbose("Could not fetch records out of database $tName.", e) + logger.verbose("Could not update event in database $tableName.", e) false } } From 3b25b326c0f547e4398be1b37bcc31527bac8bf5 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Mon, 28 Oct 2024 12:11:58 +0530 Subject: [PATCH 009/120] feat(multi_triggers) : add upSertEventsByDeviceID for bulk insert/update MC-2083 --- .../sdk/userEventLogs/UserEventLogDAO.kt | 3 ++ .../sdk/userEventLogs/UserEventLogDAOImpl.kt | 29 +++++++++++++++++++ 2 files changed, 32 insertions(+) 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 index 5ae65789c..218087377 100644 --- 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 @@ -12,6 +12,9 @@ interface UserEventLogDAO { @WorkerThread fun updateEventByDeviceID(deviceID: String, eventName: String): Boolean + @WorkerThread + fun upSertEventsByDeviceID(deviceID: String, eventNameList: Set): Boolean + // Read an event by deviceID @WorkerThread fun readEventByDeviceID(deviceID: String, eventName: String): UserEventLog? 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 index ebfe31b8b..db67d0614 100644 --- 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 @@ -74,6 +74,35 @@ internal class UserEventLogDAOImpl( } } + @WorkerThread + override fun upSertEventsByDeviceID(deviceID: String, eventNameList: Set): Boolean { + val tableName = table.tableName + logger.verbose("UserEventLog: upSert EventLog for bulk events") + return try { + db.writableDatabase.beginTransaction() + eventNameList.forEach { + if (eventExistsByDeviceID(deviceID, it)) { + logger.verbose("UserEventLog: Updating EventLog for event $it") + updateEventByDeviceID(deviceID, it) + } else { + logger.verbose("UserEventLog: Inserting EventLog for event $it") + insertEventByDeviceID(deviceID, it) + } + } + 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 readEventByDeviceID(deviceID: String, eventName: String): UserEventLog? { val tName = table.tableName From 1ec33f4eb326e139bc257803e1fb98956936136f Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Mon, 28 Oct 2024 12:15:31 +0530 Subject: [PATCH 010/120] feat(multi_triggers) : add eventExistsByDeviceIDAndCount implementation MC-2083 --- .../sdk/userEventLogs/UserEventLogDAO.kt | 4 +++ .../sdk/userEventLogs/UserEventLogDAOImpl.kt | 30 +++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) 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 index 218087377..bac54dd37 100644 --- 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 @@ -23,6 +23,10 @@ interface UserEventLogDAO { @WorkerThread fun eventExistsByDeviceID(deviceID: String, eventName: String): Boolean + // Check if an event exists by deviceID and count + @WorkerThread + fun eventExistsByDeviceIDAndCount(deviceID: String, eventName: String, count: Int): Boolean + // Get all events for a particular deviceID @WorkerThread fun allEventsByDeviceID(deviceID: String): List 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 index db67d0614..7c94d860c 100644 --- 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 @@ -146,13 +146,39 @@ internal class UserEventLogDAOImpl( """.trimIndent() return try { - db.readableDatabase.rawQuery(query, selectionArgs).use { cursor -> + db.readableDatabase.rawQuery(query, selectionArgs)?.use { cursor -> if (cursor.moveToFirst()) { cursor.getInt(cursor.getColumnIndexOrThrow("eventExists")) == 1 } else { false } - } + } ?: false + } catch (e: Exception) { + logger.verbose("Could not fetch records out of database $tName.", e) + false + } + } + + @WorkerThread + override fun eventExistsByDeviceIDAndCount(deviceID: String, eventName: String, count: Int): Boolean { + val tName = table.tableName + val selection = "${Column.DEVICE_ID} = ? AND ${Column.EVENT_NAME} = ? AND ${Column.COUNT} = ?" + val selectionArgs = arrayOf(deviceID, eventName, count.toString()) + val query = """ + SELECT EXISTS( + SELECT 1 + FROM $tName + WHERE $selection + ) AS eventExists; + """.trimIndent() + return try { + db.readableDatabase.rawQuery(query, selectionArgs)?.use { cursor -> + if (cursor.moveToFirst()) { + cursor.getInt(cursor.getColumnIndexOrThrow("eventExists")) == 1 + } else { + false + } + } ?: false } catch (e: Exception) { logger.verbose("Could not fetch records out of database $tName.", e) false From ced2da6e6691d451764cb5970e5bd2b42920ea4e Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Mon, 28 Oct 2024 12:19:12 +0530 Subject: [PATCH 011/120] feat(multi_triggers) : add methods to query count, firstTs and lastTs MC-2083 --- .../sdk/userEventLogs/UserEventLogDAO.kt | 12 ++++ .../sdk/userEventLogs/UserEventLogDAOImpl.kt | 67 +++++++++++++++++++ 2 files changed, 79 insertions(+) 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 index bac54dd37..14e814211 100644 --- 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 @@ -19,6 +19,18 @@ interface UserEventLogDAO { @WorkerThread fun readEventByDeviceID(deviceID: String, eventName: String): UserEventLog? + // Read an event count by deviceID + @WorkerThread + fun readEventCountByDeviceID(deviceID: String, eventName: String): Int + + // Read an event firstTs by deviceID + @WorkerThread + fun readEventFirstTsByDeviceID(deviceID: String, eventName: String): Long + + // Read an event lastTs by deviceID + @WorkerThread + fun readEventLastTsByDeviceID(deviceID: String, eventName: String): Long + // Check if an event exists by deviceID @WorkerThread fun eventExistsByDeviceID(deviceID: String, eventName: String): 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 index 7c94d860c..1a967ea96 100644 --- 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 @@ -1,6 +1,7 @@ package com.clevertap.android.sdk.userEventLogs import android.content.ContentValues +import android.database.Cursor import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteException import androidx.annotation.WorkerThread @@ -131,6 +132,43 @@ internal class UserEventLogDAOImpl( } } + @WorkerThread + override fun readEventCountByDeviceID(deviceID: String, eventName: String): Int = + readEventColumnByDeviceID( + deviceID, + eventName, + Column.COUNT, + defaultValueExtractor = { -1 }, + valueExtractor = { cursor, columnName -> + cursor.getInt(cursor.getColumnIndexOrThrow(columnName)) + } + ) + + + @WorkerThread + override fun readEventFirstTsByDeviceID(deviceID: String, eventName: String): Long = + readEventColumnByDeviceID( + deviceID, + eventName, + Column.FIRST_TS, + defaultValueExtractor = { -1L }, + valueExtractor = { cursor, columnName -> + cursor.getLong(cursor.getColumnIndexOrThrow(columnName)) + } + ) + + @WorkerThread + override fun readEventLastTsByDeviceID(deviceID: String, eventName: String): Long = + readEventColumnByDeviceID( + deviceID, + eventName, + Column.LAST_TS, + defaultValueExtractor = { -1L }, + valueExtractor = { cursor, columnName -> + cursor.getLong(cursor.getColumnIndexOrThrow(columnName)) + } + ) + @WorkerThread override fun eventExistsByDeviceID(deviceID: String, eventName: String): Boolean { val tName = table.tableName @@ -277,4 +315,33 @@ internal class UserEventLogDAOImpl( } } + @WorkerThread + private fun readEventColumnByDeviceID( + deviceID: String, + eventName: String, + column: String, + defaultValueExtractor: () -> T, + valueExtractor: (cursor: Cursor, columnName: String) -> T + ): T { + val tName = table.tableName + val selection = "${Column.DEVICE_ID} = ? AND ${Column.EVENT_NAME} = ?" + val selectionArgs = arrayOf(deviceID, eventName) + val projection = arrayOf(column) + + return try { + db.readableDatabase.query( + tName, projection, selection, selectionArgs, null, null, null, null + )?.use { cursor -> + if (cursor.moveToFirst()) { + valueExtractor(cursor, column) + } else { + defaultValueExtractor() + } + } ?: defaultValueExtractor() + } catch (e: Exception) { + logger.verbose("Could not fetch $column from database $tName.", e) + defaultValueExtractor() + } + } + } From 9287cd02ab6b9c346a859e3f18e28a62b483fc12 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Mon, 28 Oct 2024 12:39:51 +0530 Subject: [PATCH 012/120] feat(multi_triggers) : inject dbManager in LocalDataStore MC-2083 --- .../com/clevertap/android/sdk/CleverTapFactory.java | 2 +- .../com/clevertap/android/sdk/LocalDataStore.java | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) 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..557993815 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 @@ -97,7 +97,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); 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..1cdd4dcb5 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,6 +12,7 @@ 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; @@ -38,8 +39,7 @@ public class LocalDataStore { private final Context context; private final CryptHandler cryptHandler; - - private DBAdapter dbAdapter; + private final BaseDatabaseManager baseDatabaseManager; private final ExecutorService es; @@ -47,12 +47,13 @@ public class LocalDataStore { private final DeviceInfo deviceInfo; - 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 @@ -242,9 +243,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()); @@ -353,6 +352,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); From 69180694ea438efd839bb2a909c5d149360f785f Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Mon, 28 Oct 2024 12:53:38 +0530 Subject: [PATCH 013/120] feat(multi_triggers) : add user event log handling methods for database interaction in LocalDataStore MC-2083 --- .../clevertap/android/sdk/LocalDataStore.java | 197 ++++++++++++++++++ 1 file changed, 197 insertions(+) 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 1cdd4dcb5..3a672d7aa 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 @@ -15,6 +15,7 @@ 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.json.JSONArray; import org.json.JSONException; @@ -22,7 +23,9 @@ import java.util.HashMap; 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; @@ -116,6 +119,200 @@ public void persistEvent(Context context, JSONObject event, int type) { getConfigLogger().verbose(getConfigAccountId(), "Failed to sync with upstream", t); } } + @WorkerThread + public void persistUserEventLogsInBulk(Set eventNames){ + upSertUserEventLogsInBulk(eventNames); + } + + @WorkerThread + public void persistUserEventLog(String eventName) { + + if (eventName == null) { + return; + } + + 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); + updateUserEventLog(eventName); + } else { + logger.verbose(accountId,"UserEventLog: Inserting EventLog for event "+eventName); + insertUserEventLog(eventName ); + } + /* + * ==========TESTING BLOCK START ========== + * TODO: Remove block code after testing + */ + /*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); + } + } + + @WorkerThread + private void updateEventByDeviceID(String deviceID, String eventName) { + DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context); + boolean updatedEventByDeviceID = dbAdapter.userEventLogDAO().updateEventByDeviceID(deviceID, eventName); + getConfigLogger().verbose("updatedEventByDeviceID = "+updatedEventByDeviceID); + } + + @WorkerThread + public void updateUserEventLog(String eventName) { + String deviceID = deviceInfo.getDeviceID(); + updateEventByDeviceID(deviceID,eventName); + } + + @WorkerThread + public void upSertUserEventLogsInBulk(Set eventNames){ + String deviceID = deviceInfo.getDeviceID(); + DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context); + boolean upSertEventByDeviceID = dbAdapter.userEventLogDAO().upSertEventsByDeviceID(deviceID, eventNames); + getConfigLogger().verbose("upSertEventByDeviceID = "+upSertEventByDeviceID); + } + + @WorkerThread + private void insertEventByDeviceID(String deviceID, String eventName) { + DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context); + long rowId = dbAdapter.userEventLogDAO().insertEventByDeviceID(deviceID, eventName); + getConfigLogger().verbose("inserted rowId = "+rowId); + } + + @WorkerThread + public void insertUserEventLog(String eventName) { + String deviceID = deviceInfo.getDeviceID(); + insertEventByDeviceID(deviceID,eventName); + } + + @WorkerThread + private boolean eventExistsByDeviceID(String deviceID, String eventName) { + DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context); + boolean eventExistsByDeviceID = dbAdapter.userEventLogDAO().eventExistsByDeviceID(deviceID, eventName); + getConfigLogger().verbose("eventExistsByDeviceID = "+eventExistsByDeviceID); + return eventExistsByDeviceID; + } + + @WorkerThread + public boolean isUserEventLogExists(String eventName) { + String deviceID = deviceInfo.getDeviceID(); + return eventExistsByDeviceID(deviceID,eventName); + } + + @WorkerThread + private boolean eventExistsByDeviceIDAndCount(String deviceID, String eventName, int count) { + DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context); + boolean eventExistsByDeviceIDAndCount = dbAdapter.userEventLogDAO() + .eventExistsByDeviceIDAndCount(deviceID, eventName, count); + + getConfigLogger().verbose("eventExistsByDeviceIDAndCount = "+eventExistsByDeviceIDAndCount); + return eventExistsByDeviceIDAndCount; + } + + @WorkerThread + public boolean isUserEventLogFirstTime(String eventName) { + String deviceID = deviceInfo.getDeviceID(); + return eventExistsByDeviceIDAndCount(deviceID, eventName, 1); + } + + @WorkerThread + public boolean cleanUpExtraEvents(int threshold){ + DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context); + boolean cleanUpExtraEvents = dbAdapter.userEventLogDAO().cleanUpExtraEvents(threshold); + getConfigLogger().verbose("cleanUpExtraEvents boolean= "+cleanUpExtraEvents); + return cleanUpExtraEvents; + } + + @WorkerThread + private UserEventLog readEventByDeviceID(String deviceID, String eventName) { + DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context); + return dbAdapter.userEventLogDAO().readEventByDeviceID(deviceID, eventName); + } + + @WorkerThread + public UserEventLog readUserEventLog(String eventName) { + String deviceID = deviceInfo.getDeviceID(); + return readEventByDeviceID(deviceID,eventName); + } + + @WorkerThread + private int readEventCountByDeviceID(String deviceID, String eventName) { + DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context); + return dbAdapter.userEventLogDAO().readEventCountByDeviceID(deviceID, eventName); + } + + @WorkerThread + public int readUserEventLogCount(String eventName) { + String deviceID = deviceInfo.getDeviceID(); + return readEventCountByDeviceID(deviceID,eventName); + } + + @WorkerThread + private long readEventLastTsByDeviceID(String deviceID, String eventName) { + DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context); + return dbAdapter.userEventLogDAO().readEventLastTsByDeviceID(deviceID, eventName); + } + + @WorkerThread + public long readUserEventLogLastTs(String eventName) { + String deviceID = deviceInfo.getDeviceID(); + return readEventLastTsByDeviceID(deviceID,eventName); + } + + @WorkerThread + private long readEventFirstTsByDeviceID(String deviceID, String eventName) { + DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context); + return dbAdapter.userEventLogDAO().readEventFirstTsByDeviceID(deviceID, eventName); + } + + @WorkerThread + public long readUserEventLogFirstTs(String eventName) { + String deviceID = deviceInfo.getDeviceID(); + return readEventFirstTsByDeviceID(deviceID,eventName); + } + + @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) { From e44f7ba987d5b4190ea28f1bf24369e9cde666b3 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Mon, 28 Oct 2024 15:13:40 +0530 Subject: [PATCH 014/120] feat(multi_triggers) : add firstTimeOnly property in TriggerAdapter MC-2087 --- .../src/main/java/com/clevertap/android/sdk/Constants.java | 1 + .../clevertap/android/sdk/inapp/evaluation/TriggerAdapter.kt | 5 +++++ 2 files changed, 6 insertions(+) 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 8e3c29f2d..db8b3d0c9 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 @@ -229,6 +229,7 @@ public interface Constants { String KEY_ITEM_PROPERTIES = "itemProperties"; String KEY_GEO_RADIUS_PROPERTIES = "geoRadius"; String KEY_PROFILE_ATTR_NAME = "profileAttrName"; + String KEY_FIRST_TIME_ONLY = "firstTimeOnly"; String KEY_PROPERTY_VALUE = "propertyValue"; String KEY_COLOR = "color"; String KEY_MESSAGE = "message"; 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..46bd65bb4 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 @@ -101,6 +101,11 @@ class TriggerAdapter(triggerJSON: JSONObject) { */ val profileAttrName: String? = triggerJSON.optString(Constants.KEY_PROFILE_ATTR_NAME, null) + /** + * TODO(MT): Add firstTimeOnly boolean property + */ + val firstTimeOnly: Boolean = triggerJSON.optBoolean(Constants.KEY_FIRST_TIME_ONLY, false) + /** * Get the count of event property trigger conditions. */ From 8bcd848c3626dda3dcbfaa9419e557d1b6bc1b59 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Mon, 28 Oct 2024 15:18:26 +0530 Subject: [PATCH 015/120] feat(multi_triggers) : inject LocalDataStore in TriggersMatcher MC-2087 --- .../main/java/com/clevertap/android/sdk/CleverTapFactory.java | 2 +- .../clevertap/android/sdk/inapp/evaluation/TriggersMatcher.kt | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) 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 557993815..912c95db8 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 @@ -118,7 +118,7 @@ static CoreState getCoreState(Context context, CleverTapInstanceConfig cleverTap ctLockManager, callbackManager, deviceInfo, baseDatabaseManager); coreState.setControllerManager(controllerManager); - TriggersMatcher triggersMatcher = new TriggersMatcher(); + TriggersMatcher triggersMatcher = new TriggersMatcher(localDataStore/* TODO: inject localDataStore */); TriggerManager triggersManager = new TriggerManager(context, config.getAccountId(), deviceInfo); ImpressionManager impressionManager = new ImpressionManager(storeRegistry); LimitsMatcher limitsMatcher = new LimitsMatcher(impressionManager, triggersManager); 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 ab5cbd23b..0850341e2 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 @@ -2,6 +2,7 @@ package com.clevertap.android.sdk.inapp.evaluation import android.location.Location 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 +14,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. From 20be411a4c01eee564c2b6324cb6b444c221b58d Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Mon, 28 Oct 2024 15:20:54 +0530 Subject: [PATCH 016/120] feat(multi_triggers) : add firstTimeOnly match logic in TriggersMatcher MC-2087 --- .../sdk/inapp/evaluation/TriggersMatcher.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) 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 0850341e2..d156fbe8e 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,6 +1,7 @@ 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 @@ -59,6 +60,12 @@ class TriggersMatcher(private val localDataStore: LocalDataStore) { if (!matchPropertyConditions(trigger, event)) { return false } + /** + * TODO: Add matchFirstTimeOnly(trigger, event) + */ + if (!matchFirstTimeOnly(trigger)) { + return false + } if (event.isChargedEvent() && !matchChargedItemConditions(trigger, event)) { return false @@ -71,6 +78,15 @@ class TriggersMatcher(private val localDataStore: LocalDataStore) { return true } + // TODO: matchFirstTimeOnly(trigger, event) implementation + @WorkerThread + internal fun matchFirstTimeOnly(trigger: TriggerAdapter): Boolean { + if (!trigger.firstTimeOnly) { + return true + } + return localDataStore.isUserEventLogFirstTime(trigger.eventName) + } + private fun matchPropertyConditions(trigger: TriggerAdapter, event: EventAdapter): Boolean { // Property conditions are AND-ed return (0 until trigger.propertyCount) From 8c9e132c190a6cf7d8408ecfb6cd5a88db5bbe71 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Mon, 28 Oct 2024 15:34:46 +0530 Subject: [PATCH 017/120] feat(multi_triggers) : update user event log to DB from queueEvent() and before inapp evaluation MC-2087 --- .../android/sdk/events/EventQueueManager.java | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) 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..06a346b09 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 { @@ -310,7 +314,7 @@ public void processEvent(final Context context, final JSONObject event, final in } localDataStore.setDataSyncFlag(event); baseDatabaseManager.queueEventToDB(context, event, eventType); - updateLocalStore(context, event, eventType); + //updateLocalStore(context, event, eventType);// TODO: remove from here scheduleQueueFlush(context); } catch (Throwable e) { @@ -455,16 +459,19 @@ public Future queueEvent(final Context context, final JSONObject event, final @Override @WorkerThread public Void call() { - + // TODO: add here updateLocalStore(context, event, 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(eventMediator.getEventName(event), + controllerManager.getInAppController().onQueueEvent(eventName, eventMediator.getEventProperties(event), userLocation); } else if (eventType == Constants.PROFILE_EVENT) { // in case profile event, evaluate for user attribute changes @@ -474,7 +481,7 @@ public Void call() { .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), + controllerManager.getInAppController().onQueueEvent(eventName, eventMediator.getEventProperties(event), userLocation); } @@ -586,11 +593,13 @@ 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); + // TODO: persist event in DB + + localDataStore.persistUserEventLog(eventName); + } } From 2a713c5421a1bb729fbfe844a2665ef439dc8a67 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Mon, 28 Oct 2024 17:00:07 +0530 Subject: [PATCH 018/120] feat(multi_triggers) : upsert profile properties as events to DB from updateProfileFields() and before inapp evaluation MC-2089 --- .../clevertap/android/sdk/LocalDataStore.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) 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 3a672d7aa..aec3f91a6 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 @@ -630,7 +630,7 @@ private void _removeProfileField(String key) { } } } - + //int k = 0; private void _setProfileField(String key, Object value) { if (value == null) { return; @@ -654,7 +654,20 @@ private void _setProfileField(String key, Object value) { public void updateProfileFields(Map fields) { if(fields.isEmpty()) return; - + /*Set events = new HashSet<>(); + for (int i = 0; i < 5000; i++) { + events.add("profile key - "+i+" - "+ k); + }*/ + //k++; + long start = System.currentTimeMillis(); + persistUserEventLogsInBulk(fields.keySet()); +// persistUserEventLogsInBulk(events); + /*for (String key : events) + { + persistUserEventLog(key); + }*/ + long end = System.currentTimeMillis(); + config.getLogger().verbose(config.getAccountId(),"UserEventLog: persistUserEventLog execution time = "+(end-start)/1000+" seconds"); for (Map.Entry entry : fields.entrySet()) { String key = entry.getKey(); Object newValue = entry.getValue(); From 053c78bc081c40ea81f13d8c7dfc91bf70fc5ada Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Mon, 28 Oct 2024 18:19:34 +0530 Subject: [PATCH 019/120] feat(multi_triggers) : add in-memory HashSet for isFirstTime quick check MC-2091 --- .../com/clevertap/android/sdk/LocalDataStore.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) 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 aec3f91a6..9f7247129 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 @@ -21,7 +21,9 @@ 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; @@ -49,6 +51,7 @@ public class LocalDataStore { private final String eventNamespace = "local_events"; private final DeviceInfo deviceInfo; + private final Set userEventLogKeys = Collections.synchronizedSet(new HashSet<>()); LocalDataStore(Context context, CleverTapInstanceConfig config, CryptHandler cryptHandler, DeviceInfo deviceInfo, BaseDatabaseManager baseDatabaseManager) { this.context = context; @@ -236,8 +239,17 @@ private boolean eventExistsByDeviceIDAndCount(String deviceID, String eventName, @WorkerThread public boolean isUserEventLogFirstTime(String eventName) { + if (userEventLogKeys.contains(eventName)) { + return false; + } + String deviceID = deviceInfo.getDeviceID(); - return eventExistsByDeviceIDAndCount(deviceID, eventName, 1); + + int count = readEventCountByDeviceID(deviceID, eventName); + if (count > 1) { + userEventLogKeys.add(eventName); + } + return count == 1; } @WorkerThread From 6329c64c4a337552548e041d2e5207e06cff1e2c Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Mon, 28 Oct 2024 18:25:05 +0530 Subject: [PATCH 020/120] feat(multi_triggers) : on change user, clear in-memory HashSet of isFirstTime quick check MC-2091 --- .../src/main/java/com/clevertap/android/sdk/LocalDataStore.java | 1 + 1 file changed, 1 insertion(+) 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 9f7247129..a6cc40376 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 @@ -64,6 +64,7 @@ public class LocalDataStore { @WorkerThread public void changeUser() { + userEventLogKeys.clear(); resetLocalProfileSync(); } From 29037edeccc2f2d51075c0ab9a3aca9f6100ae16 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Wed, 30 Oct 2024 17:46:45 +0530 Subject: [PATCH 021/120] feat(multi_triggers) : deprecate old event APIs and add new ones in CleverTapAPI MC-2083 --- .../clevertap/android/sdk/CleverTapAPI.java | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) 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..7fa85d4e3 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,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.Future; @@ -1608,6 +1610,9 @@ 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.0.2. Use {@link #getUserEventLogCount(String)} instead. + * getUserEventLogCount() provides user-specific event counts. */ @SuppressWarnings({"unused"}) public int getCount(String event) { @@ -1619,6 +1624,24 @@ 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 the event does not exist or there was an error + */ + public int getUserEventLogCount(String eventName) { + return coreState.getLocalDataStore().readUserEventLogCount(eventName); + } + /** * Returns an EventDetail object for the particular event passed. EventDetail consists of event name, count, first * time @@ -1626,12 +1649,34 @@ 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.0.2. Use {@link #getUserEventLog(String)} instead. + * getUserEventLog() provides user-specific event log. */ @SuppressWarnings({"unused"}) 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) { + return coreState.getLocalDataStore().readUserEventLog(eventName); + } + /** * Returns the device push token or null * @@ -1728,6 +1773,8 @@ 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.0.2. Use {@link #getUserEventLogFirstTs(String)} instead. + * getUserEventLogFirstTs() provides user-specific event first occurrence timestamp. */ @SuppressWarnings({"unused"}) public int getFirstTime(String event) { @@ -1739,6 +1786,25 @@ public int getFirstTime(String event) { return -1; } + /** + * Retrieves first occurrence timestamp of event 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
+ * long firstTs = getUserEventLogFirstTs("navigation_clicked") + *
+ * + * @param eventName Name of the event to get first timestamp for (e.g., "navigation_clicked", "item_selected") + * @return First occurrence timestamp of the event for current user, or -1 if the event does not exist or there was an error + */ + @WorkerThread + public long getUserEventLogFirstTs(String eventName) { + return coreState.getLocalDataStore().readUserEventLogFirstTs(eventName); + } + /** * Returns the GeofenceCallback object * @@ -1766,12 +1832,37 @@ 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.0.2. Use {@link #getUserEventLogHistory()} instead. + * getUserEventLogHistory() provides user-specific event logs. */ @SuppressWarnings({"unused"}) 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}. + * 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() { + List logs = coreState.getLocalDataStore().readUserEventLogs(); + Map history = new HashMap<>(); + for (UserEventLog log : logs) { + history.put(log.getEventName(), log); + } + return history; + } + /** * Returns the InAppNotificationListener object * @@ -1883,6 +1974,8 @@ public int getInboxMessageUnreadCount() { * * @param event The event name for which you want the last time timestamp * @return The timestamp in int + * @deprecated since v7.0.2. Use {@link #getUserEventLogLastTs(String)} instead. + * getUserEventLogLastTs() provides user-specific event last occurrence timestamp. */ @SuppressWarnings({"unused"}) public int getLastTime(String event) { @@ -1894,6 +1987,25 @@ public int getLastTime(String event) { return -1; } + /** + * Retrieves last occurrence timestamp of event 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
+ * long lastTs = getUserEventLogLastTs("navigation_clicked") + *
+ * + * @param eventName Name of the event to get last timestamp for (e.g., "navigation_clicked", "item_selected") + * @return Last occurrence timestamp of the event for current user, or -1 if the event does not exist or there was an error + */ + @WorkerThread + public long getUserEventLogLastTs(String eventName) { + return coreState.getLocalDataStore().readUserEventLogLastTs(eventName); + } + /** * get the current device location * requires Location Permission in AndroidManifest e.g. "android.permission.ACCESS_COARSE_LOCATION" @@ -2002,6 +2114,8 @@ 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.0.2. Use {@link #getUserAppLaunchCount()} instead. + * getUserAppLaunchCount() provides user-specific app launch count. */ @SuppressWarnings({"unused"}) public int getTotalVisits() { @@ -2013,6 +2127,24 @@ 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() { + return coreState.getLocalDataStore().readUserEventLogCount(Constants.APP_LAUNCHED_EVENT); + } + /** * Returns a UTMDetail object which consists of UTM parameters like source, medium & campaign * From b2f4e3b22b826fc9d0d4429047a8f5ecbb630cb4 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Wed, 30 Oct 2024 18:08:40 +0530 Subject: [PATCH 022/120] feat(multi_triggers) : deprecate getPreviousVisitTime() in favor of getUserLastVisitTs() MC-2083 --- .../clevertap/android/sdk/CleverTapAPI.java | 18 ++++++++++++++++++ .../clevertap/android/sdk/SessionManager.java | 13 +++++++++++++ 2 files changed, 31 insertions(+) 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 7fa85d4e3..6e3fa8d66 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 @@ -1246,6 +1246,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; @@ -2034,12 +2035,29 @@ public void setLocation(Location location) { * Returns the timestamp of the previous visit * * @return Timestamp of previous visit in int + * @deprecated since v7.0.2. Use {@link #getUserLastVisitTs()} instead. + * getUserLastVisitTs() provides user-specific last visit timestamp. */ @SuppressWarnings({"unused"}) 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() { + 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 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..34d5d52ff 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,20 @@ 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.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 +91,10 @@ void setLastVisitTime() { lastVisitTime = ed.getLastTime(); } } + @WorkerThread + void setUserLastVisitTs() { + userLastVisitTs = localDataStore.readUserEventLogLastTs(Constants.APP_LAUNCHED_EVENT); + } private void createSession(final Context context) { int sessionId = getNow(); @@ -118,4 +128,7 @@ int getNow() { return (int) (System.currentTimeMillis() / 1000); } + public long getUserLastVisitTs() { + return userLastVisitTs; + } } \ No newline at end of file From 7aff990421f3fba6f4f34116502208dcfb7c6af3 Mon Sep 17 00:00:00 2001 From: Vassil Angelov <148857285+vasct@users.noreply.github.com> Date: Mon, 4 Nov 2024 12:03:51 +0200 Subject: [PATCH 023/120] bug(MC-2299): Fix c2a param not tracked for open_url and close actions (#681) --- .../main/java/com/clevertap/android/sdk/Constants.java | 1 + .../clevertap/android/sdk/inapp/CTInAppBaseFragment.java | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) 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 8e3c29f2d..39478b9ce 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 @@ -446,4 +446,5 @@ public interface Constants { String FLUSH_PUSH_IMPRESSIONS_ONE_TIME_WORKER_NAME = "CTFlushPushImpressionsOneTime"; + String URL_PARAM_DL_SEPARATOR = "__dl__"; } \ No newline at end of file diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFragment.java index d11599b8b..06b9b5bfe 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFragment.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFragment.java @@ -89,13 +89,13 @@ void openActionUrl(String url) { try { final Bundle formData = UriHelper.getAllKeyValuePairs(url, false); - String actionParts = formData.getString(Constants.KEY_C2A); - String callToAction = null; - if (actionParts != null) { - final String[] parts = actionParts.split("__dl__"); + String callToAction = formData.getString(Constants.KEY_C2A); + if (callToAction != null) { + final String[] parts = callToAction.split(Constants.URL_PARAM_DL_SEPARATOR); if (parts.length == 2) { // Decode it here as wzrk_c2a is not decoded by UriHelper callToAction = URLDecoder.decode(parts[0], "UTF-8"); + formData.putString(Constants.KEY_C2A, callToAction); url = parts[1]; } } From 3fa75da31c37ab8378ea5e277815ca36c7052c89 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Mon, 4 Nov 2024 17:35:24 +0530 Subject: [PATCH 024/120] feat(multi_triggers) : delete half of the table when row count is greater than threshold, on CleverTapAPI instance creation MC-2107 --- .../src/main/java/com/clevertap/android/sdk/db/DBManager.kt | 2 ++ .../android/sdk/userEventLogs/UserEventLogDAOImpl.kt | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) 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..3a98611f4 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 @@ -18,6 +18,7 @@ internal class DBManager( ) : BaseDatabaseManager { private var dbAdapter: DBAdapter? = null + private val userEventLogsThreshold = 11_520 //12.59 MB table and index size @WorkerThread @Synchronized @@ -30,6 +31,7 @@ internal class DBManager( dbAdapter.cleanupStaleEvents(PROFILE_EVENTS) dbAdapter.cleanupStaleEvents(PUSH_NOTIFICATION_VIEWED) dbAdapter.cleanUpPushNotifications() + dbAdapter.userEventLogDAO().cleanUpExtraEvents(userEventLogsThreshold) } return dbAdapter } 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 index 1a967ea96..ac07b3c16 100644 --- 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 @@ -297,7 +297,7 @@ internal class UserEventLogDAOImpl( ORDER BY ${Column.LAST_TS} ASC LIMIT ( SELECT CASE - WHEN COUNT(*) > ? THEN COUNT(*) - ? + WHEN COUNT(*) > ? THEN COUNT(*) / 2 ELSE 0 END FROM $tName @@ -306,8 +306,8 @@ internal class UserEventLogDAOImpl( """.trimIndent() // Execute the delete query with the threshold as an argument - db.writableDatabase.execSQL(query, arrayOf(threshold,threshold)) - logger.verbose("Cleaned up extra events in $tName, keeping only $threshold rows.") + db.writableDatabase.execSQL(query, arrayOf(threshold)) + logger.verbose("If events are above $threshold then cleaned up half of the events in $tName") true } catch (e: Exception) { logger.verbose("Error cleaning up extra events in $tName.", e) From d97be4eafe92d927d36c7a4c4fda2f0e31c6d449 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Mon, 4 Nov 2024 20:48:47 +0530 Subject: [PATCH 025/120] feat(multi_triggers) : deprecate event APIs in LocalDataStore MC-2083 --- .../clevertap/android/sdk/LocalDataStore.java | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) 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 a6cc40376..9c799fe9a 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 @@ -17,6 +17,7 @@ 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; @@ -68,6 +69,9 @@ public void changeUser() { resetLocalProfileSync(); } + /** + * @deprecated since v7.0.2. Use {@link #readUserEventLog(String)} + */ EventDetail getEventDetail(String eventName) { try { if (!isPersonalisationEnabled()) { @@ -85,7 +89,9 @@ EventDetail getEventDetail(String eventName) { return null; } } - + /** + * @deprecated since v7.0.2. Use {@link #readUserEventLogs()} + */ Map getEventHistory(Context context) { try { String namespace; @@ -108,6 +114,9 @@ Map getEventHistory(Context context) { } } + /** + * @deprecated since v7.0.2. Use {@link #persistUserEventLog(String)} + */ @WorkerThread public void persistEvent(Context context, JSONObject event, int type) { @@ -395,6 +404,9 @@ public Object getProfileProperty(String key) { } } + /** + * @deprecated since v7.0.2 in favor of DB. See {@link UserEventLog} + */ private EventDetail decodeEventDetails(String name, String encoded) { if (encoded == null) { return null; @@ -405,6 +417,9 @@ private EventDetail decodeEventDetails(String name, String encoded) { Integer.parseInt(parts[2]), name); } + /** + * @deprecated since v7.0.2 in favor of DB. See {@link UserEventLog} + */ private String encodeEventDetails(int first, int last, int count) { return count + "|" + first + "|" + last; } @@ -431,6 +446,9 @@ private int getLocalCacheExpiryInterval(int defaultInterval) { return getIntFromPrefs("local_cache_expires_in", defaultInterval); } + /** + * @deprecated since v7.0.2 in favor of DB. See {@link UserEventLog} + */ private String getStringFromPrefs(String rawKey, String defaultValue, String nameSpace) { if (this.config.isDefaultInstance()) { String _new = StorageHelper @@ -503,6 +521,9 @@ private boolean isPersonalisationEnabled() { return this.config.isPersonalizationEnabled(); } + /** + * @deprecated since v7.0.2. Use {@link #persistUserEventLog(String)} + */ @SuppressWarnings("ConstantConditions") @SuppressLint("CommitPrefEdits") private void persistEvent(Context context, JSONObject event) { @@ -643,7 +664,6 @@ private void _removeProfileField(String key) { } } } - //int k = 0; private void _setProfileField(String key, Object value) { if (value == null) { return; @@ -664,14 +684,16 @@ 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++) { - events.add("profile key - "+i+" - "+ k); - }*/ - //k++; + String s = "profile field - "+k+"-"+i;//RandomStringUtils.randomAlphanumeric(512); + events.add(s); + } + k++;*/ long start = System.currentTimeMillis(); persistUserEventLogsInBulk(fields.keySet()); // persistUserEventLogsInBulk(events); From 42c05c23a67e58b491858ec6b729bda3857a2747 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Wed, 6 Nov 2024 14:42:08 +0530 Subject: [PATCH 026/120] feat(multi_triggers) : use Utils to get millis and replace SQLiteException with generic MC-2083 --- .../android/sdk/userEventLogs/UserEventLogDAOImpl.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index ac07b3c16..8c61abd79 100644 --- 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 @@ -44,7 +44,7 @@ internal class UserEventLogDAOImpl( values, SQLiteDatabase.CONFLICT_REPLACE ) - } catch (e: SQLiteException) { + } catch (e: Exception) { logger.verbose("Error adding row to table $tableName Recreating DB") db.deleteDatabase() DB_UPDATE_ERROR @@ -54,7 +54,7 @@ internal class UserEventLogDAOImpl( @WorkerThread override fun updateEventByDeviceID(deviceID: String, eventName: String): Boolean { val tableName = table.tableName - val now = System.currentTimeMillis() + val now = Utils.getNowInMillis() return try { val query = """ From b1e0865de7c74668f68ba6660e3436f36cefc9f5 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Wed, 6 Nov 2024 15:23:28 +0530 Subject: [PATCH 027/120] feat(multi_triggers) : remove unused class MC-2083 --- .../android/sdk/LocalDataStoreProvider.kt | 16 ---------------- .../android/sdk/network/NetworkManagerTest.kt | 18 ++++++++++-------- 2 files changed, 10 insertions(+), 24 deletions(-) delete mode 100644 clevertap-core/src/test/java/com/clevertap/android/sdk/LocalDataStoreProvider.kt 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/network/NetworkManagerTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/network/NetworkManagerTest.kt index d546dda9b..c197a319b 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,7 +5,6 @@ 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 @@ -24,12 +23,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 @@ -163,8 +165,8 @@ class NetworkManagerTest : BaseTestCase() { 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 localDataStore = + LocalDataStoreProvider.provideLocalDataStore(appCtx, cleverTapInstanceConfig, cryptHandler, deviceInfo)*/ val triggersManager = TriggerManager(appCtx, cleverTapInstanceConfig.accountId, deviceInfo) val inAppResponse = InAppResponse( From b0a0e72c4864829aabee48ba064f0be9dabd7bfd Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Wed, 6 Nov 2024 18:03:31 +0530 Subject: [PATCH 028/120] test(multi_triggers) : fix broken LocalDataStoreTest MC-2344 --- .../android/sdk/LocalDataStoreTest.kt | 45 ++++++++++++++----- 1 file changed, 35 insertions(+), 10 deletions(-) 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..8057a82bc 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,25 @@ 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.UserEventLogDAO +import com.clevertap.android.sdk.userEventLogs.UserEventLogDAOImpl 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.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 +29,7 @@ class LocalDataStoreTest : BaseTestCase() { private lateinit var localDataStoreWithConfigSpy: LocalDataStore private lateinit var cryptHandler : CryptHandler private lateinit var deviceInfo : DeviceInfo + private lateinit var dbAdapter: DBAdapter override fun setUp() { super.setUp() @@ -29,10 +37,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 From 61e5411011c9c7fcdfe0f0234a4691dd255b5367 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Wed, 6 Nov 2024 18:11:05 +0530 Subject: [PATCH 029/120] test(multi_triggers) : fix broken SessionManagerTest MC-2344 --- .../com/clevertap/android/sdk/SessionManagerTest.kt | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) 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..e0bb3b639 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,6 +3,8 @@ 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.validation.Validator import com.clevertap.android.shared.test.BaseTestCase @@ -23,6 +25,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 +37,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) From 98e3457a1dc2b9d86e486b033c2a260aff74f4ea Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Wed, 6 Nov 2024 18:12:28 +0530 Subject: [PATCH 030/120] test(multi_triggers) : fix broken TriggersMatcherTest MC-2344 --- .../sdk/inapp/evaluation/TriggersMatcherTest.kt | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) 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..88c72d636 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,29 @@ 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.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 From c699a65a34dcff24066e202aee636afb41b55b53 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Wed, 6 Nov 2024 19:00:24 +0530 Subject: [PATCH 031/120] test(multi_triggers) : add tests for insertEventByDeviceID in UserEventLogDAOImpl MC-2344 --- .../userEventLogs/UserEventLogDAOImplTest.kt | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 clevertap-core/src/test/java/com/clevertap/android/sdk/userEventLogs/UserEventLogDAOImplTest.kt 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..331c4e0b6 --- /dev/null +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/userEventLogs/UserEventLogDAOImplTest.kt @@ -0,0 +1,118 @@ +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.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@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" + + companion object { + private const val TEST_DEVICE_ID = "test_device_id" + private const val TEST_EVENT_NAME = "test_event" + private const val TEST_EVENT_NAME_2 = "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 insertEventByDeviceID when below memory threshold`() { + + // When + val result = userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // Then + assertTrue(result > 0) + } + + @Test + fun `test insertEventByDeviceID 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.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // Then + assertEquals(-2L, result) // DB_OUT_OF_MEMORY_ERROR + } + + @Test + fun `test insertEventByDeviceID 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.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // Then + assertEquals(-1L, result) // DB_UPDATE_ERROR + + verify { + dbHelper.deleteDatabase() + } + + } + + +} \ No newline at end of file From 3b1eca58fb98828b975344e8bdb3b9291803b1ca Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Wed, 6 Nov 2024 19:06:43 +0530 Subject: [PATCH 032/120] test(multi_triggers) : add tests for updateEventByDeviceID in UserEventLogDAOImpl MC-2344 --- .../userEventLogs/UserEventLogDAOImplTest.kt | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) 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 index 331c4e0b6..9ca50a559 100644 --- 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 @@ -15,11 +15,14 @@ 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 { @@ -114,5 +117,72 @@ class UserEventLogDAOImplTest { } + @Test + fun `test updateEventByDeviceID success`() { + // Given + val insertResult = userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + assertTrue(insertResult > 0) + + // When + val updateResult1 = userEventLogDAO.updateEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + val updateResult2 = userEventLogDAO.updateEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // Then + assertTrue(updateResult1) + assertTrue(updateResult2) + + // Verify count increased + val count = userEventLogDAO.readEventCountByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + assertEquals(3, count) + } + + @Test + fun `test updateEventByDeviceID 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.updateEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // Then + assertFalse(result) + } + + @Test + fun `test updateEventByDeviceID success with timestamp verification`() { + // Given + userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // Mock different time for update + val updateTime = MOCK_TIME + 1000 + every { Utils.getNowInMillis() } returns updateTime + + // When + val result = userEventLogDAO.updateEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // Then + assertTrue(result) + + // Verify event details + val eventLog = userEventLogDAO.readEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + assertNotNull(eventLog) + with(requireNotNull(eventLog)) { + assertEquals(2, countOfEvents) + assertEquals(MOCK_TIME, firstTs) + assertEquals(updateTime, lastTs) + } + + verify { + Utils.getNowInMillis() + } + } } \ No newline at end of file From b5eef52bc6e20ac321d4e75cc4237daf1a8f6297 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Wed, 6 Nov 2024 19:08:26 +0530 Subject: [PATCH 033/120] test(multi_triggers) : add tests for upSertEventsByDeviceID in UserEventLogDAOImpl MC-2344 --- .../userEventLogs/UserEventLogDAOImplTest.kt | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) 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 index 9ca50a559..668f93020 100644 --- 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 @@ -185,4 +185,40 @@ class UserEventLogDAOImplTest { } } + @Test + fun `test upSertEventsByDeviceID with new and existing events`() { + // Given + userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + val eventNames = setOf(TEST_EVENT_NAME, TEST_EVENT_NAME_2) + + // When + val result = userEventLogDAO.upSertEventsByDeviceID(TEST_DEVICE_ID, eventNames) + + // Then + assertTrue(result) + assertEquals(2, userEventLogDAO.readEventCountByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME)) + assertEquals(1, userEventLogDAO.readEventCountByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME_2)) + } + + @Test + fun `test upSertEventsByDeviceID when db error occurs`() { + // Given + val dbHelper = mockk(relaxed = true) + every { dbHelper.writableDatabase.beginTransaction() } throws SQLiteException() + + val dao = UserEventLogDAOImpl(dbHelper, logger, table) + val eventNames = setOf(TEST_EVENT_NAME, TEST_EVENT_NAME_2) + + // When + val result = dao.upSertEventsByDeviceID(TEST_DEVICE_ID, eventNames) + + // Then + assertFalse(result) + + verify { + dbHelper.writableDatabase.beginTransaction() + dbHelper.writableDatabase.endTransaction() // verify that endTransaction is called even when error occurs + } + } + } \ No newline at end of file From 40d62135c35e23b5af8dd6d09cd9e916c544bce1 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Wed, 6 Nov 2024 19:09:39 +0530 Subject: [PATCH 034/120] test(multi_triggers) : add tests for readEventByDeviceID in UserEventLogDAOImpl MC-2344 --- .../userEventLogs/UserEventLogDAOImplTest.kt | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) 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 index 668f93020..edbb07649 100644 --- 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 @@ -221,4 +221,77 @@ class UserEventLogDAOImplTest { } } + @Test + fun `test readEventByDeviceID returns null when event does not exist`() { + // When + val result = userEventLogDAO.readEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // Then + assertNull(result) + } + + @Test + fun `test readEventByDeviceID returns correct event log after insert`() { + // Given + userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // When + val result = userEventLogDAO.readEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // Then + assertNotNull(result) + with(requireNotNull(result)) { + assertEquals(TEST_EVENT_NAME, eventName) + assertEquals(TEST_DEVICE_ID, deviceID) + assertEquals(1, countOfEvents) + assertEquals(MOCK_TIME, firstTs) + assertEquals(MOCK_TIME, lastTs) + } + verify { + Utils.getNowInMillis() + } + } + + @Test + fun `test readEventByDeviceID after multiple updates`() { + // Given + userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // Mock different time for update + val updateTime = MOCK_TIME + 1000 + every { Utils.getNowInMillis() } returns updateTime + + userEventLogDAO.updateEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // When + val result = userEventLogDAO.readEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // Then + assertNotNull(result) + with(requireNotNull(result)) { + assertEquals(TEST_EVENT_NAME, eventName) + assertEquals(TEST_DEVICE_ID, 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 readEventByDeviceID when db error occurs`() { + // Given + val dbHelper = mockk() + every { dbHelper.readableDatabase } throws SQLiteException() + val dao = UserEventLogDAOImpl(dbHelper, logger, table) + + // When + val result = dao.readEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // Then + assertNull(result) + } + } \ No newline at end of file From a3b072b3acc6694e67a1d5e4dadd08dd969efa18 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Wed, 6 Nov 2024 19:19:15 +0530 Subject: [PATCH 035/120] test(multi_triggers) : add tests for readEventCountByDeviceID in UserEventLogDAOImpl MC-2344 --- .../userEventLogs/UserEventLogDAOImplTest.kt | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) 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 index edbb07649..ca9bb3cad 100644 --- 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 @@ -294,4 +294,53 @@ class UserEventLogDAOImplTest { assertNull(result) } + @Test + fun `test readEventCountByDeviceID returns minus one when event does not exist`() { + // When + val result = userEventLogDAO.readEventCountByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // Then + assertEquals(-1, result) + } + + @Test + fun `test readEventCountByDeviceID when db error occurs`() { + // Given + val dbHelper = mockk() + every { dbHelper.readableDatabase } throws SQLiteException() + val dao = UserEventLogDAOImpl(dbHelper, logger, table) + + // When + val result = dao.readEventCountByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // Then + assertEquals(-1, result) + } + + @Test + fun `test readEventCountByDeviceID returns correct count after insert`() { + // Given + userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // When + val result = userEventLogDAO.readEventCountByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // Then + assertEquals(1, result) + } + + @Test + fun `test readEventCountByDeviceID returns correct count after multiple updates`() { + // Given + userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + userEventLogDAO.updateEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + userEventLogDAO.updateEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // When + val result = userEventLogDAO.readEventCountByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // Then + assertEquals(3, result) + } + } \ No newline at end of file From 1536dd8efb8f14ae1b17a0cb87016ac30c09ba1f Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Wed, 6 Nov 2024 19:23:05 +0530 Subject: [PATCH 036/120] test(multi_triggers) : add tests for readEventFirstTsByDeviceID in UserEventLogDAOImpl MC-2344 --- .../userEventLogs/UserEventLogDAOImplTest.kt | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) 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 index ca9bb3cad..6b3160ea6 100644 --- 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 @@ -343,4 +343,62 @@ class UserEventLogDAOImplTest { assertEquals(3, result) } + @Test + fun `test readEventFirstTsByDeviceID returns minus one when event does not exist`() { + // When + val result = userEventLogDAO.readEventFirstTsByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // Then + assertEquals(-1L, result) + } + + @Test + fun `test readEventFirstTsByDeviceID when db error occurs`() { + // Given + val dbHelper = mockk() + every { dbHelper.readableDatabase } throws SQLiteException() + val dao = UserEventLogDAOImpl(dbHelper, logger, table) + + // When + val result = dao.readEventFirstTsByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // Then + assertEquals(-1L, result) + } + + @Test + fun `test readEventFirstTsByDeviceID returns correct timestamp after insert`() { + // Given + userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // When + val result = userEventLogDAO.readEventFirstTsByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // Then + assertEquals(MOCK_TIME, result) + } + + @Test + fun `test readEventFirstTsByDeviceID returns same timestamp after updates`() { + // Given + userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // Mock different time for update + val updateTime = MOCK_TIME + 1000 + every { Utils.getNowInMillis() } returns updateTime + + userEventLogDAO.updateEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + userEventLogDAO.updateEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // When + val result = userEventLogDAO.readEventFirstTsByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // Then + assertEquals(MOCK_TIME, result) // First timestamp should remain same after updates + + verify { + Utils.getNowInMillis() + } + } + } \ No newline at end of file From 5d66aa2ec61fc856969ea422223b3b8e24663673 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Wed, 6 Nov 2024 19:25:37 +0530 Subject: [PATCH 037/120] test(multi_triggers) : add tests for readEventLastTsByDeviceID in UserEventLogDAOImpl MC-2344 --- .../userEventLogs/UserEventLogDAOImplTest.kt | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) 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 index 6b3160ea6..abb5dfa5d 100644 --- 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 @@ -401,4 +401,61 @@ class UserEventLogDAOImplTest { } } + @Test + fun `test readEventLastTsByDeviceID returns minus one when event does not exist`() { + // When + val result = userEventLogDAO.readEventLastTsByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // Then + assertEquals(-1L, result) + } + + @Test + fun `test readEventLastTsByDeviceID when db error occurs`() { + // Given + val dbHelper = mockk() + every { dbHelper.readableDatabase } throws SQLiteException() + val dao = UserEventLogDAOImpl(dbHelper, logger, table) + + // When + val result = dao.readEventLastTsByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // Then + assertEquals(-1L, result) + } + + @Test + fun `test readEventLastTsByDeviceID returns correct timestamp after insert`() { + // Given + userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // When + val result = userEventLogDAO.readEventLastTsByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // Then + assertEquals(MOCK_TIME, result) + } + + @Test + fun `test readEventLastTsByDeviceID returns updated timestamp after updates`() { + // Given + userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // Mock different time for update + val updateTime = MOCK_TIME + 1000 + every { Utils.getNowInMillis() } returns updateTime + + userEventLogDAO.updateEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // When + val result = userEventLogDAO.readEventLastTsByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // Then + assertEquals(updateTime, result) // Last timestamp should be updated + + verify { + Utils.getNowInMillis() + } + } + } \ No newline at end of file From 5795b92e36ff221cf31d2ddde452c2a5ee044eb3 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Wed, 6 Nov 2024 19:28:35 +0530 Subject: [PATCH 038/120] test(multi_triggers) : add tests for eventExistsByDeviceID in UserEventLogDAOImpl MC-2344 --- .../userEventLogs/UserEventLogDAOImplTest.kt | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) 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 index abb5dfa5d..33a12a399 100644 --- 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 @@ -458,4 +458,54 @@ class UserEventLogDAOImplTest { } } + @Test + fun `test eventExistsByDeviceID returns false when event does not exist`() { + // When + val result = userEventLogDAO.eventExistsByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // Then + assertFalse(result) + } + + @Test + fun `test eventExistsByDeviceID when db error occurs`() { + // Given + val dbHelper = mockk() + every { dbHelper.readableDatabase } throws SQLiteException() + val dao = UserEventLogDAOImpl(dbHelper, logger, table) + + // When + val result = dao.eventExistsByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // Then + assertFalse(result) + } + + @Test + fun `test eventExistsByDeviceID returns true after insert`() { + // Given + userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // When + val result = userEventLogDAO.eventExistsByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // Then + assertTrue(result) + } + + @Test + fun `test eventExistsByDeviceID returns true for specific deviceID only`() { + // Given + val otherDeviceId = "other_device_id" + userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // When + val resultForTestDevice = userEventLogDAO.eventExistsByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + val resultForOtherDevice = userEventLogDAO.eventExistsByDeviceID(otherDeviceId, TEST_EVENT_NAME) + + // Then + assertTrue(resultForTestDevice) + assertFalse(resultForOtherDevice) + } + } \ No newline at end of file From d50007497df520ff52e333807556f5392f111ef0 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Wed, 6 Nov 2024 19:43:03 +0530 Subject: [PATCH 039/120] test(multi_triggers) : add tests for eventExistsByDeviceIDAndCount in UserEventLogDAOImpl MC-2344 --- .../userEventLogs/UserEventLogDAOImplTest.kt | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) 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 index 33a12a399..0f0087dce 100644 --- 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 @@ -508,4 +508,82 @@ class UserEventLogDAOImplTest { assertFalse(resultForOtherDevice) } + @Test + fun `test eventExistsByDeviceIDAndCount returns false when event does not exist`() { + // When + val result = userEventLogDAO.eventExistsByDeviceIDAndCount(TEST_DEVICE_ID, TEST_EVENT_NAME, 1) + + // Then + assertFalse(result) + } + + @Test + fun `test eventExistsByDeviceIDAndCount when db error occurs`() { + // Given + val dbHelper = mockk() + every { dbHelper.readableDatabase } throws SQLiteException() + val dao = UserEventLogDAOImpl(dbHelper, logger, table) + + // When + val result = dao.eventExistsByDeviceIDAndCount(TEST_DEVICE_ID, TEST_EVENT_NAME, 1) + + // Then + assertFalse(result) + } + + @Test + fun `test eventExistsByDeviceIDAndCount returns true for matching count`() { + // Given + userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // When + val result = userEventLogDAO.eventExistsByDeviceIDAndCount(TEST_DEVICE_ID, TEST_EVENT_NAME, 1) + + // Then + assertTrue(result) + } + + @Test + fun `test eventExistsByDeviceIDAndCount returns false for non-matching count`() { + // Given + userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // When + val result = userEventLogDAO.eventExistsByDeviceIDAndCount(TEST_DEVICE_ID, TEST_EVENT_NAME, 2) + + // Then + assertFalse(result) + } + + @Test + fun `test eventExistsByDeviceIDAndCount verifies count after updates`() { + // Given + userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + userEventLogDAO.updateEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // When + val resultForCount1 = userEventLogDAO.eventExistsByDeviceIDAndCount(TEST_DEVICE_ID, TEST_EVENT_NAME, 1) + val resultForCount2 = userEventLogDAO.eventExistsByDeviceIDAndCount(TEST_DEVICE_ID, TEST_EVENT_NAME, 2) + + // Then + assertFalse(resultForCount1) + assertTrue(resultForCount2) + } + + @Test fun `test eventExistsByDeviceIDAndCount returns true for specific deviceID and count`(){ + // Given + val otherDeviceId = "other_device_id" + userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + userEventLogDAO.insertEventByDeviceID(otherDeviceId, TEST_EVENT_NAME) + userEventLogDAO.updateEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // When + val resultForTestDevice = userEventLogDAO.eventExistsByDeviceIDAndCount(TEST_DEVICE_ID, TEST_EVENT_NAME, 2) + val resultForOtherDevice = userEventLogDAO.eventExistsByDeviceIDAndCount(otherDeviceId, TEST_EVENT_NAME, 1) + + // Then + assertTrue(resultForTestDevice) + assertTrue(resultForOtherDevice) + } + } \ No newline at end of file From 73613a963ff259c33b33ac1d2b5795fd25ee162c Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Thu, 7 Nov 2024 11:59:18 +0530 Subject: [PATCH 040/120] test(multi_triggers) : add tests for allEventsByDeviceID in UserEventLogDAOImpl MC-2344 --- .../userEventLogs/UserEventLogDAOImplTest.kt | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) 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 index 0f0087dce..dda052ddb 100644 --- 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 @@ -586,4 +586,92 @@ class UserEventLogDAOImplTest { assertTrue(resultForOtherDevice) } + @Test + fun `test allEventsByDeviceID returns empty list when no events exist`() { + // When + val result = userEventLogDAO.allEventsByDeviceID(TEST_DEVICE_ID) + + // 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(TEST_DEVICE_ID) + + // Then + assertTrue(result.isEmpty()) + } + + @Test + fun `test allEventsByDeviceID returns correct list after inserts`() { + // Given + val list = listOf(TEST_EVENT_NAME, TEST_EVENT_NAME_2) + list.forEach { + userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, it) + } + + // When + val result = userEventLogDAO.allEventsByDeviceID(TEST_DEVICE_ID) + + // Then + assertEquals(2, result.size) + + assertTrue(result.all { + list.contains(it.eventName) && it.deviceID == TEST_DEVICE_ID + }) + } + + @Test + fun `test allEventsByDeviceID returns events for specific deviceID only`() { + // Given + val otherDeviceId = "other_device_id" + listOf(TEST_DEVICE_ID, otherDeviceId).forEach { deviceId -> + userEventLogDAO.insertEventByDeviceID(deviceId, TEST_EVENT_NAME) + } + + // When + val results = listOf(TEST_DEVICE_ID, 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.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // Mock different time for second event + val laterTime = MOCK_TIME + 1000 + every { Utils.getNowInMillis() } returns laterTime + + userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME_2) + + // When + val result = userEventLogDAO.allEventsByDeviceID(TEST_DEVICE_ID) + + // Then + assertEquals(2, result.size) + assertEquals(TEST_EVENT_NAME, result[0].eventName) // Earlier event first + assertEquals(TEST_EVENT_NAME_2, result[1].eventName) // Later event second + assertTrue(result[0].lastTs < result[1].lastTs) + + verify { + Utils.getNowInMillis() + } + } + } \ No newline at end of file From b18066139e902abd101acb1f85fc1511be8b1313 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Thu, 7 Nov 2024 12:27:55 +0530 Subject: [PATCH 041/120] test(multi_triggers) : add tests for allEvents in UserEventLogDAOImpl MC-2344 --- .../userEventLogs/UserEventLogDAOImplTest.kt | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) 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 index dda052ddb..76015e2cb 100644 --- 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 @@ -674,4 +674,110 @@ class UserEventLogDAOImplTest { } } + @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(TEST_DEVICE_ID, "other_device_id") + val eventNames = listOf(TEST_EVENT_NAME, TEST_EVENT_NAME_2) + + deviceIds.forEach { deviceId -> + eventNames.forEach { eventName -> + userEventLogDAO.insertEventByDeviceID(deviceId, eventName) + } + } + + // When + val result = userEventLogDAO.allEvents() + + // Then + assertEquals(4, result.size) + result.all { + deviceIds.contains(it.deviceID) && eventNames.contains(it.eventName) + } + } + + @Test + fun `test allEvents returns events ordered by lastTs`() { + // Given + val events = listOf(TEST_EVENT_NAME, TEST_EVENT_NAME_2) + + events.forEachIndexed { index, eventName -> + val mockTime = MOCK_TIME + (index * 1000L) + every { Utils.getNowInMillis() } returns mockTime + userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, eventName) + } + + // 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 count: Int) + + val testData = listOf( + EventData(TEST_DEVICE_ID, TEST_EVENT_NAME, 2), + EventData("other_device_id", TEST_EVENT_NAME_2, 1) + ) + + testData.forEach { (deviceId, eventName, updateCount) -> + userEventLogDAO.insertEventByDeviceID(deviceId, eventName) + repeat(updateCount - 1) { + userEventLogDAO.updateEventByDeviceID(deviceId, eventName) + } + } + + // When + val result = userEventLogDAO.allEvents() + .groupBy { it.deviceID } + .mapValues { (_, events) -> events.associateBy { it.eventName } } + + // Then + testData.forEach { (deviceId, eventName, expectedCount) -> + val event = result[deviceId]?.get(eventName) + assertNotNull(event) + with(requireNotNull(event)) { + assertEquals(deviceId, this.deviceID) + assertEquals(eventName, this.eventName) + assertEquals(expectedCount, this.countOfEvents) + assertEquals(MOCK_TIME, this.firstTs) + assertEquals(MOCK_TIME, this.lastTs) + } + } + } + } \ No newline at end of file From b0a5084db0520243c76b27b821543a98bf1f9920 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Thu, 7 Nov 2024 19:53:07 +0530 Subject: [PATCH 042/120] feat(multi_triggers) : change logic of cleanUpExtraEvents: keep 9216 rows when row count is greater than threshold MC-2107 --- .../clevertap/android/sdk/LocalDataStore.java | 4 +- .../com/clevertap/android/sdk/db/DBManager.kt | 4 +- .../sdk/userEventLogs/UserEventLogDAO.kt | 2 +- .../sdk/userEventLogs/UserEventLogDAOImpl.kt | 56 ++++++++++++------- 4 files changed, 41 insertions(+), 25 deletions(-) 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 9c799fe9a..37a383f8a 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 @@ -263,9 +263,9 @@ public boolean isUserEventLogFirstTime(String eventName) { } @WorkerThread - public boolean cleanUpExtraEvents(int threshold){ + public boolean cleanUpExtraEvents(int threshold, int numberOfRowsToCleanup){ DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context); - boolean cleanUpExtraEvents = dbAdapter.userEventLogDAO().cleanUpExtraEvents(threshold); + boolean cleanUpExtraEvents = dbAdapter.userEventLogDAO().cleanUpExtraEvents(threshold, numberOfRowsToCleanup); getConfigLogger().verbose("cleanUpExtraEvents boolean= "+cleanUpExtraEvents); return cleanUpExtraEvents; } 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 3a98611f4..52469f557 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 @@ -19,6 +19,7 @@ internal class DBManager( private var dbAdapter: DBAdapter? = null private val userEventLogsThreshold = 11_520 //12.59 MB table and index size + private val numberOfRowsToCleanupForUserEventLogs = 2_304 //( 2048 events + 256 profile props = 1 user) @WorkerThread @Synchronized @@ -31,7 +32,8 @@ internal class DBManager( dbAdapter.cleanupStaleEvents(PROFILE_EVENTS) dbAdapter.cleanupStaleEvents(PUSH_NOTIFICATION_VIEWED) dbAdapter.cleanUpPushNotifications() - dbAdapter.userEventLogDAO().cleanUpExtraEvents(userEventLogsThreshold) + dbAdapter.userEventLogDAO() + .cleanUpExtraEvents(userEventLogsThreshold, numberOfRowsToCleanupForUserEventLogs) } return dbAdapter } 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 index 14e814211..7a3da8803 100644 --- 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 @@ -47,5 +47,5 @@ interface UserEventLogDAO { @WorkerThread fun allEvents(): List @WorkerThread - fun cleanUpExtraEvents(threshold: Int): Boolean + fun cleanUpExtraEvents(threshold: Int = 11_520, numberOfRowsToCleanup: Int = 2_304): 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 index 8c61abd79..28b0b627c 100644 --- 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 @@ -3,7 +3,6 @@ package com.clevertap.android.sdk.userEventLogs import android.content.ContentValues import android.database.Cursor import android.database.sqlite.SQLiteDatabase -import android.database.sqlite.SQLiteException import androidx.annotation.WorkerThread import com.clevertap.android.sdk.Logger import com.clevertap.android.sdk.Utils @@ -148,26 +147,26 @@ internal class UserEventLogDAOImpl( @WorkerThread override fun readEventFirstTsByDeviceID(deviceID: String, eventName: String): Long = readEventColumnByDeviceID( - deviceID, - eventName, - Column.FIRST_TS, - defaultValueExtractor = { -1L }, - valueExtractor = { cursor, columnName -> - cursor.getLong(cursor.getColumnIndexOrThrow(columnName)) - } - ) + deviceID, + eventName, + Column.FIRST_TS, + defaultValueExtractor = { -1L }, + valueExtractor = { cursor, columnName -> + cursor.getLong(cursor.getColumnIndexOrThrow(columnName)) + } + ) @WorkerThread override fun readEventLastTsByDeviceID(deviceID: String, eventName: String): Long = readEventColumnByDeviceID( - deviceID, - eventName, - Column.LAST_TS, - defaultValueExtractor = { -1L }, - valueExtractor = { cursor, columnName -> - cursor.getLong(cursor.getColumnIndexOrThrow(columnName)) - } - ) + deviceID, + eventName, + Column.LAST_TS, + defaultValueExtractor = { -1L }, + valueExtractor = { cursor, columnName -> + cursor.getLong(cursor.getColumnIndexOrThrow(columnName)) + } + ) @WorkerThread override fun eventExistsByDeviceID(deviceID: String, eventName: String): Boolean { @@ -284,11 +283,26 @@ internal class UserEventLogDAOImpl( } @WorkerThread - override fun cleanUpExtraEvents(threshold: Int): Boolean { + 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.EVENT_NAME}, ${Column.DEVICE_ID}) IN ( @@ -297,7 +311,7 @@ internal class UserEventLogDAOImpl( ORDER BY ${Column.LAST_TS} ASC LIMIT ( SELECT CASE - WHEN COUNT(*) > ? THEN COUNT(*) / 2 + WHEN COUNT(*) > ? THEN COUNT(*) - ? ELSE 0 END FROM $tName @@ -306,8 +320,8 @@ internal class UserEventLogDAOImpl( """.trimIndent() // Execute the delete query with the threshold as an argument - db.writableDatabase.execSQL(query, arrayOf(threshold)) - logger.verbose("If events are above $threshold then cleaned up half of the events in $tName") + 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) From 0d9eb537463a0b98292b61d3fa97d0023fa76aba Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Thu, 7 Nov 2024 22:21:52 +0530 Subject: [PATCH 043/120] test(multi_triggers) : add tests for cleanUpExtraEvents in UserEventLogDAOImpl MC-2344 --- .../userEventLogs/UserEventLogDAOImplTest.kt | 302 ++++++++++++++++++ 1 file changed, 302 insertions(+) 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 index 76015e2cb..610e27957 100644 --- 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 @@ -780,4 +780,306 @@ class UserEventLogDAOImplTest { } } + @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" } + events.forEach { eventName -> + userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, eventName) + } + + // 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" } + + events.forEachIndexed { index, eventName -> + val mockTime = MOCK_TIME + (index * 1000L) + every { Utils.getNowInMillis() } returns mockTime + userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, eventName) + } + + 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.eventName } + .let { eventNames -> + // First 6 events should be deleted (10 - 4 = 6) + (1..6).forEach { + assertFalse(eventNames.contains("event_$it")) + } + // Last 4 events should remain + (7..10).forEach { + assertTrue(eventNames.contains("event_$it")) + } + } + + verify { + Utils.getNowInMillis() + } + } + + @Test + fun `test cleanUpExtraEvents maintains events when below threshold`() { + // Given + val events = (1..3).map { "event_$it" } + + events.forEachIndexed { index, eventName -> + val mockTime = MOCK_TIME + (index * 1000L) + every { Utils.getNowInMillis() } returns mockTime + userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, eventName) + } + + val threshold = 5 + val numberOfRowsToCleanup = 2 + + // When + val result = userEventLogDAO.cleanUpExtraEvents(threshold, numberOfRowsToCleanup) + + // Then + assertTrue(result) + + userEventLogDAO.allEvents() + .map { it.eventName } + .let { eventNames -> + assertEquals(3, eventNames.size) // All events should remain + assertTrue(eventNames.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" } + + events.forEachIndexed { index, eventName -> + val mockTime = MOCK_TIME + (index * 1000L) + every { Utils.getNowInMillis() } returns mockTime + userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, eventName) + } + + 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.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + + // When + val result = userEventLogDAO.cleanUpExtraEvents(1, 0) + + // Then + assertTrue(result) + userEventLogDAO.allEvents().let { events -> + assertEquals(1, events.size) + assertEquals(TEST_EVENT_NAME, events[0].eventName) + } + } + + @Test + fun `test cleanUpExtraEvents with threshold 1 and numberOfRowsToCleanup 0 when multiple events exist`() { + // Given + val events = (1..3).map { "event_$it" } + + events.forEachIndexed { index, eventName -> + val mockTime = MOCK_TIME + (index * 1000L) + every { Utils.getNowInMillis() } returns mockTime + userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, eventName) + } + + // When + val result = userEventLogDAO.cleanUpExtraEvents(1, 0) + + // Then + assertTrue(result) + userEventLogDAO.allEvents().let { remainingEvents -> + assertEquals(1, remainingEvents.size) + assertEquals("event_3", remainingEvents[0].eventName) // 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(TEST_DEVICE_ID, "other_device_id") + + devices.forEachIndexed { index, deviceId -> + val mockTime = MOCK_TIME + (index * 1000L) + every { Utils.getNowInMillis() } returns mockTime + userEventLogDAO.insertEventByDeviceID(deviceId, TEST_EVENT_NAME) + } + + // 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 + userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, "event_$index") + + // Add some updates to earlier events to mix up lastTs + if (index > 1) { + every { Utils.getNowInMillis() } returns mockTime + 100 + userEventLogDAO.updateEventByDeviceID(TEST_DEVICE_ID, "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].eventName) + } + + verify { + Utils.getNowInMillis() + } + } + } \ No newline at end of file From 692cb75945987f8965ae3a0435942de4bd3adf5b Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Fri, 8 Nov 2024 14:04:39 +0530 Subject: [PATCH 044/120] test(multi_triggers) : add tests for firstTimeOnly in TriggerAdapterTest MC-2344 --- .../inapp/evaluation/TriggerAdapterTest.kt | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) 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..fb4b2e8f3 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 @@ -580,4 +580,46 @@ class TriggerAdapterTest { assertEquals(geoRadiusArray, triggerJSON.optJSONArray(Constants.KEY_GEO_RADIUS_PROPERTIES)) } + + @Test + fun `test firstTimeOnly with firstTimeOnly value as true`() { + // Arrange + val triggerJSON = JSONObject() + triggerJSON.put(Constants.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(Constants.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) + } + } From 1b0d8e7e1f1ac6ba80135e644e35288c75d283e1 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Fri, 8 Nov 2024 15:07:13 +0530 Subject: [PATCH 045/120] feat(multi_triggers) : make matchFirstTimeOnly private MC-2087 --- .../clevertap/android/sdk/inapp/evaluation/TriggersMatcher.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 d156fbe8e..c1fc3adbb 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 @@ -80,7 +80,7 @@ class TriggersMatcher(private val localDataStore: LocalDataStore) { // TODO: matchFirstTimeOnly(trigger, event) implementation @WorkerThread - internal fun matchFirstTimeOnly(trigger: TriggerAdapter): Boolean { + private fun matchFirstTimeOnly(trigger: TriggerAdapter): Boolean { if (!trigger.firstTimeOnly) { return true } From 5b1fa1323eb3f058f17a2715450940ca566a6baa Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Fri, 8 Nov 2024 15:11:20 +0530 Subject: [PATCH 046/120] test(multi_triggers) : add tests for firstTimeOnly in TriggersMatcherTest MC-2344 --- .../inapp/evaluation/TriggersMatcherTest.kt | 57 ++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) 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 88c72d636..a0d365a59 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 @@ -1293,6 +1293,59 @@ 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 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 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( @@ -1533,10 +1586,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("firstTimeOnly", firstTimeOnly) if(profileAttrName != null) put("profileAttrName",profileAttrName) if (propertyConditions.isNotEmpty()) { From b3e664875d7e253d708c067314c7cb0d49a75eaa Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Fri, 8 Nov 2024 19:26:32 +0530 Subject: [PATCH 047/120] feat(multi_triggers) : refactor userEventLogs related APIs in LocalDataStore to return boolean MC-2083 --- .../clevertap/android/sdk/LocalDataStore.java | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) 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 37a383f8a..673b9f552 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 @@ -133,15 +133,15 @@ public void persistEvent(Context context, JSONObject event, int type) { } } @WorkerThread - public void persistUserEventLogsInBulk(Set eventNames){ - upSertUserEventLogsInBulk(eventNames); + public boolean persistUserEventLogsInBulk(Set eventNames){ + return upSertUserEventLogsInBulk(eventNames); } @WorkerThread - public void persistUserEventLog(String eventName) { + public boolean persistUserEventLog(String eventName) { if (eventName == null) { - return; + return false; } Logger logger = config.getLogger(); @@ -150,10 +150,10 @@ public void persistUserEventLog(String eventName) { logger.verbose(accountId,"UserEventLog: Persisting EventLog for event "+eventName); if (isUserEventLogExists(eventName)){ logger.verbose(accountId,"UserEventLog: Updating EventLog for event "+eventName); - updateUserEventLog(eventName); + return updateUserEventLog(eventName); } else { logger.verbose(accountId,"UserEventLog: Inserting EventLog for event "+eventName); - insertUserEventLog(eventName ); + return insertUserEventLog(eventName ); } /* * ==========TESTING BLOCK START ========== @@ -186,41 +186,46 @@ public void persistUserEventLog(String eventName) { */ } catch (Throwable t) { logger.verbose(accountId, "UserEventLog: Failed to insert user event log: for event" + eventName, t); + return false; } } @WorkerThread - private void updateEventByDeviceID(String deviceID, String eventName) { + private boolean updateEventByDeviceID(String deviceID, String eventName) { DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context); boolean updatedEventByDeviceID = dbAdapter.userEventLogDAO().updateEventByDeviceID(deviceID, eventName); getConfigLogger().verbose("updatedEventByDeviceID = "+updatedEventByDeviceID); + return updatedEventByDeviceID; } @WorkerThread - public void updateUserEventLog(String eventName) { + public boolean updateUserEventLog(String eventName) { String deviceID = deviceInfo.getDeviceID(); - updateEventByDeviceID(deviceID,eventName); + return updateEventByDeviceID(deviceID,eventName); } @WorkerThread - public void upSertUserEventLogsInBulk(Set eventNames){ + public boolean upSertUserEventLogsInBulk(Set eventNames){ String deviceID = deviceInfo.getDeviceID(); DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context); boolean upSertEventByDeviceID = dbAdapter.userEventLogDAO().upSertEventsByDeviceID(deviceID, eventNames); getConfigLogger().verbose("upSertEventByDeviceID = "+upSertEventByDeviceID); + return upSertEventByDeviceID; } @WorkerThread - private void insertEventByDeviceID(String deviceID, String eventName) { + private long insertEventByDeviceID(String deviceID, String eventName) { DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context); long rowId = dbAdapter.userEventLogDAO().insertEventByDeviceID(deviceID, eventName); getConfigLogger().verbose("inserted rowId = "+rowId); + return rowId; } @WorkerThread - public void insertUserEventLog(String eventName) { + public boolean insertUserEventLog(String eventName) { String deviceID = deviceInfo.getDeviceID(); - insertEventByDeviceID(deviceID,eventName); + long rowId = insertEventByDeviceID(deviceID, eventName); + return rowId >= 0; } @WorkerThread From e2b4f1dc7f57c56ab882386d970bac4e4b617239 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Fri, 8 Nov 2024 19:38:46 +0530 Subject: [PATCH 048/120] test(multi_triggers) : add tests for persistUserEventLog in LocalDataStoreTest MC-2344 --- .../android/sdk/LocalDataStoreTest.kt | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) 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 8057a82bc..080773cde 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 @@ -11,6 +11,7 @@ import com.clevertap.android.sdk.userEventLogs.UserEventLogDAOImpl import com.clevertap.android.shared.test.BaseTestCase import org.json.JSONObject import org.junit.Test +import org.mockito.ArgumentMatchers.anyString import org.mockito.Mockito import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -297,4 +298,99 @@ 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()).eventExistsByDeviceID(anyString(), anyString()) + } + + @Test + fun `test persistUserEventLog when event exists updates event successfully`() { + // Given + val eventName = "test_event" + Mockito.`when`(userEventLogDaoMock.eventExistsByDeviceID(deviceInfo.deviceID, eventName)) + .thenReturn(true) + Mockito.`when`(userEventLogDaoMock.updateEventByDeviceID(deviceInfo.deviceID, eventName)) + .thenReturn(true) + + // When + val result = localDataStoreWithConfig.persistUserEventLog(eventName) + + // Then + assertTrue(result) + Mockito.verify(userEventLogDaoMock).eventExistsByDeviceID(deviceInfo.deviceID, eventName) + Mockito.verify(userEventLogDaoMock).updateEventByDeviceID(deviceInfo.deviceID, eventName) + Mockito.verify(userEventLogDaoMock, Mockito.never()).insertEventByDeviceID(anyString(), anyString()) + } + + @Test + fun `test persistUserEventLog when event exists but update fails returns false`() { + // Given + val eventName = "test_event" + Mockito.`when`(userEventLogDaoMock.eventExistsByDeviceID(deviceInfo.deviceID, eventName)) + .thenReturn(true) + Mockito.`when`(userEventLogDaoMock.updateEventByDeviceID(deviceInfo.deviceID, eventName)) + .thenReturn(false) + + // When + val result = localDataStoreWithConfig.persistUserEventLog(eventName) + + // Then + assertFalse(result) + } + + @Test + fun `test persistUserEventLog when event does not exist inserts successfully`() { + // Given + val eventName = "test_event" + Mockito.`when`(userEventLogDaoMock.eventExistsByDeviceID(deviceInfo.deviceID, eventName)) + .thenReturn(false) + Mockito.`when`(userEventLogDaoMock.insertEventByDeviceID(deviceInfo.deviceID, eventName)) + .thenReturn(1L) + + // When + val result = localDataStoreWithConfig.persistUserEventLog(eventName) + + // Then + assertTrue(result) + Mockito.verify(userEventLogDaoMock).eventExistsByDeviceID(deviceInfo.deviceID, eventName) + Mockito.verify(userEventLogDaoMock).insertEventByDeviceID(deviceInfo.deviceID, eventName) + Mockito.verify(userEventLogDaoMock, Mockito.never()).updateEventByDeviceID(anyString(), anyString()) + } + + @Test + fun `test persistUserEventLog when event does not exist and insert fails returns false`() { + // Given + val eventName = "test_event" + Mockito.`when`(userEventLogDaoMock.eventExistsByDeviceID(deviceInfo.deviceID, eventName)) + .thenReturn(false) + Mockito.`when`(userEventLogDaoMock.insertEventByDeviceID(deviceInfo.deviceID, eventName)) + .thenReturn(-1L) + + // When + val result = localDataStoreWithConfig.persistUserEventLog(eventName) + + // Then + assertFalse(result) + } + + @Test + fun `test persistUserEventLog when exception occurs returns false`() { + // Given + val eventName = "test_event" + Mockito.`when`(userEventLogDaoMock.eventExistsByDeviceID(deviceInfo.deviceID, eventName)) + .thenThrow(RuntimeException("DB Error")) + + // When + val result = localDataStoreWithConfig.persistUserEventLog(eventName) + + // Then + assertFalse(result) + } } \ No newline at end of file From dc829fbce86103b95dcef5bdaff4c3e3a2bb898b Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Fri, 8 Nov 2024 19:43:32 +0530 Subject: [PATCH 049/120] test(multi_triggers) : add tests for persistUserEventLogsInBulk in LocalDataStoreTest MC-2344 --- .../android/sdk/LocalDataStoreTest.kt | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) 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 080773cde..8fb5b5317 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 @@ -393,4 +393,34 @@ class LocalDataStoreTest : BaseTestCase() { // Then assertFalse(result) } + + @Test + fun `test persistUserEventLogsInBulk success`() { + // Given + val eventNames = setOf("event1", "event2") + Mockito.`when`(userEventLogDaoMock.upSertEventsByDeviceID(deviceInfo.deviceID, eventNames)) + .thenReturn(true) + + // When + val result = localDataStoreWithConfig.persistUserEventLogsInBulk(eventNames) + + // Then + assertTrue(result) + Mockito.verify(userEventLogDaoMock).upSertEventsByDeviceID(deviceInfo.deviceID, eventNames) + } + + @Test + fun `test persistUserEventLogsInBulk when operation fails returns false`() { + // Given + val eventNames = setOf("event1", "event2") + Mockito.`when`(userEventLogDaoMock.upSertEventsByDeviceID(deviceInfo.deviceID, eventNames)) + .thenReturn(false) + + // When + val result = localDataStoreWithConfig.persistUserEventLogsInBulk(eventNames) + + // Then + assertFalse(result) + Mockito.verify(userEventLogDaoMock).upSertEventsByDeviceID(deviceInfo.deviceID, eventNames) + } } \ No newline at end of file From 3d81488f1912616c3b6bcc1d2794b37de1963596 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Fri, 8 Nov 2024 21:31:57 +0530 Subject: [PATCH 050/120] test(multi_triggers) : add tests for isUserEventLogFirstTime in LocalDataStoreTest MC-2344 --- .../android/sdk/LocalDataStoreTest.kt | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) 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 8fb5b5317..98b6aedfe 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 @@ -423,4 +423,133 @@ class LocalDataStoreTest : BaseTestCase() { assertFalse(result) Mockito.verify(userEventLogDaoMock).upSertEventsByDeviceID(deviceInfo.deviceID, eventNames) } + + @Test + fun `test isUserEventLogFirstTime when count is 1 returns true`() { + // Given + val eventName = "test_event" + Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceID(deviceInfo.deviceID, eventName)) + .thenReturn(1) + + // When + val result = localDataStoreWithConfig.isUserEventLogFirstTime(eventName) + + // Then + assertTrue(result) + Mockito.verify(userEventLogDaoMock).readEventCountByDeviceID(deviceInfo.deviceID, eventName) + } + + @Test + fun `test isUserEventLogFirstTime when count is greater than 1 returns false`() { + // Given + val eventName = "test_event" + Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceID(deviceInfo.deviceID, eventName)) + .thenReturn(2) + + // When + val result = localDataStoreWithConfig.isUserEventLogFirstTime(eventName) + + // Then + assertFalse(result) + Mockito.verify(userEventLogDaoMock).readEventCountByDeviceID(deviceInfo.deviceID, eventName) + } + + @Test + fun `test isUserEventLogFirstTime caches result for subsequent calls`() { + // Given + val eventName = "test_event" + Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceID(deviceInfo.deviceID, eventName)) + .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)) + .readEventCountByDeviceID(deviceInfo.deviceID, eventName) + } + + @Test + fun `test isUserEventLogFirstTime when count is 0 returns false`() { + // Given + val eventName = "test_event" + Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceID(deviceInfo.deviceID, eventName)) + .thenReturn(0) + + // When + val result = localDataStoreWithConfig.isUserEventLogFirstTime(eventName) + + // Then + assertFalse(result) + Mockito.verify(userEventLogDaoMock).readEventCountByDeviceID(deviceInfo.deviceID, eventName) + } + + @Test + fun `test isUserEventLogFirstTime when count is -1 returns false`() { + // Given + val eventName = "test_event" + Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceID(deviceInfo.deviceID, eventName)) + .thenReturn(-1) + + // When + val result = localDataStoreWithConfig.isUserEventLogFirstTime(eventName) + + // Then + assertFalse(result) + Mockito.verify(userEventLogDaoMock).readEventCountByDeviceID(deviceInfo.deviceID, eventName) + } + + @Test + fun `test isUserEventLogFirstTime behavior with changing event counts`() { + // Given + val eventName = "test_event" + + // First call setup - count 0 + Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceID(deviceInfo.deviceID, eventName)) + .thenReturn(0) + + // When - First call + val firstCallResult = localDataStoreWithConfig.isUserEventLogFirstTime(eventName) + + // Then + assertFalse(firstCallResult) + Mockito.verify(userEventLogDaoMock).readEventCountByDeviceID(deviceInfo.deviceID, eventName) + + // Given - Second call setup - count 1 + Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceID(deviceInfo.deviceID, eventName)) + .thenReturn(1) + + // When - Second call + val secondCallResult = localDataStoreWithConfig.isUserEventLogFirstTime(eventName) + + // Then + assertTrue(secondCallResult) + Mockito.verify(userEventLogDaoMock, Mockito.times(2)) + .readEventCountByDeviceID(deviceInfo.deviceID, eventName) + + // Given - Third call setup - count 2 + Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceID(deviceInfo.deviceID, eventName)) + .thenReturn(2) + + // When - Third call + val thirdCallResult = localDataStoreWithConfig.isUserEventLogFirstTime(eventName) + + // Then + assertFalse(thirdCallResult) + Mockito.verify(userEventLogDaoMock, Mockito.times(3)) + .readEventCountByDeviceID(deviceInfo.deviceID, eventName) + + // 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)) + .readEventCountByDeviceID(deviceInfo.deviceID, eventName) + } } \ No newline at end of file From 2df7bfa3f36adce080c3a3465b51eb03326cd3a6 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Fri, 8 Nov 2024 21:41:12 +0530 Subject: [PATCH 051/120] test(multi_triggers) : add tests for cleanUpExtraEvents and readUserEventLog in LocalDataStoreTest MC-2344 --- .../android/sdk/LocalDataStoreTest.kt | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) 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 98b6aedfe..915292b97 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 @@ -6,6 +6,7 @@ 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.shared.test.BaseTestCase @@ -552,4 +553,37 @@ class LocalDataStoreTest : BaseTestCase() { Mockito.verify(userEventLogDaoMock, Mockito.times(3)) .readEventCountByDeviceID(deviceInfo.deviceID, eventName) } + + @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 eventName = "test_event" + val userEventLog = UserEventLog(eventName, 123L, 456L, 1, deviceInfo.deviceID) + Mockito.`when`(userEventLogDaoMock.readEventByDeviceID(deviceInfo.deviceID, eventName)) + .thenReturn(userEventLog) + + // When + val result = localDataStoreWithConfig.readUserEventLog(eventName) + + // Then + assertNotNull(result) + assertEquals(userEventLog, result) + Mockito.verify(userEventLogDaoMock).readEventByDeviceID(deviceInfo.deviceID, eventName) + } } \ No newline at end of file From b4e366f7ce89cb4c0b3a594d73eac3f4e9436dbb Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Fri, 8 Nov 2024 22:47:34 +0530 Subject: [PATCH 052/120] test(multi_triggers) : add tests for remaining methods in LocalDataStoreTest MC-2344 --- .../android/sdk/LocalDataStoreTest.kt | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) 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 915292b97..c68185704 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 @@ -586,4 +586,100 @@ class LocalDataStoreTest : BaseTestCase() { assertEquals(userEventLog, result) Mockito.verify(userEventLogDaoMock).readEventByDeviceID(deviceInfo.deviceID, eventName) } + + @Test + fun `test readUserEventLogCount returns correct count when event exists`() { + // Given + val eventName = "test_event" + val expectedCount = 5 + Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceID(deviceInfo.deviceID, eventName)) + .thenReturn(expectedCount) + + // When + val result = localDataStoreWithConfig.readUserEventLogCount(eventName) + + // Then + assertEquals(expectedCount, result) + Mockito.verify(userEventLogDaoMock).readEventCountByDeviceID(deviceInfo.deviceID, eventName) + } + + @Test + fun `test readUserEventLogLastTs returns correct timestamp when event exists`() { + // Given + val eventName = "test_event" + val expectedTs = 1234567890L + Mockito.`when`( + userEventLogDaoMock.readEventLastTsByDeviceID( + deviceInfo.deviceID, + eventName + ) + ) + .thenReturn(expectedTs) + + // When + val result = localDataStoreWithConfig.readUserEventLogLastTs(eventName) + + // Then + assertEquals(expectedTs, result) + Mockito.verify(userEventLogDaoMock) + .readEventLastTsByDeviceID(deviceInfo.deviceID, eventName) + } + + @Test + fun `test readUserEventLogFirstTs returns correct timestamp when event exists`() { + // Given + val eventName = "test_event" + val expectedTs = 1234567890L + Mockito.`when`( + userEventLogDaoMock.readEventFirstTsByDeviceID( + deviceInfo.deviceID, + eventName + ) + ) + .thenReturn(expectedTs) + + // When + val result = localDataStoreWithConfig.readUserEventLogFirstTs(eventName) + + // Then + assertEquals(expectedTs, result) + Mockito.verify(userEventLogDaoMock) + .readEventFirstTsByDeviceID(deviceInfo.deviceID, eventName) + } + + @Test + fun `test readUserEventLogs returns correct event list for device`() { + // Given + val expectedLogs = listOf( + UserEventLog("event1", 1L, 2L, 1, deviceInfo.deviceID), + UserEventLog("event2", 3L, 4L, 2, deviceInfo.deviceID) + ) + 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 = listOf( + UserEventLog("event1", 1L, 2L, 1, "user1"), + UserEventLog("event2", 3L, 4L, 2, "user2") + ) + 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 From a2a6d960e5c2bc2c34ed40fc906f57c5a6ac57b8 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Mon, 11 Nov 2024 12:57:12 +0530 Subject: [PATCH 053/120] test(multi_triggers) : add tests for userEventLogDAO in DBAdapterTest MC-2344 --- .../com/clevertap/android/sdk/db/DBAdapterTest.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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", From 66859e02006060611e8ccd67cfe58ecb1e4fd095 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Mon, 11 Nov 2024 14:07:18 +0530 Subject: [PATCH 054/120] test(multi_triggers) : add tests for updateLocalStore in EventQueueManagerTest MC-2344 --- .../android/sdk/EventQueueManagerTest.kt | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) 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..05412761a 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 @@ -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 { From ace27e2c18e8c8b536b17e3aa30890eda46b4ed6 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Mon, 11 Nov 2024 15:09:54 +0530 Subject: [PATCH 055/120] test(multi_triggers) : add tests for setUserLastVisitTs in SessionManagerTest MC-2344 --- .../com/clevertap/android/sdk/SessionManagerTest.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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 e0bb3b639..bd606491b 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 @@ -8,6 +8,8 @@ import com.clevertap.android.sdk.db.DBManager import com.clevertap.android.sdk.events.EventDetail 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 @@ -171,5 +173,16 @@ class SessionManagerTest : BaseTestCase() { } + @Test + fun `test setUserLastVisitTs`(){ + val localDataStoreMockk = mockk() + sessionManagerDef = SessionManager(configDef,coreMetaData,validator,localDataStoreMockk) + every { localDataStoreMockk.readUserEventLogLastTs(Constants.APP_LAUNCHED_EVENT) } returns 1000000L + sessionManagerDef.setUserLastVisitTs() + assertEquals(1000000L,sessionManagerDef.userLastVisitTs) + + + } + } \ No newline at end of file From dc80fe3d01901daa81b052fb86e7d77cee714620 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Mon, 11 Nov 2024 18:23:33 +0530 Subject: [PATCH 056/120] test(multi_triggers) : add tests for user event logs in CleverTapAPITest MC-2344 --- .../clevertap/android/sdk/CleverTapAPITest.kt | 194 +++++++++++++++++- 1 file changed, 185 insertions(+), 9 deletions(-) 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..d1f3ca394 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.UserEventLog import com.clevertap.android.shared.test.BaseTestCase import com.clevertap.android.shared.test.Constant import org.json.JSONObject @@ -28,15 +29,6 @@ class CleverTapAPITest : BaseTestCase() { corestate = MockCoreState(application, 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 { @@ -61,6 +53,7 @@ class CleverTapAPITest : BaseTestCase() { // Assert assertTrue("isCreatedPostAppLaunch must be true", cleverTapInstanceConfig.isCreatedPostAppLaunch) verify(corestate.sessionManager).setLastVisitTime() + verify(corestate.sessionManager).setUserLastVisitTs() verify(corestate.deviceInfo).setDeviceNetworkInfoReportingFromStorage() verify(corestate.deviceInfo).setCurrentUserOptOutStateFromStorage() val actualConfig = @@ -97,6 +90,7 @@ class CleverTapAPITest : BaseTestCase() { cleverTapInstanceConfig.isCreatedPostAppLaunch ) verify(corestate.sessionManager).setLastVisitTime() + verify(corestate.sessionManager).setUserLastVisitTs() verify(corestate.deviceInfo).setDeviceNetworkInfoReportingFromStorage() verify(corestate.deviceInfo).setCurrentUserOptOutStateFromStorage() @@ -605,6 +599,188 @@ class CleverTapAPITest : BaseTestCase() { } } } + + @Test + fun `test getUserEventLogCount`(){ + // Arrange + val evt = "test" + `when`(corestate.localDataStore.readUserEventLogCount(evt)).thenReturn(1) + + // 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) + cleverTapAPI = CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig) + val userEventLogCountActual = cleverTapAPI.getUserEventLogCount(evt) + + // Assert + assertEquals(1, userEventLogCountActual) + verify(corestate.localDataStore).readUserEventLogCount(evt) + } + } + } + + @Test + fun `test getUserEventLog`(){ + // Arrange + val evt = "test" + val log = UserEventLog(evt,1000L,1000L,1,"dId") + `when`(corestate.localDataStore.readUserEventLog(evt)).thenReturn(log) + + // 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) + cleverTapAPI = CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig) + val userEventLogActual = cleverTapAPI.getUserEventLog(evt) + + // Assert + assertSame(log, userEventLogActual) + verify(corestate.localDataStore).readUserEventLog(evt) + } + } + } + + @Test + fun `test getUserEventLogFirstTs`(){ + // Arrange + val evt = "test" + `when`(corestate.localDataStore.readUserEventLogFirstTs(evt)).thenReturn(1000L) + + // 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) + cleverTapAPI = CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig) + val firstTsActual = cleverTapAPI.getUserEventLogFirstTs(evt) + + // Assert + assertEquals(1000L, firstTsActual) + verify(corestate.localDataStore).readUserEventLogFirstTs(evt) + } + } + } + + @Test + fun `test getUserEventLogLastTs`(){ + // Arrange + val evt = "test" + `when`(corestate.localDataStore.readUserEventLogLastTs(evt)).thenReturn(1000L) + + // 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) + cleverTapAPI = CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig) + val lastTsActual = cleverTapAPI.getUserEventLogLastTs(evt) + + // Assert + assertEquals(1000L, lastTsActual) + verify(corestate.localDataStore).readUserEventLogLastTs(evt) + } + } + + } + + @Test + fun `test getUserEventLogHistory`() { + // Arrange + val logs = listOf( + UserEventLog("event1", 1000L, 1000L, 1, "dId"), + UserEventLog("event2", 2000L, 2000L, 2, "dId") + ) + `when`(corestate.localDataStore.readUserEventLogs()).thenReturn(logs) + + // 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) + cleverTapAPI = CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig) + val historyActual = cleverTapAPI.userEventLogHistory + + // Assert + assertEquals(2, historyActual.size) + assertEquals(logs[0], historyActual["event1"]) + assertEquals(logs[1], historyActual["event2"]) + verify(corestate.localDataStore).readUserEventLogs() + } + } + } + + @Test + fun `test getUserLastVisitTs`() { + // Arrange + `when`(corestate.sessionManager.userLastVisitTs).thenReturn(1000L) + + // 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) + cleverTapAPI = CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig) + val lastVisitTsActual = cleverTapAPI.userLastVisitTs + + // Assert + assertEquals(1000L, lastVisitTsActual) + verify(corestate.sessionManager).userLastVisitTs + } + } + } + + @Test + fun `test getUserAppLaunchCount`() { + // Arrange + `when`(corestate.localDataStore.readUserEventLogCount(Constants.APP_LAUNCHED_EVENT)) + .thenReturn(5) + + // 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) + cleverTapAPI = CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig) + val appLaunchCountActual = cleverTapAPI.userAppLaunchCount + + // Assert + assertEquals(5, appLaunchCountActual) + verify(corestate.localDataStore).readUserEventLogCount(Constants.APP_LAUNCHED_EVENT) + } + } + } /* @Test fun testPushDeepLink(){ // Arrange From 97a48975485889c5129097b1865944ae0e4a6d96 Mon Sep 17 00:00:00 2001 From: Vassil Angelov <148857285+vasct@users.noreply.github.com> Date: Tue, 12 Nov 2024 11:40:50 +0200 Subject: [PATCH 057/120] refactor(MC-2362): Add parsing of url params on open-url action (#684) --- .../sdk/inapp/CTInAppBaseFragment.java | 58 ++++-- .../sdk/inapp/CTInAppBaseFragmentTest.kt | 171 ++++++++++++++++++ 2 files changed, 209 insertions(+), 20 deletions(-) create mode 100644 clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/CTInAppBaseFragmentTest.kt diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFragment.java index 06b9b5bfe..595df1d2d 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFragment.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFragment.java @@ -14,6 +14,8 @@ import com.clevertap.android.sdk.customviews.CloseImageView; import com.clevertap.android.sdk.inapp.images.FileResourceProvider; import com.clevertap.android.sdk.utils.UriHelper; + +import java.io.UnsupportedEncodingException; import java.lang.ref.WeakReference; import java.net.URLDecoder; import java.util.concurrent.atomic.AtomicBoolean; @@ -81,31 +83,47 @@ public void triggerAction( @NonNull CTInAppAction action, @Nullable String callToAction, @Nullable Bundle additionalData) { + if (action.getType() == InAppActionType.OPEN_URL) { + //All URL parameters should be tracked as additional data + final Bundle urlActionData = UriHelper.getAllKeyValuePairs(action.getActionUrl(), false); + + // callToAction is handled as a parameter + String callToActionUrlParam = urlActionData.getString(Constants.KEY_C2A); + // no need to keep it in the data bundle + urlActionData.remove(Constants.KEY_C2A); + + // add all additional params, overriding the url params if there is a collision + if (additionalData != null) { + urlActionData.putAll(additionalData); + } + // Use the merged data for the action + additionalData = urlActionData; + if (callToActionUrlParam != null) { + // check if there is a deeplink within the callToAction param + final String[] parts = callToActionUrlParam.split(Constants.URL_PARAM_DL_SEPARATOR); + if (parts.length == 2) { + // Decode it here as it is not decoded by UriHelper + try { + // Extract the actual callToAction value + callToActionUrlParam = URLDecoder.decode(parts[0], "UTF-8"); + } catch (UnsupportedEncodingException | IllegalArgumentException e) { + config.getLogger().debug("Error parsing c2a param", e); + } + // use the url from the callToAction param + action = CTInAppAction.createOpenUrlAction(parts[1]); + } + } + if (callToAction == null) { + // Use the url param value only if no other value is passed + callToAction = callToActionUrlParam; + } + } Bundle actionData = notifyActionTriggered(action, callToAction != null ? callToAction : "", additionalData); didDismiss(actionData); } void openActionUrl(String url) { - try { - final Bundle formData = UriHelper.getAllKeyValuePairs(url, false); - - String callToAction = formData.getString(Constants.KEY_C2A); - if (callToAction != null) { - final String[] parts = callToAction.split(Constants.URL_PARAM_DL_SEPARATOR); - if (parts.length == 2) { - // Decode it here as wzrk_c2a is not decoded by UriHelper - callToAction = URLDecoder.decode(parts[0], "UTF-8"); - formData.putString(Constants.KEY_C2A, callToAction); - url = parts[1]; - } - } - - CTInAppAction action = CTInAppAction.createOpenUrlAction(url); - config.getLogger().debug("Executing call to action for in-app: " + url); - triggerAction(action, callToAction != null ? callToAction : "", formData); - } catch (Throwable t) { - config.getLogger().debug("Error parsing the in-app notification action!", t); - } + triggerAction(CTInAppAction.createOpenUrlAction(url), null, null); } public void didDismiss(Bundle data) { diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/CTInAppBaseFragmentTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/CTInAppBaseFragmentTest.kt new file mode 100644 index 000000000..648c37de9 --- /dev/null +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/CTInAppBaseFragmentTest.kt @@ -0,0 +1,171 @@ +package com.clevertap.android.sdk.inapp + +import android.net.Uri +import android.os.Bundle +import com.clevertap.android.sdk.Constants +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class CTInAppBaseFragmentTest { + + @Test + fun `triggerAction should parse url parameters as additionalData`() { + val fragment = spyk() + val inAppListener = mockk(relaxed = true) + every { fragment.listener } returns inAppListener + + val param1 = "value" + val param2 = "value 2" + val param3 = "5" + val url = Uri.parse("https://clevertap.com") + .buildUpon() + .appendQueryParameter("param1", param1) + .appendQueryParameter("param2", param2) + .appendQueryParameter("param3", param3) + .build().toString() + + fragment.triggerAction(CTInAppAction.createOpenUrlAction(url), null, null) + verify { + inAppListener.inAppNotificationActionTriggered( + inAppNotification = any(), + action = match { action -> + action.type == InAppActionType.OPEN_URL + && action.actionUrl == url + }, + callToAction = any(), + additionalData = match { data -> + param1 == data.getString("param1") + && param2 == data.getString("param2") + && param3 == data.getString("param3") + }, + activityContext = any() + ) + } + } + + @Test + fun `triggerAction should merge url parameters with provided additionalData `() { + val fragment = spyk() + val inAppListener = mockk(relaxed = true) + every { fragment.listener } returns inAppListener + + val urlParam1 = "value" + val urlParam2 = "value 2" + val urlParam3 = "5" + val url = Uri.parse("https://clevertap.com") + .buildUpon() + .appendQueryParameter("param1", urlParam1) + .appendQueryParameter("param2", urlParam2) + .appendQueryParameter("param3", urlParam3) + .build().toString() + + val dataParam1 = "dataValue" + val dataParam2 = "data value 2" + val data = Bundle().apply { + putString("param1", dataParam1) + putString("param2", dataParam2) + } + + fragment.triggerAction(CTInAppAction.createOpenUrlAction(url), null, data) + verify { + inAppListener.inAppNotificationActionTriggered( + inAppNotification = any(), + action = any(), + callToAction = any(), + additionalData = match { data -> + dataParam1 == data.getString("param1") + && dataParam2 == data.getString("param2") + && urlParam3 == data.getString("param3") + }, + activityContext = any() + ) + } + } + + @Test + fun `triggerAction should use callToAction argument or c2a url param`() { + val fragment = spyk() + val inAppListener = mockk(relaxed = true) + every { fragment.listener } returns inAppListener + + val callToActionParam = "c2aParam" + val url = Uri.parse("https://clevertap.com") + .buildUpon() + .appendQueryParameter(Constants.KEY_C2A, callToActionParam) + .build().toString() + + fragment.triggerAction(CTInAppAction.createOpenUrlAction(url), null, null) + verify { + inAppListener.inAppNotificationActionTriggered( + inAppNotification = any(), + action = any(), + callToAction = callToActionParam, + additionalData = any(), + activityContext = any() + ) + } + + val callToActionArgument = "argument" + fragment.triggerAction(CTInAppAction.createOpenUrlAction(url), callToActionArgument, null) + verify { + inAppListener.inAppNotificationActionTriggered( + inAppNotification = any(), + action = any(), + callToAction = callToActionArgument, + additionalData = any(), + activityContext = any() + ) + } + } + + @Test + fun `triggerAction should parse c2a url param with __dl__ data`() { + val fragment = spyk() + val inAppListener = mockk(relaxed = true) + every { fragment.listener } returns inAppListener + + val dl = "https://deeplink.com?param1=asd¶m2=value2" + val callToActionParam = "c2aParam" + val param1 = "value" + val url = Uri.parse("https://clevertap.com") + .buildUpon() + .appendQueryParameter(Constants.KEY_C2A, "${callToActionParam}__dl__$dl") + .appendQueryParameter("param1", param1) + .build().toString() + + fragment.triggerAction(CTInAppAction.createOpenUrlAction(url), null, null) + verify { + inAppListener.inAppNotificationActionTriggered( + inAppNotification = any(), + action = any(), + callToAction = callToActionParam, + additionalData = match { data -> + data.size() == 1 + && param1 == data.getString("param1") + }, + activityContext = any() + ) + } + + val callToActionArgument = "argument" + fragment.triggerAction(CTInAppAction.createOpenUrlAction(url), callToActionArgument, null) + verify { + inAppListener.inAppNotificationActionTriggered( + inAppNotification = any(), + action = any(), + callToAction = callToActionArgument, + additionalData = match { data -> + data.size() == 1 + && param1 == data.getString("param1") + }, + activityContext = any() + ) + } + } +} From c7f3a4f751c6151022e9c0f22f37e87ae0233128 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Tue, 12 Nov 2024 16:27:07 +0530 Subject: [PATCH 058/120] feat(multi_triggers) : remove TODOs MC-2252 --- .../java/com/clevertap/android/sdk/CleverTapFactory.java | 2 +- .../com/clevertap/android/sdk/events/EventQueueManager.java | 5 ----- .../clevertap/android/sdk/inapp/evaluation/TriggerAdapter.kt | 3 --- .../android/sdk/inapp/evaluation/TriggersMatcher.kt | 5 +---- .../android/sdk/userEventLogs/UserEventLogDAOImpl.kt | 4 ++-- 5 files changed, 4 insertions(+), 15 deletions(-) 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 912c95db8..f1eae5301 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 @@ -118,7 +118,7 @@ static CoreState getCoreState(Context context, CleverTapInstanceConfig cleverTap ctLockManager, callbackManager, deviceInfo, baseDatabaseManager); coreState.setControllerManager(controllerManager); - TriggersMatcher triggersMatcher = new TriggersMatcher(localDataStore/* TODO: inject localDataStore */); + TriggersMatcher triggersMatcher = new TriggersMatcher(localDataStore); TriggerManager triggersManager = new TriggerManager(context, config.getAccountId(), deviceInfo); ImpressionManager impressionManager = new ImpressionManager(storeRegistry); LimitsMatcher limitsMatcher = new LimitsMatcher(impressionManager, triggersManager); 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 06a346b09..55293e61c 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 @@ -314,7 +314,6 @@ public void processEvent(final Context context, final JSONObject event, final in } localDataStore.setDataSyncFlag(event); baseDatabaseManager.queueEventToDB(context, event, eventType); - //updateLocalStore(context, event, eventType);// TODO: remove from here scheduleQueueFlush(context); } catch (Throwable e) { @@ -459,7 +458,6 @@ public Future queueEvent(final Context context, final JSONObject event, final @Override @WorkerThread public Void call() { - // TODO: add here updateLocalStore(context, event, eventType); String eventName = eventMediator.getEventName(event); Location userLocation = cleverTapMetaData.getLocationFromUser(); @@ -596,10 +594,7 @@ public void run() { @WorkerThread private void updateLocalStore(final String eventName, final int type) { if (type == Constants.RAISED_EVENT) { - // TODO: persist event in DB - 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 46bd65bb4..4a986ffa2 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 @@ -101,9 +101,6 @@ class TriggerAdapter(triggerJSON: JSONObject) { */ val profileAttrName: String? = triggerJSON.optString(Constants.KEY_PROFILE_ATTR_NAME, null) - /** - * TODO(MT): Add firstTimeOnly boolean property - */ val firstTimeOnly: Boolean = triggerJSON.optBoolean(Constants.KEY_FIRST_TIME_ONLY, false) /** 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 c1fc3adbb..2d392c989 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 @@ -60,9 +60,7 @@ class TriggersMatcher(private val localDataStore: LocalDataStore) { if (!matchPropertyConditions(trigger, event)) { return false } - /** - * TODO: Add matchFirstTimeOnly(trigger, event) - */ + if (!matchFirstTimeOnly(trigger)) { return false } @@ -78,7 +76,6 @@ class TriggersMatcher(private val localDataStore: LocalDataStore) { return true } - // TODO: matchFirstTimeOnly(trigger, event) implementation @WorkerThread private fun matchFirstTimeOnly(trigger: TriggerAdapter): Boolean { if (!trigger.firstTimeOnly) { 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 index 28b0b627c..25bef2bd1 100644 --- 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 @@ -222,7 +222,7 @@ internal class UserEventLogDAOImpl( } } - //TODO: Create index on deviceID,lastTs column if this method is frequently used + // Create index on deviceID,lastTs column if this method is frequently used @WorkerThread override fun allEventsByDeviceID(deviceID: String): List { val tName = table.tableName @@ -253,7 +253,7 @@ internal class UserEventLogDAOImpl( } } - //TODO: Create index on lastTs column if this method is frequently used + // Create index on lastTs column if this method is frequently used @WorkerThread override fun allEvents(): List { val tName = table.tableName From 09ac6ec368f5fb39ac207736e752fccc4640aa48 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Tue, 12 Nov 2024 20:26:56 +0530 Subject: [PATCH 059/120] feat(multi_triggers) : remove TODO from LocalDataStore MC-2252 --- .../src/main/java/com/clevertap/android/sdk/LocalDataStore.java | 1 - 1 file changed, 1 deletion(-) 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 673b9f552..ee1c359c7 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 @@ -157,7 +157,6 @@ public boolean persistUserEventLog(String eventName) { } /* * ==========TESTING BLOCK START ========== - * TODO: Remove block code after testing */ /*cleanUpExtraEvents(50); From cf57bfc283142988df1a92e9506a323765391b53 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Wed, 13 Nov 2024 18:21:54 +0530 Subject: [PATCH 060/120] feat(multi_triggers) : fix upSertEventsByDeviceID method name typo MC-2252 --- .../java/com/clevertap/android/sdk/LocalDataStore.java | 10 +++++----- .../android/sdk/userEventLogs/UserEventLogDAO.kt | 2 +- .../android/sdk/userEventLogs/UserEventLogDAOImpl.kt | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) 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 ee1c359c7..fcbd2af45 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 @@ -134,7 +134,7 @@ public void persistEvent(Context context, JSONObject event, int type) { } @WorkerThread public boolean persistUserEventLogsInBulk(Set eventNames){ - return upSertUserEventLogsInBulk(eventNames); + return upsertUserEventLogsInBulk(eventNames); } @WorkerThread @@ -204,12 +204,12 @@ public boolean updateUserEventLog(String eventName) { } @WorkerThread - public boolean upSertUserEventLogsInBulk(Set eventNames){ + public boolean upsertUserEventLogsInBulk(Set eventNames){ String deviceID = deviceInfo.getDeviceID(); DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context); - boolean upSertEventByDeviceID = dbAdapter.userEventLogDAO().upSertEventsByDeviceID(deviceID, eventNames); - getConfigLogger().verbose("upSertEventByDeviceID = "+upSertEventByDeviceID); - return upSertEventByDeviceID; + boolean upsertEventByDeviceID = dbAdapter.userEventLogDAO().upsertEventsByDeviceID(deviceID, eventNames); + getConfigLogger().verbose("upsertEventByDeviceID = "+upsertEventByDeviceID); + return upsertEventByDeviceID; } @WorkerThread 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 index 7a3da8803..19d7a9b78 100644 --- 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 @@ -13,7 +13,7 @@ interface UserEventLogDAO { fun updateEventByDeviceID(deviceID: String, eventName: String): Boolean @WorkerThread - fun upSertEventsByDeviceID(deviceID: String, eventNameList: Set): Boolean + fun upsertEventsByDeviceID(deviceID: String, eventNameList: Set): Boolean // Read an event by deviceID @WorkerThread 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 index 25bef2bd1..d169394b2 100644 --- 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 @@ -75,7 +75,7 @@ internal class UserEventLogDAOImpl( } @WorkerThread - override fun upSertEventsByDeviceID(deviceID: String, eventNameList: Set): Boolean { + override fun upsertEventsByDeviceID(deviceID: String, eventNameList: Set): Boolean { val tableName = table.tableName logger.verbose("UserEventLog: upSert EventLog for bulk events") return try { From 194923d530517f38db5e23d3aa6c8a5cb8a566f9 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Wed, 13 Nov 2024 18:38:01 +0530 Subject: [PATCH 061/120] feat(multi_triggers) : add deprecated annotation for old user event apis MC-2252 --- .../clevertap/android/sdk/CleverTapAPI.java | 21 ++++++++++++------- .../clevertap/android/sdk/LocalDataStore.java | 21 ++++++++++++------- 2 files changed, 28 insertions(+), 14 deletions(-) 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 6e3fa8d66..ef81b789d 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 @@ -1612,10 +1612,11 @@ 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.0.2. Use {@link #getUserEventLogCount(String)} instead. + * @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) { @@ -1650,10 +1651,11 @@ public int getUserEventLogCount(String eventName) { * * @param event The event name for which you want the Event details * @return The {@link EventDetail} object - * @deprecated since v7.0.2. Use {@link #getUserEventLog(String)} instead. + * @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); } @@ -1774,10 +1776,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.0.2. Use {@link #getUserEventLogFirstTs(String)} instead. + * @deprecated since v7.1.0. Use {@link #getUserEventLogFirstTs(String)} instead. * getUserEventLogFirstTs() provides user-specific event first occurrence timestamp. */ @SuppressWarnings({"unused"}) + @Deprecated(since = "7.1.0") public int getFirstTime(String event) { EventDetail eventDetail = coreState.getLocalDataStore().getEventDetail(event); if (eventDetail != null) { @@ -1833,10 +1836,11 @@ 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.0.2. Use {@link #getUserEventLogHistory()} instead. + * @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); } @@ -1975,10 +1979,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.0.2. Use {@link #getUserEventLogLastTs(String)} instead. + * @deprecated since v7.1.0. Use {@link #getUserEventLogLastTs(String)} instead. * getUserEventLogLastTs() provides user-specific event last occurrence timestamp. */ @SuppressWarnings({"unused"}) + @Deprecated(since = "7.1.0") public int getLastTime(String event) { EventDetail eventDetail = coreState.getLocalDataStore().getEventDetail(event); if (eventDetail != null) { @@ -2035,10 +2040,11 @@ public void setLocation(Location location) { * Returns the timestamp of the previous visit * * @return Timestamp of previous visit in int - * @deprecated since v7.0.2. Use {@link #getUserLastVisitTs()} instead. + * @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(); } @@ -2132,10 +2138,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.0.2. Use {@link #getUserAppLaunchCount()} instead. + * @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) { 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 fcbd2af45..583ae29a9 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 @@ -70,8 +70,9 @@ public void changeUser() { } /** - * @deprecated since v7.0.2. Use {@link #readUserEventLog(String)} + * @deprecated since v7.1.0. Use {@link #readUserEventLog(String)} */ + @Deprecated(since = "7.1.0") EventDetail getEventDetail(String eventName) { try { if (!isPersonalisationEnabled()) { @@ -90,8 +91,9 @@ EventDetail getEventDetail(String eventName) { } } /** - * @deprecated since v7.0.2. Use {@link #readUserEventLogs()} + * @deprecated since v7.1.0. Use {@link #readUserEventLogs()} */ + @Deprecated(since = "7.1.0") Map getEventHistory(Context context) { try { String namespace; @@ -115,8 +117,9 @@ Map getEventHistory(Context context) { } /** - * @deprecated since v7.0.2. Use {@link #persistUserEventLog(String)} + * @deprecated since v7.1.0. Use {@link #persistUserEventLog(String)} */ + @Deprecated(since = "7.1.0") @WorkerThread public void persistEvent(Context context, JSONObject event, int type) { @@ -409,8 +412,9 @@ public Object getProfileProperty(String key) { } /** - * @deprecated since v7.0.2 in favor of DB. See {@link UserEventLog} + * @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; @@ -422,8 +426,9 @@ private EventDetail decodeEventDetails(String name, String encoded) { } /** - * @deprecated since v7.0.2 in favor of DB. See {@link UserEventLog} + * @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; } @@ -451,8 +456,9 @@ private int getLocalCacheExpiryInterval(int defaultInterval) { } /** - * @deprecated since v7.0.2 in favor of DB. See {@link UserEventLog} + * @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 @@ -526,8 +532,9 @@ private boolean isPersonalisationEnabled() { } /** - * @deprecated since v7.0.2. Use {@link #persistUserEventLog(String)} + * @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) { From e894266e44593b815fb790ebdbb9c903c55f3e90 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Wed, 13 Nov 2024 20:30:07 +0530 Subject: [PATCH 062/120] test(multi_triggers) : fix broken tests after renaming method upSertEventsByDeviceID MC-2344 --- .../java/com/clevertap/android/sdk/LocalDataStoreTest.kt | 8 ++++---- .../android/sdk/userEventLogs/UserEventLogDAOImplTest.kt | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) 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 c68185704..9f4d5aed9 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 @@ -399,7 +399,7 @@ class LocalDataStoreTest : BaseTestCase() { fun `test persistUserEventLogsInBulk success`() { // Given val eventNames = setOf("event1", "event2") - Mockito.`when`(userEventLogDaoMock.upSertEventsByDeviceID(deviceInfo.deviceID, eventNames)) + Mockito.`when`(userEventLogDaoMock.upsertEventsByDeviceID(deviceInfo.deviceID, eventNames)) .thenReturn(true) // When @@ -407,14 +407,14 @@ class LocalDataStoreTest : BaseTestCase() { // Then assertTrue(result) - Mockito.verify(userEventLogDaoMock).upSertEventsByDeviceID(deviceInfo.deviceID, eventNames) + Mockito.verify(userEventLogDaoMock).upsertEventsByDeviceID(deviceInfo.deviceID, eventNames) } @Test fun `test persistUserEventLogsInBulk when operation fails returns false`() { // Given val eventNames = setOf("event1", "event2") - Mockito.`when`(userEventLogDaoMock.upSertEventsByDeviceID(deviceInfo.deviceID, eventNames)) + Mockito.`when`(userEventLogDaoMock.upsertEventsByDeviceID(deviceInfo.deviceID, eventNames)) .thenReturn(false) // When @@ -422,7 +422,7 @@ class LocalDataStoreTest : BaseTestCase() { // Then assertFalse(result) - Mockito.verify(userEventLogDaoMock).upSertEventsByDeviceID(deviceInfo.deviceID, eventNames) + Mockito.verify(userEventLogDaoMock).upsertEventsByDeviceID(deviceInfo.deviceID, eventNames) } @Test 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 index 610e27957..eb57583be 100644 --- 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 @@ -186,13 +186,13 @@ class UserEventLogDAOImplTest { } @Test - fun `test upSertEventsByDeviceID with new and existing events`() { + fun `test upsertEventsByDeviceID with new and existing events`() { // Given userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) val eventNames = setOf(TEST_EVENT_NAME, TEST_EVENT_NAME_2) // When - val result = userEventLogDAO.upSertEventsByDeviceID(TEST_DEVICE_ID, eventNames) + val result = userEventLogDAO.upsertEventsByDeviceID(TEST_DEVICE_ID, eventNames) // Then assertTrue(result) @@ -201,7 +201,7 @@ class UserEventLogDAOImplTest { } @Test - fun `test upSertEventsByDeviceID when db error occurs`() { + fun `test upsertEventsByDeviceID when db error occurs`() { // Given val dbHelper = mockk(relaxed = true) every { dbHelper.writableDatabase.beginTransaction() } throws SQLiteException() @@ -210,7 +210,7 @@ class UserEventLogDAOImplTest { val eventNames = setOf(TEST_EVENT_NAME, TEST_EVENT_NAME_2) // When - val result = dao.upSertEventsByDeviceID(TEST_DEVICE_ID, eventNames) + val result = dao.upsertEventsByDeviceID(TEST_DEVICE_ID, eventNames) // Then assertFalse(result) From 138db9c99fa752f5c7ea5bc27b9e594b8081bbfb Mon Sep 17 00:00:00 2001 From: Vassil Angelov <148857285+vasct@users.noreply.github.com> Date: Wed, 13 Nov 2024 19:53:32 +0200 Subject: [PATCH 063/120] task(MC-2360): Add normalized event and property names evaluation (#685) * task(MC-2360): Add normalized event and property names evaluation --- .../java/com/clevertap/android/sdk/Utils.java | 31 +++++++++++++++++++ .../sdk/inapp/evaluation/TriggersMatcher.kt | 16 ++++++---- .../android/sdk/validation/Validator.java | 4 ++- .../com/clevertap/android/sdk/UtilsTest.kt | 13 ++++++++ 4 files changed, 57 insertions(+), 7 deletions(-) diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/Utils.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/Utils.java index d39a91ec1..dbcb8b152 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/Utils.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/Utils.java @@ -30,6 +30,7 @@ import android.telephony.TelephonyManager; import android.text.TextUtils; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.RestrictTo; import androidx.annotation.WorkerThread; import androidx.core.content.ContextCompat; @@ -46,13 +47,19 @@ import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Locale; +import java.util.Objects; import java.util.Scanner; +import java.util.regex.Pattern; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; public final class Utils { + private static final Pattern normalizedNameExcludePattern = Pattern.compile("\\s+"); + public static boolean containsIgnoreCase(Collection collection, String key) { if (collection == null || key == null) { return false; @@ -551,4 +558,28 @@ public static String readAssetFile(Context context, String fileName) throws IOEx return new Scanner(inputStream).useDelimiter("\\A").next(); } } + + /** + * Get the CT normalized version of an event or a property name. + * + * @param name The event/property name + */ + public static String getNormalizedName(@Nullable String name) { + if (name == null) { + return null; + } + // lowercase with English locale for consistent behavior with the backend and across different device locales + return normalizedNameExcludePattern.matcher(name).replaceAll("").toLowerCase(Locale.ENGLISH); + } + + /** + * Check if two event/property names are equal with applied CT normalization + * + * @param name Event or property name + * @param other Event or property name to compare to + * @see #getNormalizedName(String) + */ + public static boolean areNamesNormalizedEqual(@Nullable String name, @Nullable String other) { + return Objects.equals(getNormalizedName(name), getNormalizedName(other)); + } } \ No newline at end of file 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 ab5cbd23b..7a5ce3519 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 @@ -24,11 +24,9 @@ class TriggersMatcher { * within that event are met, the function returns `true`. * * @param whenTriggers A list of event triggers with conditions to match against the event. - * @param eventName The name of the event to be matched. - * @param eventProperties A map of event properties where keys are property names and - * values are property values. - * @return `true` if any event matches, and all conditions - * within that event are met, `false` otherwise. + * @param event The [EventAdapter] having event to be matched + * @return `true` if any event matches, and all condition within that event are met, + * `false` otherwise. */ fun matchEvent( whenTriggers: List, @@ -51,7 +49,13 @@ class TriggersMatcher { @VisibleForTesting internal fun match(trigger: TriggerAdapter, event: EventAdapter): Boolean { // Evaluate further if either the eventNames match or the profileAttrName's match. Make sure both profileAttrName's are not null and equal - if (event.eventName != trigger.eventName && (event.profileAttrName == null || !event.profileAttrName.equals(trigger.profileAttrName, true))) { + if (!Utils.areNamesNormalizedEqual( + event.eventName, + trigger.eventName + ) && (event.profileAttrName == null || !Utils.areNamesNormalizedEqual( + event.profileAttrName, trigger.profileAttrName + )) + ) { return false } diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/validation/Validator.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/validation/Validator.java index 9398ab627..335346de0 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/validation/Validator.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/validation/Validator.java @@ -4,6 +4,8 @@ import com.clevertap.android.sdk.Constants; import com.clevertap.android.sdk.Logger; +import com.clevertap.android.sdk.Utils; + import java.util.ArrayList; import java.util.BitSet; import java.util.Date; @@ -338,7 +340,7 @@ public ValidationResult isRestrictedEventName(String name) { return error; } for (String x : restrictedNames) { - if (name.equalsIgnoreCase(x)) { + if (Utils.areNamesNormalizedEqual(name, x)) { // The event name is restricted ValidationResult vr = ValidationResultFactory.create(513, Constants.RESTRICTED_EVENT_NAME, name); error.setErrorCode(vr.getErrorCode()); diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/UtilsTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/UtilsTest.kt index cdcbe8a4b..5eb851f93 100644 --- a/clevertap-core/src/test/java/com/clevertap/android/sdk/UtilsTest.kt +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/UtilsTest.kt @@ -53,6 +53,7 @@ import com.google.firebase.messaging.RemoteMessage import org.json.JSONArray import org.json.JSONObject import org.junit.* +import org.junit.jupiter.api.assertAll import org.junit.runner.* import org.robolectric.RobolectricTestRunner import org.robolectric.Shadows @@ -834,6 +835,18 @@ class UtilsTest : BaseTestCase() { } } + @Test + fun test_areNormalizedNamesEqual_should_compare_correctly() { + assertTrue(Utils.areNamesNormalizedEqual(null, null)) + assertTrue(Utils.areNamesNormalizedEqual("", "")) + assertTrue(Utils.areNamesNormalizedEqual("Event 1", "Event1")) + assertTrue(Utils.areNamesNormalizedEqual("Event 1", "event1")) + assertTrue(Utils.areNamesNormalizedEqual("Event 1", "EVENT 1")) + assertFalse(Utils.areNamesNormalizedEqual("Event 1", null)) + assertFalse(Utils.areNamesNormalizedEqual("", null)) + assertFalse(Utils.areNamesNormalizedEqual("Event 1", "Event 2")) + } + //------------------------------------------------------------------------------------ private fun prepareForWifiConnectivityTest( From 1cdd2f1aa27dc8df9e098978ceb784c6501b7cb7 Mon Sep 17 00:00:00 2001 From: Vassil Angelov <148857285+vasct@users.noreply.github.com> Date: Thu, 14 Nov 2024 11:05:34 +0200 Subject: [PATCH 064/120] chore(MC-1987): Update HTML template for In-Apps preview (#680) * chore(MC-1987): Update HTML template for In-Apps preview --- .../src/main/assets/image_interstitial.html | 37 +++++++------------ .../android/sdk/AnalyticsManager.java | 3 +- .../com/clevertap/android/sdk/Constants.java | 1 + 3 files changed, 17 insertions(+), 24 deletions(-) diff --git a/clevertap-core/src/main/assets/image_interstitial.html b/clevertap-core/src/main/assets/image_interstitial.html index 51d6237fe..a143d2b02 100644 --- a/clevertap-core/src/main/assets/image_interstitial.html +++ b/clevertap-core/src/main/assets/image_interstitial.html @@ -1,37 +1,28 @@ - +
-
- - - - -
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
- + \ No newline at end of file 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 db44f04c4..36d7edefb 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 @@ -484,7 +484,8 @@ public Void call() { JSONObject inappPreviewPayload = new JSONObject(inappPreviewString); JSONArray inappNotifs = new JSONArray(); - if (Constants.INAPP_IMAGE_INTERSTITIAL_TYPE.equals(inappPreviewPayloadType)) { + if (Constants.INAPP_IMAGE_INTERSTITIAL_TYPE.equals(inappPreviewPayloadType) + || Constants.INAPP_ADVANCED_BUILDER_TYPE.equals(inappPreviewPayloadType)) { inappNotifs.put(getHalfInterstitialInApp(inappPreviewPayload)); } else { inappNotifs.put(inappPreviewPayload); 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 39478b9ce..e33557d89 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 @@ -77,6 +77,7 @@ public interface Constants { String INAPP_PREVIEW_PUSH_PAYLOAD_KEY = "wzrk_inapp"; String INAPP_PREVIEW_PUSH_PAYLOAD_TYPE_KEY = "wzrk_inapp_type"; String INAPP_IMAGE_INTERSTITIAL_TYPE = "image-interstitial"; + String INAPP_ADVANCED_BUILDER_TYPE = "advanced-builder"; String INAPP_IMAGE_INTERSTITIAL_CONFIG = "imageInterstitialConfig"; String INAPP_HTML_SPLIT = "\"##Vars##\""; String INAPP_IMAGE_INTERSTITIAL_HTML_NAME = "image_interstitial.html"; From 3dd8174c0ae9123eb7d26eae965cb74a3e107fc8 Mon Sep 17 00:00:00 2001 From: Vassil Angelov <148857285+vasct@users.noreply.github.com> Date: Thu, 14 Nov 2024 11:06:59 +0200 Subject: [PATCH 065/120] Add normalized names check to discarded events check (#688) --- .../java/com/clevertap/android/sdk/validation/Validator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/validation/Validator.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/validation/Validator.java index 335346de0..eab6b2c04 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/validation/Validator.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/validation/Validator.java @@ -310,7 +310,7 @@ public ValidationResult isEventDiscarded(String name) { } if (getDiscardedEvents() != null) { for (String x : getDiscardedEvents()) { - if (name.equalsIgnoreCase(x)) { + if (Utils.areNamesNormalizedEqual(name, x)) { // The event name is discarded ValidationResult vr = ValidationResultFactory.create(513, Constants.DISCARDED_EVENT_NAME, name); error.setErrorCode(vr.getErrorCode()); From ffebac20860f0c9bdc3381ce51d273cb37a7e0b6 Mon Sep 17 00:00:00 2001 From: CTLalit <144685420+CTLalit@users.noreply.github.com> Date: Thu, 14 Nov 2024 16:46:55 +0530 Subject: [PATCH 066/120] bug(SDK-4107): Make clevertap instance singleton (#687) - initialises singleton clevertap instance in application class - uses singleton ct instance in whole app --- sample/build.gradle | 4 +- .../com/clevertap/demo/HomeScreenActivity.kt | 76 ++++++++++--------- .../java/com/clevertap/demo/MyApplication.kt | 72 ++++++++++-------- .../com/clevertap/demo/WebViewActivity.kt | 2 +- .../demo/ui/main/HomeScreenFragment.kt | 3 +- 5 files changed, 85 insertions(+), 72 deletions(-) diff --git a/sample/build.gradle b/sample/build.gradle index 961806d56..acdfe8dc8 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -18,8 +18,8 @@ android { applicationId "com.clevertap.demo" minSdkVersion 21 targetSdkVersion 34 - versionCode 7000000 - versionName "7.0.0" + versionCode 7000002 + versionName "7.0.2" multiDexEnabled true testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } diff --git a/sample/src/main/java/com/clevertap/demo/HomeScreenActivity.kt b/sample/src/main/java/com/clevertap/demo/HomeScreenActivity.kt index 4dc3f67f2..bd77a0024 100644 --- a/sample/src/main/java/com/clevertap/demo/HomeScreenActivity.kt +++ b/sample/src/main/java/com/clevertap/demo/HomeScreenActivity.kt @@ -13,7 +13,6 @@ import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.commitNow import com.clevertap.android.sdk.* -import com.clevertap.android.sdk.CleverTapAPI.LogLevel.VERBOSE import com.clevertap.android.sdk.displayunits.DisplayUnitListener import com.clevertap.android.sdk.displayunits.model.CleverTapDisplayUnit import com.clevertap.android.sdk.inapp.CTInAppNotification @@ -37,7 +36,7 @@ class HomeScreenActivity : AppCompatActivity(), CTInboxListener, DisplayUnitList PushPermissionResponseListener, InAppNotificationButtonListener { - var cleverTapDefaultInstance: CleverTapAPI? = null + var cleverTapDefaultInstance: CleverTapAPI? = MyApplication.ctInstance // access singleton override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -50,36 +49,8 @@ class HomeScreenActivity : AppCompatActivity(), CTInboxListener, DisplayUnitList } } - /*val bundle = Bundle().apply { - putString("wzrk_acct_id", "TEST-46W-WWR-R85Z") - putString("nm", "Grab 'em on Myntra's Maxessorize Sale") - putString("nt", "Ye dil ❤️️ maange more accessories?") - putString("pr", "") - putString("wzrk_pivot", "") - putString("wzrk_sound", "true") - putString("wzrk_cid", "BRTesting") - putString("wzrk_clr", "#ed732d") - putString("wzrk_nms", "Grab 'em on Myntra's Maxessorize Sale") - putString("wzrk_pid", (10000_00000..99999_99999).random().toString()) - putString("wzrk_rnv", "true") - putString("wzrk_ttl", "1627731067") - putString("wzrk_push_amp", "false") - putString("wzrk_bc", "") - putString("wzrk_bi", "2") - putString("wzrk_bp", "https://imgur.com/6DavQwg.jpg") - putString("wzrk_dl", "") - putString("wzrk_dt", "FIREBASE") - putString("wzrk_id", "1627639375_20210730") - putString("wzrk_pn", "true") - } - - Thread { - CleverTapAPI.createNotification(applicationContext, bundle) - }.start() - Thread { - CleverTapAPI.createNotification(applicationContext, bundle) - }.start()*/ - initCleverTap() + fakeNotification(send = false) + cleverTapListeners() val isReadPolicy: Boolean val email: String? @@ -127,13 +98,44 @@ class HomeScreenActivity : AppCompatActivity(), CTInboxListener, DisplayUnitList } } - private fun initCleverTap() { + private fun fakeNotification(send: Boolean = false) { + + if (send.not()) { + return + } + + val bundle = Bundle().apply { + putString("wzrk_acct_id", "TEST-46W-WWR-R85Z") + putString("nm", "Grab 'em on Myntra's Maxessorize Sale") + putString("nt", "Ye dil ❤️️ maange more accessories?") + putString("pr", "") + putString("wzrk_pivot", "") + putString("wzrk_sound", "true") + putString("wzrk_cid", "BRTesting") + putString("wzrk_clr", "#ed732d") + putString("wzrk_nms", "Grab 'em on Myntra's Maxessorize Sale") + putString("wzrk_pid", (10000_00000..99999_99999).random().toString()) + putString("wzrk_rnv", "true") + putString("wzrk_ttl", "1627731067") + putString("wzrk_push_amp", "false") + putString("wzrk_bc", "") + putString("wzrk_bi", "2") + putString("wzrk_bp", "https://imgur.com/6DavQwg.jpg") + putString("wzrk_dl", "") + putString("wzrk_dt", "FIREBASE") + putString("wzrk_id", "1627639375_20210730") + putString("wzrk_pn", "true") + } - //Set Debug level for CleverTap - CleverTapAPI.setDebugLevel(VERBOSE) + Thread { + CleverTapAPI.createNotification(applicationContext, bundle) + }.start() + Thread { + CleverTapAPI.createNotification(applicationContext, bundle) + }.start() + } - //Create CleverTap's default instance - cleverTapDefaultInstance = CleverTapAPI.getDefaultInstance(this) + private fun cleverTapListeners() { cleverTapDefaultInstance?.apply { syncListener = this@HomeScreenActivity diff --git a/sample/src/main/java/com/clevertap/demo/MyApplication.kt b/sample/src/main/java/com/clevertap/demo/MyApplication.kt index 92ada1693..5e1dfbe07 100644 --- a/sample/src/main/java/com/clevertap/demo/MyApplication.kt +++ b/sample/src/main/java/com/clevertap/demo/MyApplication.kt @@ -36,17 +36,43 @@ class MyApplication : MultiDexApplication(), CTPushNotificationListener, Activit companion object { private const val TAG = "MyApplication" + + var ctInstance: CleverTapAPI? = null } override fun onCreate() { ANRWatchDog().start() setupStrictMode() - cleverTapPreAppCreated() + + val preOnCreateTime = measureTimeMillis { + cleverTapPreAppCreated() // setup pre 'super.onCreate()' + } + Log.i(TAG, "clevertap setup pre application onCreate took = $preOnCreateTime milliseconds") + super.onCreate() - ctPostAppCreated() + + val postOnCreateTime = measureTimeMillis { + cleverTapPostAppCreated() // setup post 'super.onCreate()' + } + Log.i(TAG, "clevertap instance creation took = $postOnCreateTime milliseconds") + } + + private fun cleverTapPreAppCreated() { + CleverTapAPI.setDebugLevel(VERBOSE) + //CleverTapAPI.changeXiaomiCredentials("your xiaomi app id","your xiaomi app key") + //CleverTapAPI.enableXiaomiPushOn(XIAOMI_MIUI_DEVICES) + TemplateRenderer.debugLevel = 3; + CleverTapAPI.setNotificationHandler(PushTemplateNotificationHandler() as NotificationHandler) + + // this is for clevertap to start sending events => app launched => hence done in app create + val measureTimeMillis = measureTimeMillis { ActivityLifecycleCallback.register(this) } + Log.i(TAG, "Time taken to execute ActivityLifecycleCallback.register = $measureTimeMillis milliseconds") + + // this is for the setup for canceling notifications + registerActivityLifecycleCallbacks(this) } - private fun ctPostAppCreated() { + private fun cleverTapPostAppCreated() { ProviderInstaller.installIfNeededAsync(this, object : ProviderInstallListener { override fun onProviderInstalled() {} override fun onProviderInstallFailed(i: Int, intent: Intent?) { @@ -54,8 +80,10 @@ class MyApplication : MultiDexApplication(), CTPushNotificationListener, Activit } }) - val ctInstance: CleverTapAPI = buildCtInstance(useDefaultInstance = true) // to make readable - ctInstance.apply { + ctInstance = buildCtInstance(useDefaultInstance = true) + + // attach necessary/needed listeners + ctInstance?.apply { syncListener = object : SyncListener { override fun profileDataUpdated(updates: JSONObject?) {//no op } @@ -84,11 +112,6 @@ class MyApplication : MultiDexApplication(), CTPushNotificationListener, Activit // FileVarsData.defineFileVars(cleverTapAPI = this) // uncomment to define file vars before app launch } - /*println( - "CleverTapAttribution Identifier from Application class= " + - "${defaultInstance?.cleverTapAttributionIdentifier}" - )*/ - createNotificationChannels() } @@ -147,19 +170,6 @@ class MyApplication : MultiDexApplication(), CTPushNotificationListener, Activit } } - private fun cleverTapPreAppCreated() { - CleverTapAPI.setDebugLevel(VERBOSE) - //CleverTapAPI.changeXiaomiCredentials("your xiaomi app id","your xiaomi app key") - //CleverTapAPI.enableXiaomiPushOn(XIAOMI_MIUI_DEVICES) - TemplateRenderer.debugLevel = 3; - CleverTapAPI.setNotificationHandler(PushTemplateNotificationHandler() as NotificationHandler) - - val measureTimeMillis = measureTimeMillis { ActivityLifecycleCallback.register(this) } - println("Time taken to execute ActivityLifecycleCallback.register = $measureTimeMillis milliseconds") - - registerActivityLifecycleCallbacks(this) - } - private fun setupStrictMode(enable: Boolean = false) { if (enable) { StrictMode.setThreadPolicy( @@ -180,7 +190,7 @@ class MyApplication : MultiDexApplication(), CTPushNotificationListener, Activit } override fun onNotificationClickedPayloadReceived(payload: HashMap?) { - Log.i("MyApplication", "onNotificationClickedPayloadReceived = $payload") + Log.i(TAG, "onNotificationClickedPayloadReceived = $payload") } override fun attachBaseContext(base: Context?) { @@ -224,13 +234,13 @@ class MyApplication : MultiDexApplication(), CTPushNotificationListener, Activit } override fun onInboxButtonClick(payload: HashMap?) { - Log.i("MyApplication", "InboxButtonClick with payload: $payload") + Log.i(TAG, "InboxButtonClick with payload: $payload") //dismissAppInbox() } override fun onInboxItemClicked(message: CTInboxMessage?, contentPageIndex: Int, buttonIndex: Int) { Log.i( - "MyApplication", + TAG, "InboxItemClicked at $contentPageIndex page-index with button-index: $buttonIndex" ) @@ -247,7 +257,7 @@ class MyApplication : MultiDexApplication(), CTPushNotificationListener, Activit "copy" -> { //this type copies the associated text to the clipboard val copiedText = buttonObject.optJSONObject("copyText")?.optString("text") - Log.i("MyApplication", "copied text to Clipboard: $copiedText") + Log.i(TAG, "copied text to Clipboard: $copiedText") //dismissAppInbox() } "url" -> { @@ -255,19 +265,19 @@ class MyApplication : MultiDexApplication(), CTPushNotificationListener, Activit val firedDeepLinkUrl = buttonObject.optJSONObject("url")?.optJSONObject("android") ?.optString("text") - Log.i("MyApplication", "fired deeplink url: $firedDeepLinkUrl") + Log.i(TAG, "fired deeplink url: $firedDeepLinkUrl") //dismissAppInbox() } "kv" -> { //this type contains the custom key-value pairs val kvPair = buttonObject.optJSONObject("kv") - Log.i("MyApplication", "custom key-value pair: $kvPair") + Log.i(TAG, "custom key-value pair: $kvPair") //dismissAppInbox() } "rfp" -> { //this type triggers the hard prompt of the notification permission val rfpData = buttonObject.optString("text") - Log.i("MyApplication", "notification permission data: $rfpData") + Log.i(TAG, "notification permission data: $rfpData") } else -> { //do nothing here @@ -276,7 +286,7 @@ class MyApplication : MultiDexApplication(), CTPushNotificationListener, Activit } } else { //Item's body is clicked - Log.i("MyApplication", "type/template of App Inbox item: ${message?.type}") + Log.i(TAG, "type/template of App Inbox item: ${message?.type}") //dismissAppInbox() } } diff --git a/sample/src/main/java/com/clevertap/demo/WebViewActivity.kt b/sample/src/main/java/com/clevertap/demo/WebViewActivity.kt index 9bedba538..309037b61 100644 --- a/sample/src/main/java/com/clevertap/demo/WebViewActivity.kt +++ b/sample/src/main/java/com/clevertap/demo/WebViewActivity.kt @@ -25,7 +25,7 @@ class WebViewActivity : AppCompatActivity() { settings.allowContentAccess = false settings.allowFileAccess = false settings.allowFileAccessFromFileURLs = false - addJavascriptInterface(CTWebInterface(CleverTapAPI.getDefaultInstance(this@WebViewActivity)), "CleverTap") + addJavascriptInterface(CTWebInterface(MyApplication.ctInstance), "CleverTap") } } diff --git a/sample/src/main/java/com/clevertap/demo/ui/main/HomeScreenFragment.kt b/sample/src/main/java/com/clevertap/demo/ui/main/HomeScreenFragment.kt index 1cf1e76ee..aeb05e239 100644 --- a/sample/src/main/java/com/clevertap/demo/ui/main/HomeScreenFragment.kt +++ b/sample/src/main/java/com/clevertap/demo/ui/main/HomeScreenFragment.kt @@ -27,6 +27,7 @@ import com.clevertap.android.geofence.interfaces.CTGeofenceEventsListener import com.clevertap.android.sdk.CleverTapAPI import com.clevertap.demo.BuildConfig import com.clevertap.demo.HomeScreenActivity +import com.clevertap.demo.MyApplication import com.clevertap.demo.R import com.clevertap.demo.ViewModelFactory import com.clevertap.demo.WebViewActivity @@ -45,7 +46,7 @@ data class HomeScreenFragmentBinding( class HomeScreenFragment : Fragment() { private val viewModel by viewModels { - ViewModelFactory((activity as? HomeScreenActivity)?.cleverTapDefaultInstance) + ViewModelFactory(MyApplication.ctInstance) } companion object { From 01edb2cf6ad4ede8251ea32a57d6227352fa1552 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Thu, 14 Nov 2024 17:11:13 +0530 Subject: [PATCH 067/120] feat(multi_triggers) : replace HashMap with LinkedHashMap to maintain insertion order of user event history MC-2083 --- .../main/java/com/clevertap/android/sdk/CleverTapAPI.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 ef81b789d..7e55b0e6f 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 @@ -84,6 +84,7 @@ 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; @@ -1846,7 +1847,7 @@ public Map getHistory() { } /** - * Retrieves history of all event logs associated with the current user/{@link CleverTapAPI#getCleverTapID(OnInitCleverTapIDListener) CleverTap ID}. + * 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: @@ -1861,7 +1862,7 @@ public Map getHistory() { @WorkerThread public Map getUserEventLogHistory() { List logs = coreState.getLocalDataStore().readUserEventLogs(); - Map history = new HashMap<>(); + Map history = new LinkedHashMap<>(); for (UserEventLog log : logs) { history.put(log.getEventName(), log); } From 12aa74cfe88d8c5977a2d8b159a5f18b337ab797 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Thu, 14 Nov 2024 17:18:46 +0530 Subject: [PATCH 068/120] test(multi_triggers) : update user event history test to check order MC-2344 --- .../java/com/clevertap/android/sdk/CleverTapAPITest.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 d1f3ca394..d2a7a044c 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 @@ -710,8 +710,8 @@ class CleverTapAPITest : BaseTestCase() { fun `test getUserEventLogHistory`() { // Arrange val logs = listOf( - UserEventLog("event1", 1000L, 1000L, 1, "dId"), - UserEventLog("event2", 2000L, 2000L, 2, "dId") + UserEventLog("z", 1000L, 1000L, 1, "dId"), + UserEventLog("a", 2000L, 2000L, 2, "dId") ) `when`(corestate.localDataStore.readUserEventLogs()).thenReturn(logs) @@ -728,8 +728,8 @@ class CleverTapAPITest : BaseTestCase() { // Assert assertEquals(2, historyActual.size) - assertEquals(logs[0], historyActual["event1"]) - assertEquals(logs[1], historyActual["event2"]) + assertEquals(logs[0], historyActual.values.elementAt(0)) + assertEquals(logs[1], historyActual.values.elementAt(1)) verify(corestate.localDataStore).readUserEventLogs() } } From 4935e17b66db25cb42282cb8b6477ac953d4b7ea Mon Sep 17 00:00:00 2001 From: CTLalit Date: Thu, 14 Nov 2024 21:32:26 +0530 Subject: [PATCH 069/120] feat(SDK-4171): Analytics manager testable - removes context passed in Clevertap instance config - breaks down code to separate out context - uses context held in the api wrapper and not holding another ref in class - breaking down incorrect static usages - private constructor to init things in test class - provides control over clevertap instance config and also manifest info - tests can now cover manifest info testing - fixes singleton access for manifestinfo (sets stage) - moves manifest constants to correct class -> breaks down large constants class - creates clevertap fixtures which will replace base test case class which provides unpredictable mocks and incomplete tests. --- .../clevertap/android/sdk/CleverTapAPI.java | 4 +- .../android/sdk/CleverTapFactory.java | 2 +- .../android/sdk/CleverTapInstanceConfig.java | 116 ++++---- .../com/clevertap/android/sdk/Constants.java | 20 -- .../com/clevertap/android/sdk/CoreState.java | 12 +- .../clevertap/android/sdk/ManifestInfo.java | 263 +++++++++++------- .../sdk/ActivityLifeCycleManagerTest.kt | 2 +- .../android/sdk/AnalyticsManagerTest.kt | 14 +- .../clevertap/android/sdk/CleverTapAPITest.kt | 2 +- .../android/sdk/EventQueueManagerTest.kt | 2 +- .../clevertap/android/sdk/MockCoreState.kt | 4 +- .../android/sdk/fixtures/CleverTapFixtures.kt | 43 +++ .../android/sdk/network/NetworkManagerTest.kt | 2 +- .../pushtemplates/PushTemplateReceiver.java | 3 +- 14 files changed, 296 insertions(+), 193 deletions(-) create mode 100644 clevertap-core/src/test/java/com/clevertap/android/sdk/fixtures/CleverTapFixtures.kt 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..b043f25ab 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 @@ -2219,7 +2219,7 @@ public CTProductConfigController productConfig() { getConfig().getLogger().debug(getAccountId(), "Product config is not supported with analytics only configuration"); } - return coreState.getCtProductConfigController(); + return coreState.getCtProductConfigController(context); } /** @@ -3111,6 +3111,8 @@ private static CleverTapInstanceConfig getDefaultConfig(Context context) { 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..ba995a271 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 @@ -44,7 +44,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); 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..9b72551ed 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,88 @@ 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); + } + + // todo lp visibility + @VisibleForTesting + public 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 +149,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 +166,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 +533,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..159eb04ae 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"; 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..53e243ae0 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,8 +110,7 @@ public void setParser(final Parser parser) { this.parser = parser; } - CoreState(final Context context) { - this.context = context; + CoreState() { } public ActivityLifeCycleManager getActivityLifeCycleManager() { @@ -189,8 +187,8 @@ void setCoreMetaData(final CoreMetaData coreMetaData) { *

*/ @Deprecated - public CTProductConfigController getCtProductConfigController() { - initProductConfig(); + public CTProductConfigController getCtProductConfigController(Context context) { + initProductConfig(context); return getControllerManager().getCTProductConfigController(); } @@ -336,7 +334,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/ManifestInfo.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/ManifestInfo.java index b9cca9a55..a74f03dbd 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,125 @@ 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); + } + + // todo lp visibility -> private + @VisibleForTesting + public 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 +317,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 +324,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/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..96b69ec3c 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,7 +1,9 @@ package com.clevertap.android.sdk +import android.content.Context import com.clevertap.android.sdk.events.BaseEventQueueManager import com.clevertap.android.sdk.events.EventQueueManager +import com.clevertap.android.sdk.fixtures.CleverTapFixtures import com.clevertap.android.sdk.response.InAppResponse import com.clevertap.android.sdk.task.CTExecutorFactory import com.clevertap.android.sdk.task.MockCTExecutors @@ -9,7 +11,6 @@ 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 org.json.JSONArray import org.json.JSONObject import org.junit.* @@ -26,24 +27,25 @@ 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 validator: Validator private lateinit var validationResultStack: ValidationResultStack private lateinit var baseEventQueueManager: BaseEventQueueManager + private lateinit var cleverTapInstanceConfig: CleverTapInstanceConfig @Before - override fun setUp() { - super.setUp() + fun setUp() { + cleverTapInstanceConfig = CleverTapFixtures.provideCleverTapInstanceConfig() 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) + coreState = MockCoreState(cleverTapInstanceConfig) analyticsManagerSUT = AnalyticsManager( - application, + mock(Context::class.java), cleverTapInstanceConfig, baseEventQueueManager, validator, 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..a3e186d1c 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 @@ -25,7 +25,7 @@ class CleverTapAPITest : BaseTestCase() { @Throws(Exception::class) override fun setUp() { super.setUp() - corestate = MockCoreState(application, cleverTapInstanceConfig) + corestate = MockCoreState(cleverTapInstanceConfig) } /* @Test 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..183a7e3e2 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( 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..20f1d0255 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 @@ -14,7 +13,8 @@ import com.clevertap.android.sdk.variables.Parser import com.clevertap.android.sdk.variables.VarCache 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 diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/fixtures/CleverTapFixtures.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/fixtures/CleverTapFixtures.kt new file mode 100644 index 000000000..97199efb0 --- /dev/null +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/fixtures/CleverTapFixtures.kt @@ -0,0 +1,43 @@ +package com.clevertap.android.sdk.fixtures + +import com.clevertap.android.sdk.CleverTapInstanceConfig +import com.clevertap.android.sdk.ManifestInfo +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/network/NetworkManagerTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/network/NetworkManagerTest.kt index d546dda9b..631d4acc7 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 @@ -156,7 +156,7 @@ 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) 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(); } From a786cf12a2adfcf92c768f52c896cbbe0187cd21 Mon Sep 17 00:00:00 2001 From: CTLalit Date: Fri, 15 Nov 2024 16:34:51 +0530 Subject: [PATCH 070/120] feat(SDK-4171): Breaks down long method into smaller ones --- .../android/sdk/AnalyticsManager.java | 108 ++++++++++-------- 1 file changed, 58 insertions(+), 50 deletions(-) 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 36d7edefb..2f8ee1f15 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 @@ -474,60 +474,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; } @@ -583,6 +535,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); From bd05894769fd0b7facfaca90ec04ab6290eccff9 Mon Sep 17 00:00:00 2001 From: CTLalit Date: Fri, 15 Nov 2024 19:49:43 +0530 Subject: [PATCH 071/120] test(SDK-4171): adds tests for analytics manager - cases where ct does not process pn added --- .../android/sdk/AnalyticsManager.java | 33 ++++++++++--------- .../android/sdk/AnalyticsManagerTest.kt | 24 ++++++++++++++ 2 files changed, 41 insertions(+), 16 deletions(-) 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 2f8ee1f15..83fc21534 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 @@ -66,16 +66,18 @@ public class AnalyticsManager extends BaseAnalyticsManager { 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) { + AnalyticsManager( + Context context, + CleverTapInstanceConfig config, + BaseEventQueueManager baseEventQueueManager, + Validator validator, + ValidationResultStack validationResultStack, + CoreMetaData coreMetaData, + DeviceInfo deviceInfo, + BaseCallbackManager callbackManager, ControllerManager controllerManager, + final CTLockManager ctLockManager, + InAppResponse inAppResponse + ) { this.context = context; this.config = config; this.baseEventQueueManager = baseEventQueueManager; @@ -658,11 +660,10 @@ 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; } @@ -671,11 +672,11 @@ public void pushNotificationViewedEvent(Bundle extras) { 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 { 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 96b69ec3c..e39d1b20a 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,6 +1,7 @@ package com.clevertap.android.sdk import android.content.Context +import android.os.Bundle import com.clevertap.android.sdk.events.BaseEventQueueManager import com.clevertap.android.sdk.events.EventQueueManager import com.clevertap.android.sdk.fixtures.CleverTapFixtures @@ -59,6 +60,29 @@ class AnalyticsManagerTest { ) } + @Test + fun `clevertap does not process push notification viewed event if it is not from clevertap`() { + val bundle = Bundle().apply { + putString("some", "random") + putString("non clevertap", "bundle") + } + + analyticsManagerSUT.pushNotificationViewedEvent(bundle) + verifyNoInteractions(baseEventQueueManager) + } + + @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) + verifyNoInteractions(baseEventQueueManager) + } + @Test fun test_incrementValue_nullKey_noAction() { analyticsManagerSUT.incrementValue(null, 10) From cde4f26382c4e1d147db3583d528dd34b6ac0167 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Mon, 18 Nov 2024 18:02:20 +0530 Subject: [PATCH 072/120] test(multi_triggers) : add helper functions and refactor tests in CleverTapAPITest MC-2344 --- .../clevertap/android/sdk/CleverTapAPITest.kt | 804 +++++++----------- 1 file changed, 302 insertions(+), 502 deletions(-) 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 d2a7a044c..2a7f4668d 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 @@ -22,6 +22,67 @@ class CleverTapAPITest : BaseTestCase() { private lateinit var corestate: MockCoreState + // Common setup helper functions + private fun setupMockExecutors(block: () -> Unit) { + mockStatic(CTExecutorFactory::class.java).use { + `when`(CTExecutorFactory.executors(any())).thenReturn( + MockCTExecutors(cleverTapInstanceConfig) + ) + block() + } + } + + private fun setupMockFactory(block: () -> Unit) { + mockStatic(CleverTapFactory::class.java).use { + `when`(CleverTapFactory.getCoreState(application, cleverTapInstanceConfig, null)) + .thenReturn(corestate) + block() + } + } + + private fun setupMockFactoryWithAny(block: () -> Unit) { + mockStatic(CleverTapFactory::class.java).use { + `when`( + CleverTapFactory.getCoreState( + ArgumentMatchers.any(), + ArgumentMatchers.any(), + ArgumentMatchers.any() + ) + ) + .thenReturn(corestate) + block() + } + } + + private fun setupBasicTest(block: () -> Unit) { + setupMockExecutors { + setupMockFactory { + block() + } + } + } + + private fun setupBasicTestWithAny(block: () -> Unit) { + setupMockExecutors { + setupMockFactoryWithAny { + 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() { @@ -31,365 +92,229 @@ class CleverTapAPITest : BaseTestCase() { @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.sessionManager).setUserLastVisitTs() - verify(corestate.deviceInfo).setDeviceNetworkInfoReportingFromStorage() - verify(corestate.deviceInfo).setCurrentUserOptOutStateFromStorage() - val actualConfig = - StorageHelper.getString(application, "instance:" + cleverTapInstanceConfig.accountId, "") - assertEquals(cleverTapInstanceConfig.toJSONString(), actualConfig) - } + setupBasicTest { + 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 - ) - ) + setupBasicTest { 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.sessionManager).setUserLastVisitTs() - 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) + setupBasicTest { + 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") - } + setupBasicTest { + // Arrange + val geofenceCallback = object : GeofenceCallback { + override fun handleGeoFences(jsonObject: JSONObject?) { + } + override fun triggerLocation() { } + } - cleverTapAPI.geofenceCallback = geofenceCallback + // Act + initializeCleverTapAPI() + cleverTapAPI.geofenceCallback = geofenceCallback - assertEquals(geofenceCallback, cleverTapAPI.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) - } + setupBasicTest { + // 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 - ) + setupBasicTest { + // 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 - ) + setupBasicTest { + // 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) + setupBasicTestWithAny { + 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 - ) - ) + setupMockExecutors { + // 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 - ) + setupBasicTestWithAny { + 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 ) - 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 - ) + setupBasicTestWithAny { + 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 - ) + setupMockFactoryWithAny { + 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 - ) - } } } @@ -434,53 +359,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) - } + setupMockFactoryWithAny { + 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) - } + setupBasicTestWithAny { + 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) } } @@ -493,23 +386,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).ctInboxController - verifyNoMoreInteractions(controllerManager) - } + setupBasicTest { + `when`(controllerManager.ctInboxController).thenReturn(inboxController) + initializeCleverTapAPI() + cleverTapAPI.deleteInboxMessagesForIDs(messageIDs) + + // Assert + verify(controllerManager).ctInboxController + verifyNoMoreInteractions(controllerManager) } } @@ -522,23 +406,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) - } + setupBasicTest { + `when`(controllerManager.ctInboxController).thenReturn(inboxController) + initializeCleverTapAPI() + cleverTapAPI.deleteInboxMessagesForIDs(messageIDs) + + // Assert + verify(controllerManager, times(2)).ctInboxController + verify(inboxController).deleteInboxMessagesForIDs(messageIDs) } } @@ -551,23 +426,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.markReadInboxMessagesForIDs(messageIDs) - - // Assert - verify(controllerManager).ctInboxController - verifyNoMoreInteractions(controllerManager) - } + setupBasicTest { + `when`(controllerManager.ctInboxController).thenReturn(inboxController) + initializeCleverTapAPI() + cleverTapAPI.markReadInboxMessagesForIDs(messageIDs) + + // Assert + verify(controllerManager).ctInboxController + verifyNoMoreInteractions(controllerManager) } } @@ -580,23 +446,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.markReadInboxMessagesForIDs(messageIDs) - - // Assert - verify(controllerManager,times(2)).ctInboxController - verify(inboxController).markReadInboxMessagesForIDs(messageIDs) - } + setupBasicTest { + `when`(controllerManager.ctInboxController).thenReturn(inboxController) + initializeCleverTapAPI() + cleverTapAPI.markReadInboxMessagesForIDs(messageIDs) + + // Assert + verify(controllerManager, times(2)).ctInboxController + verify(inboxController).markReadInboxMessagesForIDs(messageIDs) } } @@ -607,22 +464,13 @@ class CleverTapAPITest : BaseTestCase() { `when`(corestate.localDataStore.readUserEventLogCount(evt)).thenReturn(1) // 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) - cleverTapAPI = CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig) - val userEventLogCountActual = cleverTapAPI.getUserEventLogCount(evt) + setupBasicTest { + initializeCleverTapAPI() + val userEventLogCountActual = cleverTapAPI.getUserEventLogCount(evt) - // Assert - assertEquals(1, userEventLogCountActual) - verify(corestate.localDataStore).readUserEventLogCount(evt) - } + // Assert + assertEquals(1, userEventLogCountActual) + verify(corestate.localDataStore).readUserEventLogCount(evt) } } @@ -634,22 +482,13 @@ class CleverTapAPITest : BaseTestCase() { `when`(corestate.localDataStore.readUserEventLog(evt)).thenReturn(log) // 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) - cleverTapAPI = CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig) - val userEventLogActual = cleverTapAPI.getUserEventLog(evt) + setupBasicTest { + initializeCleverTapAPI() + val userEventLogActual = cleverTapAPI.getUserEventLog(evt) - // Assert - assertSame(log, userEventLogActual) - verify(corestate.localDataStore).readUserEventLog(evt) - } + // Assert + assertSame(log, userEventLogActual) + verify(corestate.localDataStore).readUserEventLog(evt) } } @@ -660,22 +499,13 @@ class CleverTapAPITest : BaseTestCase() { `when`(corestate.localDataStore.readUserEventLogFirstTs(evt)).thenReturn(1000L) // 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) - cleverTapAPI = CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig) - val firstTsActual = cleverTapAPI.getUserEventLogFirstTs(evt) + setupBasicTest { + initializeCleverTapAPI() + val firstTsActual = cleverTapAPI.getUserEventLogFirstTs(evt) - // Assert - assertEquals(1000L, firstTsActual) - verify(corestate.localDataStore).readUserEventLogFirstTs(evt) - } + // Assert + assertEquals(1000L, firstTsActual) + verify(corestate.localDataStore).readUserEventLogFirstTs(evt) } } @@ -686,22 +516,13 @@ class CleverTapAPITest : BaseTestCase() { `when`(corestate.localDataStore.readUserEventLogLastTs(evt)).thenReturn(1000L) // 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) - cleverTapAPI = CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig) - val lastTsActual = cleverTapAPI.getUserEventLogLastTs(evt) + setupBasicTest { + initializeCleverTapAPI() + val lastTsActual = cleverTapAPI.getUserEventLogLastTs(evt) - // Assert - assertEquals(1000L, lastTsActual) - verify(corestate.localDataStore).readUserEventLogLastTs(evt) - } + // Assert + assertEquals(1000L, lastTsActual) + verify(corestate.localDataStore).readUserEventLogLastTs(evt) } } @@ -716,22 +537,15 @@ class CleverTapAPITest : BaseTestCase() { `when`(corestate.localDataStore.readUserEventLogs()).thenReturn(logs) // 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) - cleverTapAPI = CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig) - 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() - } + setupBasicTest { + 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() } } @@ -741,20 +555,13 @@ class CleverTapAPITest : BaseTestCase() { `when`(corestate.sessionManager.userLastVisitTs).thenReturn(1000L) // 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) - cleverTapAPI = CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig) - val lastVisitTsActual = cleverTapAPI.userLastVisitTs + setupBasicTest { + initializeCleverTapAPI() + val lastVisitTsActual = cleverTapAPI.userLastVisitTs - // Assert - assertEquals(1000L, lastVisitTsActual) - verify(corestate.sessionManager).userLastVisitTs - } + // Assert + assertEquals(1000L, lastVisitTsActual) + verify(corestate.sessionManager).userLastVisitTs } } @@ -765,20 +572,13 @@ class CleverTapAPITest : BaseTestCase() { .thenReturn(5) // 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) - cleverTapAPI = CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig) - val appLaunchCountActual = cleverTapAPI.userAppLaunchCount + setupBasicTest { + initializeCleverTapAPI() + val appLaunchCountActual = cleverTapAPI.userAppLaunchCount - // Assert - assertEquals(5, appLaunchCountActual) - verify(corestate.localDataStore).readUserEventLogCount(Constants.APP_LAUNCHED_EVENT) - } + // Assert + assertEquals(5, appLaunchCountActual) + verify(corestate.localDataStore).readUserEventLogCount(Constants.APP_LAUNCHED_EVENT) } } /* @Test From de3cc2d6bd83e3856f1ad0ea0b827ab92f7708a9 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Mon, 18 Nov 2024 19:10:41 +0530 Subject: [PATCH 073/120] feat(multi_triggers) : move firstTimeOnly const to respective class MC-2087 --- .../src/main/java/com/clevertap/android/sdk/Constants.java | 1 - .../android/sdk/inapp/evaluation/TriggerAdapter.kt | 6 +++++- 2 files changed, 5 insertions(+), 2 deletions(-) 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 cd26567ac..39478b9ce 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 @@ -229,7 +229,6 @@ public interface Constants { String KEY_ITEM_PROPERTIES = "itemProperties"; String KEY_GEO_RADIUS_PROPERTIES = "geoRadius"; String KEY_PROFILE_ATTR_NAME = "profileAttrName"; - String KEY_FIRST_TIME_ONLY = "firstTimeOnly"; String KEY_PROPERTY_VALUE = "propertyValue"; String KEY_COLOR = "color"; String KEY_MESSAGE = "message"; 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 4a986ffa2..111dfcda7 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 @@ -101,7 +101,7 @@ class TriggerAdapter(triggerJSON: JSONObject) { */ val profileAttrName: String? = triggerJSON.optString(Constants.KEY_PROFILE_ATTR_NAME, null) - val firstTimeOnly: Boolean = triggerJSON.optBoolean(Constants.KEY_FIRST_TIME_ONLY, false) + val firstTimeOnly: Boolean = triggerJSON.optBoolean(KEY_FIRST_TIME_ONLY, false) /** * Get the count of event property trigger conditions. @@ -121,6 +121,10 @@ class TriggerAdapter(triggerJSON: JSONObject) { val geoRadiusCount: Int get() = geoRadiusArray?.length() ?: 0 + companion object { + const val KEY_FIRST_TIME_ONLY = "firstTimeOnly" + } + /** * Internal function to create a TriggerCondition from a JSON property object. * From b710d89aa10233e7116eaedd62fa15f885566ee5 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Mon, 18 Nov 2024 19:13:05 +0530 Subject: [PATCH 074/120] test(multi_triggers) : fix broken tests by KEY_FIRST_TIME_ONLY refactoring MC-2344 --- .../android/sdk/inapp/evaluation/TriggerAdapterTest.kt | 5 +++-- .../android/sdk/inapp/evaluation/TriggersMatcherTest.kt | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) 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 fb4b2e8f3..98d4fcbcd 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,7 @@ package com.clevertap.android.sdk.inapp.evaluation import com.clevertap.android.sdk.Constants +import com.clevertap.android.sdk.inapp.evaluation.TriggerAdapter.Companion.KEY_FIRST_TIME_ONLY import org.json.JSONArray import org.json.JSONObject import org.junit.* @@ -585,7 +586,7 @@ class TriggerAdapterTest { fun `test firstTimeOnly with firstTimeOnly value as true`() { // Arrange val triggerJSON = JSONObject() - triggerJSON.put(Constants.KEY_FIRST_TIME_ONLY, true) + triggerJSON.put(KEY_FIRST_TIME_ONLY, true) val triggerAdapter = TriggerAdapter(triggerJSON) // Act @@ -599,7 +600,7 @@ class TriggerAdapterTest { fun `test firstTimeOnly with firstTimeOnly value as false`(){ // Arrange val triggerJSON = JSONObject() - triggerJSON.put(Constants.KEY_FIRST_TIME_ONLY, false) + triggerJSON.put(KEY_FIRST_TIME_ONLY, false) val triggerAdapter = TriggerAdapter(triggerJSON) // Act 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 a0d365a59..0b55e4a83 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 @@ -1591,7 +1591,7 @@ class TriggersMatcherTest : BaseTestCase() { ): TriggerAdapter { val triggerJSON = JSONObject().apply { put("eventName", eventName) - put("firstTimeOnly", firstTimeOnly) + put(TriggerAdapter.KEY_FIRST_TIME_ONLY, firstTimeOnly) if(profileAttrName != null) put("profileAttrName",profileAttrName) if (propertyConditions.isNotEmpty()) { From 8bd0c51046ef33f935701916218d7e6c3c4f35bb Mon Sep 17 00:00:00 2001 From: CTLalit Date: Tue, 19 Nov 2024 14:35:25 +0530 Subject: [PATCH 075/120] test(SDK-4171): removes new lines --- .../android/sdk/AnalyticsManager.java | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) 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 83fc21534..c60130a5d 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 @@ -37,33 +37,20 @@ 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 Object notificationMapLock = new Object(); + private final HashMap notificationIdTagMap = new HashMap<>(); private final HashMap notificationViewedIdTagMap = new HashMap<>(); AnalyticsManager( @@ -668,7 +655,8 @@ public void pushNotificationViewedEvent(Bundle extras) { } // Check for dupe notification views; if same notficationdId within specified time interval (2 secs) don't process - boolean isDuplicate = checkDuplicateNotificationIds(extras, notificationViewedIdTagMap, + boolean isDuplicate = checkDuplicateNotificationIds(extras, + notificationViewedIdTagMap, Constants.NOTIFICATION_VIEWED_ID_TAG_INTERVAL); if (isDuplicate) { config.getLogger().debug(config.getAccountId(), From e361ab8a9bdc27ea813afcb2d394a8371bd02645 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya <61137760+piyush-kukadiya@users.noreply.github.com> Date: Tue, 19 Nov 2024 16:57:27 +0530 Subject: [PATCH 076/120] chore(multi_triggers) : Resolves PR comments MC-2252 (#691) * chore(multi_triggers) : replace query measure time to nano MC-2252 * chore(multi_triggers) : move all constants of TriggerAdapter class in companion object MC-2252 * chore(multi_triggers) : rename package userEventLogs to usereventlogs MC-2252 * chore(multi_triggers) : remove duplicate logic for userEventLogs table creation on DB upgrade MC-2252 * chore(multi_triggers) : mark companion object internal to module in TriggerAdapter MC-2252 * chore(multi_triggers) : extract eventExists string into variable MC-2252 * chore(multi_triggers) : remove unused code from NetworkManagerTest MC-2252 * chore(multi_triggers) : rename helper methods and format code MC-2252 --- .../clevertap/android/sdk/CleverTapAPI.java | 2 +- .../com/clevertap/android/sdk/Constants.java | 8 - .../clevertap/android/sdk/LocalDataStore.java | 8 +- .../clevertap/android/sdk/db/CtDatabase.kt | 9 +- .../com/clevertap/android/sdk/db/DBAdapter.kt | 4 +- .../sdk/inapp/evaluation/TriggerAdapter.kt | 26 ++-- .../UserEventLog.kt | 2 +- .../UserEventLogDAO.kt | 2 +- .../UserEventLogDAOImpl.kt | 13 +- .../clevertap/android/sdk/CleverTapAPITest.kt | 141 +++++++----------- .../android/sdk/LocalDataStoreTest.kt | 6 +- .../inapp/evaluation/EvaluationManagerTest.kt | 9 +- .../inapp/evaluation/TriggerAdapterTest.kt | 124 ++++++++------- .../inapp/evaluation/TriggersMatcherTest.kt | 11 +- .../android/sdk/network/NetworkManagerTest.kt | 5 - .../UserEventLogDAOImplTest.kt | 2 +- 16 files changed, 176 insertions(+), 196 deletions(-) rename clevertap-core/src/main/java/com/clevertap/android/sdk/{userEventLogs => usereventlogs}/UserEventLog.kt (89%) rename clevertap-core/src/main/java/com/clevertap/android/sdk/{userEventLogs => usereventlogs}/UserEventLogDAO.kt (97%) rename clevertap-core/src/main/java/com/clevertap/android/sdk/{userEventLogs => usereventlogs}/UserEventLogDAOImpl.kt (97%) rename clevertap-core/src/test/java/com/clevertap/android/sdk/{userEventLogs => usereventlogs}/UserEventLogDAOImplTest.kt (99%) 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 7e55b0e6f..c487f1dbe 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,7 +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.usereventlogs.UserEventLog; import com.clevertap.android.sdk.utils.UriHelper; import com.clevertap.android.sdk.validation.ManifestValidator; import com.clevertap.android.sdk.validation.ValidationResult; 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..194e65580 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 @@ -225,12 +225,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"; @@ -272,8 +266,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/LocalDataStore.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/LocalDataStore.java index 583ae29a9..ab3e1f72b 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 @@ -15,7 +15,7 @@ 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 com.clevertap.android.sdk.usereventlogs.UserEventLog; //import org.apache.commons.lang3.RandomStringUtils; import org.json.JSONArray; @@ -705,15 +705,15 @@ public void updateProfileFields(Map fields) { events.add(s); } k++;*/ - long start = System.currentTimeMillis(); + long start = System.nanoTime(); persistUserEventLogsInBulk(fields.keySet()); // persistUserEventLogsInBulk(events); /*for (String key : events) { persistUserEventLog(key); }*/ - long end = System.currentTimeMillis(); - config.getLogger().verbose(config.getAccountId(),"UserEventLog: persistUserEventLog execution time = "+(end-start)/1000+" seconds"); + 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/db/CtDatabase.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/db/CtDatabase.kt index 7a7ff7fe0..f38402286 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 @@ -73,7 +73,6 @@ class DatabaseHelper internal constructor(val context: Context, val config: Clev executeStatement(db, INBOX_MESSAGES_COMP_ID_USERID_INDEX) executeStatement(db, NOTIFICATION_VIEWED_INDEX) migrateUserProfilesTable(db) - executeStatement(db, CREATE_USER_EVENT_LOGS_TABLE)// when app updates 1 to 5 } 2 -> { @@ -82,18 +81,16 @@ class DatabaseHelper internal constructor(val context: Context, val config: Clev executeStatement(db, CREATE_NOTIFICATION_VIEWED_TABLE) executeStatement(db, NOTIFICATION_VIEWED_INDEX) migrateUserProfilesTable(db) - executeStatement(db, CREATE_USER_EVENT_LOGS_TABLE)// when app updates 2 to 5 } 3 -> { // For DB Version 4, just migrate userProfiles table migrateUserProfilesTable(db) - executeStatement(db, CREATE_USER_EVENT_LOGS_TABLE)// when app updates 3 to 5 } + } - 4 -> { - executeStatement(db, CREATE_USER_EVENT_LOGS_TABLE)// when app updates 4 to 5 - } + if (oldVersion < 5) { + executeStatement(db, CREATE_USER_EVENT_LOGS_TABLE)// when app updates [1,2,3,4] to 5 } } 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 3dee345f8..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 @@ -14,8 +14,8 @@ 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 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 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 111dfcda7..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,29 @@ 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) @@ -121,8 +121,16 @@ class TriggerAdapter(triggerJSON: JSONObject) { val geoRadiusCount: Int get() = geoRadiusArray?.length() ?: 0 - companion object { + 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" } /** @@ -133,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/userEventLogs/UserEventLog.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/usereventlogs/UserEventLog.kt similarity index 89% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/userEventLogs/UserEventLog.kt rename to clevertap-core/src/main/java/com/clevertap/android/sdk/usereventlogs/UserEventLog.kt index 87392b6df..3dc51d99d 100644 --- 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 @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk.userEventLogs +package com.clevertap.android.sdk.usereventlogs data class UserEventLog( val eventName: String, // The name of the event 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 similarity index 97% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/userEventLogs/UserEventLogDAO.kt rename to clevertap-core/src/main/java/com/clevertap/android/sdk/usereventlogs/UserEventLogDAO.kt index 19d7a9b78..cb4129e95 100644 --- 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 @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk.userEventLogs +package com.clevertap.android.sdk.usereventlogs import androidx.annotation.WorkerThread 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 similarity index 97% rename from clevertap-core/src/main/java/com/clevertap/android/sdk/userEventLogs/UserEventLogDAOImpl.kt rename to clevertap-core/src/main/java/com/clevertap/android/sdk/usereventlogs/UserEventLogDAOImpl.kt index d169394b2..e12f309ee 100644 --- 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 @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk.userEventLogs +package com.clevertap.android.sdk.usereventlogs import android.content.ContentValues import android.database.Cursor @@ -173,19 +173,20 @@ internal class UserEventLogDAOImpl( val tName = table.tableName val selection = "${Column.DEVICE_ID} = ? AND ${Column.EVENT_NAME} = ?" val selectionArgs = arrayOf(deviceID, eventName) + val resultColumn = "eventExists" val query = """ SELECT EXISTS( SELECT 1 FROM $tName WHERE $selection - ) AS eventExists; + ) AS $resultColumn; """.trimIndent() return try { db.readableDatabase.rawQuery(query, selectionArgs)?.use { cursor -> if (cursor.moveToFirst()) { - cursor.getInt(cursor.getColumnIndexOrThrow("eventExists")) == 1 + cursor.getInt(cursor.getColumnIndexOrThrow(resultColumn)) == 1 } else { false } @@ -201,17 +202,19 @@ internal class UserEventLogDAOImpl( val tName = table.tableName val selection = "${Column.DEVICE_ID} = ? AND ${Column.EVENT_NAME} = ? AND ${Column.COUNT} = ?" val selectionArgs = arrayOf(deviceID, eventName, count.toString()) + val resultColumn = "eventExists" + val query = """ SELECT EXISTS( SELECT 1 FROM $tName WHERE $selection - ) AS eventExists; + ) AS $resultColumn; """.trimIndent() return try { db.readableDatabase.rawQuery(query, selectionArgs)?.use { cursor -> if (cursor.moveToFirst()) { - cursor.getInt(cursor.getColumnIndexOrThrow("eventExists")) == 1 + cursor.getInt(cursor.getColumnIndexOrThrow(resultColumn)) == 1 } else { false } 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 2a7f4668d..e85aac5ff 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,7 +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.UserEventLog +import com.clevertap.android.sdk.usereventlogs.UserEventLog import com.clevertap.android.shared.test.BaseTestCase import com.clevertap.android.shared.test.Constant import org.json.JSONObject @@ -23,7 +23,7 @@ class CleverTapAPITest : BaseTestCase() { private lateinit var corestate: MockCoreState // Common setup helper functions - private fun setupMockExecutors(block: () -> Unit) { + private fun executeMockExecutors(block: () -> Unit) { mockStatic(CTExecutorFactory::class.java).use { `when`(CTExecutorFactory.executors(any())).thenReturn( MockCTExecutors(cleverTapInstanceConfig) @@ -32,7 +32,7 @@ class CleverTapAPITest : BaseTestCase() { } } - private fun setupMockFactory(block: () -> Unit) { + private fun executeMockFactory(block: () -> Unit) { mockStatic(CleverTapFactory::class.java).use { `when`(CleverTapFactory.getCoreState(application, cleverTapInstanceConfig, null)) .thenReturn(corestate) @@ -40,7 +40,7 @@ class CleverTapAPITest : BaseTestCase() { } } - private fun setupMockFactoryWithAny(block: () -> Unit) { + private fun executeMockFactoryWithAny(block: () -> Unit) { mockStatic(CleverTapFactory::class.java).use { `when`( CleverTapFactory.getCoreState( @@ -54,17 +54,17 @@ class CleverTapAPITest : BaseTestCase() { } } - private fun setupBasicTest(block: () -> Unit) { - setupMockExecutors { - setupMockFactory { + private fun executeBasicTest(block: () -> Unit) { + executeMockExecutors { + executeMockFactory { block() } } } - private fun setupBasicTestWithAny(block: () -> Unit) { - setupMockExecutors { - setupMockFactoryWithAny { + private fun executeBasicTestWithAny(block: () -> Unit) { + executeMockExecutors { + executeMockFactoryWithAny { block() } } @@ -79,7 +79,11 @@ class CleverTapAPITest : BaseTestCase() { verify(corestate.sessionManager).setUserLastVisitTs() verify(corestate.deviceInfo).setDeviceNetworkInfoReportingFromStorage() verify(corestate.deviceInfo).setCurrentUserOptOutStateFromStorage() - val actualConfig = StorageHelper.getString(application, "instance:" + cleverTapInstanceConfig.accountId, "") + val actualConfig = StorageHelper.getString( + application, + "instance:" + cleverTapInstanceConfig.accountId, + "" + ) assertEquals(cleverTapInstanceConfig.toJSONString(), actualConfig) } @@ -92,7 +96,7 @@ class CleverTapAPITest : BaseTestCase() { @Test fun testCleverTapAPI_constructor_when_InitialAppEnteredForegroundTime_greater_than_5_secs() { - setupBasicTest { + executeBasicTest { mockStatic(Utils::class.java).use { // Arrange `when`(Utils.getNow()).thenReturn(Int.MAX_VALUE) @@ -102,7 +106,10 @@ class CleverTapAPITest : BaseTestCase() { initializeCleverTapAPI() // Assert - assertTrue("isCreatedPostAppLaunch must be true", cleverTapInstanceConfig.isCreatedPostAppLaunch) + assertTrue( + "isCreatedPostAppLaunch must be true", + cleverTapInstanceConfig.isCreatedPostAppLaunch + ) verifyCommonConstructorBehavior() } } @@ -110,7 +117,7 @@ class CleverTapAPITest : BaseTestCase() { @Test fun testCleverTapAPI_constructor_when_InitialAppEnteredForegroundTime_less_than_5_secs() { - setupBasicTest { + executeBasicTest { mockStatic(Utils::class.java).use { // Arrange `when`(Utils.getNow()).thenReturn(0) @@ -120,7 +127,10 @@ class CleverTapAPITest : BaseTestCase() { initializeCleverTapAPI() // Assert - assertFalse("isCreatedPostAppLaunch must be false", cleverTapInstanceConfig.isCreatedPostAppLaunch) + assertFalse( + "isCreatedPostAppLaunch must be false", + cleverTapInstanceConfig.isCreatedPostAppLaunch + ) verifyCommonConstructorBehavior() } } @@ -128,7 +138,7 @@ class CleverTapAPITest : BaseTestCase() { @Test fun test_setLocationForGeofences() { - setupBasicTest { + executeBasicTest { val location = Location("").apply { latitude = 17.4444 longitude = 4.444 @@ -145,11 +155,12 @@ class CleverTapAPITest : BaseTestCase() { @Test fun test_setGeofenceCallback() { - setupBasicTest { + executeBasicTest { // Arrange val geofenceCallback = object : GeofenceCallback { override fun handleGeoFences(jsonObject: JSONObject?) { } + override fun triggerLocation() { } } @@ -165,7 +176,7 @@ class CleverTapAPITest : BaseTestCase() { @Test fun test_pushGeoFenceError() { - setupBasicTest { + executeBasicTest { // Arrange val expectedErrorCode = 999 val expectedErrorMessage = "Fire in the hall" @@ -183,7 +194,7 @@ class CleverTapAPITest : BaseTestCase() { @Test fun test_pushGeoFenceExitedEvent() { - setupBasicTest { + executeBasicTest { // Arrange val expectedJson = JSONObject("{\"key\":\"value\"}") val argumentCaptor = ArgumentCaptor.forClass(JSONObject::class.java) @@ -203,7 +214,7 @@ class CleverTapAPITest : BaseTestCase() { @Test fun test_pushGeoFenceEnteredEvent() { - setupBasicTest { + executeBasicTest { // Arrange val expectedJson = JSONObject("{\"key\":\"value\"}") val argumentCaptor = ArgumentCaptor.forClass(JSONObject::class.java) @@ -223,7 +234,7 @@ class CleverTapAPITest : BaseTestCase() { @Test fun test_changeCredentials_whenDefaultConfigNotNull_credentialsMustNotChange() { - setupBasicTestWithAny { + executeBasicTestWithAny { val expectedAccountId = "acct123" val expectedToken = "token123" val expectedRegion = "eu" @@ -244,7 +255,7 @@ class CleverTapAPITest : BaseTestCase() { @Test fun test_changeCredentials_whenDefaultConfigNull_credentialsMustChange() { - setupMockExecutors { + executeMockExecutors { // Arrange val expectedAccountId = "acct123" val expectedToken = "token123" @@ -266,11 +277,9 @@ class CleverTapAPITest : BaseTestCase() { @Test fun test_createNotification_whenInstancesNull__createNotificationMustBeCalled() { - setupBasicTestWithAny { + executeBasicTestWithAny { 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 = @@ -285,7 +294,7 @@ class CleverTapAPITest : BaseTestCase() { @Test fun test_createNotification_whenInstanceNotNullAndAcctIDMatches__createNotificationMustBeCalled() { - setupBasicTestWithAny { + executeBasicTestWithAny { val bundle = Bundle() val lock = Object() bundle.putString(Constants.WZRK_ACCT_ID_KEY, Constant.ACC_ID) @@ -305,7 +314,7 @@ class CleverTapAPITest : BaseTestCase() { @Test fun test_createNotification_whenInstanceNotNullAndAcctIdDontMatch_createNotificationMustNotBeCalled() { - setupMockFactoryWithAny { + executeMockFactoryWithAny { val bundle = Bundle() bundle.putString(Constants.WZRK_ACCT_ID_KEY, "acct123") CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig) @@ -359,7 +368,7 @@ class CleverTapAPITest : BaseTestCase() { @Test fun test_processPushNotification_whenInstancesNull__processCustomPushNotificationMustBeCalled() { - setupMockFactoryWithAny { + executeMockFactoryWithAny { val bundle = Bundle() CleverTapAPI.processPushNotification(application, bundle) verify(corestate.pushProviders).processCustomPushNotification(bundle) @@ -368,7 +377,7 @@ class CleverTapAPITest : BaseTestCase() { @Test fun test_processPushNotification_whenInstancesNotNull__processCustomPushNotificationMustBeCalled() { - setupBasicTestWithAny { + executeBasicTestWithAny { val bundle = Bundle() bundle.putString(Constants.WZRK_ACCT_ID_KEY, Constant.ACC_ID) CleverTapAPI.instanceWithConfig(application, cleverTapInstanceConfig) @@ -383,10 +392,10 @@ 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 - setupBasicTest { + executeBasicTest { `when`(controllerManager.ctInboxController).thenReturn(inboxController) initializeCleverTapAPI() cleverTapAPI.deleteInboxMessagesForIDs(messageIDs) @@ -406,7 +415,7 @@ class CleverTapAPITest : BaseTestCase() { corestate.controllerManager = controllerManager // Act - setupBasicTest { + executeBasicTest { `when`(controllerManager.ctInboxController).thenReturn(inboxController) initializeCleverTapAPI() cleverTapAPI.deleteInboxMessagesForIDs(messageIDs) @@ -423,10 +432,10 @@ 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 - setupBasicTest { + executeBasicTest { `when`(controllerManager.ctInboxController).thenReturn(inboxController) initializeCleverTapAPI() cleverTapAPI.markReadInboxMessagesForIDs(messageIDs) @@ -446,7 +455,7 @@ class CleverTapAPITest : BaseTestCase() { corestate.controllerManager = controllerManager // Act - setupBasicTest { + executeBasicTest { `when`(controllerManager.ctInboxController).thenReturn(inboxController) initializeCleverTapAPI() cleverTapAPI.markReadInboxMessagesForIDs(messageIDs) @@ -458,13 +467,13 @@ class CleverTapAPITest : BaseTestCase() { } @Test - fun `test getUserEventLogCount`(){ + fun `test getUserEventLogCount`() { // Arrange val evt = "test" `when`(corestate.localDataStore.readUserEventLogCount(evt)).thenReturn(1) - + // Act - setupBasicTest { + executeBasicTest { initializeCleverTapAPI() val userEventLogCountActual = cleverTapAPI.getUserEventLogCount(evt) @@ -475,14 +484,14 @@ class CleverTapAPITest : BaseTestCase() { } @Test - fun `test getUserEventLog`(){ + fun `test getUserEventLog`() { // Arrange val evt = "test" - val log = UserEventLog(evt,1000L,1000L,1,"dId") + val log = UserEventLog(evt, 1000L, 1000L, 1, "dId") `when`(corestate.localDataStore.readUserEventLog(evt)).thenReturn(log) // Act - setupBasicTest { + executeBasicTest { initializeCleverTapAPI() val userEventLogActual = cleverTapAPI.getUserEventLog(evt) @@ -493,13 +502,13 @@ class CleverTapAPITest : BaseTestCase() { } @Test - fun `test getUserEventLogFirstTs`(){ + fun `test getUserEventLogFirstTs`() { // Arrange val evt = "test" `when`(corestate.localDataStore.readUserEventLogFirstTs(evt)).thenReturn(1000L) // Act - setupBasicTest { + executeBasicTest { initializeCleverTapAPI() val firstTsActual = cleverTapAPI.getUserEventLogFirstTs(evt) @@ -510,13 +519,13 @@ class CleverTapAPITest : BaseTestCase() { } @Test - fun `test getUserEventLogLastTs`(){ + fun `test getUserEventLogLastTs`() { // Arrange val evt = "test" `when`(corestate.localDataStore.readUserEventLogLastTs(evt)).thenReturn(1000L) // Act - setupBasicTest { + executeBasicTest { initializeCleverTapAPI() val lastTsActual = cleverTapAPI.getUserEventLogLastTs(evt) @@ -537,7 +546,7 @@ class CleverTapAPITest : BaseTestCase() { `when`(corestate.localDataStore.readUserEventLogs()).thenReturn(logs) // Act - setupBasicTest { + executeBasicTest { initializeCleverTapAPI() val historyActual = cleverTapAPI.userEventLogHistory @@ -555,7 +564,7 @@ class CleverTapAPITest : BaseTestCase() { `when`(corestate.sessionManager.userLastVisitTs).thenReturn(1000L) // Act - setupBasicTest { + executeBasicTest { initializeCleverTapAPI() val lastVisitTsActual = cleverTapAPI.userLastVisitTs @@ -572,7 +581,7 @@ class CleverTapAPITest : BaseTestCase() { .thenReturn(5) // Act - setupBasicTest { + executeBasicTest { initializeCleverTapAPI() val appLaunchCountActual = cleverTapAPI.userAppLaunchCount @@ -581,42 +590,6 @@ class CleverTapAPITest : BaseTestCase() { verify(corestate.localDataStore).readUserEventLogCount(Constants.APP_LAUNCHED_EVENT) } } -/* @Test - fun testPushDeepLink(){ - // Arrange - var cleverTapAPISpy : CleverTapAPI = Mockito.spy(cleverTapAPI) - val uri = Uri.parse("https://www.google.com/") - - //Act - cleverTapAPISpy.pushDeepLink(uri) - - //Assert - verify(cleverTapAPISpy).pushDeepLink(uri,false) - } - - @Test - fun testPushDeviceTokenEvent(){ - // Arrange - val ctAPI = CleverTapAPI.instanceWithConfig(application,cleverTapInstanceConfig) - var cleverTapAPISpy = Mockito.spy(ctAPI) - - cleverTapAPISpy.pushDeviceTokenEvent("12345",true,FCM) - verify(cleverTapAPISpy).queueEvent(ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.anyInt()) - } - - @Test - fun testPushLink(){ - var cleverTapAPISpy : CleverTapAPI = Mockito.spy(cleverTapAPI) - val uri = Uri.parse("https://www.google.com/") - - val mockStatic = Mockito.mockStatic(StorageHelper::class.java) - `when`(StorageHelper.getInt(ArgumentMatchers.any(), ArgumentMatchers.anyString(), ArgumentMatchers.anyInt())).thenReturn(0) - - //Act - cleverTapAPISpy.pushInstallReferrer("abc","def","ghi") - - verify(cleverTapAPISpy).pushDeepLink(ArgumentMatchers.any(), ArgumentMatchers.anyBoolean()) - }*/ @After fun tearDown() { 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 9f4d5aed9..c83c66c84 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 @@ -6,9 +6,9 @@ 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.UserEventLog +import com.clevertap.android.sdk.usereventlogs.UserEventLogDAO +import com.clevertap.android.sdk.usereventlogs.UserEventLogDAOImpl import com.clevertap.android.shared.test.BaseTestCase import org.json.JSONObject import org.junit.Test 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 98d4fcbcd..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,7 +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.* @@ -13,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) @@ -40,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) @@ -64,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) @@ -90,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) @@ -114,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) @@ -132,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) @@ -154,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) @@ -172,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) @@ -194,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()) @@ -216,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 @@ -246,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 @@ -273,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 @@ -300,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 @@ -330,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 @@ -360,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 @@ -387,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) @@ -412,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() @@ -421,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) @@ -439,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() @@ -448,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) @@ -463,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) @@ -487,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 @@ -517,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 @@ -544,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() @@ -553,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() @@ -571,14 +579,14 @@ 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)) } 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 0b55e4a83..2189269d2 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 @@ -3,6 +3,7 @@ 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.every import io.mockk.mockk @@ -68,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) @@ -79,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) @@ -100,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") })) } @@ -110,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") })) } @@ -1626,7 +1627,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 c197a319b..4f6b3b9b1 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 @@ -7,8 +7,6 @@ import com.clevertap.android.sdk.ControllerManager import com.clevertap.android.sdk.CoreMetaData 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 @@ -164,9 +162,6 @@ class NetworkManagerTest : BaseTestCase() { 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 similarity index 99% rename from clevertap-core/src/test/java/com/clevertap/android/sdk/userEventLogs/UserEventLogDAOImplTest.kt rename to clevertap-core/src/test/java/com/clevertap/android/sdk/usereventlogs/UserEventLogDAOImplTest.kt index eb57583be..ea2f351b5 100644 --- 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 @@ -1,4 +1,4 @@ -package com.clevertap.android.sdk.userEventLogs +package com.clevertap.android.sdk.usereventlogs import android.content.Context import android.database.sqlite.SQLiteException From bb16f66578083e742efb42e981901fe5ed12a674 Mon Sep 17 00:00:00 2001 From: Vassil Angelov <148857285+vasct@users.noreply.github.com> Date: Tue, 19 Nov 2024 13:57:19 +0200 Subject: [PATCH 077/120] Update preview in-app html template (#690) Applies fix for new lines and special js symbols --- .../src/main/assets/image_interstitial.html | 4 ++-- .../com/clevertap/android/sdk/AnalyticsManager.java | 2 +- .../android/sdk/inapp/CTInAppBaseFragmentTest.kt | 12 ++++++++++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/clevertap-core/src/main/assets/image_interstitial.html b/clevertap-core/src/main/assets/image_interstitial.html index a143d2b02..bf59a29df 100644 --- a/clevertap-core/src/main/assets/image_interstitial.html +++ b/clevertap-core/src/main/assets/image_interstitial.html @@ -22,7 +22,7 @@
- \ No newline at end of file + 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 36d7edefb..5b66da21d 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 @@ -624,7 +624,7 @@ public String wrapImageInterstitialContent(String content) { if (html != null && content != null) { String[] parts = html.split(Constants.INAPP_HTML_SPLIT); if (parts.length == 2) { - return String.format("%s'%s'%s", parts[0], content, parts[1]); + return parts[0] + content + parts[1]; } } } catch (IOException e) { diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/CTInAppBaseFragmentTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/CTInAppBaseFragmentTest.kt index 648c37de9..6d7c7665c 100644 --- a/clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/CTInAppBaseFragmentTest.kt +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/CTInAppBaseFragmentTest.kt @@ -143,9 +143,13 @@ class CTInAppBaseFragmentTest { verify { inAppListener.inAppNotificationActionTriggered( inAppNotification = any(), - action = any(), + action = match { action -> + // the open-url action should be performed with the url after __dl__ + dl == action.actionUrl + }, callToAction = callToActionParam, additionalData = match { data -> + // only the params of the original url should be tracked data.size() == 1 && param1 == data.getString("param1") }, @@ -158,9 +162,13 @@ class CTInAppBaseFragmentTest { verify { inAppListener.inAppNotificationActionTriggered( inAppNotification = any(), - action = any(), + action = match { action -> + // the open-url action should be performed with the url after __dl__ + dl == action.actionUrl + }, callToAction = callToActionArgument, additionalData = match { data -> + // only the params of the original url should be tracked data.size() == 1 && param1 == data.getString("param1") }, From 31947cebd07dc57f58d620219a48b9050b53c8de Mon Sep 17 00:00:00 2001 From: CTLalit Date: Tue, 19 Nov 2024 19:49:58 +0530 Subject: [PATCH 078/120] test(SDK-4171): extracts out current time + migrates to mockk - migrates from mockito to mockk - extracts out current time ms func, this is to gain control over clock and testing - changes test cases to mockk - adds a new test for duplicate PN not rendering logic --- .../android/sdk/AnalyticsManager.java | 9 +- .../android/sdk/CleverTapFactory.java | 3 +- .../android/sdk/AnalyticsManagerTest.kt | 657 ++++++++++-------- .../clevertap/android/sdk/MockCoreState.kt | 29 + 4 files changed, 387 insertions(+), 311 deletions(-) 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 c60130a5d..2b3c43f27 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 @@ -34,6 +34,8 @@ import org.json.JSONException; import org.json.JSONObject; +import kotlin.jvm.functions.Function0; + public class AnalyticsManager extends BaseAnalyticsManager { private final CTLockManager ctLockManager; @@ -48,6 +50,7 @@ public class AnalyticsManager extends BaseAnalyticsManager { private final ValidationResultStack validationResultStack; private final Validator validator; private final InAppResponse inAppResponse; + private final Function0 currentTimeProvider; private final Object notificationMapLock = new Object(); private final HashMap notificationIdTagMap = new HashMap<>(); @@ -63,7 +66,8 @@ public class AnalyticsManager extends BaseAnalyticsManager { DeviceInfo deviceInfo, BaseCallbackManager callbackManager, ControllerManager controllerManager, final CTLockManager ctLockManager, - InAppResponse inAppResponse + InAppResponse inAppResponse, + Function0 currentTimeProvider ) { this.context = context; this.config = config; @@ -76,6 +80,7 @@ public class AnalyticsManager extends BaseAnalyticsManager { this.ctLockManager = ctLockManager; this.controllerManager = controllerManager; this.inAppResponse = inAppResponse; + this.currentTimeProvider = currentTimeProvider; } @Override @@ -1167,7 +1172,7 @@ private boolean checkDuplicateNotificationIds(Bundle extras, HashMap + assertEquals(512, arg.errorCode) + }) + } } @Test @@ -113,10 +177,11 @@ class AnalyticsManagerTest { 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 @@ -126,15 +191,16 @@ class AnalyticsManagerTest { 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 @@ -143,15 +209,17 @@ class AnalyticsManagerTest { 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 @@ -160,29 +228,37 @@ class AnalyticsManagerTest { 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 @@ -192,15 +268,18 @@ class AnalyticsManagerTest { 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 @@ -210,15 +289,18 @@ class AnalyticsManagerTest { 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 @@ -228,70 +310,78 @@ class AnalyticsManagerTest { 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 @@ -300,33 +390,27 @@ class AnalyticsManagerTest { 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 @@ -335,18 +419,14 @@ class AnalyticsManagerTest { 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 @@ -355,18 +435,14 @@ class AnalyticsManagerTest { 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 @@ -374,63 +450,55 @@ class AnalyticsManagerTest { 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 @@ -442,18 +510,15 @@ class AnalyticsManagerTest { 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 @@ -467,63 +532,51 @@ class AnalyticsManagerTest { 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 + } } @@ -533,24 +586,22 @@ class AnalyticsManagerTest { 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 @@ -562,23 +613,21 @@ class AnalyticsManagerTest { 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 @@ -591,24 +640,21 @@ class AnalyticsManagerTest { 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 @@ -618,28 +664,24 @@ class AnalyticsManagerTest { 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 @@ -651,48 +693,47 @@ class AnalyticsManagerTest { 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/MockCoreState.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/MockCoreState.kt index 20f1d0255..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 @@ -11,6 +11,7 @@ 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.* // todo lp check usages and eliminate context setup @@ -41,3 +42,31 @@ class MockCoreState(cleverTapInstanceConfig: CleverTapInstanceConfig) : CoreStat 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) + } +} From b5a84717dbb188311e20718075b7e7aadf7d2c51 Mon Sep 17 00:00:00 2001 From: CTLalit Date: Tue, 19 Nov 2024 20:03:31 +0530 Subject: [PATCH 079/120] test(SDK-4171): corrects json comparison - compares json within method which gets generated correctly. --- .../android/sdk/AnalyticsManagerTest.kt | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) 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 8a4c1f843..dfa0a064a 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 @@ -130,14 +130,26 @@ class AnalyticsManagerTest { analyticsManagerSUT.pushNotificationViewedEvent(bundle) verify { - eventQueueManager.queueEvent(context, any(), Constants.NV_EVENT) + eventQueueManager.queueEvent( + context, + withArg { + JSONAssert.assertEquals(json, it, true) + }, + Constants.NV_EVENT + ) } // Send duplicate PN analyticsManagerSUT.pushNotificationViewedEvent(bundle) verify(exactly = 1) { - eventQueueManager.queueEvent(context, any(), Constants.NV_EVENT) + eventQueueManager.queueEvent( + context, + withArg { + JSONAssert.assertEquals(json, it, true) + }, + Constants.NV_EVENT + ) } confirmVerified(eventQueueManager) } From 6b6b32d85aaf13d92942817a8d5c5649f3e03766 Mon Sep 17 00:00:00 2001 From: CTLalit Date: Thu, 21 Nov 2024 17:25:33 +0530 Subject: [PATCH 080/120] test(SDK-4171): uses mock in tests - this is to emulate the current time --- .../android/sdk/AnalyticsManagerTest.kt | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) 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 dfa0a064a..3146c0a03 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 @@ -53,6 +53,9 @@ class AnalyticsManagerTest { @MockK(relaxed = true) private lateinit var inAppResponse: InAppResponse + @MockK(relaxed = true) + private lateinit var timeProvider: (() -> Long) + @Before fun setUp() { MockKAnnotations.init(this) @@ -70,10 +73,9 @@ class AnalyticsManagerTest { coreState.callbackManager, coreState.controllerManager, coreState.ctLockManager, - inAppResponse - ) { - 10000 - } + inAppResponse, + timeProvider + ) } @After @@ -127,6 +129,8 @@ class AnalyticsManagerTest { put("evtData", CTJsonConverter.getWzrkFields(bundle)) } + every { timeProvider.invoke() } returns 10000 + analyticsManagerSUT.pushNotificationViewedEvent(bundle) verify { @@ -139,9 +143,13 @@ class AnalyticsManagerTest { ) } + // setup again, 200 ms has passed + every { timeProvider.invoke() } returns 10200 + // Send duplicate PN analyticsManagerSUT.pushNotificationViewedEvent(bundle) + // verify it was not called again, one time was from before verify(exactly = 1) { eventQueueManager.queueEvent( context, From 4ff1d69e385057f4053624e764caaae2a2cdeb5f Mon Sep 17 00:00:00 2001 From: CTLalit Date: Thu, 21 Nov 2024 17:43:55 +0530 Subject: [PATCH 081/120] test(SDK-4171): extracts out method for bundler - for a seamless test code so we can avoid replication --- .../kotlin/PIFlushWorkInstrumentationTest.kt | 12 ++---- .../android/sdk/AnalyticsManager.java | 11 +---- .../android/sdk/AnalyticsManagerBundler.kt | 40 +++++++++++++++++++ .../android/sdk/utils/CTJsonConverter.java | 19 --------- .../android/sdk/AnalyticsManagerTest.kt | 9 ++--- 5 files changed, 48 insertions(+), 43 deletions(-) create mode 100644 clevertap-core/src/main/java/com/clevertap/android/sdk/AnalyticsManagerBundler.kt 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 2b3c43f27..235154d05 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 @@ -514,7 +514,7 @@ public void pushNotificationClickedEvent(final Bundle extras) { baseEventQueueManager.queueEvent(context, event, Constants.RAISED_EVENT); try { - coreMetaData.setWzrkParams(getWzrkFields(extras)); + coreMetaData.setWzrkParams(AnalyticsManagerBundler.INSTANCE.wzrkBundleToJson(extras)); } catch (Throwable t) { // no-op } @@ -671,14 +671,7 @@ public void pushNotificationViewedEvent(Bundle extras) { 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.INSTANCE.notificationViewedJson(extras); baseEventQueueManager.queueEvent(context, event, Constants.NV_EVENT); } 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..5f49c0296 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/AnalyticsManagerBundler.kt @@ -0,0 +1,40 @@ +package com.clevertap.android.sdk + +import android.os.Bundle +import org.json.JSONException +import org.json.JSONObject + +object AnalyticsManagerBundler { + + @Throws(JSONException::class) + 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 + } + + 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/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/test/java/com/clevertap/android/sdk/AnalyticsManagerTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/AnalyticsManagerTest.kt index 3146c0a03..784892211 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 @@ -2,12 +2,12 @@ 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.fixtures.CleverTapFixtures 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.CTJsonConverter import com.clevertap.android.sdk.validation.ValidationResult import com.clevertap.android.sdk.validation.ValidationResultStack import com.clevertap.android.sdk.validation.Validator @@ -114,7 +114,7 @@ class AnalyticsManagerTest { } @Test - fun `clevertap does not process duplicate PN viewed within 2 seconds`() { + fun `clevertap does not process duplicate PN viewed within 2 seconds - case 2nd notif in 200ms`() { // send PN first time val bundle = Bundle().apply { @@ -124,10 +124,7 @@ class AnalyticsManagerTest { putString("wzrk_someid", "someid") } - val json = JSONObject().apply { - put("evtName", Constants.NOTIFICATION_VIEWED_EVENT_NAME) - put("evtData", CTJsonConverter.getWzrkFields(bundle)) - } + val json = notificationViewedJson(bundle); every { timeProvider.invoke() } returns 10000 From 736629e217147cac9a845bff3d4ee5e759cd434a Mon Sep 17 00:00:00 2001 From: CTLalit Date: Thu, 21 Nov 2024 17:48:22 +0530 Subject: [PATCH 082/120] test(SDK-4171): adds test for PN arriving after span of 2 seconds with same wzrk id --- .../android/sdk/AnalyticsManagerTest.kt | 48 ++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) 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 784892211..3413942c6 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 @@ -119,7 +119,7 @@ class AnalyticsManagerTest { // send PN first time val bundle = Bundle().apply { putString("wzrk_pn", "wzrk_pn") - putString("wzrk_id", "id") + putString("wzrk_id", "duplicate-id") putString("wzrk_pid", "pid") putString("wzrk_someid", "someid") } @@ -159,6 +159,52 @@ class AnalyticsManagerTest { confirmVerified(eventQueueManager) } + @Test + fun `clevertap processes PN viewed for same wzrk_id if separated by a span of greater than 2 seconds`() { + + // send PN first time + val bundle = Bundle().apply { + putString("wzrk_pn", "wzrk_pn") + putString("wzrk_id", "duplicate-id") + putString("wzrk_pid", "pid") + putString("wzrk_someid", "someid") + } + + val json = notificationViewedJson(bundle); + + every { timeProvider.invoke() } returns 10000 + + analyticsManagerSUT.pushNotificationViewedEvent(bundle) + + verify(exactly = 1) { + eventQueueManager.queueEvent( + context, + withArg { + JSONAssert.assertEquals(json, it, true) + }, + Constants.NV_EVENT + ) + } + + // setup again, 10000 ms has passed + every { timeProvider.invoke() } returns 20000 + + // Send duplicate PN + analyticsManagerSUT.pushNotificationViewedEvent(bundle) + + // verify it was not called again, one time was from before + verify(exactly = 2) { + eventQueueManager.queueEvent( + context, + withArg { + JSONAssert.assertEquals(json, it, true) + }, + Constants.NV_EVENT + ) + } + confirmVerified(eventQueueManager) + } + @Test fun test_incrementValue_nullKey_noAction() { analyticsManagerSUT.incrementValue(null, 10) From 3035182e0dbcfe61763cf597e20eb58fcae033ff Mon Sep 17 00:00:00 2001 From: CTLalit Date: Thu, 21 Nov 2024 18:15:57 +0530 Subject: [PATCH 083/120] test(SDK-4171): calls extracted method to build PN bundle --- .../clevertap/android/sdk/AnalyticsManager.java | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) 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 235154d05..26c84de76 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 @@ -498,21 +498,11 @@ public void pushNotificationClickedEvent(final Bundle extras) { 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.INSTANCE.notificationViewedJson(extras); - event.put("evtName", Constants.NOTIFICATION_CLICKED_EVENT_NAME); - event.put("evtData", notif); baseEventQueueManager.queueEvent(context, event, Constants.RAISED_EVENT); - try { coreMetaData.setWzrkParams(AnalyticsManagerBundler.INSTANCE.wzrkBundleToJson(extras)); } catch (Throwable t) { From 678dc9e8339c0a25c34f525e2200686ecefcbb1e Mon Sep 17 00:00:00 2001 From: CTLalit Date: Thu, 21 Nov 2024 18:29:51 +0530 Subject: [PATCH 084/120] test(SDK-4171): adds tests for notification clicked events - adds notification clicked events. --- .../android/sdk/AnalyticsManager.java | 6 +- .../android/sdk/AnalyticsManagerTest.kt | 94 ++++++++++++++++++- 2 files changed, 94 insertions(+), 6 deletions(-) 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 26c84de76..59b2f63f3 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 @@ -458,8 +458,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(), @@ -484,8 +483,7 @@ public void pushNotificationClickedEvent(final Bundle extras) { 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; } 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 3413942c6..bad5547d4 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 @@ -36,7 +36,7 @@ class AnalyticsManagerTest { private lateinit var analyticsManagerSUT: AnalyticsManager private lateinit var coreState: MockCoreStateKotlin - private val cleverTapInstanceConfig: CleverTapInstanceConfig = CleverTapFixtures.provideCleverTapInstanceConfig() + private val cleverTapInstanceConfig = CleverTapFixtures.provideCleverTapInstanceConfig() @MockK(relaxed = true) private lateinit var validator: Validator @@ -192,7 +192,7 @@ class AnalyticsManagerTest { // Send duplicate PN analyticsManagerSUT.pushNotificationViewedEvent(bundle) - // verify it was not called again, one time was from before + // verify queue event called again verify(exactly = 2) { eventQueueManager.queueEvent( context, @@ -205,6 +205,96 @@ class AnalyticsManagerTest { confirmVerified(eventQueueManager) } + @Test + fun `clevertap does not process duplicate (same wzrk_id) PN clicked within 2 seconds - case 2nd click happens in 200ms`() { + // send PN first time + val bundle = Bundle().apply { + putString("wzrk_pn", "wzrk_pn") + putString("wzrk_id", "duplicate-id") + putString("wzrk_pid", "pid") + putString("wzrk_someid", "someid") + } + + val json = notificationViewedJson(bundle); + + every { timeProvider.invoke() } returns 10000 + + analyticsManagerSUT.pushNotificationClickedEvent(bundle) + + verify(exactly = 1) { + eventQueueManager.queueEvent( + context, + withArg { + JSONAssert.assertEquals(json, it, true) + }, + Constants.RAISED_EVENT + ) + } + + // setup again, 10000 ms has passed + every { timeProvider.invoke() } returns 12000 + + // Send duplicate PN + analyticsManagerSUT.pushNotificationClickedEvent(bundle) + + // 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`() { + // send PN first time + val bundle = Bundle().apply { + putString("wzrk_pn", "wzrk_pn") + putString("wzrk_id", "duplicate-id") + putString("wzrk_pid", "pid") + putString("wzrk_someid", "someid") + } + + val json = notificationViewedJson(bundle); + + every { timeProvider.invoke() } returns 10000 + + analyticsManagerSUT.pushNotificationClickedEvent(bundle) + + verify(exactly = 1) { + eventQueueManager.queueEvent( + context, + withArg { + JSONAssert.assertEquals(json, it, true) + }, + Constants.RAISED_EVENT + ) + } + + // setup again, 10000 ms has passed + every { timeProvider.invoke() } returns 20000 + + // Send duplicate PN + analyticsManagerSUT.pushNotificationClickedEvent(bundle) + + // verify queue event called again + verify(exactly = 2) { + eventQueueManager.queueEvent( + context, + withArg { + JSONAssert.assertEquals(json, it, true) + }, + Constants.RAISED_EVENT + ) + } + confirmVerified(eventQueueManager) + } + @Test fun test_incrementValue_nullKey_noAction() { analyticsManagerSUT.incrementValue(null, 10) From be704b348d53995975c88939e758f41930f707e0 Mon Sep 17 00:00:00 2001 From: CTLalit Date: Thu, 21 Nov 2024 18:41:10 +0530 Subject: [PATCH 085/120] test(SDK-4171): adds test coverage for notification clicked. - failure cases when sdk is in analytics mode - notifications not from clevertap - comes with incorrect params. --- .../android/sdk/AnalyticsManagerTest.kt | 63 +++++++++---------- 1 file changed, 29 insertions(+), 34 deletions(-) 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 bad5547d4..d7f96d4e4 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 @@ -56,6 +56,13 @@ class AnalyticsManagerTest { @MockK(relaxed = true) private lateinit var timeProvider: (() -> Long) + private val bundle = Bundle().apply { + putString("wzrk_pn", "wzrk_pn") + putString("wzrk_id", "duplicate-id") + putString("wzrk_pid", "pid") + putString("wzrk_someid", "someid") + } + @Before fun setUp() { MockKAnnotations.init(this) @@ -86,17 +93,19 @@ class AnalyticsManagerTest { } @Test - fun `clevertap does not process push notification viewed event if it is not from clevertap`() { + 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 @@ -108,26 +117,20 @@ class AnalyticsManagerTest { } 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`() { - - // send PN first time - val bundle = Bundle().apply { - putString("wzrk_pn", "wzrk_pn") - putString("wzrk_id", "duplicate-id") - putString("wzrk_pid", "pid") - putString("wzrk_someid", "someid") - } - - val json = notificationViewedJson(bundle); + val json = notificationViewedJson(bundle) every { timeProvider.invoke() } returns 10000 + // send PN first time analyticsManagerSUT.pushNotificationViewedEvent(bundle) verify { @@ -162,18 +165,11 @@ class AnalyticsManagerTest { @Test fun `clevertap processes PN viewed for same wzrk_id if separated by a span of greater than 2 seconds`() { - // send PN first time - val bundle = Bundle().apply { - putString("wzrk_pn", "wzrk_pn") - putString("wzrk_id", "duplicate-id") - putString("wzrk_pid", "pid") - putString("wzrk_someid", "someid") - } - val json = notificationViewedJson(bundle); every { timeProvider.invoke() } returns 10000 + // send PN first time analyticsManagerSUT.pushNotificationViewedEvent(bundle) verify(exactly = 1) { @@ -206,19 +202,25 @@ class AnalyticsManagerTest { } @Test - fun `clevertap does not process duplicate (same wzrk_id) PN clicked within 2 seconds - case 2nd click happens in 200ms`() { + fun `clevertap does not process PN Clicked if SDK is set to analytics only`() { + cleverTapInstanceConfig.isAnalyticsOnly = true + // send PN first time - val bundle = Bundle().apply { - putString("wzrk_pn", "wzrk_pn") - putString("wzrk_id", "duplicate-id") - putString("wzrk_pid", "pid") - putString("wzrk_someid", "someid") + analyticsManagerSUT.pushNotificationClickedEvent(bundle) + + verify { + eventQueueManager wasNot called } + confirmVerified(eventQueueManager) + } - val json = notificationViewedJson(bundle); + @Test + fun `clevertap does not process duplicate (same wzrk_id) PN clicked within 2 seconds - case 2nd click happens in 200ms`() { + val json = notificationViewedJson(bundle) every { timeProvider.invoke() } returns 10000 + // send PN first time analyticsManagerSUT.pushNotificationClickedEvent(bundle) verify(exactly = 1) { @@ -252,18 +254,11 @@ class AnalyticsManagerTest { @Test fun `clevertap processes PN clicked for same wzrk_id if separated by a span of greater than 5 seconds`() { - // send PN first time - val bundle = Bundle().apply { - putString("wzrk_pn", "wzrk_pn") - putString("wzrk_id", "duplicate-id") - putString("wzrk_pid", "pid") - putString("wzrk_someid", "someid") - } val json = notificationViewedJson(bundle); - every { timeProvider.invoke() } returns 10000 + // send PN first time analyticsManagerSUT.pushNotificationClickedEvent(bundle) verify(exactly = 1) { From a357dd052e0655ddf58ce93435ac997e1be2cf59 Mon Sep 17 00:00:00 2001 From: CTLalit Date: Thu, 21 Nov 2024 18:46:10 +0530 Subject: [PATCH 086/120] test(SDK-4171): removes redundant try catch --- .../java/com/clevertap/android/sdk/AnalyticsManager.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) 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 59b2f63f3..a4d89b150 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 @@ -501,11 +501,7 @@ public void pushNotificationClickedEvent(final Bundle extras) { JSONObject event = AnalyticsManagerBundler.INSTANCE.notificationViewedJson(extras); baseEventQueueManager.queueEvent(context, event, Constants.RAISED_EVENT); - try { - coreMetaData.setWzrkParams(AnalyticsManagerBundler.INSTANCE.wzrkBundleToJson(extras)); - } catch (Throwable t) { - // no-op - } + coreMetaData.setWzrkParams(AnalyticsManagerBundler.INSTANCE.wzrkBundleToJson(extras)); } catch (Throwable t) { // We won't get here } From 8f700604621aa6263a49fc666239769af0eefc5c Mon Sep 17 00:00:00 2001 From: CTLalit <144685420+CTLalit@users.noreply.github.com> Date: Tue, 26 Nov 2024 18:47:10 +0530 Subject: [PATCH 087/120] Bug/v2/sdk 4183 (#698) * feat(SDK-4183): Event properties normalisation - backend sends normalised event properties, we need to compare on sdk side based on normalised event which client app might fire * feat(SDK-4183): Charged event product props normalisation - normalises product item prop keys * chore(SDK-4183): reformatting * test(SDK-4183): adds test to check normalisation * test(SDK-4183): adds test to check normalisation in non charged events --- .../android/sdk/inapp/InAppController.java | 20 ++- .../sdk/inapp/evaluation/EventAdapter.kt | 19 ++- .../sdk/inapp/evaluation/TriggersMatcher.kt | 37 +++--- .../sdk/inapp/evaluation/EventAdapterTest.kt | 122 +++++++++++++++++- 4 files changed, 170 insertions(+), 28 deletions(-) diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/InAppController.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/InAppController.java index 6b92f72ce..69c1ab5f0 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/InAppController.java +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/InAppController.java @@ -966,8 +966,11 @@ public void onQueueEvent(final String eventName, Map eventProper } @WorkerThread - public void onQueueChargedEvent(Map chargeDetails, - List> items, Location userLocation) { + public void onQueueChargedEvent( + Map chargeDetails, + List> items, + Location userLocation + ) { final Map appFieldsWithChargedEventProperties = JsonUtil.mapFromJson( deviceInfo.getAppLaunchedFields()); appFieldsWithChargedEventProperties.putAll(chargeDetails); @@ -979,8 +982,10 @@ public void onQueueChargedEvent(Map chargeDetails, } @WorkerThread - public void onQueueProfileEvent(final Map> userAttributeChangedProperties, - Location location) { + public void onQueueProfileEvent( + final Map> userAttributeChangedProperties, + Location location + ) { final Map appFields = JsonUtil.mapFromJson( deviceInfo.getAppLaunchedFields()); final JSONArray clientSideInAppsToDisplay = evaluationManager.evaluateOnUserAttributeChange( @@ -990,9 +995,10 @@ public void onQueueProfileEvent(final Map> userAttri } } - public void onAppLaunchServerSideInAppsResponse(@NonNull JSONArray appLaunchServerSideInApps, - Location userLocation) - throws JSONException { + public void onAppLaunchServerSideInAppsResponse( + @NonNull JSONArray appLaunchServerSideInApps, + Location userLocation + ) throws JSONException { final Map appLaunchedProperties = JsonUtil.mapFromJson( deviceInfo.getAppLaunchedFields()); List appLaunchSsInAppList = Utils.toJSONObjectList(appLaunchServerSideInApps); diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/evaluation/EventAdapter.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/evaluation/EventAdapter.kt index d7d520e9f..102bf4988 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/evaluation/EventAdapter.kt +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/evaluation/EventAdapter.kt @@ -17,6 +17,7 @@ import com.clevertap.android.sdk.Constants.CLTAP_PROP_VARIANT import com.clevertap.android.sdk.Constants.CLTAP_SDK_VERSION import com.clevertap.android.sdk.Constants.INAPP_WZRK_PIVOT import com.clevertap.android.sdk.Constants.NOTIFICATION_ID_TAG +import com.clevertap.android.sdk.Utils /** * Represents an event and its associated properties. @@ -59,6 +60,7 @@ class EventAdapter( /** * Gets the property value for the specified property name. + * Note: Compares after normalising (removing all whitespaces) * * @param propertyName The name of the property to retrieve. * @return A [TriggerValue] representing the property value. @@ -70,12 +72,21 @@ class EventAdapter( /** * Gets the item value for the specified property name from the list of items. + * Note: Compares after normalising (removing all whitespaces) * * @param propertyName The name of the property to retrieve from the items. * @return A [TriggerValue] representing the item value. */ fun getItemValue(propertyName: String): List { - return items.filterNotNull().map { TriggerValue(it[propertyName]) } + return items + .filterNotNull() + .map { productMap: Map -> + val normalisedMap = productMap.map { + Utils.getNormalizedName(it.key) to it.value + }.toMap() + + TriggerValue(normalisedMap[Utils.getNormalizedName(propertyName)]) + }.filter { it.value != null } } /** @@ -99,7 +110,11 @@ class EventAdapter( @VisibleForTesting internal fun getActualPropertyValue(propertyName: String): Any? { - var value = eventProperties[propertyName] + val normalisedMap = eventProperties.map { item -> + Utils.getNormalizedName(item.key) to item.value + }.toMap() + + var value = normalisedMap[Utils.getNormalizedName(propertyName)] if (value == null) { value = when (propertyName) { 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 7a5ce3519..e5b9682ea 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 @@ -49,13 +49,8 @@ class TriggersMatcher { @VisibleForTesting internal fun match(trigger: TriggerAdapter, event: EventAdapter): Boolean { // Evaluate further if either the eventNames match or the profileAttrName's match. Make sure both profileAttrName's are not null and equal - if (!Utils.areNamesNormalizedEqual( - event.eventName, - trigger.eventName - ) && (event.profileAttrName == null || !Utils.areNamesNormalizedEqual( - event.profileAttrName, trigger.profileAttrName - )) - ) { + if (!Utils.areNamesNormalizedEqual(event.eventName, trigger.eventName) + && (event.profileAttrName == null || !Utils.areNamesNormalizedEqual(event.profileAttrName, trigger.profileAttrName))) { return false } @@ -74,30 +69,36 @@ class TriggersMatcher { return true } - private fun matchPropertyConditions(trigger: TriggerAdapter, event: EventAdapter): Boolean { + private fun matchPropertyConditions( + triggerAdapter: TriggerAdapter, + event: EventAdapter + ): Boolean { // Property conditions are AND-ed - return (0 until trigger.propertyCount) - .mapNotNull { trigger.propertyAtIndex(it) } + return (0 until triggerAdapter.propertyCount) + .mapNotNull { triggerAdapter.propertyAtIndex(it) } .all { evaluate( - it.op, - it.value, - event.getPropertyValue(it.propertyName) + op = it.op, + expected = it.value, + actual = event.getPropertyValue(it.propertyName) ) } } - private fun matchChargedItemConditions(trigger: TriggerAdapter, event: EventAdapter): Boolean { + private fun matchChargedItemConditions( + trigger: TriggerAdapter, + event: EventAdapter + ): Boolean { // (chargedEvent only) Property conditions for items are AND-ed return (0 until trigger.itemsCount) .mapNotNull { trigger.itemAtIndex(it) } .all { condition -> event.getItemValue(condition.propertyName) - .any { + .any { item -> evaluate( - condition.op, - condition.value, - it + op = condition.op, + expected = condition.value, + actual = item ) } } diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/evaluation/EventAdapterTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/evaluation/EventAdapterTest.kt index c3483da85..9f8aec7bd 100644 --- a/clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/evaluation/EventAdapterTest.kt +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/evaluation/EventAdapterTest.kt @@ -3,6 +3,7 @@ package com.clevertap.android.sdk.inapp.evaluation import com.clevertap.android.sdk.Constants import com.clevertap.android.shared.test.BaseTestCase import org.junit.* +import org.junit.Assert.assertNotEquals import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotNull @@ -28,6 +29,69 @@ class EventAdapterTest : BaseTestCase() { assertFalse { expected.isList() } } + @Test + fun testGetPropertyPresentNormalisations() { + // Arrange + val eventProperties = mapOf(" name " to "John", "a ge " to 30) + val eventAdapter = EventAdapter("eventName", eventProperties) + + // Act + val result = eventAdapter.getPropertyValue("name") + val expected = TriggerValue("John") + + // Assert + assertNotNull(result) + assertEquals(expected.stringValue(), result.stringValue()) + assertNull(result.numberValue()) + assertNull(result.listValue()) + assertFalse { expected.isList() } + + // Act + val result1 = eventAdapter.getPropertyValue("age") + val expected1 = TriggerValue(30) + + // Assert + assertNotNull(result1) + assertEquals(expected1.numberValue(), result1.numberValue()) + assertNull(result1.stringValue()) + assertNull(result1.listValue()) + assertFalse { expected1.isList() } + } + + @Test + fun testGetPropertyPresentNormalisations2() { + // Arrange + val eventProperties = mapOf(" name " to "John Doe", "Lucky Numbers" to listOf(1, 2)) + val eventAdapter = EventAdapter("eventName", eventProperties) + + // Act + val result = eventAdapter.getPropertyValue("name") + val expected = TriggerValue("John Doe") + val notExpected = TriggerValue("JohnDoe") + + // Assert + assertNotNull(result) + assertEquals(expected.stringValue(), result.stringValue()) + assertNotEquals(notExpected.stringValue(), result.stringValue()) + assertNull(result.numberValue()) + assertNull(result.listValue()) + assertFalse { expected.isList() } + + // Act + val result1 = eventAdapter.getPropertyValue("luckynumbers") + val expected1 = TriggerValue( + value = null, + listValue = listOf(1, 2) + ) + + // Assert + assertNotNull(result1) + assertEquals(expected1.listValue(), result1.listValue()) + assertNull(result1.stringValue()) + assertNull(result1.numberValue()) + assertTrue { expected1.isList() } + } + @Test fun testGetPropertyMissing() { // Arrange @@ -68,6 +132,62 @@ class EventAdapterTest : BaseTestCase() { assertTrue(expected.isList()) } + @Test + fun testGetItemValuePresentNormalisation() { + // Arrange + val items = listOf( + mapOf("i temName" to "item1", "ite mPrice" to 10), + mapOf("itemN ame" to "item2", " itemPrice" to 20), + mapOf("itemName " to "item3", "itemPrice " to 30), + mapOf("itemNameInvalid" to " item33", "itemPriceInvalid " to 330) + ) + val eventAdapter = EventAdapter("eventName", emptyMap(), items) + + // Act + val result1 = eventAdapter.getItemValue("itemName") + + // Assert + assertNotNull(result1) + result1.onEach { + assertNull(it.numberValue()) + assertNull(it.listValue()) + } + assertEquals(listOf("item1", "item2", "item3"), result1.map { it.stringValue() }) + + // Act + val result2 = eventAdapter.getItemValue("itemNameInvalid") + + // Assert + assertNotNull(result2) + result2.onEach { + assertNull(it.numberValue()) + assertNull(it.listValue()) + } + assertEquals(listOf(" item33"), result2.map { it.stringValue() }) + + // Act + val result3 = eventAdapter.getItemValue("itemPrice") + + // Assert + assertNotNull(result3) + result3.onEach { + assertNull(it.stringValue()) + assertNull(it.listValue()) + } + assertEquals(listOf(10, 20, 30), result3.map { it.numberValue() }) + + // Act + val result4 = eventAdapter.getItemValue("itemPriceInvalid") + + // Assert + assertNotNull(result4) + result4.onEach { + assertNull(it.stringValue()) + assertNull(it.listValue()) + } + assertEquals(listOf(330), result4.map { it.numberValue() }) + } + @Test fun testGetItemValueMissing() { // Arrange @@ -81,7 +201,7 @@ class EventAdapterTest : BaseTestCase() { val result = eventAdapter.getItemValue("itemPrice") // Assert - assertEquals(listOf(null, null), result.map { it.stringValue() }) + assertEquals(emptyList(), result.map { it.stringValue() }) } @Test From 25ba6a87caf7e447d96338d36b8324b32199b3b7 Mon Sep 17 00:00:00 2001 From: CTLalit <144685420+CTLalit@users.noreply.github.com> Date: Fri, 29 Nov 2024 16:10:28 +0530 Subject: [PATCH 088/120] feat(SDK-4183): changes normalisation to follow LP logic (#700) * feat(SDK-4183): changes normalisation to follow LP logic - makes changes to event names and properties and also to charged event logic - tries exact name match first - tries match with normalised event name - tries match after normalising both event name/property and triggers we get from BE * tests(SDK-4183): adds test cases for nomalisation logic - adds for both charged and normal events. --- .../sdk/inapp/evaluation/EventAdapter.kt | 50 +++-- .../sdk/inapp/evaluation/EventAdapterTest.kt | 198 ++++++++++++++++++ 2 files changed, 234 insertions(+), 14 deletions(-) diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/evaluation/EventAdapter.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/evaluation/EventAdapter.kt index 102bf4988..78eef5518 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/evaluation/EventAdapter.kt +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/evaluation/EventAdapter.kt @@ -34,7 +34,7 @@ class EventAdapter( val profileAttrName: String? = null // for profile events only ) { - private val systemPropToKey = mapOf( + internal val systemPropToKey = mapOf( "CT App Version" to CLTAP_APP_VERSION, "ct_app_version" to CLTAP_APP_VERSION, "CT Latitude" to CLTAP_LATITUDE, @@ -81,11 +81,20 @@ class EventAdapter( return items .filterNotNull() .map { productMap: Map -> - val normalisedMap = productMap.map { - Utils.getNormalizedName(it.key) to it.value - }.toMap() - TriggerValue(normalisedMap[Utils.getNormalizedName(propertyName)]) + var op = productMap[propertyName] + + if (op == null) { + op = productMap[Utils.getNormalizedName(propertyName)] + } + + if (op == null) { + val normalisedMap = productMap.map { + Utils.getNormalizedName(it.key) to it.value + }.toMap() + op = normalisedMap[Utils.getNormalizedName(propertyName)] + } + TriggerValue(op) }.filter { it.value != null } } @@ -110,22 +119,35 @@ class EventAdapter( @VisibleForTesting internal fun getActualPropertyValue(propertyName: String): Any? { - val normalisedMap = eventProperties.map { item -> - Utils.getNormalizedName(item.key) to item.value - }.toMap() - var value = normalisedMap[Utils.getNormalizedName(propertyName)] + var value = evaluateActualPropertyValue(propertyName) if (value == null) { value = when (propertyName) { - CLTAP_PROP_CAMPAIGN_ID -> eventProperties[NOTIFICATION_ID_TAG] - NOTIFICATION_ID_TAG -> eventProperties[CLTAP_PROP_CAMPAIGN_ID] - CLTAP_PROP_VARIANT -> eventProperties[INAPP_WZRK_PIVOT] - INAPP_WZRK_PIVOT -> eventProperties[CLTAP_PROP_VARIANT] - else -> systemPropToKey[propertyName]?.let { eventProperties[it] } + CLTAP_PROP_CAMPAIGN_ID -> evaluateActualPropertyValue(NOTIFICATION_ID_TAG) + NOTIFICATION_ID_TAG -> evaluateActualPropertyValue(CLTAP_PROP_CAMPAIGN_ID) + CLTAP_PROP_VARIANT -> evaluateActualPropertyValue(INAPP_WZRK_PIVOT) + INAPP_WZRK_PIVOT -> evaluateActualPropertyValue(CLTAP_PROP_VARIANT) + else -> systemPropToKey[propertyName]?.let { evaluateActualPropertyValue(it) } } } return value } + + private fun evaluateActualPropertyValue(propertyName: String): Any? { + var value = eventProperties[propertyName] + + if (value == null) { + value = eventProperties[Utils.getNormalizedName(propertyName)] + } + + if (value == null) { + val normalisedMap = eventProperties.map { item -> + Utils.getNormalizedName(item.key) to item.value + }.toMap() + value = normalisedMap[Utils.getNormalizedName(propertyName)] + } + return value + } } diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/evaluation/EventAdapterTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/evaluation/EventAdapterTest.kt index 9f8aec7bd..a3f8ac229 100644 --- a/clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/evaluation/EventAdapterTest.kt +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/inapp/evaluation/EventAdapterTest.kt @@ -29,6 +29,49 @@ class EventAdapterTest : BaseTestCase() { assertFalse { expected.isList() } } + @Test + fun testGetPropertyPresentSpecialCases() { + // Arrange + val eventProperties = mapOf( + "name" to "John", + Constants.CLTAP_PROP_CAMPAIGN_ID to "cpid", + Constants.NOTIFICATION_ID_TAG to "some_wzrk_id", + Constants.CLTAP_PROP_VARIANT to "some variant", + Constants.INAPP_WZRK_PIVOT to "some_wzrk_pivot", + ) + val eventAdapter = EventAdapter("eventName", eventProperties) + + val triggerProps = listOf( + Constants.CLTAP_PROP_CAMPAIGN_ID, + Constants.NOTIFICATION_ID_TAG, + Constants.CLTAP_PROP_VARIANT, + Constants.INAPP_WZRK_PIVOT + ) + + val expectedResults = listOf( + TriggerValue("cpid"), + TriggerValue("some_wzrk_id"), + TriggerValue("some variant"), + TriggerValue("some_wzrk_pivot") + ) + + val results = mutableListOf().apply { + triggerProps.forEach { + add(eventAdapter.getPropertyValue(it)) + } + } + + results.forEachIndexed { index, result -> + val expected = expectedResults[index] + // Assert + assertNotNull(result) + assertEquals(expected.stringValue(), result.stringValue()) + assertNull(result.numberValue()) + assertNull(result.listValue()) + assertFalse { expected.isList() } + } + } + @Test fun testGetPropertyPresentNormalisations() { // Arrange @@ -92,6 +135,94 @@ class EventAdapterTest : BaseTestCase() { assertTrue { expected1.isList() } } + @Test + fun testGetPropertyPresentNormalisationsExactMatch() { + // Arrange + val eventProperties = mapOf( + "fullname" to "John", + "full name" to "Lenon", + "FullName" to "Augustus", + "age" to 30 + ) + val eventAdapter = EventAdapter("eventName", eventProperties) + + // Act + val result = eventAdapter.getPropertyValue("fullname") + val expected = TriggerValue("John") + + // Assert + assertNotNull(result) + assertEquals(expected.stringValue(), result.stringValue()) + assertNull(result.numberValue()) + assertNull(result.listValue()) + assertFalse { expected.isList() } + + // Act + val result1 = eventAdapter.getPropertyValue("full name") + val expected1 = TriggerValue("Lenon") + + // Assert + assertNotNull(result1) + assertEquals(expected1.stringValue(), result1.stringValue()) + assertNull(result1.numberValue()) + assertNull(result1.listValue()) + assertFalse { expected1.isList() } + } + + @Test + fun testGetPropertyPresentNormalisationsNormalisedMatch() { + // Arrange + val eventProperties = mapOf( + "fullname" to "John", + "full-name" to "Lennon", + "age" to 30 + ) + val eventAdapter = EventAdapter("eventName", eventProperties) + + // Act + val result = eventAdapter.getPropertyValue("full name") + val expected = TriggerValue("John") + + // Assert + assertNotNull(result) + assertEquals(expected.stringValue(), result.stringValue()) + assertNull(result.numberValue()) + assertNull(result.listValue()) + assertFalse { expected.isList() } + + // Act + val result1 = eventAdapter.getPropertyValue("FullName") + val expected1 = TriggerValue("John") + + // Assert + assertNotNull(result1) + assertEquals(expected1.stringValue(), result1.stringValue()) + assertNull(result1.numberValue()) + assertNull(result1.listValue()) + assertFalse { expected1.isList() } + } + + @Test + fun testGetPropertyPresentNormalisationsNormalisedMatchBothSides() { + // Arrange + val eventProperties = mapOf( + "FullName" to "John", + "age" to 30 + ) + val eventAdapter = EventAdapter("eventName", eventProperties) + + // Act + val result = eventAdapter.getPropertyValue("full name") + val expected = TriggerValue("John") + + // Assert + assertNotNull(result) + assertEquals(expected.stringValue(), result.stringValue()) + assertNull(result.numberValue()) + assertNull(result.listValue()) + assertFalse { expected.isList() } + } + @Test fun testGetPropertyMissing() { // Arrange @@ -188,6 +319,73 @@ class EventAdapterTest : BaseTestCase() { assertEquals(listOf(330), result4.map { it.numberValue() }) } + @Test + fun testGetItemValuePresentExactMatch() { + // Arrange + val items = listOf( + mapOf("itemName" to "item1", "itemPrice" to 10), + mapOf("item mame" to "item2", "itemPrice" to 20) + ) + val eventAdapter = EventAdapter("itemName", emptyMap(), items) + val expected = TriggerValue("item1") + + // Act + val result = eventAdapter.getItemValue("itemName") + + // Assert + assertNotNull(result) + result.onEach { + assertNull(it.numberValue()) + assertNull(it.listValue()) + } + assertEquals(listOf("item1"), result.map { it.stringValue() }) + assertFalse(expected.isList()) + } + + @Test + fun testGetItemValuePresentNormalisedMatch() { + // Arrange + val items = listOf( + mapOf("itemName" to "item1", "itemPrice" to 10), + ) + val eventAdapter = EventAdapter("item name", emptyMap(), items) + val expected = TriggerValue("item1") + + // Act + val result = eventAdapter.getItemValue("itemName") + + // Assert + assertNotNull(result) + result.onEach { + assertNull(it.numberValue()) + assertNull(it.listValue()) + } + assertEquals(listOf("item1"), result.map { it.stringValue() }) + assertFalse(expected.isList()) + } + + @Test + fun testGetItemValuePresentNormalisedMatchBothWays() { + // Arrange + val items = listOf( + mapOf("item name" to "item1", "itemPrice" to 10), + ) + val eventAdapter = EventAdapter("ItemName", emptyMap(), items) + val expected = TriggerValue("item1") + + // Act + val result = eventAdapter.getItemValue("itemName") + + // Assert + assertNotNull(result) + result.onEach { + assertNull(it.numberValue()) + assertNull(it.listValue()) + } + assertEquals(listOf("item1"), result.map { it.stringValue() }) + assertFalse(expected.isList()) + } + @Test fun testGetItemValueMissing() { // Arrange From c6e1056642749eaeccbc22e13a5db4cd63f4a0e6 Mon Sep 17 00:00:00 2001 From: CTLalit <144685420+CTLalit@users.noreply.github.com> Date: Fri, 29 Nov 2024 16:56:26 +0530 Subject: [PATCH 089/120] Release corev7.0.3 (#699) * feat(SDK-4183): updates docs for core SDK release 7.0.3 * feat(SDK-4163): updates the release date - changes title from bug fixes -> new features * feat(SDK-4163): updates the release date in changelog --- CHANGELOG.md | 3 +++ README.md | 6 +++--- docs/CTCORECHANGELOG.md | 6 ++++++ docs/CTGEOFENCE.md | 2 +- docs/CTPUSHTEMPLATES.md | 2 +- gradle/libs.versions.toml | 2 +- sample/build.gradle | 8 ++++---- templates/CTCORECHANGELOG.md | 6 ++++++ 8 files changed, 25 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 58c9b2992..38422b622 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## CHANGE LOG. +### November 29, 2024 +* [CleverTap Android SDK v7.0.3](docs/CTCORECHANGELOG.md) + ### October 10, 2024 * [CleverTap Android SDK v7.0.2](docs/CTCORECHANGELOG.md) diff --git a/README.md b/README.md index 12d731c94..d97d6fb99 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.2" + implementation "com.clevertap.android:clevertap-android-sdk:7.0.3" } ``` @@ -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.2", ext: 'aar') + implementation (name: "clevertap-android-sdk-7.0.3", ext: 'aar') } ``` @@ -46,7 +46,7 @@ Add the Firebase Messaging library and Android Support Library v4 as dependencie ```groovy dependencies { - implementation "com.clevertap.android:clevertap-android-sdk:7.0.2" + implementation "com.clevertap.android:clevertap-android-sdk:7.0.3" 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). diff --git a/docs/CTCORECHANGELOG.md b/docs/CTCORECHANGELOG.md index 44110c303..945381b49 100644 --- a/docs/CTCORECHANGELOG.md +++ b/docs/CTCORECHANGELOG.md @@ -1,5 +1,11 @@ ## CleverTap Android SDK CHANGE LOG +### Version 7.0.3 (November 29, 2024) + +#### New Features +* Changes campaign triggering evaluation of event names, event properties, and profile properties to ignore letter case and whitespace. +* Adds support for previewing in-apps created through the new dashboard advanced builder. + ### Version 7.0.2 (October 10, 2024) #### New Features diff --git a/docs/CTGEOFENCE.md b/docs/CTGEOFENCE.md index 52c26961b..9aaeaf173 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.2" // 3.9.0 and above +implementation "com.clevertap.android:clevertap-android-sdk:7.0.3" // 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 260eedeab..1936f78d7 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.2" // 4.4.0 and above +implementation "com.clevertap.android:clevertap-android-sdk:7.0.3" // 4.4.0 and above ``` 2. Add the following line to your Application class before the `onCreate()` diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f384b46e9..f98e6ef52 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.2" +clevertap_android_sdk = "7.0.3" clevertap_rendermax_sdk = "1.0.3" clevertap_geofence_sdk = "1.3.0" clevertap_hms_sdk = "1.3.4" diff --git a/sample/build.gradle b/sample/build.gradle index acdfe8dc8..51f5faff7 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -18,8 +18,8 @@ android { applicationId "com.clevertap.demo" minSdkVersion 21 targetSdkVersion 34 - versionCode 7000002 - versionName "7.0.2" + versionCode 7000003 + versionName "7.0.3" multiDexEnabled true testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } @@ -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.2") + remoteImplementation("com.clevertap.android:clevertap-android-sdk:7.0.3") 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.2") + stagingImplementation("com.clevertap.android:clevertap-android-sdk:7.0.3") 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 44110c303..945381b49 100644 --- a/templates/CTCORECHANGELOG.md +++ b/templates/CTCORECHANGELOG.md @@ -1,5 +1,11 @@ ## CleverTap Android SDK CHANGE LOG +### Version 7.0.3 (November 29, 2024) + +#### New Features +* Changes campaign triggering evaluation of event names, event properties, and profile properties to ignore letter case and whitespace. +* Adds support for previewing in-apps created through the new dashboard advanced builder. + ### Version 7.0.2 (October 10, 2024) #### New Features From 0e0e692f17a3245a005ce5876887879204c8ec96 Mon Sep 17 00:00:00 2001 From: anush Date: Wed, 4 Dec 2024 15:10:26 +0530 Subject: [PATCH 090/120] chore - Updates google-ads-sdk - Updates the sample app - Updates the docs in sdk --- README.md | 2 +- gradle/libs.versions.toml | 2 +- sample/build.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 12d731c94..c4f4f4ca0 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Add the Firebase Messaging library and Android Support Library v4 as dependencie implementation "com.clevertap.android:clevertap-android-sdk:7.0.2" 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). } ``` diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f384b46e9..b063c3544 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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/sample/build.gradle b/sample/build.gradle index acdfe8dc8..f493509a1 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -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") From e3d5787192037134e091e631e2a4db8f152b3498 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya <61137760+piyush-kukadiya@users.noreply.github.com> Date: Thu, 5 Dec 2024 12:07:24 +0530 Subject: [PATCH 091/120] Perform event/profile property normalization before querying DB MC-2432 (#706) * feat(multi_triggers) : add normalizedEventName column to userEventLogs table and replace eventName with normalizedEventName as primary key MC-2432 * feat(multi_triggers) : add normalizedEventName field to UserEventLog class MC-2432 * feat(multi_triggers) : refactor DAO methods to include normalizedEventName as part of sqlite queries, CRUD is now performed on normalizedEventName column instead of eventName MC-2432 * feat(multi_triggers) : update gradle and android gradle plugin MC-2432 * feat(multi_triggers) : fix multidex and duplicate resources error for test module MC-2432 * feat(multi_triggers) : fix matchFirstTimeOnly for profile properties MC-2432 * feat(multi_triggers) : fix broken CleverTapAPITest due to event normalization MC-2432 * feat(multi_triggers) : fix broken LocalDataStoreTest due to event normalization MC-2432 * feat(multi_triggers) : fix broken UserEventLogDAOImplTest due to event normalization MC-2432 * feat(multi_triggers) : add kdoc for UserEventLog MC-2432 * feat(multi_triggers) : add missing tests for first time profile properties in TriggersMatcherTest MC-2432 * feat(multi_triggers) : bump CleverTap core version to 8.0.0 for multi triggers testing MC-2432 --- .../clevertap/android/sdk/LocalDataStore.java | 88 +++-- .../clevertap/android/sdk/db/CtDatabase.kt | 5 +- .../sdk/inapp/evaluation/TriggersMatcher.kt | 3 +- .../android/sdk/usereventlogs/UserEventLog.kt | 17 + .../sdk/usereventlogs/UserEventLogDAO.kt | 21 +- .../sdk/usereventlogs/UserEventLogDAOImpl.kt | 81 ++-- .../clevertap/android/sdk/CleverTapAPITest.kt | 34 +- .../android/sdk/LocalDataStoreTest.kt | 136 +++---- .../inapp/evaluation/TriggersMatcherTest.kt | 42 ++ .../usereventlogs/UserEventLogDAOImplTest.kt | 359 ++++++++++-------- .../sdk/usereventlogs/UserEventLogTestData.kt | 70 ++++ gradle/libs.versions.toml | 4 +- gradle/wrapper/gradle-wrapper.properties | 2 +- test_shared/build.gradle | 8 + 14 files changed, 527 insertions(+), 343 deletions(-) create mode 100644 clevertap-core/src/test/java/com/clevertap/android/sdk/usereventlogs/UserEventLogTestData.kt 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 ab3e1f72b..102c678ff 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 @@ -32,6 +32,10 @@ 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 { @@ -53,6 +57,7 @@ public class LocalDataStore { private final DeviceInfo deviceInfo; private final Set userEventLogKeys = Collections.synchronizedSet(new HashSet<>()); + private final Map normalizedEventNames = new HashMap<>(); LocalDataStore(Context context, CleverTapInstanceConfig config, CryptHandler cryptHandler, DeviceInfo deviceInfo, BaseDatabaseManager baseDatabaseManager) { this.context = context; @@ -137,7 +142,10 @@ public void persistEvent(Context context, JSONObject event, int type) { } @WorkerThread public boolean persistUserEventLogsInBulk(Set eventNames){ - return upsertUserEventLogsInBulk(eventNames); + Set> setOfActualAndNormalizedEventNamePair = new HashSet<>(); + CollectionsKt.mapTo(eventNames, setOfActualAndNormalizedEventNamePair, + (actualEventName) -> new Pair<>(actualEventName, getOrPutNormalizedEventName(actualEventName))); + return upsertUserEventLogsInBulk(setOfActualAndNormalizedEventNamePair); } @WorkerThread @@ -193,64 +201,73 @@ public boolean persistUserEventLog(String eventName) { } @WorkerThread - private boolean updateEventByDeviceID(String deviceID, String eventName) { + private boolean updateEventByDeviceIdAndNormalizedEventName(String deviceID, String normalizedEventName) { DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context); - boolean updatedEventByDeviceID = dbAdapter.userEventLogDAO().updateEventByDeviceID(deviceID, eventName); - getConfigLogger().verbose("updatedEventByDeviceID = "+updatedEventByDeviceID); + boolean updatedEventByDeviceID = dbAdapter.userEventLogDAO().updateEventByDeviceIdAndNormalizedEventName(deviceID, normalizedEventName); + getConfigLogger().verbose("updatedEventByDeviceID = " + updatedEventByDeviceID); return updatedEventByDeviceID; } @WorkerThread public boolean updateUserEventLog(String eventName) { String deviceID = deviceInfo.getDeviceID(); - return updateEventByDeviceID(deviceID,eventName); + String normalizedEventName = getOrPutNormalizedEventName(eventName); + return updateEventByDeviceIdAndNormalizedEventName(deviceID, normalizedEventName); } @WorkerThread - public boolean upsertUserEventLogsInBulk(Set eventNames){ + private boolean upsertUserEventLogsInBulk(Set> setOfActualAndNormalizedEventNamePair) { String deviceID = deviceInfo.getDeviceID(); DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context); - boolean upsertEventByDeviceID = dbAdapter.userEventLogDAO().upsertEventsByDeviceID(deviceID, eventNames); - getConfigLogger().verbose("upsertEventByDeviceID = "+upsertEventByDeviceID); + boolean upsertEventByDeviceID = dbAdapter.userEventLogDAO() + .upsertEventsByDeviceIdAndNormalizedEventName(deviceID, setOfActualAndNormalizedEventNamePair); + getConfigLogger().verbose("upsertEventByDeviceID = " + upsertEventByDeviceID); return upsertEventByDeviceID; } @WorkerThread - private long insertEventByDeviceID(String deviceID, String eventName) { + private long insertEvent(String deviceID, String actualEventName, String normalizedEventName) { DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context); - long rowId = dbAdapter.userEventLogDAO().insertEventByDeviceID(deviceID, eventName); - getConfigLogger().verbose("inserted rowId = "+rowId); + 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(); - long rowId = insertEventByDeviceID(deviceID, eventName); + String normalizedEventName = getOrPutNormalizedEventName(eventName); + long rowId = insertEvent(deviceID, eventName, normalizedEventName); return rowId >= 0; } @WorkerThread - private boolean eventExistsByDeviceID(String deviceID, String eventName) { + private boolean eventExistsByDeviceIdAndNormalizedEventName(String deviceID, String normalizedEventName) { DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context); - boolean eventExistsByDeviceID = dbAdapter.userEventLogDAO().eventExistsByDeviceID(deviceID, eventName); - getConfigLogger().verbose("eventExistsByDeviceID = "+eventExistsByDeviceID); - return eventExistsByDeviceID; + boolean eventExists = dbAdapter.userEventLogDAO().eventExistsByDeviceIdAndNormalizedEventName(deviceID, normalizedEventName); + getConfigLogger().verbose("eventExists = "+eventExists); + return eventExists; } @WorkerThread public boolean isUserEventLogExists(String eventName) { String deviceID = deviceInfo.getDeviceID(); - return eventExistsByDeviceID(deviceID,eventName); + String normalizedEventName = getOrPutNormalizedEventName(eventName); + return eventExistsByDeviceIdAndNormalizedEventName(deviceID, normalizedEventName); } @WorkerThread - private boolean eventExistsByDeviceIDAndCount(String deviceID, String eventName, int count) { + private boolean eventExistsByDeviceIdAndNormalizedEventNameAndCount(String deviceID, String normalizedEventName, int count) { DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context); boolean eventExistsByDeviceIDAndCount = dbAdapter.userEventLogDAO() - .eventExistsByDeviceIDAndCount(deviceID, eventName, count); + .eventExistsByDeviceIdAndNormalizedEventNameAndCount(deviceID, normalizedEventName, count); - getConfigLogger().verbose("eventExistsByDeviceIDAndCount = "+eventExistsByDeviceIDAndCount); + getConfigLogger().verbose("eventExistsByDeviceIDAndCount = " + eventExistsByDeviceIDAndCount); return eventExistsByDeviceIDAndCount; } @@ -261,8 +278,9 @@ public boolean isUserEventLogFirstTime(String eventName) { } String deviceID = deviceInfo.getDeviceID(); + String normalizedEventName = getOrPutNormalizedEventName(eventName); - int count = readEventCountByDeviceID(deviceID, eventName); + int count = readEventCountByDeviceIdAndNormalizedEventName(deviceID, normalizedEventName); if (count > 1) { userEventLogKeys.add(eventName); } @@ -278,51 +296,55 @@ public boolean cleanUpExtraEvents(int threshold, int numberOfRowsToCleanup){ } @WorkerThread - private UserEventLog readEventByDeviceID(String deviceID, String eventName) { + private UserEventLog readEventByDeviceIdAndNormalizedEventName(String deviceID, String normalizedEventName) { DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context); - return dbAdapter.userEventLogDAO().readEventByDeviceID(deviceID, eventName); + return dbAdapter.userEventLogDAO().readEventByDeviceIdAndNormalizedEventName(deviceID, normalizedEventName); } @WorkerThread public UserEventLog readUserEventLog(String eventName) { String deviceID = deviceInfo.getDeviceID(); - return readEventByDeviceID(deviceID,eventName); + String normalizedEventName = getOrPutNormalizedEventName(eventName); + return readEventByDeviceIdAndNormalizedEventName(deviceID, normalizedEventName); } @WorkerThread - private int readEventCountByDeviceID(String deviceID, String eventName) { + private int readEventCountByDeviceIdAndNormalizedEventName(String deviceID, String normalizedEventName) { DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context); - return dbAdapter.userEventLogDAO().readEventCountByDeviceID(deviceID, eventName); + return dbAdapter.userEventLogDAO().readEventCountByDeviceIdAndNormalizedEventName(deviceID, normalizedEventName); } @WorkerThread public int readUserEventLogCount(String eventName) { String deviceID = deviceInfo.getDeviceID(); - return readEventCountByDeviceID(deviceID,eventName); + String normalizedEventName = getOrPutNormalizedEventName(eventName); + return readEventCountByDeviceIdAndNormalizedEventName(deviceID, normalizedEventName); } @WorkerThread - private long readEventLastTsByDeviceID(String deviceID, String eventName) { + private long readEventLastTsByDeviceIdAndNormalizedEventName(String deviceID, String normalizedEventName) { DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context); - return dbAdapter.userEventLogDAO().readEventLastTsByDeviceID(deviceID, eventName); + return dbAdapter.userEventLogDAO().readEventLastTsByDeviceIdAndNormalizedEventName(deviceID, normalizedEventName); } @WorkerThread public long readUserEventLogLastTs(String eventName) { String deviceID = deviceInfo.getDeviceID(); - return readEventLastTsByDeviceID(deviceID,eventName); + String normalizedEventName = getOrPutNormalizedEventName(eventName); + return readEventLastTsByDeviceIdAndNormalizedEventName(deviceID, normalizedEventName); } @WorkerThread - private long readEventFirstTsByDeviceID(String deviceID, String eventName) { + private long readEventFirstTsByDeviceIdAndNormalizedEventName(String deviceID, String normalizedEventName) { DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context); - return dbAdapter.userEventLogDAO().readEventFirstTsByDeviceID(deviceID, eventName); + return dbAdapter.userEventLogDAO().readEventFirstTsByDeviceIdAndNormalizedEventName(deviceID, normalizedEventName); } @WorkerThread public long readUserEventLogFirstTs(String eventName) { String deviceID = deviceInfo.getDeviceID(); - return readEventFirstTsByDeviceID(deviceID,eventName); + String normalizedEventName = getOrPutNormalizedEventName(eventName); + return readEventFirstTsByDeviceIdAndNormalizedEventName(deviceID,normalizedEventName); } @WorkerThread 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 f38402286..1cdaf76bd 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 @@ -219,6 +219,7 @@ object Column { 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" @@ -235,12 +236,12 @@ private val CREATE_EVENTS_TABLE = """ private val CREATE_USER_EVENT_LOGS_TABLE = """ CREATE TABLE ${Table.USER_EVENT_LOGS_TABLE.tableName} ( ${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, ${Column.DEVICE_ID} STRING NOT NULL, - PRIMARY KEY (${Column.EVENT_NAME}, ${Column.DEVICE_ID}) - + PRIMARY KEY (${Column.NORMALIZED_EVENT_NAME}, ${Column.DEVICE_ID}) ); """ 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 c9d87a9e5..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 @@ -80,7 +80,8 @@ class TriggersMatcher(private val localDataStore: LocalDataStore) { if (!trigger.firstTimeOnly) { return true } - return localDataStore.isUserEventLogFirstTime(trigger.eventName) + val keyToCheckFirstTime: String = trigger.profileAttrName ?: trigger.eventName + return localDataStore.isUserEventLogFirstTime(keyToCheckFirstTime) } private fun matchPropertyConditions( 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 index 3dc51d99d..88ef522eb 100644 --- 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 @@ -1,7 +1,24 @@ 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 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 index cb4129e95..c910f3354 100644 --- 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 @@ -6,38 +6,41 @@ interface UserEventLogDAO { // Insert a new event by deviceID @WorkerThread - fun insertEventByDeviceID(deviceID: String, eventName: String): Long + fun insertEvent(deviceID: String, eventName: String, normalizedEventName: String): Long // Update an event by deviceID @WorkerThread - fun updateEventByDeviceID(deviceID: String, eventName: String): Boolean + fun updateEventByDeviceIdAndNormalizedEventName(deviceID: String, normalizedEventName: String): Boolean @WorkerThread - fun upsertEventsByDeviceID(deviceID: String, eventNameList: Set): Boolean + fun upsertEventsByDeviceIdAndNormalizedEventName( + deviceID: String, + setOfActualAndNormalizedEventNamePair: Set> + ): Boolean // Read an event by deviceID @WorkerThread - fun readEventByDeviceID(deviceID: String, eventName: String): UserEventLog? + fun readEventByDeviceIdAndNormalizedEventName(deviceID: String, normalizedEventName: String): UserEventLog? // Read an event count by deviceID @WorkerThread - fun readEventCountByDeviceID(deviceID: String, eventName: String): Int + fun readEventCountByDeviceIdAndNormalizedEventName(deviceID: String, normalizedEventName: String): Int // Read an event firstTs by deviceID @WorkerThread - fun readEventFirstTsByDeviceID(deviceID: String, eventName: String): Long + fun readEventFirstTsByDeviceIdAndNormalizedEventName(deviceID: String, normalizedEventName: String): Long // Read an event lastTs by deviceID @WorkerThread - fun readEventLastTsByDeviceID(deviceID: String, eventName: String): Long + fun readEventLastTsByDeviceIdAndNormalizedEventName(deviceID: String, normalizedEventName: String): Long // Check if an event exists by deviceID @WorkerThread - fun eventExistsByDeviceID(deviceID: String, eventName: String): Boolean + fun eventExistsByDeviceIdAndNormalizedEventName(deviceID: String, normalizedEventName: String): Boolean // Check if an event exists by deviceID and count @WorkerThread - fun eventExistsByDeviceIDAndCount(deviceID: String, eventName: String, count: Int): Boolean + fun eventExistsByDeviceIdAndNormalizedEventNameAndCount(deviceID: String, normalizedEventName: String, count: Int): Boolean // Get all events for a particular deviceID @WorkerThread 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 index e12f309ee..441dcc219 100644 --- 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 @@ -19,9 +19,13 @@ internal class UserEventLogDAOImpl( private val table: Table ) : UserEventLogDAO { - + // Replace multiple params with single POJO param if param length increases @WorkerThread - override fun insertEventByDeviceID(deviceID: String, eventName: String): Long { + 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 @@ -31,6 +35,7 @@ internal class UserEventLogDAOImpl( 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) @@ -51,7 +56,7 @@ internal class UserEventLogDAOImpl( } @WorkerThread - override fun updateEventByDeviceID(deviceID: String, eventName: String): Boolean { + override fun updateEventByDeviceIdAndNormalizedEventName(deviceID: String, normalizedEventName: String): Boolean { val tableName = table.tableName val now = Utils.getNowInMillis() @@ -62,11 +67,11 @@ internal class UserEventLogDAOImpl( ${Column.COUNT} = ${Column.COUNT} + 1, ${Column.LAST_TS} = ? WHERE ${Column.DEVICE_ID} = ? - AND ${Column.EVENT_NAME} = ?; + AND ${Column.NORMALIZED_EVENT_NAME} = ?; """.trimIndent() - logger.verbose("Updating event $eventName with deviceID = $deviceID in $tableName") - db.writableDatabase.execSQL(query, arrayOf(now, deviceID, eventName)) + 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) @@ -75,18 +80,21 @@ internal class UserEventLogDAOImpl( } @WorkerThread - override fun upsertEventsByDeviceID(deviceID: String, eventNameList: Set): Boolean { + 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() - eventNameList.forEach { - if (eventExistsByDeviceID(deviceID, it)) { + setOfActualAndNormalizedEventNamePair.forEach { + if (eventExistsByDeviceIdAndNormalizedEventName(deviceID, it.second)) { logger.verbose("UserEventLog: Updating EventLog for event $it") - updateEventByDeviceID(deviceID, it) + updateEventByDeviceIdAndNormalizedEventName(deviceID, it.second) } else { logger.verbose("UserEventLog: Inserting EventLog for event $it") - insertEventByDeviceID(deviceID, it) + insertEvent(deviceID, it.first, it.second) } } db.writableDatabase.setTransactionSuccessful() @@ -104,10 +112,10 @@ internal class UserEventLogDAOImpl( } @WorkerThread - override fun readEventByDeviceID(deviceID: String, eventName: String): UserEventLog? { + override fun readEventByDeviceIdAndNormalizedEventName(deviceID: String, normalizedEventName: String): UserEventLog? { val tName = table.tableName - val selection = "${Column.DEVICE_ID} = ? AND ${Column.EVENT_NAME} = ?" - val selectionArgs = arrayOf(deviceID, eventName) + 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 @@ -115,6 +123,7 @@ internal class UserEventLogDAOImpl( 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)), @@ -132,10 +141,10 @@ internal class UserEventLogDAOImpl( } @WorkerThread - override fun readEventCountByDeviceID(deviceID: String, eventName: String): Int = - readEventColumnByDeviceID( + override fun readEventCountByDeviceIdAndNormalizedEventName(deviceID: String, normalizedEventName: String): Int = + readEventColumnByDeviceIdAndNormalizedEventName( deviceID, - eventName, + normalizedEventName, Column.COUNT, defaultValueExtractor = { -1 }, valueExtractor = { cursor, columnName -> @@ -145,10 +154,10 @@ internal class UserEventLogDAOImpl( @WorkerThread - override fun readEventFirstTsByDeviceID(deviceID: String, eventName: String): Long = - readEventColumnByDeviceID( + override fun readEventFirstTsByDeviceIdAndNormalizedEventName(deviceID: String, normalizedEventName: String): Long = + readEventColumnByDeviceIdAndNormalizedEventName( deviceID, - eventName, + normalizedEventName, Column.FIRST_TS, defaultValueExtractor = { -1L }, valueExtractor = { cursor, columnName -> @@ -157,10 +166,10 @@ internal class UserEventLogDAOImpl( ) @WorkerThread - override fun readEventLastTsByDeviceID(deviceID: String, eventName: String): Long = - readEventColumnByDeviceID( + override fun readEventLastTsByDeviceIdAndNormalizedEventName(deviceID: String, normalizedEventName: String): Long = + readEventColumnByDeviceIdAndNormalizedEventName( deviceID, - eventName, + normalizedEventName, Column.LAST_TS, defaultValueExtractor = { -1L }, valueExtractor = { cursor, columnName -> @@ -169,10 +178,10 @@ internal class UserEventLogDAOImpl( ) @WorkerThread - override fun eventExistsByDeviceID(deviceID: String, eventName: String): Boolean { + override fun eventExistsByDeviceIdAndNormalizedEventName(deviceID: String, normalizedEventName: String): Boolean { val tName = table.tableName - val selection = "${Column.DEVICE_ID} = ? AND ${Column.EVENT_NAME} = ?" - val selectionArgs = arrayOf(deviceID, eventName) + val selection = "${Column.DEVICE_ID} = ? AND ${Column.NORMALIZED_EVENT_NAME} = ?" + val selectionArgs = arrayOf(deviceID, normalizedEventName) val resultColumn = "eventExists" val query = """ @@ -198,10 +207,10 @@ internal class UserEventLogDAOImpl( } @WorkerThread - override fun eventExistsByDeviceIDAndCount(deviceID: String, eventName: String, count: Int): Boolean { + override fun eventExistsByDeviceIdAndNormalizedEventNameAndCount(deviceID: String, normalizedEventName: String, count: Int): Boolean { val tName = table.tableName - val selection = "${Column.DEVICE_ID} = ? AND ${Column.EVENT_NAME} = ? AND ${Column.COUNT} = ?" - val selectionArgs = arrayOf(deviceID, eventName, count.toString()) + 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 = """ @@ -241,6 +250,7 @@ internal class UserEventLogDAOImpl( 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)), @@ -270,6 +280,7 @@ internal class UserEventLogDAOImpl( 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)), @@ -308,8 +319,8 @@ internal class UserEventLogDAOImpl( // When above threshold is reached, delete in such a way that (threshold - numberOfRowsToCleanup) rows exists after cleanup val query = """ DELETE FROM $tName - WHERE (${Column.EVENT_NAME}, ${Column.DEVICE_ID}) IN ( - SELECT ${Column.EVENT_NAME}, ${Column.DEVICE_ID} + 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 ( @@ -333,16 +344,16 @@ internal class UserEventLogDAOImpl( } @WorkerThread - private fun readEventColumnByDeviceID( + private fun readEventColumnByDeviceIdAndNormalizedEventName( deviceID: String, - eventName: String, + normalizedEventName: String, column: String, defaultValueExtractor: () -> T, valueExtractor: (cursor: Cursor, columnName: String) -> T ): T { val tName = table.tableName - val selection = "${Column.DEVICE_ID} = ? AND ${Column.EVENT_NAME} = ?" - val selectionArgs = arrayOf(deviceID, eventName) + val selection = "${Column.DEVICE_ID} = ? AND ${Column.NORMALIZED_EVENT_NAME} = ?" + val selectionArgs = arrayOf(deviceID, normalizedEventName) val projection = arrayOf(column) return try { 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 e85aac5ff..b1f9d7b69 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,7 +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.UserEventLog +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 @@ -469,7 +469,7 @@ class CleverTapAPITest : BaseTestCase() { @Test fun `test getUserEventLogCount`() { // Arrange - val evt = "test" + val evt = UserEventLogTestData.EventNames.TEST_EVENT `when`(corestate.localDataStore.readUserEventLogCount(evt)).thenReturn(1) // Act @@ -486,8 +486,8 @@ class CleverTapAPITest : BaseTestCase() { @Test fun `test getUserEventLog`() { // Arrange - val evt = "test" - val log = UserEventLog(evt, 1000L, 1000L, 1, "dId") + val evt = UserEventLogTestData.EventNames.TEST_EVENT + val log = UserEventLogTestData.EventNames.sampleUserEventLogsForSameDeviceId[0] `when`(corestate.localDataStore.readUserEventLog(evt)).thenReturn(log) // Act @@ -504,8 +504,11 @@ class CleverTapAPITest : BaseTestCase() { @Test fun `test getUserEventLogFirstTs`() { // Arrange - val evt = "test" - `when`(corestate.localDataStore.readUserEventLogFirstTs(evt)).thenReturn(1000L) + val evt = UserEventLogTestData.EventNames.TEST_EVENT + val firstTsExpected = UserEventLogTestData.TestTimestamps.SAMPLE_TIMESTAMP + `when`(corestate.localDataStore.readUserEventLogFirstTs(evt)).thenReturn( + firstTsExpected + ) // Act executeBasicTest { @@ -513,7 +516,7 @@ class CleverTapAPITest : BaseTestCase() { val firstTsActual = cleverTapAPI.getUserEventLogFirstTs(evt) // Assert - assertEquals(1000L, firstTsActual) + assertEquals(firstTsExpected, firstTsActual) verify(corestate.localDataStore).readUserEventLogFirstTs(evt) } } @@ -521,8 +524,9 @@ class CleverTapAPITest : BaseTestCase() { @Test fun `test getUserEventLogLastTs`() { // Arrange - val evt = "test" - `when`(corestate.localDataStore.readUserEventLogLastTs(evt)).thenReturn(1000L) + val evt = UserEventLogTestData.EventNames.TEST_EVENT + val lastTsExpected = UserEventLogTestData.TestTimestamps.SAMPLE_TIMESTAMP + `when`(corestate.localDataStore.readUserEventLogLastTs(evt)).thenReturn(lastTsExpected) // Act executeBasicTest { @@ -530,7 +534,7 @@ class CleverTapAPITest : BaseTestCase() { val lastTsActual = cleverTapAPI.getUserEventLogLastTs(evt) // Assert - assertEquals(1000L, lastTsActual) + assertEquals(lastTsExpected, lastTsActual) verify(corestate.localDataStore).readUserEventLogLastTs(evt) } @@ -539,10 +543,7 @@ class CleverTapAPITest : BaseTestCase() { @Test fun `test getUserEventLogHistory`() { // Arrange - val logs = listOf( - UserEventLog("z", 1000L, 1000L, 1, "dId"), - UserEventLog("a", 2000L, 2000L, 2, "dId") - ) + val logs = UserEventLogTestData.EventNames.sampleUserEventLogsForSameDeviceId `when`(corestate.localDataStore.readUserEventLogs()).thenReturn(logs) // Act @@ -560,8 +561,9 @@ class CleverTapAPITest : BaseTestCase() { @Test fun `test getUserLastVisitTs`() { + val expectedUserLastVisitTs = UserEventLogTestData.TestTimestamps.SAMPLE_TIMESTAMP // Arrange - `when`(corestate.sessionManager.userLastVisitTs).thenReturn(1000L) + `when`(corestate.sessionManager.userLastVisitTs).thenReturn(expectedUserLastVisitTs) // Act executeBasicTest { @@ -569,7 +571,7 @@ class CleverTapAPITest : BaseTestCase() { val lastVisitTsActual = cleverTapAPI.userLastVisitTs // Assert - assertEquals(1000L, lastVisitTsActual) + assertEquals(expectedUserLastVisitTs, lastVisitTsActual) verify(corestate.sessionManager).userLastVisitTs } } 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 c83c66c84..6a845aa81 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 @@ -9,6 +9,7 @@ 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.Test @@ -32,6 +33,11 @@ class LocalDataStoreTest : BaseTestCase() { 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() @@ -118,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"]) @@ -308,16 +313,15 @@ class LocalDataStoreTest : BaseTestCase() { // Then assertFalse(result) Mockito.verify(dbAdapter, Mockito.never()).userEventLogDAO() - Mockito.verify(userEventLogDaoMock, Mockito.never()).eventExistsByDeviceID(anyString(), anyString()) + Mockito.verify(userEventLogDaoMock, Mockito.never()).eventExistsByDeviceIdAndNormalizedEventName(anyString(), anyString()) } @Test fun `test persistUserEventLog when event exists updates event successfully`() { // Given - val eventName = "test_event" - Mockito.`when`(userEventLogDaoMock.eventExistsByDeviceID(deviceInfo.deviceID, eventName)) + Mockito.`when`(userEventLogDaoMock.eventExistsByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName)) .thenReturn(true) - Mockito.`when`(userEventLogDaoMock.updateEventByDeviceID(deviceInfo.deviceID, eventName)) + Mockito.`when`(userEventLogDaoMock.updateEventByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName)) .thenReturn(true) // When @@ -325,18 +329,17 @@ class LocalDataStoreTest : BaseTestCase() { // Then assertTrue(result) - Mockito.verify(userEventLogDaoMock).eventExistsByDeviceID(deviceInfo.deviceID, eventName) - Mockito.verify(userEventLogDaoMock).updateEventByDeviceID(deviceInfo.deviceID, eventName) - Mockito.verify(userEventLogDaoMock, Mockito.never()).insertEventByDeviceID(anyString(), anyString()) + 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 - val eventName = "test_event" - Mockito.`when`(userEventLogDaoMock.eventExistsByDeviceID(deviceInfo.deviceID, eventName)) + Mockito.`when`(userEventLogDaoMock.eventExistsByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName)) .thenReturn(true) - Mockito.`when`(userEventLogDaoMock.updateEventByDeviceID(deviceInfo.deviceID, eventName)) + Mockito.`when`(userEventLogDaoMock.updateEventByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName)) .thenReturn(false) // When @@ -349,10 +352,9 @@ class LocalDataStoreTest : BaseTestCase() { @Test fun `test persistUserEventLog when event does not exist inserts successfully`() { // Given - val eventName = "test_event" - Mockito.`when`(userEventLogDaoMock.eventExistsByDeviceID(deviceInfo.deviceID, eventName)) + Mockito.`when`(userEventLogDaoMock.eventExistsByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName)) .thenReturn(false) - Mockito.`when`(userEventLogDaoMock.insertEventByDeviceID(deviceInfo.deviceID, eventName)) + Mockito.`when`(userEventLogDaoMock.insertEvent(deviceInfo.deviceID, eventName, normalizedEventName)) .thenReturn(1L) // When @@ -360,18 +362,17 @@ class LocalDataStoreTest : BaseTestCase() { // Then assertTrue(result) - Mockito.verify(userEventLogDaoMock).eventExistsByDeviceID(deviceInfo.deviceID, eventName) - Mockito.verify(userEventLogDaoMock).insertEventByDeviceID(deviceInfo.deviceID, eventName) - Mockito.verify(userEventLogDaoMock, Mockito.never()).updateEventByDeviceID(anyString(), anyString()) + 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 - val eventName = "test_event" - Mockito.`when`(userEventLogDaoMock.eventExistsByDeviceID(deviceInfo.deviceID, eventName)) + Mockito.`when`(userEventLogDaoMock.eventExistsByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName)) .thenReturn(false) - Mockito.`when`(userEventLogDaoMock.insertEventByDeviceID(deviceInfo.deviceID, eventName)) + Mockito.`when`(userEventLogDaoMock.insertEvent(deviceInfo.deviceID, eventName, normalizedEventName)) .thenReturn(-1L) // When @@ -384,8 +385,7 @@ class LocalDataStoreTest : BaseTestCase() { @Test fun `test persistUserEventLog when exception occurs returns false`() { // Given - val eventName = "test_event" - Mockito.`when`(userEventLogDaoMock.eventExistsByDeviceID(deviceInfo.deviceID, eventName)) + Mockito.`when`(userEventLogDaoMock.eventExistsByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName)) .thenThrow(RuntimeException("DB Error")) // When @@ -398,8 +398,7 @@ class LocalDataStoreTest : BaseTestCase() { @Test fun `test persistUserEventLogsInBulk success`() { // Given - val eventNames = setOf("event1", "event2") - Mockito.`when`(userEventLogDaoMock.upsertEventsByDeviceID(deviceInfo.deviceID, eventNames)) + Mockito.`when`(userEventLogDaoMock.upsertEventsByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, setOfActualAndNormalizedEventNamePair)) .thenReturn(true) // When @@ -407,14 +406,13 @@ class LocalDataStoreTest : BaseTestCase() { // Then assertTrue(result) - Mockito.verify(userEventLogDaoMock).upsertEventsByDeviceID(deviceInfo.deviceID, eventNames) + Mockito.verify(userEventLogDaoMock).upsertEventsByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, setOfActualAndNormalizedEventNamePair) } @Test fun `test persistUserEventLogsInBulk when operation fails returns false`() { // Given - val eventNames = setOf("event1", "event2") - Mockito.`when`(userEventLogDaoMock.upsertEventsByDeviceID(deviceInfo.deviceID, eventNames)) + Mockito.`when`(userEventLogDaoMock.upsertEventsByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, setOfActualAndNormalizedEventNamePair)) .thenReturn(false) // When @@ -422,14 +420,13 @@ class LocalDataStoreTest : BaseTestCase() { // Then assertFalse(result) - Mockito.verify(userEventLogDaoMock).upsertEventsByDeviceID(deviceInfo.deviceID, eventNames) + Mockito.verify(userEventLogDaoMock).upsertEventsByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, setOfActualAndNormalizedEventNamePair) } @Test fun `test isUserEventLogFirstTime when count is 1 returns true`() { // Given - val eventName = "test_event" - Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceID(deviceInfo.deviceID, eventName)) + Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName)) .thenReturn(1) // When @@ -437,14 +434,13 @@ class LocalDataStoreTest : BaseTestCase() { // Then assertTrue(result) - Mockito.verify(userEventLogDaoMock).readEventCountByDeviceID(deviceInfo.deviceID, eventName) + Mockito.verify(userEventLogDaoMock).readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName) } @Test fun `test isUserEventLogFirstTime when count is greater than 1 returns false`() { // Given - val eventName = "test_event" - Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceID(deviceInfo.deviceID, eventName)) + Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName)) .thenReturn(2) // When @@ -452,14 +448,13 @@ class LocalDataStoreTest : BaseTestCase() { // Then assertFalse(result) - Mockito.verify(userEventLogDaoMock).readEventCountByDeviceID(deviceInfo.deviceID, eventName) + Mockito.verify(userEventLogDaoMock).readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName) } @Test fun `test isUserEventLogFirstTime caches result for subsequent calls`() { // Given - val eventName = "test_event" - Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceID(deviceInfo.deviceID, eventName)) + Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName)) .thenReturn(2) // When @@ -471,14 +466,13 @@ class LocalDataStoreTest : BaseTestCase() { assertFalse(secondCall) // Should only call readEventCountByDeviceID once as result is cached Mockito.verify(userEventLogDaoMock, Mockito.times(1)) - .readEventCountByDeviceID(deviceInfo.deviceID, eventName) + .readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName) } @Test fun `test isUserEventLogFirstTime when count is 0 returns false`() { // Given - val eventName = "test_event" - Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceID(deviceInfo.deviceID, eventName)) + Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName)) .thenReturn(0) // When @@ -486,14 +480,13 @@ class LocalDataStoreTest : BaseTestCase() { // Then assertFalse(result) - Mockito.verify(userEventLogDaoMock).readEventCountByDeviceID(deviceInfo.deviceID, eventName) + Mockito.verify(userEventLogDaoMock).readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName) } @Test fun `test isUserEventLogFirstTime when count is -1 returns false`() { // Given - val eventName = "test_event" - Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceID(deviceInfo.deviceID, eventName)) + Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName)) .thenReturn(-1) // When @@ -501,16 +494,15 @@ class LocalDataStoreTest : BaseTestCase() { // Then assertFalse(result) - Mockito.verify(userEventLogDaoMock).readEventCountByDeviceID(deviceInfo.deviceID, eventName) + Mockito.verify(userEventLogDaoMock).readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName) } @Test fun `test isUserEventLogFirstTime behavior with changing event counts`() { // Given - val eventName = "test_event" // First call setup - count 0 - Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceID(deviceInfo.deviceID, eventName)) + Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName)) .thenReturn(0) // When - First call @@ -518,10 +510,10 @@ class LocalDataStoreTest : BaseTestCase() { // Then assertFalse(firstCallResult) - Mockito.verify(userEventLogDaoMock).readEventCountByDeviceID(deviceInfo.deviceID, eventName) + Mockito.verify(userEventLogDaoMock).readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName) // Given - Second call setup - count 1 - Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceID(deviceInfo.deviceID, eventName)) + Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName)) .thenReturn(1) // When - Second call @@ -530,10 +522,10 @@ class LocalDataStoreTest : BaseTestCase() { // Then assertTrue(secondCallResult) Mockito.verify(userEventLogDaoMock, Mockito.times(2)) - .readEventCountByDeviceID(deviceInfo.deviceID, eventName) + .readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName) // Given - Third call setup - count 2 - Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceID(deviceInfo.deviceID, eventName)) + Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName)) .thenReturn(2) // When - Third call @@ -542,7 +534,7 @@ class LocalDataStoreTest : BaseTestCase() { // Then assertFalse(thirdCallResult) Mockito.verify(userEventLogDaoMock, Mockito.times(3)) - .readEventCountByDeviceID(deviceInfo.deviceID, eventName) + .readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName) // When - Fourth call (should use cached result) val fourthCallResult = localDataStoreWithConfig.isUserEventLogFirstTime(eventName) @@ -551,7 +543,7 @@ class LocalDataStoreTest : BaseTestCase() { assertFalse(fourthCallResult) // Should not make additional DB call as result is now cached Mockito.verify(userEventLogDaoMock, Mockito.times(3)) - .readEventCountByDeviceID(deviceInfo.deviceID, eventName) + .readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName) } @Test @@ -573,9 +565,8 @@ class LocalDataStoreTest : BaseTestCase() { @Test fun `test readUserEventLog success`() { // Given - val eventName = "test_event" - val userEventLog = UserEventLog(eventName, 123L, 456L, 1, deviceInfo.deviceID) - Mockito.`when`(userEventLogDaoMock.readEventByDeviceID(deviceInfo.deviceID, eventName)) + val userEventLog = UserEventLogTestData.EventNames.sampleUserEventLogsForSameDeviceId[0] + Mockito.`when`(userEventLogDaoMock.readEventByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName)) .thenReturn(userEventLog) // When @@ -584,15 +575,14 @@ class LocalDataStoreTest : BaseTestCase() { // Then assertNotNull(result) assertEquals(userEventLog, result) - Mockito.verify(userEventLogDaoMock).readEventByDeviceID(deviceInfo.deviceID, eventName) + Mockito.verify(userEventLogDaoMock).readEventByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName) } @Test fun `test readUserEventLogCount returns correct count when event exists`() { // Given - val eventName = "test_event" val expectedCount = 5 - Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceID(deviceInfo.deviceID, eventName)) + Mockito.`when`(userEventLogDaoMock.readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName)) .thenReturn(expectedCount) // When @@ -600,18 +590,17 @@ class LocalDataStoreTest : BaseTestCase() { // Then assertEquals(expectedCount, result) - Mockito.verify(userEventLogDaoMock).readEventCountByDeviceID(deviceInfo.deviceID, eventName) + Mockito.verify(userEventLogDaoMock).readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName) } @Test fun `test readUserEventLogLastTs returns correct timestamp when event exists`() { // Given - val eventName = "test_event" - val expectedTs = 1234567890L + val expectedTs = UserEventLogTestData.TestTimestamps.SAMPLE_TIMESTAMP Mockito.`when`( - userEventLogDaoMock.readEventLastTsByDeviceID( + userEventLogDaoMock.readEventLastTsByDeviceIdAndNormalizedEventName( deviceInfo.deviceID, - eventName + normalizedEventName ) ) .thenReturn(expectedTs) @@ -622,18 +611,17 @@ class LocalDataStoreTest : BaseTestCase() { // Then assertEquals(expectedTs, result) Mockito.verify(userEventLogDaoMock) - .readEventLastTsByDeviceID(deviceInfo.deviceID, eventName) + .readEventLastTsByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName) } @Test fun `test readUserEventLogFirstTs returns correct timestamp when event exists`() { // Given - val eventName = "test_event" - val expectedTs = 1234567890L + val expectedTs = UserEventLogTestData.TestTimestamps.SAMPLE_TIMESTAMP Mockito.`when`( - userEventLogDaoMock.readEventFirstTsByDeviceID( + userEventLogDaoMock.readEventFirstTsByDeviceIdAndNormalizedEventName( deviceInfo.deviceID, - eventName + normalizedEventName ) ) .thenReturn(expectedTs) @@ -644,16 +632,13 @@ class LocalDataStoreTest : BaseTestCase() { // Then assertEquals(expectedTs, result) Mockito.verify(userEventLogDaoMock) - .readEventFirstTsByDeviceID(deviceInfo.deviceID, eventName) + .readEventFirstTsByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName) } @Test fun `test readUserEventLogs returns correct event list for device`() { // Given - val expectedLogs = listOf( - UserEventLog("event1", 1L, 2L, 1, deviceInfo.deviceID), - UserEventLog("event2", 3L, 4L, 2, deviceInfo.deviceID) - ) + val expectedLogs = UserEventLogTestData.EventNames.sampleUserEventLogsForSameDeviceId Mockito.`when`(userEventLogDaoMock.allEventsByDeviceID(deviceInfo.deviceID)) .thenReturn(expectedLogs) @@ -668,10 +653,7 @@ class LocalDataStoreTest : BaseTestCase() { @Test fun `test readEventLogsForAllUsers returns correct event list`() { // Given - val expectedLogs = listOf( - UserEventLog("event1", 1L, 2L, 1, "user1"), - UserEventLog("event2", 3L, 4L, 2, "user2") - ) + val expectedLogs = UserEventLogTestData.EventNames.sampleUserEventLogsForMixedDeviceId Mockito.`when`(userEventLogDaoMock.allEvents()) .thenReturn(expectedLogs) 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 2189269d2..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 @@ -1312,6 +1312,27 @@ class TriggersMatcherTest : BaseTestCase() { 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 @@ -1330,6 +1351,27 @@ class TriggersMatcherTest : BaseTestCase() { 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 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 index ea2f351b5..0c137f80b 100644 --- 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 @@ -37,11 +37,17 @@ class UserEventLogDAOImplTest { 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_DEVICE_ID = "test_device_id" - private const val TEST_EVENT_NAME = "test_event" - private const val TEST_EVENT_NAME_2 = "test_event_2" + 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 } @@ -68,31 +74,31 @@ class UserEventLogDAOImplTest { } @Test - fun `test insertEventByDeviceID when below memory threshold`() { + fun `test insertEvent when below memory threshold`() { // When - val result = userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + val result = userEventLogDAO.insertEvent(testDeviceId, testEventName, testEventNameNormalized) // Then assertTrue(result > 0) } @Test - fun `test insertEventByDeviceID when above memory threshold`() { + 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.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + val result = dao.insertEvent(testDeviceId, testEventName, testEventNameNormalized) // Then assertEquals(-2L, result) // DB_OUT_OF_MEMORY_ERROR } @Test - fun `test insertEventByDeviceID when db error occurs`() { + fun `test insertEvent when db error occurs`() { // Given val dbHelper = mockk(relaxed = true) every { dbHelper.belowMemThreshold() } returns true @@ -106,7 +112,7 @@ class UserEventLogDAOImplTest { val dao = UserEventLogDAOImpl(dbHelper, logger, table) // When - val result = dao.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + val result = dao.insertEvent(testDeviceId, testEventName, testEventNameNormalized) // Then assertEquals(-1L, result) // DB_UPDATE_ERROR @@ -118,26 +124,26 @@ class UserEventLogDAOImplTest { } @Test - fun `test updateEventByDeviceID success`() { + fun `test updateEventByDeviceIdAndNormalizedEventName success`() { // Given - val insertResult = userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + val insertResult = userEventLogDAO.insertEvent(testDeviceId,testEventName, testEventNameNormalized) assertTrue(insertResult > 0) // When - val updateResult1 = userEventLogDAO.updateEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) - val updateResult2 = userEventLogDAO.updateEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + val updateResult1 = userEventLogDAO.updateEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) + val updateResult2 = userEventLogDAO.updateEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) // Then assertTrue(updateResult1) assertTrue(updateResult2) // Verify count increased - val count = userEventLogDAO.readEventCountByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + val count = userEventLogDAO.readEventCountByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) assertEquals(3, count) } @Test - fun `test updateEventByDeviceID when db error occurs`() { + fun `test updateEventByDeviceIdAndNormalizedEventName when db error occurs`() { // Given val dbHelper = mockk(relaxed = true) every { @@ -150,29 +156,29 @@ class UserEventLogDAOImplTest { val dao = UserEventLogDAOImpl(dbHelper, logger, table) // When - val result = dao.updateEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + val result = dao.updateEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) // Then assertFalse(result) } @Test - fun `test updateEventByDeviceID success with timestamp verification`() { + fun `test updateEventByDeviceIdAndNormalizedEventName success with timestamp verification`() { // Given - userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + userEventLogDAO.insertEvent(testDeviceId, testEventName, testEventNameNormalized) // Mock different time for update val updateTime = MOCK_TIME + 1000 every { Utils.getNowInMillis() } returns updateTime // When - val result = userEventLogDAO.updateEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + val result = userEventLogDAO.updateEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) // Then assertTrue(result) // Verify event details - val eventLog = userEventLogDAO.readEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + val eventLog = userEventLogDAO.readEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) assertNotNull(eventLog) with(requireNotNull(eventLog)) { assertEquals(2, countOfEvents) @@ -186,31 +192,29 @@ class UserEventLogDAOImplTest { } @Test - fun `test upsertEventsByDeviceID with new and existing events`() { + fun `test upsertEventsByDeviceIdAndNormalizedEventName with new and existing events`() { // Given - userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) - val eventNames = setOf(TEST_EVENT_NAME, TEST_EVENT_NAME_2) + userEventLogDAO.insertEvent(testDeviceId, testEventName, testEventNameNormalized) // When - val result = userEventLogDAO.upsertEventsByDeviceID(TEST_DEVICE_ID, eventNames) + val result = userEventLogDAO.upsertEventsByDeviceIdAndNormalizedEventName(testDeviceId, setOfActualAndNormalizedEventNamePair) // Then assertTrue(result) - assertEquals(2, userEventLogDAO.readEventCountByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME)) - assertEquals(1, userEventLogDAO.readEventCountByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME_2)) + assertEquals(2, userEventLogDAO.readEventCountByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized)) + assertEquals(1, userEventLogDAO.readEventCountByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized2)) } @Test - fun `test upsertEventsByDeviceID when db error occurs`() { + 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) - val eventNames = setOf(TEST_EVENT_NAME, TEST_EVENT_NAME_2) // When - val result = dao.upsertEventsByDeviceID(TEST_DEVICE_ID, eventNames) + val result = dao.upsertEventsByDeviceIdAndNormalizedEventName(testDeviceId, setOfActualAndNormalizedEventNamePair) // Then assertFalse(result) @@ -222,27 +226,28 @@ class UserEventLogDAOImplTest { } @Test - fun `test readEventByDeviceID returns null when event does not exist`() { + fun `test readEventByDeviceIdAndNormalizedEventName returns null when event does not exist`() { // When - val result = userEventLogDAO.readEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + val result = userEventLogDAO.readEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) // Then assertNull(result) } @Test - fun `test readEventByDeviceID returns correct event log after insert`() { + fun `test readEventByDeviceIdAndNormalizedEventName returns correct event log after insert`() { // Given - userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + userEventLogDAO.insertEvent(testDeviceId,testEventName, testEventNameNormalized) // When - val result = userEventLogDAO.readEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + val result = userEventLogDAO.readEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) // Then assertNotNull(result) with(requireNotNull(result)) { - assertEquals(TEST_EVENT_NAME, eventName) - assertEquals(TEST_DEVICE_ID, deviceID) + assertEquals(testEventName, eventName) + assertEquals(testEventNameNormalized, normalizedEventName) + assertEquals(testDeviceId, deviceID) assertEquals(1, countOfEvents) assertEquals(MOCK_TIME, firstTs) assertEquals(MOCK_TIME, lastTs) @@ -253,24 +258,25 @@ class UserEventLogDAOImplTest { } @Test - fun `test readEventByDeviceID after multiple updates`() { + fun `test readEventByDeviceIdAndNormalizedEventName after multiple updates`() { // Given - userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + userEventLogDAO.insertEvent(testDeviceId,testEventName, testEventNameNormalized) // Mock different time for update val updateTime = MOCK_TIME + 1000 every { Utils.getNowInMillis() } returns updateTime - userEventLogDAO.updateEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + userEventLogDAO.updateEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) // When - val result = userEventLogDAO.readEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + val result = userEventLogDAO.readEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) // Then assertNotNull(result) with(requireNotNull(result)) { - assertEquals(TEST_EVENT_NAME, eventName) - assertEquals(TEST_DEVICE_ID, deviceID) + 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 @@ -281,117 +287,117 @@ class UserEventLogDAOImplTest { } @Test - fun `test readEventByDeviceID when db error occurs`() { + 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.readEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + val result = dao.readEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) // Then assertNull(result) } @Test - fun `test readEventCountByDeviceID returns minus one when event does not exist`() { + fun `test readEventCountByDeviceIdAndNormalizedEventName returns minus one when event does not exist`() { // When - val result = userEventLogDAO.readEventCountByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + val result = userEventLogDAO.readEventCountByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) // Then assertEquals(-1, result) } @Test - fun `test readEventCountByDeviceID when db error occurs`() { + 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.readEventCountByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + val result = dao.readEventCountByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) // Then assertEquals(-1, result) } @Test - fun `test readEventCountByDeviceID returns correct count after insert`() { + fun `test readEventCountByDeviceIdAndNormalizedEventName returns correct count after insert`() { // Given - userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + userEventLogDAO.insertEvent(testDeviceId, testEventName, testEventNameNormalized) // When - val result = userEventLogDAO.readEventCountByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + val result = userEventLogDAO.readEventCountByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) // Then assertEquals(1, result) } @Test - fun `test readEventCountByDeviceID returns correct count after multiple updates`() { + fun `test readEventCountByDeviceIdAndNormalizedEventName returns correct count after multiple updates`() { // Given - userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) - userEventLogDAO.updateEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) - userEventLogDAO.updateEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + userEventLogDAO.insertEvent(testDeviceId,testEventName, testEventNameNormalized) + userEventLogDAO.updateEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) + userEventLogDAO.updateEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) // When - val result = userEventLogDAO.readEventCountByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + val result = userEventLogDAO.readEventCountByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) // Then assertEquals(3, result) } @Test - fun `test readEventFirstTsByDeviceID returns minus one when event does not exist`() { + fun `test readEventFirstTsByDeviceIdAndNormalizedEventName returns minus one when event does not exist`() { // When - val result = userEventLogDAO.readEventFirstTsByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + val result = userEventLogDAO.readEventFirstTsByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) // Then assertEquals(-1L, result) } @Test - fun `test readEventFirstTsByDeviceID when db error occurs`() { + fun `test readEventFirstTsByDeviceIdAndNormalizedEventName when db error occurs`() { // Given val dbHelper = mockk() every { dbHelper.readableDatabase } throws SQLiteException() val dao = UserEventLogDAOImpl(dbHelper, logger, table) // When - val result = dao.readEventFirstTsByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + val result = dao.readEventFirstTsByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) // Then assertEquals(-1L, result) } @Test - fun `test readEventFirstTsByDeviceID returns correct timestamp after insert`() { + fun `test readEventFirstTsByDeviceIdAndNormalizedEventName returns correct timestamp after insert`() { // Given - userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + userEventLogDAO.insertEvent(testDeviceId, testEventName, testEventNameNormalized) // When - val result = userEventLogDAO.readEventFirstTsByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + val result = userEventLogDAO.readEventFirstTsByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) // Then assertEquals(MOCK_TIME, result) } @Test - fun `test readEventFirstTsByDeviceID returns same timestamp after updates`() { + fun `test readEventFirstTsByDeviceIdAndNormalizedEventName returns same timestamp after updates`() { // Given - userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + userEventLogDAO.insertEvent(testDeviceId,testEventName, testEventNameNormalized) // Mock different time for update val updateTime = MOCK_TIME + 1000 every { Utils.getNowInMillis() } returns updateTime - userEventLogDAO.updateEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) - userEventLogDAO.updateEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + userEventLogDAO.updateEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) + userEventLogDAO.updateEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) // When - val result = userEventLogDAO.readEventFirstTsByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + val result = userEventLogDAO.readEventFirstTsByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) // Then assertEquals(MOCK_TIME, result) // First timestamp should remain same after updates @@ -402,53 +408,53 @@ class UserEventLogDAOImplTest { } @Test - fun `test readEventLastTsByDeviceID returns minus one when event does not exist`() { + fun `test readEventLastTsByDeviceIdAndNormalizedEventName returns minus one when event does not exist`() { // When - val result = userEventLogDAO.readEventLastTsByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + val result = userEventLogDAO.readEventLastTsByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) // Then assertEquals(-1L, result) } @Test - fun `test readEventLastTsByDeviceID when db error occurs`() { + fun `test readEventLastTsByDeviceIdAndNormalizedEventName when db error occurs`() { // Given val dbHelper = mockk() every { dbHelper.readableDatabase } throws SQLiteException() val dao = UserEventLogDAOImpl(dbHelper, logger, table) // When - val result = dao.readEventLastTsByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + val result = dao.readEventLastTsByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) // Then assertEquals(-1L, result) } @Test - fun `test readEventLastTsByDeviceID returns correct timestamp after insert`() { + fun `test readEventLastTsByDeviceIdAndNormalizedEventName returns correct timestamp after insert`() { // Given - userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + userEventLogDAO.insertEvent(testDeviceId,testEventName, testEventNameNormalized) // When - val result = userEventLogDAO.readEventLastTsByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + val result = userEventLogDAO.readEventLastTsByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) // Then assertEquals(MOCK_TIME, result) } @Test - fun `test readEventLastTsByDeviceID returns updated timestamp after updates`() { + fun `test readEventLastTsByDeviceIdAndNormalizedEventName returns updated timestamp after updates`() { // Given - userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + userEventLogDAO.insertEvent(testDeviceId,testEventName, testEventNameNormalized) // Mock different time for update val updateTime = MOCK_TIME + 1000 every { Utils.getNowInMillis() } returns updateTime - userEventLogDAO.updateEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + userEventLogDAO.updateEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) // When - val result = userEventLogDAO.readEventLastTsByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + val result = userEventLogDAO.readEventLastTsByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) // Then assertEquals(updateTime, result) // Last timestamp should be updated @@ -459,49 +465,49 @@ class UserEventLogDAOImplTest { } @Test - fun `test eventExistsByDeviceID returns false when event does not exist`() { + fun `test eventExistsByDeviceIdAndNormalizedEventName returns false when event does not exist`() { // When - val result = userEventLogDAO.eventExistsByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + val result = userEventLogDAO.eventExistsByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) // Then assertFalse(result) } @Test - fun `test eventExistsByDeviceID when db error occurs`() { + 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.eventExistsByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + val result = dao.eventExistsByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) // Then assertFalse(result) } @Test - fun `test eventExistsByDeviceID returns true after insert`() { + fun `test eventExistsByDeviceIdAndNormalizedEventName returns true after insert`() { // Given - userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + userEventLogDAO.insertEvent(testDeviceId,testEventName, testEventNameNormalized) // When - val result = userEventLogDAO.eventExistsByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + val result = userEventLogDAO.eventExistsByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) // Then assertTrue(result) } @Test - fun `test eventExistsByDeviceID returns true for specific deviceID only`() { + fun `test eventExistsByDeviceIdAndNormalizedEventName returns true for specific deviceID only`() { // Given val otherDeviceId = "other_device_id" - userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + userEventLogDAO.insertEvent(testDeviceId,testEventName, testEventNameNormalized) // When - val resultForTestDevice = userEventLogDAO.eventExistsByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) - val resultForOtherDevice = userEventLogDAO.eventExistsByDeviceID(otherDeviceId, TEST_EVENT_NAME) + val resultForTestDevice = userEventLogDAO.eventExistsByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) + val resultForOtherDevice = userEventLogDAO.eventExistsByDeviceIdAndNormalizedEventName(otherDeviceId, testEventNameNormalized) // Then assertTrue(resultForTestDevice) @@ -509,77 +515,77 @@ class UserEventLogDAOImplTest { } @Test - fun `test eventExistsByDeviceIDAndCount returns false when event does not exist`() { + fun `test eventExistsByDeviceIdAndNormalizedEventNameAndCount returns false when event does not exist`() { // When - val result = userEventLogDAO.eventExistsByDeviceIDAndCount(TEST_DEVICE_ID, TEST_EVENT_NAME, 1) + val result = userEventLogDAO.eventExistsByDeviceIdAndNormalizedEventNameAndCount(testDeviceId, testEventNameNormalized, 1) // Then assertFalse(result) } @Test - fun `test eventExistsByDeviceIDAndCount when db error occurs`() { + 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.eventExistsByDeviceIDAndCount(TEST_DEVICE_ID, TEST_EVENT_NAME, 1) + val result = dao.eventExistsByDeviceIdAndNormalizedEventNameAndCount(testDeviceId, testEventNameNormalized, 1) // Then assertFalse(result) } @Test - fun `test eventExistsByDeviceIDAndCount returns true for matching count`() { + fun `test eventExistsByDeviceIdAndNormalizedEventNameAndCount returns true for matching count`() { // Given - userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + userEventLogDAO.insertEvent(testDeviceId, testEventName, testEventNameNormalized) // When - val result = userEventLogDAO.eventExistsByDeviceIDAndCount(TEST_DEVICE_ID, TEST_EVENT_NAME, 1) + val result = userEventLogDAO.eventExistsByDeviceIdAndNormalizedEventNameAndCount(testDeviceId, testEventNameNormalized, 1) // Then assertTrue(result) } @Test - fun `test eventExistsByDeviceIDAndCount returns false for non-matching count`() { + fun `test eventExistsByDeviceIdAndNormalizedEventNameAndCount returns false for non-matching count`() { // Given - userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + userEventLogDAO.insertEvent(testDeviceId,testEventName, testEventNameNormalized) // When - val result = userEventLogDAO.eventExistsByDeviceIDAndCount(TEST_DEVICE_ID, TEST_EVENT_NAME, 2) + val result = userEventLogDAO.eventExistsByDeviceIdAndNormalizedEventNameAndCount(testDeviceId, testEventNameNormalized, 2) // Then assertFalse(result) } @Test - fun `test eventExistsByDeviceIDAndCount verifies count after updates`() { + fun `test eventExistsByDeviceIdAndNormalizedEventNameAndCount verifies count after updates`() { // Given - userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) - userEventLogDAO.updateEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + userEventLogDAO.insertEvent(testDeviceId, testEventName, testEventNameNormalized) + userEventLogDAO.updateEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) // When - val resultForCount1 = userEventLogDAO.eventExistsByDeviceIDAndCount(TEST_DEVICE_ID, TEST_EVENT_NAME, 1) - val resultForCount2 = userEventLogDAO.eventExistsByDeviceIDAndCount(TEST_DEVICE_ID, TEST_EVENT_NAME, 2) + val resultForCount1 = userEventLogDAO.eventExistsByDeviceIdAndNormalizedEventNameAndCount(testDeviceId, testEventNameNormalized, 1) + val resultForCount2 = userEventLogDAO.eventExistsByDeviceIdAndNormalizedEventNameAndCount(testDeviceId, testEventNameNormalized, 2) // Then assertFalse(resultForCount1) assertTrue(resultForCount2) } - @Test fun `test eventExistsByDeviceIDAndCount returns true for specific deviceID and count`(){ + @Test fun `test eventExistsByDeviceIdAndNormalizedEventNameAndCount returns true for specific deviceID and count`(){ // Given val otherDeviceId = "other_device_id" - userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) - userEventLogDAO.insertEventByDeviceID(otherDeviceId, TEST_EVENT_NAME) - userEventLogDAO.updateEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + userEventLogDAO.insertEvent(testDeviceId,testEventName, testEventNameNormalized) + userEventLogDAO.insertEvent(otherDeviceId, testEventName, testEventNameNormalized) + userEventLogDAO.updateEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) // When - val resultForTestDevice = userEventLogDAO.eventExistsByDeviceIDAndCount(TEST_DEVICE_ID, TEST_EVENT_NAME, 2) - val resultForOtherDevice = userEventLogDAO.eventExistsByDeviceIDAndCount(otherDeviceId, TEST_EVENT_NAME, 1) + val resultForTestDevice = userEventLogDAO.eventExistsByDeviceIdAndNormalizedEventNameAndCount(testDeviceId, testEventNameNormalized, 2) + val resultForOtherDevice = userEventLogDAO.eventExistsByDeviceIdAndNormalizedEventNameAndCount(otherDeviceId, testEventNameNormalized, 1) // Then assertTrue(resultForTestDevice) @@ -589,7 +595,7 @@ class UserEventLogDAOImplTest { @Test fun `test allEventsByDeviceID returns empty list when no events exist`() { // When - val result = userEventLogDAO.allEventsByDeviceID(TEST_DEVICE_ID) + val result = userEventLogDAO.allEventsByDeviceID(testDeviceId) // Then assertTrue(result.isEmpty()) @@ -603,7 +609,7 @@ class UserEventLogDAOImplTest { val dao = UserEventLogDAOImpl(dbHelper, logger, table) // When - val result = dao.allEventsByDeviceID(TEST_DEVICE_ID) + val result = dao.allEventsByDeviceID(testDeviceId) // Then assertTrue(result.isEmpty()) @@ -612,19 +618,18 @@ class UserEventLogDAOImplTest { @Test fun `test allEventsByDeviceID returns correct list after inserts`() { // Given - val list = listOf(TEST_EVENT_NAME, TEST_EVENT_NAME_2) - list.forEach { - userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, it) + setOfActualAndNormalizedEventNamePair.forEach { + userEventLogDAO.insertEvent(testDeviceId, it.first,it.second) } // When - val result = userEventLogDAO.allEventsByDeviceID(TEST_DEVICE_ID) + val result = userEventLogDAO.allEventsByDeviceID(testDeviceId) // Then assertEquals(2, result.size) assertTrue(result.all { - list.contains(it.eventName) && it.deviceID == TEST_DEVICE_ID + setOfActualAndNormalizedEventNamePair.contains(Pair(it.eventName,it.normalizedEventName)) && it.deviceID == testDeviceId }) } @@ -632,12 +637,12 @@ class UserEventLogDAOImplTest { fun `test allEventsByDeviceID returns events for specific deviceID only`() { // Given val otherDeviceId = "other_device_id" - listOf(TEST_DEVICE_ID, otherDeviceId).forEach { deviceId -> - userEventLogDAO.insertEventByDeviceID(deviceId, TEST_EVENT_NAME) + listOf(testDeviceId, otherDeviceId).forEach { deviceId -> + userEventLogDAO.insertEvent(deviceId,testEventName, testEventNameNormalized) } // When - val results = listOf(TEST_DEVICE_ID, otherDeviceId) + val results = listOf(testDeviceId, otherDeviceId) .associateWith { deviceId -> userEventLogDAO.allEventsByDeviceID(deviceId) } @@ -652,21 +657,21 @@ class UserEventLogDAOImplTest { @Test fun `test allEventsByDeviceID returns events ordered by lastTs`() { // Given - userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + userEventLogDAO.insertEvent(testDeviceId,testEventName, testEventNameNormalized) // Mock different time for second event val laterTime = MOCK_TIME + 1000 every { Utils.getNowInMillis() } returns laterTime - userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME_2) + userEventLogDAO.insertEvent(testDeviceId, testEventName2, testEventNameNormalized2) // When - val result = userEventLogDAO.allEventsByDeviceID(TEST_DEVICE_ID) + val result = userEventLogDAO.allEventsByDeviceID(testDeviceId) // Then assertEquals(2, result.size) - assertEquals(TEST_EVENT_NAME, result[0].eventName) // Earlier event first - assertEquals(TEST_EVENT_NAME_2, result[1].eventName) // Later event second + assertEquals(testEventNameNormalized, result[0].normalizedEventName) // Earlier event first + assertEquals(testEventNameNormalized2, result[1].normalizedEventName) // Later event second assertTrue(result[0].lastTs < result[1].lastTs) verify { @@ -700,12 +705,11 @@ class UserEventLogDAOImplTest { @Test fun `test allEvents returns all events from different users`() { // Given - val deviceIds = listOf(TEST_DEVICE_ID, "other_device_id") - val eventNames = listOf(TEST_EVENT_NAME, TEST_EVENT_NAME_2) + val deviceIds = listOf(testDeviceId, "other_device_id") deviceIds.forEach { deviceId -> - eventNames.forEach { eventName -> - userEventLogDAO.insertEventByDeviceID(deviceId, eventName) + setOfActualAndNormalizedEventNamePair.forEach { pair -> + userEventLogDAO.insertEvent(deviceId, pair.first, pair.second) } } @@ -715,19 +719,23 @@ class UserEventLogDAOImplTest { // Then assertEquals(4, result.size) result.all { - deviceIds.contains(it.deviceID) && eventNames.contains(it.eventName) + deviceIds.contains(it.deviceID) && setOfActualAndNormalizedEventNamePair.contains( + Pair( + it.eventName, + it.normalizedEventName + ) + ) } } @Test fun `test allEvents returns events ordered by lastTs`() { // Given - val events = listOf(TEST_EVENT_NAME, TEST_EVENT_NAME_2) - events.forEachIndexed { index, eventName -> + setOfActualAndNormalizedEventNamePair.forEachIndexed { index, pair -> val mockTime = MOCK_TIME + (index * 1000L) every { Utils.getNowInMillis() } returns mockTime - userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, eventName) + userEventLogDAO.insertEvent(testDeviceId,pair.first, pair.second) } // When @@ -747,17 +755,17 @@ class UserEventLogDAOImplTest { @Test fun `test allEvents returns correct event details`() { // Given - data class EventData(val deviceId: String, val name: String, val count: Int) + data class EventData(val deviceId: String, val name: String, val normalizedName: String, val count: Int) val testData = listOf( - EventData(TEST_DEVICE_ID, TEST_EVENT_NAME, 2), - EventData("other_device_id", TEST_EVENT_NAME_2, 1) + EventData(testDeviceId, testEventName, testEventNameNormalized, 2), + EventData("other_device_id", testEventName2, testEventNameNormalized2, 1) ) - testData.forEach { (deviceId, eventName, updateCount) -> - userEventLogDAO.insertEventByDeviceID(deviceId, eventName) + testData.forEach { (deviceId, eventName, normalizedName, updateCount) -> + userEventLogDAO.insertEvent(deviceId, eventName, normalizedName) repeat(updateCount - 1) { - userEventLogDAO.updateEventByDeviceID(deviceId, eventName) + userEventLogDAO.updateEventByDeviceIdAndNormalizedEventName(deviceId, normalizedName) } } @@ -767,12 +775,13 @@ class UserEventLogDAOImplTest { .mapValues { (_, events) -> events.associateBy { it.eventName } } // Then - testData.forEach { (deviceId, eventName, expectedCount) -> + 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) @@ -838,8 +847,11 @@ class UserEventLogDAOImplTest { fun `test cleanUpExtraEvents validation ensures database is not modified with invalid params`() { // Given val events = (1..5).map { "event_$it" } - events.forEach { eventName -> - userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, eventName) + val setOfActualAndNormalizedEventNamePair = events.map { + Pair(it, Utils.getNormalizedName(it)) + }.toSet() + setOfActualAndNormalizedEventNamePair.forEach { pair -> + userEventLogDAO.insertEvent(testDeviceId, pair.first,pair.second) } // When @@ -878,11 +890,14 @@ class UserEventLogDAOImplTest { 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() - events.forEachIndexed { index, eventName -> + setOfActualAndNormalizedEventNamePair.forEachIndexed { index, pair -> val mockTime = MOCK_TIME + (index * 1000L) every { Utils.getNowInMillis() } returns mockTime - userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, eventName) + userEventLogDAO.insertEvent(testDeviceId, pair.first,pair.second) } val threshold = 6 @@ -899,15 +914,15 @@ class UserEventLogDAOImplTest { // Verify oldest events were deleted and newest remain remainingEvents - .map { it.eventName } - .let { eventNames -> + .map { it.normalizedEventName } + .let { normalizedEventNames -> // First 6 events should be deleted (10 - 4 = 6) (1..6).forEach { - assertFalse(eventNames.contains("event_$it")) + assertFalse(normalizedEventNames.contains("event_$it")) } // Last 4 events should remain (7..10).forEach { - assertTrue(eventNames.contains("event_$it")) + assertTrue(normalizedEventNames.contains("event_$it")) } } @@ -920,11 +935,14 @@ class UserEventLogDAOImplTest { 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() - events.forEachIndexed { index, eventName -> + setOfActualAndNormalizedEventNamePair.forEachIndexed { index, pair -> val mockTime = MOCK_TIME + (index * 1000L) every { Utils.getNowInMillis() } returns mockTime - userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, eventName) + userEventLogDAO.insertEvent(testDeviceId, pair.first,pair.second) } val threshold = 5 @@ -937,10 +955,10 @@ class UserEventLogDAOImplTest { assertTrue(result) userEventLogDAO.allEvents() - .map { it.eventName } - .let { eventNames -> - assertEquals(3, eventNames.size) // All events should remain - assertTrue(eventNames.containsAll(events)) + .map { it.normalizedEventName } + .let { normalizedEventNames -> + assertEquals(3, normalizedEventNames.size) // All events should remain + assertTrue(normalizedEventNames.containsAll(events)) } verify { @@ -953,11 +971,14 @@ class UserEventLogDAOImplTest { // Given val eventCount = 10 val events = (1..eventCount).map { "event_$it" } + val setOfActualAndNormalizedEventNamePair = events.map { + Pair(it, Utils.getNormalizedName(it)) + }.toSet() - events.forEachIndexed { index, eventName -> + setOfActualAndNormalizedEventNamePair.forEachIndexed { index, pair -> val mockTime = MOCK_TIME + (index * 1000L) every { Utils.getNowInMillis() } returns mockTime - userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, eventName) + userEventLogDAO.insertEvent(testDeviceId, pair.first, pair.second) } val threshold = 6 @@ -984,7 +1005,7 @@ class UserEventLogDAOImplTest { @Test fun `test cleanUpExtraEvents with threshold 1 and numberOfRowsToCleanup 0 when single event exists`() { // Given - userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, TEST_EVENT_NAME) + userEventLogDAO.insertEvent(testDeviceId,testEventName, testEventNameNormalized) // When val result = userEventLogDAO.cleanUpExtraEvents(1, 0) @@ -993,7 +1014,7 @@ class UserEventLogDAOImplTest { assertTrue(result) userEventLogDAO.allEvents().let { events -> assertEquals(1, events.size) - assertEquals(TEST_EVENT_NAME, events[0].eventName) + assertEquals(testEventNameNormalized, events[0].normalizedEventName) } } @@ -1001,11 +1022,14 @@ class UserEventLogDAOImplTest { 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() - events.forEachIndexed { index, eventName -> + setOfActualAndNormalizedEventNamePair.forEachIndexed { index, pair -> val mockTime = MOCK_TIME + (index * 1000L) every { Utils.getNowInMillis() } returns mockTime - userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, eventName) + userEventLogDAO.insertEvent(testDeviceId, pair.first, pair.second) } // When @@ -1015,7 +1039,7 @@ class UserEventLogDAOImplTest { assertTrue(result) userEventLogDAO.allEvents().let { remainingEvents -> assertEquals(1, remainingEvents.size) - assertEquals("event_3", remainingEvents[0].eventName) // Should keep the most recent event + assertEquals("event_3", remainingEvents[0].normalizedEventName) // Should keep the most recent event assertEquals(MOCK_TIME + 2000L, remainingEvents[0].lastTs) } @@ -1027,12 +1051,12 @@ class UserEventLogDAOImplTest { @Test fun `test cleanUpExtraEvents with threshold 1 and numberOfRowsToCleanup 0 with multiple users`() { // Given - val devices = listOf(TEST_DEVICE_ID, "other_device_id") + val devices = listOf(testDeviceId, "other_device_id") devices.forEachIndexed { index, deviceId -> val mockTime = MOCK_TIME + (index * 1000L) every { Utils.getNowInMillis() } returns mockTime - userEventLogDAO.insertEventByDeviceID(deviceId, TEST_EVENT_NAME) + userEventLogDAO.insertEvent(deviceId, testEventName, testEventNameNormalized) } // When @@ -1057,12 +1081,13 @@ class UserEventLogDAOImplTest { (1..5).forEach { index -> val mockTime = MOCK_TIME + (index * 1000L) every { Utils.getNowInMillis() } returns mockTime - userEventLogDAO.insertEventByDeviceID(TEST_DEVICE_ID, "event_$index") + 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.updateEventByDeviceID(TEST_DEVICE_ID, "event_1") + userEventLogDAO.updateEventByDeviceIdAndNormalizedEventName(testDeviceId, "event_1") } } @@ -1074,7 +1099,7 @@ class UserEventLogDAOImplTest { 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].eventName) + assertEquals("event_1", remainingEvents[0].normalizedEventName) } verify { 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/gradle/libs.versions.toml b/gradle/libs.versions.toml index f98e6ef52..056212826 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] # Project android_compileSdk = "34" -android_gradle_plugin = "8.2.2" +android_gradle_plugin = "8.7.0" android_minSdk = "19" kotlin_plugin = "1.7.20" sonarqube_plugin = "3.3" @@ -54,7 +54,7 @@ coroutines_test = "1.7.3" installreferrer = "2.2" #SDK Versions -clevertap_android_sdk = "7.0.3" +clevertap_android_sdk = "8.0.0" clevertap_rendermax_sdk = "1.0.3" clevertap_geofence_sdk = "1.3.0" clevertap_hms_sdk = "1.3.4" 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/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' } From 0727511d7d2acbcef085478ef16078146fd0c49f Mon Sep 17 00:00:00 2001 From: anush Date: Wed, 11 Dec 2024 11:38:23 +0530 Subject: [PATCH 092/120] task(MC-2803) - Updates libVersion --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9b153db79..e9fb53a47 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 = "8.0.0" +clevertap_android_sdk = "7.1.0" clevertap_rendermax_sdk = "1.0.3" clevertap_geofence_sdk = "1.3.0" clevertap_hms_sdk = "1.3.4" From d23fafd22b82a90c9704881201aaaf2601ba0c14 Mon Sep 17 00:00:00 2001 From: Vassil Angelov <148857285+vasct@users.noreply.github.com> Date: Mon, 16 Dec 2024 16:14:21 +0200 Subject: [PATCH 093/120] Add minor optimiziations to UserEventLog operations (#708) Change the cached user event log keys set to contain the normalized event names to match the db logic Reorder deviceID column in the primary key of UserEventLog table to speed up allEventsByDeviceID query --- .../com/clevertap/android/sdk/LocalDataStore.java | 11 +++++------ .../java/com/clevertap/android/sdk/db/CtDatabase.kt | 4 ++-- 2 files changed, 7 insertions(+), 8 deletions(-) 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 102c678ff..ce4acf708 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 @@ -56,7 +56,7 @@ public class LocalDataStore { private final String eventNamespace = "local_events"; private final DeviceInfo deviceInfo; - private final Set userEventLogKeys = Collections.synchronizedSet(new HashSet<>()); + private final Set userNormalizedEventLogKeys = Collections.synchronizedSet(new HashSet<>()); private final Map normalizedEventNames = new HashMap<>(); LocalDataStore(Context context, CleverTapInstanceConfig config, CryptHandler cryptHandler, DeviceInfo deviceInfo, BaseDatabaseManager baseDatabaseManager) { @@ -70,7 +70,7 @@ public class LocalDataStore { @WorkerThread public void changeUser() { - userEventLogKeys.clear(); + userNormalizedEventLogKeys.clear(); resetLocalProfileSync(); } @@ -273,16 +273,15 @@ private boolean eventExistsByDeviceIdAndNormalizedEventNameAndCount(String devic @WorkerThread public boolean isUserEventLogFirstTime(String eventName) { - if (userEventLogKeys.contains(eventName)) { + String normalizedEventName = getOrPutNormalizedEventName(eventName); + if (userNormalizedEventLogKeys.contains(normalizedEventName)) { return false; } String deviceID = deviceInfo.getDeviceID(); - String normalizedEventName = getOrPutNormalizedEventName(eventName); - int count = readEventCountByDeviceIdAndNormalizedEventName(deviceID, normalizedEventName); if (count > 1) { - userEventLogKeys.add(eventName); + userNormalizedEventLogKeys.add(normalizedEventName); } return count == 1; } 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 1cdaf76bd..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 @@ -235,13 +235,13 @@ 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, - ${Column.DEVICE_ID} STRING NOT NULL, - PRIMARY KEY (${Column.NORMALIZED_EVENT_NAME}, ${Column.DEVICE_ID}) + PRIMARY KEY (${Column.DEVICE_ID}, ${Column.NORMALIZED_EVENT_NAME}) ); """ From 8f0f04109db9c8655f172e76059bf6f26097e0bb Mon Sep 17 00:00:00 2001 From: CTLalit Date: Wed, 18 Dec 2024 12:01:42 +0530 Subject: [PATCH 094/120] feat(SDK-4171): changes code to use clock interface - passes clock interface rather than a function --- .../android/sdk/AnalyticsManager.java | 7 ++++--- .../android/sdk/CleverTapFactory.java | 3 ++- .../android/sdk/AnalyticsManagerTest.kt | 19 ++++++++++--------- 3 files changed, 16 insertions(+), 13 deletions(-) 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 a4d89b150..634a3d21c 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; @@ -50,7 +51,7 @@ public class AnalyticsManager extends BaseAnalyticsManager { private final ValidationResultStack validationResultStack; private final Validator validator; private final InAppResponse inAppResponse; - private final Function0 currentTimeProvider; + private final Clock currentTimeProvider; private final Object notificationMapLock = new Object(); private final HashMap notificationIdTagMap = new HashMap<>(); @@ -67,7 +68,7 @@ public class AnalyticsManager extends BaseAnalyticsManager { BaseCallbackManager callbackManager, ControllerManager controllerManager, final CTLockManager ctLockManager, InAppResponse inAppResponse, - Function0 currentTimeProvider + Clock currentTimeProvider ) { this.context = context; this.config = config; @@ -1149,7 +1150,7 @@ private boolean checkDuplicateNotificationIds(Bundle extras, HashMap Long) + private lateinit var timeProvider: Clock private val bundle = Bundle().apply { putString("wzrk_pn", "wzrk_pn") @@ -128,7 +129,7 @@ class AnalyticsManagerTest { fun `clevertap does not process duplicate PN viewed within 2 seconds - case 2nd notif in 200ms`() { val json = notificationViewedJson(bundle) - every { timeProvider.invoke() } returns 10000 + every { timeProvider.currentTimeMillis() } returns 10000 // send PN first time analyticsManagerSUT.pushNotificationViewedEvent(bundle) @@ -144,7 +145,7 @@ class AnalyticsManagerTest { } // setup again, 200 ms has passed - every { timeProvider.invoke() } returns 10200 + every { timeProvider.currentTimeMillis() } returns 10200 // Send duplicate PN analyticsManagerSUT.pushNotificationViewedEvent(bundle) @@ -167,7 +168,7 @@ class AnalyticsManagerTest { val json = notificationViewedJson(bundle); - every { timeProvider.invoke() } returns 10000 + every { timeProvider.currentTimeMillis() } returns 10000 // send PN first time analyticsManagerSUT.pushNotificationViewedEvent(bundle) @@ -183,7 +184,7 @@ class AnalyticsManagerTest { } // setup again, 10000 ms has passed - every { timeProvider.invoke() } returns 20000 + every { timeProvider.currentTimeMillis() } returns 20000 // Send duplicate PN analyticsManagerSUT.pushNotificationViewedEvent(bundle) @@ -218,7 +219,7 @@ class AnalyticsManagerTest { fun `clevertap does not process duplicate (same wzrk_id) PN clicked within 2 seconds - case 2nd click happens in 200ms`() { val json = notificationViewedJson(bundle) - every { timeProvider.invoke() } returns 10000 + every { timeProvider.currentTimeMillis() } returns 10000 // send PN first time analyticsManagerSUT.pushNotificationClickedEvent(bundle) @@ -234,7 +235,7 @@ class AnalyticsManagerTest { } // setup again, 10000 ms has passed - every { timeProvider.invoke() } returns 12000 + every { timeProvider.currentTimeMillis() } returns 12000 // Send duplicate PN analyticsManagerSUT.pushNotificationClickedEvent(bundle) @@ -256,7 +257,7 @@ class AnalyticsManagerTest { fun `clevertap processes PN clicked for same wzrk_id if separated by a span of greater than 5 seconds`() { val json = notificationViewedJson(bundle); - every { timeProvider.invoke() } returns 10000 + every { timeProvider.currentTimeMillis() } returns 10000 // send PN first time analyticsManagerSUT.pushNotificationClickedEvent(bundle) @@ -272,7 +273,7 @@ class AnalyticsManagerTest { } // setup again, 10000 ms has passed - every { timeProvider.invoke() } returns 20000 + every { timeProvider.currentTimeMillis() } returns 20000 // Send duplicate PN analyticsManagerSUT.pushNotificationClickedEvent(bundle) From 1ea60aa4627c99fc8e98328b520c5e7deb12dab4 Mon Sep 17 00:00:00 2001 From: CTLalit Date: Wed, 18 Dec 2024 12:05:14 +0530 Subject: [PATCH 095/120] feat(SDK-4171): adds jvmstatic annotation --- .../java/com/clevertap/android/sdk/AnalyticsManager.java | 6 +++--- .../com/clevertap/android/sdk/AnalyticsManagerBundler.kt | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) 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 634a3d21c..599a783a1 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 @@ -499,10 +499,10 @@ public void pushNotificationClickedEvent(final Bundle extras) { try { // convert bundle to json - JSONObject event = AnalyticsManagerBundler.INSTANCE.notificationViewedJson(extras); + JSONObject event = AnalyticsManagerBundler.notificationViewedJson(extras); baseEventQueueManager.queueEvent(context, event, Constants.RAISED_EVENT); - coreMetaData.setWzrkParams(AnalyticsManagerBundler.INSTANCE.wzrkBundleToJson(extras)); + coreMetaData.setWzrkParams(AnalyticsManagerBundler.wzrkBundleToJson(extras)); } catch (Throwable t) { // We won't get here } @@ -656,7 +656,7 @@ public void pushNotificationViewedEvent(Bundle extras) { config.getLogger().debug("Recording Notification Viewed event for notification: " + extras); - JSONObject event = AnalyticsManagerBundler.INSTANCE.notificationViewedJson(extras); + JSONObject event = AnalyticsManagerBundler.notificationViewedJson(extras); baseEventQueueManager.queueEvent(context, event, Constants.NV_EVENT); } 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 index 5f49c0296..45586483e 100644 --- a/clevertap-core/src/main/java/com/clevertap/android/sdk/AnalyticsManagerBundler.kt +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/AnalyticsManagerBundler.kt @@ -7,6 +7,7 @@ import org.json.JSONObject object AnalyticsManagerBundler { @Throws(JSONException::class) + @JvmStatic fun wzrkBundleToJson(root: Bundle): JSONObject { val fields = JSONObject() for (s in root.keySet()) { @@ -26,6 +27,7 @@ object AnalyticsManagerBundler { return fields } + @JvmStatic fun notificationViewedJson(root: Bundle): JSONObject { val event = JSONObject() try { From 4d2388d097de5023cfac1831754a0ffb048b0d91 Mon Sep 17 00:00:00 2001 From: CTLalit Date: Wed, 18 Dec 2024 12:35:35 +0530 Subject: [PATCH 096/120] feat(SDK-4171): fixes todo - visibility for createInstanceWithManifest --- .../src/main/java/com/clevertap/android/sdk/CleverTapAPI.java | 3 +-- .../com/clevertap/android/sdk/CleverTapInstanceConfig.java | 4 +--- .../java/com/clevertap/android/sdk/AnalyticsManagerTest.kt | 1 - .../clevertap/android/sdk/{fixtures => }/CleverTapFixtures.kt | 4 +--- 4 files changed, 3 insertions(+), 9 deletions(-) rename clevertap-core/src/test/java/com/clevertap/android/sdk/{fixtures => }/CleverTapFixtures.kt (86%) 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 b043f25ab..0797d7074 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 @@ -3104,8 +3104,7 @@ 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) { 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 9b72551ed..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 @@ -101,9 +101,7 @@ protected static CleverTapInstanceConfig createDefaultInstance( return CleverTapInstanceConfig.createInstanceWithManifest(manifestInfo, accountId, accountToken, accountRegion, true); } - // todo lp visibility - @VisibleForTesting - public static CleverTapInstanceConfig createInstanceWithManifest( + static CleverTapInstanceConfig createInstanceWithManifest( @NonNull ManifestInfo manifest, @NonNull String accountId, @NonNull String accountToken, 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 e3d7dc93e..e8ecb42bc 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 @@ -4,7 +4,6 @@ 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.fixtures.CleverTapFixtures import com.clevertap.android.sdk.response.InAppResponse import com.clevertap.android.sdk.task.CTExecutorFactory import com.clevertap.android.sdk.task.MockCTExecutors diff --git a/clevertap-core/src/test/java/com/clevertap/android/sdk/fixtures/CleverTapFixtures.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/CleverTapFixtures.kt similarity index 86% rename from clevertap-core/src/test/java/com/clevertap/android/sdk/fixtures/CleverTapFixtures.kt rename to clevertap-core/src/test/java/com/clevertap/android/sdk/CleverTapFixtures.kt index 97199efb0..c44123deb 100644 --- a/clevertap-core/src/test/java/com/clevertap/android/sdk/fixtures/CleverTapFixtures.kt +++ b/clevertap-core/src/test/java/com/clevertap/android/sdk/CleverTapFixtures.kt @@ -1,7 +1,5 @@ -package com.clevertap.android.sdk.fixtures +package com.clevertap.android.sdk -import com.clevertap.android.sdk.CleverTapInstanceConfig -import com.clevertap.android.sdk.ManifestInfo import com.clevertap.android.shared.test.Constant class CleverTapFixtures { From d415cd37c973a94294ed0ed628b32810e7111cb8 Mon Sep 17 00:00:00 2001 From: CTLalit Date: Wed, 18 Dec 2024 12:37:26 +0530 Subject: [PATCH 097/120] chore(SDK-4171): removes redundant constructor --- .../src/main/java/com/clevertap/android/sdk/CoreState.java | 3 --- 1 file changed, 3 deletions(-) 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 53e243ae0..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 @@ -110,9 +110,6 @@ public void setParser(final Parser parser) { this.parser = parser; } - CoreState() { - } - public ActivityLifeCycleManager getActivityLifeCycleManager() { return activityLifeCycleManager; } From c39c2ce8eae3870bbc901f3068993092e1c645d6 Mon Sep 17 00:00:00 2001 From: CTLalit Date: Wed, 18 Dec 2024 13:52:19 +0530 Subject: [PATCH 098/120] chore(SDK-4171): removes public modifier for manifestinfo --- .../src/main/java/com/clevertap/android/sdk/ManifestInfo.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 a74f03dbd..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 @@ -162,9 +162,7 @@ private ManifestInfo(Context context) { profileKeys = parseProfileKeys(metaData); } - // todo lp visibility -> private - @VisibleForTesting - public ManifestInfo( + ManifestInfo( String accountId, String accountToken, String accountRegion, From b8af96b08dfafe7afcf2964a83df461bb5548ba8 Mon Sep 17 00:00:00 2001 From: CTLalit <144685420+CTLalit@users.noreply.github.com> Date: Wed, 18 Dec 2024 15:00:10 +0530 Subject: [PATCH 099/120] SDK 4133 - Dedupe check for notifications (#694) * feat(SDK-4133): dedupe check - change mentioned in the TAN * feat(SDK-4133): changes map to contain long value - corrects object save and casting * feat(SDK-4171): Adds check for dedupe behind feature flag - adds a wzrk_dd flag so that it can be used in client specific fashion until it is rolled out to general audience. * feat(SDK-4133): parses string or boolean after check --- .../android/sdk/AnalyticsManager.java | 35 +++++++++++++++---- .../com/clevertap/android/sdk/Constants.java | 1 + 2 files changed, 30 insertions(+), 6 deletions(-) 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..7690674d2 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 @@ -60,11 +60,11 @@ public class AnalyticsManager extends BaseAnalyticsManager { private final InAppResponse inAppResponse; - private final HashMap notificationIdTagMap = new HashMap<>(); + private final HashMap notificationIdTagMap = new HashMap<>(); private final Object notificationMapLock = new Object(); - private final HashMap notificationViewedIdTagMap = new HashMap<>(); + private final HashMap notificationViewedIdTagMap = new HashMap<>(); AnalyticsManager(Context context, CleverTapInstanceConfig config, @@ -1163,18 +1163,41 @@ private void _pushMultiValue(ArrayList originalValues, String key, Strin } } - private boolean checkDuplicateNotificationIds(Bundle extras, HashMap notificationTagMap, - int interval) { + private boolean checkDuplicateNotificationIds( + Bundle extras, + 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); + + // 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); + } + long now = System.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/Constants.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/Constants.java index e33557d89..804610337 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 @@ -249,6 +249,7 @@ 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"; From aa507fbc62b84643b6e9e4048ecb39fec1265bd0 Mon Sep 17 00:00:00 2001 From: Vassil Angelov <148857285+vasct@users.noreply.github.com> Date: Fri, 20 Dec 2024 15:40:26 +0200 Subject: [PATCH 100/120] Multi-triggers updates (#709) * Multi-triggers updates Move update of LocalStore to EventQueueManager.processEvent Remove getUserEventLogFirstTs and getUserEventLogLastTs. Replace usage with getUserEventLog Change getUserEventLogCount to return 0 when no events match Add isPersonalizationEnabled check to userEventLog methods in CleverTapAPI --- .../clevertap/android/sdk/CleverTapAPI.java | 72 ++++++--------- .../clevertap/android/sdk/LocalDataStore.java | 26 ------ .../clevertap/android/sdk/SessionManager.java | 4 +- .../com/clevertap/android/sdk/db/DBManager.kt | 9 +- .../android/sdk/events/EventQueueManager.java | 78 ++++++++-------- .../sdk/usereventlogs/UserEventLogDAO.kt | 11 +-- .../sdk/usereventlogs/UserEventLogDAOImpl.kt | 88 +++++-------------- .../clevertap/android/sdk/CleverTapAPITest.kt | 39 -------- .../android/sdk/EventQueueManagerTest.kt | 2 - .../android/sdk/LocalDataStoreTest.kt | 42 --------- .../android/sdk/SessionManagerTest.kt | 15 +++- .../usereventlogs/UserEventLogDAOImplTest.kt | 74 +++------------- 12 files changed, 121 insertions(+), 339 deletions(-) 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 c487f1dbe..205347ba3 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 @@ -1628,7 +1628,8 @@ public int getCount(String event) { } /** - * Retrieves the count of logged events for a specific event name associated with the current user/{@link CleverTapAPI#getCleverTapID(OnInitCleverTapIDListener) CleverTap ID}. + * 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: @@ -1639,9 +1640,13 @@ public int getCount(String event) { * * * @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 the event does not exist or there was an error + * @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); } @@ -1662,7 +1667,8 @@ public EventDetail getDetails(String event) { } /** - * Retrieves user-specific event log associated with the current user/{@link CleverTapAPI#getCleverTapID(OnInitCleverTapIDListener) CleverTap ID}. + * 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: @@ -1678,6 +1684,9 @@ public EventDetail getDetails(String event) { */ @WorkerThread public UserEventLog getUserEventLog(String eventName) { + if (!getConfig().isPersonalizationEnabled()) { + return null; + } return coreState.getLocalDataStore().readUserEventLog(eventName); } @@ -1777,8 +1786,8 @@ 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 #getUserEventLogFirstTs(String)} instead. - * getUserEventLogFirstTs() provides user-specific event first occurrence timestamp. + * @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") @@ -1791,25 +1800,6 @@ public int getFirstTime(String event) { return -1; } - /** - * Retrieves first occurrence timestamp of event 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
- * long firstTs = getUserEventLogFirstTs("navigation_clicked") - *
- * - * @param eventName Name of the event to get first timestamp for (e.g., "navigation_clicked", "item_selected") - * @return First occurrence timestamp of the event for current user, or -1 if the event does not exist or there was an error - */ - @WorkerThread - public long getUserEventLogFirstTs(String eventName) { - return coreState.getLocalDataStore().readUserEventLogFirstTs(eventName); - } - /** * Returns the GeofenceCallback object * @@ -1861,8 +1851,11 @@ public Map getHistory() { */ @WorkerThread public Map getUserEventLogHistory() { - List logs = coreState.getLocalDataStore().readUserEventLogs(); Map history = new LinkedHashMap<>(); + if (!getConfig().isPersonalizationEnabled()) { + return history; + } + List logs = coreState.getLocalDataStore().readUserEventLogs(); for (UserEventLog log : logs) { history.put(log.getEventName(), log); } @@ -1980,8 +1973,8 @@ 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 #getUserEventLogLastTs(String)} instead. - * getUserEventLogLastTs() provides user-specific event last occurrence timestamp. + * @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") @@ -1994,25 +1987,6 @@ public int getLastTime(String event) { return -1; } - /** - * Retrieves last occurrence timestamp of event 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
- * long lastTs = getUserEventLogLastTs("navigation_clicked") - *
- * - * @param eventName Name of the event to get last timestamp for (e.g., "navigation_clicked", "item_selected") - * @return Last occurrence timestamp of the event for current user, or -1 if the event does not exist or there was an error - */ - @WorkerThread - public long getUserEventLogLastTs(String eventName) { - return coreState.getLocalDataStore().readUserEventLogLastTs(eventName); - } - /** * get the current device location * requires Location Permission in AndroidManifest e.g. "android.permission.ACCESS_COARSE_LOCATION" @@ -2062,6 +2036,9 @@ public int getPreviousVisitTime() { * @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(); } @@ -2168,6 +2145,9 @@ public int getTotalVisits() { */ @WorkerThread public int getUserAppLaunchCount() { + if (!getConfig().isPersonalizationEnabled()) { + return -1; + } return coreState.getLocalDataStore().readUserEventLogCount(Constants.APP_LAUNCHED_EVENT); } 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 ce4acf708..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 @@ -320,32 +320,6 @@ public int readUserEventLogCount(String eventName) { return readEventCountByDeviceIdAndNormalizedEventName(deviceID, normalizedEventName); } - @WorkerThread - private long readEventLastTsByDeviceIdAndNormalizedEventName(String deviceID, String normalizedEventName) { - DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context); - return dbAdapter.userEventLogDAO().readEventLastTsByDeviceIdAndNormalizedEventName(deviceID, normalizedEventName); - } - - @WorkerThread - public long readUserEventLogLastTs(String eventName) { - String deviceID = deviceInfo.getDeviceID(); - String normalizedEventName = getOrPutNormalizedEventName(eventName); - return readEventLastTsByDeviceIdAndNormalizedEventName(deviceID, normalizedEventName); - } - - @WorkerThread - private long readEventFirstTsByDeviceIdAndNormalizedEventName(String deviceID, String normalizedEventName) { - DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context); - return dbAdapter.userEventLogDAO().readEventFirstTsByDeviceIdAndNormalizedEventName(deviceID, normalizedEventName); - } - - @WorkerThread - public long readUserEventLogFirstTs(String eventName) { - String deviceID = deviceInfo.getDeviceID(); - String normalizedEventName = getOrPutNormalizedEventName(eventName); - return readEventFirstTsByDeviceIdAndNormalizedEventName(deviceID,normalizedEventName); - } - @WorkerThread private List allEventsByDeviceID(String deviceID) { DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context); 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 34d5d52ff..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 @@ -7,6 +7,7 @@ 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) @@ -93,7 +94,8 @@ void setLastVisitTime() { } @WorkerThread void setUserLastVisitTs() { - userLastVisitTs = localDataStore.readUserEventLogLastTs(Constants.APP_LAUNCHED_EVENT); + UserEventLog appLaunchedEventLog = localDataStore.readUserEventLog(Constants.APP_LAUNCHED_EVENT); + userLastVisitTs = appLaunchedEventLog != null ? appLaunchedEventLog.getLastTs() : -1; } private void createSession(final Context context) { 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 52469f557..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,9 +17,12 @@ 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 - private val userEventLogsThreshold = 11_520 //12.59 MB table and index size - private val numberOfRowsToCleanupForUserEventLogs = 2_304 //( 2048 events + 256 profile props = 1 user) @WorkerThread @Synchronized @@ -33,7 +36,7 @@ internal class DBManager( dbAdapter.cleanupStaleEvents(PUSH_NOTIFICATION_VIEWED) dbAdapter.cleanUpPushNotifications() dbAdapter.userEventLogDAO() - .cleanUpExtraEvents(userEventLogsThreshold, numberOfRowsToCleanupForUserEventLogs) + .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 55293e61c..c56cafbdd 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 @@ -314,8 +314,32 @@ public void processEvent(final Context context, final JSONObject event, final in } localDataStore.setDataSyncFlag(event); baseDatabaseManager.queueEventToDB(context, event, eventType); - scheduleQueueFlush(context); + 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); + } + + scheduleQueueFlush(context); } catch (Throwable e) { config.getLogger().verbose(config.getAccountId(), "Failed to queue event: " + event.toString(), e); } @@ -458,52 +482,24 @@ public Future queueEvent(final Context context, final JSONObject event, final @Override @WorkerThread public Void call() { - 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); - } - 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) { 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 index c910f3354..70816d2bf 100644 --- 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 @@ -26,14 +26,6 @@ interface UserEventLogDAO { @WorkerThread fun readEventCountByDeviceIdAndNormalizedEventName(deviceID: String, normalizedEventName: String): Int - // Read an event firstTs by deviceID - @WorkerThread - fun readEventFirstTsByDeviceIdAndNormalizedEventName(deviceID: String, normalizedEventName: String): Long - - // Read an event lastTs by deviceID - @WorkerThread - fun readEventLastTsByDeviceIdAndNormalizedEventName(deviceID: String, normalizedEventName: String): Long - // Check if an event exists by deviceID @WorkerThread fun eventExistsByDeviceIdAndNormalizedEventName(deviceID: String, normalizedEventName: String): Boolean @@ -49,6 +41,7 @@ interface UserEventLogDAO { // Get all events @WorkerThread fun allEvents(): List + @WorkerThread - fun cleanUpExtraEvents(threshold: Int = 11_520, numberOfRowsToCleanup: Int = 2_304): Boolean + 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 index 441dcc219..46a8308b8 100644 --- 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 @@ -49,7 +49,7 @@ internal class UserEventLogDAOImpl( SQLiteDatabase.CONFLICT_REPLACE ) } catch (e: Exception) { - logger.verbose("Error adding row to table $tableName Recreating DB") + logger.verbose("Error adding row to table $tableName Recreating DB. Exception: $e") db.deleteDatabase() DB_UPDATE_ERROR } @@ -85,7 +85,7 @@ internal class UserEventLogDAOImpl( setOfActualAndNormalizedEventNamePair: Set> ): Boolean { val tableName = table.tableName - logger.verbose("UserEventLog: upSert EventLog for bulk events") + logger.verbose("UserEventLog: upsert EventLog for bulk events") return try { db.writableDatabase.beginTransaction() setOfActualAndNormalizedEventNamePair.forEach { @@ -141,41 +141,27 @@ internal class UserEventLogDAOImpl( } @WorkerThread - override fun readEventCountByDeviceIdAndNormalizedEventName(deviceID: String, normalizedEventName: String): Int = - readEventColumnByDeviceIdAndNormalizedEventName( - deviceID, - normalizedEventName, - Column.COUNT, - defaultValueExtractor = { -1 }, - valueExtractor = { cursor, columnName -> - cursor.getInt(cursor.getColumnIndexOrThrow(columnName)) - } - ) - - - @WorkerThread - override fun readEventFirstTsByDeviceIdAndNormalizedEventName(deviceID: String, normalizedEventName: String): Long = - readEventColumnByDeviceIdAndNormalizedEventName( - deviceID, - normalizedEventName, - Column.FIRST_TS, - defaultValueExtractor = { -1L }, - valueExtractor = { cursor, columnName -> - cursor.getLong(cursor.getColumnIndexOrThrow(columnName)) - } - ) + 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) - @WorkerThread - override fun readEventLastTsByDeviceIdAndNormalizedEventName(deviceID: String, normalizedEventName: String): Long = - readEventColumnByDeviceIdAndNormalizedEventName( - deviceID, - normalizedEventName, - Column.LAST_TS, - defaultValueExtractor = { -1L }, - valueExtractor = { cursor, columnName -> - cursor.getLong(cursor.getColumnIndexOrThrow(columnName)) - } - ) + 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 { @@ -342,34 +328,4 @@ internal class UserEventLogDAOImpl( false } } - - @WorkerThread - private fun readEventColumnByDeviceIdAndNormalizedEventName( - deviceID: String, - normalizedEventName: String, - column: String, - defaultValueExtractor: () -> T, - valueExtractor: (cursor: Cursor, columnName: String) -> T - ): T { - val tName = table.tableName - val selection = "${Column.DEVICE_ID} = ? AND ${Column.NORMALIZED_EVENT_NAME} = ?" - val selectionArgs = arrayOf(deviceID, normalizedEventName) - val projection = arrayOf(column) - - return try { - db.readableDatabase.query( - tName, projection, selection, selectionArgs, null, null, null, null - )?.use { cursor -> - if (cursor.moveToFirst()) { - valueExtractor(cursor, column) - } else { - defaultValueExtractor() - } - } ?: defaultValueExtractor() - } catch (e: Exception) { - logger.verbose("Could not fetch $column from database $tName.", e) - defaultValueExtractor() - } - } - } 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 b1f9d7b69..c7c6d6bc6 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 @@ -501,45 +501,6 @@ class CleverTapAPITest : BaseTestCase() { } } - @Test - fun `test getUserEventLogFirstTs`() { - // Arrange - val evt = UserEventLogTestData.EventNames.TEST_EVENT - val firstTsExpected = UserEventLogTestData.TestTimestamps.SAMPLE_TIMESTAMP - `when`(corestate.localDataStore.readUserEventLogFirstTs(evt)).thenReturn( - firstTsExpected - ) - - // Act - executeBasicTest { - initializeCleverTapAPI() - val firstTsActual = cleverTapAPI.getUserEventLogFirstTs(evt) - - // Assert - assertEquals(firstTsExpected, firstTsActual) - verify(corestate.localDataStore).readUserEventLogFirstTs(evt) - } - } - - @Test - fun `test getUserEventLogLastTs`() { - // Arrange - val evt = UserEventLogTestData.EventNames.TEST_EVENT - val lastTsExpected = UserEventLogTestData.TestTimestamps.SAMPLE_TIMESTAMP - `when`(corestate.localDataStore.readUserEventLogLastTs(evt)).thenReturn(lastTsExpected) - - // Act - executeBasicTest { - initializeCleverTapAPI() - val lastTsActual = cleverTapAPI.getUserEventLogLastTs(evt) - - // Assert - assertEquals(lastTsExpected, lastTsActual) - verify(corestate.localDataStore).readUserEventLogLastTs(evt) - } - - } - @Test fun `test getUserEventLogHistory`() { // Arrange 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 05412761a..24579a826 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 @@ -212,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) @@ -223,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) 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 6a845aa81..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 @@ -593,48 +593,6 @@ class LocalDataStoreTest : BaseTestCase() { Mockito.verify(userEventLogDaoMock).readEventCountByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName) } - @Test - fun `test readUserEventLogLastTs returns correct timestamp when event exists`() { - // Given - val expectedTs = UserEventLogTestData.TestTimestamps.SAMPLE_TIMESTAMP - Mockito.`when`( - userEventLogDaoMock.readEventLastTsByDeviceIdAndNormalizedEventName( - deviceInfo.deviceID, - normalizedEventName - ) - ) - .thenReturn(expectedTs) - - // When - val result = localDataStoreWithConfig.readUserEventLogLastTs(eventName) - - // Then - assertEquals(expectedTs, result) - Mockito.verify(userEventLogDaoMock) - .readEventLastTsByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName) - } - - @Test - fun `test readUserEventLogFirstTs returns correct timestamp when event exists`() { - // Given - val expectedTs = UserEventLogTestData.TestTimestamps.SAMPLE_TIMESTAMP - Mockito.`when`( - userEventLogDaoMock.readEventFirstTsByDeviceIdAndNormalizedEventName( - deviceInfo.deviceID, - normalizedEventName - ) - ) - .thenReturn(expectedTs) - - // When - val result = localDataStoreWithConfig.readUserEventLogFirstTs(eventName) - - // Then - assertEquals(expectedTs, result) - Mockito.verify(userEventLogDaoMock) - .readEventFirstTsByDeviceIdAndNormalizedEventName(deviceInfo.deviceID, normalizedEventName) - } - @Test fun `test readUserEventLogs returns correct event list for device`() { // Given 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 bd606491b..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 @@ -6,6 +6,7 @@ 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 @@ -177,11 +178,17 @@ class SessionManagerTest : BaseTestCase() { fun `test setUserLastVisitTs`(){ val localDataStoreMockk = mockk() sessionManagerDef = SessionManager(configDef,coreMetaData,validator,localDataStoreMockk) - every { localDataStoreMockk.readUserEventLogLastTs(Constants.APP_LAUNCHED_EVENT) } returns 1000000L + 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(1000000L,sessionManagerDef.userLastVisitTs) - - + assertEquals(appLaunchedEventLog.lastTs, sessionManagerDef.userLastVisitTs) } 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 index 0c137f80b..9913096c4 100644 --- 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 @@ -301,12 +301,12 @@ class UserEventLogDAOImplTest { } @Test - fun `test readEventCountByDeviceIdAndNormalizedEventName returns minus one when event does not exist`() { + fun `test readEventCountByDeviceIdAndNormalizedEventName returns 0 when event does not exist`() { // When val result = userEventLogDAO.readEventCountByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) // Then - assertEquals(-1, result) + assertEquals(0, result) } @Test @@ -350,42 +350,19 @@ class UserEventLogDAOImplTest { } @Test - fun `test readEventFirstTsByDeviceIdAndNormalizedEventName returns minus one when event does not exist`() { - // When - val result = userEventLogDAO.readEventFirstTsByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) - - // Then - assertEquals(-1L, result) - } - - @Test - fun `test readEventFirstTsByDeviceIdAndNormalizedEventName when db error occurs`() { - // Given - val dbHelper = mockk() - every { dbHelper.readableDatabase } throws SQLiteException() - val dao = UserEventLogDAOImpl(dbHelper, logger, table) - - // When - val result = dao.readEventFirstTsByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) - - // Then - assertEquals(-1L, result) - } - - @Test - fun `test readEventFirstTsByDeviceIdAndNormalizedEventName returns correct timestamp after insert`() { + fun `test readEventByDeviceIdAndNormalizedEventName returns correct first timestamp after insert`() { // Given userEventLogDAO.insertEvent(testDeviceId, testEventName, testEventNameNormalized) // When - val result = userEventLogDAO.readEventFirstTsByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) + val eventLog = userEventLogDAO.readEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) // Then - assertEquals(MOCK_TIME, result) + assertEquals(MOCK_TIME, eventLog?.firstTs) } @Test - fun `test readEventFirstTsByDeviceIdAndNormalizedEventName returns same timestamp after updates`() { + fun `test readEventByDeviceIdAndNormalizedEventName returns same timestamp after updates`() { // Given userEventLogDAO.insertEvent(testDeviceId,testEventName, testEventNameNormalized) @@ -397,10 +374,10 @@ class UserEventLogDAOImplTest { userEventLogDAO.updateEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) // When - val result = userEventLogDAO.readEventFirstTsByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) + val eventLog = userEventLogDAO.readEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) // Then - assertEquals(MOCK_TIME, result) // First timestamp should remain same after updates + assertEquals(MOCK_TIME, eventLog?.firstTs) // First timestamp should remain same after updates verify { Utils.getNowInMillis() @@ -408,42 +385,19 @@ class UserEventLogDAOImplTest { } @Test - fun `test readEventLastTsByDeviceIdAndNormalizedEventName returns minus one when event does not exist`() { - // When - val result = userEventLogDAO.readEventLastTsByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) - - // Then - assertEquals(-1L, result) - } - - @Test - fun `test readEventLastTsByDeviceIdAndNormalizedEventName when db error occurs`() { - // Given - val dbHelper = mockk() - every { dbHelper.readableDatabase } throws SQLiteException() - val dao = UserEventLogDAOImpl(dbHelper, logger, table) - - // When - val result = dao.readEventLastTsByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) - - // Then - assertEquals(-1L, result) - } - - @Test - fun `test readEventLastTsByDeviceIdAndNormalizedEventName returns correct timestamp after insert`() { + fun `test readEventByDeviceIdAndNormalizedEventName returns correct last timestamp after insert`() { // Given userEventLogDAO.insertEvent(testDeviceId,testEventName, testEventNameNormalized) // When - val result = userEventLogDAO.readEventLastTsByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) + val eventLog = userEventLogDAO.readEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) // Then - assertEquals(MOCK_TIME, result) + assertEquals(MOCK_TIME, eventLog?.lastTs) } @Test - fun `test readEventLastTsByDeviceIdAndNormalizedEventName returns updated timestamp after updates`() { + fun `test readEventByDeviceIdAndNormalizedEventName returns updated last timestamp after updates`() { // Given userEventLogDAO.insertEvent(testDeviceId,testEventName, testEventNameNormalized) @@ -454,10 +408,10 @@ class UserEventLogDAOImplTest { userEventLogDAO.updateEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) // When - val result = userEventLogDAO.readEventLastTsByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) + val eventLog = userEventLogDAO.readEventByDeviceIdAndNormalizedEventName(testDeviceId, testEventNameNormalized) // Then - assertEquals(updateTime, result) // Last timestamp should be updated + assertEquals(updateTime, eventLog?.lastTs) // Last timestamp should be updated verify { Utils.getNowInMillis() From 9446d4f104a6aaed0c9412175519220fb2c0c212 Mon Sep 17 00:00:00 2001 From: Vassil Angelov <148857285+vasct@users.noreply.github.com> Date: Mon, 23 Dec 2024 14:09:25 +0200 Subject: [PATCH 101/120] bug(MC-2455): Fix file variables update to a null value (#710) Fix VarCacheTest tests --- .../clevertap/android/sdk/variables/Var.java | 3 + .../android/sdk/variables/VarCache.java | 10 +-- .../android/sdk/variables/VarCacheTest.kt | 73 +++++++++++++++++-- 3 files changed, 75 insertions(+), 11 deletions(-) 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/test/java/com/clevertap/android/sdk/variables/VarCacheTest.kt b/clevertap-core/src/test/java/com/clevertap/android/sdk/variables/VarCacheTest.kt index 1909527f9..fe0434287 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,5 +1,6 @@ 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 @@ -9,15 +10,15 @@ 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 @@ -40,6 +41,16 @@ 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) + } + @Test fun `test updateDiffsAndTriggerHandlers`() { ctVariables.init() @@ -348,6 +359,8 @@ class VarCacheTest : BaseTestCase() { assertEquals(2, var1.value()) verify { StorageHelper.getString(application, "variablesKey:" + cleverTapInstanceConfig.accountId, "{}") } + + unmockkStatic(StorageHelper::class) } @Test @@ -362,6 +375,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 +392,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 +405,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 +427,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 +441,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() } + } } From f0f36ee1daf14001683bedce85f16d693d9feb0c Mon Sep 17 00:00:00 2001 From: CTLalit Date: Mon, 23 Dec 2024 18:21:05 +0530 Subject: [PATCH 102/120] chore(SDK-4171): fixes compilation issue in conflict resolution from web editor. --- .../main/java/com/clevertap/android/sdk/AnalyticsManager.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 f07023f14..73632bef1 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 @@ -252,9 +252,7 @@ public void pushError(final String errorMessage, final int errorCode) { @Override public void pushEvent(String eventName, Map eventActions) { - if (eventName - - null || eventName.equals("")) { + if (eventName == null || eventName.equals("")) { return; } From 3fc1bfc01771506582f79e0c32a93e4d4b1fdd15 Mon Sep 17 00:00:00 2001 From: CTLalit Date: Mon, 23 Dec 2024 18:35:12 +0530 Subject: [PATCH 103/120] test(SDK-4171): fixes test constraints as per pr comment --- .../java/com/clevertap/android/sdk/AnalyticsManagerTest.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 e8ecb42bc..641639de9 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 @@ -233,7 +233,7 @@ class AnalyticsManagerTest { ) } - // setup again, 10000 ms has passed + // setup again, 2000 ms has passed every { timeProvider.currentTimeMillis() } returns 12000 // Send duplicate PN @@ -271,8 +271,7 @@ class AnalyticsManagerTest { ) } - // setup again, 10000 ms has passed - every { timeProvider.currentTimeMillis() } returns 20000 + every { timeProvider.currentTimeMillis() } returns 15001 // Send duplicate PN analyticsManagerSUT.pushNotificationClickedEvent(bundle) From 3870fc7a13b5957ee187dd5a8c13aff1a1ab9bda Mon Sep 17 00:00:00 2001 From: CTLalit Date: Mon, 23 Dec 2024 20:04:57 +0530 Subject: [PATCH 104/120] test(SDK-4171): adds test for wzrk_pid check --- .../android/sdk/AnalyticsManagerTest.kt | 55 ++++++++++++++++++- 1 file changed, 53 insertions(+), 2 deletions(-) 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 641639de9..2c4470489 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 @@ -218,7 +218,7 @@ class AnalyticsManagerTest { fun `clevertap does not process duplicate (same wzrk_id) PN clicked within 2 seconds - case 2nd click happens in 200ms`() { val json = notificationViewedJson(bundle) - every { timeProvider.currentTimeMillis() } returns 10000 + every { timeProvider.currentTimeMillis() } returns 0 // send PN first time analyticsManagerSUT.pushNotificationClickedEvent(bundle) @@ -234,7 +234,7 @@ class AnalyticsManagerTest { } // setup again, 2000 ms has passed - every { timeProvider.currentTimeMillis() } returns 12000 + every { timeProvider.currentTimeMillis() } returns 200 // Send duplicate PN analyticsManagerSUT.pushNotificationClickedEvent(bundle) @@ -289,6 +289,57 @@ class AnalyticsManagerTest { confirmVerified(eventQueueManager) } + @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) From 7136848f27bf22a63f7a0d6a8a0001477bb1a014 Mon Sep 17 00:00:00 2001 From: Vassil Angelov <148857285+vasct@users.noreply.github.com> Date: Tue, 24 Dec 2024 07:52:59 +0200 Subject: [PATCH 105/120] Use MockCTExecutors in VarCacheTest (#711) --- .../com/clevertap/android/sdk/variables/VarCacheTest.kt | 6 ++++++ 1 file changed, 6 insertions(+) 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 fe0434287..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 @@ -5,6 +5,8 @@ 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 @@ -31,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) @@ -49,6 +54,7 @@ class VarCacheTest : BaseTestCase() { Constants.CACHED_VARIABLES_KEY ) StorageHelper.removeImmediate(application, varCacheKey) + unmockkStatic(CTExecutorFactory::class) } @Test From 7d43eb9b8a030b58e16b44fd86748e98312096b5 Mon Sep 17 00:00:00 2001 From: CTLalit Date: Tue, 24 Dec 2024 12:42:30 +0530 Subject: [PATCH 106/120] feat(SDK-4228): handles show/hide big icon in notification - hides app icon (large) if the client passes custom KV with a key "wzrk_hide_icon" --- .../java/com/clevertap/android/sdk/Constants.java | 1 + .../pushnotification/CoreNotificationRenderer.java | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) 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 804610337..7e9008ca8 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 @@ -255,6 +255,7 @@ public interface Constants { String NOTIF_MSG = "nm"; String NOTIF_TITLE = "nt"; String NOTIF_ICON = "ico"; + String NOTIF_HIDE_APP_ICON = "wzrk_hide_icon"; String WZRK_ACTIONS = "wzrk_acts"; String WZRK_BIG_PICTURE = "wzrk_bp"; String WZRK_MSG_SUMMARY = "wzrk_nms"; 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..51585f9c3 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_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 From f2cf510da068d71247fed85a3b02ece71a553c94 Mon Sep 17 00:00:00 2001 From: anush Date: Mon, 16 Dec 2024 16:40:36 +0530 Subject: [PATCH 107/120] task(SDK-4165) - Fixes media3 related AbstractMethodError - Adds empty implementations for all default methods of Player.Listener --- .../sdk/video/inbox/ExoplayerHandle.kt | 2 +- .../video/inbox/ExoplayerPlayerListener.kt | 56 +++++++++++++++++++ .../android/sdk/video/inbox/Media3Handle.kt | 2 +- .../sdk/video/inbox/Media3PlayerListener.kt | 53 ++++++++++++++++++ 4 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 clevertap-core/src/main/java/com/clevertap/android/sdk/video/inbox/ExoplayerPlayerListener.kt create mode 100644 clevertap-core/src/main/java/com/clevertap/android/sdk/video/inbox/Media3PlayerListener.kt 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..ea6729b59 --- /dev/null +++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/video/inbox/ExoplayerPlayerListener.kt @@ -0,0 +1,56 @@ +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 onPlaybackStateChanged(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) {} +} From 49ecffeecf62fe1f7af1341a7025e72f6804285d Mon Sep 17 00:00:00 2001 From: anush Date: Mon, 16 Dec 2024 16:41:31 +0530 Subject: [PATCH 108/120] task(SDK-4165) - Removes onPlaybackStateChanged --- .../clevertap/android/sdk/video/inbox/ExoplayerPlayerListener.kt | 1 - 1 file changed, 1 deletion(-) 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 index ea6729b59..465ca439e 100644 --- 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 @@ -23,7 +23,6 @@ open class ExoplayerPlayerListener : Player.Listener { override fun onAvailableCommandsChanged(availableCommands: Player.Commands) {} override fun onTrackSelectionParametersChanged(parameters: TrackSelectionParameters) {} override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {} - override fun onPlaybackStateChanged(playbackState: Int) {} override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {} override fun onPlaybackSuppressionReasonChanged(playbackSuppressionReason: Int) {} override fun onIsPlayingChanged(isPlaying: Boolean) {} From 1b44c0f4d7c5bef9b6b436e0187dfbbb0566e090 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Tue, 24 Dec 2024 15:56:55 +0530 Subject: [PATCH 109/120] fix(multi_triggers) : fix missing in-app evaluation on notification viewed SDK-4229 --- .../android/sdk/events/EventQueueManager.java | 55 ++++++++++--------- .../android/sdk/EventQueueManagerTest.kt | 12 ++-- 2 files changed, 37 insertions(+), 30 deletions(-) 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 c56cafbdd..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 @@ -119,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 { @@ -315,29 +315,7 @@ public void processEvent(final Context context, final JSONObject event, final in localDataStore.setDataSyncFlag(event); baseDatabaseManager.queueEventToDB(context, event, 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); - } + initInAppEvaluation(context, event, eventType); scheduleQueueFlush(context); } catch (Throwable e) { @@ -346,7 +324,33 @@ public void processEvent(final Context context, final JSONObject event, final in } } - 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(); @@ -360,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); 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 24579a826..35109983b 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 @@ -242,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) } } @@ -263,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) } } @@ -280,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")) @@ -317,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"]) From e938a5c4c3916e4505e94a3a5b0ec26de94c4648 Mon Sep 17 00:00:00 2001 From: CTLalit Date: Tue, 24 Dec 2024 17:42:28 +0530 Subject: [PATCH 110/120] test(SDK-4171): adds tests for dedupecheckkey - extracts out logic to use key for dedupe check - wzrk_id/wzrk_pid based on flag from BE wzrk_dd - adds tests --- .../android/sdk/AnalyticsManager.java | 57 ++++++++------- .../android/sdk/AnalyticsManagerTest.kt | 71 +++++++++++++++---- 2 files changed, 90 insertions(+), 38 deletions(-) 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 73632bef1..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 @@ -489,7 +489,11 @@ public void pushNotificationClickedEvent(final Bundle extras) { } // 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() @@ -645,9 +649,11 @@ public void pushNotificationViewedEvent(Bundle extras) { } // Check for dupe notification views; if same notficationdId within specified time interval (2 secs) don't process - boolean isDuplicate = checkDuplicateNotificationIds(extras, + boolean isDuplicate = checkDuplicateNotificationIds( + dedupeCheckKey(extras), notificationViewedIdTagMap, - Constants.NOTIFICATION_VIEWED_ID_TAG_INTERVAL); + Constants.NOTIFICATION_VIEWED_ID_TAG_INTERVAL + ); if (isDuplicate) { config.getLogger().debug(config.getAccountId(), "Already processed Notification Viewed event for " + extras + ", dropping duplicate."); @@ -1143,8 +1149,31 @@ private void _pushMultiValue(ArrayList originalValues, String key, Strin } } + 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( - Bundle extras, + String notificationIdTag, HashMap notificationTagMap, int interval ) { @@ -1152,26 +1181,6 @@ private boolean checkDuplicateNotificationIds( // default to false; only return true if we are sure we've seen this one before boolean isDupe = false; try { - // 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); - } - long now = currentTimeProvider.currentTimeMillis(); if (notificationTagMap.containsKey(notificationIdTag)) { long timestamp; 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 2c4470489..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 @@ -56,13 +56,21 @@ class AnalyticsManagerTest { @MockK(relaxed = true) private lateinit var timeProvider: Clock - private val bundle = Bundle().apply { + 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 fun setUp() { MockKAnnotations.init(this) @@ -126,12 +134,12 @@ class AnalyticsManagerTest { @Test fun `clevertap does not process duplicate PN viewed within 2 seconds - case 2nd notif in 200ms`() { - val json = notificationViewedJson(bundle) + val json = notificationViewedJson(bundleIdCheck) every { timeProvider.currentTimeMillis() } returns 10000 // send PN first time - analyticsManagerSUT.pushNotificationViewedEvent(bundle) + analyticsManagerSUT.pushNotificationViewedEvent(bundleIdCheck) verify { eventQueueManager.queueEvent( @@ -147,7 +155,7 @@ class AnalyticsManagerTest { every { timeProvider.currentTimeMillis() } returns 10200 // Send duplicate PN - analyticsManagerSUT.pushNotificationViewedEvent(bundle) + analyticsManagerSUT.pushNotificationViewedEvent(bundleIdCheck) // verify it was not called again, one time was from before verify(exactly = 1) { @@ -165,12 +173,12 @@ class AnalyticsManagerTest { @Test fun `clevertap processes PN viewed for same wzrk_id if separated by a span of greater than 2 seconds`() { - val json = notificationViewedJson(bundle); + val json = notificationViewedJson(bundleIdCheck); every { timeProvider.currentTimeMillis() } returns 10000 // send PN first time - analyticsManagerSUT.pushNotificationViewedEvent(bundle) + analyticsManagerSUT.pushNotificationViewedEvent(bundleIdCheck) verify(exactly = 1) { eventQueueManager.queueEvent( @@ -186,7 +194,7 @@ class AnalyticsManagerTest { every { timeProvider.currentTimeMillis() } returns 20000 // Send duplicate PN - analyticsManagerSUT.pushNotificationViewedEvent(bundle) + analyticsManagerSUT.pushNotificationViewedEvent(bundleIdCheck) // verify queue event called again verify(exactly = 2) { @@ -206,7 +214,7 @@ class AnalyticsManagerTest { cleverTapInstanceConfig.isAnalyticsOnly = true // send PN first time - analyticsManagerSUT.pushNotificationClickedEvent(bundle) + analyticsManagerSUT.pushNotificationClickedEvent(bundleIdCheck) verify { eventQueueManager wasNot called @@ -217,11 +225,11 @@ class AnalyticsManagerTest { @Test fun `clevertap does not process duplicate (same wzrk_id) PN clicked within 2 seconds - case 2nd click happens in 200ms`() { - val json = notificationViewedJson(bundle) + val json = notificationViewedJson(bundleIdCheck) every { timeProvider.currentTimeMillis() } returns 0 // send PN first time - analyticsManagerSUT.pushNotificationClickedEvent(bundle) + analyticsManagerSUT.pushNotificationClickedEvent(bundleIdCheck) verify(exactly = 1) { eventQueueManager.queueEvent( @@ -237,7 +245,7 @@ class AnalyticsManagerTest { every { timeProvider.currentTimeMillis() } returns 200 // Send duplicate PN - analyticsManagerSUT.pushNotificationClickedEvent(bundle) + analyticsManagerSUT.pushNotificationClickedEvent(bundleIdCheck) // verify it was not called again, one time was from before verify(exactly = 1) { @@ -255,11 +263,11 @@ class AnalyticsManagerTest { @Test fun `clevertap processes PN clicked for same wzrk_id if separated by a span of greater than 5 seconds`() { - val json = notificationViewedJson(bundle); + val json = notificationViewedJson(bundleIdCheck); every { timeProvider.currentTimeMillis() } returns 10000 // send PN first time - analyticsManagerSUT.pushNotificationClickedEvent(bundle) + analyticsManagerSUT.pushNotificationClickedEvent(bundleIdCheck) verify(exactly = 1) { eventQueueManager.queueEvent( @@ -274,7 +282,7 @@ class AnalyticsManagerTest { every { timeProvider.currentTimeMillis() } returns 15001 // Send duplicate PN - analyticsManagerSUT.pushNotificationClickedEvent(bundle) + analyticsManagerSUT.pushNotificationClickedEvent(bundleIdCheck) // verify queue event called again verify(exactly = 2) { @@ -289,6 +297,41 @@ class AnalyticsManagerTest { 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`() { From 3726e321b675010c3b67ec41ff1f5393109931f1 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Wed, 18 Dec 2024 11:46:30 +0530 Subject: [PATCH 111/120] chore(multi_triggers) : update core changelog SDK-4219 --- templates/CTCORECHANGELOG.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/templates/CTCORECHANGELOG.md b/templates/CTCORECHANGELOG.md index 945381b49..e88bd99e0 100644 --- a/templates/CTCORECHANGELOG.md +++ b/templates/CTCORECHANGELOG.md @@ -1,5 +1,30 @@ ## CleverTap Android SDK CHANGE LOG +### Version 7.1.0 (December 23, 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 + * `getUserEventLogFirstTs()`: Get timestamp of first occurrence + * `getUserEventLogLastTs()`: Get timestamp of last occurrence + * `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 `getUserEventLogFirstTs()` instead for user-specific first occurrence timestamp + * `getLastTime()`: Use `getUserEventLogLastTs()` 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 + ### Version 7.0.3 (November 29, 2024) #### New Features From 42e209d85004487d9fd93082595634eab948c1eb Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Wed, 18 Dec 2024 11:46:39 +0530 Subject: [PATCH 112/120] chore(multi_triggers) : update main changelog SDK-4219 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38422b622..65b2e260d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## CHANGE LOG. +### December 23, 2024 +* [CleverTap Android SDK v7.1.0](docs/CTCORECHANGELOG.md) + ### November 29, 2024 * [CleverTap Android SDK v7.0.3](docs/CTCORECHANGELOG.md) From 184c133a3d5ed84d01597e9b25bd25194794834f Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Wed, 18 Dec 2024 11:47:00 +0530 Subject: [PATCH 113/120] chore(multi_triggers) : update EXAMPLES.md SDK-4219 --- templates/EXAMPLES.md | 92 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/templates/EXAMPLES.md b/templates/EXAMPLES.md index 71aef4467..c523af282 100644 --- a/templates/EXAMPLES.md +++ b/templates/EXAMPLES.md @@ -747,4 +747,96 @@ 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(); +} +``` +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 +} +``` +Get count of event occurrences + +Java +```java +int eventCount = clevertap.getUserEventLogCount("Product Viewed"); +``` +Kotlin +```kotlin +val eventCount = clevertap.getUserEventLogCount("Product Viewed") +``` +Get timestamp of first occurrence + +Java +```java +long firstTs = clevertap.getUserEventLogFirstTs("Product Viewed"); +``` +```kotlin +val firstTs = clevertap.getUserEventLogFirstTs("Product Viewed") +``` +Get timestamp of last occurrence + +Java +```java +long lastTs = clevertap.getUserEventLogLastTs("Product Viewed"); +``` +Kotlin +```kotlin +val lastTs = clevertap.getUserEventLogLastTs("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 +} ``` \ No newline at end of file From feff82fc5ec8f1a4c0323cd1bfed47a9806cc8e6 Mon Sep 17 00:00:00 2001 From: anush Date: Wed, 18 Dec 2024 14:44:40 +0530 Subject: [PATCH 114/120] chore(SDK-4165) - Adds changelog for AbstractMethodError --- templates/CTCORECHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/templates/CTCORECHANGELOG.md b/templates/CTCORECHANGELOG.md index e88bd99e0..577a2bae1 100644 --- a/templates/CTCORECHANGELOG.md +++ b/templates/CTCORECHANGELOG.md @@ -25,6 +25,10 @@ * `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. + + ### Version 7.0.3 (November 29, 2024) #### New Features From 3f7ae9e663f85e9a7e8cc4faf2c891195311e102 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Tue, 24 Dec 2024 17:34:58 +0530 Subject: [PATCH 115/120] chore(multi_triggers) : remove firstTs and lastTs related methods from changelog and EXAMPLES.md SDK-4219 --- CHANGELOG.md | 2 +- templates/CTCORECHANGELOG.md | 6 ++---- templates/EXAMPLES.md | 25 ++++--------------------- 3 files changed, 7 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65b2e260d..cc42a758a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## CHANGE LOG. -### December 23, 2024 +### December 24, 2024 * [CleverTap Android SDK v7.1.0](docs/CTCORECHANGELOG.md) ### November 29, 2024 diff --git a/templates/CTCORECHANGELOG.md b/templates/CTCORECHANGELOG.md index 577a2bae1..ba5e6cc5e 100644 --- a/templates/CTCORECHANGELOG.md +++ b/templates/CTCORECHANGELOG.md @@ -8,8 +8,6 @@ * 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 - * `getUserEventLogFirstTs()`: Get timestamp of first occurrence - * `getUserEventLogLastTs()`: Get timestamp of last occurrence * `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 @@ -19,8 +17,8 @@ * **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 `getUserEventLogFirstTs()` instead for user-specific first occurrence timestamp - * `getLastTime()`: Use `getUserEventLogLastTs()` instead for user-specific last occurrence timestamp + * `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 diff --git a/templates/EXAMPLES.md b/templates/EXAMPLES.md index c523af282..dbb029f4a 100644 --- a/templates/EXAMPLES.md +++ b/templates/EXAMPLES.md @@ -761,6 +761,8 @@ if (eventLog != null) { long lastTime = eventLog.getLastTs(); int count = eventLog.getCountOfEvents(); String deviceId = eventLog.getDeviceID(); +} else { + System.out.println("Event not performed"); } ``` Kotlin @@ -771,7 +773,7 @@ clevertap.getUserEventLog("Product Viewed")?.let { eventLog -> val lastTime = eventLog.lastTs val count = eventLog.countOfEvents val deviceId = eventLog.deviceID -} +} ?: println("Event not performed") ``` Get count of event occurrences @@ -783,25 +785,6 @@ Kotlin ```kotlin val eventCount = clevertap.getUserEventLogCount("Product Viewed") ``` -Get timestamp of first occurrence - -Java -```java -long firstTs = clevertap.getUserEventLogFirstTs("Product Viewed"); -``` -```kotlin -val firstTs = clevertap.getUserEventLogFirstTs("Product Viewed") -``` -Get timestamp of last occurrence - -Java -```java -long lastTs = clevertap.getUserEventLogLastTs("Product Viewed"); -``` -Kotlin -```kotlin -val lastTs = clevertap.getUserEventLogLastTs("Product Viewed") -``` Get user's last app visit timestamp Java @@ -838,5 +821,5 @@ Kotlin val eventHistory = clevertap.userEventLogHistory eventHistory?.forEach { (eventName, log) -> // Process event details -} +} ?: println("Events not performed") ``` \ No newline at end of file From 41fd1dc0c1f0b4d3d1bd5affbde60e9884ffc697 Mon Sep 17 00:00:00 2001 From: piyush-kukadiya Date: Tue, 24 Dec 2024 17:36:20 +0530 Subject: [PATCH 116/120] chore(multi_triggers) : change release date in CTCORECHANGELOG.md SDK-4219 --- templates/CTCORECHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/CTCORECHANGELOG.md b/templates/CTCORECHANGELOG.md index ba5e6cc5e..dec5d604c 100644 --- a/templates/CTCORECHANGELOG.md +++ b/templates/CTCORECHANGELOG.md @@ -1,6 +1,6 @@ ## CleverTap Android SDK CHANGE LOG -### Version 7.1.0 (December 23, 2024) +### Version 7.1.0 (December 24, 2024) #### New Features From 600649671187e5fa86f4343022577a5261954990 Mon Sep 17 00:00:00 2001 From: CTLalit Date: Tue, 24 Dec 2024 19:19:36 +0530 Subject: [PATCH 117/120] feat(SDK-4228): adds changes to key as per comments. --- .../src/main/java/com/clevertap/android/sdk/Constants.java | 2 +- .../android/sdk/pushnotification/CoreNotificationRenderer.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 911d16e93..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 @@ -229,7 +229,7 @@ public interface Constants { String NOTIF_MSG = "nm"; String NOTIF_TITLE = "nt"; String NOTIF_ICON = "ico"; - String NOTIF_HIDE_APP_ICON = "wzrk_hide_icon"; + 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"; 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 51585f9c3..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 @@ -123,7 +123,7 @@ public Builder renderNotification(final Bundle extras, final Context context, .setSmallIcon(smallIcon); String icoPath = extras.getString(Constants.NOTIF_ICON);// uncommon - boolean showIcon = !"true".equalsIgnoreCase(extras.getString(Constants.NOTIF_HIDE_APP_ICON)); + boolean showIcon = !"true".equalsIgnoreCase(extras.getString(Constants.NOTIF_HIDE_APP_LARGE_ICON)); if (showIcon) { // uncommon nb.setLargeIcon(Utils.getNotificationBitmapWithTimeout(icoPath, true, context, From 8e8a520221c459228c5bcfcc6afa9a52c8ae85f4 Mon Sep 17 00:00:00 2001 From: CTLalit Date: Tue, 24 Dec 2024 18:23:22 +0530 Subject: [PATCH 118/120] docs(SDK-4178): changelog for core Release 7.1.0 --- README.md | 8 ++-- docs/CTCORECHANGELOG.md | 28 ++++++++++++++ docs/CTGEOFENCE.md | 2 +- docs/CTPUSHTEMPLATES.md | 2 +- docs/EXAMPLES.md | 75 ++++++++++++++++++++++++++++++++++++ templates/CTCORECHANGELOG.md | 1 + 6 files changed, 110 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 4faca4f6c..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,7 +46,7 @@ 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: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/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/templates/CTCORECHANGELOG.md b/templates/CTCORECHANGELOG.md index dec5d604c..be1630e7d 100644 --- a/templates/CTCORECHANGELOG.md +++ b/templates/CTCORECHANGELOG.md @@ -25,6 +25,7 @@ #### 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) From bad0a2714e3250a81bfb64f9267dd35b8fc8eccf Mon Sep 17 00:00:00 2001 From: CTLalit Date: Tue, 24 Dec 2024 19:16:56 +0530 Subject: [PATCH 119/120] docs(SDK-4178): adds changelog for hide icon change --- templates/CTCORECHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/templates/CTCORECHANGELOG.md b/templates/CTCORECHANGELOG.md index be1630e7d..acbed87fc 100644 --- a/templates/CTCORECHANGELOG.md +++ b/templates/CTCORECHANGELOG.md @@ -11,6 +11,7 @@ * `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 From 17071405dc1a151f66a5c0cfe68127ce785f26f9 Mon Sep 17 00:00:00 2001 From: CTLalit Date: Tue, 24 Dec 2024 19:57:15 +0530 Subject: [PATCH 120/120] chore(SDK-4178): updates sample app dependencies --- sample/build.gradle | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sample/build.gradle b/sample/build.gradle index 70b2fd14f..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" } @@ -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")