-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #82 from Parsely/engagement_manager_tests
- Loading branch information
Showing
4 changed files
with
299 additions
and
121 deletions.
There are no files selected for viewing
120 changes: 120 additions & 0 deletions
120
parsely/src/main/java/com/parsely/parselyandroid/EngagementManager.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
package com.parsely.parselyandroid; | ||
|
||
import java.util.Calendar; | ||
import java.util.HashMap; | ||
import java.util.Map; | ||
import java.util.TimeZone; | ||
import java.util.Timer; | ||
import java.util.TimerTask; | ||
|
||
/** | ||
* Engagement manager for article and video engagement. | ||
* <p> | ||
* Implemented to handle its own queuing of future executions to accomplish | ||
* two things: | ||
* <p> | ||
* 1. Flushing any engaged time before canceling. | ||
* 2. Progressive backoff for long engagements to save data. | ||
*/ | ||
class EngagementManager { | ||
|
||
private final ParselyTracker parselyTracker; | ||
public Map<String, Object> baseEvent; | ||
private boolean started; | ||
private final Timer parentTimer; | ||
private TimerTask waitingTimerTask; | ||
private long latestDelayMillis, totalTime; | ||
private Calendar startTime; | ||
private final UpdateEngagementIntervalCalculator intervalCalculator; | ||
|
||
public EngagementManager( | ||
ParselyTracker parselyTracker, | ||
Timer parentTimer, | ||
long intervalMillis, | ||
Map<String, Object> baseEvent, | ||
UpdateEngagementIntervalCalculator intervalCalculator | ||
) { | ||
this.parselyTracker = parselyTracker; | ||
this.baseEvent = baseEvent; | ||
this.parentTimer = parentTimer; | ||
this.intervalCalculator = intervalCalculator; | ||
latestDelayMillis = intervalMillis; | ||
totalTime = 0; | ||
startTime = Calendar.getInstance(TimeZone.getTimeZone("UTC")); | ||
} | ||
|
||
public boolean isRunning() { | ||
return started; | ||
} | ||
|
||
public void start() { | ||
scheduleNextExecution(latestDelayMillis); | ||
started = true; | ||
startTime = Calendar.getInstance(TimeZone.getTimeZone("UTC")); | ||
} | ||
|
||
public void stop() { | ||
waitingTimerTask.cancel(); | ||
started = false; | ||
} | ||
|
||
public boolean isSameVideo(String url, String urlRef, ParselyVideoMetadata metadata) { | ||
Map<String, Object> baseMetadata = (Map<String, Object>) baseEvent.get("metadata"); | ||
return (baseEvent.get("url").equals(url) && | ||
baseEvent.get("urlref").equals(urlRef) && | ||
baseMetadata.get("link").equals(metadata.link) && | ||
(int) (baseMetadata.get("duration")) == metadata.durationSeconds); | ||
} | ||
|
||
private void scheduleNextExecution(long delay) { | ||
TimerTask task = new TimerTask() { | ||
public void run() { | ||
doEnqueue(scheduledExecutionTime()); | ||
latestDelayMillis = intervalCalculator.updateLatestInterval(startTime); | ||
scheduleNextExecution(latestDelayMillis); | ||
} | ||
|
||
public boolean cancel() { | ||
boolean output = super.cancel(); | ||
// Only enqueue when we actually canceled something. If output is false then | ||
// this has already been canceled. | ||
if (output) { | ||
doEnqueue(scheduledExecutionTime()); | ||
} | ||
return output; | ||
} | ||
}; | ||
latestDelayMillis = delay; | ||
parentTimer.schedule(task, delay); | ||
waitingTimerTask = task; | ||
} | ||
|
||
private void doEnqueue(long scheduledExecutionTime) { | ||
// Create a copy of the base event to enqueue | ||
Map<String, Object> event = new HashMap<>(baseEvent); | ||
ParselyTracker.PLog(String.format("Enqueuing %s event.", event.get("action"))); | ||
|
||
// Update `ts` for the event since it's happening right now. | ||
Calendar now = Calendar.getInstance(TimeZone.getTimeZone("UTC")); | ||
@SuppressWarnings("unchecked") | ||
Map<String, Object> baseEventData = (Map<String, Object>) event.get("data"); | ||
assert baseEventData != null; | ||
Map<String, Object> data = new HashMap<>(baseEventData); | ||
data.put("ts", now.getTimeInMillis()); | ||
event.put("data", data); | ||
|
||
// Adjust inc by execution time in case we're late or early. | ||
long executionDiff = (System.currentTimeMillis() - scheduledExecutionTime); | ||
long inc = (latestDelayMillis + executionDiff); | ||
totalTime += inc; | ||
event.put("inc", inc / 1000); | ||
event.put("tt", totalTime); | ||
|
||
parselyTracker.enqueueEvent(event); | ||
} | ||
|
||
|
||
public double getIntervalMillis() { | ||
return latestDelayMillis; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
22 changes: 22 additions & 0 deletions
22
parsely/src/main/java/com/parsely/parselyandroid/UpdateEngagementIntervalCalculator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package com.parsely.parselyandroid; | ||
|
||
import androidx.annotation.NonNull; | ||
|
||
import java.util.Calendar; | ||
import java.util.TimeZone; | ||
|
||
class UpdateEngagementIntervalCalculator { | ||
|
||
private static final long MAX_TIME_BETWEEN_HEARTBEATS = 60 * 60; | ||
private static final long OFFSET_MATCHING_BASE_INTERVAL = 35; | ||
private static final double BACKOFF_PROPORTION = 0.3; | ||
|
||
long updateLatestInterval(@NonNull final Calendar startTime) { | ||
Calendar now = Calendar.getInstance(TimeZone.getTimeZone("UTC")); | ||
long totalTrackedTime = (now.getTime().getTime() - startTime.getTime().getTime()) / 1000; | ||
double totalWithOffset = totalTrackedTime + OFFSET_MATCHING_BASE_INTERVAL; | ||
double newInterval = totalWithOffset * BACKOFF_PROPORTION; | ||
long clampedNewInterval = (long) Math.min(MAX_TIME_BETWEEN_HEARTBEATS, newInterval); | ||
return clampedNewInterval * 1000; | ||
} | ||
} |
Oops, something went wrong.