diff --git a/.github/mini_flows/build_code_debug/action.yml b/.github/mini_flows/build_code_debug/action.yml
index 5ecefdaac..2bfc95a30 100644
--- a/.github/mini_flows/build_code_debug/action.yml
+++ b/.github/mini_flows/build_code_debug/action.yml
@@ -9,7 +9,7 @@ runs:
- name: Upload AAR and apk files
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: assembleDebug-clevertap-core
path: clevertap-core/build/outputs/aar
@@ -23,7 +23,7 @@ runs:
- name: Upload AAR and apk files
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: assembleDebug-clevertap-geofence
path: clevertap-geofence/build/outputs/aar
@@ -37,7 +37,7 @@ runs:
- name: Upload AAR and apk files
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: assembleDebug-clevertap-hms
path: clevertap-hms/build/outputs/aar
@@ -51,7 +51,7 @@ runs:
- name: Upload AAR and apk files
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: assembleDebug-clevertap-pushTemplates
path: clevertap-pushtemplates/build/outputs/aar
diff --git a/.github/mini_flows/build_code_release/action.yml b/.github/mini_flows/build_code_release/action.yml
index 9292ef0cb..ec9d20dac 100644
--- a/.github/mini_flows/build_code_release/action.yml
+++ b/.github/mini_flows/build_code_release/action.yml
@@ -9,7 +9,7 @@ runs:
- name: Upload AAR and apk files
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: assembleRelease-clevertap-core
path: clevertap-core/build/outputs/aar
@@ -23,7 +23,7 @@ runs:
- name: Upload AAR and apk files
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: assembleRelease-clevertap-geofence
path: clevertap-geofence/build/outputs/aar
@@ -37,7 +37,7 @@ runs:
- name: Upload AAR and apk files
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: assembleRelease-clevertap-hms
path: clevertap-hms/build/outputs/aar
@@ -51,7 +51,7 @@ runs:
- name: Upload AAR and apk files
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: assembleRelease-clevertap-pushTemplates
path: clevertap-pushtemplates/build/outputs/aar
\ No newline at end of file
diff --git a/.github/mini_flows/codechecks_checkstyle/action.yml b/.github/mini_flows/codechecks_checkstyle/action.yml
index 2d6361805..028a11512 100644
--- a/.github/mini_flows/codechecks_checkstyle/action.yml
+++ b/.github/mini_flows/codechecks_checkstyle/action.yml
@@ -9,7 +9,7 @@ runs:
- name: Upload AAR and apk files
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: checkstyle-clevertap-core
path: clevertap-core/build/reports/checkstyle
@@ -23,7 +23,7 @@ runs:
- name: Upload AAR and apk files
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: checkstyle-clevertap-geofence
path: clevertap-geofence/build/reports/checkstyle
@@ -37,7 +37,7 @@ runs:
- name: Upload AAR and apk files
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: checkstyle-clevertap-hms
path: clevertap-hms/build/reports/checkstyle
@@ -51,7 +51,7 @@ runs:
- name: Upload AAR and apk files
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: checkstyle-clevertap-pushTemplates
path: clevertap-pushtemplates/build/reports/checkstyle
diff --git a/.github/mini_flows/codechecks_detekt/action.yml b/.github/mini_flows/codechecks_detekt/action.yml
index a138e879c..bc288f3cd 100644
--- a/.github/mini_flows/codechecks_detekt/action.yml
+++ b/.github/mini_flows/codechecks_detekt/action.yml
@@ -9,7 +9,7 @@ runs:
- name: Upload AAR and apk files
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: detekt-clevertap-core
path: clevertap-core/build/reports/detekt
@@ -23,7 +23,7 @@ runs:
- name: Upload AAR and apk files
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: detekt-clevertap-geofence
path: clevertap-geofence/build/reports/detekt
@@ -37,7 +37,7 @@ runs:
- name: Upload AAR and apk files
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: detekt-clevertap-hms
path: clevertap-hms/build/reports/detekt
@@ -51,7 +51,7 @@ runs:
- name: Upload AAR and apk files
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: detekt-clevertap-pushTemplates
path: clevertap-pushtemplates/build/reports/detekt
diff --git a/.github/mini_flows/lint/action.yml b/.github/mini_flows/lint/action.yml
index e358dabe6..ebf552546 100644
--- a/.github/mini_flows/lint/action.yml
+++ b/.github/mini_flows/lint/action.yml
@@ -9,7 +9,7 @@ runs:
- name: Upload AAR and apk files
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: lint-clevertap-core
path: clevertap-core/build/reports/lint-results-debug.html
@@ -23,7 +23,7 @@ runs:
- name: Upload AAR and apk files
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: lint-clevertap-geofence
path: clevertap-geofence/build/reports/lint-results-debug.html
@@ -37,7 +37,7 @@ runs:
- name: Upload AAR and apk files
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: lint-clevertap-hms
path: clevertap-hms/build/reports/lint-results-debug.html
@@ -51,7 +51,7 @@ runs:
- name: Upload AAR and apk files
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: lint-clevertap-pushTemplates
path: clevertap-pushtemplates/build/reports/lint-results-debug.html
\ No newline at end of file
diff --git a/.github/mini_flows/test_and_coverage_debug/action.yml b/.github/mini_flows/test_and_coverage_debug/action.yml
index 46e835c3d..45164c49b 100644
--- a/.github/mini_flows/test_and_coverage_debug/action.yml
+++ b/.github/mini_flows/test_and_coverage_debug/action.yml
@@ -9,14 +9,14 @@ runs:
- name: Upload1
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: TestReportDebug-clevertap-core
path: clevertap-core/build/reports/tests
- name: Upload2
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: JacocoReportDebug-clevertap-core
path: clevertap-core/build/reports/jacoco
@@ -30,14 +30,14 @@ runs:
- name: Upload1
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: TestReportDebug-clevertap-geofence
path: clevertap-geofence/build/reports/tests
- name: Upload2
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: JacocoReportDebug-clevertap-geofence
path: clevertap-geofence/build/reports/jacoco
@@ -51,14 +51,14 @@ runs:
- name: Upload1
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: TestReportDebug-clevertap-hms
path: clevertap-hms/build/reports/tests
- name: Upload2
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: JacocoReportDebug-clevertap-hms
path: clevertap-hms/build/reports/jacoco
@@ -73,14 +73,14 @@ runs:
- name: Upload1
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: TestReportDebug-clevertap-pushtemplates
path: clevertap-pushtemplates/build/reports/tests
- name: Upload2
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: JacocoReportDebug-clevertap-pushtemplates
path: clevertap-pushtemplates/build/reports/jacoco
diff --git a/.github/mini_flows/test_and_coverage_release/action.yml b/.github/mini_flows/test_and_coverage_release/action.yml
index 6791b6739..480f8ad3e 100644
--- a/.github/mini_flows/test_and_coverage_release/action.yml
+++ b/.github/mini_flows/test_and_coverage_release/action.yml
@@ -8,14 +8,14 @@ runs:
- name: Upload1
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: TestReportRelease-clevertap-core
path: clevertap-core/build/reports/tests
- name: Upload2
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: JacocoReportRelease-clevertap-core
path: clevertap-core/build/reports/jacoco
@@ -29,14 +29,14 @@ runs:
- name: Upload1
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: TestReportRelease-clevertap-geofence
path: clevertap-geofence/build/reports/tests
- name: Upload2
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: JacocoReportRelease-clevertap-geofence
path: clevertap-geofence/build/reports/jacoco
@@ -50,14 +50,14 @@ runs:
- name: Upload1
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: TestReportRelease-clevertap-hms
path: clevertap-hms/build/reports/tests
- name: Upload2
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: JacocoReportRelease-clevertap-hms
path: clevertap-hms/build/reports/jacoco
@@ -71,14 +71,14 @@ runs:
- name: Upload1
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: TestReportRelease-clevertap-pushtemplates
path: clevertap-pushtemplates/build/reports/tests
- name: Upload2
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: JacocoReportRelease-clevertap-pushtemplates
path: clevertap-pushtemplates/build/reports/jacoco
diff --git a/.github/mini_flows/test_debug/action.yml b/.github/mini_flows/test_debug/action.yml
index b11ef0729..304cdb867 100644
--- a/.github/mini_flows/test_debug/action.yml
+++ b/.github/mini_flows/test_debug/action.yml
@@ -12,7 +12,7 @@ runs:
- name: Upload Unit tests
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: unit-tests-results
path: |
diff --git a/.github/mini_flows/test_release/action.yml b/.github/mini_flows/test_release/action.yml
index b11ef0729..304cdb867 100644
--- a/.github/mini_flows/test_release/action.yml
+++ b/.github/mini_flows/test_release/action.yml
@@ -12,7 +12,7 @@ runs:
- name: Upload Unit tests
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: unit-tests-results
path: |
diff --git a/.github/workflows/build_sample_app.yml b/.github/workflows/build_sample_app.yml
index a0c68f7f5..5daa09bd2 100644
--- a/.github/workflows/build_sample_app.yml
+++ b/.github/workflows/build_sample_app.yml
@@ -98,21 +98,21 @@ jobs:
- name: Upload Debug APK
if: ${{ inputs.build_debug_apk}}
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: sample-debug-apk
path: sample/build/outputs/apk/remote/debug/*.apk
- name: Upload Debug Bundle
if: ${{ inputs.build_debug_bundle}}
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: sample-debug-bundle
path: sample/build/outputs/bundle/remoteDebug/*.aab
- name: Upload Signed Release APK
if: ${{ inputs.build_signed_release_apk}}
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: sample-signed-release-apk
path: sample/build/outputs/apk/remote/signed/*.apk
@@ -120,7 +120,7 @@ jobs:
- name: Upload Signed Release Bundle
if: ${{ inputs.build_signed_release_bundle}}
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: sample-signed-release-bundle
path: sample/build/outputs/bundle/remoteSigned/*.aab
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 58c9b2992..eebe20b24 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,26 @@
## CHANGE LOG.
+### January 21, 2025
+* [CleverTap Android SDK v7.2.2](docs/CTCORECHANGELOG.md)
+
+### January 16, 2025
+* [CleverTap Android SDK v7.2.1](docs/CTCORECHANGELOG.md)
+
+### January 7, 2025
+* [CleverTap Android SDK v7.2.0](docs/CTCORECHANGELOG.md)
+* [CleverTap Push Templates SDK v1.3.0](docs/CTPUSHTEMPLATESCHANGELOG.md).
+* [CleverTap Geofence SDK v1.4.0](docs/CTGEOFENCECHANGELOG.md)
+* [CleverTap Huawei Push SDK v1.4.0](docs/CTHUAWEIPUSHCHANGELOG.md)
+
+### January 29, 2025
+* [CleverTap Android SDK v7.1.2](docs/CTCORECHANGELOG.md)
+
+### December 24, 2024
+* [CleverTap Android SDK v7.1.0](docs/CTCORECHANGELOG.md)
+
+### 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..68e9962db 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.2.2"
}
```
@@ -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.2.2", ext: 'aar')
}
```
@@ -46,10 +46,10 @@ Add the Firebase Messaging library and Android Support Library v4 as dependencie
```groovy
dependencies {
- implementation "com.clevertap.android:clevertap-android-sdk:7.0.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.clevertap.android:clevertap-android-sdk:7.2.2"
+ implementation "androidx.core:core:1.13.0"
+ implementation "com.google.firebase:firebase-messaging:24.0.0"
+ 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,8 +70,8 @@ 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.google.gms:google-services:4.4.0"
+ classpath "com.android.tools.build:gradle:8.6.0"
+ classpath "com.google.gms:google-services:4.4.2"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
diff --git a/build.gradle b/build.gradle
index 60e095f80..c9937b1c8 100644
--- a/build.gradle
+++ b/build.gradle
@@ -21,6 +21,7 @@ buildscript {
plugins {
alias(libs.plugins.sonarqube)
+ alias(libs.plugins.compose.compiler) apply false
}
allprojects {
repositories {
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/assets/image_interstitial.html b/clevertap-core/src/main/assets/image_interstitial.html
index 51d6237fe..bf59a29df 100644
--- a/clevertap-core/src/main/assets/image_interstitial.html
+++ b/clevertap-core/src/main/assets/image_interstitial.html
@@ -1,37 +1,28 @@
-
+
-
-
![]()
-
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..51659eff6 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;
@@ -37,45 +38,36 @@
public class AnalyticsManager extends BaseAnalyticsManager {
private final CTLockManager ctLockManager;
-
private final HashMap
installReferrerMap = new HashMap<>(8);
-
private final BaseEventQueueManager baseEventQueueManager;
-
private final BaseCallbackManager callbackManager;
-
private final CleverTapInstanceConfig config;
-
private final Context context;
-
private final ControllerManager controllerManager;
-
private final CoreMetaData coreMetaData;
-
private final DeviceInfo deviceInfo;
-
private final ValidationResultStack validationResultStack;
-
private final Validator validator;
-
private final InAppResponse inAppResponse;
-
- private final HashMap notificationIdTagMap = new HashMap<>();
-
+ private final Clock currentTimeProvider;
private final Object notificationMapLock = new Object();
- private final HashMap notificationViewedIdTagMap = new HashMap<>();
-
- AnalyticsManager(Context context,
- CleverTapInstanceConfig config,
- BaseEventQueueManager baseEventQueueManager,
- Validator validator,
- ValidationResultStack validationResultStack,
- CoreMetaData coreMetaData,
- DeviceInfo deviceInfo,
- BaseCallbackManager callbackManager, ControllerManager controllerManager,
- final CTLockManager ctLockManager,
- InAppResponse inAppResponse) {
+ private final HashMap notificationIdTagMap = new HashMap<>();
+ private final HashMap notificationViewedIdTagMap = new HashMap<>();
+
+ AnalyticsManager(
+ Context context,
+ CleverTapInstanceConfig config,
+ BaseEventQueueManager baseEventQueueManager,
+ Validator validator,
+ ValidationResultStack validationResultStack,
+ CoreMetaData coreMetaData,
+ DeviceInfo deviceInfo,
+ BaseCallbackManager callbackManager, ControllerManager controllerManager,
+ final CTLockManager ctLockManager,
+ InAppResponse inAppResponse,
+ Clock currentTimeProvider
+ ) {
this.context = context;
this.config = config;
this.baseEventQueueManager = baseEventQueueManager;
@@ -87,6 +79,7 @@ public class AnalyticsManager extends BaseAnalyticsManager {
this.ctLockManager = ctLockManager;
this.controllerManager = controllerManager;
this.inAppResponse = inAppResponse;
+ this.currentTimeProvider = currentTimeProvider;
}
@Override
@@ -464,8 +457,7 @@ public void pushNotificationClickedEvent(final Bundle extras) {
}
boolean shouldProcess = (accountId == null && config.isDefaultInstance())
- || config.getAccountId()
- .equals(accountId);
+ || config.getAccountId().equals(accountId);
if (!shouldProcess) {
config.getLogger().debug(config.getAccountId(),
@@ -474,59 +466,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)) {
- 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;
}
@@ -537,40 +482,29 @@ public Void call() {
if (!extras.containsKey(Constants.NOTIFICATION_ID_TAG) || (extras.getString(Constants.NOTIFICATION_ID_TAG) == null)) {
config.getLogger().debug(config.getAccountId(),
- "Push notification ID Tag is null, not processing Notification Clicked event for: " + extras
- .toString());
+ "Push notification ID Tag is null, not processing Notification Clicked event for: " + extras);
return;
}
// Check for dupe notification views; if same notficationdId within specified time interval (5 secs) don't process
- boolean isDuplicate = checkDuplicateNotificationIds(extras, notificationIdTagMap, Constants.NOTIFICATION_ID_TAG_INTERVAL);
+ boolean isDuplicate = checkDuplicateNotificationIds(
+ dedupeCheckKey(extras),
+ notificationIdTagMap,
+ Constants.NOTIFICATION_ID_TAG_INTERVAL
+ );
if (isDuplicate) {
config.getLogger().debug(config.getAccountId(),
- "Already processed Notification Clicked event for " + extras.toString()
+ "Already processed Notification Clicked event for " + extras
+ ", dropping duplicate.");
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.notificationClickedJson(extras);
- event.put("evtName", Constants.NOTIFICATION_CLICKED_EVENT_NAME);
- event.put("evtData", notif);
baseEventQueueManager.queueEvent(context, event, Constants.RAISED_EVENT);
-
- try {
- coreMetaData.setWzrkParams(getWzrkFields(extras));
- } catch (Throwable t) {
- // no-op
- }
+ coreMetaData.setWzrkParams(AnalyticsManagerBundler.wzrkBundleToJson(extras));
} catch (Throwable t) {
// We won't get here
}
@@ -582,6 +516,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);
@@ -623,7 +613,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) {
@@ -649,33 +639,28 @@ public void pushNotificationViewedEvent(Bundle extras) {
return;
}
- if (!extras.containsKey(Constants.NOTIFICATION_ID_TAG) || (extras.getString(Constants.NOTIFICATION_ID_TAG)
- == null)) {
+ if (!extras.containsKey(Constants.NOTIFICATION_ID_TAG)
+ || (extras.getString(Constants.NOTIFICATION_ID_TAG) == null)) {
config.getLogger().debug(config.getAccountId(),
- "Push notification ID Tag is null, not processing Notification Viewed event for: " + extras
- .toString());
+ "Push notification ID Tag is null, not processing Notification Viewed event for: " + extras);
return;
}
// Check for dupe notification views; if same notficationdId within specified time interval (2 secs) don't process
- boolean isDuplicate = checkDuplicateNotificationIds(extras, notificationViewedIdTagMap,
- Constants.NOTIFICATION_VIEWED_ID_TAG_INTERVAL);
+ boolean isDuplicate = checkDuplicateNotificationIds(
+ dedupeCheckKey(extras),
+ notificationViewedIdTagMap,
+ Constants.NOTIFICATION_VIEWED_ID_TAG_INTERVAL
+ );
if (isDuplicate) {
config.getLogger().debug(config.getAccountId(),
- "Already processed Notification Viewed event for " + extras.toString() + ", dropping duplicate.");
+ "Already processed Notification Viewed event for " + extras + ", dropping duplicate.");
return;
}
- config.getLogger().debug("Recording Notification Viewed event for notification: " + extras.toString());
+ config.getLogger().debug("Recording Notification Viewed event for notification: " + extras);
- JSONObject event = new JSONObject();
- try {
- JSONObject notif = getWzrkFields(extras);
- event.put("evtName", Constants.NOTIFICATION_VIEWED_EVENT_NAME);
- event.put("evtData", notif);
- } catch (Throwable ignored) {
- //no-op
- }
+ JSONObject event = AnalyticsManagerBundler.notificationViewedJson(extras);
baseEventQueueManager.queueEvent(context, event, Constants.NV_EVENT);
}
@@ -1088,7 +1073,7 @@ private void _push(Map profile) {
}
config.getLogger()
- .verbose(config.getAccountId(), "Constructed custom profile: " + customProfile.toString());
+ .verbose(config.getAccountId(), "Constructed custom profile: " + customProfile);
baseEventQueueManager.pushBasicProfile(customProfile, false);
@@ -1155,25 +1140,50 @@ private void _pushMultiValue(ArrayList originalValues, String key, Strin
baseEventQueueManager.pushBasicProfile(fields, false);
config.getLogger()
- .verbose(config.getAccountId(), "Constructed multi-value profile push: " + fields.toString());
+ .verbose(config.getAccountId(), "Constructed multi-value profile push: " + fields);
} catch (Throwable t) {
config.getLogger().verbose(config.getAccountId(), "Error pushing multiValue for key " + key, t);
}
}
- private boolean checkDuplicateNotificationIds(Bundle extras, HashMap notificationTagMap,
- int interval) {
+ String dedupeCheckKey(Bundle extras) {
+ // This flag is used so that we can release in phased manner, eventually the check has to go away.
+ Object doDedupeCheck = extras.get(Constants.WZRK_DEDUPE);
+
+ boolean check = false;
+ if (doDedupeCheck != null) {
+ if (doDedupeCheck instanceof String) {
+ check = "true".equalsIgnoreCase((String) doDedupeCheck);
+ }
+ if (doDedupeCheck instanceof Boolean) {
+ check = (Boolean) doDedupeCheck;
+ }
+ }
+
+ String notificationIdTag;
+ if (check) {
+ notificationIdTag = extras.getString(Constants.WZRK_PUSH_ID);
+ } else {
+ notificationIdTag = extras.getString(Constants.NOTIFICATION_ID_TAG);
+ }
+ return notificationIdTag;
+ }
+
+ private boolean checkDuplicateNotificationIds(
+ String notificationIdTag,
+ HashMap notificationTagMap,
+ int interval
+ ) {
synchronized (notificationMapLock) {
// default to false; only return true if we are sure we've seen this one before
boolean isDupe = false;
try {
- String notificationIdTag = extras.getString(Constants.NOTIFICATION_ID_TAG);
- long now = System.currentTimeMillis();
+ long now = currentTimeProvider.currentTimeMillis();
if (notificationTagMap.containsKey(notificationIdTag)) {
long timestamp;
// noinspection ConstantConditions
- timestamp = (Long) notificationTagMap.get(notificationIdTag);
+ timestamp = notificationTagMap.get(notificationIdTag);
// same notificationId within time internal treat as dupe
if (now - timestamp < interval) {
isDupe = true;
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/AnalyticsManagerBundler.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/AnalyticsManagerBundler.kt
new file mode 100644
index 000000000..bf1a412c4
--- /dev/null
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/AnalyticsManagerBundler.kt
@@ -0,0 +1,55 @@
+package com.clevertap.android.sdk
+
+import android.os.Bundle
+import org.json.JSONException
+import org.json.JSONObject
+
+object AnalyticsManagerBundler {
+
+ @Throws(JSONException::class)
+ @JvmStatic
+ fun wzrkBundleToJson(root: Bundle): JSONObject {
+ val fields = JSONObject()
+ for (s in root.keySet()) {
+ val o = root[s]
+ if (o is Bundle) {
+ val wzrkFields = wzrkBundleToJson(o)
+ val keys = wzrkFields.keys()
+ while (keys.hasNext()) {
+ val k = keys.next()
+ fields.put(k, wzrkFields[k])
+ }
+ } else if (s.startsWith(Constants.WZRK_PREFIX)) {
+ fields.put(s, root[s])
+ }
+ }
+
+ return fields
+ }
+
+ @JvmStatic
+ fun notificationViewedJson(root: Bundle): JSONObject {
+ val event = JSONObject()
+ try {
+ val notif = wzrkBundleToJson(root)
+ event.put("evtName", Constants.NOTIFICATION_VIEWED_EVENT_NAME)
+ event.put("evtData", notif)
+ } catch (ignored: Throwable) {
+ //no-op
+ }
+ return event
+ }
+
+ @JvmStatic
+ fun notificationClickedJson(root: Bundle): JSONObject {
+ val event = JSONObject()
+ try {
+ val notif = wzrkBundleToJson(root)
+ event.put("evtName", Constants.NOTIFICATION_CLICKED_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/CTXtensions.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/CTXtensions.kt
index 0ba2c9452..a2d043991 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CTXtensions.kt
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/CTXtensions.kt
@@ -10,10 +10,16 @@ import android.content.SharedPreferences
import android.location.Location
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
+import android.view.View
+import android.view.ViewGroup.MarginLayoutParams
import androidx.annotation.MainThread
import androidx.annotation.RequiresApi
import androidx.annotation.WorkerThread
import androidx.core.app.NotificationManagerCompat
+import androidx.core.graphics.Insets
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updateLayoutParams
import com.clevertap.android.sdk.events.EventGroup.PUSH_NOTIFICATION_VIEWED
import com.clevertap.android.sdk.task.CTExecutorFactory
import org.json.JSONArray
@@ -307,3 +313,40 @@ fun String?.isNotNullAndBlank() : Boolean {
contract { returns(true) implies (this@isNotNullAndBlank != null) }
return isNullOrBlank().not()
}
+
+/**
+ * Adjusts the margins of the view based on the system bar insets (such as the status bar, navigation bar,
+ * or display cutout) using the provided margin adjustment logic.
+ *
+ * This function sets a listener on the view to handle window insets and invokes the provided `marginAdjuster`
+ * block to allow custom margin adjustments. The `marginAdjuster` lambda receives the system bar insets and
+ * the view's margin layout parameters, allowing the caller to modify the margins as needed.
+ *
+ * @param marginAdjuster A lambda function that takes two parameters:
+ * - `bars`: The insets for system bars and display cutouts, representing the space occupied by UI elements
+ * such as the status bar or navigation bar.
+ * - `mlp`: The `MarginLayoutParams` of the view, which can be modified to adjust the margins based on the insets.
+ *
+ * Example usage:
+ * ```
+ * view.applyInsetsWithMarginAdjustment { insets, layoutParams ->
+ * layoutParams.leftMargin = insets.left
+ * layoutParams.rightMargin = insets.right
+ * layoutParams.topMargin = insets.top
+ * layoutParams.bottomMargin = insets.bottom
+ * }
+ * ```
+ */
+fun View.applyInsetsWithMarginAdjustment(marginAdjuster : (insets:Insets, mlp:MarginLayoutParams) -> Unit) {
+ ViewCompat.setOnApplyWindowInsetsListener(this
+ ) { v, insets ->
+ val bars: Insets = insets.getInsets(
+ WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()
+ )
+ v.updateLayoutParams {
+ marginAdjuster(bars,this)
+ }
+ WindowInsetsCompat.CONSUMED
+ }
+}
+
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CallbackManager.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/CallbackManager.java
index 85bc4de55..86bc15ad2 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CallbackManager.java
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/CallbackManager.java
@@ -282,8 +282,10 @@ public void removeOnInitCleverTapIDListener(@NonNull final OnInitCleverTapIDList
@Override
public void notifyCleverTapIDChanged(final String id) {
Handler mainHandler = new Handler(Looper.getMainLooper());
- for (OnInitCleverTapIDListener listener : onInitCleverTapIDListeners) {
- mainHandler.post(() -> listener.onInitCleverTapID(id));
+ for (final OnInitCleverTapIDListener listener : onInitCleverTapIDListeners) {
+ if (listener != null) {
+ mainHandler.post(() -> listener.onInitCleverTapID(id));
+ }
}
}
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..b6b89813b 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapAPI.java
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapAPI.java
@@ -70,6 +70,7 @@
import com.clevertap.android.sdk.pushnotification.amp.CTPushAmpListener;
import com.clevertap.android.sdk.task.CTExecutorFactory;
import com.clevertap.android.sdk.task.Task;
+import com.clevertap.android.sdk.usereventlogs.UserEventLog;
import com.clevertap.android.sdk.utils.UriHelper;
import com.clevertap.android.sdk.validation.ManifestValidator;
import com.clevertap.android.sdk.validation.ValidationResult;
@@ -83,6 +84,8 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
@@ -1223,8 +1226,7 @@ public void promptForPushPermission(boolean showFallbackSettings){
private CleverTapAPI(final Context context, final CleverTapInstanceConfig config, String cleverTapID) {
this.context = context;
- CoreState coreState = CleverTapFactory
- .getCoreState(context, config, cleverTapID);
+ CoreState coreState = CleverTapFactory.getCoreState(context, config, cleverTapID);
setCoreState(coreState);
getConfigLogger().verbose(config.getAccountId() + ":async_deviceID", "CoreState is set");
@@ -1244,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;
@@ -1608,8 +1611,12 @@ void setCoreState(final CoreState cleverTapState) {
*
* @param event The event for which you want to get the total count
* @return Total count in int
+ *
+ * @deprecated since v7.1.0
. Use {@link #getUserEventLogCount(String)} instead.
+ * getUserEventLogCount() provides user-specific event counts.
*/
@SuppressWarnings({"unused"})
+ @Deprecated(since = "7.1.0")
public int getCount(String event) {
EventDetail eventDetail = coreState.getLocalDataStore().getEventDetail(event);
if (eventDetail != null) {
@@ -1619,6 +1626,29 @@ public int getCount(String event) {
return -1;
}
+ /**
+ * Retrieves the count of logged events for a specific event name associated with the current
+ * user/{@link CleverTapAPI#getCleverTapID(OnInitCleverTapIDListener) CleverTap ID}.
+ * This operation involves a database query and should be called from a background thread.
+ *
+ * Example usage:
+ *
+ *
+ * // Call from background thread
+ * int itemSelectedCount = getUserEventLogCount("item_selected")
+ *
+ *
+ * @param eventName Name of the event to get the count for (e.g., "navigation_clicked", "item_selected")
+ * @return The number of times the specified event has occurred for current user, or -1 if there was an error
+ */
+ @WorkerThread
+ public int getUserEventLogCount(String eventName) {
+ if (!getConfig().isPersonalizationEnabled()) {
+ return -1;
+ }
+ return coreState.getLocalDataStore().readUserEventLogCount(eventName);
+ }
+
/**
* Returns an EventDetail object for the particular event passed. EventDetail consists of event name, count, first
* time
@@ -1626,12 +1656,39 @@ public int getCount(String event) {
*
* @param event The event name for which you want the Event details
* @return The {@link EventDetail} object
+ * @deprecated since v7.1.0
. Use {@link #getUserEventLog(String)} instead.
+ * getUserEventLog() provides user-specific event log.
*/
@SuppressWarnings({"unused"})
+ @Deprecated(since = "7.1.0")
public EventDetail getDetails(String event) {
return coreState.getLocalDataStore().getEventDetail(event);
}
+ /**
+ * Retrieves user-specific event log associated with the current user/
+ * {@link CleverTapAPI#getCleverTapID(OnInitCleverTapIDListener) CleverTap ID}.
+ * This operation involves a database query and should be called from a background thread.
+ *
+ * Example usage:
+ *
+ *
+ * // Call from background thread
+ * UserEventLog log = getUserEventLog("navigation_clicked")
+ * long firstOccurrence = log.firstTs
+ *
+ *
+ * @param eventName Name of the event to get the log for (e.g., "navigation_clicked", "item_selected")
+ * @return {@link UserEventLog} or null if the event log does not exist or there was an error
+ */
+ @WorkerThread
+ public UserEventLog getUserEventLog(String eventName) {
+ if (!getConfig().isPersonalizationEnabled()) {
+ return null;
+ }
+ return coreState.getLocalDataStore().readUserEventLog(eventName);
+ }
+
/**
* Returns the device push token or null
*
@@ -1728,8 +1785,11 @@ public CleverTapDisplayUnit getDisplayUnitForId(String unitID) {
*
* @param event The event name for which you want the first time timestamp
* @return The timestamp in int
+ * @deprecated since v7.1.0
. Use {@link #getUserEventLog(String)} instead.
+ * It provides user-specific event log with first occurrence timestamp.
*/
@SuppressWarnings({"unused"})
+ @Deprecated(since = "7.1.0")
public int getFirstTime(String event) {
EventDetail eventDetail = coreState.getLocalDataStore().getEventDetail(event);
if (eventDetail != null) {
@@ -1766,12 +1826,41 @@ public void setGeofenceCallback(GeofenceCallback geofenceCallback) {
* Returns a Map of event names and corresponding event details of all the events raised
*
* @return A Map of Event Name and its corresponding EventDetail object
+ * @deprecated since v7.1.0
. Use {@link #getUserEventLogHistory()} instead.
+ * getUserEventLogHistory() provides user-specific event logs.
*/
@SuppressWarnings({"unused"})
+ @Deprecated(since = "7.1.0")
public Map getHistory() {
return coreState.getLocalDataStore().getEventHistory(context);
}
+ /**
+ * Retrieves history of all event logs associated with the current user/{@link CleverTapAPI#getCleverTapID(OnInitCleverTapIDListener) CleverTap ID} in the ascending order of lastTs.
+ * This operation involves a database query and should be called from a background thread.
+ *
+ * Example usage:
+ *
+ *
+ * // Call from background thread
+ * Map<String, UserEventLog> history = getUserEventLogHistory()
+ *
+ *
+ * @return Map of event name to {@link UserEventLog} for all events by current user, or empty map if there was an error
+ */
+ @WorkerThread
+ public Map getUserEventLogHistory() {
+ Map history = new LinkedHashMap<>();
+ if (!getConfig().isPersonalizationEnabled()) {
+ return history;
+ }
+ List logs = coreState.getLocalDataStore().readUserEventLogs();
+ for (UserEventLog log : logs) {
+ history.put(log.getEventName(), log);
+ }
+ return history;
+ }
+
/**
* Returns the InAppNotificationListener object
*
@@ -1883,8 +1972,11 @@ public int getInboxMessageUnreadCount() {
*
* @param event The event name for which you want the last time timestamp
* @return The timestamp in int
+ * @deprecated since v7.1.0
. Use {@link #getUserEventLog(String)} instead.
+ * It provides user-specific event log with last occurrence timestamp.
*/
@SuppressWarnings({"unused"})
+ @Deprecated(since = "7.1.0")
public int getLastTime(String event) {
EventDetail eventDetail = coreState.getLocalDataStore().getEventDetail(event);
if (eventDetail != null) {
@@ -1922,12 +2014,33 @@ public void setLocation(Location location) {
* Returns the timestamp of the previous visit
*
* @return Timestamp of previous visit in int
+ * @deprecated since v7.1.0
. Use {@link #getUserLastVisitTs()} instead.
+ * getUserLastVisitTs() provides user-specific last visit timestamp.
*/
@SuppressWarnings({"unused"})
+ @Deprecated(since = "7.1.0")
public int getPreviousVisitTime() {
return coreState.getSessionManager().getLastVisitTime();
}
+ /**
+ * Retrieves timestamp of last visit by current user/{@link CleverTapAPI#getCleverTapID(OnInitCleverTapIDListener) CleverTap ID}.
+ *
+ * Example usage:
+ *
+ *
+ * long lastVisitTs = getUserLastVisitTs()
+ *
+ *
+ * @return Timestamp of last visit by current user, or -1 if there was an error
+ */
+ public long getUserLastVisitTs() {
+ if (!getConfig().isPersonalizationEnabled()) {
+ return -1;
+ }
+ return coreState.getSessionManager().getUserLastVisitTs();
+ }
+
/**
* Return the user profile property value for the specified key.
* Date related property values are returned as number of seconds since January 1, 1970, 00:00:00 GMT
@@ -2002,8 +2115,11 @@ public int getTimeElapsed() {
* Returns the total number of times the app has been launched
*
* @return Total number of app launches in int
+ * @deprecated since v7.1.0
. Use {@link #getUserAppLaunchCount()} instead.
+ * getUserAppLaunchCount() provides user-specific app launch count.
*/
@SuppressWarnings({"unused"})
+ @Deprecated(since = "7.1.0")
public int getTotalVisits() {
EventDetail ed = coreState.getLocalDataStore().getEventDetail(Constants.APP_LAUNCHED_EVENT);
if (ed != null) {
@@ -2013,6 +2129,27 @@ public int getTotalVisits() {
return 0;
}
+ /**
+ * Retrieves number of times app launched by current user/{@link CleverTapAPI#getCleverTapID(OnInitCleverTapIDListener) CleverTap ID}.
+ * This operation involves a database query and should be called from a background thread.
+ *
+ * Example usage:
+ *
+ *
+ * // Call from background thread
+ * int launchCount = getUserAppLaunchCount()
+ *
+ *
+ * @return Number of times app launched by current user, or -1 if there was an error
+ */
+ @WorkerThread
+ public int getUserAppLaunchCount() {
+ if (!getConfig().isPersonalizationEnabled()) {
+ return -1;
+ }
+ return coreState.getLocalDataStore().readUserEventLogCount(Constants.APP_LAUNCHED_EVENT);
+ }
+
/**
* Returns a UTMDetail object which consists of UTM parameters like source, medium & campaign
*
@@ -2219,7 +2356,7 @@ public CTProductConfigController productConfig() {
getConfig().getLogger().debug(getAccountId(),
"Product config is not supported with analytics only configuration");
}
- return coreState.getCtProductConfigController();
+ return coreState.getCtProductConfigController(context);
}
/**
@@ -3104,13 +3241,14 @@ private static CleverTapInstanceConfig getDefaultConfig(Context context) {
String spikyProxyDomain = manifest.getSpikeyProxyDomain();
String handshakeDomain = manifest.getHandshakeDomain();
if (accountId == null || accountToken == null) {
- Logger.i(
- "Account ID or Account token is missing from AndroidManifest.xml, unable to create default instance");
+ Logger.i("Account ID or Account token is missing from AndroidManifest.xml, unable to create default instance");
return null;
}
if (accountRegion == null) {
Logger.i("Account Region not specified in the AndroidManifest - using default region");
}
+
+ // todo lp pass manifest info here
CleverTapInstanceConfig defaultInstanceConfig = CleverTapInstanceConfig.createDefaultInstance(context, accountId, accountToken, accountRegion);
if (proxyDomain != null && !proxyDomain.trim().isEmpty()) {
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapFactory.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapFactory.java
index 201c12192..e69de29bb 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
@@ -1,349 +0,0 @@
-package com.clevertap.android.sdk;
-
-import android.content.Context;
-import com.clevertap.android.sdk.cryption.CryptHandler;
-import com.clevertap.android.sdk.cryption.CryptUtils;
-import com.clevertap.android.sdk.db.DBManager;
-import com.clevertap.android.sdk.events.EventMediator;
-import com.clevertap.android.sdk.events.EventQueueManager;
-import com.clevertap.android.sdk.featureFlags.CTFeatureFlagsFactory;
-import com.clevertap.android.sdk.inapp.ImpressionManager;
-import com.clevertap.android.sdk.inapp.InAppController;
-import com.clevertap.android.sdk.inapp.InAppQueue;
-import com.clevertap.android.sdk.inapp.TriggerManager;
-import com.clevertap.android.sdk.inapp.customtemplates.TemplatesManager;
-import com.clevertap.android.sdk.inapp.evaluation.EvaluationManager;
-import com.clevertap.android.sdk.inapp.evaluation.LimitsMatcher;
-import com.clevertap.android.sdk.inapp.evaluation.TriggersMatcher;
-import com.clevertap.android.sdk.inapp.images.FileResourceProvider;
-import com.clevertap.android.sdk.inapp.images.repo.FileResourcesRepoFactory;
-import com.clevertap.android.sdk.inapp.images.repo.FileResourcesRepoImpl;
-import com.clevertap.android.sdk.inapp.store.preference.ImpressionStore;
-import com.clevertap.android.sdk.inapp.store.preference.InAppStore;
-import com.clevertap.android.sdk.inapp.store.preference.StoreRegistry;
-import com.clevertap.android.sdk.login.LoginController;
-import com.clevertap.android.sdk.network.AppLaunchListener;
-import com.clevertap.android.sdk.network.CompositeBatchListener;
-import com.clevertap.android.sdk.network.FetchInAppListener;
-import com.clevertap.android.sdk.network.NetworkManager;
-import com.clevertap.android.sdk.network.api.CtApiWrapper;
-import com.clevertap.android.sdk.pushnotification.PushProviders;
-import com.clevertap.android.sdk.pushnotification.work.CTWorkManager;
-import com.clevertap.android.sdk.response.InAppResponse;
-import com.clevertap.android.sdk.task.CTExecutorFactory;
-import com.clevertap.android.sdk.task.MainLooperHandler;
-import com.clevertap.android.sdk.task.Task;
-import com.clevertap.android.sdk.validation.ValidationResultStack;
-import com.clevertap.android.sdk.validation.Validator;
-import com.clevertap.android.sdk.variables.CTVariables;
-import com.clevertap.android.sdk.variables.Parser;
-import com.clevertap.android.sdk.variables.VarCache;
-import java.util.concurrent.Callable;
-
-class CleverTapFactory {
-
- static CoreState getCoreState(Context context, CleverTapInstanceConfig cleverTapInstanceConfig,
- String cleverTapID) {
- CoreState coreState = new CoreState(context);
-
- TemplatesManager templatesManager = TemplatesManager.createInstance(cleverTapInstanceConfig);
- coreState.setTemplatesManager(templatesManager);
-
- // create storeRegistry, preferences for features
- final StoreProvider storeProvider = StoreProvider.getInstance();
- String accountId = cleverTapInstanceConfig.getAccountId();
-
- StoreRegistry storeRegistry = new StoreRegistry(
- null,
- null,
- storeProvider.provideLegacyInAppStore(context, accountId),
- storeProvider.provideInAppAssetsStore(context, accountId),
- storeProvider.provideFileStore(context, accountId)
- );
-
- coreState.setStoreRegistry(storeRegistry);
-
- CoreMetaData coreMetaData = new CoreMetaData();
- coreState.setCoreMetaData(coreMetaData);
-
- Validator validator = new Validator();
-
- ValidationResultStack validationResultStack = new ValidationResultStack();
- coreState.setValidationResultStack(validationResultStack);
-
- CTLockManager ctLockManager = new CTLockManager();
- coreState.setCTLockManager(ctLockManager);
-
- MainLooperHandler mainLooperHandler = new MainLooperHandler();
- coreState.setMainLooperHandler(mainLooperHandler);
-
- CleverTapInstanceConfig config = new CleverTapInstanceConfig(cleverTapInstanceConfig);
- coreState.setConfig(config);
-
- DBManager baseDatabaseManager = new DBManager(config, ctLockManager);
- coreState.setDatabaseManager(baseDatabaseManager);
-
- CryptHandler cryptHandler = new CryptHandler(config.getEncryptionLevel(),
- CryptHandler.EncryptionAlgorithm.AES, config.getAccountId());
- coreState.setCryptHandler(cryptHandler);
- Task task = CTExecutorFactory.executors(config).postAsyncSafelyTask();
- task.execute("migratingEncryptionLevel", () -> {
- CryptUtils.migrateEncryptionLevel(context, config, cryptHandler,
- baseDatabaseManager.loadDBAdapter(context));
- return null;
- });
-
- DeviceInfo deviceInfo = new DeviceInfo(context, config, cleverTapID, coreMetaData);
- coreState.setDeviceInfo(deviceInfo);
- deviceInfo.onInitDeviceInfo(cleverTapID);
-
- LocalDataStore localDataStore = new LocalDataStore(context, config, cryptHandler, deviceInfo);
- coreState.setLocalDataStore(localDataStore);
-
- ProfileValueHandler profileValueHandler = new ProfileValueHandler(validator, validationResultStack);
- coreState.setProfileValueHandler(profileValueHandler);
-
- EventMediator eventMediator = new EventMediator(context, config, coreMetaData, localDataStore, profileValueHandler);
- coreState.setEventMediator(eventMediator);
-
- CTPreferenceCache.getInstance(context, config);
-
- BaseCallbackManager callbackManager = new CallbackManager(config, deviceInfo);
- coreState.setCallbackManager(callbackManager);
-
- SessionManager sessionManager = new SessionManager(config, coreMetaData, validator, localDataStore);
- coreState.setSessionManager(sessionManager);
-
- ControllerManager controllerManager = new ControllerManager(context, config,
- ctLockManager, callbackManager, deviceInfo, baseDatabaseManager);
- coreState.setControllerManager(controllerManager);
-
- TriggersMatcher triggersMatcher = new TriggersMatcher();
- TriggerManager triggersManager = new TriggerManager(context, config.getAccountId(), deviceInfo);
- ImpressionManager impressionManager = new ImpressionManager(storeRegistry);
- LimitsMatcher limitsMatcher = new LimitsMatcher(impressionManager, triggersManager);
-
- coreState.setImpressionManager(impressionManager);
-
- EvaluationManager evaluationManager = new EvaluationManager(
- triggersMatcher,
- triggersManager,
- limitsMatcher,
- storeRegistry,
- templatesManager
- );
- coreState.setEvaluationManager(evaluationManager);
-
- Task taskInitStores = CTExecutorFactory.executors(config).ioTask();
- taskInitStores.execute("initStores", () -> {
-
- if (coreState.getDeviceInfo() != null && coreState.getDeviceInfo().getDeviceID() != null) {
- if (storeRegistry.getInAppStore() == null) {
- InAppStore inAppStore = storeProvider.provideInAppStore(context, cryptHandler, deviceInfo.getDeviceID(),
- config.getAccountId());
- storeRegistry.setInAppStore(inAppStore);
- evaluationManager.loadSuppressedCSAndEvaluatedSSInAppsIds();
- callbackManager.addChangeUserCallback(inAppStore);
- }
- if (storeRegistry.getImpressionStore() == null) {
- ImpressionStore impStore = storeProvider.provideImpressionStore(context, deviceInfo.getDeviceID(),
- config.getAccountId());
- storeRegistry.setImpressionStore(impStore);
- callbackManager.addChangeUserCallback(impStore);
- }
- }
- return null;
- });
-
- //Get device id should be async to avoid strict mode policy.
- Task taskInitFCManager = CTExecutorFactory.executors(config).ioTask();
- taskInitFCManager.execute("initFCManager", new Callable() {
- @Override
- public Void call() throws Exception {
- if (coreState.getDeviceInfo() != null && coreState.getDeviceInfo().getDeviceID() != null
- && controllerManager.getInAppFCManager() == null) {
- coreState.getConfig().getLogger()
- .verbose(config.getAccountId() + ":async_deviceID",
- "Initializing InAppFC with device Id = " + coreState.getDeviceInfo()
- .getDeviceID());
- controllerManager
- .setInAppFCManager(
- new InAppFCManager(context, config, coreState.getDeviceInfo().getDeviceID(),
- storeRegistry, impressionManager));
- }
- return null;
- }
- });
-
- FileResourcesRepoImpl impl = FileResourcesRepoFactory.createFileResourcesRepo(context, config.getLogger(), storeRegistry);
- FileResourceProvider fileResourceProvider = new FileResourceProvider(context, config.getLogger());
-
- VarCache varCache = new VarCache(
- config,
- context,
- impl,
- fileResourceProvider
- );
- coreState.setVarCache(varCache);
-
- CTVariables ctVariables = new CTVariables(varCache);
- coreState.setCTVariables(ctVariables);
- coreState.getControllerManager().setCtVariables(ctVariables);
-
- Parser parser = new Parser(ctVariables);
- coreState.setParser(parser);
-
- Task taskVariablesInit = CTExecutorFactory.executors(config).ioTask();
- taskVariablesInit.execute("initCTVariables", () -> {
- ctVariables.init();
- return null;
- });
-
- InAppResponse inAppResponse = new InAppResponse(
- config,
- controllerManager,
- false,
- storeRegistry,
- triggersManager,
- templatesManager,
- coreMetaData
- );
-
- final CtApiWrapper ctApiWrapper = new CtApiWrapper(context, config, deviceInfo);
- NetworkManager networkManager = new NetworkManager(
- context,
- config,
- deviceInfo,
- coreMetaData,
- validationResultStack,
- controllerManager,
- baseDatabaseManager,
- callbackManager,
- ctLockManager,
- validator,
- inAppResponse,
- ctApiWrapper
- );
- coreState.setNetworkManager(networkManager);
-
- EventQueueManager baseEventQueueManager = new EventQueueManager(
- baseDatabaseManager,
- context,
- config,
- eventMediator,
- sessionManager,
- callbackManager,
- mainLooperHandler,
- deviceInfo,
- validationResultStack,
- networkManager,
- coreMetaData,
- ctLockManager,
- localDataStore,
- controllerManager,
- cryptHandler
- );
- coreState.setBaseEventQueueManager(baseEventQueueManager);
-
- InAppResponse inAppResponseForSendTestInApp = new InAppResponse(
- config,
- controllerManager,
- true,
- storeRegistry,
- triggersManager,
- templatesManager,
- coreMetaData
- );
-
- AnalyticsManager analyticsManager = new AnalyticsManager(
- context,
- config,
- baseEventQueueManager,
- validator,
- validationResultStack,
- coreMetaData,
- deviceInfo,
- callbackManager,
- controllerManager,
- ctLockManager,
- inAppResponseForSendTestInApp
- );
- coreState.setAnalyticsManager(analyticsManager);
-
- networkManager.addNetworkHeadersListener(evaluationManager);
- InAppController inAppController = new InAppController(
- context,
- config,
- mainLooperHandler,
- controllerManager,
- callbackManager,
- analyticsManager,
- coreMetaData,
- deviceInfo,
- new InAppQueue(config, storeRegistry),
- evaluationManager,
- fileResourceProvider,
- templatesManager,
- storeRegistry
- );
-
- coreState.setInAppController(inAppController);
- coreState.getControllerManager().setInAppController(inAppController);
-
- final AppLaunchListener appLaunchListener = new AppLaunchListener();
- appLaunchListener.addListener(inAppController.onAppLaunchEventSent);
-
- CompositeBatchListener batchListener = new CompositeBatchListener();
- batchListener.addListener(appLaunchListener);
- batchListener.addListener(new FetchInAppListener(callbackManager));
- callbackManager.setBatchListener(batchListener);
-
- Task taskInitFeatureFlags = CTExecutorFactory.executors(config).ioTask();
- taskInitFeatureFlags.execute("initFeatureFlags", new Callable() {
- @Override
- public Void call() throws Exception {
- initFeatureFlags(context, controllerManager, config, deviceInfo, callbackManager, analyticsManager);
- return null;
- }
- });
-
- LocationManager locationManager = new LocationManager(context, config, coreMetaData, baseEventQueueManager);
- coreState.setLocationManager(locationManager);
-
- CTWorkManager ctWorkManager = new CTWorkManager(context, config);
-
- PushProviders pushProviders = PushProviders
- .load(context, config, baseDatabaseManager, validationResultStack,
- analyticsManager, controllerManager, ctWorkManager);
- coreState.setPushProviders(pushProviders);
-
- ActivityLifeCycleManager activityLifeCycleManager = new ActivityLifeCycleManager(context, config,
- analyticsManager, coreMetaData, sessionManager, pushProviders, callbackManager, inAppController,
- baseEventQueueManager);
- coreState.setActivityLifeCycleManager(activityLifeCycleManager);
-
- LoginController loginController = new LoginController(context, config, deviceInfo,
- validationResultStack, baseEventQueueManager, analyticsManager,
- coreMetaData, controllerManager, sessionManager,
- localDataStore, callbackManager, baseDatabaseManager, ctLockManager, cryptHandler);
- coreState.setLoginController(loginController);
-
- return coreState;
- }
-
- static void initFeatureFlags(Context context, ControllerManager controllerManager, CleverTapInstanceConfig config,
- DeviceInfo deviceInfo, BaseCallbackManager callbackManager, AnalyticsManager analyticsManager) {
-
- config.getLogger().verbose(config.getAccountId() + ":async_deviceID",
- "Initializing Feature Flags with device Id = " + deviceInfo.getDeviceID());
- if (config.isAnalyticsOnly()) {
- config.getLogger().debug(config.getAccountId(), "Feature Flag is not enabled for this instance");
- } else {
- controllerManager.setCTFeatureFlagsController(CTFeatureFlagsFactory.getInstance(context,
- deviceInfo.getDeviceID(),
- config, callbackManager, analyticsManager));
- config.getLogger().verbose(config.getAccountId() + ":async_deviceID", "Feature Flags initialized");
- }
-
- }
-}
\ No newline at end of file
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapFactory.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapFactory.kt
new file mode 100644
index 000000000..1ebf74226
--- /dev/null
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/CleverTapFactory.kt
@@ -0,0 +1,446 @@
+package com.clevertap.android.sdk
+
+import android.content.Context
+import com.clevertap.android.sdk.CTPreferenceCache.Companion.getInstance
+import com.clevertap.android.sdk.StoreProvider.Companion.getInstance
+import com.clevertap.android.sdk.cryption.CryptFactory
+import com.clevertap.android.sdk.cryption.CryptHandler
+import com.clevertap.android.sdk.cryption.CryptMigrator
+import com.clevertap.android.sdk.cryption.CryptRepository
+import com.clevertap.android.sdk.cryption.DataMigrationRepository
+import com.clevertap.android.sdk.cryption.EncryptionLevel.Companion.fromInt
+import com.clevertap.android.sdk.db.DBManager
+import com.clevertap.android.sdk.events.EventMediator
+import com.clevertap.android.sdk.events.EventQueueManager
+import com.clevertap.android.sdk.featureFlags.CTFeatureFlagsFactory
+import com.clevertap.android.sdk.inapp.ImpressionManager
+import com.clevertap.android.sdk.inapp.InAppController
+import com.clevertap.android.sdk.inapp.InAppQueue
+import com.clevertap.android.sdk.inapp.TriggerManager
+import com.clevertap.android.sdk.inapp.customtemplates.TemplatesManager.Companion.createInstance
+import com.clevertap.android.sdk.inapp.evaluation.EvaluationManager
+import com.clevertap.android.sdk.inapp.evaluation.LimitsMatcher
+import com.clevertap.android.sdk.inapp.evaluation.TriggersMatcher
+import com.clevertap.android.sdk.inapp.images.FileResourceProvider
+import com.clevertap.android.sdk.inapp.images.repo.FileResourcesRepoFactory.Companion.createFileResourcesRepo
+import com.clevertap.android.sdk.inapp.store.preference.ImpressionStore
+import com.clevertap.android.sdk.inapp.store.preference.InAppStore
+import com.clevertap.android.sdk.inapp.store.preference.StoreRegistry
+import com.clevertap.android.sdk.login.LoginController
+import com.clevertap.android.sdk.login.LoginInfoProvider
+import com.clevertap.android.sdk.network.AppLaunchListener
+import com.clevertap.android.sdk.network.CompositeBatchListener
+import com.clevertap.android.sdk.network.FetchInAppListener
+import com.clevertap.android.sdk.network.NetworkManager
+import com.clevertap.android.sdk.network.api.CtApiWrapper
+import com.clevertap.android.sdk.pushnotification.PushProviders
+import com.clevertap.android.sdk.pushnotification.work.CTWorkManager
+import com.clevertap.android.sdk.response.InAppResponse
+import com.clevertap.android.sdk.task.CTExecutorFactory
+import com.clevertap.android.sdk.task.MainLooperHandler
+import com.clevertap.android.sdk.utils.Clock.Companion.SYSTEM
+import com.clevertap.android.sdk.validation.ValidationResultStack
+import com.clevertap.android.sdk.validation.Validator
+import com.clevertap.android.sdk.variables.CTVariables
+import com.clevertap.android.sdk.variables.Parser
+import com.clevertap.android.sdk.variables.VarCache
+
+internal object CleverTapFactory {
+ @JvmStatic
+ fun getCoreState(
+ context: Context?,
+ cleverTapInstanceConfig: CleverTapInstanceConfig?,
+ cleverTapID: String?
+ ): CoreState {
+
+ if (context == null || cleverTapInstanceConfig == null) {
+ // todo this needs to be fixed with kotlin usage+using kotlin testing libs
+ throw RuntimeException("This is invalid case and will not happen. Context/Config is null")
+ }
+
+ val coreState = CoreState()
+
+ val templatesManager = createInstance(cleverTapInstanceConfig)
+ coreState.templatesManager = templatesManager
+
+ // create storeRegistry, preferences for features
+ val storeProvider = getInstance()
+ val accountId = cleverTapInstanceConfig.accountId
+
+ val storeRegistry = StoreRegistry(
+ inAppStore = null,
+ impressionStore = null,
+ legacyInAppStore = storeProvider.provideLegacyInAppStore(context = context, accountId = accountId),
+ inAppAssetsStore = storeProvider.provideInAppAssetsStore(context, accountId),
+ filesStore = storeProvider.provideFileStore(context, accountId)
+ )
+
+ coreState.storeRegistry = storeRegistry
+
+ val coreMetaData = CoreMetaData()
+ coreState.coreMetaData = coreMetaData
+
+ val validator = Validator()
+
+ val validationResultStack = ValidationResultStack()
+ coreState.validationResultStack = validationResultStack
+
+ val ctLockManager = CTLockManager()
+ coreState.ctLockManager = ctLockManager
+
+ val mainLooperHandler = MainLooperHandler()
+ coreState.mainLooperHandler = mainLooperHandler
+
+ val config = CleverTapInstanceConfig(cleverTapInstanceConfig)
+ coreState.config = config
+
+ val baseDatabaseManager = DBManager(config, ctLockManager)
+ coreState.databaseManager = baseDatabaseManager
+
+ val repository = CryptRepository(
+ context = context,
+ accountId = config.accountId
+ )
+ val cryptFactory = CryptFactory(
+ context = context,
+ accountId = config.accountId
+ )
+ val cryptHandler = CryptHandler(
+ encryptionLevel = fromInt(value = config.encryptionLevel),
+ accountID = config.accountId,
+ repository = repository,
+ cryptFactory = cryptFactory
+ )
+ coreState.cryptHandler = cryptHandler
+ val task = CTExecutorFactory.executors(config).postAsyncSafelyTask()
+ task.execute("migratingEncryption") {
+
+ val dataMigrationRepository = DataMigrationRepository(
+ context = context,
+ config = config,
+ dbAdapter = baseDatabaseManager.loadDBAdapter(context)
+ )
+
+ val cryptMigrator = CryptMigrator(
+ logPrefix = config.accountId,
+ configEncryptionLevel = config.encryptionLevel,
+ logger = config.logger,
+ cryptHandler = cryptHandler,
+ cryptRepository = repository,
+ dataMigrationRepository = dataMigrationRepository
+ )
+ cryptMigrator.migrateEncryption()
+ null
+ }
+
+ val deviceInfo = DeviceInfo(context, config, cleverTapID, coreMetaData)
+ coreState.deviceInfo = deviceInfo
+ deviceInfo.onInitDeviceInfo(cleverTapID)
+
+ val localDataStore =
+ LocalDataStore(context, config, cryptHandler, deviceInfo, baseDatabaseManager)
+ coreState.localDataStore = localDataStore
+
+ val profileValueHandler = ProfileValueHandler(validator, validationResultStack)
+ coreState.profileValueHandler = profileValueHandler
+
+ val eventMediator =
+ EventMediator(context, config, coreMetaData, localDataStore, profileValueHandler)
+ coreState.eventMediator = eventMediator
+
+ getInstance(context, config)
+
+ val callbackManager: BaseCallbackManager = CallbackManager(config, deviceInfo)
+ coreState.callbackManager = callbackManager
+
+ val sessionManager = SessionManager(config, coreMetaData, validator, localDataStore)
+ coreState.sessionManager = sessionManager
+
+ val controllerManager = ControllerManager(
+ context,
+ config,
+ ctLockManager,
+ callbackManager,
+ deviceInfo,
+ baseDatabaseManager
+ )
+ coreState.controllerManager = controllerManager
+
+ val triggersMatcher = TriggersMatcher(localDataStore)
+ val triggersManager = TriggerManager(context, config.accountId, deviceInfo)
+ val impressionManager = ImpressionManager(storeRegistry)
+ val limitsMatcher = LimitsMatcher(impressionManager, triggersManager)
+
+ coreState.impressionManager = impressionManager
+
+ val evaluationManager = EvaluationManager(
+ triggersMatcher = triggersMatcher,
+ triggersManager = triggersManager,
+ limitsMatcher = limitsMatcher,
+ storeRegistry = storeRegistry,
+ templatesManager = templatesManager
+ )
+ coreState.evaluationManager = evaluationManager
+
+ val taskInitStores = CTExecutorFactory.executors(config).ioTask()
+ taskInitStores.execute("initStores") {
+ if (coreState.deviceInfo != null && coreState.deviceInfo.getDeviceID() != null) {
+ if (storeRegistry.inAppStore == null) {
+ val inAppStore: InAppStore = storeProvider.provideInAppStore(
+ context = context,
+ cryptHandler = cryptHandler,
+ deviceId = deviceInfo.getDeviceID(),
+ accountId = config.accountId
+ )
+ storeRegistry.inAppStore = inAppStore
+ evaluationManager.loadSuppressedCSAndEvaluatedSSInAppsIds()
+ callbackManager.addChangeUserCallback(inAppStore)
+ }
+ if (storeRegistry.impressionStore == null) {
+ val impStore: ImpressionStore = storeProvider.provideImpressionStore(
+ context = context,
+ deviceId = deviceInfo.getDeviceID(),
+ accountId = config.accountId
+ )
+ storeRegistry.impressionStore = impStore
+ callbackManager.addChangeUserCallback(impStore)
+ }
+ }
+ null
+ }
+
+ //Get device id should be async to avoid strict mode policy.
+ val taskInitFCManager = CTExecutorFactory.executors(config).ioTask()
+ taskInitFCManager.execute("initFCManager") {
+ if (coreState.deviceInfo != null && coreState.deviceInfo.deviceID != null && controllerManager.inAppFCManager == null) {
+ coreState.config.logger
+ .verbose(
+ config.accountId + ":async_deviceID",
+ "Initializing InAppFC with device Id = " + coreState.deviceInfo
+ .deviceID
+ )
+ controllerManager.inAppFCManager = InAppFCManager(
+ context,
+ config,
+ coreState.deviceInfo.deviceID,
+ storeRegistry,
+ impressionManager
+ )
+ }
+ null
+ }
+
+ val impl = createFileResourcesRepo(
+ context = context,
+ logger = config.logger,
+ storeRegistry = storeRegistry
+ )
+ val fileResourceProvider = FileResourceProvider(
+ context = context,
+ logger = config.logger
+ )
+
+ val varCache = VarCache(
+ config,
+ context,
+ impl,
+ fileResourceProvider
+ )
+ coreState.varCache = varCache
+
+ val ctVariables = CTVariables(varCache)
+ coreState.ctVariables = ctVariables
+ coreState.controllerManager.ctVariables = ctVariables
+
+ val parser = Parser(ctVariables)
+ coreState.parser = parser
+
+ val taskVariablesInit = CTExecutorFactory.executors(config).ioTask()
+ taskVariablesInit.execute("initCTVariables") {
+ ctVariables.init()
+ null
+ }
+
+ val inAppResponse = InAppResponse(
+ config,
+ controllerManager,
+ false,
+ storeRegistry,
+ triggersManager,
+ templatesManager,
+ coreMetaData
+ )
+
+ val ctApiWrapper = CtApiWrapper(
+ context = context,
+ config = config,
+ deviceInfo = deviceInfo
+ )
+ val networkManager = NetworkManager(
+ context,
+ config,
+ deviceInfo,
+ coreMetaData,
+ validationResultStack,
+ controllerManager,
+ baseDatabaseManager,
+ callbackManager,
+ ctLockManager,
+ validator,
+ inAppResponse,
+ ctApiWrapper
+ )
+ coreState.networkManager = networkManager
+
+ val loginInfoProvider = LoginInfoProvider(
+ context,
+ config,
+ cryptHandler
+ )
+
+ val baseEventQueueManager = EventQueueManager(
+ baseDatabaseManager,
+ context,
+ config,
+ eventMediator,
+ sessionManager,
+ callbackManager,
+ mainLooperHandler,
+ deviceInfo,
+ validationResultStack,
+ networkManager,
+ coreMetaData,
+ ctLockManager,
+ localDataStore,
+ controllerManager,
+ loginInfoProvider
+ )
+ coreState.baseEventQueueManager = baseEventQueueManager
+
+ val inAppResponseForSendTestInApp = InAppResponse(
+ config,
+ controllerManager,
+ true,
+ storeRegistry,
+ triggersManager,
+ templatesManager,
+ coreMetaData
+ )
+
+ val analyticsManager = AnalyticsManager(
+ context,
+ config,
+ baseEventQueueManager,
+ validator,
+ validationResultStack,
+ coreMetaData,
+ deviceInfo,
+ callbackManager,
+ controllerManager,
+ ctLockManager,
+ inAppResponseForSendTestInApp,
+ SYSTEM
+ )
+ coreState.analyticsManager = analyticsManager
+
+ networkManager.addNetworkHeadersListener(evaluationManager)
+ val inAppController = InAppController(
+ context,
+ config,
+ mainLooperHandler,
+ controllerManager,
+ callbackManager,
+ analyticsManager,
+ coreMetaData,
+ deviceInfo,
+ InAppQueue(config, storeRegistry),
+ evaluationManager,
+ fileResourceProvider,
+ templatesManager,
+ storeRegistry
+ )
+
+ coreState.inAppController = inAppController
+ coreState.controllerManager.inAppController = inAppController
+
+ val appLaunchListener = AppLaunchListener()
+ appLaunchListener.addListener(inAppController.onAppLaunchEventSent)
+
+ val batchListener = CompositeBatchListener()
+ batchListener.addListener(appLaunchListener)
+ batchListener.addListener(FetchInAppListener(callbackManager))
+ callbackManager.batchListener = batchListener
+
+ val taskInitFeatureFlags = CTExecutorFactory.executors(config).ioTask()
+ taskInitFeatureFlags.execute("initFeatureFlags") {
+ initFeatureFlags(
+ context,
+ controllerManager,
+ config,
+ deviceInfo,
+ callbackManager,
+ analyticsManager
+ )
+ null
+ }
+
+ val locationManager = LocationManager(context, config, coreMetaData, baseEventQueueManager)
+ coreState.locationManager = locationManager
+
+ val ctWorkManager = CTWorkManager(context, config)
+
+ val pushProviders = PushProviders
+ .load(
+ context, config, baseDatabaseManager, validationResultStack,
+ analyticsManager, controllerManager, ctWorkManager
+ )
+ coreState.pushProviders = pushProviders
+
+ val activityLifeCycleManager = ActivityLifeCycleManager(
+ context,
+ config,
+ analyticsManager,
+ coreMetaData,
+ sessionManager,
+ pushProviders,
+ callbackManager,
+ inAppController,
+ baseEventQueueManager
+ )
+ coreState.activityLifeCycleManager = activityLifeCycleManager
+
+ val loginController = LoginController(
+ context, config, deviceInfo,
+ validationResultStack, baseEventQueueManager, analyticsManager,
+ coreMetaData, controllerManager, sessionManager,
+ localDataStore, callbackManager, baseDatabaseManager, ctLockManager, loginInfoProvider
+ )
+ coreState.loginController = loginController
+
+ return coreState
+ }
+
+ private fun initFeatureFlags(
+ context: Context?,
+ controllerManager: ControllerManager,
+ config: CleverTapInstanceConfig,
+ deviceInfo: DeviceInfo,
+ callbackManager: BaseCallbackManager?,
+ analyticsManager: AnalyticsManager?
+ ) {
+ config.logger.verbose(
+ config.accountId + ":async_deviceID",
+ "Initializing Feature Flags with device Id = " + deviceInfo.deviceID
+ )
+ if (config.isAnalyticsOnly) {
+ config.logger.debug(config.accountId, "Feature Flag is not enabled for this instance")
+ } else {
+ controllerManager.ctFeatureFlagsController = CTFeatureFlagsFactory.getInstance(
+ context,
+ deviceInfo.deviceID,
+ config, callbackManager, analyticsManager
+ )
+ config.logger.verbose(config.accountId + ":async_deviceID", "Feature Flags initialized")
+ }
+ }
+}
\ No newline at end of file
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..4e8a6aca9 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,11 +9,12 @@
import android.text.TextUtils;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import com.clevertap.android.sdk.Constants.IdentityType;
-import com.clevertap.android.sdk.cryption.CryptHandler;
+import com.clevertap.android.sdk.cryption.EncryptionLevel;
import com.clevertap.android.sdk.login.LoginConstants;
import org.json.JSONObject;
@@ -40,73 +39,86 @@ public CleverTapInstanceConfig[] newArray(int size) {
};
private String accountId;
-
private String accountRegion;
-
private String accountToken;
-
private String proxyDomain;
-
private String spikyProxyDomain;
-
private String customHandshakeDomain;
-
- @NonNull
- private ArrayList allowedPushTypes = getAll();
-
+ @NonNull private ArrayList allowedPushTypes = getAll();
private boolean analyticsOnly;
-
private boolean backgroundSync;
-
private boolean beta;
-
private boolean createdPostAppLaunch;
-
private int debugLevel;
-
private boolean disableAppLaunchedEvent;
-
private boolean enableCustomCleverTapId;
-
private String fcmSenderId;
-
private boolean isDefaultInstance;
-
private Logger logger;
-
private String packageName;
-
private boolean personalization;
-
private String[] identityKeys = Constants.NULL_STRING_ARRAY;
-
private boolean sslPinning;
-
private boolean useGoogleAdId;
private int encryptionLevel;
-
@SuppressWarnings("unused")
- public static CleverTapInstanceConfig createInstance(Context context, @NonNull String accountId,
- @NonNull String accountToken) {
+ public static CleverTapInstanceConfig createInstance(
+ Context context,
+ @NonNull String accountId,
+ @NonNull String accountToken
+ ) {
+ return CleverTapInstanceConfig.createInstance(context, accountId, accountToken, null);
+ }
+ @SuppressWarnings({"unused"})
+ public static CleverTapInstanceConfig createInstance(
+ @NonNull Context context,
+ @NonNull String accountId,
+ @NonNull String accountToken,
+ @Nullable String accountRegion
+ ) {
//noinspection ConstantConditions
if (accountId == null || accountToken == null) {
Logger.i("CleverTap accountId and accountToken cannot be null");
return null;
}
- return new CleverTapInstanceConfig(context, accountId, accountToken, null, false);
+ ManifestInfo manifestInfo = ManifestInfo.getInstance(context);
+ return CleverTapInstanceConfig.createInstanceWithManifest(manifestInfo, accountId, accountToken, accountRegion, false);
}
- @SuppressWarnings({"unused"})
- public static CleverTapInstanceConfig createInstance(Context context, @NonNull String accountId,
- @NonNull String accountToken, String accountRegion) {
- //noinspection ConstantConditions
- if (accountId == null || accountToken == null) {
- Logger.i("CleverTap accountId and accountToken cannot be null");
+
+ // convenience to construct the internal only default config
+ @SuppressWarnings({"unused", "WeakerAccess"})
+ protected static CleverTapInstanceConfig createDefaultInstance(
+ @NonNull Context context,
+ @NonNull String accountId,
+ @NonNull String accountToken,
+ @Nullable String accountRegion
+ ) {
+ ManifestInfo manifestInfo = ManifestInfo.getInstance(context);
+ return CleverTapInstanceConfig.createInstanceWithManifest(manifestInfo, accountId, accountToken, accountRegion, true);
+ }
+
+ static CleverTapInstanceConfig createInstanceWithManifest(
+ @NonNull ManifestInfo manifest,
+ @NonNull String accountId,
+ @NonNull String accountToken,
+ @Nullable String accountRegion,
+ boolean isDefaultInstance
+ ) {
+ return new CleverTapInstanceConfig(manifest, accountId, accountToken, accountRegion, isDefaultInstance);
+ }
+
+ // for internal use only!
+ @SuppressWarnings({"unused", "WeakerAccess"})
+ @Nullable
+ protected static CleverTapInstanceConfig createInstance(@NonNull String jsonString) {
+ try {
+ return new CleverTapInstanceConfig(jsonString);
+ } catch (Throwable t) {
return null;
}
- return new CleverTapInstanceConfig(context, accountId, accountToken, accountRegion, false);
}
CleverTapInstanceConfig(CleverTapInstanceConfig config) {
@@ -134,9 +146,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 +163,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();
@@ -475,7 +490,7 @@ boolean isUseGoogleAdId() {
void setCreatedPostAppLaunch() {
this.createdPostAppLaunch = true;
}
- public void setEncryptionLevel(CryptHandler.EncryptionLevel encryptionLevel) {
+ public void setEncryptionLevel(EncryptionLevel encryptionLevel) {
this.encryptionLevel = encryptionLevel.intValue();
}
public int getEncryptionLevel() {
@@ -515,22 +530,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 8e3c29f2d..8318d8a9f 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";
@@ -77,6 +57,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";
@@ -154,6 +135,7 @@ public interface Constants {
String KEY_I = "comms_i";
String KEY_J = "comms_j";
String CACHED_GUIDS_KEY = "cachedGUIDsKey";
+ String CACHED_GUIDS_LENGTH_KEY = "cachedGUIDsLengthKey";
String CACHED_VARIABLES_KEY = "variablesKey";
String MULTI_USER_PREFIX = "mt_";
String NOTIFICATION_TAG = "wzrk_pn";
@@ -224,12 +206,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";
@@ -248,11 +224,13 @@ public interface Constants {
String KEY_ENCRYPTION_LEVEL = "encryptionLevel";
String KEY_ENCRYPTION_FLAG_STATUS = "encryptionFlagStatus";
String WZRK_PUSH_ID = "wzrk_pid";
+ String WZRK_DEDUPE = "wzrk_dd";
String WZRK_PUSH_SILENT = "wzrk_pn_s";
String EXTRAS_FROM = "extras_from";
String NOTIF_MSG = "nm";
String NOTIF_TITLE = "nt";
String NOTIF_ICON = "ico";
+ String NOTIF_HIDE_APP_LARGE_ICON = "wzrk_hide_large_icon";
String WZRK_ACTIONS = "wzrk_acts";
String WZRK_BIG_PICTURE = "wzrk_bp";
String WZRK_MSG_SUMMARY = "wzrk_nms";
@@ -271,8 +249,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";
@@ -350,6 +326,12 @@ public interface Constants {
String CRYPTION_SALT = "W1ZRCl3>";
String CRYPTION_IV = "__CL3>3Rt#P__1V_";
+ String AES_PREFIX = "[";
+ String AES_SUFFIX = "]";
+
+ String AES_GCM_PREFIX = "
*/
@Deprecated
- public CTProductConfigController getCtProductConfigController() {
- initProductConfig();
+ public CTProductConfigController getCtProductConfigController(Context context) {
+ initProductConfig(context);
return getControllerManager().getCTProductConfigController();
}
@@ -336,7 +331,7 @@ public ProfileValueHandler getProfileValueHandler() {
*
*/
@Deprecated
- private void initProductConfig() {
+ private void initProductConfig(Context context) {
if (getConfig().isAnalyticsOnly()) {
getConfig().getLogger()
.debug(getConfig().getAccountId(), "Product Config is not enabled for this instance");
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/DeviceInfo.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/DeviceInfo.java
index 9df1aa08b..fb79d8155 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/DeviceInfo.java
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/DeviceInfo.java
@@ -29,6 +29,7 @@
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.annotation.WorkerThread;
+
import com.clevertap.android.sdk.login.LoginInfoProvider;
import com.clevertap.android.sdk.task.CTExecutorFactory;
import com.clevertap.android.sdk.task.OnSuccessListener;
@@ -172,8 +173,7 @@ private WindowSize getWindowSizeData() {
private String getBluetoothVersion() {
String bluetoothVersion = "none";
- if (android.os.Build.VERSION.SDK_INT >= 18 &&
- context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
+ if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
bluetoothVersion = "ble";
} else if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH)) {
bluetoothVersion = "classic";
@@ -510,7 +510,7 @@ public JSONObject getAppLaunchedFields() {
try {
boolean deviceIsMultiUser = false;
if (getGoogleAdID() != null) {
- deviceIsMultiUser = new LoginInfoProvider(context, config, this).deviceIsMultiUser();
+ deviceIsMultiUser = new LoginInfoProvider(context, config).deviceIsMultiUser();
}
return CTJsonConverter.from(this, mCoreMetaData, enableNetworkInfoReporting,
deviceIsMultiUser);
@@ -798,9 +798,9 @@ private synchronized void fetchGoogleAdID() {
} catch (Throwable t) {
if (t.getCause() != null) {
getConfigLogger().verbose(config.getAccountId(),
- "Failed to get Advertising ID: " + t.toString() + t.getCause().toString());
+ "Failed to get Advertising ID: " + t + t.getCause().toString());
} else {
- getConfigLogger().verbose(config.getAccountId(), "Failed to get Advertising ID: " + t.toString());
+ getConfigLogger().verbose(config.getAccountId(), "Failed to get Advertising ID: " + t);
}
}
if (advertisingID != null && advertisingID.trim().length() > 2) {
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/InAppNotificationActivity.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/InAppNotificationActivity.java
index 4f87c0143..afc6d5d88 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/InAppNotificationActivity.java
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/InAppNotificationActivity.java
@@ -54,6 +54,8 @@ public final class InAppNotificationActivity extends FragmentActivity implements
private PushPermissionManager pushPermissionManager;
+ private boolean invokedCallbacks = false;
+
public interface PushPermissionResultCallback {
void onPushPermissionAccept();
@@ -264,13 +266,23 @@ public void onRequestPermissionsResult(
}
void didDismiss(Bundle data) {
+ didDismiss(data, true);
+ }
+
+ void didDismiss(Bundle data, boolean killActivity) {
if (isAlertVisible) {
isAlertVisible = false;
}
- finish();
- InAppListener listener = getListener();
- if (listener != null && inAppNotification != null) {
- listener.inAppNotificationDidDismiss(inAppNotification, data);
+
+ if (!invokedCallbacks) {
+ InAppListener listener = getListener();
+ if (listener != null && inAppNotification != null) {
+ listener.inAppNotificationDidDismiss(inAppNotification, data);
+ }
+ invokedCallbacks = true;
+ }
+ if (killActivity) {
+ finish();
}
}
@@ -449,4 +461,12 @@ private void onAlertButtonClick(CTInAppNotificationButton button, boolean isPosi
didDismiss(clickData);
}
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (!isChangingConfigurations()) {
+ didDismiss(null, false);
+ }
+ }
}
\ No newline at end of file
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..581763d93 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
@@ -11,20 +11,31 @@
import androidx.annotation.WorkerThread;
import com.clevertap.android.sdk.cryption.CryptHandler;
-import com.clevertap.android.sdk.cryption.CryptUtils;
+import com.clevertap.android.sdk.cryption.CryptHandler.EncryptionAlgorithm;
+import com.clevertap.android.sdk.db.BaseDatabaseManager;
import com.clevertap.android.sdk.db.DBAdapter;
import com.clevertap.android.sdk.events.EventDetail;
+import com.clevertap.android.sdk.usereventlogs.UserEventLog;
+//import org.apache.commons.lang3.RandomStringUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Iterator;
+import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import kotlin.Pair;
+import kotlin.collections.CollectionsKt;
+import kotlin.collections.MapsKt;
+
@SuppressWarnings("unused")
@RestrictTo(Scope.LIBRARY)
public class LocalDataStore {
@@ -38,28 +49,35 @@ public class LocalDataStore {
private final Context context;
private final CryptHandler cryptHandler;
-
- private DBAdapter dbAdapter;
+ private final BaseDatabaseManager baseDatabaseManager;
private final ExecutorService es;
private final String eventNamespace = "local_events";
private final DeviceInfo deviceInfo;
+ private final Set userNormalizedEventLogKeys = Collections.synchronizedSet(new HashSet<>());
+ private final Map normalizedEventNames = new HashMap<>();
- LocalDataStore(Context context, CleverTapInstanceConfig config, CryptHandler cryptHandler, DeviceInfo deviceInfo) {
+ LocalDataStore(Context context, CleverTapInstanceConfig config, CryptHandler cryptHandler, DeviceInfo deviceInfo, BaseDatabaseManager baseDatabaseManager) {
this.context = context;
this.config = config;
this.es = Executors.newFixedThreadPool(1);
this.cryptHandler = cryptHandler;
this.deviceInfo = deviceInfo;
+ this.baseDatabaseManager = baseDatabaseManager;
}
@WorkerThread
public void changeUser() {
+ userNormalizedEventLogKeys.clear();
resetLocalProfileSync();
}
+ /**
+ * @deprecated since v7.1.0
. Use {@link #readUserEventLog(String)}
+ */
+ @Deprecated(since = "7.1.0")
EventDetail getEventDetail(String eventName) {
try {
if (!isPersonalisationEnabled()) {
@@ -77,7 +95,10 @@ EventDetail getEventDetail(String eventName) {
return null;
}
}
-
+ /**
+ * @deprecated since v7.1.0
. Use {@link #readUserEventLogs()}
+ */
+ @Deprecated(since = "7.1.0")
Map getEventHistory(Context context) {
try {
String namespace;
@@ -100,6 +121,10 @@ Map getEventHistory(Context context) {
}
}
+ /**
+ * @deprecated since v7.1.0
. Use {@link #persistUserEventLog(String)}
+ */
+ @Deprecated(since = "7.1.0")
@WorkerThread
public void persistEvent(Context context, JSONObject event, int type) {
@@ -115,6 +140,203 @@ public void persistEvent(Context context, JSONObject event, int type) {
getConfigLogger().verbose(getConfigAccountId(), "Failed to sync with upstream", t);
}
}
+ @WorkerThread
+ public boolean persistUserEventLogsInBulk(Set eventNames){
+ Set> setOfActualAndNormalizedEventNamePair = new HashSet<>();
+ CollectionsKt.mapTo(eventNames, setOfActualAndNormalizedEventNamePair,
+ (actualEventName) -> new Pair<>(actualEventName, getOrPutNormalizedEventName(actualEventName)));
+ return upsertUserEventLogsInBulk(setOfActualAndNormalizedEventNamePair);
+ }
+
+ @WorkerThread
+ public boolean persistUserEventLog(String eventName) {
+
+ if (eventName == null) {
+ return false;
+ }
+
+ Logger logger = config.getLogger();
+ String accountId = config.getAccountId();
+ try {
+ logger.verbose(accountId,"UserEventLog: Persisting EventLog for event "+eventName);
+ if (isUserEventLogExists(eventName)){
+ logger.verbose(accountId,"UserEventLog: Updating EventLog for event "+eventName);
+ return updateUserEventLog(eventName);
+ } else {
+ logger.verbose(accountId,"UserEventLog: Inserting EventLog for event "+eventName);
+ return insertUserEventLog(eventName );
+ }
+ /*
+ * ==========TESTING BLOCK START ==========
+ */
+ /*cleanUpExtraEvents(50);
+
+ UserEventLog userEventLog = readUserEventLog(eventName);
+ logger.verbose(accountId,"UserEventLog: EventLog for event "+eventName+" = "+userEventLog);
+
+ List list = readUserEventLogs();
+ logger.verbose(accountId,"UserEventLog: All EventLog list for User "+list);
+
+ List list1 = readEventLogsForAllUsers();
+ logger.verbose(accountId,"UserEventLog: All user EventLog list "+list1);
+
+ int count = readUserEventLogCount(eventName);
+ logger.verbose(accountId,"UserEventLog: EventLog count for event "+eventName+" = "+count);
+
+ long logFirstTs = readUserEventLogFirstTs(eventName);
+ logger.verbose(accountId,"UserEventLog: EventLog firstTs for event "+eventName+" = "+logFirstTs);
+
+ long logLastTs = readUserEventLogLastTs(eventName);
+ logger.verbose(accountId,"UserEventLog: EventLog lastTs for event "+eventName+" = "+logLastTs);
+
+ boolean isUserEventLogFirstTime = isUserEventLogFirstTime(eventName);
+ logger.verbose(accountId,"UserEventLog: EventLog isUserEventLogFirstTime for event "+eventName+" = "+isUserEventLogFirstTime);*/
+ /*
+ * ==========TESTING BLOCK END ==========
+ */
+ } catch (Throwable t) {
+ logger.verbose(accountId, "UserEventLog: Failed to insert user event log: for event" + eventName, t);
+ return false;
+ }
+ }
+
+ @WorkerThread
+ private boolean updateEventByDeviceIdAndNormalizedEventName(String deviceID, String normalizedEventName) {
+ DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context);
+ boolean updatedEventByDeviceID = dbAdapter.userEventLogDAO().updateEventByDeviceIdAndNormalizedEventName(deviceID, normalizedEventName);
+ getConfigLogger().verbose("updatedEventByDeviceID = " + updatedEventByDeviceID);
+ return updatedEventByDeviceID;
+ }
+
+ @WorkerThread
+ public boolean updateUserEventLog(String eventName) {
+ String deviceID = deviceInfo.getDeviceID();
+ String normalizedEventName = getOrPutNormalizedEventName(eventName);
+ return updateEventByDeviceIdAndNormalizedEventName(deviceID, normalizedEventName);
+ }
+
+ @WorkerThread
+ private boolean upsertUserEventLogsInBulk(Set> setOfActualAndNormalizedEventNamePair) {
+ String deviceID = deviceInfo.getDeviceID();
+ DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context);
+ boolean upsertEventByDeviceID = dbAdapter.userEventLogDAO()
+ .upsertEventsByDeviceIdAndNormalizedEventName(deviceID, setOfActualAndNormalizedEventNamePair);
+ getConfigLogger().verbose("upsertEventByDeviceID = " + upsertEventByDeviceID);
+ return upsertEventByDeviceID;
+ }
+
+ @WorkerThread
+ private long insertEvent(String deviceID, String actualEventName, String normalizedEventName) {
+ DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context);
+ long rowId = dbAdapter.userEventLogDAO().insertEvent(deviceID, actualEventName, normalizedEventName);
+ getConfigLogger().verbose("inserted rowId = " + rowId);
+ return rowId;
+ }
+
+ private String getOrPutNormalizedEventName(String actualEventName) {
+ return MapsKt.getOrPut(normalizedEventNames, actualEventName,
+ () -> Utils.getNormalizedName(actualEventName));
+ }
+
+ @WorkerThread
+ public boolean insertUserEventLog(String eventName) {
+ String deviceID = deviceInfo.getDeviceID();
+ String normalizedEventName = getOrPutNormalizedEventName(eventName);
+ long rowId = insertEvent(deviceID, eventName, normalizedEventName);
+ return rowId >= 0;
+ }
+
+ @WorkerThread
+ private boolean eventExistsByDeviceIdAndNormalizedEventName(String deviceID, String normalizedEventName) {
+ DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context);
+ boolean eventExists = dbAdapter.userEventLogDAO().eventExistsByDeviceIdAndNormalizedEventName(deviceID, normalizedEventName);
+ getConfigLogger().verbose("eventExists = "+eventExists);
+ return eventExists;
+ }
+
+ @WorkerThread
+ public boolean isUserEventLogExists(String eventName) {
+ String deviceID = deviceInfo.getDeviceID();
+ String normalizedEventName = getOrPutNormalizedEventName(eventName);
+ return eventExistsByDeviceIdAndNormalizedEventName(deviceID, normalizedEventName);
+ }
+
+ @WorkerThread
+ private boolean eventExistsByDeviceIdAndNormalizedEventNameAndCount(String deviceID, String normalizedEventName, int count) {
+ DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context);
+ boolean eventExistsByDeviceIDAndCount = dbAdapter.userEventLogDAO()
+ .eventExistsByDeviceIdAndNormalizedEventNameAndCount(deviceID, normalizedEventName, count);
+
+ getConfigLogger().verbose("eventExistsByDeviceIDAndCount = " + eventExistsByDeviceIDAndCount);
+ return eventExistsByDeviceIDAndCount;
+ }
+
+ @WorkerThread
+ public boolean isUserEventLogFirstTime(String eventName) {
+ String normalizedEventName = getOrPutNormalizedEventName(eventName);
+ if (userNormalizedEventLogKeys.contains(normalizedEventName)) {
+ return false;
+ }
+
+ String deviceID = deviceInfo.getDeviceID();
+ int count = readEventCountByDeviceIdAndNormalizedEventName(deviceID, normalizedEventName);
+ if (count > 1) {
+ userNormalizedEventLogKeys.add(normalizedEventName);
+ }
+ return count == 1;
+ }
+
+ @WorkerThread
+ public boolean cleanUpExtraEvents(int threshold, int numberOfRowsToCleanup){
+ DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context);
+ boolean cleanUpExtraEvents = dbAdapter.userEventLogDAO().cleanUpExtraEvents(threshold, numberOfRowsToCleanup);
+ getConfigLogger().verbose("cleanUpExtraEvents boolean= "+cleanUpExtraEvents);
+ return cleanUpExtraEvents;
+ }
+
+ @WorkerThread
+ private UserEventLog readEventByDeviceIdAndNormalizedEventName(String deviceID, String normalizedEventName) {
+ DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context);
+ return dbAdapter.userEventLogDAO().readEventByDeviceIdAndNormalizedEventName(deviceID, normalizedEventName);
+ }
+
+ @WorkerThread
+ public UserEventLog readUserEventLog(String eventName) {
+ String deviceID = deviceInfo.getDeviceID();
+ String normalizedEventName = getOrPutNormalizedEventName(eventName);
+ return readEventByDeviceIdAndNormalizedEventName(deviceID, normalizedEventName);
+ }
+
+ @WorkerThread
+ private int readEventCountByDeviceIdAndNormalizedEventName(String deviceID, String normalizedEventName) {
+ DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context);
+ return dbAdapter.userEventLogDAO().readEventCountByDeviceIdAndNormalizedEventName(deviceID, normalizedEventName);
+ }
+
+ @WorkerThread
+ public int readUserEventLogCount(String eventName) {
+ String deviceID = deviceInfo.getDeviceID();
+ String normalizedEventName = getOrPutNormalizedEventName(eventName);
+ return readEventCountByDeviceIdAndNormalizedEventName(deviceID, normalizedEventName);
+ }
+
+ @WorkerThread
+ private List allEventsByDeviceID(String deviceID) {
+ DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context);
+ return dbAdapter.userEventLogDAO().allEventsByDeviceID(deviceID);
+ }
+
+ @WorkerThread
+ public List readUserEventLogs(){
+ String deviceID = deviceInfo.getDeviceID();
+ return allEventsByDeviceID(deviceID);
+ }
+
+ @WorkerThread
+ public List readEventLogsForAllUsers() {
+ DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context);
+ return dbAdapter.userEventLogDAO().allEvents();
+ }
@WorkerThread
public void setDataSyncFlag(JSONObject event) {
@@ -184,6 +406,10 @@ public Object getProfileProperty(String key) {
}
}
+ /**
+ * @deprecated since v7.1.0
in favor of DB. See {@link UserEventLog}
+ */
+ @Deprecated(since = "7.1.0")
private EventDetail decodeEventDetails(String name, String encoded) {
if (encoded == null) {
return null;
@@ -194,6 +420,10 @@ private EventDetail decodeEventDetails(String name, String encoded) {
Integer.parseInt(parts[2]), name);
}
+ /**
+ * @deprecated since v7.1.0
in favor of DB. See {@link UserEventLog}
+ */
+ @Deprecated(since = "7.1.0")
private String encodeEventDetails(int first, int last, int count) {
return count + "|" + first + "|" + last;
}
@@ -220,6 +450,10 @@ private int getLocalCacheExpiryInterval(int defaultInterval) {
return getIntFromPrefs("local_cache_expires_in", defaultInterval);
}
+ /**
+ * @deprecated since v7.1.0
in favor of DB. See {@link UserEventLog}
+ */
+ @Deprecated(since = "7.1.0")
private String getStringFromPrefs(String rawKey, String defaultValue, String nameSpace) {
if (this.config.isDefaultInstance()) {
String _new = StorageHelper
@@ -242,9 +476,7 @@ void inflateLocalProfileAsync(final Context context) {
this.postAsyncSafely("LocalDataStore#inflateLocalProfileAsync", new Runnable() {
@Override
public void run() {
- if (dbAdapter == null) {
- dbAdapter = new DBAdapter(context, config);
- }
+ DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context);
synchronized (PROFILE_FIELDS_IN_THIS_SESSION) {
try {
JSONObject profile = dbAdapter.fetchUserProfileByAccountIdAndDeviceID(accountID, deviceInfo.getDeviceID());
@@ -267,7 +499,7 @@ public void run() {
} else {
Object decrypted = value;
if (value instanceof String) {
- decrypted = cryptHandler.decrypt((String) value, key);
+ decrypted = cryptHandler.decrypt((String) value, key, EncryptionAlgorithm.AES_GCM);
if (decrypted == null)
decrypted = value;
}
@@ -294,6 +526,10 @@ private boolean isPersonalisationEnabled() {
return this.config.isPersonalizationEnabled();
}
+ /**
+ * @deprecated since v7.1.0
. Use {@link #persistUserEventLog(String)}
+ */
+ @Deprecated(since = "7.1.0")
@SuppressWarnings("ConstantConditions")
@SuppressLint("CommitPrefEdits")
private void persistEvent(Context context, JSONObject event) {
@@ -339,7 +575,7 @@ public void run() {
if (profile.get(piiKey) != null) {
Object value = profile.get(piiKey);
if (value instanceof String) {
- String encrypted = cryptHandler.encrypt((String) value, piiKey);
+ String encrypted = cryptHandler.encrypt((String) value, piiKey, EncryptionAlgorithm.AES_GCM);
if (encrypted == null) {
passFlag = false;
continue;
@@ -351,8 +587,9 @@ public void run() {
JSONObject jsonObjectEncrypted = new JSONObject(profile);
if (!passFlag)
- CryptUtils.updateEncryptionFlagOnFailure(context, config, Constants.ENCRYPTION_FLAG_DB_SUCCESS, cryptHandler);
+ cryptHandler.updateMigrationFailureCount(false);
+ DBAdapter dbAdapter = baseDatabaseManager.loadDBAdapter(context);
long status = dbAdapter.storeUserProfile(profileID, deviceInfo.getDeviceID(), jsonObjectEncrypted);
getConfigLogger().verbose(getConfigAccountId(),
"Persist Local Profile complete with status " + status + " for id " + profileID);
@@ -433,7 +670,6 @@ private void _removeProfileField(String key) {
}
}
}
-
private void _setProfileField(String key, Object value) {
if (value == null) {
return;
@@ -454,10 +690,25 @@ private void _setProfileField(String key, Object value) {
* @param fields, a map of key value pairs to be updated locally. The value will be null if that key needs to be
* removed
*/
+// int k = 0;
public void updateProfileFields(Map fields) {
if(fields.isEmpty())
return;
-
+ /*Set events = new HashSet<>();
+ for (int i = 0; i < 5000; i++) {
+ String s = "profile field - "+k+"-"+i;//RandomStringUtils.randomAlphanumeric(512);
+ events.add(s);
+ }
+ k++;*/
+ long start = System.nanoTime();
+ persistUserEventLogsInBulk(fields.keySet());
+// persistUserEventLogsInBulk(events);
+ /*for (String key : events)
+ {
+ persistUserEventLog(key);
+ }*/
+ long end = System.nanoTime();
+ config.getLogger().verbose(config.getAccountId(),"UserEventLog: persistUserEventLog execution time = "+(end - start)+" nano seconds");
for (Map.Entry entry : fields.entrySet()) {
String key = entry.getKey();
Object newValue = entry.getValue();
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/Logger.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/Logger.java
index e2f065dfd..d45a50525 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/Logger.java
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/Logger.java
@@ -6,6 +6,10 @@ public final class Logger implements ILogger {
private int debugLevel;
+ Logger(int level) {
+ this.debugLevel = level;
+ }
+
/**
* Logs to Debug if the debug level is greater than 1.
*/
@@ -90,10 +94,6 @@ public static void v(String message, Throwable t) {
}
}
- Logger(int level) {
- this.debugLevel = level;
- }
-
@Override
public void debug(String message) {
if (getStaticDebugLevel() > CleverTapAPI.LogLevel.INFO.intValue()) {
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/ManifestInfo.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/ManifestInfo.java
index b9cca9a55..c10465c76 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/ManifestInfo.java
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/ManifestInfo.java
@@ -6,58 +6,89 @@
import android.os.Bundle;
import android.text.TextUtils;
import androidx.annotation.RestrictTo;
-
+import androidx.annotation.VisibleForTesting;
+
+/**
+ * Parser for android manifest and picks up fields from manifest once to be references
+ *
+ * Should be singleton and initialised only once -> need to validate.
+ */
+// todo lp Remove context dependency from here
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class ManifestInfo {
- private static String accountId;
+ private static final String LABEL_ACCOUNT_ID = "CLEVERTAP_ACCOUNT_ID";
+ private static final String LABEL_TOKEN = "CLEVERTAP_TOKEN";
+ public static final String LABEL_NOTIFICATION_ICON = "CLEVERTAP_NOTIFICATION_ICON";
+ private static final String LABEL_INAPP_EXCLUDE = "CLEVERTAP_INAPP_EXCLUDE";
+ private static final String LABEL_REGION = "CLEVERTAP_REGION";
+ private static final String LABEL_PROXY_DOMAIN = "CLEVERTAP_PROXY_DOMAIN";
+ private static final String LABEL_SPIKY_PROXY_DOMAIN = "CLEVERTAP_SPIKY_PROXY_DOMAIN";
+ private static final String LABEL_CLEVERTAP_HANDSHAKE_DOMAIN = "CLEVERTAP_HANDSHAKE_DOMAIN";
+ private static final String LABEL_DISABLE_APP_LAUNCH = "CLEVERTAP_DISABLE_APP_LAUNCHED";
+ private static final String LABEL_SSL_PINNING = "CLEVERTAP_SSL_PINNING";
+ private static final String LABEL_BACKGROUND_SYNC = "CLEVERTAP_BACKGROUND_SYNC";
+ private static final String LABEL_CUSTOM_ID = "CLEVERTAP_USE_CUSTOM_ID";
+ private static final String LABEL_USE_GOOGLE_AD_ID = "CLEVERTAP_USE_GOOGLE_AD_ID";
+ private static final String LABEL_FCM_SENDER_ID = "FCM_SENDER_ID";
+ private static final String LABEL_PACKAGE_NAME = "CLEVERTAP_APP_PACKAGE";
+ private static final String LABEL_BETA = "CLEVERTAP_BETA";
+ private static final String LABEL_INTENT_SERVICE = "CLEVERTAP_INTENT_SERVICE";
+ private static final String LABEL_ENCRYPTION_LEVEL = "CLEVERTAP_ENCRYPTION_LEVEL";
+ private static final String LABEL_DEFAULT_CHANNEL_ID = "CLEVERTAP_DEFAULT_CHANNEL_ID";
+
+ private static ManifestInfo instance; // singleton
- private static String accountToken;
+ public synchronized static ManifestInfo getInstance(Context context) {
+ if (instance == null) {
+ instance = new ManifestInfo(context);
+ }
+ return instance;
+ }
- private static String accountRegion;
+ static void changeCredentials(String id, String token, String region) {
+ accountId = id;
+ accountToken = token;
+ accountRegion = region;
+ }
- private static String proxyDomain;
+ static void changeCredentials(String id, String token, String _proxyDomain, String _spikyProxyDomain) {
+ accountId = id;
+ accountToken = token;
+ proxyDomain = _proxyDomain;
+ spikyProxyDomain = _spikyProxyDomain;
+ }
- private static String spikyProxyDomain;
+ static void changeCredentials(String id, String token, String _proxyDomain, String _spikyProxyDomain, String customHandshakeDomain) {
+ accountId = id;
+ accountToken = token;
+ proxyDomain = _proxyDomain;
+ spikyProxyDomain = _spikyProxyDomain;
+ handshakeDomain = customHandshakeDomain;
+ }
+ // Have to keep static due to change creds
+ private static String accountId;
+ private static String accountToken;
+ private static String accountRegion;
+ private static String proxyDomain;
+ private static String spikyProxyDomain;
private static String handshakeDomain;
- private static boolean useADID;
-
- private static boolean appLaunchedDisabled;
-
- private static String notificationIcon;
-
- private static ManifestInfo instance;
-
- private static String excludedActivitiesForInApps;
-
- private static boolean sslPinning;
-
- private static boolean backgroundSync;
-
- private static boolean useCustomID;
-
- private static String fcmSenderId;
-
- private static String packageName;
-
- private static boolean beta;
-
- private static String intentServiceName;
-
+ private final boolean useADID;
+ private final boolean appLaunchedDisabled;
+ private final String notificationIcon;
+ private final String excludedActivitiesForInApps;
+ private final boolean sslPinning;
+ private final boolean backgroundSync;
+ private final boolean useCustomID;
+ private final String fcmSenderId;
+ private final String packageName;
+ private final boolean beta;
+ private final String intentServiceName;
private final String devDefaultPushChannelId;
-
private final String[] profileKeys;
-
- private static int encryptionLevel;
-
- public synchronized static ManifestInfo getInstance(Context context) {
- if (instance == null) {
- instance = new ManifestInfo(context);
- }
- return instance;
- }
+ private final int encryptionLevel;
private ManifestInfo(Context context) {
Bundle metaData = null;
@@ -71,58 +102,123 @@ private ManifestInfo(Context context) {
if (metaData == null) {
metaData = new Bundle();
}
+
+ // start -> assign these if they did not happen in changeCredentials
if (accountId == null) {
- accountId = _getManifestStringValueForKey(metaData, Constants.LABEL_ACCOUNT_ID);
+ accountId = _getManifestStringValueForKey(metaData, ManifestInfo.LABEL_ACCOUNT_ID);
}
if (accountToken == null) {
- accountToken = _getManifestStringValueForKey(metaData, Constants.LABEL_TOKEN);
+ accountToken = _getManifestStringValueForKey(metaData, ManifestInfo.LABEL_TOKEN);
}
if (accountRegion == null) {
- accountRegion = _getManifestStringValueForKey(metaData, Constants.LABEL_REGION);
+ accountRegion = _getManifestStringValueForKey(metaData, ManifestInfo.LABEL_REGION);
}
if (proxyDomain == null) {
- proxyDomain = _getManifestStringValueForKey(metaData, Constants.LABEL_PROXY_DOMAIN);
+ proxyDomain = _getManifestStringValueForKey(metaData, ManifestInfo.LABEL_PROXY_DOMAIN);
}
if (spikyProxyDomain == null) {
- spikyProxyDomain = _getManifestStringValueForKey(metaData, Constants.LABEL_SPIKY_PROXY_DOMAIN);
+ spikyProxyDomain = _getManifestStringValueForKey(metaData, ManifestInfo.LABEL_SPIKY_PROXY_DOMAIN);
}
if (handshakeDomain == null) {
- handshakeDomain = _getManifestStringValueForKey(metaData, Constants.LABEL_CLEVERTAP_HANDSHAKE_DOMAIN);
+ handshakeDomain = _getManifestStringValueForKey(metaData, ManifestInfo.LABEL_CLEVERTAP_HANDSHAKE_DOMAIN);
+ }
+ // end -> assign these if they did not happen in changeCredentials
+
+ notificationIcon = _getManifestStringValueForKey(metaData, ManifestInfo.LABEL_NOTIFICATION_ICON);
+ useADID = "1".equals(_getManifestStringValueForKey(metaData, ManifestInfo.LABEL_USE_GOOGLE_AD_ID));
+ appLaunchedDisabled = "1".equals(_getManifestStringValueForKey(metaData, ManifestInfo.LABEL_DISABLE_APP_LAUNCH));
+ excludedActivitiesForInApps = _getManifestStringValueForKey(metaData, ManifestInfo.LABEL_INAPP_EXCLUDE);
+ sslPinning = "1".equals(_getManifestStringValueForKey(metaData, ManifestInfo.LABEL_SSL_PINNING));
+ backgroundSync = "1".equals(_getManifestStringValueForKey(metaData, ManifestInfo.LABEL_BACKGROUND_SYNC));
+ useCustomID = "1".equals(_getManifestStringValueForKey(metaData, ManifestInfo.LABEL_CUSTOM_ID));
+
+ String fcmSenderIdTemp;
+ fcmSenderIdTemp = _getManifestStringValueForKey(metaData, ManifestInfo.LABEL_FCM_SENDER_ID);
+ if (fcmSenderIdTemp != null) {
+ fcmSenderIdTemp = fcmSenderIdTemp.replace("id:", "");
}
- notificationIcon = _getManifestStringValueForKey(metaData, Constants.LABEL_NOTIFICATION_ICON);
- useADID = "1".equals(_getManifestStringValueForKey(metaData, Constants.LABEL_USE_GOOGLE_AD_ID));
- appLaunchedDisabled = "1".equals(_getManifestStringValueForKey(metaData, Constants.LABEL_DISABLE_APP_LAUNCH));
- excludedActivitiesForInApps = _getManifestStringValueForKey(metaData, Constants.LABEL_INAPP_EXCLUDE);
- sslPinning = "1".equals(_getManifestStringValueForKey(metaData, Constants.LABEL_SSL_PINNING));
- backgroundSync = "1".equals(_getManifestStringValueForKey(metaData, Constants.LABEL_BACKGROUND_SYNC));
- useCustomID = "1".equals(_getManifestStringValueForKey(metaData, Constants.LABEL_CUSTOM_ID));
- fcmSenderId = _getManifestStringValueForKey(metaData, Constants.LABEL_FCM_SENDER_ID);
+ fcmSenderId = fcmSenderIdTemp;
+
+ int encLvlTemp;
try {
- int parsedEncryptionLevel = Integer.parseInt(_getManifestStringValueForKey(metaData,Constants.LABEL_ENCRYPTION_LEVEL));
- if(parsedEncryptionLevel >= 0 && parsedEncryptionLevel <= 1){
- encryptionLevel = parsedEncryptionLevel;
- }
- else{
- encryptionLevel = 0;
+ int parsedEncryptionLevel = Integer.parseInt(_getManifestStringValueForKey(metaData, ManifestInfo.LABEL_ENCRYPTION_LEVEL));
+
+ if (parsedEncryptionLevel >= 0 && parsedEncryptionLevel <= 1) {
+ encLvlTemp = parsedEncryptionLevel;
+ } else {
+ encLvlTemp = 0;
Logger.v("Supported encryption levels are only 0 and 1. Setting it to 0 by default");
}
- } catch (Throwable t){
- encryptionLevel = 0;
+ } catch (Throwable t) {
+ encLvlTemp = 0;
Logger.v("Unable to parse encryption level from the Manifest, Setting it to 0 by default", t.getCause());
}
+ encryptionLevel = encLvlTemp;
- if (fcmSenderId != null) {
- fcmSenderId = fcmSenderId.replace("id:", "");
+ packageName = _getManifestStringValueForKey(metaData, ManifestInfo.LABEL_PACKAGE_NAME);
+ beta = "1".equals(_getManifestStringValueForKey(metaData, ManifestInfo.LABEL_BETA));
+ intentServiceName = _getManifestStringValueForKey(metaData, ManifestInfo.LABEL_INTENT_SERVICE);
+ devDefaultPushChannelId = _getManifestStringValueForKey(metaData, ManifestInfo.LABEL_DEFAULT_CHANNEL_ID);
+ profileKeys = parseProfileKeys(metaData);
+ }
+
+ ManifestInfo(
+ String accountId,
+ String accountToken,
+ String accountRegion,
+ String proxyDomain,
+ String spikyProxyDomain,
+ String handshakeDomain,
+ boolean useADID,
+ boolean appLaunchedDisabled,
+ String notificationIcon,
+ String excludedActivitiesForInApps,
+ boolean sslPinning,
+ boolean backgroundSync,
+ boolean useCustomID,
+ String fcmSenderId,
+ String packageName,
+ boolean beta,
+ String intentServiceName,
+ String devDefaultPushChannelId,
+ String[] profileKeys,
+ int encryptionLevel
+ ) {
+
+ // assign these if they did not happen in change creds
+ if (ManifestInfo.accountId == null) {
+ ManifestInfo.accountId = accountId;
}
- packageName = _getManifestStringValueForKey(metaData, Constants.LABEL_PACKAGE_NAME);
- beta = "1".equals(_getManifestStringValueForKey(metaData, Constants.LABEL_BETA));
- if (intentServiceName == null) {
- intentServiceName = _getManifestStringValueForKey(metaData, Constants.LABEL_INTENT_SERVICE);
+ if (ManifestInfo.accountToken == null) {
+ ManifestInfo.accountToken = accountToken;
+ }
+ if (ManifestInfo.accountRegion == null) {
+ ManifestInfo.accountRegion = accountRegion;
+ }
+ if (ManifestInfo.proxyDomain == null) {
+ ManifestInfo.proxyDomain = proxyDomain;
+ }
+ if (ManifestInfo.spikyProxyDomain == null) {
+ ManifestInfo.spikyProxyDomain = spikyProxyDomain;
+ }
+ if (ManifestInfo.handshakeDomain == null) {
+ ManifestInfo.handshakeDomain = handshakeDomain;
}
- devDefaultPushChannelId = _getManifestStringValueForKey(metaData, Constants.LABEL_DEFAULT_CHANNEL_ID);
-
- profileKeys = parseProfileKeys(metaData);
+ this.useADID = useADID;
+ this.appLaunchedDisabled = appLaunchedDisabled;
+ this.notificationIcon = notificationIcon;
+ this.excludedActivitiesForInApps = excludedActivitiesForInApps;
+ this.sslPinning = sslPinning;
+ this.backgroundSync = backgroundSync;
+ this.useCustomID = useCustomID;
+ this.fcmSenderId = fcmSenderId;
+ this.packageName = packageName;
+ this.beta = beta;
+ this.intentServiceName = intentServiceName;
+ this.devDefaultPushChannelId = devDefaultPushChannelId;
+ this.profileKeys = profileKeys;
+ this.encryptionLevel = encryptionLevel;
}
public String getAccountId() {
@@ -219,27 +315,6 @@ private String[] parseProfileKeys(final Bundle metaData) {
: Constants.NULL_STRING_ARRAY;
}
- static void changeCredentials(String id, String token, String region) {
- accountId = id;
- accountToken = token;
- accountRegion = region;
- }
-
- static void changeCredentials(String id, String token, String _proxyDomain, String _spikyProxyDomain) {
- accountId = id;
- accountToken = token;
- proxyDomain = _proxyDomain;
- spikyProxyDomain = _spikyProxyDomain;
- }
-
- static void changeCredentials(String id, String token, String _proxyDomain, String _spikyProxyDomain, String customHandshakeDomain) {
- accountId = id;
- accountToken = token;
- proxyDomain = _proxyDomain;
- spikyProxyDomain = _spikyProxyDomain;
- handshakeDomain = customHandshakeDomain;
- }
-
/**
* This returns string representation of int,boolean,string,float value of given key
*
@@ -247,7 +322,7 @@ static void changeCredentials(String id, String token, String _proxyDomain, Stri
* @param name key of bundle
* @return string representation of int,boolean,string,float
*/
- private static String _getManifestStringValueForKey(Bundle manifest, String name) {
+ private String _getManifestStringValueForKey(Bundle manifest, String name) {
try {
Object o = manifest.get(name);
return (o != null) ? o.toString() : null;
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/SessionManager.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/SessionManager.java
index d93866e34..8fc42d075 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/SessionManager.java
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/SessionManager.java
@@ -2,14 +2,21 @@
import android.content.Context;
import android.content.SharedPreferences;
+
+import androidx.annotation.RestrictTo;
+import androidx.annotation.WorkerThread;
+
import com.clevertap.android.sdk.events.EventDetail;
+import com.clevertap.android.sdk.usereventlogs.UserEventLog;
import com.clevertap.android.sdk.validation.Validator;
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class SessionManager extends BaseSessionManager {
private long appLastSeen = 0;
private int lastVisitTime;
+ private long userLastVisitTs;
private final CoreMetaData cleverTapMetaData;
@@ -85,6 +92,11 @@ void setLastVisitTime() {
lastVisitTime = ed.getLastTime();
}
}
+ @WorkerThread
+ void setUserLastVisitTs() {
+ UserEventLog appLaunchedEventLog = localDataStore.readUserEventLog(Constants.APP_LAUNCHED_EVENT);
+ userLastVisitTs = appLaunchedEventLog != null ? appLaunchedEventLog.getLastTs() : -1;
+ }
private void createSession(final Context context) {
int sessionId = getNow();
@@ -118,4 +130,7 @@ int getNow() {
return (int) (System.currentTimeMillis() / 1000);
}
+ public long getUserLastVisitTs() {
+ return userLastVisitTs;
+ }
}
\ No newline at end of file
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/StorageHelper.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/StorageHelper.java
index d8ae8adfa..711adac27 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/StorageHelper.java
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/StorageHelper.java
@@ -102,10 +102,20 @@ public static void removeImmediate(Context context, String key) {
}
//Preferences
+
+ @Deprecated
+ /*
+ Use the method storageKeyWithSuffix(String accountID, String key) instead.")
+ */
public static String storageKeyWithSuffix(@NonNull CleverTapInstanceConfig config,@NonNull String key) {
return key + ":" + config.getAccountId();
}
+ public static String storageKeyWithSuffix(String accountID, @NonNull String key) {
+ // todo - use this throughout the sdk instead of the one-above
+ return key + ":" + accountID;
+ }
+
@SuppressWarnings("SameParameterValue")
public static boolean getBoolean(Context context, String key, boolean defaultValue) {
return getPreferences(context).getBoolean(key, defaultValue);
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/StoreProvider.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/StoreProvider.kt
index 7507d55ed..d5f5f9aef 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/StoreProvider.kt
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/StoreProvider.kt
@@ -30,7 +30,7 @@ const val STORE_TYPE_FILES = 5
*
* @property INSTANCE The singleton instance of the [StoreProvider].
*/
-class StoreProvider {
+internal class StoreProvider {
companion object {
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/cryption/AESCrypt.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/AESCrypt.kt
index 584e1e5fb..807d365c9 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/AESCrypt.kt
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/AESCrypt.kt
@@ -14,30 +14,25 @@ import javax.crypto.spec.SecretKeySpec
/**
* This class implements the AES Cryption algorithm
*/
-class AESCrypt : Crypt() {
+class AESCrypt(accountID: String) : Crypt() {
/**
* This method returns the key-password to be used for encryption/decryption
*
- * @param accountID : accountId of the current instance
* @return key-password
*/
- private fun generateKeyPassword(accountID: String): String {
- return APP_ID_KEY_PREFIX + accountID + APP_ID_KEY_SUFFIX
- }
+ private val keyPassword: String = APP_ID_KEY_PREFIX + accountID + APP_ID_KEY_SUFFIX
/**
* This method is used internally to encrypt the plain text
*
* @param plainText - plainText to be encrypted
- * @param accountID - accountID used for password generation
* @return encrypted text
*/
- override fun encryptInternal(plainText: String, accountID: String): String? {
-
+ override fun encryptInternal(plainText: String): String? {
return performCryptOperation(
- Cipher.ENCRYPT_MODE, generateKeyPassword(accountID), plainText.toByteArray(
- StandardCharsets.UTF_8
- )
+ mode = Cipher.ENCRYPT_MODE,
+ password = keyPassword,
+ text = plainText.toByteArray(StandardCharsets.UTF_8)
)?.let { encryptedBytes ->
encryptedBytes.contentToString()
}
@@ -48,12 +43,15 @@ class AESCrypt : Crypt() {
* This method is used internally to decrypt the cipher text
*
* @param cipherText - cipherText to be decrypted
- * @param accountID - accountID used for password generation
* @return decrypted text
*/
- override fun decryptInternal(cipherText: String, accountID: String): String? {
+ override fun decryptInternal(cipherText: String): String? {
return parseCipherText(cipherText)?.let { bytes ->
- performCryptOperation(Cipher.DECRYPT_MODE, generateKeyPassword(accountID), bytes)
+ performCryptOperation(
+ mode = Cipher.DECRYPT_MODE,
+ password = keyPassword,
+ text = bytes
+ )
}?.let { decryptedBytes ->
String(decryptedBytes, StandardCharsets.UTF_8)
}
@@ -65,7 +63,7 @@ class AESCrypt : Crypt() {
* @param cipherText - cipher text to be parsed
* @return Parsed string in the form of a byte array
*/
- override fun parseCipherText(cipherText: String): ByteArray? {
+ private fun parseCipherText(cipherText: String): ByteArray? {
return try {
// Removes the enclosing brackets, trims any leading or trailing whitespace, and then splits the resulting string based on commas
val byteStrings =
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/AESGCMCrypt.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/AESGCMCrypt.kt
new file mode 100644
index 000000000..105bc6269
--- /dev/null
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/AESGCMCrypt.kt
@@ -0,0 +1,214 @@
+package com.clevertap.android.sdk.cryption
+
+import android.content.Context
+import android.os.Build
+import android.security.keystore.KeyGenParameterSpec
+import android.security.keystore.KeyProperties
+import android.util.Base64
+import com.clevertap.android.sdk.Constants.AES_GCM_PREFIX
+import com.clevertap.android.sdk.Constants.AES_GCM_SUFFIX
+import com.clevertap.android.sdk.Logger
+import com.clevertap.android.sdk.StorageHelper
+import java.nio.charset.StandardCharsets
+import java.security.KeyStore
+import javax.crypto.Cipher
+import javax.crypto.KeyGenerator
+import javax.crypto.SecretKey
+import javax.crypto.spec.GCMParameterSpec
+import javax.crypto.spec.SecretKeySpec
+
+private const val ENCRYPTION_KEY = "EncryptionKey"
+
+/**
+ * This class implements the AES-GCM Crypt algorithm
+ *
+ */
+class AESGCMCrypt(private val context: Context) : Crypt() {
+
+ /**
+ * This method is used internally to encrypt the plain text
+ *
+ * @param plainText - plainText to be encrypted
+ * @return encrypted text appended with iv, prefix and suffix
+ */
+ override fun encryptInternal(plainText: String): String? {
+ return performCryptOperation(
+ mode = Cipher.ENCRYPT_MODE,
+ data = plainText.toByteArray(StandardCharsets.UTF_8)
+ )?.let { (iv, encryptedBytes) ->
+ // Concatenate IV and encrypted text with a ":" delimiter
+ "$AES_GCM_PREFIX${iv.toBase64()}:${encryptedBytes.toBase64()}$AES_GCM_SUFFIX"
+ }
+ }
+
+
+ /**
+ * This method is used internally to decrypt the cipher text
+ *
+ * @param cipherText - cipherText to be decrypted
+ * @return decrypted text
+ */
+ override fun decryptInternal(cipherText: String): String? {
+ return parseCipherText(cipherText)?.let { (iv, encryptedBytes) ->
+ performCryptOperation(
+ mode = Cipher.DECRYPT_MODE,
+ data = encryptedBytes,
+ iv = iv
+ )
+ }?.let { (_, decryptedBytes) ->
+ String(decryptedBytes, StandardCharsets.UTF_8)
+ }
+ }
+
+ /**
+ * This method is used to parse the cipher text (i.e remove the prefix and suffix, extract out the IV and encryptedBytes) and convert it to a byte array
+ *
+ * @param cipherText - cipher text to be parsed
+ * @return AESGCMCryptResult
+ */
+ private fun parseCipherText(cipherText: String): AESGCMCryptResult? {
+ return try {
+ // Remove the prefix and suffix
+ val content = cipherText.removePrefix(AES_GCM_PREFIX).removeSuffix(AES_GCM_SUFFIX)
+
+ // Split IV and encrypted bytes using a delimiter
+ val parts = content.split(":") // Use ":" as a delimiter
+ val iv = parts[0].fromBase64()
+ val encryptedBytes = parts[1].fromBase64()
+ AESGCMCryptResult(iv, encryptedBytes)
+ } catch (e: Exception) {
+ Logger.v("Error parsing cipherText", e)
+ null
+ }
+ }
+
+ /**
+ * This method actually performs both the encryption and decryption crypt task.
+ *
+ * @param mode - mode to determine encryption/decryption
+ * @param data - data to be crypted
+ * @param iv - iv required for decryption
+ * @return AESGCMCryptResult
+ */
+ private fun performCryptOperation(
+ mode: Int,
+ data: ByteArray,
+ iv: ByteArray? = null
+ ): AESGCMCryptResult? {
+ return try {
+ val secretKey = generateOrGetKey()
+ val cipher = Cipher.getInstance("AES/GCM/NoPadding")
+
+ when (mode) {
+ Cipher.ENCRYPT_MODE -> {
+ cipher.init(mode, secretKey)
+ val generatedIv = cipher.iv // Automatically generates 12-byte IV for GCM
+ val encryptedBytes = cipher.doFinal(data)
+ AESGCMCryptResult(generatedIv, encryptedBytes)
+ }
+ Cipher.DECRYPT_MODE -> {
+ if (iv != null) {
+ val gcmParameterSpec =
+ GCMParameterSpec(128, iv) // 128-bit authentication tag length
+ cipher.init(mode, secretKey, gcmParameterSpec)
+ val decryptedBytes = cipher.doFinal(data)
+ AESGCMCryptResult(iv, decryptedBytes)
+ } else {
+ Logger.v("IV is required for decryption")
+ null
+ }
+ }
+ else -> {
+ Logger.v("Invalid mode used")
+ null
+ }
+ }
+ } catch (e: Exception) {
+ Logger.v("Error performing crypt operation", e)
+ null
+ }
+ }
+
+ /**
+ * Generates or retrieves a secret key for encryption/decryption.
+ *
+ * This method uses the Android Keystore system on devices running API 23 (Marshmallow) or higher
+ * to securely store the key. If the Android Keystore is not available (on older API levels),
+ * it falls back to generating a key and storing it in SharedPreferences, encoded in Base64.
+ *
+ * @return The secret key for encryption/decryption, or null if an error occurs during key generation/retrieval.
+ */
+ private fun generateOrGetKey(): SecretKey? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ try {
+ val keyStore = KeyStore.getInstance("AndroidKeyStore")
+ keyStore.load(null)
+
+ if (keyStore.containsAlias(ENCRYPTION_KEY)) {
+ keyStore.getKey(ENCRYPTION_KEY, null) as SecretKey
+ } else {
+ val keyGenerator =
+ KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
+ val keyGenParameterSpec = KeyGenParameterSpec.Builder(
+ ENCRYPTION_KEY,
+ KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
+ )
+ .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
+ .build()
+ keyGenerator.init(keyGenParameterSpec)
+ keyGenerator.generateKey()
+ }
+ } catch (e: Exception) {
+ Logger.v("Error generating or retrieving key", e)
+ null
+ }
+ } else {
+ Logger.v("KeyStore is not supported on API levels below 23")
+
+ val encodedKey = StorageHelper.getString(context, ENCRYPTION_KEY, null)
+ if (encodedKey != null) {
+ // If the key exists, decode it and return as SecretKey
+ val decodedKey = encodedKey.fromBase64()
+ SecretKeySpec(decodedKey, "AES")
+ } else {
+ // If key doesn't exist, generate a new one and store it
+ val keyGenerator = KeyGenerator.getInstance("AES")
+ keyGenerator.init(256) // 256-bit AES key
+ val secretKey = keyGenerator.generateKey()
+
+ // Store the key in SharedPreferences
+ val encodedNewKey = secretKey.encoded.toBase64()
+ StorageHelper.putString(context, ENCRYPTION_KEY, encodedNewKey)
+ secretKey
+ }
+ }
+ }
+
+ private data class AESGCMCryptResult(
+ val iv: ByteArray,
+ val encryptedBytes: ByteArray
+ ) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as AESGCMCryptResult
+
+ if (!iv.contentEquals(other.iv)) return false
+ if (!encryptedBytes.contentEquals(other.encryptedBytes)) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = iv.contentHashCode()
+ result = 31 * result + encryptedBytes.contentHashCode()
+ return result
+ }
+ }
+
+ // Utility extension functions for Base64 encoding/decoding
+ private fun ByteArray.toBase64(): String = Base64.encodeToString(this, Base64.NO_WRAP)
+ private fun String.fromBase64(): ByteArray = Base64.decode(this, Base64.NO_WRAP)
+}
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/Crypt.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/Crypt.kt
index a9035437a..f6e40c692 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/Crypt.kt
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/Crypt.kt
@@ -1,7 +1,6 @@
package com.clevertap.android.sdk.cryption
abstract class Crypt protected constructor() {
- abstract fun encryptInternal(plainText: String, accountID: String): String?
- abstract fun decryptInternal(cipherText: String, accountID: String): String?
- protected abstract fun parseCipherText(cipherText: String): ByteArray?
+ abstract fun encryptInternal(plainText: String): String?
+ abstract fun decryptInternal(cipherText: String): String?
}
\ No newline at end of file
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/CryptFactory.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/CryptFactory.kt
index f339b2eef..70f8c90a6 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/CryptFactory.kt
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/CryptFactory.kt
@@ -1,15 +1,36 @@
package com.clevertap.android.sdk.cryption
+import android.content.Context
+import com.clevertap.android.sdk.cryption.CryptHandler.EncryptionAlgorithm
+
/**
* This class is a factory class to generate a Crypt object based on the EncryptionAlgorithm
*/
-class CryptFactory {
+internal class CryptFactory(
+ private val context: Context,
+ private val accountId: String
+) {
+
+ // Cache to hold instances of Crypt for different encryption algorithms.
+ private val cryptInstances: MutableMap = mutableMapOf()
+
companion object {
@JvmStatic
- fun getCrypt(type: CryptHandler.EncryptionAlgorithm): Crypt {
+ fun getCrypt(type: EncryptionAlgorithm, accountID: String, context: Context): Crypt {
return when (type) {
- CryptHandler.EncryptionAlgorithm.AES -> AESCrypt()
+ EncryptionAlgorithm.AES -> AESCrypt(accountID)
+ EncryptionAlgorithm.AES_GCM -> AESGCMCrypt(context)
}
}
}
+
+ /**
+ * Retrieves or creates a Crypt instance for the specified algorithm.
+ *
+ * @param algorithm - The encryption algorithm to use.
+ * @return The Crypt instance for the specified algorithm.
+ */
+ fun getCryptInstance(algorithm: EncryptionAlgorithm): Crypt {
+ return cryptInstances.getOrPut(algorithm) { getCrypt(algorithm, accountId, context) }
+ }
}
\ No newline at end of file
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/CryptHandler.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/CryptHandler.kt
index 70e0bdb49..928768963 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/CryptHandler.kt
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/CryptHandler.kt
@@ -1,99 +1,157 @@
package com.clevertap.android.sdk.cryption
import com.clevertap.android.sdk.Constants
+import com.clevertap.android.sdk.Constants.AES_GCM_SUFFIX
+import com.clevertap.android.sdk.Constants.AES_GCM_PREFIX
+import com.clevertap.android.sdk.Constants.AES_SUFFIX
+import com.clevertap.android.sdk.Constants.AES_PREFIX
/**
- * This class handles any encryption-decryption related tasks
+ * Handles encryption and decryption for various encryption algorithms and levels.
+ *
+ * @param encryptionLevel - The encryption level to use.
+ * @param accountID - The account ID for which the cryptographic operations are performed.
*/
-class CryptHandler(encryptionLevel: Int, encryptionType: EncryptionAlgorithm, accountID: String) {
- private var encryptionLevel: EncryptionLevel
- private var encryptionType: EncryptionAlgorithm
- private var crypt: Crypt
- private var accountID: String
- var encryptionFlagStatus: Int
+internal class CryptHandler constructor(
+ private val encryptionLevel: EncryptionLevel,
+ private val accountID: String,
+ private val repository: CryptRepository,
+ private val cryptFactory: CryptFactory
+) {
- enum class EncryptionAlgorithm {
- AES
+ /**
+ * Supported encryption algorithms.
+ */
+ enum class EncryptionAlgorithm(val value: Int) {
+ AES(0),
+ AES_GCM(1);
}
- enum class EncryptionLevel(private val value: Int) {
- NONE(0), MEDIUM(1);
+ /**
+ * Encrypts the given plain text using a specific key and the AES_GCM algorithm by default.
+ *
+ * @param plainText - The text to encrypt.
+ * @param key - The key used for encryption.
+ * @return The encrypted text, or the original plain text if encryption is not required.
+ */
+ @JvmOverloads
+ fun encrypt(
+ plainText: String,
+ key: String,
+ algorithm: EncryptionAlgorithm = EncryptionAlgorithm.AES_GCM
+ ): String? {
- fun intValue(): Int {
- return value
+ if (isTextEncrypted(plainText)) {
+ return plainText
}
- }
- init {
- this.encryptionLevel = EncryptionLevel.values()[encryptionLevel]
- this.encryptionType = encryptionType
- this.accountID = accountID
- this.encryptionFlagStatus = 0b00
- this.crypt = CryptFactory.getCrypt(encryptionType)
+ // Use AES_GCM algorithm by default.
+ val crypt = cryptFactory.getCryptInstance(algorithm)
+ when (encryptionLevel) {
+ EncryptionLevel.MEDIUM -> {
+ // Encrypt only if the key is valid
+ if (key in Constants.MEDIUM_CRYPT_KEYS) {
+ return crypt.encryptInternal(plainText)
+ }
+ }
+ else -> {
+ return plainText
+ }
+ }
+ return plainText
}
/**
- * This method returns the encrypted text if the key is a part of the current encryption level and is not already encrypted
- * Returns null if encryptInternal fails
+ * Decrypts the given cipher text using the specified algorithm.
*
- * @param plainText - plainText to be encrypted
- * @param key - key of the plainText to be encrypted
- * @return encrypted text
+ * @param cipherText - The text to decrypt.
+ * @param key - The key used for decryption.
+ * @param algorithm - The encryption algorithm to use (default is AES_GCM).
+ * @return The decrypted text, or the original cipher text if decryption is not required.
*/
- fun encrypt(plainText: String, key: String): String? {
- when (encryptionLevel) {
- EncryptionLevel.MEDIUM ->
- if (key in Constants.MEDIUM_CRYPT_KEYS && !isTextEncrypted(plainText))
- return crypt.encryptInternal(plainText, accountID)
+ @JvmOverloads
+ fun decrypt(
+ cipherText: String,
+ key: String,
+ algorithm: EncryptionAlgorithm = EncryptionAlgorithm.AES_GCM
+ ): String? {
+ if (!isTextEncrypted(cipherText)) {
+ return cipherText
+ }
- else -> return plainText
+ val crypt = cryptFactory.getCryptInstance(algorithm)
+ when (encryptionLevel) {
+ EncryptionLevel.MEDIUM -> {
+ // Decrypt only if the key is valid.
+ if (key in Constants.MEDIUM_CRYPT_KEYS) {
+ return crypt.decryptInternal(cipherText)
+ }
+ }
+ else -> {
+ return crypt.decryptInternal(cipherText)
+ }
}
- return plainText
+ return cipherText
}
- fun encrypt(plainText: String): String? {
- return crypt.encryptInternal(plainText, accountID)
+ /**
+ * Encrypts the given plain text without any checks
+ *
+ * @param plainText - The text to encrypt.
+ * @return The encrypted text, or null if encryption fails.
+ */
+ fun encrypt(
+ plainText: String,
+ algorithm: EncryptionAlgorithm = EncryptionAlgorithm.AES_GCM
+ ): String? {
+ val crypt = cryptFactory.getCryptInstance(algorithm)
+ return crypt.encryptInternal(plainText)
}
/**
- * This method returns the decrypted text if the key is a part of the current encryption level
- * Returns null if decryptInternal fails
+ * Decrypts the given cipher text without any checks.
*
- * @param cipherText - cipherText to be decrypted
- * @param key - key of the cipherText that needs to be decrypted
- * @return decrypted text
+ * @param cipherText - The text to decrypt.
+ * @return The decrypted text, or null if decryption fails.
*/
- fun decrypt(cipherText: String, key: String): String? {
- if (isTextEncrypted(cipherText)) {
- when (encryptionLevel) {
- EncryptionLevel.MEDIUM -> {
- if (key in Constants.MEDIUM_CRYPT_KEYS)
- return crypt.decryptInternal(cipherText, accountID)
- }
-
- else -> {
- return crypt.decryptInternal(cipherText, accountID)
- }
- }
- }
- return cipherText
+ @JvmOverloads
+ fun decrypt(
+ cipherText: String,
+ algorithm: EncryptionAlgorithm = EncryptionAlgorithm.AES_GCM
+ ): String? {
+ val crypt = cryptFactory.getCryptInstance(algorithm)
+ return crypt.decryptInternal(cipherText)
}
- fun decrypt(cipherText: String): String? {
- return crypt.decryptInternal(cipherText, accountID)
+ /**
+ * Updates the encryption state in case of failure while processing new data.
+ *
+ * @param migrationSuccessful - Indicates if migration was successful
+ */
+ fun updateMigrationFailureCount(migrationSuccessful: Boolean) {
+ repository.updateMigrationFailureCount(migrationSuccessful)
}
companion object {
-
/**
- * This method checks if text is already encrypted. Encrypted text is always of the format [.....]
+ * Checks if the given text is encrypted (either using AES or AES_GCM).
*
- * @param plainText - plain text
- * @return boolean indicating if text is encrypted
+ * @param plainText - The text to check.
+ * @return True if the text is encrypted; false otherwise.
*/
@JvmStatic
fun isTextEncrypted(plainText: String): Boolean {
- return plainText.startsWith('[') && plainText.endsWith(']')
+ return isTextAESEncrypted(plainText) || isTextAESGCMEncrypted(plainText)
+ }
+
+ // Determines if the text is AES encrypted.
+ fun isTextAESEncrypted(plainText: String): Boolean {
+ return plainText.startsWith(AES_PREFIX) && plainText.endsWith(AES_SUFFIX)
+ }
+
+ // Determines if the text is AES_GCM encrypted.
+ fun isTextAESGCMEncrypted(plainText: String): Boolean {
+ return plainText.startsWith(AES_GCM_PREFIX) && plainText.endsWith(AES_GCM_SUFFIX)
}
}
-}
\ No newline at end of file
+}
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/CryptMigrator.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/CryptMigrator.kt
new file mode 100644
index 000000000..c97d89aac
--- /dev/null
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/CryptMigrator.kt
@@ -0,0 +1,376 @@
+package com.clevertap.android.sdk.cryption
+
+import com.clevertap.android.sdk.Constants.PREFS_INAPP_KEY_CS
+import com.clevertap.android.sdk.Constants.PREFS_INAPP_KEY_SS
+import com.clevertap.android.sdk.Constants.piiDBKeys
+import com.clevertap.android.sdk.ILogger
+import com.clevertap.android.sdk.cryption.CryptHandler.EncryptionAlgorithm
+import com.clevertap.android.sdk.cryption.EncryptionState.ENCRYPTED_AES
+import com.clevertap.android.sdk.cryption.EncryptionState.ENCRYPTED_AES_GCM
+import com.clevertap.android.sdk.cryption.EncryptionState.PLAIN_TEXT
+import com.clevertap.android.sdk.utils.getStringOrNull
+import org.json.JSONObject
+
+internal data class CryptMigrator(
+ private val logPrefix: String,
+ private val configEncryptionLevel: Int,
+ private val logger: ILogger,
+ private val cryptHandler: CryptHandler,
+ private val cryptRepository: CryptRepository,
+ private val dataMigrationRepository: DataMigrationRepository
+) {
+
+ companion object {
+ const val MIGRATION_FAILURE_COUNT_KEY = "encryptionMigrationFailureCount"
+ const val UNKNOWN_LEVEL = -1
+ const val MIGRATION_NOT_NEEDED = 0
+ const val MIGRATION_NEEDED = 1
+ const val MIGRATION_FIRST_UPGRADE = -1
+ }
+
+ /**
+ * Handles the migration of encryption levels for stored data.
+ *
+ *
+ * - Scenarios handled:
+ * 1. **Fresh Install**: If the stored encryption level is unknown and the configured level is
+ * `EncryptionLevel.NONE`, no migration is needed.
+ * 2. **Encryption Level Upgrade**: If the stored encryption level differs from the configured
+ * encryption level and migration failures are not recorded, migration is required.
+ * 3. **Existing Failures**: If there are recorded migration failures, the process attempts to
+ * continue from where it left off.
+ *
+ * - Updates:
+ * - Updates the stored encryption level to the current configuration.
+ * - Updates the migration failure count based on the success or failure of the migration.
+ *
+ */
+ fun migrateEncryption() {
+ val storedEncryptionLevel = cryptRepository.storedEncryptionLevel()
+
+ val storedFailureCount = cryptRepository.migrationFailureCount()
+
+ val migrationFailureCount = when {
+ // Encryption level changed and upgrade to v2 already complete
+ storedEncryptionLevel != configEncryptionLevel && storedFailureCount != -1 -> MIGRATION_NEEDED
+ else -> storedFailureCount
+ }
+
+ cryptRepository.updateEncryptionLevel(configEncryptionLevel)
+
+ if (migrationFailureCount == MIGRATION_NOT_NEEDED) {
+ logger.verbose(
+ logPrefix,
+ "Migration not required: config-encryption-level $configEncryptionLevel, " +
+ "stored-encryption-level $storedEncryptionLevel"
+ )
+ return
+ }
+
+ logger.verbose(
+ logPrefix,
+ "Starting migration from encryption level $storedEncryptionLevel to $configEncryptionLevel " +
+ "with migrationFailureCount $migrationFailureCount"
+ )
+ val migrationSuccess = handleAllMigrations(
+ configEncryptionLevel == EncryptionLevel.MEDIUM.intValue(),
+ migrationFailureCount == -1
+ )
+ cryptRepository.updateMigrationFailureCount(migrationSuccess)
+ }
+
+ private fun handleAllMigrations(encrypt: Boolean, firstUpgrade: Boolean): Boolean {
+ val cgkMigrationSuccess = migrateCachedGuidsKeyPref(encrypt, firstUpgrade)
+ val dbMigrationSuccess = migrateDBProfile(encrypt)
+ val inAppMigrationSuccess = migrateInAppData()
+ return cgkMigrationSuccess && dbMigrationSuccess && inAppMigrationSuccess
+ }
+
+ /**
+ * This method migrates the encryption level of the value under cachedGUIDsKey stored in the shared preference file
+ *
+ * If decryption from AES to plain-text fails, this data is deleted
+ * @param encrypt - Flag to indicate the task to be either encryption or decryption
+ * @param firstUpgrade - Flag to indicate whether this is the first upgrade to v2
+ * Returns true if migration was successful and false otherwise
+ */
+ private fun migrateCachedGuidsKeyPref(
+ encrypt: Boolean,
+ firstUpgrade: Boolean,
+ ): Boolean {
+ logger.verbose(
+ logPrefix,
+ "Migrating encryption level for cachedGUIDsKey prefs"
+ )
+ val cgkString: String = if (firstUpgrade) {
+ // translate from old format to new format, in new format we encrypt entire string
+ val cgkJson = migrateFormatForCachedGuidsKeyPref()
+ val cgkLength = cgkJson.length()
+ dataMigrationRepository.saveCachedGuidJsonLength(cgkLength)
+ if (cgkLength == 0) {
+ dataMigrationRepository.removeCachedGuidJson()
+ return true
+ }
+ cgkJson.toString()
+ } else {
+ dataMigrationRepository.cachedGuidString() ?: return true
+ }
+
+ val migrationResult = performMigrationStep(encrypt, cgkString)
+ dataMigrationRepository.saveCachedGuidJson(migrationResult.data)
+ logger.verbose(
+ logPrefix,
+ "Cached GUIDs migrated with success = $migrationResult.migrationSuccessful = ${migrationResult.data}"
+ )
+ return migrationResult.migrationSuccessful
+ }
+
+
+ /**
+ * This method migrates converts the older format of cgk to an all plain text JSONObject
+ * If decryption for any key-value fails that key is dropped forever
+ *
+ * The older format when encryption level is 1 was {Email_[]:__g... , Name_[]:__i...} -> Only the identifier was encrypted
+ * The migrated format will encrypt the entire JSONObject and not just the identifier
+ *
+ * @return JSONObject with all plain text
+ */
+ private fun migrateFormatForCachedGuidsKeyPref(): JSONObject {
+ val cachedGuidJsonObj = dataMigrationRepository.cachedGuidJsonObject()
+ val migratedGuidJsonObj = JSONObject()
+ try {
+ val keysIterator = cachedGuidJsonObj.keys()
+ while (keysIterator.hasNext()) {
+ val nextKey = keysIterator.next()
+ val (key, identifier) = nextKey.split("_", limit = 2)
+ val migrationResult = performMigrationStep(false, identifier)
+ if (migrationResult.migrationSuccessful) {
+ val cryptedKey = "${key}_${migrationResult.data}"
+ migratedGuidJsonObj.put(cryptedKey, cachedGuidJsonObj[nextKey])
+ }
+ }
+ } catch (t: Throwable) {
+ logger.verbose(
+ logPrefix,
+ "Error migrating format for cached GUIDs: Clearing and starting fresh $t"
+ )
+ }
+ return migratedGuidJsonObj
+ }
+
+ /**
+ * This method migrates the encryption level of the user profiles stored in the local db
+ * Only pii data such as name, phone, email and identity are encrypted from the user profile, remaining are kept as is
+ *
+ * If decryption from AES to plain-text fails, this data point is deleted
+ * @param encrypt - Flag to indicate the task to be either encryption or decryption
+ * Returns true if migration was successful and false otherwise
+ */
+ private fun migrateDBProfile(
+ encrypt: Boolean
+ ): Boolean {
+ logger.verbose(
+ logPrefix,
+ "Migrating encryption level for user profiles in DB"
+ )
+
+ val profiles = dataMigrationRepository.userProfilesInAccount()
+
+ var migrationSuccessful = true
+ for ((deviceID, profile) in profiles) {
+ try {
+ piiDBKeys.forEach { piiKey ->
+ profile.getStringOrNull(piiKey)?.let { value ->
+ val migrationResult =
+ performMigrationStep(encrypt, value)
+ migrationSuccessful =
+ migrationSuccessful && migrationResult.migrationSuccessful
+ profile.put(piiKey, migrationResult.data)
+ }
+ }
+ logger.verbose(
+ logPrefix,
+ "DB migrated with success = $migrationSuccessful = $profile"
+ )
+ if (dataMigrationRepository.saveUserProfile(deviceID, profile) <= -1L) {
+ migrationSuccessful = false
+ }
+ } catch (e: Exception) {
+ logger.verbose(logPrefix, "Error migrating profile $deviceID: $e")
+ migrationSuccessful = false
+ }
+ }
+
+ return migrationSuccessful
+ }
+
+
+ /**
+ * This method migrates the encryption level of the inapp data stored in the shared preferences file.
+ * Migration(if needed) is always performed to AES_GCM
+ *
+ * If decryption from AES to plain-text fails, this data point is deleted
+ * Returns true if migration was successful and false otherwise
+ */
+ private fun migrateInAppData(): Boolean {
+ logger.verbose(logPrefix, "Migrating encryption for InAppData")
+ var migrationSuccessful = true
+
+ val migrateCode: (String) -> String? = { spData: String ->
+ val result = performMigrationStep(true, spData)
+ migrationSuccessful = migrationSuccessful && result.migrationSuccessful
+ result.data
+ }
+ val keysToProcess = listOf(PREFS_INAPP_KEY_CS, PREFS_INAPP_KEY_SS)
+
+ dataMigrationRepository.inAppDataFiles(keysToProcess, migrateCode)
+
+ return migrationSuccessful
+ }
+
+ private fun performMigrationStep(
+ encrypt: Boolean,
+ data: String
+ ): MigrationResult {
+ val currentState = getCurrentEncryptionState(data)
+ val targetState = getFinalEncryptionState(encrypt)
+
+ return transitionEncryptionState(currentState, targetState, data)
+ }
+
+ private fun transitionEncryptionState(
+ currentState: EncryptionState,
+ targetState: EncryptionState,
+ data: String,
+ ): MigrationResult {
+
+ if (currentState == targetState) {
+ // No migration needed if current state matches the final state
+ return MigrationResult(data = data, migrationSuccessful = true)
+ }
+
+ return when (currentState) {
+ ENCRYPTED_AES -> handleEncryptedAesTransition(targetState, data)
+ ENCRYPTED_AES_GCM -> handleEncryptedAesGcmTransition(targetState, data)
+ PLAIN_TEXT -> handlePlainTextTransition(targetState, data)
+ }
+ }
+
+ /**
+ * Handles the transition of data encrypted from AES to the specified target state.
+ *
+ * This function decrypts the input data using AES encryption and then transitions it to the
+ * target encryption state:
+ * - **AES_GCM**: Re-encrypts the decrypted data using AES-GCM encryption.
+ * - **Plain Text**: Returns the decrypted data
+ *
+ * This function returns (null, true) when decryption from AES state fails. This indicates that the data must be deleted
+ *
+ * @param targetState The target encryption state to which the data should transition.
+ * @param data The encrypted input data as a string.
+ *
+ * @return A `MigrationResult` containing the transformed data and whether the operation succeeded.
+ */
+ private fun handleEncryptedAesTransition(
+ targetState: EncryptionState,
+ data: String,
+ ): MigrationResult {
+ val decrypted = cryptHandler.decrypt(data, EncryptionAlgorithm.AES)
+ return when (targetState) {
+ ENCRYPTED_AES_GCM -> {
+ val encrypted = decrypted?.let {
+ cryptHandler.encrypt(
+ it,
+ EncryptionAlgorithm.AES_GCM
+ )
+ }
+ MigrationResult(encrypted ?: decrypted, encrypted != null || decrypted == null)
+ }
+
+ PLAIN_TEXT -> {
+ MigrationResult(decrypted ?: data, decrypted != null)
+ }
+ else -> {
+ logger.verbose(logPrefix, "Invalid transition from ENCRYPTED_AES to $targetState")
+ MigrationResult.failure(data)
+ }
+ }
+ }
+
+ /**
+ * Handles the transition of data encrypted from AES-GCM to the specified target state.
+ * Target State should will be PLAIN_TEXT
+ *
+ * This function decrypts the input data using AES-GCM algorithm
+ *
+ * @param targetState The target encryption state to which the data should transition.
+ * @param data The encrypted input data as a string.
+ *
+ * @return A `MigrationResult` containing the transformed data and whether the operation succeeded.
+ */
+ private fun handleEncryptedAesGcmTransition(
+ targetState: EncryptionState,
+ data: String,
+ ): MigrationResult {
+ val decrypted = cryptHandler.decrypt(
+ data,
+ EncryptionAlgorithm.AES_GCM
+ )
+ return when (targetState) {
+ PLAIN_TEXT -> {
+ MigrationResult(decrypted ?: data, decrypted != null)
+ }
+ else -> {
+ logger.verbose(logPrefix, "Invalid transition from ENCRYPTED_AES_GCM to $targetState")
+ MigrationResult.failure(data)
+ }
+ }
+ }
+
+ /**
+ * Handles the transition of plain text data to the specified target state.
+ *
+ * This function transitions the input plain text data to the desired encryption state:
+ * - **AES_GCM**: Encrypts the data using AES-GCM encryption.
+ *
+ * @param targetState The target encryption state to which the data should transition.
+ * @param data The input plain text data as a string.
+ *
+ * @return A `MigrationResult` containing the transformed data and whether the operation succeeded.
+ */
+ private fun handlePlainTextTransition(
+ targetState: EncryptionState,
+ data: String,
+ ): MigrationResult {
+ return when (targetState) {
+ ENCRYPTED_AES_GCM -> {
+ val encrypted = cryptHandler.encrypt(
+ data,
+ EncryptionAlgorithm.AES_GCM
+ )
+ MigrationResult(encrypted ?: data, encrypted != null)
+ }
+ else -> {
+ logger.verbose(logPrefix, "Invalid transition from PLAIN_TEXT to $targetState")
+ MigrationResult.failure(data)
+ }
+ }
+ }
+
+
+ private fun getFinalEncryptionState(encrypt: Boolean): EncryptionState {
+ return if (encrypt) {
+ ENCRYPTED_AES_GCM
+ } else {
+ PLAIN_TEXT
+ }
+ }
+
+ private fun getCurrentEncryptionState(data: String): EncryptionState {
+ return when {
+ CryptHandler.isTextAESEncrypted(data) -> ENCRYPTED_AES
+ CryptHandler.isTextAESGCMEncrypted(data) -> ENCRYPTED_AES_GCM
+ else -> PLAIN_TEXT
+ }
+ }
+}
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/CryptRepository.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/CryptRepository.kt
new file mode 100644
index 000000000..8d297c233
--- /dev/null
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/CryptRepository.kt
@@ -0,0 +1,64 @@
+package com.clevertap.android.sdk.cryption
+
+import android.content.Context
+import com.clevertap.android.sdk.Constants.KEY_ENCRYPTION_LEVEL
+import com.clevertap.android.sdk.Logger
+import com.clevertap.android.sdk.StorageHelper
+import com.clevertap.android.sdk.cryption.CryptMigrator.Companion.MIGRATION_FAILURE_COUNT_KEY
+import com.clevertap.android.sdk.cryption.CryptMigrator.Companion.MIGRATION_FIRST_UPGRADE
+import com.clevertap.android.sdk.cryption.CryptMigrator.Companion.UNKNOWN_LEVEL
+
+interface ICryptRepository {
+ fun storedEncryptionLevel(): Int
+ fun migrationFailureCount(): Int
+ fun updateEncryptionLevel(configEncryptionLevel: Int)
+ fun updateMigrationFailureCount(migrationSuccessful: Boolean)
+}
+
+class CryptRepository(
+ val context: Context,
+ val accountId: String
+) : ICryptRepository {
+ private var migrationFailureCount: Int = 0
+
+ override fun storedEncryptionLevel() =
+ StorageHelper.getInt(
+ context,
+ StorageHelper.storageKeyWithSuffix(accountId, KEY_ENCRYPTION_LEVEL),
+ UNKNOWN_LEVEL
+ )
+
+ override fun migrationFailureCount() = StorageHelper.getInt(
+ context,
+ StorageHelper.storageKeyWithSuffix(accountId, MIGRATION_FAILURE_COUNT_KEY),
+ MIGRATION_FIRST_UPGRADE
+ )
+
+ override fun updateEncryptionLevel(configEncryptionLevel: Int) {
+ StorageHelper.putInt(
+ context,
+ StorageHelper.storageKeyWithSuffix(accountId, KEY_ENCRYPTION_LEVEL),
+ configEncryptionLevel
+ )
+ }
+
+ override fun updateMigrationFailureCount(migrationSuccessful: Boolean) {
+ migrationFailureCount = if (migrationSuccessful) {
+ 0
+ } else {
+ migrationFailureCount + 1
+ }
+
+ Logger.v(
+ accountId,
+ "Updating migrationFailureCount to $migrationFailureCount"
+ )
+
+ StorageHelper.putInt(
+ context,
+ StorageHelper.storageKeyWithSuffix(accountId, CryptMigrator.MIGRATION_FAILURE_COUNT_KEY),
+ migrationFailureCount
+ )
+ }
+
+}
\ No newline at end of file
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/CryptUtils.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/CryptUtils.kt
deleted file mode 100644
index f7f578286..000000000
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/CryptUtils.kt
+++ /dev/null
@@ -1,288 +0,0 @@
-package com.clevertap.android.sdk.cryption
-
-import android.content.Context
-import com.clevertap.android.sdk.CleverTapInstanceConfig
-import com.clevertap.android.sdk.Constants.CACHED_GUIDS_KEY
-import com.clevertap.android.sdk.Constants.ENCRYPTION_FLAG_ALL_SUCCESS
-import com.clevertap.android.sdk.Constants.ENCRYPTION_FLAG_CGK_SUCCESS
-import com.clevertap.android.sdk.Constants.ENCRYPTION_FLAG_DB_SUCCESS
-import com.clevertap.android.sdk.Constants.ENCRYPTION_FLAG_FAIL
-import com.clevertap.android.sdk.Constants.KEY_ENCRYPTION_FLAG_STATUS
-import com.clevertap.android.sdk.Constants.KEY_ENCRYPTION_LEVEL
-import com.clevertap.android.sdk.Constants.KEY_ENCRYPTION_MIGRATION
-import com.clevertap.android.sdk.Constants.piiDBKeys
-import com.clevertap.android.sdk.StorageHelper
-import com.clevertap.android.sdk.db.DBAdapter
-import com.clevertap.android.sdk.utils.CTJsonConverter
-import org.json.JSONObject
-
-/**
- * This class is a utils class, mainly used to handle migration when encryption fails or encryption level is changed
- */
-internal object CryptUtils {
-
- /**
- * This method migrates the encryption level of the stored data for the current account ID
- *
- * @param context - The Android context
- * @param config - The [CleverTapInstanceConfig] object
- * @param cryptHandler - The [CryptHandler] object
- * @param dbAdapter - The [DBAdapter] object
- */
- @JvmStatic
- fun migrateEncryptionLevel(
- context: Context,
- config: CleverTapInstanceConfig,
- cryptHandler: CryptHandler,
- dbAdapter: DBAdapter
- ) {
-
- val encryptionFlagStatus: Int
- val configEncryptionLevel = config.encryptionLevel
- val storedEncryptionLevel = StorageHelper.getInt(
- context,
- StorageHelper.storageKeyWithSuffix(config, KEY_ENCRYPTION_LEVEL),
- -1
- )
-
- // Nothing to migrate if a new app install and configEncryption level is 0, hence return
- // If encryption level is updated (0 to 1 or 1 to 0) then set status to all migrations failed (0)
- encryptionFlagStatus = if (storedEncryptionLevel == -1 && configEncryptionLevel == 0)
- return
- else if (storedEncryptionLevel != configEncryptionLevel) {
- ENCRYPTION_FLAG_FAIL
- } else {
- StorageHelper.getInt(
- context,
- StorageHelper.storageKeyWithSuffix(config, KEY_ENCRYPTION_FLAG_STATUS),
- ENCRYPTION_FLAG_FAIL
- )
- }
- StorageHelper.putInt(
- context,
- StorageHelper.storageKeyWithSuffix(config, KEY_ENCRYPTION_LEVEL),
- configEncryptionLevel
- )
-
- if (encryptionFlagStatus == ENCRYPTION_FLAG_ALL_SUCCESS) {
- config.logger.verbose(
- config.accountId,
- "Encryption flag status is 100% success, no need to migrate"
- )
- cryptHandler.encryptionFlagStatus = ENCRYPTION_FLAG_ALL_SUCCESS
- return
- }
-
- config.logger.verbose(
- config.accountId,
- "Migrating encryption level from $storedEncryptionLevel to $configEncryptionLevel with current flag status $encryptionFlagStatus"
- )
-
- // If configEncryptionLevel is one then encrypt otherwise decrypt
- migrateEncryption(
- configEncryptionLevel == 1,
- context,
- config,
- cryptHandler,
- encryptionFlagStatus,
- dbAdapter
- )
- }
-
- /**
- * This method migrates the encryption level. There are currently 2 migrations required.
- * The migration strategy is such that even if one entry fails in one of the 2 migrations, flag bit for that migration is set to 0
- * and reattempted during the next instance creation
- *
- * @param encrypt - Flag to indicate the task to be either encryption or decryption
- * @param config - The [CleverTapInstanceConfig] object
- * @param context - The Android context
- * @param cryptHandler - The [CryptHandler] object
- * @param encryptionFlagStatus - Current value of the flag
- * @param dbAdapter - The [dbAdapter] object
- */
- private fun migrateEncryption(
- encrypt: Boolean,
- context: Context,
- config: CleverTapInstanceConfig,
- cryptHandler: CryptHandler,
- encryptionFlagStatus: Int,
- dbAdapter: DBAdapter
- ) {
- // And operation checks if the required bit is set or not
- var cgkFlag = encryptionFlagStatus and ENCRYPTION_FLAG_CGK_SUCCESS
- if (cgkFlag == ENCRYPTION_FLAG_FAIL)
- cgkFlag = migrateCachedGuidsKeyPref(encrypt, config, context, cryptHandler)
-
- var dbFlag = encryptionFlagStatus and ENCRYPTION_FLAG_DB_SUCCESS
- if (dbFlag == ENCRYPTION_FLAG_FAIL)
- dbFlag = migrateDBProfile(encrypt, config, cryptHandler, dbAdapter)
-
- val updatedFlagStatus = cgkFlag or dbFlag
-
- config.logger.verbose(
- config.accountId,
- "Updating encryption flag status to $updatedFlagStatus"
- )
- StorageHelper.putInt(
- context,
- StorageHelper.storageKeyWithSuffix(config, KEY_ENCRYPTION_FLAG_STATUS),
- updatedFlagStatus
- )
- cryptHandler.encryptionFlagStatus = updatedFlagStatus
- }
-
- /**
- * This method migrates the encryption level of the value under cachedGUIDsKey stored in the shared preference file
- * Only the value of the identifier(eg: johndoe@gmail.com) is encrypted/decrypted for this key throughout the sdk
- *
- * @param encrypt - Flag to indicate the task to be either encryption or decryption
- * @param config - The [CleverTapInstanceConfig] object
- * @param context - The Android context
- * @param cryptHandler - The [CryptHandler] object
- * Returns the status of cgk migration
- */
- private fun migrateCachedGuidsKeyPref(
- encrypt: Boolean,
- config: CleverTapInstanceConfig,
- context: Context,
- cryptHandler: CryptHandler
- ): Int {
- config.logger.verbose(
- config.accountId,
- "Migrating encryption level for cachedGUIDsKey prefs"
- )
- val json =
- StorageHelper.getStringFromPrefs(context, config, CACHED_GUIDS_KEY, null)
- val cachedGuidJsonObj = CTJsonConverter.toJsonObject(json, config.logger, config.accountId)
- val newGuidJsonObj = JSONObject()
- var migrationStatus = ENCRYPTION_FLAG_CGK_SUCCESS
- try {
- val i = cachedGuidJsonObj.keys()
- while (i.hasNext()) {
- val nextJSONObjKey = i.next()
- val key = nextJSONObjKey.substringBefore("_")
- val identifier = nextJSONObjKey.substringAfter("_")
- var crypted: String? =
- if (encrypt)
- cryptHandler.encrypt(identifier, key)
- else
- cryptHandler.decrypt(identifier, KEY_ENCRYPTION_MIGRATION)
- if (crypted == null) {
- config.logger.verbose(
- config.accountId,
- "Error migrating $identifier in Cached Guid Key Pref"
- )
- crypted = identifier
- migrationStatus = ENCRYPTION_FLAG_FAIL
- }
- val cryptedKey = "${key}_$crypted"
- newGuidJsonObj.put(cryptedKey, cachedGuidJsonObj[nextJSONObjKey])
- }
- if (cachedGuidJsonObj.length() > 0) {
- val cachedGuid = newGuidJsonObj.toString()
- StorageHelper.putString(
- context,
- StorageHelper.storageKeyWithSuffix(config, CACHED_GUIDS_KEY),
- cachedGuid
- )
- config.logger.verbose(
- config.accountId,
- "setCachedGUIDs after migration:[$cachedGuid]"
- )
- }
- } catch (t: Throwable) {
- config.logger.verbose(config.accountId, "Error migrating cached guids: $t")
- migrationStatus = ENCRYPTION_FLAG_FAIL
- }
- return migrationStatus
- }
-
-
- /**
- * This method migrates the encryption level of the user profiles stored in the local db
- * Only pii data such as name, phone, email and identity are encrypted from the user profile, remaining are kept as is
- *
- * @param encrypt - Flag to indicate the task to be either encryption or decryption
- * @param config - The [CleverTapInstanceConfig] object
- * @param cryptHandler - The [CryptHandler] object
- * @param dbAdapter - The [dbAdapter] object
- * Returns the status of db migration
- */
- private fun migrateDBProfile(
- encrypt: Boolean,
- config: CleverTapInstanceConfig,
- cryptHandler: CryptHandler,
- dbAdapter: DBAdapter
- ): Int {
- config.logger.verbose(config.accountId, "Migrating encryption level for user profiles in DB")
- val profiles = dbAdapter.fetchUserProfilesByAccountId(config.accountId)
-
- var migrationStatus = ENCRYPTION_FLAG_DB_SUCCESS
- for(profileIterator in profiles) {
- val profile = profileIterator.value
- try {
- for (piiKey in piiDBKeys) {
- if (profile.has(piiKey)) {
- val value = profile[piiKey]
- if (value is String) {
- var crypted = if (encrypt)
- cryptHandler.encrypt(value, piiKey)
- else
- cryptHandler.decrypt(value, KEY_ENCRYPTION_MIGRATION)
- if (crypted == null) {
- config.logger.verbose(
- config.accountId,
- "Error migrating $piiKey entry in db profile"
- )
- crypted = value
- migrationStatus = ENCRYPTION_FLAG_FAIL
- }
- profile.put(piiKey, crypted)
- }
- }
- }
- if (dbAdapter.storeUserProfile(config.accountId, profileIterator.key, profile) <= -1L)
- migrationStatus = ENCRYPTION_FLAG_FAIL
- } catch (e: Exception) {
- config.logger.verbose(config.accountId, "Error migrating local DB profile for $profileIterator.key: $e")
- migrationStatus = ENCRYPTION_FLAG_FAIL
- }
- }
- return migrationStatus
- }
-
- /**
- * This method updates the encryptionFlagStatus if encryption fails when new data is added
- *
- * @param context - Context object
- * @param config - The [CleverTapInstanceConfig] object
- * @param failedFlag - Indicates which encryption has failed
- * @param cryptHandler - The [CryptHandler] object
- */
- @JvmStatic
- fun updateEncryptionFlagOnFailure(
- context: Context,
- config: CleverTapInstanceConfig,
- failedFlag: Int,
- cryptHandler: CryptHandler
- ) {
-
- // This operation sets the bit for the required encryption fail to 0
- val updatedEncryptionFlag =
- (failedFlag xor cryptHandler.encryptionFlagStatus) and cryptHandler.encryptionFlagStatus
- config.logger.verbose(
- config.accountId,
- "Updating encryption flag status after error in $failedFlag to $updatedEncryptionFlag"
- )
- StorageHelper.putInt(
- context, StorageHelper.storageKeyWithSuffix(
- config,
- KEY_ENCRYPTION_FLAG_STATUS
- ),
- updatedEncryptionFlag
- )
- cryptHandler.encryptionFlagStatus = updatedEncryptionFlag
-
- }
-}
\ No newline at end of file
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/DataMigrationRepository.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/DataMigrationRepository.kt
new file mode 100644
index 000000000..81662b48b
--- /dev/null
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/DataMigrationRepository.kt
@@ -0,0 +1,91 @@
+package com.clevertap.android.sdk.cryption
+
+import android.content.Context
+import com.clevertap.android.sdk.CleverTapInstanceConfig
+import com.clevertap.android.sdk.Constants
+import com.clevertap.android.sdk.Constants.CACHED_GUIDS_KEY
+import com.clevertap.android.sdk.Constants.INAPP_KEY
+import com.clevertap.android.sdk.StorageHelper
+import com.clevertap.android.sdk.db.DBAdapter
+import com.clevertap.android.sdk.utils.CTJsonConverter
+import org.json.JSONObject
+import java.io.File
+
+interface IDataMigrationRepository {
+ fun cachedGuidJsonObject(): JSONObject
+ fun cachedGuidString(): String?
+ fun saveCachedGuidJson(json: String?)
+ fun removeCachedGuidJson()
+ fun saveCachedGuidJsonLength(length: Int)
+ fun userProfilesInAccount(): Map
+ fun saveUserProfile(deviceID: String, profile: JSONObject): Long
+ fun inAppDataFiles(keysToMigrate: List, migrate: (String) -> String?)
+}
+
+internal class DataMigrationRepository(
+ private val context: Context,
+ private val config: CleverTapInstanceConfig,
+ private val dbAdapter: DBAdapter
+) : IDataMigrationRepository {
+
+ override fun cachedGuidString(): String? {
+ return StorageHelper.getStringFromPrefs(context, config, CACHED_GUIDS_KEY, null)
+ }
+ override fun cachedGuidJsonObject(): JSONObject {
+ val json = cachedGuidString()
+ val cachedGuidJsonObj = CTJsonConverter.toJsonObject(json, config.logger, config.accountId)
+ return cachedGuidJsonObj
+ }
+
+ override fun saveCachedGuidJson(json: String?) {
+ StorageHelper.putString(
+ context,
+ StorageHelper.storageKeyWithSuffix(config.accountId, CACHED_GUIDS_KEY),
+ json
+ )
+ }
+
+ override fun removeCachedGuidJson() {
+ StorageHelper.remove(
+ context,
+ StorageHelper.storageKeyWithSuffix(config.accountId, CACHED_GUIDS_KEY),
+ )
+ }
+
+ override fun saveCachedGuidJsonLength(length: Int) {
+ StorageHelper.putInt(
+ context,
+ StorageHelper.storageKeyWithSuffix(config.accountId, Constants.CACHED_GUIDS_LENGTH_KEY),
+ length
+ )
+ }
+
+ override fun userProfilesInAccount(): Map {
+ return dbAdapter.fetchUserProfilesByAccountId(config.accountId)
+ }
+
+ override fun saveUserProfile(deviceID: String, profile: JSONObject): Long {
+ return dbAdapter.storeUserProfile(config.accountId, deviceID, profile)
+ }
+
+ override fun inAppDataFiles(
+ keysToMigrate: List,
+ migrate: (String) -> String?
+ ) {
+ File(context.applicationInfo.dataDir, "shared_prefs")
+ .listFiles { _, name ->
+ // Check StoreProvider.constructStorePreferenceName() to check how the name is constructed
+ name.startsWith(INAPP_KEY) && name.endsWith("${config.accountId}.xml")
+ }?.map { file ->
+ val prefName = file.nameWithoutExtension
+ context.getSharedPreferences(prefName, Context.MODE_PRIVATE)
+ }?.forEach { sp ->
+ keysToMigrate.forEach { key ->
+ sp.getString(key, null)?.let { data ->
+ val encryptedData = migrate(data)
+ sp.edit().putString(key, encryptedData).apply()
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/EncryptionLevel.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/EncryptionLevel.kt
new file mode 100644
index 000000000..c9950c149
--- /dev/null
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/EncryptionLevel.kt
@@ -0,0 +1,18 @@
+package com.clevertap.android.sdk.cryption
+
+/**
+ * Encryption levels indicating the degree of security.
+ */
+enum class EncryptionLevel(private val value: Int) {
+ NONE(0), // No encryption
+ MEDIUM(1); // Medium level encryption
+
+ fun intValue(): Int = value
+
+ companion object {
+ @JvmStatic
+ fun fromInt(value: Int): EncryptionLevel {
+ return EncryptionLevel.values().firstOrNull { it.value == value } ?: NONE
+ }
+ }
+}
\ No newline at end of file
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/EncryptionState.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/EncryptionState.kt
new file mode 100644
index 000000000..04394c389
--- /dev/null
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/EncryptionState.kt
@@ -0,0 +1,10 @@
+package com.clevertap.android.sdk.cryption
+
+/**
+ * Enum representing encryption states
+ */
+internal enum class EncryptionState {
+ ENCRYPTED_AES,
+ ENCRYPTED_AES_GCM,
+ PLAIN_TEXT;
+}
\ No newline at end of file
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/MigrationResult.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/MigrationResult.kt
new file mode 100644
index 000000000..d4640d307
--- /dev/null
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/cryption/MigrationResult.kt
@@ -0,0 +1,10 @@
+package com.clevertap.android.sdk.cryption
+
+internal data class MigrationResult(
+ val data: String?,
+ val migrationSuccessful: Boolean
+) {
+ companion object {
+ fun failure(data: String?) = MigrationResult(data, false)
+ }
+}
\ No newline at end of file
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/db/CtDatabase.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/db/CtDatabase.kt
index 6486c6a4f..764c1579f 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/db/CtDatabase.kt
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/db/CtDatabase.kt
@@ -28,7 +28,7 @@ class DatabaseHelper internal constructor(val context: Context, val config: Clev
companion object {
- private const val DATABASE_VERSION = 4
+ private const val DATABASE_VERSION = 5
private const val DB_LIMIT = 20 * 1024 * 1024 //20mb
}
@@ -41,6 +41,7 @@ class DatabaseHelper internal constructor(val context: Context, val config: Clev
override fun onCreate(db: SQLiteDatabase) {
logger.verbose("Creating CleverTap DB")
executeStatement(db, CREATE_EVENTS_TABLE)
+ executeStatement(db, CREATE_USER_EVENT_LOGS_TABLE)
executeStatement(db, CREATE_PROFILE_EVENTS_TABLE)
executeStatement(db, CREATE_USER_PROFILES_TABLE)
executeStatement(db, CREATE_INBOX_MESSAGES_TABLE)
@@ -87,6 +88,10 @@ class DatabaseHelper internal constructor(val context: Context, val config: Clev
migrateUserProfilesTable(db)
}
}
+
+ if (oldVersion < 5) {
+ executeStatement(db, CREATE_USER_EVENT_LOGS_TABLE)// when app updates [1,2,3,4] to 5
+ }
}
private fun getDeviceIdForAccountIdFromPrefs(accountId: String): String {
@@ -190,14 +195,15 @@ class DatabaseHelper internal constructor(val context: Context, val config: Clev
}
}
-enum class Table(val tableName: String) {
+ enum class Table(val tableName: String) {
EVENTS("events"),
PROFILE_EVENTS("profileEvents"),
USER_PROFILES("userProfiles"),
INBOX_MESSAGES("inboxMessages"),
PUSH_NOTIFICATIONS("pushNotifications"),
UNINSTALL_TS("uninstallTimestamp"),
- PUSH_NOTIFICATION_VIEWED("notificationViewed")
+ PUSH_NOTIFICATION_VIEWED("notificationViewed"),
+ USER_EVENT_LOGS_TABLE("userEventLogs")
}
object Column {
@@ -212,6 +218,11 @@ object Column {
const val CAMPAIGN = "campaignId"
const val WZRKPARAMS = "wzrkParams"
const val DEVICE_ID = "deviceID"
+ const val EVENT_NAME = "eventName"
+ const val NORMALIZED_EVENT_NAME = "normalizedEventName"
+ const val FIRST_TS = "firstTs"
+ const val LAST_TS = "lastTs"
+ const val COUNT = "count"
}
private val CREATE_EVENTS_TABLE = """
@@ -222,6 +233,18 @@ private val CREATE_EVENTS_TABLE = """
);
"""
+private val CREATE_USER_EVENT_LOGS_TABLE = """
+ CREATE TABLE ${Table.USER_EVENT_LOGS_TABLE.tableName} (
+ ${Column.DEVICE_ID} STRING NOT NULL,
+ ${Column.EVENT_NAME} STRING NOT NULL,
+ ${Column.NORMALIZED_EVENT_NAME} STRING NOT NULL,
+ ${Column.FIRST_TS} INTEGER NOT NULL,
+ ${Column.LAST_TS} INTEGER NOT NULL,
+ ${Column.COUNT} INTEGER NOT NULL,
+ PRIMARY KEY (${Column.DEVICE_ID}, ${Column.NORMALIZED_EVENT_NAME})
+ );
+"""
+
private val CREATE_PROFILE_EVENTS_TABLE = """
CREATE TABLE ${PROFILE_EVENTS.tableName} (
${Column.ID} INTEGER PRIMARY KEY AUTOINCREMENT,
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/db/DBAdapter.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/db/DBAdapter.kt
index f32848731..136f3f211 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/db/DBAdapter.kt
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/db/DBAdapter.kt
@@ -11,8 +11,11 @@ import com.clevertap.android.sdk.Constants
import com.clevertap.android.sdk.db.Table.INBOX_MESSAGES
import com.clevertap.android.sdk.db.Table.PUSH_NOTIFICATIONS
import com.clevertap.android.sdk.db.Table.UNINSTALL_TS
+import com.clevertap.android.sdk.db.Table.USER_EVENT_LOGS_TABLE
import com.clevertap.android.sdk.db.Table.USER_PROFILES
import com.clevertap.android.sdk.inbox.CTMessageDAO
+import com.clevertap.android.sdk.usereventlogs.UserEventLogDAO
+import com.clevertap.android.sdk.usereventlogs.UserEventLogDAOImpl
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
@@ -26,19 +29,21 @@ internal class DBAdapter(context: Context, config: CleverTapInstanceConfig) {
//Notification Inbox Messages Table fields
- private const val DB_UPDATE_ERROR = -1L
+ internal const val DB_UPDATE_ERROR = -1L
- private const val DB_OUT_OF_MEMORY_ERROR = -2L
+ internal const val DB_OUT_OF_MEMORY_ERROR = -2L
@Suppress("unused")
private const val DB_UNDEFINED_CODE = -3L
private const val DATABASE_NAME = "clevertap"
- private const val NOT_ENOUGH_SPACE_LOG =
+ internal const val NOT_ENOUGH_SPACE_LOG =
"There is not enough space left on the device to store data, data discarded"
}
+ @Volatile
+ private var userEventLogDao: UserEventLogDAO? = null
private val logger = config.logger
private val dbHelper: DatabaseHelper = DatabaseHelper(context, config, getDatabaseName(config), logger)
@@ -618,6 +623,23 @@ internal class DBAdapter(context: Context, config: CleverTapInstanceConfig) {
}
}
+ /**
+ * -----------------------------
+ * -----------DAO---------------
+ * -----------------------------
+ */
+ @WorkerThread
+ fun userEventLogDAO(): UserEventLogDAO {
+ return userEventLogDao ?: synchronized(this) {
+ userEventLogDao ?: UserEventLogDAOImpl(
+ dbHelper,
+ logger,
+ USER_EVENT_LOGS_TABLE
+ ).also { userEventLogDao = it }
+
+ }
+ }
+
@WorkerThread
private fun belowMemThreshold(): Boolean {
return dbHelper.belowMemThreshold()
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/db/DBManager.kt b/clevertap-core/src/main/java/com/clevertap/android/sdk/db/DBManager.kt
index d600fc26d..eef4e3ecf 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/db/DBManager.kt
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/db/DBManager.kt
@@ -17,6 +17,11 @@ internal class DBManager(
private val ctLockManager: CTLockManager
) : BaseDatabaseManager {
+ private companion object {
+ private const val USER_EVENT_LOG_ROWS_PER_USER = 2_048 + 256 // events + profile props
+ private const val USER_EVENT_LOG_ROWS_THRESHOLD = 5 * USER_EVENT_LOG_ROWS_PER_USER
+ }
+
private var dbAdapter: DBAdapter? = null
@WorkerThread
@@ -30,6 +35,8 @@ internal class DBManager(
dbAdapter.cleanupStaleEvents(PROFILE_EVENTS)
dbAdapter.cleanupStaleEvents(PUSH_NOTIFICATION_VIEWED)
dbAdapter.cleanUpPushNotifications()
+ dbAdapter.userEventLogDAO()
+ .cleanUpExtraEvents(USER_EVENT_LOG_ROWS_THRESHOLD, USER_EVENT_LOG_ROWS_PER_USER)
}
return dbAdapter
}
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/events/EventQueueManager.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/events/EventQueueManager.java
index fb0ec4107..20d6c809c 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;
@@ -18,7 +20,6 @@
import com.clevertap.android.sdk.Logger;
import com.clevertap.android.sdk.SessionManager;
import com.clevertap.android.sdk.Utils;
-import com.clevertap.android.sdk.cryption.CryptHandler;
import com.clevertap.android.sdk.db.BaseDatabaseManager;
import com.clevertap.android.sdk.login.IdentityRepo;
import com.clevertap.android.sdk.login.IdentityRepoFactory;
@@ -29,14 +30,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 {
@@ -60,8 +63,6 @@ public class EventQueueManager extends BaseEventQueueManager implements FailureF
private final Logger logger;
- private LoginInfoProvider loginInfoProvider;
-
private final MainLooperHandler mainLooperHandler;
private final NetworkManager networkManager;
@@ -73,7 +74,8 @@ public class EventQueueManager extends BaseEventQueueManager implements FailureF
private Runnable pushNotificationViewedRunnable = null;
private final ControllerManager controllerManager;
- private final CryptHandler cryptHandler;
+
+ private final LoginInfoProvider loginInfoProvider;
public EventQueueManager(final BaseDatabaseManager baseDatabaseManager,
Context context,
@@ -89,7 +91,7 @@ public EventQueueManager(final BaseDatabaseManager baseDatabaseManager,
CTLockManager ctLockManager,
final LocalDataStore localDataStore,
ControllerManager controllerManager,
- CryptHandler cryptHandler) {
+ LoginInfoProvider loginInfoProvider) {
this.baseDatabaseManager = baseDatabaseManager;
this.context = context;
this.config = config;
@@ -104,7 +106,7 @@ public EventQueueManager(final BaseDatabaseManager baseDatabaseManager,
cleverTapMetaData = coreMetaData;
this.ctLockManager = ctLockManager;
this.controllerManager = controllerManager;
- this.cryptHandler = cryptHandler;
+ this.loginInfoProvider = loginInfoProvider;
callbackManager.setFailureFlushListener(this);
}
@@ -115,7 +117,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 {
@@ -243,14 +245,6 @@ public void sendImmediately(Context context, EventGroup eventGroup, JSONObject e
}
}
- public LoginInfoProvider getLoginInfoProvider() {
- return loginInfoProvider;
- }
-
- public void setLoginInfoProvider(final LoginInfoProvider loginInfoProvider) {
- this.loginInfoProvider = loginInfoProvider;
- }
-
public int getNow() {
return (int) (System.currentTimeMillis() / 1000);
}
@@ -310,16 +304,43 @@ public void processEvent(final Context context, final JSONObject event, final in
}
localDataStore.setDataSyncFlag(event);
baseDatabaseManager.queueEventToDB(context, event, eventType);
- updateLocalStore(context, event, eventType);
- scheduleQueueFlush(context);
+ initInAppEvaluation(context, event, eventType);
+
+ scheduleQueueFlush(context);
} catch (Throwable e) {
config.getLogger().verbose(config.getAccountId(), "Failed to queue event: " + event.toString(), e);
}
}
}
- public void processPushNotificationViewedEvent(final Context context, final JSONObject event) {
+ public void initInAppEvaluation(Context context, JSONObject event, int eventType) {
+ String eventName = eventMediator.getEventName(event);
+ Location userLocation = cleverTapMetaData.getLocationFromUser();
+ updateLocalStore(eventName, eventType);
+
+ if (eventMediator.isChargedEvent(event)) {
+ controllerManager.getInAppController()
+ .onQueueChargedEvent(eventMediator.getChargedEventDetails(event),
+ eventMediator.getChargedEventItemDetails(event), userLocation);
+ } else if (!NetworkManager.isNetworkOnline(context) && eventMediator.isEvent(event)) {
+ // in case device is offline just evaluate all events
+ controllerManager.getInAppController().onQueueEvent(eventName,
+ eventMediator.getEventProperties(event), userLocation);
+ } else if (eventType == Constants.PROFILE_EVENT) {
+ // in case profile event, evaluate for user attribute changes
+ Map> userAttributeChangedProperties
+ = eventMediator.computeUserAttributeChangeProperties(event);
+ controllerManager.getInAppController()
+ .onQueueProfileEvent(userAttributeChangedProperties, userLocation);
+ } else if (!eventMediator.isAppLaunchedEvent(event) && eventMediator.isEvent(event)) {
+ // in case device is online only evaluate non-appLaunched events
+ controllerManager.getInAppController().onQueueEvent(eventName,
+ eventMediator.getEventProperties(event), userLocation);
+ }
+ }
+
+ public void processPushNotificationViewedEvent(final Context context, final JSONObject event, final int eventType) {
synchronized (ctLockManager.getEventLock()) {
try {
int session = cleverTapMetaData.getCurrentSessionId();
@@ -333,6 +354,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);
@@ -355,8 +377,7 @@ public void pushBasicProfile(JSONObject baseProfile, boolean removeFromSharedPre
if (baseProfile != null && baseProfile.length() > 0) {
Iterator i = baseProfile.keys();
IdentityRepo iProfileHandler = IdentityRepoFactory
- .getRepo(context, config, deviceInfo, validationResultStack);
- setLoginInfoProvider(new LoginInfoProvider(context, config, deviceInfo, cryptHandler));
+ .getRepo(context, config, validationResultStack);
while (i.hasNext()) {
String next = i.next();
@@ -380,17 +401,18 @@ public void pushBasicProfile(JSONObject baseProfile, boolean removeFromSharedPre
/*If key is present in IdentitySet and removeFromSharedPrefs is true then
proceed to removing PII key(Email) from shared prefs*/
- if (isProfileKey && removeFromSharedPrefs){
- try{
- getLoginInfoProvider().removeValueFromCachedGUIDForIdentifier(guid,next);
- } catch (Throwable t){
- //no op
- }
- }else if (isProfileKey) {
+
+ if (isProfileKey && !deviceInfo.isErrorDeviceId()) {
try {
- getLoginInfoProvider().cacheGUIDForIdentifier(guid, next, value.toString());
+ if (removeFromSharedPrefs) {
+ // Remove the value associated with the GUID
+ loginInfoProvider.removeValueFromCachedGUIDForIdentifier(guid, next);
+ } else {
+ // Cache the new value for the GUID
+ loginInfoProvider.cacheGUIDForIdentifier(guid, next, value.toString());
+ }
} catch (Throwable t) {
- // no-op
+ // Log or handle the exception if needed; currently no-op
}
}
}
@@ -455,50 +477,24 @@ public Future> queueEvent(final Context context, final JSONObject event, final
@Override
@WorkerThread
public Void call() {
-
- Location userLocation = cleverTapMetaData.getLocationFromUser();
-
- if (eventMediator.isChargedEvent(event)) {
- controllerManager.getInAppController()
- .onQueueChargedEvent(eventMediator.getChargedEventDetails(event),
- eventMediator.getChargedEventItemDetails(event), userLocation);
- } else if (!NetworkManager.isNetworkOnline(context) && eventMediator.isEvent(event)) {
- // in case device is offline just evaluate all events
- controllerManager.getInAppController().onQueueEvent(eventMediator.getEventName(event),
- eventMediator.getEventProperties(event), userLocation);
- } else if (eventType == Constants.PROFILE_EVENT) {
- // in case profile event, evaluate for user attribute changes
- Map> userAttributeChangedProperties
- = eventMediator.computeUserAttributeChangeProperties(event);
- controllerManager.getInAppController()
- .onQueueProfileEvent(userAttributeChangedProperties, userLocation);
- } else if (!eventMediator.isAppLaunchedEvent(event) && eventMediator.isEvent(event)) {
- // in case device is online only evaluate non-appLaunched events
- controllerManager.getInAppController().onQueueEvent(eventMediator.getEventName(event),
- eventMediator.getEventProperties(event), userLocation);
- }
-
if (eventMediator.shouldDropEvent(event, eventType)) {
return null;
}
if (eventMediator.shouldDeferProcessingEvent(event, eventType)) {
config.getLogger().debug(config.getAccountId(),
"App Launched not yet processed, re-queuing event " + event + "after 2s");
- mainLooperHandler.postDelayed(new Runnable() {
- @Override
- public void run() {
- Task task = CTExecutorFactory.executors(config).postAsyncSafelyTask();
- task.execute("queueEventWithDelay", new Callable() {
- @Override
- @WorkerThread
- public Void call() {
- sessionManager.lazyCreateSession(context);
- pushInitialEventsAsync();
- addToQueue(context, event, eventType);
- return null;
- }
- });
- }
+ mainLooperHandler.postDelayed(() -> {
+ Task task1 = CTExecutorFactory.executors(config).postAsyncSafelyTask();
+ task1.execute("queueEventWithDelay", new Callable() {
+ @Override
+ @WorkerThread
+ public Void call() {
+ sessionManager.lazyCreateSession(context);
+ pushInitialEventsAsync();
+ addToQueue(context, event, eventType);
+ return null;
+ }
+ });
}, 2000);
} else {
if (eventType == Constants.FETCH_EVENT || eventType == Constants.NV_EVENT) {
@@ -586,11 +582,10 @@ public void run() {
mainLooperHandler.post(pushNotificationViewedRunnable);
}
- //Util
- // only call async
- private void updateLocalStore(final Context context, final JSONObject event, final int type) {
+ @WorkerThread
+ private void updateLocalStore(final String eventName, final int type) {
if (type == Constants.RAISED_EVENT) {
- localDataStore.persistEvent(context, event, type);
+ localDataStore.persistUserEventLog(eventName);
}
}
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFragment.java
index d11599b8b..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 actionParts = formData.getString(Constants.KEY_C2A);
- String callToAction = null;
- if (actionParts != null) {
- final String[] parts = actionParts.split("__dl__");
- if (parts.length == 2) {
- // Decode it here as wzrk_c2a is not decoded by UriHelper
- callToAction = URLDecoder.decode(parts[0], "UTF-8");
- 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/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFullHtmlFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFullHtmlFragment.java
index 1379f2d39..f3903228a 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFullHtmlFragment.java
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppBaseFullHtmlFragment.java
@@ -36,7 +36,7 @@ public boolean shouldOverrideUrlLoading(WebView view, String url) {
}
}
- protected CTInAppWebView webView;
+ CTInAppWebView webView;
@Override
public void onAttach(@NonNull Context context) {
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlCoverFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlCoverFragment.java
index 627f3703c..d4106e864 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlCoverFragment.java
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlCoverFragment.java
@@ -1,7 +1,17 @@
package com.clevertap.android.sdk.inapp;
+import static com.clevertap.android.sdk.CTXtensions.applyInsetsWithMarginAdjustment;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
import android.widget.RelativeLayout;
import android.widget.RelativeLayout.LayoutParams;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
import com.clevertap.android.sdk.Constants;
public class CTInAppHtmlCoverFragment extends CTInAppBaseFullHtmlFragment {
@@ -19,4 +29,20 @@ protected RelativeLayout.LayoutParams getLayoutParamsForCloseButton() {
closeIvLp.setMargins(0, sub, sub, 0);
return closeIvLp;
}
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
+ View inAppView = super.onCreateView(inflater, container, savedInstanceState);
+ if (inAppView != null) {
+ applyInsetsWithMarginAdjustment(inAppView, (insets, mlp) -> {
+ mlp.leftMargin = insets.left;
+ mlp.rightMargin = insets.right;
+ mlp.topMargin = insets.top;
+ mlp.bottomMargin = insets.bottom;
+ return null;
+ });
+ }
+ return inAppView;
+ }
}
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlFooterFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlFooterFragment.java
index 38c981942..6e6485967 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlFooterFragment.java
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlFooterFragment.java
@@ -1,8 +1,11 @@
package com.clevertap.android.sdk.inapp;
+import static com.clevertap.android.sdk.CTXtensions.applyInsetsWithMarginAdjustment;
+
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+
import com.clevertap.android.sdk.R;
public class CTInAppHtmlFooterFragment extends CTInAppBasePartialHtmlFragment {
@@ -14,6 +17,13 @@ ViewGroup getLayout(View view) {
@Override
View getView(LayoutInflater inflater, ViewGroup container) {
- return inflater.inflate(R.layout.inapp_html_footer, container, false);
+ View inAppView = inflater.inflate(R.layout.inapp_html_footer, container, false);
+ applyInsetsWithMarginAdjustment(inAppView, (insets, mlp) -> {
+ mlp.leftMargin = insets.left;
+ mlp.rightMargin = insets.right;
+ mlp.bottomMargin = insets.bottom;
+ return null;
+ });
+ return inAppView;
}
}
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlHalfInterstitialFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlHalfInterstitialFragment.java
index 60dc6609d..7e57acb32 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlHalfInterstitialFragment.java
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlHalfInterstitialFragment.java
@@ -1,5 +1,29 @@
package com.clevertap.android.sdk.inapp;
-public class CTInAppHtmlHalfInterstitialFragment extends CTInAppBaseFullHtmlFragment {
+import static com.clevertap.android.sdk.CTXtensions.applyInsetsWithMarginAdjustment;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+public class CTInAppHtmlHalfInterstitialFragment extends CTInAppBaseFullHtmlFragment {
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
+ View inAppView = super.onCreateView(inflater, container, savedInstanceState);
+ if (inAppView != null) {
+ applyInsetsWithMarginAdjustment(inAppView, (insets, mlp) -> {
+ mlp.leftMargin = insets.left;
+ mlp.rightMargin = insets.right;
+ mlp.topMargin = insets.top;
+ mlp.bottomMargin = insets.bottom;
+ return null;
+ });
+ }
+ return inAppView;
+ }
}
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlHeaderFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlHeaderFragment.java
index b04d0ec54..a92f07d42 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlHeaderFragment.java
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlHeaderFragment.java
@@ -1,8 +1,11 @@
package com.clevertap.android.sdk.inapp;
+import static com.clevertap.android.sdk.CTXtensions.applyInsetsWithMarginAdjustment;
+
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+
import com.clevertap.android.sdk.R;
public class CTInAppHtmlHeaderFragment extends CTInAppBasePartialHtmlFragment {
@@ -14,7 +17,13 @@ ViewGroup getLayout(View view) {
@Override
View getView(LayoutInflater inflater, ViewGroup container) {
- return inflater.inflate(R.layout.inapp_html_header, container, false);
+ View inAppView = inflater.inflate(R.layout.inapp_html_header, container, false);
+ applyInsetsWithMarginAdjustment(inAppView, (insets, mlp) -> {
+ mlp.leftMargin = insets.left;
+ mlp.rightMargin = insets.right;
+ mlp.topMargin = insets.top;
+ return null;
+ });
+ return inAppView;
}
-
}
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlInterstitialFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlInterstitialFragment.java
index 4c2906ada..52136c645 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlInterstitialFragment.java
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppHtmlInterstitialFragment.java
@@ -1,5 +1,29 @@
package com.clevertap.android.sdk.inapp;
-public class CTInAppHtmlInterstitialFragment extends CTInAppBaseFullHtmlFragment {
+import static com.clevertap.android.sdk.CTXtensions.applyInsetsWithMarginAdjustment;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+public class CTInAppHtmlInterstitialFragment extends CTInAppBaseFullHtmlFragment {
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
+ View inAppView = super.onCreateView(inflater, container, savedInstanceState);
+ if (inAppView != null) {
+ applyInsetsWithMarginAdjustment(inAppView, (insets, mlp) -> {
+ mlp.leftMargin = insets.left;
+ mlp.rightMargin = insets.right;
+ mlp.topMargin = insets.top;
+ mlp.bottomMargin = insets.bottom;
+ return null;
+ });
+ }
+ return inAppView;
+ }
}
diff --git a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeCoverFragment.java b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeCoverFragment.java
index 5c1327c4b..78bc07ee1 100644
--- a/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeCoverFragment.java
+++ b/clevertap-core/src/main/java/com/clevertap/android/sdk/inapp/CTInAppNativeCoverFragment.java
@@ -1,5 +1,7 @@
package com.clevertap.android.sdk.inapp;
+import static com.clevertap.android.sdk.CTXtensions.applyInsetsWithMarginAdjustment;
+
import android.annotation.SuppressLint;
import android.content.res.Configuration;
import android.graphics.Bitmap;
@@ -14,9 +16,12 @@
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
+
import androidx.annotation.Nullable;
+
import com.clevertap.android.sdk.R;
import com.clevertap.android.sdk.customviews.CloseImageView;
+
import java.util.ArrayList;
public class CTInAppNativeCoverFragment extends CTInAppBaseFullNativeFragment {
@@ -27,6 +32,13 @@ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
ArrayList