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