Skip to content

Commit 362842a

Browse files
authored
Support IAM caps (#459)
* Initial changes for localCaps (not tested) * Read/write localCaps. Little fixes * Unit tests. Added Clock class for easier testing. * Enable local IAM caps. * Do not record impression for local push. Fix VarCache for empty localCaps. * Fix FCU check
1 parent 7028dd4 commit 362842a

File tree

17 files changed

+746
-46
lines changed

17 files changed

+746
-46
lines changed

AndroidSDKCore/src/main/java/com/leanplum/ActionContext.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,17 @@ private boolean createActionContextForMessageId(String messageAction,
388388
@Override
389389
public void variablesChanged() {
390390
try {
391+
// We do not want to count occurrences for action kind, because in multi message
392+
// campaigns the Open URL action is not a message. Also if the user has defined
393+
// actions of type Action we do not want to count them.
394+
int actionKind = VarCache.getActionDefinitionType(actionContext.name);
395+
if (actionKind == Leanplum.ACTION_KIND_ACTION) {
396+
ActionManager.getInstance().recordChainedActionImpression(messageId);
397+
} else {
398+
ActionManager.getInstance().recordMessageImpression(messageId);
399+
}
391400
Leanplum.triggerMessageDisplayed(actionContext);
401+
392402
} catch (Throwable t) {
393403
Log.exception(t);
394404
}

AndroidSDKCore/src/main/java/com/leanplum/Leanplum.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,7 @@ static synchronized void start(final Context context, final String userId,
552552
new HashMap<>(),
553553
new HashMap<>(),
554554
new ArrayList<>(),
555+
new ArrayList<>(),
555556
new HashMap<>(),
556557
"",
557558
"");
@@ -957,6 +958,8 @@ private static void applyContentInResponse(JSONObject response, boolean alwaysAp
957958
response.optJSONObject(Constants.Keys.REGIONS));
958959
List<Map<String, Object>> variants = JsonConverter.listFromJsonOrDefault(
959960
response.optJSONArray(Constants.Keys.VARIANTS));
961+
List<Map<String, Object>> localCaps = JsonConverter.listFromJsonOrDefault(
962+
response.optJSONArray(Constants.Keys.LOCAL_CAPS));
960963
Map<String, Object> variantDebugInfo = JsonConverter.mapFromJsonOrDefault(
961964
response.optJSONObject(Constants.Keys.VARIANT_DEBUG_INFO));
962965
JSONObject varsJsonObj = response.optJSONObject(Constants.Keys.VARS);
@@ -967,12 +970,14 @@ private static void applyContentInResponse(JSONObject response, boolean alwaysAp
967970
|| !values.equals(VarCache.getDiffs())
968971
|| !messages.equals(VarCache.getMessageDiffs())
969972
|| !variants.equals(VarCache.variants())
973+
|| !localCaps.equals(VarCache.localCaps())
970974
|| !regions.equals(VarCache.regions())) {
971975
VarCache.applyVariableDiffs(
972976
values,
973977
messages,
974978
regions,
975979
variants,
980+
localCaps,
976981
variantDebugInfo,
977982
varsJson,
978983
varsSignature);
@@ -1305,7 +1310,6 @@ public static void removeMessageDisplayedHandler(
13051310
}
13061311

13071312
public static void triggerMessageDisplayed(ActionContext actionContext) {
1308-
ActionManager.getInstance().recordMessageImpression(actionContext.getMessageId());
13091313
synchronized (messageDisplayedHandlers) {
13101314
for (MessageDisplayedCallback callback : messageDisplayedHandlers) {
13111315
MessageArchiveData messageArchiveData = messageArchiveDataFromContext(actionContext);

AndroidSDKCore/src/main/java/com/leanplum/internal/ActionManager.java

Lines changed: 167 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2014, Leanplum, Inc. All rights reserved.
2+
* Copyright 2021, Leanplum, Inc. All rights reserved.
33
*
44
* Licensed to the Apache Software Foundation (ASF) under one
55
* or more contributor license agreements. See the NOTICE file
@@ -24,14 +24,15 @@
2424
import android.content.Context;
2525
import android.content.SharedPreferences;
2626

27+
import android.text.TextUtils;
28+
import androidx.annotation.VisibleForTesting;
2729
import com.leanplum.ActionContext;
2830
import com.leanplum.ActionContext.ContextualValues;
2931
import com.leanplum.Leanplum;
3032
import com.leanplum.LocationManager;
3133
import com.leanplum.callbacks.ActionCallback;
3234
import com.leanplum.utils.SharedPreferencesUtil;
3335

34-
import java.util.Date;
3536
import java.util.HashMap;
3637
import java.util.List;
3738
import java.util.Map;
@@ -43,9 +44,13 @@
4344
* @author Andrew First
4445
*/
4546
public class ActionManager {
46-
private Map<String, Map<String, Number>> messageImpressionOccurrences;
47-
private Map<String, Number> messageTriggerOccurrences;
48-
private Map<String, Number> sessionOccurrences;
47+
private static final long HOUR_MILLIS = 60 * 60 * 1000;
48+
private static final long DAY_MILLIS = 24 * HOUR_MILLIS;
49+
private static final long WEEK_MILLIS = 7 * DAY_MILLIS;
50+
51+
private final Map<String, Map<String, Number>> messageImpressionOccurrences = new HashMap<>();
52+
private final Map<String, Number> messageTriggerOccurrences = new HashMap<>();
53+
private final Map<String, Number> sessionOccurrences = new HashMap<>();
4954

5055
private static ActionManager instance;
5156

@@ -100,9 +105,6 @@ public static synchronized LocationManager getLocationManager() {
100105

101106
private ActionManager() {
102107
listenForLocalNotifications();
103-
sessionOccurrences = new HashMap<>();
104-
messageImpressionOccurrences = new HashMap<>();
105-
messageTriggerOccurrences = new HashMap<>();
106108
}
107109

108110
private void listenForLocalNotifications() {
@@ -129,7 +131,7 @@ public boolean onResponse(ActionContext actionContext) {
129131
Log.e("Invalid notification countdown: " + countdownObj);
130132
return false;
131133
}
132-
long eta = System.currentTimeMillis() + ((Number) countdownObj).longValue() * 1000L;
134+
long eta = Clock.getInstance().currentTimeMillis() + ((Number) countdownObj).longValue() * 1000L;
133135
// Schedule notification.
134136
try {
135137
return (boolean) Class.forName(LEANPLUM_LOCAL_PUSH_HELPER)
@@ -168,7 +170,7 @@ public boolean onResponse(ActionContext actionContext) {
168170
Class.forName(LEANPLUM_LOCAL_PUSH_HELPER)
169171
.getDeclaredMethod("cancelLocalPush", Context.class, String.class)
170172
.invoke(new Object(), context, messageId);
171-
boolean didCancel = existingEta > System.currentTimeMillis();
173+
boolean didCancel = existingEta > Clock.getInstance().currentTimeMillis();
172174
if (didCancel) {
173175
Log.d("Cancelled notification");
174176
}
@@ -273,7 +275,7 @@ public MessageMatchResult shouldShowMessage(String messageId, Map<String, Object
273275
if (messageStartTime == null || messageEndTime == null) {
274276
result.matchedActivePeriod = true;
275277
} else {
276-
long currentTime = new Date().getTime();
278+
long currentTime = Clock.getInstance().newDate().getTime();
277279
result.matchedActivePeriod = currentTime >= (long) messageStartTime &&
278280
currentTime <= (long) messageEndTime;
279281
}
@@ -359,7 +361,7 @@ private boolean matchesLimitTimes(int amount, int time, String units,
359361
} else if (units.equals("limitMonth")) {
360362
time *= 2592000;
361363
}
362-
long now = System.currentTimeMillis();
364+
long now = Clock.getInstance().currentTimeMillis();
363365
int matchedOccurrences = 0;
364366
for (long i = max.longValue(); i >= min.longValue(); i--) {
365367
if (occurrences.containsKey("" + i)) {
@@ -459,7 +461,8 @@ public void recordMessageTrigger(String messageId) {
459461
* @param originalMessageId The original ID of the held back message.
460462
*/
461463
public void recordHeldBackImpression(String messageId, String originalMessageId) {
462-
recordImpression(messageId, originalMessageId);
464+
trackHeldBackEvent(originalMessageId);
465+
recordImpression(messageId);
463466
}
464467

465468
/**
@@ -468,28 +471,52 @@ public void recordHeldBackImpression(String messageId, String originalMessageId)
468471
* @param messageId The ID of the message
469472
*/
470473
public void recordMessageImpression(String messageId) {
471-
recordImpression(messageId, null);
474+
trackImpressionEvent(messageId);
475+
recordImpression(messageId);
472476
}
473477

474478
/**
475-
* Records the occurrence of a message and tracks the correct impression event.
479+
* Tracks the "Open" event for an action.
476480
*
477-
* @param messageId The ID of the message.
478-
* @param originalMessageId The original message ID of the held back message. Supply this only if
479-
* the message is held back. Otherwise, use null.
481+
* @param messageId The ID of the action
482+
*/
483+
public void recordChainedActionImpression(String messageId) {
484+
trackImpressionEvent(messageId);
485+
}
486+
487+
/**
488+
* Tracks the event for local push notification.
489+
* Do not want to track impression occurrence in such case.
490+
*
491+
* @param messageId The ID of the action
492+
*/
493+
public void recordLocalPushImpression(String messageId) {
494+
trackImpressionEvent(messageId);
495+
}
496+
497+
/**
498+
* Tracks the correct held back event.
499+
*
500+
* @param originalMessageId The original message ID of the held back message.
480501
*/
481-
private void recordImpression(String messageId, String originalMessageId) {
502+
private void trackHeldBackEvent(String originalMessageId) {
482503
Map<String, String> requestArgs = new HashMap<>();
483-
if (originalMessageId != null) {
484-
// This is a held back message - track the event with the original message ID.
485-
requestArgs.put(Constants.Params.MESSAGE_ID, originalMessageId);
486-
LeanplumInternal.track(Constants.HELD_BACK_EVENT_NAME, 0.0, null, null, requestArgs);
487-
} else {
488-
// Track the message impression and occurrence.
489-
requestArgs.put(Constants.Params.MESSAGE_ID, messageId);
490-
LeanplumInternal.track(null, 0.0, null, null, requestArgs);
491-
}
504+
requestArgs.put(Constants.Params.MESSAGE_ID, originalMessageId);
505+
LeanplumInternal.track(Constants.HELD_BACK_EVENT_NAME, 0.0, null, null, requestArgs);
506+
}
492507

508+
private void trackImpressionEvent(String messageId) {
509+
Map<String, String> requestArgs = new HashMap<>();
510+
requestArgs.put(Constants.Params.MESSAGE_ID, messageId);
511+
LeanplumInternal.track(null, 0.0, null, null, requestArgs);
512+
}
513+
514+
/**
515+
* Records the occurrence of a message.
516+
*
517+
* @param messageId The ID of the message.
518+
*/
519+
private void recordImpression(String messageId) {
493520
// Record session occurrences.
494521
Number existing = sessionOccurrences.get(messageId);
495522
if (existing == null) {
@@ -504,7 +531,7 @@ private void recordImpression(String messageId, String originalMessageId) {
504531
occurrences = new HashMap<>();
505532
occurrences.put("min", 0L);
506533
occurrences.put("max", 0L);
507-
occurrences.put("0", System.currentTimeMillis());
534+
occurrences.put("0", Clock.getInstance().currentTimeMillis());
508535
} else {
509536
Number min = occurrences.get("min");
510537
Number max = occurrences.get("max");
@@ -515,7 +542,7 @@ private void recordImpression(String messageId, String originalMessageId) {
515542
max = 0L;
516543
}
517544
max = max.longValue() + 1L;
518-
occurrences.put("" + max, System.currentTimeMillis());
545+
occurrences.put("" + max, Clock.getInstance().currentTimeMillis());
519546
if (max.longValue() - min.longValue() + 1 >
520547
Constants.Messaging.MAX_STORED_OCCURRENCES_PER_MESSAGE) {
521548
occurrences.remove("" + min);
@@ -580,4 +607,114 @@ public static void addRegionNamesFromTriggersToSet(
580607
}
581608
}
582609
}
610+
611+
/**
612+
* Checks if message occurrences have reached limits coming from local IAM caps data.
613+
*
614+
* @return True to suppress messages, false otherwise.
615+
*/
616+
public boolean shouldSuppressMessages() {
617+
int dayLimit = 0;
618+
int weekLimit = 0;
619+
int sessionLimit = 0;
620+
621+
for (Map<String, Object> cap : VarCache.localCaps()) {
622+
if (!"IN_APP".equals(cap.get("channel"))) {
623+
continue;
624+
}
625+
String type = (String) cap.get("type");
626+
Integer limit = (Integer) cap.get("limit");
627+
if (limit == null) {
628+
continue;
629+
}
630+
631+
if ("DAY".equals(type)) {
632+
dayLimit = limit;
633+
} else if ("WEEK".equals(type)) {
634+
weekLimit = limit;
635+
} else if ("SESSION".equals(type)) {
636+
sessionLimit = limit;
637+
}
638+
}
639+
640+
return (weekLimit > 0 && weeklyOccurrencesCount() >= weekLimit)
641+
|| (dayLimit > 0 && dailyOccurrencesCount() >= dayLimit)
642+
|| (sessionLimit > 0 && sessionOccurrencesCount() >= sessionLimit);
643+
}
644+
645+
@VisibleForTesting
646+
int dailyOccurrencesCount() {
647+
long endTime = Clock.getInstance().currentTimeMillis();
648+
long startTime = endTime - DAY_MILLIS;
649+
return countOccurrences(startTime, endTime);
650+
}
651+
652+
@VisibleForTesting
653+
int weeklyOccurrencesCount() {
654+
long endTime = Clock.getInstance().currentTimeMillis();
655+
long startTime = endTime - WEEK_MILLIS;
656+
return countOccurrences(startTime, endTime);
657+
}
658+
659+
private int countOccurrences(long startTime, long endTime) {
660+
String prefix = String.format(Constants.Defaults.MESSAGE_IMPRESSION_OCCURRENCES_KEY, "");
661+
662+
Context context = Leanplum.getContext();
663+
SharedPreferences prefs =
664+
context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
665+
Map<String, ?> all = prefs.getAll();
666+
667+
int occurrenceCount = 0;
668+
for (Map.Entry<String, ?> entry : all.entrySet()) {
669+
if (entry.getKey().startsWith(prefix)) {
670+
String json = (String) entry.getValue();
671+
if (!TextUtils.isEmpty(json) && !json.equals("{}")) {
672+
occurrenceCount += countOccurrences(startTime, endTime, json);
673+
}
674+
}
675+
}
676+
677+
return occurrenceCount;
678+
}
679+
680+
private int countOccurrences(long startTime, long endTime, String json) {
681+
Map<String, Number> occurrences = CollectionUtil.uncheckedCast(JsonConverter.fromJson(json));
682+
Number min = occurrences.get("min");
683+
Number max = occurrences.get("max");
684+
685+
if (min == null || max == null) {
686+
return 0;
687+
}
688+
689+
long minId = min.longValue();
690+
long maxId = max.longValue();
691+
int count = 0;
692+
693+
for (long id = maxId; id >= minId; id--) {
694+
Number time = occurrences.get("" + id);
695+
if (time != null) {
696+
if (startTime <= time.longValue() && time.longValue() <= endTime) {
697+
count++;
698+
} else {
699+
// occurrences with smaller ids would fall out of time interval
700+
return count;
701+
}
702+
}
703+
}
704+
705+
return count;
706+
}
707+
708+
@VisibleForTesting
709+
int sessionOccurrencesCount() {
710+
int count = 0;
711+
for (Map.Entry<String, Number> entry : sessionOccurrences.entrySet()) {
712+
Number value = entry.getValue();
713+
if (value != null) {
714+
count += value.intValue();
715+
}
716+
}
717+
return count;
718+
}
719+
583720
}

0 commit comments

Comments
 (0)