Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## CHANGE LOG.

### June 2026
* [CleverTap Android SDK v8.3.0](docs/CTCORECHANGELOG.md).

### May 20, 2026
* [CleverTap Android SDK v8.2.0](docs/CTCORECHANGELOG.md).

Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@ We publish the SDK to `mavenCentral` as an `AAR` file. Just declare it as depend

```groovy
dependencies {
implementation "com.clevertap.android:clevertap-android-sdk:8.2.0"
implementation "com.clevertap.android:clevertap-android-sdk:8.3.0"
}
```

Alternatively, you can download and add the AAR file included in this repo in your Module libs directory and tell gradle to install it like this:

```groovy
dependencies {
implementation (name: "clevertap-android-sdk-8.2.0", ext: 'aar')
implementation (name: "clevertap-android-sdk-8.3.0", ext: 'aar')
}
```

Expand All @@ -46,7 +46,7 @@ Add the Firebase Messaging library and Android Support Library v4 as dependencie

```groovy
dependencies {
implementation "com.clevertap.android:clevertap-android-sdk:8.2.0"
implementation "com.clevertap.android:clevertap-android-sdk:8.3.0"
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).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import androidx.annotation.WorkerThread;

import com.clevertap.android.sdk.displayunits.DisplayUnitCache;
import com.clevertap.android.sdk.displayunits.model.CleverTapDisplayUnit;
import com.clevertap.android.sdk.events.BaseEventQueueManager;
import com.clevertap.android.sdk.events.FlattenedEventData;
Expand Down Expand Up @@ -205,9 +206,9 @@ public void pushDisplayUnitClickedEventForID(String unitID) {
event.put("evtName", Constants.NOTIFICATION_CLICKED_EVENT_NAME);

//wzrk fields
if (controllerManager.getCTDisplayUnitController() != null) {
CleverTapDisplayUnit displayUnit = controllerManager.getCTDisplayUnitController()
.getDisplayUnitForID(unitID);
DisplayUnitCache cache = controllerManager.getDisplayUnitCache();
if (cache != null) {
CleverTapDisplayUnit displayUnit = cache.getDisplayUnitForID(unitID);
if (displayUnit != null) {
JSONObject eventExtraData = displayUnit.getWZRKFields();
if (eventExtraData != null) {
Expand All @@ -229,6 +230,115 @@ public void pushDisplayUnitClickedEventForID(String unitID) {
}
}

/**
* Raises a Native Display element click event.
*
* Element-level analog of {@link #pushDisplayUnitClickedEventForID(String)} — for
* Native Display units that host multiple interactive child elements (buttons,
* images, etc.), this method records which child element was clicked alongside
* the existing wzrk_* campaign attribution.
*
* Caller's additionalProperties (which should include wzrk_element_id from the
* action metadata injected by the BE) are merged verbatim first; the cached
* unit's wzrk_* fields are then layered on top.
*/
@Override
public void pushDisplayUnitElementClickedEventForID(
String unitID,
HashMap<String, Object> additionalProperties) {
JSONObject event = new JSONObject();
try {
event.put("evtName", Constants.NOTIFICATION_CLICKED_EVENT_NAME);

DisplayUnitCache cache = controllerManager.getDisplayUnitCache();
if (cache == null) {
config.getLogger().verbose(config.getAccountId(),
Constants.FEATURE_DISPLAY_UNIT + "Element click dropped — no display-unit cache installed");
return;
}
CleverTapDisplayUnit displayUnit = cache.getDisplayUnitForID(unitID);
if (displayUnit == null) {
config.getLogger().verbose(config.getAccountId(),
Constants.FEATURE_DISPLAY_UNIT + "Element click dropped — no unit found for id: " + unitID);
return;
}

JSONObject eventExtraData = new JSONObject();
mergeAdditionalProperties(eventExtraData, additionalProperties);
JSONObject cachedWzrkFields = displayUnit.getWZRKFields();
if (cachedWzrkFields != null) {
Iterator<String> it = cachedWzrkFields.keys();
while (it.hasNext()) {
String k = it.next();
try {
eventExtraData.put(k, cachedWzrkFields.get(k));
} catch (JSONException ignored) {
}
}
}

event.put("evtData", eventExtraData);
try {
coreMetaData.setWzrkParams(filterWzrkFields(eventExtraData));
} catch (Throwable t) {
// no-op
}
baseEventQueueManager.queueEvent(context, event, Constants.RAISED_EVENT,
Comment thread
CTLalit marked this conversation as resolved.
getFlattenedEventProperties(eventExtraData));
} catch (Throwable t) {
config.getLogger().verbose(config.getAccountId(),
Constants.FEATURE_DISPLAY_UNIT
+ "Failed to push Display Unit element clicked event" + t);
}
}

/**
* Merge caller-supplied {@code additionalProperties} verbatim into the click
* event's {@code evtData}. The {@code wzrk_*} namespace is enforced by the
* caller of this helper — by layering the cached unit's {@code wzrk_*}
* fields on top after this merge, so server-controlled attribution wins
* over any same-named caller key.
*/
private void mergeAdditionalProperties(JSONObject eventData,
HashMap<String, Object> extras) {
if (extras == null || extras.isEmpty()) {
return;
}
for (Map.Entry<String, Object> entry : extras.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if (key == null || key.isEmpty() || value == null) {
continue;
}
try {
eventData.put(key, value);
} catch (JSONException ignored) {
// skip unserialisable entries
}
}
}

/**
* Project only the {@code wzrk_*} keys back out of the merged event data so
* {@link CoreMetaData#setWzrkParams(JSONObject)} retains its existing
* server-namespace contract (it feeds {@code wzrk_ref} batch headers; caller-
* supplied non-wzrk extras must not ride along on unrelated subsequent events).
*/
private JSONObject filterWzrkFields(JSONObject merged) {
JSONObject out = new JSONObject();
Iterator<String> it = merged.keys();
while (it.hasNext()) {
String k = it.next();
if (k.startsWith(Constants.WZRK_PREFIX)) {
try {
out.put(k, merged.get(k));
} catch (JSONException ignored) {
}
}
}
return out;
}

@Override
public void pushDisplayUnitViewedEventForID(String unitID) {
JSONObject event = new JSONObject();
Expand All @@ -237,9 +347,9 @@ public void pushDisplayUnitViewedEventForID(String unitID) {
event.put("evtName", Constants.NOTIFICATION_VIEWED_EVENT_NAME);

//wzrk fields
if (controllerManager.getCTDisplayUnitController() != null) {
CleverTapDisplayUnit displayUnit = controllerManager.getCTDisplayUnitController()
.getDisplayUnitForID(unitID);
DisplayUnitCache cache = controllerManager.getDisplayUnitCache();
if (cache != null) {
CleverTapDisplayUnit displayUnit = cache.getDisplayUnitForID(unitID);
if (displayUnit != null) {
JSONObject eventExtras = displayUnit.getWZRKFields();
if (eventExtras != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import android.os.Bundle;
import com.clevertap.android.sdk.inapp.CTInAppNotification;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import org.json.JSONObject;

Expand All @@ -25,6 +26,9 @@ public abstract class BaseAnalyticsManager {

public abstract void pushDisplayUnitClickedEventForID(String unitID);

public abstract void pushDisplayUnitElementClickedEventForID(
String unitID, HashMap<String, Object> additionalProperties);

public abstract void pushDisplayUnitViewedEventForID(String unitID);

@SuppressWarnings({"unused"})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import androidx.annotation.RestrictTo.Scope;
import androidx.annotation.WorkerThread;
import com.clevertap.android.sdk.cryption.ICryptHandler;
import com.clevertap.android.sdk.displayunits.DisplayUnitCache;
import com.clevertap.android.sdk.displayunits.DisplayUnitListener;
import com.clevertap.android.sdk.displayunits.model.CleverTapDisplayUnit;
import com.clevertap.android.sdk.events.EventDetail;
Expand Down Expand Up @@ -1472,14 +1473,13 @@ public String getAccountId() {
*/
@Nullable
public ArrayList<CleverTapDisplayUnit> getAllDisplayUnits() {

if (coreState.getControllerManager().getCTDisplayUnitController() != null) {
return coreState.getControllerManager().getCTDisplayUnitController().getAllDisplayUnits();
} else {
getConfigLogger()
.verbose(getAccountId(), Constants.FEATURE_DISPLAY_UNIT + "Failed to get all Display Units");
return null;
DisplayUnitCache cache = coreState.getControllerManager().getDisplayUnitCache();
if (cache != null) {
return cache.getAllDisplayUnits();
}
getConfigLogger()
.verbose(getAccountId(), Constants.FEATURE_DISPLAY_UNIT + "Failed to get all Display Units");
return null;
}

/**
Expand Down Expand Up @@ -1792,13 +1792,13 @@ public void onComplete(@NonNull com.google.android.gms.tasks.Task<String> task)
*/
@Nullable
public CleverTapDisplayUnit getDisplayUnitForId(String unitID) {
if (coreState.getControllerManager().getCTDisplayUnitController() != null) {
return coreState.getControllerManager().getCTDisplayUnitController().getDisplayUnitForID(unitID);
} else {
getConfigLogger().verbose(getAccountId(),
Constants.FEATURE_DISPLAY_UNIT + "Failed to get Display Unit for id: " + unitID);
return null;
DisplayUnitCache cache = coreState.getControllerManager().getDisplayUnitCache();
if (cache != null) {
return cache.getDisplayUnitForID(unitID);
}
getConfigLogger().verbose(getAccountId(),
Constants.FEATURE_DISPLAY_UNIT + "Failed to get Display Unit for id: " + unitID);
return null;
}

/**
Expand Down Expand Up @@ -2495,6 +2495,29 @@ public void pushDeepLink(Uri uri) {
coreState.getAnalyticsManager().pushDeepLink(uri, false);
}

/**
* Replaces the SDK's display-unit cache with the supplied implementation.
* Pass {@code null} to clear the reference (subsequent server responses
* will lazily install a fresh default {@link
* com.clevertap.android.sdk.displayunits.CTDisplayUnitController}).
*
* <p>The new instance receives subsequent {@code updateDisplayUnits}
* calls (e.g. from server responses) and serves all lookup sites:
* {@link #getDisplayUnitForId(String)}, {@link #getAllDisplayUnits()},
* {@link #pushDisplayUnitViewedEventForID(String)}, and
* {@link #pushDisplayUnitClickedEventForID(String)}.
*
* <p>Implementations must be thread-safe. The display-unit listener
* registered via {@link #setDisplayUnitListener} fires only for
* server-pipeline activity — replacing the cache or mutating its
* contents from outside the SDK does not synthesise a listener fire.
*
* @since 8.3.0
*/
public void setDisplayUnitCache(@Nullable DisplayUnitCache cache) {
coreState.getControllerManager().setDisplayUnitCache(cache);
}

/**
* Raises the Display Unit Clicked event
*
Expand All @@ -2505,6 +2528,36 @@ public void pushDisplayUnitClickedEventForID(String unitID) {
coreState.getAnalyticsManager().pushDisplayUnitClickedEventForID(unitID);
}

/**
* Raises a Native Display element click event for the given unit + element.
*
* Element-level analog of {@link #pushDisplayUnitClickedEventForID(String)} — for
* Native Display units that host multiple interactive child elements (buttons,
* images, etc.), this method records which child element was clicked alongside
* the existing wzrk_* campaign attribution.
*
* <p>{@code evtData} is assembled in two layers (later layers win on key collision):
* <ol>
* <li>Caller's {@code additionalProperties}, merged verbatim — should include
* {@code wzrk_element_id} and other {@code wzrk_*} attribution fields injected
* by the BE into the action's {@code metadata} object.</li>
* <li>Cached unit's {@code wzrk_*} fields layered on top — so server-controlled
* attribution always wins over same-named caller-supplied keys.</li>
* </ol>
*
* @param unitID the unitID of the Display Unit
* ({@link CleverTapDisplayUnit#getUnitID()})
* @param additionalProperties per-click context including {@code wzrk_element_id}
* and other {@code wzrk_*} fields from BE action metadata
*/
@SuppressWarnings("unused")
public void pushDisplayUnitElementClickedEventForID(
String unitID,
HashMap<String, Object> additionalProperties) {
coreState.getAnalyticsManager().pushDisplayUnitElementClickedEventForID(
unitID, additionalProperties);
}

/**
* Raises the Display Unit Viewed event
*
Expand Down
Loading
Loading