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
2424import android .content .Context ;
2525import android .content .SharedPreferences ;
2626
27+ import android .text .TextUtils ;
28+ import androidx .annotation .VisibleForTesting ;
2729import com .leanplum .ActionContext ;
2830import com .leanplum .ActionContext .ContextualValues ;
2931import com .leanplum .Leanplum ;
3032import com .leanplum .LocationManager ;
3133import com .leanplum .callbacks .ActionCallback ;
3234import com .leanplum .utils .SharedPreferencesUtil ;
3335
34- import java .util .Date ;
3536import java .util .HashMap ;
3637import java .util .List ;
3738import java .util .Map ;
4344 * @author Andrew First
4445 */
4546public 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