Skip to content

Commit 62cfa12

Browse files
authored
Merge pull request #92 from Parsely/coroutines-send-event
Coroutines: send events flow
2 parents 845947b + 784a021 commit 62cfa12

File tree

15 files changed

+453
-352
lines changed

15 files changed

+453
-352
lines changed

example/src/main/java/com/example/MainActivity.java

+3-18
Original file line numberDiff line numberDiff line change
@@ -30,33 +30,18 @@ protected void onCreate(Bundle savedInstanceState) {
3030
// Set debugging to true so we don't actually send things to Parse.ly
3131
ParselyTracker.sharedInstance().setDebug(true);
3232

33-
final TextView queueView = (TextView) findViewById(R.id.queue_size);
34-
queueView.setText(String.format("Queued events: %d", ParselyTracker.sharedInstance().queueSize()));
35-
36-
final TextView storedView = (TextView) findViewById(R.id.stored_size);
37-
storedView.setText(String.format("Stored events: %d", ParselyTracker.sharedInstance().storedEventsCount()));
38-
3933
final TextView intervalView = (TextView) findViewById(R.id.interval);
40-
storedView.setText(String.format("Flush interval: %d", ParselyTracker.sharedInstance().getFlushInterval()));
4134

4235
updateEngagementStrings();
4336

44-
final TextView views[] = new TextView[3];
45-
views[0] = queueView;
46-
views[1] = storedView;
47-
views[2] = intervalView;
37+
final TextView views[] = new TextView[1];
38+
views[0] = intervalView;
4839

4940
final Handler mHandler = new Handler() {
5041
@Override
5142
public void handleMessage(Message msg) {
5243
TextView[] v = (TextView[]) msg.obj;
53-
TextView qView = v[0];
54-
qView.setText(String.format("Queued events: %d", ParselyTracker.sharedInstance().queueSize()));
55-
56-
TextView sView = v[1];
57-
sView.setText(String.format("Stored events: %d", ParselyTracker.sharedInstance().storedEventsCount()));
58-
59-
TextView iView = v[2];
44+
TextView iView = v[0];
6045
if (ParselyTracker.sharedInstance().flushTimerIsActive()) {
6146
iView.setText(String.format("Flush Interval: %d", ParselyTracker.sharedInstance().getFlushInterval()));
6247
} else {

example/src/main/res/layout/activity_main.xml

+2-17
Original file line numberDiff line numberDiff line change
@@ -68,26 +68,11 @@
6868
android:onClick="trackReset"
6969
android:text="@string/button_reset_video" />
7070

71-
<TextView
72-
android:id="@+id/queue_size"
73-
android:layout_width="wrap_content"
74-
android:layout_height="wrap_content"
75-
android:layout_below="@+id/reset_video_button"
76-
android:layout_centerHorizontal="true"
77-
android:text="Queued events: 0" />
78-
79-
<TextView android:id="@+id/stored_size"
80-
android:layout_width="wrap_content"
81-
android:layout_height="wrap_content"
82-
android:layout_centerHorizontal="true"
83-
android:layout_below="@id/queue_size"
84-
android:text="Stored events: 0"/>
85-
8671
<TextView android:id="@+id/interval"
8772
android:layout_width="wrap_content"
8873
android:layout_height="wrap_content"
8974
android:layout_centerHorizontal="true"
90-
android:layout_below="@id/stored_size"
75+
android:layout_below="@id/reset_video_button"
9176
android:text="Flush timer inactive"/>
9277

9378
<TextView
@@ -107,4 +92,4 @@
10792
android:text="Video is inactive." />
10893

10994

110-
</RelativeLayout>
95+
</RelativeLayout>

parsely/src/androidTest/java/com/parsely/parselyandroid/FunctionalTests.kt

+4-5
Original file line numberDiff line numberDiff line change
@@ -214,13 +214,12 @@ class FunctionalTests {
214214
activity: Activity,
215215
flushInterval: Duration = defaultFlushInterval
216216
): ParselyTracker {
217+
val field: Field = ParselyTracker::class.java.getDeclaredField("ROOT_URL")
218+
field.isAccessible = true
219+
field.set(this, url)
217220
return ParselyTracker.sharedInstance(
218221
siteId, flushInterval.inWholeSeconds.toInt(), activity.application
219-
).apply {
220-
val f: Field = this::class.java.getDeclaredField("ROOT_URL")
221-
f.isAccessible = true
222-
f.set(this, url)
223-
}
222+
)
224223
}
225224

226225
private companion object {

parsely/src/main/java/com/parsely/parselyandroid/FlushManager.kt

+17-8
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,35 @@ import kotlinx.coroutines.launch
1313
* Handles stopping and starting the flush timer. The flush timer
1414
* controls how often we send events to Parse.ly servers.
1515
*/
16-
internal class FlushManager(
17-
private val parselyTracker: ParselyTracker,
18-
val intervalMillis: Long,
16+
internal interface FlushManager {
17+
fun start()
18+
fun stop()
19+
val isRunning: Boolean
20+
val intervalMillis: Long
21+
}
22+
23+
internal class ParselyFlushManager(
24+
private val onFlush: () -> Unit,
25+
override val intervalMillis: Long,
1926
private val coroutineScope: CoroutineScope
20-
) {
27+
) : FlushManager {
2128
private var job: Job? = null
2229

23-
fun start() {
30+
override fun start() {
2431
if (job?.isActive == true) return
2532

2633
job = coroutineScope.launch {
2734
while (isActive) {
2835
delay(intervalMillis)
29-
parselyTracker.flushEvents()
36+
onFlush.invoke()
3037
}
3138
}
3239
}
3340

34-
fun stop() = job?.cancel()
41+
override fun stop() {
42+
job?.cancel()
43+
}
3544

36-
val isRunning: Boolean
45+
override val isRunning: Boolean
3746
get() = job?.isActive ?: false
3847
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.parsely.parselyandroid
2+
3+
import com.parsely.parselyandroid.JsonSerializer.toParselyEventsPayload
4+
import kotlinx.coroutines.CoroutineScope
5+
import kotlinx.coroutines.launch
6+
import kotlinx.coroutines.sync.Mutex
7+
import kotlinx.coroutines.sync.withLock
8+
9+
internal class FlushQueue(
10+
private val flushManager: FlushManager,
11+
private val repository: QueueRepository,
12+
private val restClient: RestClient,
13+
private val scope: CoroutineScope
14+
) {
15+
16+
private val mutex = Mutex()
17+
18+
operator fun invoke(skipSendingEvents: Boolean) {
19+
scope.launch {
20+
mutex.withLock {
21+
val eventsToSend = repository.getStoredQueue()
22+
23+
if (eventsToSend.isEmpty()) {
24+
flushManager.stop()
25+
return@launch
26+
}
27+
28+
if (skipSendingEvents) {
29+
ParselyTracker.PLog("Debug mode on. Not sending to Parse.ly. Otherwise, would sent ${eventsToSend.size} events")
30+
repository.remove(eventsToSend)
31+
return@launch
32+
}
33+
ParselyTracker.PLog("Sending request with %d events", eventsToSend.size)
34+
val jsonPayload = toParselyEventsPayload(eventsToSend)
35+
ParselyTracker.PLog("POST Data %s", jsonPayload)
36+
ParselyTracker.PLog("Requested %s", ParselyTracker.ROOT_URL)
37+
restClient.send(jsonPayload)
38+
.fold(
39+
onSuccess = {
40+
ParselyTracker.PLog("Pixel request success")
41+
repository.remove(eventsToSend)
42+
},
43+
onFailure = {
44+
ParselyTracker.PLog("Pixel request exception")
45+
ParselyTracker.PLog(it.toString())
46+
}
47+
)
48+
}
49+
}
50+
}
51+
}

parsely/src/main/java/com/parsely/parselyandroid/InMemoryBuffer.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import kotlinx.coroutines.sync.withLock
1010

1111
internal class InMemoryBuffer(
1212
private val coroutineScope: CoroutineScope,
13-
private val localStorageRepository: LocalStorageRepository,
13+
private val localStorageRepository: QueueRepository,
1414
private val onEventAddedListener: () -> Unit,
1515
) {
1616

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.parsely.parselyandroid
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper
4+
import java.io.IOException
5+
import java.io.StringWriter
6+
7+
internal object JsonSerializer {
8+
9+
fun toParselyEventsPayload(eventsToSend: List<Map<String, Any?>?>): String {
10+
val batchMap: MutableMap<String, Any> = HashMap()
11+
batchMap["events"] = eventsToSend
12+
return toJson(batchMap).orEmpty()
13+
}
14+
/**
15+
* Encode an event Map as JSON.
16+
*
17+
* @param map The Map object to encode as JSON.
18+
* @return The JSON-encoded value of `map`.
19+
*/
20+
private fun toJson(map: Map<String, Any>): String? {
21+
val mapper = ObjectMapper()
22+
var ret: String? = null
23+
try {
24+
val strWriter = StringWriter()
25+
mapper.writeValue(strWriter, map)
26+
ret = strWriter.toString()
27+
} catch (e: IOException) {
28+
e.printStackTrace()
29+
}
30+
return ret
31+
}
32+
}

parsely/src/main/java/com/parsely/parselyandroid/LocalStorageRepository.kt

+21-25
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,16 @@ import java.io.EOFException
55
import java.io.FileNotFoundException
66
import java.io.ObjectInputStream
77
import java.io.ObjectOutputStream
8-
import kotlinx.coroutines.currentCoroutineContext
98
import kotlinx.coroutines.sync.Mutex
109
import kotlinx.coroutines.sync.withLock
1110

12-
internal open class LocalStorageRepository(private val context: Context) {
11+
internal interface QueueRepository {
12+
suspend fun remove(toRemove: List<Map<String, Any?>?>)
13+
suspend fun getStoredQueue(): ArrayList<Map<String, Any?>?>
14+
suspend fun insertEvents(toInsert: List<Map<String, Any?>?>)
15+
}
16+
17+
internal class LocalStorageRepository(private val context: Context) : QueueRepository {
1318

1419
private val mutex = Mutex()
1520

@@ -33,23 +38,7 @@ internal open class LocalStorageRepository(private val context: Context) {
3338
}
3439
}
3540

36-
/**
37-
* Delete the stored queue from persistent storage.
38-
*/
39-
fun purgeStoredQueue() {
40-
persistObject(ArrayList<Map<String, Any>>())
41-
}
42-
43-
fun remove(toRemove: List<Map<String, Any>>) {
44-
persistObject(getStoredQueue() - toRemove.toSet())
45-
}
46-
47-
/**
48-
* Get the stored event queue from persistent storage.
49-
*
50-
* @return The stored queue of events.
51-
*/
52-
open fun getStoredQueue(): ArrayList<Map<String, Any?>?> {
41+
private fun getInternalStoredQueue(): ArrayList<Map<String, Any?>?> {
5342
var storedQueue: ArrayList<Map<String, Any?>?> = ArrayList()
5443
try {
5544
val fis = context.applicationContext.openFileInput(STORAGE_KEY)
@@ -71,19 +60,26 @@ internal open class LocalStorageRepository(private val context: Context) {
7160
return storedQueue
7261
}
7362

63+
override suspend fun remove(toRemove: List<Map<String, Any?>?>) = mutex.withLock {
64+
val storedEvents = getInternalStoredQueue()
65+
persistObject(storedEvents - toRemove.toSet())
66+
}
67+
7468
/**
75-
* Delete an event from the stored queue.
69+
* Get the stored event queue from persistent storage.
70+
*
71+
* @return The stored queue of events.
7672
*/
77-
open fun expelStoredEvent() {
78-
val storedQueue = getStoredQueue()
79-
storedQueue.removeAt(0)
73+
override suspend fun getStoredQueue(): ArrayList<Map<String, Any?>?> = mutex.withLock {
74+
getInternalStoredQueue()
8075
}
8176

8277
/**
8378
* Save the event queue to persistent storage.
8479
*/
85-
open suspend fun insertEvents(toInsert: List<Map<String, Any?>?>) = mutex.withLock {
86-
persistObject(ArrayList((toInsert + getStoredQueue()).distinct()))
80+
override suspend fun insertEvents(toInsert: List<Map<String, Any?>?>) = mutex.withLock {
81+
val storedEvents = getInternalStoredQueue()
82+
persistObject(ArrayList((toInsert + storedEvents).distinct()))
8783
}
8884

8985
companion object {

parsely/src/main/java/com/parsely/parselyandroid/ParselyAPIConnection.kt

+16-34
Original file line numberDiff line numberDiff line change
@@ -13,50 +13,32 @@
1313
See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
16-
@file:Suppress("DEPRECATION")
1716
package com.parsely.parselyandroid
1817

19-
import android.os.AsyncTask
2018
import java.net.HttpURLConnection
2119
import java.net.URL
2220

23-
internal class ParselyAPIConnection(private val tracker: ParselyTracker) : AsyncTask<String?, Exception?, Void?>() {
24-
private var exception: Exception? = null
21+
internal interface RestClient {
22+
suspend fun send(payload: String): Result<Unit>
23+
}
2524

26-
@Deprecated("Deprecated in Java")
27-
override fun doInBackground(vararg data: String?): Void? {
25+
internal class ParselyAPIConnection(private val url: String) : RestClient {
26+
override suspend fun send(payload: String): Result<Unit> {
2827
var connection: HttpURLConnection? = null
2928
try {
30-
if (data.size == 1) { // non-batched (since no post data is included)
31-
connection = URL(data[0]).openConnection() as HttpURLConnection
32-
connection.inputStream
33-
} else if (data.size == 2) { // batched (post data included)
34-
connection = URL(data[0]).openConnection() as HttpURLConnection
35-
connection.doOutput = true // Triggers POST (aka silliest interface ever)
36-
connection.setRequestProperty("Content-Type", "application/json")
37-
val output = connection.outputStream
38-
output.write(data[1]?.toByteArray())
39-
output.close()
40-
connection.inputStream
41-
}
29+
connection = URL(url).openConnection() as HttpURLConnection
30+
connection.doOutput = true
31+
connection.setRequestProperty("Content-Type", "application/json")
32+
val output = connection.outputStream
33+
output.write(payload.toByteArray())
34+
output.close()
35+
connection.inputStream
4236
} catch (ex: Exception) {
43-
exception = ex
37+
return Result.failure(ex)
38+
} finally {
39+
connection?.disconnect()
4440
}
45-
return null
46-
}
4741

48-
@Deprecated("Deprecated in Java")
49-
override fun onPostExecute(result: Void?) {
50-
if (exception != null) {
51-
ParselyTracker.PLog("Pixel request exception")
52-
ParselyTracker.PLog(exception.toString())
53-
} else {
54-
ParselyTracker.PLog("Pixel request success")
55-
56-
// only purge the queue if the request was successful
57-
tracker.purgeEventsQueue()
58-
ParselyTracker.PLog("Event queue empty, flush timer cleared.")
59-
tracker.stopFlushTimer()
60-
}
42+
return Result.success(Unit)
6143
}
6244
}

0 commit comments

Comments
 (0)