diff --git a/parsely/src/main/java/com/parsely/parselyandroid/AdvertisementIdProvider.kt b/parsely/src/main/java/com/parsely/parselyandroid/AdvertisementIdProvider.kt index e9f11ce6..87e93a84 100644 --- a/parsely/src/main/java/com/parsely/parselyandroid/AdvertisementIdProvider.kt +++ b/parsely/src/main/java/com/parsely/parselyandroid/AdvertisementIdProvider.kt @@ -3,6 +3,7 @@ package com.parsely.parselyandroid import android.content.Context import android.provider.Settings import com.google.android.gms.ads.identifier.AdvertisingIdClient +import com.parsely.parselyandroid.Logging.log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -18,7 +19,7 @@ internal class AdvertisementIdProvider( try { adKey = AdvertisingIdClient.getAdvertisingIdInfo(context).id } catch (e: Exception) { - ParselyTracker.PLog("No Google play services or error!") + log("No Google play services or error!") } } } @@ -40,7 +41,7 @@ internal class AndroidIdProvider(private val context: Context) : IdProvider { } catch (ex: Exception) { null } - ParselyTracker.PLog(String.format("Android ID: %s", uuid)) + log(String.format("Android ID: %s", uuid)) return uuid } } diff --git a/parsely/src/main/java/com/parsely/parselyandroid/ConnectivityStatusProvider.kt b/parsely/src/main/java/com/parsely/parselyandroid/ConnectivityStatusProvider.kt new file mode 100644 index 00000000..f7d90842 --- /dev/null +++ b/parsely/src/main/java/com/parsely/parselyandroid/ConnectivityStatusProvider.kt @@ -0,0 +1,22 @@ +package com.parsely.parselyandroid + +import android.content.Context +import android.net.ConnectivityManager + +internal interface ConnectivityStatusProvider { + /** + * @return Whether the network is accessible. + */ + fun isReachable(): Boolean +} + +internal class AndroidConnectivityStatusProvider(private val context: Context): ConnectivityStatusProvider { + + override fun isReachable(): Boolean { + val cm = context.getSystemService( + Context.CONNECTIVITY_SERVICE + ) as ConnectivityManager + val netInfo = cm.activeNetworkInfo + return netInfo != null && netInfo.isConnectedOrConnecting + } +} diff --git a/parsely/src/main/java/com/parsely/parselyandroid/DeviceInfoRepository.kt b/parsely/src/main/java/com/parsely/parselyandroid/DeviceInfoRepository.kt index fc899215..61abdc4f 100644 --- a/parsely/src/main/java/com/parsely/parselyandroid/DeviceInfoRepository.kt +++ b/parsely/src/main/java/com/parsely/parselyandroid/DeviceInfoRepository.kt @@ -1,6 +1,7 @@ package com.parsely.parselyandroid import android.os.Build +import com.parsely.parselyandroid.Logging.log internal interface DeviceInfoRepository{ fun collectDeviceInfo(): Map @@ -34,12 +35,12 @@ internal open class AndroidDeviceInfoRepository( val adKey = advertisementIdProvider.provide() val androidId = androidIdProvider.provide() - ParselyTracker.PLog("adkey is: %s, uuid is %s", adKey, androidId) + log("adkey is: %s, uuid is %s", adKey, androidId) return if (adKey != null) { adKey } else { - ParselyTracker.PLog("falling back to device uuid") + log("falling back to device uuid") androidId .orEmpty() } } diff --git a/parsely/src/main/java/com/parsely/parselyandroid/EngagementManager.kt b/parsely/src/main/java/com/parsely/parselyandroid/EngagementManager.kt index 2f1dc620..750b2a65 100644 --- a/parsely/src/main/java/com/parsely/parselyandroid/EngagementManager.kt +++ b/parsely/src/main/java/com/parsely/parselyandroid/EngagementManager.kt @@ -1,6 +1,6 @@ package com.parsely.parselyandroid -import kotlin.time.Duration +import com.parsely.parselyandroid.Logging.log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -62,7 +62,7 @@ internal class EngagementManager( val event: MutableMap = HashMap( baseEvent ) - ParselyTracker.PLog(String.format("Enqueuing %s event.", event["action"])) + log(String.format("Enqueuing %s event.", event["action"])) // Update `ts` for the event since it's happening right now. val baseEventData = (event["data"] as Map?)!! diff --git a/parsely/src/main/java/com/parsely/parselyandroid/EventsBuilder.java b/parsely/src/main/java/com/parsely/parselyandroid/EventsBuilder.java index b70c1aec..c4bfab2a 100644 --- a/parsely/src/main/java/com/parsely/parselyandroid/EventsBuilder.java +++ b/parsely/src/main/java/com/parsely/parselyandroid/EventsBuilder.java @@ -1,8 +1,6 @@ package com.parsely.parselyandroid; -import static com.parsely.parselyandroid.ParselyTracker.PLog; - -import android.content.Context; +import static com.parsely.parselyandroid.Logging.log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -44,7 +42,7 @@ Map buildEvent( Map extraData, @Nullable String uuid ) { - PLog("buildEvent called for %s/%s", action, url); + log("buildEvent called for %s/%s", action, url); Calendar now = Calendar.getInstance(TimeZone.getTimeZone("UTC")); diff --git a/parsely/src/main/java/com/parsely/parselyandroid/FlushQueue.kt b/parsely/src/main/java/com/parsely/parselyandroid/FlushQueue.kt index 4a989b95..01de870c 100644 --- a/parsely/src/main/java/com/parsely/parselyandroid/FlushQueue.kt +++ b/parsely/src/main/java/com/parsely/parselyandroid/FlushQueue.kt @@ -1,6 +1,7 @@ package com.parsely.parselyandroid import com.parsely.parselyandroid.JsonSerializer.toParselyEventsPayload +import com.parsely.parselyandroid.Logging.log import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex @@ -10,12 +11,17 @@ internal class FlushQueue( private val flushManager: FlushManager, private val repository: QueueRepository, private val restClient: RestClient, - private val scope: CoroutineScope + private val scope: CoroutineScope, + private val connectivityStatusProvider: ConnectivityStatusProvider ) { private val mutex = Mutex() operator fun invoke(skipSendingEvents: Boolean) { + if (!connectivityStatusProvider.isReachable()) { + log("Network unreachable. Not flushing.") + return + } scope.launch { mutex.withLock { val eventsToSend = repository.getStoredQueue() @@ -26,23 +32,23 @@ internal class FlushQueue( } if (skipSendingEvents) { - ParselyTracker.PLog("Debug mode on. Not sending to Parse.ly. Otherwise, would sent ${eventsToSend.size} events") + log("Debug mode on. Not sending to Parse.ly. Otherwise, would sent ${eventsToSend.size} events") repository.remove(eventsToSend) return@launch } - ParselyTracker.PLog("Sending request with %d events", eventsToSend.size) + log("Sending request with %d events", eventsToSend.size) val jsonPayload = toParselyEventsPayload(eventsToSend) - ParselyTracker.PLog("POST Data %s", jsonPayload) - ParselyTracker.PLog("Requested %s", ParselyTracker.ROOT_URL) + log("POST Data %s", jsonPayload) + log("Requested %s", ParselyTracker.ROOT_URL) restClient.send(jsonPayload) .fold( onSuccess = { - ParselyTracker.PLog("Pixel request success") + log("Pixel request success") repository.remove(eventsToSend) }, onFailure = { - ParselyTracker.PLog("Pixel request exception") - ParselyTracker.PLog(it.toString()) + log("Pixel request exception") + log(it.toString()) } ) } diff --git a/parsely/src/main/java/com/parsely/parselyandroid/InMemoryBuffer.kt b/parsely/src/main/java/com/parsely/parselyandroid/InMemoryBuffer.kt index 619e993d..63f6e70a 100644 --- a/parsely/src/main/java/com/parsely/parselyandroid/InMemoryBuffer.kt +++ b/parsely/src/main/java/com/parsely/parselyandroid/InMemoryBuffer.kt @@ -1,5 +1,6 @@ package com.parsely.parselyandroid +import com.parsely.parselyandroid.Logging.log import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay @@ -22,7 +23,7 @@ internal class InMemoryBuffer( while (isActive) { mutex.withLock { if (buffer.isNotEmpty()) { - ParselyTracker.PLog("Persisting ${buffer.size} events") + log("Persisting ${buffer.size} events") localStorageRepository.insertEvents(buffer) buffer.clear() } @@ -35,7 +36,7 @@ internal class InMemoryBuffer( fun add(event: Map) { coroutineScope.launch { mutex.withLock { - ParselyTracker.PLog("Event added to buffer") + log("Event added to buffer") buffer.add(event) onEventAddedListener() } diff --git a/parsely/src/main/java/com/parsely/parselyandroid/LocalStorageRepository.kt b/parsely/src/main/java/com/parsely/parselyandroid/LocalStorageRepository.kt index 1f1f28fc..f8dc30fe 100644 --- a/parsely/src/main/java/com/parsely/parselyandroid/LocalStorageRepository.kt +++ b/parsely/src/main/java/com/parsely/parselyandroid/LocalStorageRepository.kt @@ -1,6 +1,7 @@ package com.parsely.parselyandroid import android.content.Context +import com.parsely.parselyandroid.Logging.log import java.io.EOFException import java.io.FileNotFoundException import java.io.ObjectInputStream @@ -34,7 +35,7 @@ internal class LocalStorageRepository(private val context: Context) : QueueRepos oos.close() fos.close() } catch (ex: Exception) { - ParselyTracker.PLog("Exception thrown during queue serialization: %s", ex.toString()) + log("Exception thrown during queue serialization: %s", ex.toString()) } } @@ -52,7 +53,7 @@ internal class LocalStorageRepository(private val context: Context) : QueueRepos } catch (ex: FileNotFoundException) { // Nothing to do here. Means there was no saved queue. } catch (ex: Exception) { - ParselyTracker.PLog( + log( "Exception thrown during queue deserialization: %s", ex.toString() ) diff --git a/parsely/src/main/java/com/parsely/parselyandroid/Logging.kt b/parsely/src/main/java/com/parsely/parselyandroid/Logging.kt new file mode 100644 index 00000000..4ded5788 --- /dev/null +++ b/parsely/src/main/java/com/parsely/parselyandroid/Logging.kt @@ -0,0 +1,17 @@ +package com.parsely.parselyandroid + +import java.util.Formatter + +object Logging { + + /** + * Log a message to the console. + */ + @JvmStatic + fun log(logString: String, vararg objects: Any?) { + if (logString == "") { + return + } + println(Formatter().format("[Parsely] $logString", *objects).toString()) + } +} diff --git a/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.java b/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.java index 0afd9471..5a741d8d 100644 --- a/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.java +++ b/parsely/src/main/java/com/parsely/parselyandroid/ParselyTracker.java @@ -16,9 +16,9 @@ package com.parsely.parselyandroid; +import static com.parsely.parselyandroid.Logging.log; + import android.content.Context; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -26,7 +26,6 @@ import androidx.lifecycle.LifecycleEventObserver; import androidx.lifecycle.ProcessLifecycleOwner; -import java.util.Formatter; import java.util.Map; import java.util.UUID; @@ -47,7 +46,6 @@ public class ParselyTracker { // static final String ROOT_URL = "http://10.0.2.2:5001/".intern(); // emulator localhost static final String ROOT_URL = "https://p1.parsely.com/".intern(); private boolean isDebug; - private final Context context; private final FlushManager flushManager; private EngagementManager engagementManager, videoEngagementManager; @Nullable @@ -59,8 +57,6 @@ public class ParselyTracker { @NonNull private final HeartbeatIntervalCalculator intervalCalculator; @NonNull - private final LocalStorageRepository localStorageRepository; - @NonNull private final InMemoryBuffer inMemoryBuffer; @NonNull private final FlushQueue flushQueue; @@ -69,13 +65,13 @@ public class ParselyTracker { * Create a new ParselyTracker instance. */ protected ParselyTracker(String siteId, int flushInterval, Context c) { - context = c.getApplicationContext(); + Context context = c.getApplicationContext(); eventsBuilder = new EventsBuilder( new AndroidDeviceInfoRepository( new AdvertisementIdProvider(context, ParselyCoroutineScopeKt.getSdkScope()), new AndroidIdProvider(context) ), siteId); - localStorageRepository = new LocalStorageRepository(context); + LocalStorageRepository localStorageRepository = new LocalStorageRepository(context); flushManager = new ParselyFlushManager(new Function0() { @Override public Unit invoke() { @@ -87,11 +83,11 @@ public Unit invoke() { inMemoryBuffer = new InMemoryBuffer(ParselyCoroutineScopeKt.getSdkScope(), localStorageRepository, () -> { if (!flushTimerIsActive()) { startFlushTimer(); - PLog("Flush flushTimer set to %ds", (flushManager.getIntervalMillis() / 1000)); + log("Flush flushTimer set to %ds", (flushManager.getIntervalMillis() / 1000)); } return Unit.INSTANCE; }); - flushQueue = new FlushQueue(flushManager, localStorageRepository, new ParselyAPIConnection(ROOT_URL + "mobileproxy"), ParselyCoroutineScopeKt.getSdkScope()); + flushQueue = new FlushQueue(flushManager, localStorageRepository, new ParselyAPIConnection(ROOT_URL + "mobileproxy"), ParselyCoroutineScopeKt.getSdkScope(), new AndroidConnectivityStatusProvider(context)); clock = new Clock(); intervalCalculator = new HeartbeatIntervalCalculator(clock); @@ -147,31 +143,23 @@ public static ParselyTracker sharedInstance(String siteId, int flushInterval, Co return instance; } - /** - * Log a message to the console. - */ - static void PLog(String logString, Object... objects) { - if (logString.equals("")) { - return; - } - System.out.println(new Formatter().format("[Parsely] " + logString, objects).toString()); - } - /** * Get the heartbeat interval * * @return The base engagement tracking interval. */ - public double getEngagementInterval() { + @Nullable + public Double getEngagementInterval() { if (engagementManager == null) { - return -1; + return null; } return engagementManager.getIntervalMillis(); } - public double getVideoEngagementInterval() { + @Nullable + public Double getVideoEngagementInterval() { if (videoEngagementManager == null) { - return -1; + return null; } return videoEngagementManager.getIntervalMillis(); } @@ -203,15 +191,6 @@ public long getFlushInterval() { return flushManager.getIntervalMillis() / 1000; } - /** - * Getter for isDebug - * - * @return Whether debug mode is active. - */ - public boolean getDebug() { - return isDebug; - } - /** * Set a debug flag which will prevent data from being sent to Parse.ly *

@@ -222,7 +201,7 @@ public boolean getDebug() { */ public void setDebug(boolean debug) { isDebug = debug; - PLog("Debugging is now set to " + isDebug); + log("Debugging is now set to " + isDebug); } /** @@ -243,7 +222,8 @@ public void trackPageview( @Nullable ParselyMetadata urlMetadata, @Nullable Map extraData) { if (url.equals("")) { - throw new IllegalArgumentException("url cannot be null or empty."); + log("url cannot be empty"); + return; } // Blank urlref is better than null @@ -283,7 +263,8 @@ public void startEngagement( final @Nullable Map extraData ) { if (url.equals("")) { - throw new IllegalArgumentException("url cannot be null or empty."); + log("url cannot be empty"); + return; } // Blank urlref is better than null @@ -341,7 +322,8 @@ public void trackPlay( @NonNull ParselyVideoMetadata videoMetadata, @Nullable Map extraData) { if (url.equals("")) { - throw new IllegalArgumentException("url cannot be null or empty."); + log("url cannot be empty"); + return; } // Blank urlref is better than null @@ -434,18 +416,6 @@ public void flushEventQueue() { // no-op } - /** - * Returns whether the network is accessible and Parsely is reachable. - * - * @return Whether the network is accessible and Parsely is reachable. - */ - private boolean isReachable() { - ConnectivityManager cm = (ConnectivityManager) context.getSystemService( - Context.CONNECTIVITY_SERVICE); - NetworkInfo netInfo = cm.getActiveNetworkInfo(); - return netInfo != null && netInfo.isConnectedOrConnecting(); - } - /** * Start the timer to flush events to Parsely. *

@@ -472,10 +442,6 @@ private String generatePixelId() { } void flushEvents() { - if (!isReachable()) { - PLog("Network unreachable. Not flushing."); - return; - } flushQueue.invoke(isDebug); } } diff --git a/parsely/src/test/java/com/parsely/parselyandroid/FlushQueueTest.kt b/parsely/src/test/java/com/parsely/parselyandroid/FlushQueueTest.kt index e49605b6..0dea4b1a 100644 --- a/parsely/src/test/java/com/parsely/parselyandroid/FlushQueueTest.kt +++ b/parsely/src/test/java/com/parsely/parselyandroid/FlushQueueTest.kt @@ -20,7 +20,8 @@ class FlushQueueTest { FakeFlushManager(), FakeRepository(), FakeRestClient(), - this + this, + FakeConnectivityStatusProvider() ) // when @@ -45,7 +46,8 @@ class FlushQueueTest { FakeFlushManager(), repository, parselyAPIConnection, - this + this, + FakeConnectivityStatusProvider() ) // when @@ -67,7 +69,8 @@ class FlushQueueTest { FakeFlushManager(), repository, FakeRestClient(), - this + this, + FakeConnectivityStatusProvider() ) // when @@ -92,7 +95,8 @@ class FlushQueueTest { FakeFlushManager(), repository, parselyAPIConnection, - this + this, + FakeConnectivityStatusProvider() ) // when @@ -118,7 +122,8 @@ class FlushQueueTest { flushManager, repository, parselyAPIConnection, - this + this, + FakeConnectivityStatusProvider() ) // when @@ -146,7 +151,8 @@ class FlushQueueTest { flushManager, repository, parselyAPIConnection, - this + this, + FakeConnectivityStatusProvider() ) // when @@ -165,7 +171,8 @@ class FlushQueueTest { flushManager, FakeRepository(), FakeRestClient(), - this + this, + FakeConnectivityStatusProvider() ) // when @@ -176,6 +183,30 @@ class FlushQueueTest { assertThat(flushManager.stopped).isTrue() } + @Test + fun `given non-empty local storage, when flushing queue with no internet connection, then events are not sent and not removed from local storage`() = + runTest { + // given + val repository = FakeRepository().apply { + insertEvents(listOf(mapOf("test" to 123))) + } + val sut = FlushQueue( + FakeFlushManager(), + repository, + FakeRestClient(), + this, + FakeConnectivityStatusProvider().apply { reachable = false } + ) + + // when + sut.invoke(false) + runCurrent() + + // then + assertThat(repository.getStoredQueue()).isNotEmpty + } + + private class FakeFlushManager : FlushManager { var stopped = false override fun start() { @@ -216,4 +247,9 @@ class FlushQueueTest { return nextResult!! } } + + private class FakeConnectivityStatusProvider : ConnectivityStatusProvider { + var reachable = true + override fun isReachable() = reachable + } }