From 7d97bb0d7adf2a5c2184992c90df7be3679acb94 Mon Sep 17 00:00:00 2001 From: Xinyi Ye Date: Wed, 27 Dec 2023 11:46:23 -0800 Subject: [PATCH] feat: offline support Save events on offline and send out when back to online automatically --- android/src/main/AndroidManifest.xml | 1 + .../java/com/amplitude/android/Amplitude.kt | 61 +- .../AndroidNetworkConnectivityChecker.kt | 46 + .../utilities/AndroidNetworkListener.kt | 97 ++ .../android/AmplitudeRobolectricTests.kt | 5 +- .../amplitude/android/AmplitudeSessionTest.kt | 773 ++++++++-------- .../com/amplitude/android/AmplitudeTest.kt | 190 ++-- .../plugins/AndroidLifecyclePluginTest.kt | 831 +++++++++--------- .../amplitude/core/platform/EventPipeline.kt | 14 +- .../platform/plugins/AmplitudeDestination.kt | 6 +- .../core/platform/EventPipelineTest.kt | 74 ++ 11 files changed, 1219 insertions(+), 879 deletions(-) create mode 100644 android/src/main/java/com/amplitude/android/utilities/AndroidNetworkConnectivityChecker.kt create mode 100644 android/src/main/java/com/amplitude/android/utilities/AndroidNetworkListener.kt create mode 100644 core/src/test/kotlin/com/amplitude/core/platform/EventPipelineTest.kt diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index b8490463..a4019a3b 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/android/src/main/java/com/amplitude/android/Amplitude.kt b/android/src/main/java/com/amplitude/android/Amplitude.kt index 79a5ff74..60e5b772 100644 --- a/android/src/main/java/com/amplitude/android/Amplitude.kt +++ b/android/src/main/java/com/amplitude/android/Amplitude.kt @@ -1,5 +1,6 @@ package com.amplitude.android +import AndroidNetworkListener import android.content.Context import com.amplitude.android.migration.ApiKeyStorageMigration import com.amplitude.android.migration.RemnantDataMigration @@ -7,6 +8,7 @@ import com.amplitude.android.plugins.AnalyticsConnectorIdentityPlugin import com.amplitude.android.plugins.AnalyticsConnectorPlugin import com.amplitude.android.plugins.AndroidContextPlugin import com.amplitude.android.plugins.AndroidLifecyclePlugin +import com.amplitude.android.utilities.AndroidNetworkConnectivityChecker import com.amplitude.core.Amplitude import com.amplitude.core.events.BaseEvent import com.amplitude.core.platform.plugins.AmplitudeDestination @@ -16,11 +18,21 @@ import com.amplitude.id.IdentityConfiguration import kotlinx.coroutines.launch open class Amplitude( - configuration: Configuration -) : Amplitude(configuration) { - + configuration: Configuration, +) : Amplitude(configuration), AndroidNetworkListener.NetworkChangeCallback { internal var inForeground = false private lateinit var androidContextPlugin: AndroidContextPlugin + private var networkListener: AndroidNetworkListener + private val networkChangeHandler = + object : AndroidNetworkListener.NetworkChangeCallback { + override fun onNetworkAvailable() { + flush() + } + + override fun onNetworkUnavailable() { + // Nothing to do so far + } + } val sessionId: Long get() { @@ -29,6 +41,9 @@ open class Amplitude( init { registerShutdownHook() + networkListener = AndroidNetworkListener((this.configuration as Configuration).context) + networkListener.setNetworkChangeCallback(networkChangeHandler) + networkListener.startListening() } override fun createTimeline(): Timeline { @@ -37,14 +52,18 @@ open class Amplitude( override fun createIdentityConfiguration(): IdentityConfiguration { val configuration = configuration as Configuration - val storageDirectory = configuration.context.getDir("${FileStorage.STORAGE_PREFIX}-${configuration.instanceName}", Context.MODE_PRIVATE) + val storageDirectory = + configuration.context.getDir( + "${FileStorage.STORAGE_PREFIX}-${configuration.instanceName}", + Context.MODE_PRIVATE, + ) return IdentityConfiguration( instanceName = configuration.instanceName, apiKey = configuration.apiKey, identityStorageProvider = configuration.identityStorageProvider, storageDirectory = storageDirectory, - logger = configuration.loggerProvider.getLogger(this) + logger = configuration.loggerProvider.getLogger(this), ) } @@ -62,7 +81,7 @@ open class Amplitude( add(AndroidLifecyclePlugin()) add(AnalyticsConnectorIdentityPlugin()) add(AnalyticsConnectorPlugin()) - add(AmplitudeDestination()) + add(AmplitudeDestination(AndroidNetworkConnectivityChecker(this.configuration.context, this.logger))) (timeline as Timeline).start() } @@ -113,11 +132,14 @@ open class Amplitude( } private fun registerShutdownHook() { - Runtime.getRuntime().addShutdownHook(object : Thread() { - override fun run() { - (this@Amplitude.timeline as Timeline).stop() - } - }) + Runtime.getRuntime().addShutdownHook( + object : Thread() { + override fun run() { + (this@Amplitude.timeline as Timeline).stop() + (this@Amplitude.networkListener as AndroidNetworkListener).stopListening() + } + }, + ) } companion object { @@ -125,6 +147,7 @@ open class Amplitude( * The event type for start session events. */ const val START_SESSION_EVENT = "session_start" + /** * The event type for end session events. */ @@ -134,12 +157,22 @@ open class Amplitude( * The event type for dummy enter foreground events. */ internal const val DUMMY_ENTER_FOREGROUND_EVENT = "dummy_enter_foreground" + /** * The event type for dummy exit foreground events. */ internal const val DUMMY_EXIT_FOREGROUND_EVENT = "dummy_exit_foreground" } + + override fun onNetworkAvailable() { + networkChangeHandler.onNetworkAvailable() + } + + override fun onNetworkUnavailable() { + networkChangeHandler.onNetworkUnavailable() + } } + /** * constructor function to build amplitude in dsl format with config options * Usage: Amplitude("123", context) { @@ -153,7 +186,11 @@ open class Amplitude( * @param configs Configuration * @return Amplitude Android Instance */ -fun Amplitude(apiKey: String, context: Context, configs: Configuration.() -> Unit): com.amplitude.android.Amplitude { +fun Amplitude( + apiKey: String, + context: Context, + configs: Configuration.() -> Unit, +): com.amplitude.android.Amplitude { val config = Configuration(apiKey, context) configs.invoke(config) return com.amplitude.android.Amplitude(config) diff --git a/android/src/main/java/com/amplitude/android/utilities/AndroidNetworkConnectivityChecker.kt b/android/src/main/java/com/amplitude/android/utilities/AndroidNetworkConnectivityChecker.kt new file mode 100644 index 00000000..9caf41ce --- /dev/null +++ b/android/src/main/java/com/amplitude/android/utilities/AndroidNetworkConnectivityChecker.kt @@ -0,0 +1,46 @@ +package com.amplitude.android.utilities + +import android.annotation.SuppressLint +import android.content.Context +import android.content.pm.PackageManager +import android.net.ConnectivityManager +import android.net.NetworkCapabilities +import android.os.Build +import com.amplitude.common.Logger +import com.amplitude.core.platform.NetworkConnectivityChecker + +class AndroidNetworkConnectivityChecker(private val context: Context, private val logger: Logger) : NetworkConnectivityChecker { + companion object { + private const val ACCESS_NETWORK_STATE = "android.permission.ACCESS_NETWORK_STATE" + } + + override suspend fun isConnected(): Boolean { + // Assume connection and proceed. + // Events will be treated like online + // regardless network connectivity + if (!hasPermission(context, ACCESS_NETWORK_STATE)) { + logger.warn("No ACCESS_NETWORK_STATE permission, offline mode is not supported.") + return true + } + + val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val network = cm.activeNetwork ?: return false + val capabilities = cm.getNetworkCapabilities(network) ?: return false + + return capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || + capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) + } else { + @SuppressLint("MissingPermission") + val networkInfo = cm.activeNetworkInfo + return networkInfo != null && networkInfo.isConnectedOrConnecting + } + } + + private fun hasPermission( + context: Context, + permission: String, + ): Boolean { + return context.checkCallingOrSelfPermission(permission) == PackageManager.PERMISSION_GRANTED + } +} diff --git a/android/src/main/java/com/amplitude/android/utilities/AndroidNetworkListener.kt b/android/src/main/java/com/amplitude/android/utilities/AndroidNetworkListener.kt new file mode 100644 index 00000000..8bd16228 --- /dev/null +++ b/android/src/main/java/com/amplitude/android/utilities/AndroidNetworkListener.kt @@ -0,0 +1,97 @@ +import android.annotation.SuppressLint +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkRequest +import android.os.Build +import java.lang.IllegalArgumentException + +class AndroidNetworkListener(private val context: Context) { + private var networkCallback: NetworkChangeCallback? = null + private var networkCallbackForLowerApiLevels: BroadcastReceiver? = null + private var networkCallbackForHigherApiLevels: ConnectivityManager.NetworkCallback? = null + + interface NetworkChangeCallback { + fun onNetworkAvailable() + + fun onNetworkUnavailable() + } + + fun setNetworkChangeCallback(callback: NetworkChangeCallback) { + this.networkCallback = callback + } + + fun startListening() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + setupNetworkCallback() + } else { + setupBroadcastReceiver() + } + } + + @SuppressLint("NewApi") + private fun setupNetworkCallback() { + val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val networkRequest = + NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .build() + + networkCallbackForHigherApiLevels = + object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + networkCallback?.onNetworkAvailable() + } + + override fun onLost(network: Network) { + networkCallback?.onNetworkUnavailable() + } + } + + connectivityManager.registerNetworkCallback(networkRequest, networkCallbackForHigherApiLevels!!) + } + + private fun setupBroadcastReceiver() { + networkCallbackForLowerApiLevels = + object : BroadcastReceiver() { + override fun onReceive( + context: Context, + intent: Intent, + ) { + if (ConnectivityManager.CONNECTIVITY_ACTION == intent.action) { + val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val activeNetwork = connectivityManager.activeNetworkInfo + val isConnected = activeNetwork?.isConnectedOrConnecting == true + + if (isConnected) { + networkCallback?.onNetworkAvailable() + } else { + networkCallback?.onNetworkUnavailable() + } + } + } + } + + val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION) + context.registerReceiver(networkCallbackForLowerApiLevels, filter) + } + + fun stopListening() { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + networkCallbackForHigherApiLevels?.let { connectivityManager.unregisterNetworkCallback(it) } + } else { + networkCallbackForLowerApiLevels?.let { context.unregisterReceiver(it) } + } + } catch (e: IllegalArgumentException) { + // callback was already unregistered. + } catch (e: IllegalStateException) { + // shutdown process is in progress and certain operations are not allowed. + } + } +} diff --git a/android/src/test/java/com/amplitude/android/AmplitudeRobolectricTests.kt b/android/src/test/java/com/amplitude/android/AmplitudeRobolectricTests.kt index aa9cdce6..7cd43a07 100644 --- a/android/src/test/java/com/amplitude/android/AmplitudeRobolectricTests.kt +++ b/android/src/test/java/com/amplitude/android/AmplitudeRobolectricTests.kt @@ -2,6 +2,7 @@ package com.amplitude.android import android.app.Application import android.content.Context +import android.net.ConnectivityManager import com.amplitude.core.events.BaseEvent import com.amplitude.core.utilities.ConsoleLoggerProvider import com.amplitude.id.IMIdentityStorageProvider @@ -23,6 +24,7 @@ import kotlin.io.path.absolutePathString class AmplitudeRobolectricTests { private lateinit var amplitude: Amplitude private var context: Context? = null + private lateinit var connectivityManager: ConnectivityManager var tempDir = TempDirectory() @@ -30,8 +32,9 @@ class AmplitudeRobolectricTests { @Before fun setup() { context = mockk(relaxed = true) + connectivityManager = mockk(relaxed = true) every { context!!.getDir(any(), any()) } returns File(tempDir.create("data").absolutePathString()) - + every { context!!.getSystemService(Context.CONNECTIVITY_SERVICE) } returns connectivityManager amplitude = Amplitude(createConfiguration()) } diff --git a/android/src/test/java/com/amplitude/android/AmplitudeSessionTest.kt b/android/src/test/java/com/amplitude/android/AmplitudeSessionTest.kt index 7aded7e6..b25165ae 100644 --- a/android/src/test/java/com/amplitude/android/AmplitudeSessionTest.kt +++ b/android/src/test/java/com/amplitude/android/AmplitudeSessionTest.kt @@ -1,6 +1,8 @@ package com.amplitude.android import android.app.Application +import android.content.Context +import android.net.ConnectivityManager import com.amplitude.android.plugins.AndroidLifecyclePlugin import com.amplitude.common.android.AndroidContextProvider import com.amplitude.core.Storage @@ -47,14 +49,18 @@ class AmplitudeSessionTest { every { anyConstructed().mostRecentLocation } returns null every { anyConstructed().appSetId } returns "" - val configuration = IdentityConfiguration( - instanceName, - identityStorageProvider = IMIdentityStorageProvider() - ) + val configuration = + IdentityConfiguration( + instanceName, + identityStorageProvider = IMIdentityStorageProvider(), + ) IdentityContainer.getInstance(configuration) } - private fun setDispatcher(amplitude: Amplitude, testScheduler: TestCoroutineScheduler) { + private fun setDispatcher( + amplitude: Amplitude, + testScheduler: TestCoroutineScheduler, + ) { val dispatcher = StandardTestDispatcher(testScheduler) // inject the amplitudeDispatcher field with reflection, as the field is val (read-only) val amplitudeDispatcherField = com.amplitude.core.Amplitude::class.java.getDeclaredField("amplitudeDispatcher") @@ -64,6 +70,8 @@ class AmplitudeSessionTest { private fun createConfiguration(storageProvider: StorageProvider? = null): Configuration { val context = mockk(relaxed = true) + var connectivityManager = mockk(relaxed = true) + every { context!!.getSystemService(Context.CONNECTIVITY_SERVICE) } returns connectivityManager return Configuration( apiKey = "api-key", @@ -74,533 +82,549 @@ class AmplitudeSessionTest { trackingSessionEvents = true, loggerProvider = ConsoleLoggerProvider(), identifyInterceptStorageProvider = InMemoryStorageProvider(), - identityStorageProvider = IMIdentityStorageProvider() + identityStorageProvider = IMIdentityStorageProvider(), ) } @Test - fun amplitude_closeBackgroundEventsShouldNotStartNewSession() = runTest { - val amplitude = Amplitude(createConfiguration()) - setDispatcher(amplitude, testScheduler) + fun amplitude_closeBackgroundEventsShouldNotStartNewSession() = + runTest { + val amplitude = Amplitude(createConfiguration()) + setDispatcher(amplitude, testScheduler) - val mockedPlugin = spyk(StubPlugin()) - amplitude.add(mockedPlugin) + val mockedPlugin = spyk(StubPlugin()) + amplitude.add(mockedPlugin) - amplitude.isBuilt.await() + amplitude.isBuilt.await() - amplitude.track(createEvent(1000, "test event 1")) - amplitude.track(createEvent(1050, "test event 2")) + amplitude.track(createEvent(1000, "test event 1")) + amplitude.track(createEvent(1050, "test event 2")) - advanceUntilIdle() - Thread.sleep(100) + advanceUntilIdle() + Thread.sleep(100) - val tracks = mutableListOf() + val tracks = mutableListOf() - verify { - mockedPlugin.track(capture(tracks)) - } + verify { + mockedPlugin.track(capture(tracks)) + } - tracks.sortBy { event -> event.eventId } + tracks.sortBy { event -> event.eventId } - Assertions.assertEquals(3, tracks.count()) + Assertions.assertEquals(3, tracks.count()) - var event = tracks[0] - Assertions.assertEquals(Amplitude.START_SESSION_EVENT, event.eventType) - Assertions.assertEquals(1000, event.sessionId) - Assertions.assertEquals(1000, event.timestamp) + var event = tracks[0] + Assertions.assertEquals(Amplitude.START_SESSION_EVENT, event.eventType) + Assertions.assertEquals(1000, event.sessionId) + Assertions.assertEquals(1000, event.timestamp) - event = tracks[1] - Assertions.assertEquals("test event 1", event.eventType) - Assertions.assertEquals(1000, event.sessionId) - Assertions.assertEquals(1000, event.timestamp) + event = tracks[1] + Assertions.assertEquals("test event 1", event.eventType) + Assertions.assertEquals(1000, event.sessionId) + Assertions.assertEquals(1000, event.timestamp) - event = tracks[2] - Assertions.assertEquals("test event 2", event.eventType) - Assertions.assertEquals(1000, event.sessionId) - Assertions.assertEquals(1050, event.timestamp) - } + event = tracks[2] + Assertions.assertEquals("test event 2", event.eventType) + Assertions.assertEquals(1000, event.sessionId) + Assertions.assertEquals(1050, event.timestamp) + } @Test - fun amplitude_distantBackgroundEventsShouldStartNewSession() = runTest { - val amplitude = Amplitude(createConfiguration()) - setDispatcher(amplitude, testScheduler) + fun amplitude_distantBackgroundEventsShouldStartNewSession() = + runTest { + val amplitude = Amplitude(createConfiguration()) + setDispatcher(amplitude, testScheduler) - val mockedPlugin = spyk(StubPlugin()) - amplitude.add(mockedPlugin) + val mockedPlugin = spyk(StubPlugin()) + amplitude.add(mockedPlugin) - amplitude.isBuilt.await() + amplitude.isBuilt.await() - amplitude.track(createEvent(1000, "test event 1")) - amplitude.track(createEvent(2000, "test event 2")) + amplitude.track(createEvent(1000, "test event 1")) + amplitude.track(createEvent(2000, "test event 2")) - advanceUntilIdle() - Thread.sleep(100) + advanceUntilIdle() + Thread.sleep(100) - val tracks = mutableListOf() + val tracks = mutableListOf() - verify { - mockedPlugin.track(capture(tracks)) - } + verify { + mockedPlugin.track(capture(tracks)) + } - tracks.sortBy { event -> event.eventId } + tracks.sortBy { event -> event.eventId } - Assertions.assertEquals(5, tracks.count()) + Assertions.assertEquals(5, tracks.count()) - var event = tracks[0] - Assertions.assertEquals(Amplitude.START_SESSION_EVENT, event.eventType) - Assertions.assertEquals(1000, event.sessionId) - Assertions.assertEquals(1000, event.timestamp) + var event = tracks[0] + Assertions.assertEquals(Amplitude.START_SESSION_EVENT, event.eventType) + Assertions.assertEquals(1000, event.sessionId) + Assertions.assertEquals(1000, event.timestamp) - event = tracks[1] - Assertions.assertEquals("test event 1", event.eventType) - Assertions.assertEquals(1000, event.sessionId) - Assertions.assertEquals(1000, event.timestamp) + event = tracks[1] + Assertions.assertEquals("test event 1", event.eventType) + Assertions.assertEquals(1000, event.sessionId) + Assertions.assertEquals(1000, event.timestamp) - event = tracks[2] - Assertions.assertEquals(Amplitude.END_SESSION_EVENT, event.eventType) - Assertions.assertEquals(1000, event.sessionId) - Assertions.assertEquals(1000, event.timestamp) + event = tracks[2] + Assertions.assertEquals(Amplitude.END_SESSION_EVENT, event.eventType) + Assertions.assertEquals(1000, event.sessionId) + Assertions.assertEquals(1000, event.timestamp) - event = tracks[3] - Assertions.assertEquals(Amplitude.START_SESSION_EVENT, event.eventType) - Assertions.assertEquals(2000, event.sessionId) - Assertions.assertEquals(2000, event.timestamp) + event = tracks[3] + Assertions.assertEquals(Amplitude.START_SESSION_EVENT, event.eventType) + Assertions.assertEquals(2000, event.sessionId) + Assertions.assertEquals(2000, event.timestamp) - event = tracks[4] - Assertions.assertEquals("test event 2", event.eventType) - Assertions.assertEquals(2000, event.sessionId) - Assertions.assertEquals(2000, event.timestamp) - } + event = tracks[4] + Assertions.assertEquals("test event 2", event.eventType) + Assertions.assertEquals(2000, event.sessionId) + Assertions.assertEquals(2000, event.timestamp) + } @Test - fun amplitude_foregroundEventsShouldNotStartNewSession() = runTest { - val amplitude = Amplitude(createConfiguration()) - setDispatcher(amplitude, testScheduler) + fun amplitude_foregroundEventsShouldNotStartNewSession() = + runTest { + val amplitude = Amplitude(createConfiguration()) + setDispatcher(amplitude, testScheduler) - val mockedPlugin = spyk(StubPlugin()) - amplitude.add(mockedPlugin) + val mockedPlugin = spyk(StubPlugin()) + amplitude.add(mockedPlugin) - amplitude.isBuilt.await() + amplitude.isBuilt.await() - amplitude.onEnterForeground(1000) - amplitude.track(createEvent(1050, "test event 1")) - amplitude.track(createEvent(2000, "test event 2")) + amplitude.onEnterForeground(1000) + amplitude.track(createEvent(1050, "test event 1")) + amplitude.track(createEvent(2000, "test event 2")) - advanceUntilIdle() - Thread.sleep(100) + advanceUntilIdle() + Thread.sleep(100) - val tracks = mutableListOf() + val tracks = mutableListOf() - verify { - mockedPlugin.track(capture(tracks)) - } + verify { + mockedPlugin.track(capture(tracks)) + } - tracks.sortBy { event -> event.eventId } + tracks.sortBy { event -> event.eventId } - Assertions.assertEquals(3, tracks.count()) + Assertions.assertEquals(3, tracks.count()) - var event = tracks[0] - Assertions.assertEquals(Amplitude.START_SESSION_EVENT, event.eventType) - Assertions.assertEquals(1000, event.sessionId) - Assertions.assertEquals(1000, event.timestamp) + var event = tracks[0] + Assertions.assertEquals(Amplitude.START_SESSION_EVENT, event.eventType) + Assertions.assertEquals(1000, event.sessionId) + Assertions.assertEquals(1000, event.timestamp) - event = tracks[1] - Assertions.assertEquals("test event 1", event.eventType) - Assertions.assertEquals(1000, event.sessionId) - Assertions.assertEquals(1050, event.timestamp) + event = tracks[1] + Assertions.assertEquals("test event 1", event.eventType) + Assertions.assertEquals(1000, event.sessionId) + Assertions.assertEquals(1050, event.timestamp) - event = tracks[2] - Assertions.assertEquals("test event 2", event.eventType) - Assertions.assertEquals(1000, event.sessionId) - Assertions.assertEquals(2000, event.timestamp) - } + event = tracks[2] + Assertions.assertEquals("test event 2", event.eventType) + Assertions.assertEquals(1000, event.sessionId) + Assertions.assertEquals(2000, event.timestamp) + } @Test - fun amplitude_closeBackgroundForegroundEventsShouldNotStartNewSession() = runTest { - val amplitude = Amplitude(createConfiguration()) - setDispatcher(amplitude, testScheduler) + fun amplitude_closeBackgroundForegroundEventsShouldNotStartNewSession() = + runTest { + val amplitude = Amplitude(createConfiguration()) + setDispatcher(amplitude, testScheduler) - val mockedPlugin = spyk(StubPlugin()) - amplitude.add(mockedPlugin) + val mockedPlugin = spyk(StubPlugin()) + amplitude.add(mockedPlugin) - amplitude.isBuilt.await() + amplitude.isBuilt.await() - amplitude.track(createEvent(1000, "test event 1")) - amplitude.onEnterForeground(1050) - amplitude.track(createEvent(2000, "test event 2")) + amplitude.track(createEvent(1000, "test event 1")) + amplitude.onEnterForeground(1050) + amplitude.track(createEvent(2000, "test event 2")) - advanceUntilIdle() - Thread.sleep(100) + advanceUntilIdle() + Thread.sleep(100) - val tracks = mutableListOf() + val tracks = mutableListOf() - verify { - mockedPlugin.track(capture(tracks)) - } + verify { + mockedPlugin.track(capture(tracks)) + } - tracks.sortBy { event -> event.eventId } + tracks.sortBy { event -> event.eventId } - Assertions.assertEquals(3, tracks.count()) + Assertions.assertEquals(3, tracks.count()) - var event = tracks[0] - Assertions.assertEquals(Amplitude.START_SESSION_EVENT, event.eventType) - Assertions.assertEquals(1000, event.sessionId) - Assertions.assertEquals(1000, event.timestamp) + var event = tracks[0] + Assertions.assertEquals(Amplitude.START_SESSION_EVENT, event.eventType) + Assertions.assertEquals(1000, event.sessionId) + Assertions.assertEquals(1000, event.timestamp) - event = tracks[1] - Assertions.assertEquals("test event 1", event.eventType) - Assertions.assertEquals(1000, event.sessionId) - Assertions.assertEquals(1000, event.timestamp) + event = tracks[1] + Assertions.assertEquals("test event 1", event.eventType) + Assertions.assertEquals(1000, event.sessionId) + Assertions.assertEquals(1000, event.timestamp) - event = tracks[2] - Assertions.assertEquals("test event 2", event.eventType) - Assertions.assertEquals(1000, event.sessionId) - Assertions.assertEquals(2000, event.timestamp) - } + event = tracks[2] + Assertions.assertEquals("test event 2", event.eventType) + Assertions.assertEquals(1000, event.sessionId) + Assertions.assertEquals(2000, event.timestamp) + } @Test - fun amplitude_distantBackgroundForegroundEventsShouldStartNewSession() = runTest { - val amplitude = Amplitude(createConfiguration()) - setDispatcher(amplitude, testScheduler) + fun amplitude_distantBackgroundForegroundEventsShouldStartNewSession() = + runTest { + val amplitude = Amplitude(createConfiguration()) + setDispatcher(amplitude, testScheduler) - val mockedPlugin = spyk(StubPlugin()) - amplitude.add(mockedPlugin) + val mockedPlugin = spyk(StubPlugin()) + amplitude.add(mockedPlugin) - amplitude.isBuilt.await() + amplitude.isBuilt.await() - amplitude.track(createEvent(1000, "test event 1")) - amplitude.onEnterForeground(2000) - amplitude.track(createEvent(3000, "test event 2")) + amplitude.track(createEvent(1000, "test event 1")) + amplitude.onEnterForeground(2000) + amplitude.track(createEvent(3000, "test event 2")) - advanceUntilIdle() - Thread.sleep(100) + advanceUntilIdle() + Thread.sleep(100) - val tracks = mutableListOf() + val tracks = mutableListOf() - verify { - mockedPlugin.track(capture(tracks)) - } + verify { + mockedPlugin.track(capture(tracks)) + } - tracks.sortBy { event -> event.eventId } + tracks.sortBy { event -> event.eventId } - Assertions.assertEquals(5, tracks.count()) + Assertions.assertEquals(5, tracks.count()) - var event = tracks[0] - Assertions.assertEquals(Amplitude.START_SESSION_EVENT, event.eventType) - Assertions.assertEquals(1000, event.sessionId) - Assertions.assertEquals(1000, event.timestamp) + var event = tracks[0] + Assertions.assertEquals(Amplitude.START_SESSION_EVENT, event.eventType) + Assertions.assertEquals(1000, event.sessionId) + Assertions.assertEquals(1000, event.timestamp) - event = tracks[1] - Assertions.assertEquals("test event 1", event.eventType) - Assertions.assertEquals(1000, event.sessionId) - Assertions.assertEquals(1000, event.timestamp) + event = tracks[1] + Assertions.assertEquals("test event 1", event.eventType) + Assertions.assertEquals(1000, event.sessionId) + Assertions.assertEquals(1000, event.timestamp) - event = tracks[2] - Assertions.assertEquals(Amplitude.END_SESSION_EVENT, event.eventType) - Assertions.assertEquals(1000, event.sessionId) - Assertions.assertEquals(1000, event.timestamp) + event = tracks[2] + Assertions.assertEquals(Amplitude.END_SESSION_EVENT, event.eventType) + Assertions.assertEquals(1000, event.sessionId) + Assertions.assertEquals(1000, event.timestamp) - event = tracks[3] - Assertions.assertEquals(Amplitude.START_SESSION_EVENT, event.eventType) - Assertions.assertEquals(2000, event.sessionId) - Assertions.assertEquals(2000, event.timestamp) + event = tracks[3] + Assertions.assertEquals(Amplitude.START_SESSION_EVENT, event.eventType) + Assertions.assertEquals(2000, event.sessionId) + Assertions.assertEquals(2000, event.timestamp) - event = tracks[4] - Assertions.assertEquals("test event 2", event.eventType) - Assertions.assertEquals(2000, event.sessionId) - Assertions.assertEquals(3000, event.timestamp) - } + event = tracks[4] + Assertions.assertEquals("test event 2", event.eventType) + Assertions.assertEquals(2000, event.sessionId) + Assertions.assertEquals(3000, event.timestamp) + } @Test - fun amplitude_closeForegroundBackgroundEventsShouldNotStartNewSession() = runTest { - val amplitude = Amplitude(createConfiguration()) - setDispatcher(amplitude, testScheduler) + fun amplitude_closeForegroundBackgroundEventsShouldNotStartNewSession() = + runTest { + val amplitude = Amplitude(createConfiguration()) + setDispatcher(amplitude, testScheduler) - val mockedPlugin = spyk(StubPlugin()) - amplitude.add(mockedPlugin) + val mockedPlugin = spyk(StubPlugin()) + amplitude.add(mockedPlugin) - amplitude.isBuilt.await() + amplitude.isBuilt.await() - amplitude.onEnterForeground(1000) - amplitude.track(createEvent(1500, "test event 1")) - amplitude.onExitForeground(2000) - amplitude.track(createEvent(2050, "test event 2")) + amplitude.onEnterForeground(1000) + amplitude.track(createEvent(1500, "test event 1")) + amplitude.onExitForeground(2000) + amplitude.track(createEvent(2050, "test event 2")) - advanceUntilIdle() - Thread.sleep(100) + advanceUntilIdle() + Thread.sleep(100) - val tracks = mutableListOf() + val tracks = mutableListOf() - verify { - mockedPlugin.track(capture(tracks)) - } + verify { + mockedPlugin.track(capture(tracks)) + } - tracks.sortBy { event -> event.eventId } + tracks.sortBy { event -> event.eventId } - Assertions.assertEquals(3, tracks.count()) + Assertions.assertEquals(3, tracks.count()) - var event = tracks[0] - Assertions.assertEquals(Amplitude.START_SESSION_EVENT, event.eventType) - Assertions.assertEquals(1000, event.sessionId) - Assertions.assertEquals(1000, event.timestamp) + var event = tracks[0] + Assertions.assertEquals(Amplitude.START_SESSION_EVENT, event.eventType) + Assertions.assertEquals(1000, event.sessionId) + Assertions.assertEquals(1000, event.timestamp) - event = tracks[1] - Assertions.assertEquals("test event 1", event.eventType) - Assertions.assertEquals(1000, event.sessionId) - Assertions.assertEquals(1500, event.timestamp) + event = tracks[1] + Assertions.assertEquals("test event 1", event.eventType) + Assertions.assertEquals(1000, event.sessionId) + Assertions.assertEquals(1500, event.timestamp) - event = tracks[2] - Assertions.assertEquals("test event 2", event.eventType) - Assertions.assertEquals(1000, event.sessionId) - Assertions.assertEquals(2050, event.timestamp) - } + event = tracks[2] + Assertions.assertEquals("test event 2", event.eventType) + Assertions.assertEquals(1000, event.sessionId) + Assertions.assertEquals(2050, event.timestamp) + } @Test - fun amplitude_distantForegroundBackgroundEventsShouldStartNewSession() = runTest { - val amplitude = Amplitude(createConfiguration()) - setDispatcher(amplitude, testScheduler) + fun amplitude_distantForegroundBackgroundEventsShouldStartNewSession() = + runTest { + val amplitude = Amplitude(createConfiguration()) + setDispatcher(amplitude, testScheduler) - val mockedPlugin = spyk(StubPlugin()) - amplitude.add(mockedPlugin) + val mockedPlugin = spyk(StubPlugin()) + amplitude.add(mockedPlugin) - amplitude.isBuilt.await() + amplitude.isBuilt.await() - amplitude.onEnterForeground(1000) - amplitude.track(createEvent(1500, "test event 1")) - amplitude.onExitForeground(2000) - amplitude.track(createEvent(3000, "test event 2")) + amplitude.onEnterForeground(1000) + amplitude.track(createEvent(1500, "test event 1")) + amplitude.onExitForeground(2000) + amplitude.track(createEvent(3000, "test event 2")) - advanceUntilIdle() - Thread.sleep(100) + advanceUntilIdle() + Thread.sleep(100) - val tracks = mutableListOf() + val tracks = mutableListOf() - verify { - mockedPlugin.track(capture(tracks)) - } + verify { + mockedPlugin.track(capture(tracks)) + } - tracks.sortBy { event -> event.eventId } + tracks.sortBy { event -> event.eventId } - Assertions.assertEquals(5, tracks.count()) + Assertions.assertEquals(5, tracks.count()) - var event = tracks[0] - Assertions.assertEquals(Amplitude.START_SESSION_EVENT, event.eventType) - Assertions.assertEquals(1000, event.sessionId) - Assertions.assertEquals(1000, event.timestamp) + var event = tracks[0] + Assertions.assertEquals(Amplitude.START_SESSION_EVENT, event.eventType) + Assertions.assertEquals(1000, event.sessionId) + Assertions.assertEquals(1000, event.timestamp) - event = tracks[1] - Assertions.assertEquals("test event 1", event.eventType) - Assertions.assertEquals(1000, event.sessionId) - Assertions.assertEquals(1500, event.timestamp) + event = tracks[1] + Assertions.assertEquals("test event 1", event.eventType) + Assertions.assertEquals(1000, event.sessionId) + Assertions.assertEquals(1500, event.timestamp) - event = tracks[2] - Assertions.assertEquals(Amplitude.END_SESSION_EVENT, event.eventType) - Assertions.assertEquals(1000, event.sessionId) - Assertions.assertEquals(2000, event.timestamp) + event = tracks[2] + Assertions.assertEquals(Amplitude.END_SESSION_EVENT, event.eventType) + Assertions.assertEquals(1000, event.sessionId) + Assertions.assertEquals(2000, event.timestamp) - event = tracks[3] - Assertions.assertEquals(Amplitude.START_SESSION_EVENT, event.eventType) - Assertions.assertEquals(3000, event.sessionId) - Assertions.assertEquals(3000, event.timestamp) + event = tracks[3] + Assertions.assertEquals(Amplitude.START_SESSION_EVENT, event.eventType) + Assertions.assertEquals(3000, event.sessionId) + Assertions.assertEquals(3000, event.timestamp) - event = tracks[4] - Assertions.assertEquals("test event 2", event.eventType) - Assertions.assertEquals(3000, event.sessionId) - Assertions.assertEquals(3000, event.timestamp) - } + event = tracks[4] + Assertions.assertEquals("test event 2", event.eventType) + Assertions.assertEquals(3000, event.sessionId) + Assertions.assertEquals(3000, event.timestamp) + } @Test - fun amplitude_sessionDataShouldBePersisted() = runTest { - val storageProvider = InstanceStorageProvider(InMemoryStorage()) + fun amplitude_sessionDataShouldBePersisted() = + runTest { + val storageProvider = InstanceStorageProvider(InMemoryStorage()) - val amplitude1 = Amplitude(createConfiguration(storageProvider)) - setDispatcher(amplitude1, testScheduler) - amplitude1.isBuilt.await() + val amplitude1 = Amplitude(createConfiguration(storageProvider)) + setDispatcher(amplitude1, testScheduler) + amplitude1.isBuilt.await() - amplitude1.onEnterForeground(1000) + amplitude1.onEnterForeground(1000) - advanceUntilIdle() - Thread.sleep(100) + advanceUntilIdle() + Thread.sleep(100) - val timeline1 = amplitude1.timeline as Timeline + val timeline1 = amplitude1.timeline as Timeline - Assertions.assertEquals(1000, amplitude1.sessionId) - Assertions.assertEquals(1000, timeline1.sessionId) - Assertions.assertEquals(1000, timeline1.lastEventTime) - Assertions.assertEquals(1, timeline1.lastEventId) + Assertions.assertEquals(1000, amplitude1.sessionId) + Assertions.assertEquals(1000, timeline1.sessionId) + Assertions.assertEquals(1000, timeline1.lastEventTime) + Assertions.assertEquals(1, timeline1.lastEventId) - amplitude1.track(createEvent(1200, "test event 1")) + amplitude1.track(createEvent(1200, "test event 1")) - advanceUntilIdle() - Thread.sleep(100) + advanceUntilIdle() + Thread.sleep(100) - Assertions.assertEquals(1000, amplitude1.sessionId) - Assertions.assertEquals(1000, timeline1.sessionId) - Assertions.assertEquals(1200, timeline1.lastEventTime) - Assertions.assertEquals(2, timeline1.lastEventId) + Assertions.assertEquals(1000, amplitude1.sessionId) + Assertions.assertEquals(1000, timeline1.sessionId) + Assertions.assertEquals(1200, timeline1.lastEventTime) + Assertions.assertEquals(2, timeline1.lastEventId) - val amplitude2 = Amplitude(createConfiguration(storageProvider)) - setDispatcher(amplitude2, testScheduler) - amplitude2.isBuilt.await() + val amplitude2 = Amplitude(createConfiguration(storageProvider)) + setDispatcher(amplitude2, testScheduler) + amplitude2.isBuilt.await() - advanceUntilIdle() - Thread.sleep(100) + advanceUntilIdle() + Thread.sleep(100) - val timeline2 = amplitude2.timeline as Timeline - Assertions.assertEquals(1000, amplitude2.sessionId) - Assertions.assertEquals(1000, timeline2.sessionId) - Assertions.assertEquals(1200, timeline2.lastEventTime) - Assertions.assertEquals(2, timeline2.lastEventId) - } + val timeline2 = amplitude2.timeline as Timeline + Assertions.assertEquals(1000, amplitude2.sessionId) + Assertions.assertEquals(1000, timeline2.sessionId) + Assertions.assertEquals(1200, timeline2.lastEventTime) + Assertions.assertEquals(2, timeline2.lastEventId) + } @Test - fun amplitude_explicitSessionForEventShouldBePreserved() = runTest { - val amplitude = Amplitude(createConfiguration()) - setDispatcher(amplitude, testScheduler) + fun amplitude_explicitSessionForEventShouldBePreserved() = + runTest { + val amplitude = Amplitude(createConfiguration()) + setDispatcher(amplitude, testScheduler) - val mockedPlugin = spyk(StubPlugin()) - amplitude.add(mockedPlugin) + val mockedPlugin = spyk(StubPlugin()) + amplitude.add(mockedPlugin) - amplitude.isBuilt.await() + amplitude.isBuilt.await() - amplitude.track(createEvent(1000, "test event 1")) - amplitude.track(createEvent(1050, "test event 2", 3000)) - amplitude.track(createEvent(1100, "test event 3")) + amplitude.track(createEvent(1000, "test event 1")) + amplitude.track(createEvent(1050, "test event 2", 3000)) + amplitude.track(createEvent(1100, "test event 3")) - advanceUntilIdle() - Thread.sleep(100) + advanceUntilIdle() + Thread.sleep(100) - val tracks = mutableListOf() + val tracks = mutableListOf() - verify { - mockedPlugin.track(capture(tracks)) - } + verify { + mockedPlugin.track(capture(tracks)) + } - tracks.sortBy { event -> event.eventId } + tracks.sortBy { event -> event.eventId } - Assertions.assertEquals(4, tracks.count()) + Assertions.assertEquals(4, tracks.count()) - var event = tracks[0] - Assertions.assertEquals(Amplitude.START_SESSION_EVENT, event.eventType) - Assertions.assertEquals(1000, event.sessionId) - Assertions.assertEquals(1000, event.timestamp) + var event = tracks[0] + Assertions.assertEquals(Amplitude.START_SESSION_EVENT, event.eventType) + Assertions.assertEquals(1000, event.sessionId) + Assertions.assertEquals(1000, event.timestamp) - event = tracks[1] - Assertions.assertEquals("test event 1", event.eventType) - Assertions.assertEquals(1000, event.sessionId) - Assertions.assertEquals(1000, event.timestamp) + event = tracks[1] + Assertions.assertEquals("test event 1", event.eventType) + Assertions.assertEquals(1000, event.sessionId) + Assertions.assertEquals(1000, event.timestamp) - event = tracks[2] - Assertions.assertEquals("test event 2", event.eventType) - Assertions.assertEquals(3000, event.sessionId) - Assertions.assertEquals(1050, event.timestamp) + event = tracks[2] + Assertions.assertEquals("test event 2", event.eventType) + Assertions.assertEquals(3000, event.sessionId) + Assertions.assertEquals(1050, event.timestamp) - event = tracks[3] - Assertions.assertEquals("test event 3", event.eventType) - Assertions.assertEquals(1000, event.sessionId) - Assertions.assertEquals(1100, event.timestamp) - } + event = tracks[3] + Assertions.assertEquals("test event 3", event.eventType) + Assertions.assertEquals(1000, event.sessionId) + Assertions.assertEquals(1100, event.timestamp) + } @Test - fun amplitude_explicitNoSessionForEventShouldBePreserved() = runTest { - val amplitude = Amplitude(createConfiguration()) - setDispatcher(amplitude, testScheduler) + fun amplitude_explicitNoSessionForEventShouldBePreserved() = + runTest { + val amplitude = Amplitude(createConfiguration()) + setDispatcher(amplitude, testScheduler) - val mockedPlugin = spyk(StubPlugin()) - amplitude.add(mockedPlugin) + val mockedPlugin = spyk(StubPlugin()) + amplitude.add(mockedPlugin) - amplitude.isBuilt.await() + amplitude.isBuilt.await() - amplitude.track(createEvent(1000, "test event 1")) - amplitude.track(createEvent(1050, "test event 2", -1)) - amplitude.track(createEvent(1100, "test event 3")) + amplitude.track(createEvent(1000, "test event 1")) + amplitude.track(createEvent(1050, "test event 2", -1)) + amplitude.track(createEvent(1100, "test event 3")) - advanceUntilIdle() - Thread.sleep(100) + advanceUntilIdle() + Thread.sleep(100) - val tracks = mutableListOf() + val tracks = mutableListOf() - verify { - mockedPlugin.track(capture(tracks)) - } + verify { + mockedPlugin.track(capture(tracks)) + } - tracks.sortBy { event -> event.eventId } + tracks.sortBy { event -> event.eventId } - Assertions.assertEquals(4, tracks.count()) + Assertions.assertEquals(4, tracks.count()) - var event = tracks[0] - Assertions.assertEquals(Amplitude.START_SESSION_EVENT, event.eventType) - Assertions.assertEquals(1000, event.sessionId) - Assertions.assertEquals(1000, event.timestamp) + var event = tracks[0] + Assertions.assertEquals(Amplitude.START_SESSION_EVENT, event.eventType) + Assertions.assertEquals(1000, event.sessionId) + Assertions.assertEquals(1000, event.timestamp) - event = tracks[1] - Assertions.assertEquals("test event 1", event.eventType) - Assertions.assertEquals(1000, event.sessionId) - Assertions.assertEquals(1000, event.timestamp) + event = tracks[1] + Assertions.assertEquals("test event 1", event.eventType) + Assertions.assertEquals(1000, event.sessionId) + Assertions.assertEquals(1000, event.timestamp) - event = tracks[2] - Assertions.assertEquals("test event 2", event.eventType) - Assertions.assertEquals(-1, event.sessionId) - Assertions.assertEquals(1050, event.timestamp) + event = tracks[2] + Assertions.assertEquals("test event 2", event.eventType) + Assertions.assertEquals(-1, event.sessionId) + Assertions.assertEquals(1050, event.timestamp) - event = tracks[3] - Assertions.assertEquals("test event 3", event.eventType) - Assertions.assertEquals(1000, event.sessionId) - Assertions.assertEquals(1100, event.timestamp) - } + event = tracks[3] + Assertions.assertEquals("test event 3", event.eventType) + Assertions.assertEquals(1000, event.sessionId) + Assertions.assertEquals(1100, event.timestamp) + } @Suppress("DEPRECATION") @Test - fun amplitude_noSessionEventsWhenDisabledWithTrackingSessionEvents() = runTest { - val configuration = createConfiguration() - configuration.trackingSessionEvents = false - val amplitude = Amplitude(configuration) - setDispatcher(amplitude, testScheduler) + fun amplitude_noSessionEventsWhenDisabledWithTrackingSessionEvents() = + runTest { + val configuration = createConfiguration() + configuration.trackingSessionEvents = false + val amplitude = Amplitude(configuration) + setDispatcher(amplitude, testScheduler) - val mockedPlugin = spyk(StubPlugin()) - amplitude.add(mockedPlugin) + val mockedPlugin = spyk(StubPlugin()) + amplitude.add(mockedPlugin) - amplitude.isBuilt.await() + amplitude.isBuilt.await() - amplitude.track(createEvent(1000, "test event")) + amplitude.track(createEvent(1000, "test event")) - advanceUntilIdle() - Thread.sleep(100) + advanceUntilIdle() + Thread.sleep(100) - val tracks = mutableListOf() + val tracks = mutableListOf() - verify { - mockedPlugin.track(capture(tracks)) + verify { + mockedPlugin.track(capture(tracks)) + } + Assertions.assertEquals(1, tracks.count()) } - Assertions.assertEquals(1, tracks.count()) - } @Test - fun amplitude_noSessionEventsWhenDisabledWithDefaultTrackingOptions() = runTest { - val configuration = createConfiguration() - configuration.defaultTracking.sessions = false - val amplitude = Amplitude(configuration) - setDispatcher(amplitude, testScheduler) + fun amplitude_noSessionEventsWhenDisabledWithDefaultTrackingOptions() = + runTest { + val configuration = createConfiguration() + configuration.defaultTracking.sessions = false + val amplitude = Amplitude(configuration) + setDispatcher(amplitude, testScheduler) - val mockedPlugin = spyk(StubPlugin()) - amplitude.add(mockedPlugin) + val mockedPlugin = spyk(StubPlugin()) + amplitude.add(mockedPlugin) - amplitude.isBuilt.await() + amplitude.isBuilt.await() - amplitude.track(createEvent(1000, "test event")) + amplitude.track(createEvent(1000, "test event")) - advanceUntilIdle() - Thread.sleep(100) + advanceUntilIdle() + Thread.sleep(100) - val tracks = mutableListOf() + val tracks = mutableListOf() - verify { - mockedPlugin.track(capture(tracks)) + verify { + mockedPlugin.track(capture(tracks)) + } + Assertions.assertEquals(1, tracks.count()) } - Assertions.assertEquals(1, tracks.count()) - } - private fun createEvent(timestamp: Long, eventType: String, sessionId: Long? = null): BaseEvent { + private fun createEvent( + timestamp: Long, + eventType: String, + sessionId: Long? = null, + ): BaseEvent { val event = BaseEvent() event.userId = "user" event.timestamp = timestamp @@ -615,7 +639,10 @@ class AmplitudeSessionTest { } class InstanceStorageProvider(private val instance: Storage) : StorageProvider { - override fun getStorage(amplitude: com.amplitude.core.Amplitude, prefix: String?): Storage { + override fun getStorage( + amplitude: com.amplitude.core.Amplitude, + prefix: String?, + ): Storage { return instance } } diff --git a/android/src/test/java/com/amplitude/android/AmplitudeTest.kt b/android/src/test/java/com/amplitude/android/AmplitudeTest.kt index 3faf6fcb..4636c773 100644 --- a/android/src/test/java/com/amplitude/android/AmplitudeTest.kt +++ b/android/src/test/java/com/amplitude/android/AmplitudeTest.kt @@ -2,6 +2,7 @@ package com.amplitude.android import android.app.Application import android.content.Context +import android.net.ConnectivityManager import com.amplitude.analytics.connector.AnalyticsConnector import com.amplitude.analytics.connector.Identity import com.amplitude.android.plugins.AndroidLifecyclePlugin @@ -38,10 +39,14 @@ open class StubPlugin : EventPlugin { class AmplitudeTest { private var context: Context? = null private var amplitude: Amplitude? = null + private lateinit var connectivityManager: ConnectivityManager @BeforeEach fun setUp() { context = mockk(relaxed = true) + connectivityManager = mockk(relaxed = true) + every { context!!.getSystemService(Context.CONNECTIVITY_SERVICE) } returns connectivityManager + mockkStatic(AndroidLifecyclePlugin::class) mockkConstructor(AndroidContextProvider::class) @@ -69,17 +74,21 @@ class AmplitudeTest { amplitudeDispatcherField.set(amplitude, dispatcher) } - private fun createConfiguration(minTimeBetweenSessionsMillis: Long? = null, storageProvider: StorageProvider = InMemoryStorageProvider()): Configuration { - val configuration = Configuration( - apiKey = "api-key", - context = context!!, - instanceName = instanceName, - storageProvider = storageProvider, - trackingSessionEvents = minTimeBetweenSessionsMillis != null, - loggerProvider = ConsoleLoggerProvider(), - identifyInterceptStorageProvider = InMemoryStorageProvider(), - identityStorageProvider = IMIdentityStorageProvider(), - ) + private fun createConfiguration( + minTimeBetweenSessionsMillis: Long? = null, + storageProvider: StorageProvider = InMemoryStorageProvider(), + ): Configuration { + val configuration = + Configuration( + apiKey = "api-key", + context = context!!, + instanceName = instanceName, + storageProvider = storageProvider, + trackingSessionEvents = minTimeBetweenSessionsMillis != null, + loggerProvider = ConsoleLoggerProvider(), + identifyInterceptStorageProvider = InMemoryStorageProvider(), + identityStorageProvider = IMIdentityStorageProvider(), + ) if (minTimeBetweenSessionsMillis != null) { configuration.minTimeBetweenSessionsMillis = minTimeBetweenSessionsMillis @@ -89,98 +98,101 @@ class AmplitudeTest { } @Test - fun amplitude_reset_wipesUserIdDeviceId() = runTest { - setDispatcher(testScheduler) - if (amplitude?.isBuilt!!.await()) { - amplitude?.setUserId("test user") - amplitude?.setDeviceId("test device") - advanceUntilIdle() - Assertions.assertEquals("test user", amplitude?.store?.userId) - Assertions.assertEquals("test device", amplitude?.store?.deviceId) - Assertions.assertEquals("test user", amplitude?.getUserId()) - Assertions.assertEquals("test device", amplitude?.getDeviceId()) - - amplitude?.reset() - advanceUntilIdle() - Assertions.assertNull(amplitude?.store?.userId) - Assertions.assertNotEquals("test device", amplitude?.store?.deviceId) - Assertions.assertNull(amplitude?.getUserId()) - Assertions.assertNotEquals("test device", amplitude?.getDeviceId()) + fun amplitude_reset_wipesUserIdDeviceId() = + runTest { + setDispatcher(testScheduler) + if (amplitude?.isBuilt!!.await()) { + amplitude?.setUserId("test user") + amplitude?.setDeviceId("test device") + advanceUntilIdle() + Assertions.assertEquals("test user", amplitude?.store?.userId) + Assertions.assertEquals("test device", amplitude?.store?.deviceId) + Assertions.assertEquals("test user", amplitude?.getUserId()) + Assertions.assertEquals("test device", amplitude?.getDeviceId()) + + amplitude?.reset() + advanceUntilIdle() + Assertions.assertNull(amplitude?.store?.userId) + Assertions.assertNotEquals("test device", amplitude?.store?.deviceId) + Assertions.assertNull(amplitude?.getUserId()) + Assertions.assertNotEquals("test device", amplitude?.getDeviceId()) + } } - } @Test - fun amplitude_unset_country_with_remote_ip() = runTest { - setDispatcher(testScheduler) - val mockedPlugin = spyk(StubPlugin()) - amplitude?.add(mockedPlugin) - - if (amplitude?.isBuilt!!.await()) { - val event = BaseEvent() - event.eventType = "test event" - amplitude?.track(event) - advanceUntilIdle() - Thread.sleep(100) - - val track = slot() - verify { mockedPlugin.track(capture(track)) } - track.captured.let { - Assertions.assertEquals("\$remote", it.ip) - Assertions.assertNull(it.country) + fun amplitude_unset_country_with_remote_ip() = + runTest { + setDispatcher(testScheduler) + val mockedPlugin = spyk(StubPlugin()) + amplitude?.add(mockedPlugin) + + if (amplitude?.isBuilt!!.await()) { + val event = BaseEvent() + event.eventType = "test event" + amplitude?.track(event) + advanceUntilIdle() + Thread.sleep(100) + + val track = slot() + verify { mockedPlugin.track(capture(track)) } + track.captured.let { + Assertions.assertEquals("\$remote", it.ip) + Assertions.assertNull(it.country) + } } } - } @Test - fun amplitude_fetch_country_with_customized_ip() = runTest { - setDispatcher(testScheduler) - val mockedPlugin = spyk(StubPlugin()) - amplitude?.add(mockedPlugin) - - if (amplitude?.isBuilt!!.await()) { - val event = BaseEvent() - event.eventType = "test event" - event.ip = "127.0.0.1" - amplitude?.track(event) - advanceUntilIdle() - Thread.sleep(100) - - val track = slot() - verify { mockedPlugin.track(capture(track)) } - track.captured.let { - Assertions.assertEquals("127.0.0.1", it.ip) - Assertions.assertEquals("US", it.country) + fun amplitude_fetch_country_with_customized_ip() = + runTest { + setDispatcher(testScheduler) + val mockedPlugin = spyk(StubPlugin()) + amplitude?.add(mockedPlugin) + + if (amplitude?.isBuilt!!.await()) { + val event = BaseEvent() + event.eventType = "test event" + event.ip = "127.0.0.1" + amplitude?.track(event) + advanceUntilIdle() + Thread.sleep(100) + + val track = slot() + verify { mockedPlugin.track(capture(track)) } + track.captured.let { + Assertions.assertEquals("127.0.0.1", it.ip) + Assertions.assertEquals("US", it.country) + } } } - } @Test - fun test_analytics_connector() = runTest { - setDispatcher(testScheduler) - val mockedPlugin = spyk(StubPlugin()) - amplitude?.add(mockedPlugin) - - if (amplitude?.isBuilt!!.await()) { - - val connector = AnalyticsConnector.getInstance(instanceName) - val connectorUserId = "connector user id" - val connectorDeviceId = "connector device id" - var connectorIdentitySet = false - val identityListener = { _: Identity -> - if (connectorIdentitySet) { - Assertions.assertEquals(connectorUserId, connector.identityStore.getIdentity().userId) - Assertions.assertEquals(connectorDeviceId, connector.identityStore.getIdentity().deviceId) - connectorIdentitySet = false + fun test_analytics_connector() = + runTest { + setDispatcher(testScheduler) + val mockedPlugin = spyk(StubPlugin()) + amplitude?.add(mockedPlugin) + + if (amplitude?.isBuilt!!.await()) { + val connector = AnalyticsConnector.getInstance(instanceName) + val connectorUserId = "connector user id" + val connectorDeviceId = "connector device id" + var connectorIdentitySet = false + val identityListener = { _: Identity -> + if (connectorIdentitySet) { + Assertions.assertEquals(connectorUserId, connector.identityStore.getIdentity().userId) + Assertions.assertEquals(connectorDeviceId, connector.identityStore.getIdentity().deviceId) + connectorIdentitySet = false + } } + connector.identityStore.addIdentityListener(identityListener) + amplitude?.setUserId(connectorUserId) + amplitude?.setDeviceId(connectorDeviceId) + advanceUntilIdle() + connectorIdentitySet = true + connector.identityStore.removeIdentityListener(identityListener) } - connector.identityStore.addIdentityListener(identityListener) - amplitude?.setUserId(connectorUserId) - amplitude?.setDeviceId(connectorDeviceId) - advanceUntilIdle() - connectorIdentitySet = true - connector.identityStore.removeIdentityListener(identityListener) } - } companion object { const val instanceName = "testInstance" diff --git a/android/src/test/java/com/amplitude/android/plugins/AndroidLifecyclePluginTest.kt b/android/src/test/java/com/amplitude/android/plugins/AndroidLifecyclePluginTest.kt index c30999d0..c3ce74e8 100644 --- a/android/src/test/java/com/amplitude/android/plugins/AndroidLifecyclePluginTest.kt +++ b/android/src/test/java/com/amplitude/android/plugins/AndroidLifecyclePluginTest.kt @@ -2,10 +2,12 @@ package com.amplitude.android.plugins import android.app.Activity import android.app.Application +import android.content.Context import android.content.Intent import android.content.pm.ActivityInfo import android.content.pm.PackageInfo import android.content.pm.PackageManager +import android.net.ConnectivityManager import android.net.Uri import android.os.Bundle import com.amplitude.android.Amplitude @@ -44,6 +46,7 @@ class AndroidLifecyclePluginTest { private val mockedContext = mockk(relaxed = true) private var mockedPackageManager: PackageManager + private lateinit var connectivityManager: ConnectivityManager init { val packageInfo = PackageInfo() @@ -51,9 +54,10 @@ class AndroidLifecyclePluginTest { packageInfo.versionCode = 66 packageInfo.versionName = "6.0.0" - mockedPackageManager = mockk { - every { getPackageInfo("com.plugin.test", 0) } returns packageInfo - } + mockedPackageManager = + mockk { + every { getPackageInfo("com.plugin.test", 0) } returns packageInfo + } every { mockedContext.packageName } returns "com.plugin.test" every { mockedContext.packageManager } returns mockedPackageManager } @@ -82,444 +86,469 @@ class AndroidLifecyclePluginTest { every { anyConstructed().mostRecentLocation } returns null every { anyConstructed().appSetId } returns "" - configuration = Configuration( - apiKey = "api-key", - context = mockedContext, - storageProvider = InMemoryStorageProvider(), - loggerProvider = ConsoleLoggerProvider(), - identifyInterceptStorageProvider = InMemoryStorageProvider(), - identityStorageProvider = IMIdentityStorageProvider(), - trackingSessionEvents = false, - ) + connectivityManager = mockk(relaxed = true) + every { mockedContext!!.getSystemService(Context.CONNECTIVITY_SERVICE) } returns connectivityManager + + configuration = + Configuration( + apiKey = "api-key", + context = mockedContext, + storageProvider = InMemoryStorageProvider(), + loggerProvider = ConsoleLoggerProvider(), + identifyInterceptStorageProvider = InMemoryStorageProvider(), + identityStorageProvider = IMIdentityStorageProvider(), + trackingSessionEvents = false, + ) amplitude = Amplitude(configuration) } @Test - fun `test application installed event is tracked`() = runTest { - setDispatcher(testScheduler) - configuration.defaultTracking.appLifecycles = true - amplitude.add(androidLifecyclePlugin) - - val mockedPlugin = spyk(StubPlugin()) - amplitude.add(mockedPlugin) - amplitude.isBuilt.await() - - val mockedActivity = mockk() - val mockedBundle = mockk() - androidLifecyclePlugin.onActivityCreated(mockedActivity, mockedBundle) - - advanceUntilIdle() - Thread.sleep(100) - - val tracks = mutableListOf() - verify { mockedPlugin.track(capture(tracks)) } - Assertions.assertEquals(1, tracks.count()) - - with(tracks[0]) { - Assertions.assertEquals(DefaultEventUtils.EventTypes.APPLICATION_INSTALLED, eventType) - Assertions.assertEquals(eventProperties?.get(DefaultEventUtils.EventProperties.BUILD), "66") - Assertions.assertEquals(eventProperties?.get(DefaultEventUtils.EventProperties.VERSION), "6.0.0") + fun `test application installed event is tracked`() = + runTest { + setDispatcher(testScheduler) + configuration.defaultTracking.appLifecycles = true + amplitude.add(androidLifecyclePlugin) + + val mockedPlugin = spyk(StubPlugin()) + amplitude.add(mockedPlugin) + amplitude.isBuilt.await() + + val mockedActivity = mockk() + val mockedBundle = mockk() + androidLifecyclePlugin.onActivityCreated(mockedActivity, mockedBundle) + + advanceUntilIdle() + Thread.sleep(100) + + val tracks = mutableListOf() + verify { mockedPlugin.track(capture(tracks)) } + Assertions.assertEquals(1, tracks.count()) + + with(tracks[0]) { + Assertions.assertEquals(DefaultEventUtils.EventTypes.APPLICATION_INSTALLED, eventType) + Assertions.assertEquals(eventProperties?.get(DefaultEventUtils.EventProperties.BUILD), "66") + Assertions.assertEquals(eventProperties?.get(DefaultEventUtils.EventProperties.VERSION), "6.0.0") + } } - } @Test - fun `test application installed event is not tracked when disabled`() = runTest { - setDispatcher(testScheduler) - configuration.defaultTracking.appLifecycles = false - amplitude.add(androidLifecyclePlugin) - - val mockedPlugin = spyk(StubPlugin()) - amplitude.add(mockedPlugin) - amplitude.isBuilt.await() - - val mockedActivity = mockk() - val mockedBundle = mockk() - androidLifecyclePlugin.onActivityCreated(mockedActivity, mockedBundle) - - advanceUntilIdle() - Thread.sleep(100) - - val tracks = mutableListOf() - verify(exactly = 0) { mockedPlugin.track(capture(tracks)) } - Assertions.assertEquals(0, tracks.count()) - } + fun `test application installed event is not tracked when disabled`() = + runTest { + setDispatcher(testScheduler) + configuration.defaultTracking.appLifecycles = false + amplitude.add(androidLifecyclePlugin) + + val mockedPlugin = spyk(StubPlugin()) + amplitude.add(mockedPlugin) + amplitude.isBuilt.await() + + val mockedActivity = mockk() + val mockedBundle = mockk() + androidLifecyclePlugin.onActivityCreated(mockedActivity, mockedBundle) + + advanceUntilIdle() + Thread.sleep(100) + + val tracks = mutableListOf() + verify(exactly = 0) { mockedPlugin.track(capture(tracks)) } + Assertions.assertEquals(0, tracks.count()) + } @Test - fun `test application updated event is tracked`() = runTest { - setDispatcher(testScheduler) - configuration.defaultTracking.appLifecycles = true - amplitude.add(androidLifecyclePlugin) - - val mockedPlugin = spyk(StubPlugin()) - amplitude.add(mockedPlugin) - amplitude.isBuilt.await() - - // Stored previous version/build - amplitude.storage.write(Storage.Constants.APP_BUILD, "55") - amplitude.storage.write(Storage.Constants.APP_VERSION, "5.0.0") - - val mockedActivity = mockk() - val mockedBundle = mockk() - androidLifecyclePlugin.onActivityCreated(mockedActivity, mockedBundle) - - advanceUntilIdle() - Thread.sleep(100) - - val tracks = mutableListOf() - verify { mockedPlugin.track(capture(tracks)) } - Assertions.assertEquals(1, tracks.count()) - - with(tracks[0]) { - Assertions.assertEquals(DefaultEventUtils.EventTypes.APPLICATION_UPDATED, eventType) - Assertions.assertEquals(eventProperties?.get(DefaultEventUtils.EventProperties.BUILD), "66") - Assertions.assertEquals(eventProperties?.get(DefaultEventUtils.EventProperties.VERSION), "6.0.0") - Assertions.assertEquals(eventProperties?.get(DefaultEventUtils.EventProperties.PREVIOUS_BUILD), "55") - Assertions.assertEquals(eventProperties?.get(DefaultEventUtils.EventProperties.PREVIOUS_VERSION), "5.0.0") + fun `test application updated event is tracked`() = + runTest { + setDispatcher(testScheduler) + configuration.defaultTracking.appLifecycles = true + amplitude.add(androidLifecyclePlugin) + + val mockedPlugin = spyk(StubPlugin()) + amplitude.add(mockedPlugin) + amplitude.isBuilt.await() + + // Stored previous version/build + amplitude.storage.write(Storage.Constants.APP_BUILD, "55") + amplitude.storage.write(Storage.Constants.APP_VERSION, "5.0.0") + + val mockedActivity = mockk() + val mockedBundle = mockk() + androidLifecyclePlugin.onActivityCreated(mockedActivity, mockedBundle) + + advanceUntilIdle() + Thread.sleep(100) + + val tracks = mutableListOf() + verify { mockedPlugin.track(capture(tracks)) } + Assertions.assertEquals(1, tracks.count()) + + with(tracks[0]) { + Assertions.assertEquals(DefaultEventUtils.EventTypes.APPLICATION_UPDATED, eventType) + Assertions.assertEquals(eventProperties?.get(DefaultEventUtils.EventProperties.BUILD), "66") + Assertions.assertEquals(eventProperties?.get(DefaultEventUtils.EventProperties.VERSION), "6.0.0") + Assertions.assertEquals(eventProperties?.get(DefaultEventUtils.EventProperties.PREVIOUS_BUILD), "55") + Assertions.assertEquals(eventProperties?.get(DefaultEventUtils.EventProperties.PREVIOUS_VERSION), "5.0.0") + } } - } @Test - fun `test application updated event is not tracked when disabled`() = runTest { - setDispatcher(testScheduler) - configuration.defaultTracking.appLifecycles = false - amplitude.add(androidLifecyclePlugin) - - val mockedPlugin = spyk(StubPlugin()) - amplitude.add(mockedPlugin) - amplitude.isBuilt.await() - - // Stored previous version/build - amplitude.storage.write(Storage.Constants.APP_BUILD, "55") - amplitude.storage.write(Storage.Constants.APP_VERSION, "5.0.0") - - val mockedActivity = mockk() - val mockedBundle = mockk() - androidLifecyclePlugin.onActivityCreated(mockedActivity, mockedBundle) - - advanceUntilIdle() - Thread.sleep(100) - - val tracks = mutableListOf() - verify(exactly = 0) { mockedPlugin.track(capture(tracks)) } - Assertions.assertEquals(0, tracks.count()) - } + fun `test application updated event is not tracked when disabled`() = + runTest { + setDispatcher(testScheduler) + configuration.defaultTracking.appLifecycles = false + amplitude.add(androidLifecyclePlugin) + + val mockedPlugin = spyk(StubPlugin()) + amplitude.add(mockedPlugin) + amplitude.isBuilt.await() + + // Stored previous version/build + amplitude.storage.write(Storage.Constants.APP_BUILD, "55") + amplitude.storage.write(Storage.Constants.APP_VERSION, "5.0.0") + + val mockedActivity = mockk() + val mockedBundle = mockk() + androidLifecyclePlugin.onActivityCreated(mockedActivity, mockedBundle) + + advanceUntilIdle() + Thread.sleep(100) + + val tracks = mutableListOf() + verify(exactly = 0) { mockedPlugin.track(capture(tracks)) } + Assertions.assertEquals(0, tracks.count()) + } @Test - fun `test application opened event is tracked`() = runTest { - setDispatcher(testScheduler) - configuration.defaultTracking.appLifecycles = true - amplitude.add(androidLifecyclePlugin) - - val mockedPlugin = spyk(StubPlugin()) - amplitude.add(mockedPlugin) - amplitude.isBuilt.await() - - val mockedActivity = mockk() - val mockedBundle = mockk() - androidLifecyclePlugin.onActivityCreated(mockedActivity, mockedBundle) - androidLifecyclePlugin.onActivityStarted(mockedActivity) - androidLifecyclePlugin.onActivityResumed(mockedActivity) - - advanceUntilIdle() - Thread.sleep(100) - - val tracks = mutableListOf() - verify { mockedPlugin.track(capture(tracks)) } - Assertions.assertEquals(2, tracks.count()) - - with(tracks[0]) { - Assertions.assertEquals(DefaultEventUtils.EventTypes.APPLICATION_INSTALLED, eventType) - } - with(tracks[1]) { - Assertions.assertEquals(DefaultEventUtils.EventTypes.APPLICATION_OPENED, eventType) - Assertions.assertEquals(eventProperties?.get(DefaultEventUtils.EventProperties.BUILD), "66") - Assertions.assertEquals(eventProperties?.get(DefaultEventUtils.EventProperties.VERSION), "6.0.0") - Assertions.assertEquals(eventProperties?.get(DefaultEventUtils.EventProperties.FROM_BACKGROUND), false) + fun `test application opened event is tracked`() = + runTest { + setDispatcher(testScheduler) + configuration.defaultTracking.appLifecycles = true + amplitude.add(androidLifecyclePlugin) + + val mockedPlugin = spyk(StubPlugin()) + amplitude.add(mockedPlugin) + amplitude.isBuilt.await() + + val mockedActivity = mockk() + val mockedBundle = mockk() + androidLifecyclePlugin.onActivityCreated(mockedActivity, mockedBundle) + androidLifecyclePlugin.onActivityStarted(mockedActivity) + androidLifecyclePlugin.onActivityResumed(mockedActivity) + + advanceUntilIdle() + Thread.sleep(100) + + val tracks = mutableListOf() + verify { mockedPlugin.track(capture(tracks)) } + Assertions.assertEquals(2, tracks.count()) + + with(tracks[0]) { + Assertions.assertEquals(DefaultEventUtils.EventTypes.APPLICATION_INSTALLED, eventType) + } + with(tracks[1]) { + Assertions.assertEquals(DefaultEventUtils.EventTypes.APPLICATION_OPENED, eventType) + Assertions.assertEquals(eventProperties?.get(DefaultEventUtils.EventProperties.BUILD), "66") + Assertions.assertEquals(eventProperties?.get(DefaultEventUtils.EventProperties.VERSION), "6.0.0") + Assertions.assertEquals(eventProperties?.get(DefaultEventUtils.EventProperties.FROM_BACKGROUND), false) + } } - } @Test - fun `test application opened event is not tracked when disabled`() = runTest { - setDispatcher(testScheduler) - configuration.defaultTracking.appLifecycles = false - amplitude.add(androidLifecyclePlugin) - - val mockedPlugin = spyk(StubPlugin()) - amplitude.add(mockedPlugin) - amplitude.isBuilt.await() - - val mockedActivity = mockk() - val mockedBundle = mockk() - androidLifecyclePlugin.onActivityCreated(mockedActivity, mockedBundle) - androidLifecyclePlugin.onActivityStarted(mockedActivity) - androidLifecyclePlugin.onActivityResumed(mockedActivity) - - advanceUntilIdle() - Thread.sleep(100) - - val tracks = mutableListOf() - verify(exactly = 0) { mockedPlugin.track(capture(tracks)) } - Assertions.assertEquals(0, tracks.count()) - } + fun `test application opened event is not tracked when disabled`() = + runTest { + setDispatcher(testScheduler) + configuration.defaultTracking.appLifecycles = false + amplitude.add(androidLifecyclePlugin) + + val mockedPlugin = spyk(StubPlugin()) + amplitude.add(mockedPlugin) + amplitude.isBuilt.await() + + val mockedActivity = mockk() + val mockedBundle = mockk() + androidLifecyclePlugin.onActivityCreated(mockedActivity, mockedBundle) + androidLifecyclePlugin.onActivityStarted(mockedActivity) + androidLifecyclePlugin.onActivityResumed(mockedActivity) + + advanceUntilIdle() + Thread.sleep(100) + + val tracks = mutableListOf() + verify(exactly = 0) { mockedPlugin.track(capture(tracks)) } + Assertions.assertEquals(0, tracks.count()) + } @Test - fun `test application backgrounded event is tracked`() = runTest { - setDispatcher(testScheduler) - configuration.defaultTracking.appLifecycles = true - amplitude.add(androidLifecyclePlugin) - - val mockedPlugin = spyk(StubPlugin()) - amplitude.add(mockedPlugin) - amplitude.isBuilt.await() - - val mockedActivity = mockk() - androidLifecyclePlugin.onActivityPaused(mockedActivity) - androidLifecyclePlugin.onActivityStopped(mockedActivity) - androidLifecyclePlugin.onActivityDestroyed(mockedActivity) - - advanceUntilIdle() - Thread.sleep(100) - - val tracks = mutableListOf() - verify { mockedPlugin.track(capture(tracks)) } - Assertions.assertEquals(1, tracks.count()) - - with(tracks[0]) { - Assertions.assertEquals(DefaultEventUtils.EventTypes.APPLICATION_BACKGROUNDED, eventType) + fun `test application backgrounded event is tracked`() = + runTest { + setDispatcher(testScheduler) + configuration.defaultTracking.appLifecycles = true + amplitude.add(androidLifecyclePlugin) + + val mockedPlugin = spyk(StubPlugin()) + amplitude.add(mockedPlugin) + amplitude.isBuilt.await() + + val mockedActivity = mockk() + androidLifecyclePlugin.onActivityPaused(mockedActivity) + androidLifecyclePlugin.onActivityStopped(mockedActivity) + androidLifecyclePlugin.onActivityDestroyed(mockedActivity) + + advanceUntilIdle() + Thread.sleep(100) + + val tracks = mutableListOf() + verify { mockedPlugin.track(capture(tracks)) } + Assertions.assertEquals(1, tracks.count()) + + with(tracks[0]) { + Assertions.assertEquals(DefaultEventUtils.EventTypes.APPLICATION_BACKGROUNDED, eventType) + } } - } @Test - fun `test application backgrounded event is not tracked when disabled`() = runTest { - setDispatcher(testScheduler) - (amplitude.configuration as Configuration).defaultTracking.appLifecycles = false - amplitude.add(androidLifecyclePlugin) - - val mockedPlugin = spyk(StubPlugin()) - amplitude.add(mockedPlugin) - amplitude.isBuilt.await() - - val mockedActivity = mockk() - androidLifecyclePlugin.onActivityPaused(mockedActivity) - androidLifecyclePlugin.onActivityStopped(mockedActivity) - androidLifecyclePlugin.onActivityDestroyed(mockedActivity) - - advanceUntilIdle() - Thread.sleep(100) - - val tracks = mutableListOf() - verify(exactly = 0) { mockedPlugin.track(capture(tracks)) } - Assertions.assertEquals(0, tracks.count()) - } + fun `test application backgrounded event is not tracked when disabled`() = + runTest { + setDispatcher(testScheduler) + (amplitude.configuration as Configuration).defaultTracking.appLifecycles = false + amplitude.add(androidLifecyclePlugin) + + val mockedPlugin = spyk(StubPlugin()) + amplitude.add(mockedPlugin) + amplitude.isBuilt.await() + + val mockedActivity = mockk() + androidLifecyclePlugin.onActivityPaused(mockedActivity) + androidLifecyclePlugin.onActivityStopped(mockedActivity) + androidLifecyclePlugin.onActivityDestroyed(mockedActivity) + + advanceUntilIdle() + Thread.sleep(100) + + val tracks = mutableListOf() + verify(exactly = 0) { mockedPlugin.track(capture(tracks)) } + Assertions.assertEquals(0, tracks.count()) + } @Test - fun `test screen viewed event is tracked`() = runTest { - setDispatcher(testScheduler) - configuration.defaultTracking.screenViews = true - amplitude.add(androidLifecyclePlugin) - - val mockedPlugin = spyk(StubPlugin()) - amplitude.add(mockedPlugin) - amplitude.isBuilt.await() - - val mockedActivity = mockk() - every { mockedActivity.packageManager } returns mockedPackageManager - every { mockedActivity.componentName } returns mockk() - val mockedActivityInfo = mockk() - every { mockedPackageManager.getActivityInfo(any(), any()) } returns mockedActivityInfo - every { mockedActivityInfo.loadLabel(mockedPackageManager) } returns "test-label" - val mockedBundle = mockk() - androidLifecyclePlugin.onActivityCreated(mockedActivity, mockedBundle) - androidLifecyclePlugin.onActivityStarted(mockedActivity) - - advanceUntilIdle() - Thread.sleep(100) - - val tracks = mutableListOf() - verify { mockedPlugin.track(capture(tracks)) } - Assertions.assertEquals(1, tracks.count()) - - with(tracks[0]) { - Assertions.assertEquals(DefaultEventUtils.EventTypes.SCREEN_VIEWED, eventType) - Assertions.assertEquals(eventProperties?.get(DefaultEventUtils.EventProperties.SCREEN_NAME), "test-label") + fun `test screen viewed event is tracked`() = + runTest { + setDispatcher(testScheduler) + configuration.defaultTracking.screenViews = true + amplitude.add(androidLifecyclePlugin) + + val mockedPlugin = spyk(StubPlugin()) + amplitude.add(mockedPlugin) + amplitude.isBuilt.await() + + val mockedActivity = mockk() + every { mockedActivity.packageManager } returns mockedPackageManager + every { mockedActivity.componentName } returns mockk() + val mockedActivityInfo = mockk() + every { mockedPackageManager.getActivityInfo(any(), any()) } returns mockedActivityInfo + every { mockedActivityInfo.loadLabel(mockedPackageManager) } returns "test-label" + val mockedBundle = mockk() + androidLifecyclePlugin.onActivityCreated(mockedActivity, mockedBundle) + androidLifecyclePlugin.onActivityStarted(mockedActivity) + + advanceUntilIdle() + Thread.sleep(100) + + val tracks = mutableListOf() + verify { mockedPlugin.track(capture(tracks)) } + Assertions.assertEquals(1, tracks.count()) + + with(tracks[0]) { + Assertions.assertEquals(DefaultEventUtils.EventTypes.SCREEN_VIEWED, eventType) + Assertions.assertEquals(eventProperties?.get(DefaultEventUtils.EventProperties.SCREEN_NAME), "test-label") + } } - } @Test - fun `test screen viewed event is not tracked when disabled`() = runTest { - setDispatcher(testScheduler) - configuration.defaultTracking.screenViews = false - amplitude.add(androidLifecyclePlugin) - - val mockedPlugin = spyk(StubPlugin()) - amplitude.add(mockedPlugin) - amplitude.isBuilt.await() - - val mockedActivity = mockk() - every { mockedActivity.packageManager } returns mockedPackageManager - every { mockedActivity.componentName } returns mockk() - val mockedActivityInfo = mockk() - every { mockedPackageManager.getActivityInfo(any(), any()) } returns mockedActivityInfo - every { mockedActivityInfo.loadLabel(mockedPackageManager) } returns "test-label" - val mockedBundle = mockk() - androidLifecyclePlugin.onActivityCreated(mockedActivity, mockedBundle) - androidLifecyclePlugin.onActivityStarted(mockedActivity) - - advanceUntilIdle() - Thread.sleep(100) - - val tracks = mutableListOf() - verify(exactly = 0) { mockedPlugin.track(capture(tracks)) } - Assertions.assertEquals(0, tracks.count()) - } + fun `test screen viewed event is not tracked when disabled`() = + runTest { + setDispatcher(testScheduler) + configuration.defaultTracking.screenViews = false + amplitude.add(androidLifecyclePlugin) + + val mockedPlugin = spyk(StubPlugin()) + amplitude.add(mockedPlugin) + amplitude.isBuilt.await() + + val mockedActivity = mockk() + every { mockedActivity.packageManager } returns mockedPackageManager + every { mockedActivity.componentName } returns mockk() + val mockedActivityInfo = mockk() + every { mockedPackageManager.getActivityInfo(any(), any()) } returns mockedActivityInfo + every { mockedActivityInfo.loadLabel(mockedPackageManager) } returns "test-label" + val mockedBundle = mockk() + androidLifecyclePlugin.onActivityCreated(mockedActivity, mockedBundle) + androidLifecyclePlugin.onActivityStarted(mockedActivity) + + advanceUntilIdle() + Thread.sleep(100) + + val tracks = mutableListOf() + verify(exactly = 0) { mockedPlugin.track(capture(tracks)) } + Assertions.assertEquals(0, tracks.count()) + } @Test - fun `test deep link opened event is tracked`() = runTest { - setDispatcher(testScheduler) - configuration.defaultTracking.deepLinks = true - amplitude.add(androidLifecyclePlugin) - - val mockedPlugin = spyk(StubPlugin()) - amplitude.add(mockedPlugin) - amplitude.isBuilt.await() - - val mockedIntent = mockk() - every { mockedIntent.data } returns Uri.parse("app://url.com/open") - val mockedActivity = mockk() - every { mockedActivity.intent } returns mockedIntent - every { mockedActivity.referrer } returns Uri.parse("android-app://com.android.chrome") - val mockedBundle = mockk() - androidLifecyclePlugin.onActivityCreated(mockedActivity, mockedBundle) - - advanceUntilIdle() - Thread.sleep(100) - - val tracks = mutableListOf() - verify { mockedPlugin.track(capture(tracks)) } - Assertions.assertEquals(1, tracks.count()) - - with(tracks[0]) { - Assertions.assertEquals(DefaultEventUtils.EventTypes.DEEP_LINK_OPENED, eventType) - Assertions.assertEquals(eventProperties?.get(DefaultEventUtils.EventProperties.LINK_URL), "app://url.com/open") - Assertions.assertEquals(eventProperties?.get(DefaultEventUtils.EventProperties.LINK_REFERRER), "android-app://com.android.chrome") + fun `test deep link opened event is tracked`() = + runTest { + setDispatcher(testScheduler) + configuration.defaultTracking.deepLinks = true + amplitude.add(androidLifecyclePlugin) + + val mockedPlugin = spyk(StubPlugin()) + amplitude.add(mockedPlugin) + amplitude.isBuilt.await() + + val mockedIntent = mockk() + every { mockedIntent.data } returns Uri.parse("app://url.com/open") + val mockedActivity = mockk() + every { mockedActivity.intent } returns mockedIntent + every { mockedActivity.referrer } returns Uri.parse("android-app://com.android.chrome") + val mockedBundle = mockk() + androidLifecyclePlugin.onActivityCreated(mockedActivity, mockedBundle) + + advanceUntilIdle() + Thread.sleep(100) + + val tracks = mutableListOf() + verify { mockedPlugin.track(capture(tracks)) } + Assertions.assertEquals(1, tracks.count()) + + with(tracks[0]) { + Assertions.assertEquals(DefaultEventUtils.EventTypes.DEEP_LINK_OPENED, eventType) + Assertions.assertEquals(eventProperties?.get(DefaultEventUtils.EventProperties.LINK_URL), "app://url.com/open") + Assertions.assertEquals( + eventProperties?.get(DefaultEventUtils.EventProperties.LINK_REFERRER), + "android-app://com.android.chrome", + ) + } } - } @Config(sdk = [21]) @Test - fun `test deep link opened event is tracked when using sdk is between 17 and 21`() = runTest { - setDispatcher(testScheduler) - configuration.defaultTracking.deepLinks = true - amplitude.add(androidLifecyclePlugin) - - val mockedPlugin = spyk(StubPlugin()) - amplitude.add(mockedPlugin) - amplitude.isBuilt.await() - - val mockedIntent = mockk() - every { mockedIntent.data } returns Uri.parse("app://url.com/open") - every { mockedIntent.getParcelableExtra(any()) } returns Uri.parse("android-app://com.android.chrome") - val mockedActivity = mockk() - every { mockedActivity.intent } returns mockedIntent - val mockedBundle = mockk() - androidLifecyclePlugin.onActivityCreated(mockedActivity, mockedBundle) - - advanceUntilIdle() - Thread.sleep(100) - - val tracks = mutableListOf() - verify { mockedPlugin.track(capture(tracks)) } - Assertions.assertEquals(1, tracks.count()) - - with(tracks[0]) { - Assertions.assertEquals(DefaultEventUtils.EventTypes.DEEP_LINK_OPENED, eventType) - Assertions.assertEquals(eventProperties?.get(DefaultEventUtils.EventProperties.LINK_URL), "app://url.com/open") - Assertions.assertEquals(eventProperties?.get(DefaultEventUtils.EventProperties.LINK_REFERRER), "android-app://com.android.chrome") + fun `test deep link opened event is tracked when using sdk is between 17 and 21`() = + runTest { + setDispatcher(testScheduler) + configuration.defaultTracking.deepLinks = true + amplitude.add(androidLifecyclePlugin) + + val mockedPlugin = spyk(StubPlugin()) + amplitude.add(mockedPlugin) + amplitude.isBuilt.await() + + val mockedIntent = mockk() + every { mockedIntent.data } returns Uri.parse("app://url.com/open") + every { mockedIntent.getParcelableExtra(any()) } returns Uri.parse("android-app://com.android.chrome") + val mockedActivity = mockk() + every { mockedActivity.intent } returns mockedIntent + val mockedBundle = mockk() + androidLifecyclePlugin.onActivityCreated(mockedActivity, mockedBundle) + + advanceUntilIdle() + Thread.sleep(100) + + val tracks = mutableListOf() + verify { mockedPlugin.track(capture(tracks)) } + Assertions.assertEquals(1, tracks.count()) + + with(tracks[0]) { + Assertions.assertEquals(DefaultEventUtils.EventTypes.DEEP_LINK_OPENED, eventType) + Assertions.assertEquals(eventProperties?.get(DefaultEventUtils.EventProperties.LINK_URL), "app://url.com/open") + Assertions.assertEquals( + eventProperties?.get(DefaultEventUtils.EventProperties.LINK_REFERRER), + "android-app://com.android.chrome", + ) + } } - } @Config(sdk = [16]) @Test - fun `test deep link opened event is tracked when using sdk is lower than 17`() = runTest { - setDispatcher(testScheduler) - configuration.defaultTracking.deepLinks = true - amplitude.add(androidLifecyclePlugin) - - val mockedPlugin = spyk(StubPlugin()) - amplitude.add(mockedPlugin) - amplitude.isBuilt.await() - - val mockedIntent = mockk() - every { mockedIntent.data } returns Uri.parse("app://url.com/open") - every { mockedIntent.getParcelableExtra(any()) } returns Uri.parse("android-app://com.android.chrome") - val mockedActivity = mockk() - every { mockedActivity.intent } returns mockedIntent - val mockedBundle = mockk() - androidLifecyclePlugin.onActivityCreated(mockedActivity, mockedBundle) - - advanceUntilIdle() - Thread.sleep(100) - - val tracks = mutableListOf() - verify { mockedPlugin.track(capture(tracks)) } - Assertions.assertEquals(1, tracks.count()) - - with(tracks[0]) { - Assertions.assertEquals(DefaultEventUtils.EventTypes.DEEP_LINK_OPENED, eventType) - Assertions.assertEquals(eventProperties?.get(DefaultEventUtils.EventProperties.LINK_URL), "app://url.com/open") - Assertions.assertEquals(eventProperties?.get(DefaultEventUtils.EventProperties.LINK_REFERRER), null) + fun `test deep link opened event is tracked when using sdk is lower than 17`() = + runTest { + setDispatcher(testScheduler) + configuration.defaultTracking.deepLinks = true + amplitude.add(androidLifecyclePlugin) + + val mockedPlugin = spyk(StubPlugin()) + amplitude.add(mockedPlugin) + amplitude.isBuilt.await() + + val mockedIntent = mockk() + every { mockedIntent.data } returns Uri.parse("app://url.com/open") + every { mockedIntent.getParcelableExtra(any()) } returns Uri.parse("android-app://com.android.chrome") + val mockedActivity = mockk() + every { mockedActivity.intent } returns mockedIntent + val mockedBundle = mockk() + androidLifecyclePlugin.onActivityCreated(mockedActivity, mockedBundle) + + advanceUntilIdle() + Thread.sleep(100) + + val tracks = mutableListOf() + verify { mockedPlugin.track(capture(tracks)) } + Assertions.assertEquals(1, tracks.count()) + + with(tracks[0]) { + Assertions.assertEquals(DefaultEventUtils.EventTypes.DEEP_LINK_OPENED, eventType) + Assertions.assertEquals(eventProperties?.get(DefaultEventUtils.EventProperties.LINK_URL), "app://url.com/open") + Assertions.assertEquals(eventProperties?.get(DefaultEventUtils.EventProperties.LINK_REFERRER), null) + } } - } @Test - fun `test deep link opened event is not tracked when disabled`() = runTest { - setDispatcher(testScheduler) - configuration.defaultTracking.deepLinks = false - amplitude.add(androidLifecyclePlugin) - - val mockedPlugin = spyk(StubPlugin()) - amplitude.add(mockedPlugin) - amplitude.isBuilt.await() - - val mockedIntent = mockk() - every { mockedIntent.data } returns Uri.parse("app://url.com/open") - val mockedActivity = mockk() - every { mockedActivity.intent } returns mockedIntent - every { mockedActivity.referrer } returns Uri.parse("android-app://com.android.chrome") - val mockedBundle = mockk() - androidLifecyclePlugin.onActivityCreated(mockedActivity, mockedBundle) - - advanceUntilIdle() - Thread.sleep(100) - - val tracks = mutableListOf() - verify(exactly = 0) { mockedPlugin.track(capture(tracks)) } - Assertions.assertEquals(0, tracks.count()) - } + fun `test deep link opened event is not tracked when disabled`() = + runTest { + setDispatcher(testScheduler) + configuration.defaultTracking.deepLinks = false + amplitude.add(androidLifecyclePlugin) + + val mockedPlugin = spyk(StubPlugin()) + amplitude.add(mockedPlugin) + amplitude.isBuilt.await() + + val mockedIntent = mockk() + every { mockedIntent.data } returns Uri.parse("app://url.com/open") + val mockedActivity = mockk() + every { mockedActivity.intent } returns mockedIntent + every { mockedActivity.referrer } returns Uri.parse("android-app://com.android.chrome") + val mockedBundle = mockk() + androidLifecyclePlugin.onActivityCreated(mockedActivity, mockedBundle) + + advanceUntilIdle() + Thread.sleep(100) + + val tracks = mutableListOf() + verify(exactly = 0) { mockedPlugin.track(capture(tracks)) } + Assertions.assertEquals(0, tracks.count()) + } @Test - fun `test deep link opened event is not tracked when URL is missing`() = runTest { - setDispatcher(testScheduler) - configuration.defaultTracking.deepLinks = true - amplitude.add(androidLifecyclePlugin) - - val mockedPlugin = spyk(StubPlugin()) - amplitude.add(mockedPlugin) - amplitude.isBuilt.await() - - val mockedIntent = mockk() - every { mockedIntent.data } returns null - val mockedActivity = mockk() - every { mockedActivity.intent } returns mockedIntent - every { mockedActivity.referrer } returns Uri.parse("android-app://com.android.unit-test") - val mockedBundle = mockk() - androidLifecyclePlugin.onActivityCreated(mockedActivity, mockedBundle) - - advanceUntilIdle() - Thread.sleep(100) - - val tracks = mutableListOf() - verify(exactly = 0) { mockedPlugin.track(capture(tracks)) } - Assertions.assertEquals(0, tracks.count()) - } + fun `test deep link opened event is not tracked when URL is missing`() = + runTest { + setDispatcher(testScheduler) + configuration.defaultTracking.deepLinks = true + amplitude.add(androidLifecyclePlugin) + + val mockedPlugin = spyk(StubPlugin()) + amplitude.add(mockedPlugin) + amplitude.isBuilt.await() + + val mockedIntent = mockk() + every { mockedIntent.data } returns null + val mockedActivity = mockk() + every { mockedActivity.intent } returns mockedIntent + every { mockedActivity.referrer } returns Uri.parse("android-app://com.android.unit-test") + val mockedBundle = mockk() + androidLifecyclePlugin.onActivityCreated(mockedActivity, mockedBundle) + + advanceUntilIdle() + Thread.sleep(100) + + val tracks = mutableListOf() + verify(exactly = 0) { mockedPlugin.track(capture(tracks)) } + Assertions.assertEquals(0, tracks.count()) + } } diff --git a/core/src/main/java/com/amplitude/core/platform/EventPipeline.kt b/core/src/main/java/com/amplitude/core/platform/EventPipeline.kt index 71d948d6..274727ac 100644 --- a/core/src/main/java/com/amplitude/core/platform/EventPipeline.kt +++ b/core/src/main/java/com/amplitude/core/platform/EventPipeline.kt @@ -14,8 +14,13 @@ import kotlinx.coroutines.withContext import java.io.FileNotFoundException import java.util.concurrent.atomic.AtomicInteger +interface NetworkConnectivityChecker { + suspend fun isConnected(): Boolean +} + class EventPipeline( - private val amplitude: Amplitude + private val amplitude: Amplitude, + private val networkConnectivityChecker: NetworkConnectivityChecker? = null ) { private val writeChannel: Channel @@ -99,6 +104,13 @@ class EventPipeline( } } + // Skip flush when offline only if + // network connectivity is not null + // and network is not connected. + if (networkConnectivityChecker?.isConnected() == false) { + continue + } + // if flush condition met, generate paths if (eventCount.incrementAndGet() >= getFlushCount() || triggerFlush) { eventCount.set(0) diff --git a/core/src/main/java/com/amplitude/core/platform/plugins/AmplitudeDestination.kt b/core/src/main/java/com/amplitude/core/platform/plugins/AmplitudeDestination.kt index b4c5d251..d2a6f186 100644 --- a/core/src/main/java/com/amplitude/core/platform/plugins/AmplitudeDestination.kt +++ b/core/src/main/java/com/amplitude/core/platform/plugins/AmplitudeDestination.kt @@ -7,10 +7,11 @@ import com.amplitude.core.events.IdentifyEvent import com.amplitude.core.events.RevenueEvent import com.amplitude.core.platform.DestinationPlugin import com.amplitude.core.platform.EventPipeline +import com.amplitude.core.platform.NetworkConnectivityChecker import com.amplitude.core.platform.intercept.IdentifyInterceptor import kotlinx.coroutines.launch -class AmplitudeDestination : DestinationPlugin() { +class AmplitudeDestination(private val networkConnectivityChecker: NetworkConnectivityChecker? = null) : DestinationPlugin() { private lateinit var pipeline: EventPipeline private lateinit var identifyInterceptor: IdentifyInterceptor @@ -66,7 +67,8 @@ class AmplitudeDestination : DestinationPlugin() { with(amplitude) { pipeline = EventPipeline( - amplitude + amplitude, + networkConnectivityChecker ) pipeline.start() identifyInterceptor = IdentifyInterceptor( diff --git a/core/src/test/kotlin/com/amplitude/core/platform/EventPipelineTest.kt b/core/src/test/kotlin/com/amplitude/core/platform/EventPipelineTest.kt new file mode 100644 index 00000000..683200a3 --- /dev/null +++ b/core/src/test/kotlin/com/amplitude/core/platform/EventPipelineTest.kt @@ -0,0 +1,74 @@ +package com.amplitude.core.platform + +import com.amplitude.core.Amplitude +import com.amplitude.core.Configuration +import com.amplitude.core.events.BaseEvent +import io.mockk.coEvery +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +@ExperimentalCoroutinesApi +class EventPipelineTest { + private lateinit var amplitude: Amplitude + private lateinit var networkConnectivityChecker: NetworkConnectivityChecker + private val config = Configuration(apiKey = "API_KEY", flushIntervalMillis = 5) + + @BeforeEach + fun setup() { + amplitude = Amplitude(config) + networkConnectivityChecker = mockk(relaxed = true) + } + + @Test + fun `should not flush when put and offline`() = + runBlocking { + coEvery { networkConnectivityChecker.isConnected() } returns false + val eventPipeline = spyk(EventPipeline(amplitude, networkConnectivityChecker)) + val event = BaseEvent().apply { eventType = "test_event" } + + eventPipeline.start() + eventPipeline.put(event) + delay(6) + + verify(exactly = 0) { eventPipeline.flush() } + } + + @Test + fun `should flush when put and online`() = + runBlocking { + coEvery { networkConnectivityChecker.isConnected() } returns true + val eventPipeline = spyk(EventPipeline(amplitude, networkConnectivityChecker)) + val event = BaseEvent().apply { eventType = "test_event" } + + eventPipeline.start() + eventPipeline.put(event) + delay(6) + + verify(exactly = 1) { eventPipeline.flush() } + } + + @Test + fun `should flush when back to online and an event is tracked`() = + runBlocking { + coEvery { networkConnectivityChecker.isConnected() } returns false + val eventPipeline = spyk(EventPipeline(amplitude, networkConnectivityChecker)) + val event1 = BaseEvent().apply { eventType = "test_event1" } + val event2 = BaseEvent().apply { eventType = "test_event2" } + + eventPipeline.start() + eventPipeline.put(event1) + delay(6) + + coEvery { networkConnectivityChecker.isConnected() } returns true + eventPipeline.put(event2) + delay(6) + + verify(exactly = 1) { eventPipeline.flush() } + } +}