Skip to content

Commit

Permalink
Merge pull request #87 from Parsely/extract_queuemanager_local_storage
Browse files Browse the repository at this point in the history
  • Loading branch information
wzieba authored Nov 6, 2023
2 parents fcb6d7c + 1b8ffca commit b9c449d
Show file tree
Hide file tree
Showing 6 changed files with 360 additions and 110 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.parsely.parselyandroid

import android.content.Context
import java.io.EOFException
import java.io.FileNotFoundException
import java.io.ObjectInputStream
import java.io.ObjectOutputStream

internal open class LocalStorageRepository(private val context: Context) {
/**
* Persist an object to storage.
*
* @param o Object to store.
*/
private fun persistObject(o: Any) {
try {
val fos = context.applicationContext.openFileOutput(
STORAGE_KEY,
Context.MODE_PRIVATE
)
val oos = ObjectOutputStream(fos)
oos.writeObject(o)
oos.close()
} catch (ex: Exception) {
ParselyTracker.PLog("Exception thrown during queue serialization: %s", ex.toString())
}
}

/**
* Delete the stored queue from persistent storage.
*/
fun purgeStoredQueue() {
persistObject(ArrayList<Map<String, Any>>())
}

/**
* Get the stored event queue from persistent storage.
*
* @return The stored queue of events.
*/
open fun getStoredQueue(): ArrayList<Map<String, Any?>?> {
var storedQueue: ArrayList<Map<String, Any?>?> = ArrayList()
try {
val fis = context.applicationContext.openFileInput(STORAGE_KEY)
val ois = ObjectInputStream(fis)
@Suppress("UNCHECKED_CAST")
storedQueue = ois.readObject() as ArrayList<Map<String, Any?>?>
ois.close()
} catch (ex: EOFException) {
// Nothing to do here.
} catch (ex: FileNotFoundException) {
// Nothing to do here. Means there was no saved queue.
} catch (ex: Exception) {
ParselyTracker.PLog(
"Exception thrown during queue deserialization: %s",
ex.toString()
)
}
return storedQueue
}

/**
* Delete an event from the stored queue.
*/
open fun expelStoredEvent() {
val storedQueue = getStoredQueue()
storedQueue.removeAt(0)
}

/**
* Save the event queue to persistent storage.
*/
@Synchronized
open fun persistQueue(inMemoryQueue: List<Map<String, Any?>?>) {
ParselyTracker.PLog("Persisting event queue")
persistObject((inMemoryQueue + getStoredQueue()).distinct())
}

companion object {
private const val STORAGE_KEY = "parsely-events.ser"
}
}
127 changes: 17 additions & 110 deletions parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,13 @@

import com.fasterxml.jackson.databind.ObjectMapper;

import java.io.EOFException;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Formatter;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
Expand All @@ -56,13 +51,10 @@ public class ParselyTracker {
private static ParselyTracker instance = null;
private static final int DEFAULT_FLUSH_INTERVAL_SECS = 60;
private static final int DEFAULT_ENGAGEMENT_INTERVAL_MILLIS = 10500;
private static final int QUEUE_SIZE_LIMIT = 50;
private static final int STORAGE_SIZE_LIMIT = 100;
private static final String STORAGE_KEY = "parsely-events.ser";
@SuppressWarnings("StringOperationCanBeSimplified")
// private static final String ROOT_URL = "http://10.0.2.2:5001/".intern(); // emulator localhost
private static final String ROOT_URL = "https://p1.parsely.com/".intern();
protected ArrayList<Map<String, Object>> eventQueue;
private final ArrayList<Map<String, Object>> eventQueue;
private boolean isDebug;
private final Context context;
private final Timer timer;
Expand All @@ -72,14 +64,18 @@ public class ParselyTracker {
private String lastPageviewUuid = null;
@NonNull
private final EventsBuilder eventsBuilder;
@NonNull final HeartbeatIntervalCalculator intervalCalculator = new HeartbeatIntervalCalculator(new Clock());
@NonNull
private final HeartbeatIntervalCalculator intervalCalculator = new HeartbeatIntervalCalculator(new Clock());
@NonNull
private final LocalStorageRepository localStorageRepository;

/**
* Create a new ParselyTracker instance.
*/
protected ParselyTracker(String siteId, int flushInterval, Context c) {
context = c.getApplicationContext();
eventsBuilder = new EventsBuilder(context, siteId);
localStorageRepository = new LocalStorageRepository(context);

// get the adkey straight away on instantiation
timer = new Timer();
Expand All @@ -89,7 +85,7 @@ protected ParselyTracker(String siteId, int flushInterval, Context c) {

flushManager = new FlushManager(timer, flushInterval * 1000L);

if (getStoredQueue().size() > 0) {
if (localStorageRepository.getStoredQueue().size() > 0) {
startFlushTimer();
}

Expand All @@ -102,6 +98,10 @@ protected ParselyTracker(String siteId, int flushInterval, Context c) {
);
}

List<Map<String, Object>> getInMemoryQueue() {
return eventQueue;
}

/**
* Singleton instance accessor. Note: This must be called after {@link #sharedInstance(String, Context)}
*
Expand Down Expand Up @@ -411,14 +411,14 @@ public void resetVideo() {
* Place a data structure representing the event into the in-memory queue for later use.
* <p>
* **Note**: Events placed into this queue will be discarded if the size of the persistent queue
* store exceeds {@link #STORAGE_SIZE_LIMIT}.
* store exceeds {@link QueueManager#STORAGE_SIZE_LIMIT}.
*
* @param event The event Map to enqueue.
*/
void enqueueEvent(Map<String, Object> event) {
// Push it onto the queue
eventQueue.add(event);
new QueueManager().execute();
new QueueManager(this, localStorageRepository).execute();
if (!flushTimerIsActive()) {
startFlushTimer();
PLog("Flush flushTimer set to %ds", (flushManager.getIntervalMillis() / 1000));
Expand Down Expand Up @@ -474,85 +474,9 @@ private boolean isReachable() {
return netInfo != null && netInfo.isConnectedOrConnecting();
}

/**
* Save the event queue to persistent storage.
*/
private synchronized void persistQueue() {
PLog("Persisting event queue");
ArrayList<Map<String, Object>> storedQueue = getStoredQueue();
HashSet<Map<String, Object>> hs = new HashSet<>();
hs.addAll(storedQueue);
hs.addAll(eventQueue);
storedQueue.clear();
storedQueue.addAll(hs);
persistObject(storedQueue);
}

/**
* Get the stored event queue from persistent storage.
*
* @return The stored queue of events.
*/
@NonNull
private ArrayList<Map<String, Object>> getStoredQueue() {
ArrayList<Map<String, Object>> storedQueue = null;
try {
FileInputStream fis = context.getApplicationContext().openFileInput(STORAGE_KEY);
ObjectInputStream ois = new ObjectInputStream(fis);
//noinspection unchecked
storedQueue = (ArrayList<Map<String, Object>>) ois.readObject();
ois.close();
} catch (EOFException ex) {
// Nothing to do here.
} catch (FileNotFoundException ex) {
// Nothing to do here. Means there was no saved queue.
} catch (Exception ex) {
PLog("Exception thrown during queue deserialization: %s", ex.toString());
}

if (storedQueue == null) {
storedQueue = new ArrayList<>();
}
return storedQueue;
}

void purgeEventsQueue() {
eventQueue.clear();
purgeStoredQueue();
}

/**
* Delete the stored queue from persistent storage.
*/
private void purgeStoredQueue() {
persistObject(new ArrayList<Map<String, Object>>());
}

/**
* Delete an event from the stored queue.
*/
private void expelStoredEvent() {
ArrayList<Map<String, Object>> storedQueue = getStoredQueue();
storedQueue.remove(0);
}

/**
* Persist an object to storage.
*
* @param o Object to store.
*/
private void persistObject(Object o) {
try {
FileOutputStream fos = context.getApplicationContext().openFileOutput(
STORAGE_KEY,
android.content.Context.MODE_PRIVATE
);
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(o);
oos.close();
} catch (Exception ex) {
PLog("Exception thrown during queue serialization: %s", ex.toString());
}
localStorageRepository.purgeStoredQueue();
}

/**
Expand Down Expand Up @@ -621,31 +545,14 @@ public int queueSize() {
* @return The number of events stored in persistent storage.
*/
public int storedEventsCount() {
ArrayList<Map<String, Object>> ar = getStoredQueue();
ArrayList<Map<String, Object>> ar = localStorageRepository.getStoredQueue();
return ar.size();
}

private class QueueManager extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... params) {
// if event queue is too big, push to persisted storage
if (eventQueue.size() > QUEUE_SIZE_LIMIT) {
PLog("Queue size exceeded, expelling oldest event to persistent memory");
persistQueue();
eventQueue.remove(0);
// if persisted storage is too big, expel one
if (storedEventsCount() > STORAGE_SIZE_LIMIT) {
expelStoredEvent();
}
}
return null;
}
}

private class FlushQueue extends AsyncTask<Void, Void, Void> {
@Override
protected synchronized Void doInBackground(Void... params) {
ArrayList<Map<String, Object>> storedQueue = getStoredQueue();
ArrayList<Map<String, Object>> storedQueue = localStorageRepository.getStoredQueue();
PLog("%d events in queue, %d stored events", eventQueue.size(), storedEventsCount());
// in case both queues have been flushed and app quits, don't crash
if ((eventQueue == null || eventQueue.size() == 0) && storedQueue.size() == 0) {
Expand Down
30 changes: 30 additions & 0 deletions parsely/src/main/java/com/parsely/parselyandroid/QueueManager.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.parsely.parselyandroid

import android.os.AsyncTask

@Suppress("DEPRECATION")
internal class QueueManager(
private val parselyTracker: ParselyTracker,
private val localStorageRepository: LocalStorageRepository
) : AsyncTask<Void?, Void?, Void?>() {

@Deprecated("Deprecated in Java")
override fun doInBackground(vararg params: Void?): Void? {
// if event queue is too big, push to persisted storage
if (parselyTracker.inMemoryQueue.size > QUEUE_SIZE_LIMIT) {
ParselyTracker.PLog("Queue size exceeded, expelling oldest event to persistent memory")
localStorageRepository.persistQueue(parselyTracker.inMemoryQueue)
parselyTracker.inMemoryQueue.removeAt(0)
// if persisted storage is too big, expel one
if (parselyTracker.storedEventsCount() > STORAGE_SIZE_LIMIT) {
localStorageRepository.expelStoredEvent()
}
}
return null
}

companion object {
const val QUEUE_SIZE_LIMIT = 50
const val STORAGE_SIZE_LIMIT = 100
}
}
Loading

0 comments on commit b9c449d

Please sign in to comment.