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

feat(SDK-4171): Make Analytics manager testable #689

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,48 +34,41 @@
import org.json.JSONException;
import org.json.JSONObject;

import kotlin.jvm.functions.Function0;

public class AnalyticsManager extends BaseAnalyticsManager {

private final CTLockManager ctLockManager;

private final HashMap<String, Integer> 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<String, Object> notificationIdTagMap = new HashMap<>();

private final Function0<Long> currentTimeProvider;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use the com.clevertap.android.sdk.utils.Clock interface which looks like it was created for this purpose. Or usejava.time.Clock

private final Object notificationMapLock = new Object();

private final HashMap<String, Object> notificationIdTagMap = new HashMap<>();
private final HashMap<String, Object> notificationViewedIdTagMap = new HashMap<>();

AnalyticsManager(Context context,
CleverTapInstanceConfig config,
BaseEventQueueManager baseEventQueueManager,
Validator validator,
ValidationResultStack validationResultStack,
CoreMetaData coreMetaData,
DeviceInfo deviceInfo,
BaseCallbackManager callbackManager, ControllerManager controllerManager,
final CTLockManager ctLockManager,
InAppResponse inAppResponse) {
AnalyticsManager(
Context context,
CleverTapInstanceConfig config,
BaseEventQueueManager baseEventQueueManager,
Validator validator,
ValidationResultStack validationResultStack,
CoreMetaData coreMetaData,
DeviceInfo deviceInfo,
BaseCallbackManager callbackManager, ControllerManager controllerManager,
final CTLockManager ctLockManager,
InAppResponse inAppResponse,
Function0<Long> currentTimeProvider
) {
this.context = context;
this.config = config;
this.baseEventQueueManager = baseEventQueueManager;
Expand All @@ -87,6 +80,7 @@ public class AnalyticsManager extends BaseAnalyticsManager {
this.ctLockManager = ctLockManager;
this.controllerManager = controllerManager;
this.inAppResponse = inAppResponse;
this.currentTimeProvider = currentTimeProvider;
}

@Override
Expand Down Expand Up @@ -464,8 +458,7 @@ public void pushNotificationClickedEvent(final Bundle extras) {
}

boolean shouldProcess = (accountId == null && config.isDefaultInstance())
|| config.getAccountId()
.equals(accountId);
|| config.getAccountId().equals(accountId);

if (!shouldProcess) {
config.getLogger().debug(config.getAccountId(),
Expand All @@ -474,60 +467,12 @@ public void pushNotificationClickedEvent(final Bundle extras) {
}

if (extras.containsKey(Constants.INAPP_PREVIEW_PUSH_PAYLOAD_KEY)) {
Task<Void> task = CTExecutorFactory.executors(config).postAsyncSafelyTask();
task.execute("testInappNotification",new Callable<Void>() {
@Override
public Void call() {
try {
String inappPreviewPayloadType = extras.getString(Constants.INAPP_PREVIEW_PUSH_PAYLOAD_TYPE_KEY);
String inappPreviewString = extras.getString(Constants.INAPP_PREVIEW_PUSH_PAYLOAD_KEY);
JSONObject inappPreviewPayload = new JSONObject(inappPreviewString);

JSONArray inappNotifs = new JSONArray();
if (Constants.INAPP_IMAGE_INTERSTITIAL_TYPE.equals(inappPreviewPayloadType)
|| Constants.INAPP_ADVANCED_BUILDER_TYPE.equals(inappPreviewPayloadType)) {
inappNotifs.put(getHalfInterstitialInApp(inappPreviewPayload));
} else {
inappNotifs.put(inappPreviewPayload);
}

JSONObject inAppResponseJson = new JSONObject();
inAppResponseJson.put(Constants.INAPP_JSON_RESPONSE_KEY, inappNotifs);

inAppResponse.processResponse(inAppResponseJson, null, context);
} catch (Throwable t) {
Logger.v("Failed to display inapp notification from push notification payload", t);
}
return null;
}
});
handleInAppPreview(extras);
return;
}

if (extras.containsKey(Constants.INBOX_PREVIEW_PUSH_PAYLOAD_KEY)) {
Task<Void> task = CTExecutorFactory.executors(config).postAsyncSafelyTask();
task.execute("testInboxNotification",new Callable<Void>() {
@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;
}

Expand All @@ -538,8 +483,7 @@ 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;
}

Expand All @@ -552,26 +496,12 @@ public Void call() {
return;
}

JSONObject event = new JSONObject();
JSONObject notif = new JSONObject();
try {
for (String x : extras.keySet()) {
if (!x.startsWith(Constants.WZRK_PREFIX)) {
continue;
}
Object value = extras.get(x);
notif.put(x, value);
}
// convert bundle to json
JSONObject event = AnalyticsManagerBundler.INSTANCE.notificationViewedJson(extras);

event.put("evtName", Constants.NOTIFICATION_CLICKED_EVENT_NAME);
event.put("evtData", notif);
baseEventQueueManager.queueEvent(context, event, Constants.RAISED_EVENT);

try {
coreMetaData.setWzrkParams(getWzrkFields(extras));
} catch (Throwable t) {
// no-op
}
coreMetaData.setWzrkParams(AnalyticsManagerBundler.INSTANCE.wzrkBundleToJson(extras));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wzrkBundleToJson throws and the exception seems unhandled now. Is possible make the method not throwing or try-catch here.

} catch (Throwable t) {
// We won't get here
}
Expand All @@ -583,6 +513,62 @@ public Void call() {
}
}

private void handleInboxPreview(Bundle extras) {
Task<Void> task = CTExecutorFactory.executors(config).postAsyncSafelyTask();
task.execute("testInboxNotification",new Callable<Void>() {
@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<Void> task = CTExecutorFactory.executors(config).postAsyncSafelyTask();
task.execute("testInappNotification",new Callable<Void>() {
@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);
Expand Down Expand Up @@ -650,33 +636,26 @@ 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,
boolean isDuplicate = checkDuplicateNotificationIds(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.INSTANCE.notificationViewedJson(extras);
baseEventQueueManager.queueEvent(context, event, Constants.NV_EVENT);
}

Expand Down Expand Up @@ -1170,7 +1149,7 @@ private boolean checkDuplicateNotificationIds(Bundle extras, HashMap<String, Obj
boolean isDupe = false;
try {
String notificationIdTag = extras.getString(Constants.NOTIFICATION_ID_TAG);
long now = System.currentTimeMillis();
long now = currentTimeProvider.invoke();
if (notificationTagMap.containsKey(notificationIdTag)) {
long timestamp;
// noinspection ConstantConditions
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.clevertap.android.sdk

import android.os.Bundle
import org.json.JSONException
import org.json.JSONObject

object AnalyticsManagerBundler {

@Throws(JSONException::class)
fun wzrkBundleToJson(root: Bundle): JSONObject {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can add @JvmStatic annotation to both methods in this object. So when calling them from java, they can be called form the class instead of the INSTANCE.

val fields = JSONObject()
for (s in root.keySet()) {
val o = root[s]
if (o is Bundle) {
val wzrkFields = wzrkBundleToJson(o)
val keys = wzrkFields.keys()
while (keys.hasNext()) {
val k = keys.next()
fields.put(k, wzrkFields[k])
}
} else if (s.startsWith(Constants.WZRK_PREFIX)) {
fields.put(s, root[s])
}
}

return fields
}

fun notificationViewedJson(root: Bundle): JSONObject {
val event = JSONObject()
try {
val notif = wzrkBundleToJson(root)
event.put("evtName", Constants.NOTIFICATION_VIEWED_EVENT_NAME)
event.put("evtData", notif)
} catch (ignored: Throwable) {
//no-op
}
return event
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2219,7 +2219,7 @@ public CTProductConfigController productConfig() {
getConfig().getLogger().debug(getAccountId(),
"Product config is not supported with analytics only configuration");
}
return coreState.getCtProductConfigController();
return coreState.getCtProductConfigController(context);
}

/**
Expand Down Expand Up @@ -3111,6 +3111,8 @@ private static CleverTapInstanceConfig getDefaultConfig(Context context) {
if (accountRegion == null) {
Logger.i("Account Region not specified in the AndroidManifest - using default region");
}

// todo lp pass manifest info here
CleverTapInstanceConfig defaultInstanceConfig = CleverTapInstanceConfig.createDefaultInstance(context, accountId, accountToken, accountRegion);

if (proxyDomain != null && !proxyDomain.trim().isEmpty()) {
Expand Down
Loading
Loading