Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release core v7.0.3 SDK-4163 #704

Merged
merged 11 commits into from
Nov 29, 2024
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## CHANGE LOG.

### November 29, 2024
* [CleverTap Android SDK v7.0.3](docs/CTCORECHANGELOG.md)

### October 10, 2024
* [CleverTap Android SDK v7.0.2](docs/CTCORECHANGELOG.md)

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:7.0.2"
implementation "com.clevertap.android:clevertap-android-sdk:7.0.3"
}
```

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-7.0.2", ext: 'aar')
implementation (name: "clevertap-android-sdk-7.0.3", 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:7.0.2"
implementation "com.clevertap.android:clevertap-android-sdk:7.0.3"
implementation "androidx.core:core:1.9.0"
implementation "com.google.firebase:firebase-messaging:23.0.6"
implementation "com.google.android.gms:play-services-ads:22.3.0" // Required only if you enable Google ADID collection in the SDK (turned off by default).
Expand Down
35 changes: 13 additions & 22 deletions clevertap-core/src/main/assets/image_interstitial.html

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,8 @@ public Void call() {
JSONObject inappPreviewPayload = new JSONObject(inappPreviewString);

JSONArray inappNotifs = new JSONArray();
if (Constants.INAPP_IMAGE_INTERSTITIAL_TYPE.equals(inappPreviewPayloadType)) {
if (Constants.INAPP_IMAGE_INTERSTITIAL_TYPE.equals(inappPreviewPayloadType)
|| Constants.INAPP_ADVANCED_BUILDER_TYPE.equals(inappPreviewPayloadType)) {
inappNotifs.put(getHalfInterstitialInApp(inappPreviewPayload));
} else {
inappNotifs.put(inappPreviewPayload);
Expand Down Expand Up @@ -623,7 +624,7 @@ public String wrapImageInterstitialContent(String content) {
if (html != null && content != null) {
String[] parts = html.split(Constants.INAPP_HTML_SPLIT);
if (parts.length == 2) {
return String.format("%s'%s'%s", parts[0], content, parts[1]);
return parts[0] + content + parts[1];
}
}
} catch (IOException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ public interface Constants {
String INAPP_PREVIEW_PUSH_PAYLOAD_KEY = "wzrk_inapp";
String INAPP_PREVIEW_PUSH_PAYLOAD_TYPE_KEY = "wzrk_inapp_type";
String INAPP_IMAGE_INTERSTITIAL_TYPE = "image-interstitial";
String INAPP_ADVANCED_BUILDER_TYPE = "advanced-builder";
String INAPP_IMAGE_INTERSTITIAL_CONFIG = "imageInterstitialConfig";
String INAPP_HTML_SPLIT = "\"##Vars##\"";
String INAPP_IMAGE_INTERSTITIAL_HTML_NAME = "image_interstitial.html";
Expand Down Expand Up @@ -446,4 +447,5 @@ public interface Constants {

String FLUSH_PUSH_IMPRESSIONS_ONE_TIME_WORKER_NAME = "CTFlushPushImpressionsOneTime";

String URL_PARAM_DL_SEPARATOR = "__dl__";
}
31 changes: 31 additions & 0 deletions clevertap-core/src/main/java/com/clevertap/android/sdk/Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<String> collection, String key) {
if (collection == null || key == null) {
return false;
Expand Down Expand Up @@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -966,8 +966,11 @@ public void onQueueEvent(final String eventName, Map<String, Object> eventProper
}

@WorkerThread
public void onQueueChargedEvent(Map<String, Object> chargeDetails,
List<Map<String, Object>> items, Location userLocation) {
public void onQueueChargedEvent(
Map<String, Object> chargeDetails,
List<Map<String, Object>> items,
Location userLocation
) {
final Map<String, Object> appFieldsWithChargedEventProperties = JsonUtil.mapFromJson(
deviceInfo.getAppLaunchedFields());
appFieldsWithChargedEventProperties.putAll(chargeDetails);
Expand All @@ -979,8 +982,10 @@ public void onQueueChargedEvent(Map<String, Object> chargeDetails,
}

@WorkerThread
public void onQueueProfileEvent(final Map<String, Map<String, Object>> userAttributeChangedProperties,
Location location) {
public void onQueueProfileEvent(
final Map<String, Map<String, Object>> userAttributeChangedProperties,
Location location
) {
final Map<String, Object> appFields = JsonUtil.mapFromJson(
deviceInfo.getAppLaunchedFields());
final JSONArray clientSideInAppsToDisplay = evaluationManager.evaluateOnUserAttributeChange(
Expand All @@ -990,9 +995,10 @@ public void onQueueProfileEvent(final Map<String, Map<String, Object>> userAttri
}
}

public void onAppLaunchServerSideInAppsResponse(@NonNull JSONArray appLaunchServerSideInApps,
Location userLocation)
throws JSONException {
public void onAppLaunchServerSideInAppsResponse(
@NonNull JSONArray appLaunchServerSideInApps,
Location userLocation
) throws JSONException {
final Map<String, Object> appLaunchedProperties = JsonUtil.mapFromJson(
deviceInfo.getAppLaunchedFields());
List<JSONObject> appLaunchSsInAppList = Utils.toJSONObjectList(appLaunchServerSideInApps);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import com.clevertap.android.sdk.Constants.CLTAP_PROP_VARIANT
import com.clevertap.android.sdk.Constants.CLTAP_SDK_VERSION
import com.clevertap.android.sdk.Constants.INAPP_WZRK_PIVOT
import com.clevertap.android.sdk.Constants.NOTIFICATION_ID_TAG
import com.clevertap.android.sdk.Utils

/**
* Represents an event and its associated properties.
Expand All @@ -33,7 +34,7 @@ class EventAdapter(
val profileAttrName: String? = null // for profile events only
) {

private val systemPropToKey = mapOf(
internal val systemPropToKey = mapOf(
"CT App Version" to CLTAP_APP_VERSION,
"ct_app_version" to CLTAP_APP_VERSION,
"CT Latitude" to CLTAP_LATITUDE,
Expand All @@ -59,6 +60,7 @@ class EventAdapter(

/**
* Gets the property value for the specified property name.
* Note: Compares after normalising (removing all whitespaces)
*
* @param propertyName The name of the property to retrieve.
* @return A [TriggerValue] representing the property value.
Expand All @@ -70,12 +72,30 @@ class EventAdapter(

/**
* Gets the item value for the specified property name from the list of items.
* Note: Compares after normalising (removing all whitespaces)
*
* @param propertyName The name of the property to retrieve from the items.
* @return A [TriggerValue] representing the item value.
*/
fun getItemValue(propertyName: String): List<TriggerValue> {
return items.filterNotNull().map { TriggerValue(it[propertyName]) }
return items
.filterNotNull()
.map { productMap: Map<String, Any> ->

var op = productMap[propertyName]

if (op == null) {
op = productMap[Utils.getNormalizedName(propertyName)]
}

if (op == null) {
val normalisedMap = productMap.map {
Utils.getNormalizedName(it.key) to it.value
}.toMap()
op = normalisedMap[Utils.getNormalizedName(propertyName)]
}
TriggerValue(op)
}.filter { it.value != null }
}

/**
Expand All @@ -99,18 +119,35 @@ class EventAdapter(

@VisibleForTesting
internal fun getActualPropertyValue(propertyName: String): Any? {
var value = eventProperties[propertyName]

var value = evaluateActualPropertyValue(propertyName)

if (value == null) {
value = when (propertyName) {
CLTAP_PROP_CAMPAIGN_ID -> eventProperties[NOTIFICATION_ID_TAG]
NOTIFICATION_ID_TAG -> eventProperties[CLTAP_PROP_CAMPAIGN_ID]
CLTAP_PROP_VARIANT -> eventProperties[INAPP_WZRK_PIVOT]
INAPP_WZRK_PIVOT -> eventProperties[CLTAP_PROP_VARIANT]
else -> systemPropToKey[propertyName]?.let { eventProperties[it] }
CLTAP_PROP_CAMPAIGN_ID -> evaluateActualPropertyValue(NOTIFICATION_ID_TAG)
NOTIFICATION_ID_TAG -> evaluateActualPropertyValue(CLTAP_PROP_CAMPAIGN_ID)
CLTAP_PROP_VARIANT -> evaluateActualPropertyValue(INAPP_WZRK_PIVOT)
INAPP_WZRK_PIVOT -> evaluateActualPropertyValue(CLTAP_PROP_VARIANT)
else -> systemPropToKey[propertyName]?.let { evaluateActualPropertyValue(it) }
}
}

return value
}

private fun evaluateActualPropertyValue(propertyName: String): Any? {
var value = eventProperties[propertyName]

if (value == null) {
value = eventProperties[Utils.getNormalizedName(propertyName)]
}

if (value == null) {
val normalisedMap = eventProperties.map { item ->
Utils.getNormalizedName(item.key) to item.value
}.toMap()
value = normalisedMap[Utils.getNormalizedName(propertyName)]
}
return value
}
}
Loading
Loading